summaryrefslogtreecommitdiff
path: root/ar/.config/mpv/scripts/slicing.lua
blob: 2653ce831417f6ff4b092e1c51c57e8b450f96b3 (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
-- 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 set_end()
    local end_time = mp.get_property_number("time-pos")
    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

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