aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArjun Satarkar <me@arjunsatarkar.net>2025-03-02 03:28:46 +0000
committerArjun Satarkar <me@arjunsatarkar.net>2025-03-02 03:28:46 +0000
commit37629a11f378a96d879c2487c406095ce9b46cd7 (patch)
tree7b25db555a8da9d611b71e7202f9b479c03b351f
parent7873c011a61ece80858dea0bbc7bc6f16251e908 (diff)
mpvclip: implement 2-pass mode!
-rw-r--r--README.adoc2
-rw-r--r--mpvclip/main.lua119
2 files changed, 97 insertions, 24 deletions
diff --git a/README.adoc b/README.adoc
index 8ff3533..d44dfc6 100644
--- a/README.adoc
+++ b/README.adoc
@@ -3,7 +3,7 @@
== Scripts
-* *mpvclip.* Clip sections of video with ffmpeg. Allows choosing CRF/two-pass target size and preset, with sensible defaults. As of 2025-03-01, requires recent mpv git master build.
+* *mpvclip.* Clip sections of video with ffmpeg. Allows choosing CRF/two-pass target size, with sensible defaults. As of 2025-03-01, requires recent mpv git master build.
* *get_subtitle.* Copy the text of the current subtitle line to the clipboard. Relies on `https://github.com/astrand/xclip[+xclip+`] to function.
== Copying
diff --git a/mpvclip/main.lua b/mpvclip/main.lua
index 88beb30..bfbc332 100644
--- a/mpvclip/main.lua
+++ b/mpvclip/main.lua
@@ -1,11 +1,11 @@
local input = require "mp.input"
-local function dump_arr(arr)
- local result = ""
- for _, v in ipairs(arr) do
- result = result .. string.format("%q", v) .. ", "
+local function log_cmd(args)
+ local cmd = ""
+ for _, v in ipairs(args) do
+ cmd = cmd .. string.format("%q", v) .. ", "
end
- return result
+ print("Running command: " .. cmd)
end
local function extend(arr1, arr2)
@@ -14,14 +14,27 @@ local function extend(arr1, arr2)
end
end
+local function copy_arr(arr)
+ local result = {}
+ for _, v in ipairs(arr) do
+ table.insert(result, v)
+ end
+ return result
+end
+
local function do_clip(a, b, crf, two_pass_target, sub_track_id, path)
+ local AUDIO_CODEC = "libopus"
+ local AUDIO_BITRATE_KIBIBITS = 128
+ local AUDIO_BITRATE_STR = tostring(AUDIO_BITRATE_KIBIBITS) .. "k"
+
local filterchain = nil
if sub_track_id then
filterchain = string.format("subtitles='%s':si=%d", path, sub_track_id - 1)
end
local out_path = string.format("clip_%d.mp4", os.time())
- local args = {
+
+ local base_args = {
"ffmpeg",
"-hide_banner",
"-loglevel", "warning",
@@ -29,28 +42,73 @@ local function do_clip(a, b, crf, two_pass_target, sub_track_id, path)
"-to", b,
"-copyts",
"-i", path,
+ "-c:v", "libx264",
"-ss", a,
"-to", b,
}
if filterchain then
- extend(args, { "-filter_complex", filterchain })
+ extend(base_args, { "-filter_complex", filterchain })
end
+
if crf then
- extend(args, { "-crf", crf })
- end
- extend(args, {
- "-pix_fmt", "yuv420p",
- "-movflags", "+faststart",
- "-c:a", "libopus",
- "-b:a", "128k",
- out_path
- })
+ local args = copy_arr(base_args)
+ extend(args, {
+ "-crf", crf,
+ "-pix_fmt", "yuv420p",
+ "-movflags", "+faststart",
+ "-c:a", AUDIO_CODEC,
+ "-b:a", AUDIO_BITRATE_STR,
+ out_path
+ })
- print(dump_arr(args))
+ log_cmd(args)
+ mp.command_native({ name = "subprocess", args = args })
+ elseif two_pass_target then
+ local clip_secs = (tonumber(b) - tonumber(a))
+ local total_bytes = two_pass_target * 1024 * 1024
+ local audio_bytes = AUDIO_BITRATE_KIBIBITS * (1024 / 8) * clip_secs
+ local video_bytes = total_bytes - audio_bytes
+ local video_bitrate = video_bytes / clip_secs * 8
- mp.command_native({ name = "subprocess", args = args })
- mp.osd_message("mpvclip: wrote clip to " .. out_path)
- print("Wrote clip to " .. out_path)
+ print(string.format("Clip audio will take up %d bytes, leaving %d for video", audio_bytes, video_bytes))
+ if video_bytes <= 0 then
+ local message = "Can't clip: not enough space for video"
+ mp.osd_message(message)
+ print(message)
+ return
+ end
+
+ local args = copy_arr(base_args)
+ extend(args, {
+ "-b:v", tostring(video_bitrate),
+ "-pass", "1",
+ "-an",
+ "-f", "null",
+ "-"
+ })
+
+ log_cmd(args)
+ mp.command_native({ name = "subprocess", args = args })
+
+ args = copy_arr(base_args)
+ extend(args, {
+ "-b:v", tostring(video_bitrate),
+ "-pass", "2",
+ "-c:a", AUDIO_CODEC,
+ "-b:a", AUDIO_BITRATE_STR,
+ out_path
+ })
+
+ log_cmd(args)
+ mp.command_native({ name = "subprocess", args = args })
+
+ os.remove("ffmpeg2pass-0.log")
+ os.remove("ffmpeg2pass-0.log.mbtree")
+ end
+
+ local message = "Wrote clip to " .. out_path
+ mp.osd_message(message)
+ print(message)
end
local function get_params()
@@ -58,7 +116,7 @@ local function get_params()
local b = mp.get_property("ab-loop-b")
if a == "no" or b == "no" then
- mp.osd_message("mpvclip: a-b loop not set; doing nothing")
+ mp.osd_message("Can't clip: a-b loop not set; doing nothing")
return
end
@@ -66,12 +124,13 @@ local function get_params()
local sub_track_id = mp.get_property_native("current-tracks/sub/id")
input.select({
- prompt = "mpvclip: select encoding mode",
+ prompt = "Select encoding mode",
items = { "CRF", "2-pass" },
default_item = 1,
keep_open = true,
submit = function(id)
if id == 1 then
+ -- CRF mode
input.get({
prompt = "Choose CRF (0-51):",
default_text = "23",
@@ -79,7 +138,21 @@ local function get_params()
if tonumber(crf) then
do_clip(a, b, crf, nil, sub_track_id, path)
else
- mp.osd_message("mpvclip: invalid CRF; doing nothing")
+ mp.osd_message("Invalid CRF; doing nothing")
+ end
+ end,
+ })
+ elseif id == 2 then
+ -- 2-pass mode
+ input.get({
+ prompt = "Choose target output size in mebibytes:",
+ default_text = "50",
+ submit = function(two_pass_target)
+ two_pass_target = tonumber(two_pass_target)
+ if two_pass_target then
+ do_clip(a, b, nil, two_pass_target, sub_track_id, path)
+ else
+ mp.osd_message("Invalid size, doing nothing")
end
end,
})