summaryrefslogtreecommitdiff
path: root/ar/.config/mpv/scripts/slicing.lua
blob: ed0ef40d9a9a2edc5ccf4884f02d6e605c431c79 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
-- slicing.lua — lossless video trimming via ffmpeg stream copy
--
-- Press the "set start" key once to mark A, then the "set end" key to mark B.
-- The marked [A, B] range is cut losslessly (-c copy) into a new file next to
-- the source, named "<name>_cut_<start>-<end>.<ext>". No re-encoding, so the
-- cut snaps to the nearest preceding keyframe (fast, but not frame-exact).
--
-- Default bindings (set in input.conf):
--   o   script-binding slicing/set-start
--   O   script-binding slicing/set-end

local mp = require("mp")
local msg = require("mp.msg")
local utils = require("mp.utils")

local start_time = nil

local function osd(text, dur)
	mp.osd_message(text, dur or 3)
	msg.info(text)
end

-- 12.345 -> "00:00:12.345" for filenames / logging
local function fmt(t)
	local h = math.floor(t / 3600)
	local m = math.floor((t % 3600) / 60)
	local s = t % 60
	return string.format("%02d:%02d:%06.3f", h, m, s)
end

-- whole-seconds token for filenames: 12.345 -> "12"
local function fmt_tag(t)
	return string.format("%d", math.floor(t + 0.5))
end

local function set_start()
	start_time = mp.get_property_number("time-pos")
	if not start_time then
		return
	end
	osd(("Cut start: %s"):format(fmt(start_time)))
end

local function do_cut(end_time)
	if not end_time then
		return
	end

	if not start_time then
		osd("Set the start point first (o)")
		return
	end
	if end_time <= start_time then
		osd("End point must be after the start point")
		return
	end

	local path = mp.get_property("path")
	if not path then
		osd("No file is playing")
		return
	end
	-- only local files can be stream-copied this way
	if path:find("^%a[%w+.-]*://") then
		osd("Cannot cut a stream (local files only)")
		return
	end

	local dir, name = utils.split_path(path)
	local stem, ext = name:match("^(.*)%.([^.]+)$")
	if not stem then
		stem, ext = name, "mkv"
	end

	local out =
		utils.join_path(dir, string.format("%s_cut_%s-%s.%s", stem, fmt_tag(start_time), fmt_tag(end_time), ext))

	local duration = end_time - start_time

	local args = {
		"ffmpeg",
		"-y",
		"-ss",
		string.format("%.3f", start_time),
		"-i",
		path,
		"-t",
		string.format("%.3f", duration),
		"-map",
		"0",
		"-c",
		"copy",
		"-avoid_negative_ts",
		"make_zero",
		out,
	}

	osd(("Cutting… %s → %s"):format(fmt(start_time), fmt(end_time)))
	msg.info("ffmpeg: " .. table.concat(args, " "))

	mp.command_native_async({
		name = "subprocess",
		args = args,
		playback_only = false,
		capture_stdout = true,
		capture_stderr = true,
	}, function(success, res, err)
		if success and res and res.status == 0 then
			local _, fname = utils.split_path(out)
			osd(("Saved: %s"):format(fname))
			start_time = nil
		else
			local detail = (res and res.stderr) or err or "unknown error"
			osd("Cut failed (check console log)")
			msg.error("ffmpeg failed: " .. tostring(detail))
		end
	end)
end

-- mark end at the current playback position
local function set_end()
	do_cut(mp.get_property_number("time-pos"))
end

-- mark end at the very end of the file, regardless of playback position,
-- so you don't have to catch the last frame before playback ends
local function set_end_eof()
	do_cut(mp.get_property_number("duration"))
end

mp.add_key_binding(nil, "set-start", set_start)
mp.add_key_binding(nil, "set-end", set_end)
mp.add_key_binding(nil, "set-end-eof", set_end_eof)