dotfiles

My personal shell configs and stuff
git clone git://git.alex.balgavy.eu/dotfiles.git
Log | Files | Refs | Submodules | README | LICENSE

encode.lua (10048B)


      1 local utils = require "mp.utils"
      2 local msg = require "mp.msg"
      3 local options = require "mp.options"
      4 
      5 local ON_WINDOWS = (package.config:sub(1,1) ~= "/")
      6 
      7 local start_timestamp = nil
      8 local profile_start = ""
      9 
     10 -- implementation detail of the osd message
     11 local timer = nil
     12 local timer_duration = 2
     13 
     14 function append_table(lhs, rhs)
     15     for i = 1,#rhs do
     16         lhs[#lhs+1] = rhs[i]
     17     end
     18     return lhs
     19 end
     20 
     21 function file_exists(name)
     22     local f = io.open(name, "r")
     23     if f ~= nil then
     24         io.close(f)
     25         return true
     26     else
     27         return false
     28     end
     29 end
     30 
     31 function get_extension(path)
     32     local candidate = string.match(path, "%.([^.]+)$")
     33     if candidate then
     34         for _, ext in ipairs({ "mkv", "webm", "mp4", "avi" }) do
     35             if candidate == ext then
     36                 return candidate
     37             end
     38         end
     39     end
     40     return "mkv"
     41 end
     42 
     43 function get_output_string(dir, format, input, extension, title, from, to, profile)
     44     local res = utils.readdir(dir)
     45     if not res then
     46         return nil
     47     end
     48     local files = {}
     49     for _, f in ipairs(res) do
     50         files[f] = true
     51     end
     52     local output = format
     53     output = string.gsub(output, "$f", input)
     54     output = string.gsub(output, "$t", title)
     55     output = string.gsub(output, "$s", seconds_to_time_string(from, true))
     56     output = string.gsub(output, "$e", seconds_to_time_string(to, true))
     57     output = string.gsub(output, "$d", seconds_to_time_string(to-from, true))
     58     output = string.gsub(output, "$x", extension)
     59     output = string.gsub(output, "$p", profile)
     60     if ON_WINDOWS then
     61         output = string.gsub(output, "[/\\|<>?:\"*]", "_")
     62     end
     63     if not string.find(output, "$n") then
     64         return files[output] and nil or output
     65     end
     66     local i = 1
     67     while true do
     68         local potential_name = string.gsub(output, "$n", tostring(i))
     69         if not files[potential_name] then
     70             return potential_name
     71         end
     72         i = i + 1
     73     end
     74 end
     75 
     76 function get_video_filters()
     77     local filters = {}
     78     for _, vf in ipairs(mp.get_property_native("vf")) do
     79         local name = vf["name"]
     80         local filter
     81         if name == "crop" then
     82             local p = vf["params"]
     83             filter = string.format("crop=%d:%d:%d:%d", p.w, p.h, p.x, p.y)
     84         elseif name == "mirror" then
     85             filter = "hflip"
     86         elseif name == "flip" then
     87             filter = "vflip"
     88         elseif name == "rotate" then
     89             local rotation = vf["params"]["angle"]
     90             -- rotate is NOT the filter we want here
     91             if rotation == "90" then
     92                 filter = "transpose=clock"
     93             elseif rotation == "180" then
     94                 filter = "transpose=clock,transpose=clock"
     95             elseif rotation == "270" then
     96                 filter = "transpose=cclock"
     97             end
     98         end
     99         filters[#filters + 1] = filter
    100     end
    101     return filters
    102 end
    103 
    104 function get_input_info(default_path, only_active)
    105     local accepted = {
    106         video = true,
    107         audio = not mp.get_property_bool("mute"),
    108         sub = mp.get_property_bool("sub-visibility")
    109     }
    110     local ret = {}
    111     for _, track in ipairs(mp.get_property_native("track-list")) do
    112         local track_path = track["external-filename"] or default_path
    113         if not only_active or (track["selected"] and accepted[track["type"]]) then
    114             local tracks = ret[track_path]
    115             if not tracks then
    116                 ret[track_path] = { track["ff-index"] }
    117             else
    118                 tracks[#tracks + 1] = track["ff-index"]
    119             end
    120         end
    121     end
    122     return ret
    123 end
    124 
    125 function seconds_to_time_string(seconds, full)
    126     local ret = string.format("%02d:%02d.%03d"
    127         , math.floor(seconds / 60) % 60
    128         , math.floor(seconds) % 60
    129         , seconds * 1000 % 1000
    130     )
    131     if full or seconds > 3600 then
    132         ret = string.format("%d:%s", math.floor(seconds / 3600), ret)
    133     end
    134     return ret
    135 end
    136 
    137 function start_encoding(from, to, settings)
    138     local args = {
    139         settings.ffmpeg_command,
    140         "-loglevel", "panic", "-hide_banner",
    141     }
    142     local append_args = function(table) args = append_table(args, table) end
    143 
    144     local path = mp.get_property("path")
    145     local is_stream = not file_exists(path)
    146     if is_stream then
    147         path = mp.get_property("stream-path")
    148     end
    149 
    150     local track_args = {}
    151     local start = seconds_to_time_string(from, false)
    152     local input_index = 0
    153     for input_path, tracks in pairs(get_input_info(path, settings.only_active_tracks)) do
    154        append_args({
    155             "-ss", start,
    156             "-i", input_path,
    157         })
    158         if settings.only_active_tracks then
    159             for _, track_index in ipairs(tracks) do
    160                 track_args = append_table(track_args, { "-map", string.format("%d:%d", input_index, track_index)})
    161             end
    162         else
    163             track_args = append_table(track_args, { "-map", tostring(input_index)})
    164         end
    165         input_index = input_index + 1
    166     end
    167 
    168     append_args({"-to", tostring(to-from)})
    169     append_args(track_args)
    170 
    171     -- apply some of the video filters currently in the chain
    172     local filters = {}
    173     if settings.preserve_filters then
    174         filters = get_video_filters()
    175     end
    176     if settings.append_filter ~= "" then
    177         filters[#filters + 1] = settings.append_filter
    178     end
    179     if #filters > 0 then
    180         append_args({ "-filter:v", table.concat(filters, ",") })
    181     end
    182 
    183     -- split the user-passed settings on whitespace
    184     for token in string.gmatch(settings.codec, "[^%s]+") do
    185         args[#args + 1] = token
    186     end
    187 
    188     -- path of the output
    189     local output_directory = settings.output_directory
    190     if output_directory == "" then
    191         if is_stream then
    192             output_directory = "."
    193         else
    194             output_directory, _ = utils.split_path(path)
    195         end
    196     else
    197         output_directory = string.gsub(output_directory, "^~", os.getenv("HOME") or "~")
    198     end
    199     local input_name = mp.get_property("filename/no-ext") or "encode"
    200     local title = mp.get_property("media-title")
    201     local extension = get_extension(path)
    202     local output_name = get_output_string(output_directory, settings.output_format, input_name, extension, title, from, to, settings.profile)
    203     if not output_name then
    204         mp.osd_message("Invalid path " .. output_directory)
    205         return
    206     end
    207     args[#args + 1] = utils.join_path(output_directory, output_name)
    208 
    209     if settings.print then
    210         local o = ""
    211         -- fuck this is ugly
    212         for i = 1, #args do
    213             local fmt = ""
    214             if i == 1 then
    215                 fmt = "%s%s"
    216             elseif i >= 2 and i <= 4 then
    217                 fmt = "%s"
    218             elseif args[i-1] == "-i" or i == #args or args[i-1] == "-filter:v" then
    219                 fmt = "%s '%s'"
    220             else
    221                 fmt = "%s %s"
    222             end
    223             o = string.format(fmt, o, args[i])
    224         end
    225         print(o)
    226     end
    227     if settings.detached then
    228         utils.subprocess_detached({ args = args })
    229     else
    230         local res = utils.subprocess({ args = args, max_size = 0, cancellable = false })
    231         if res.status == 0 then
    232             mp.osd_message("Finished encoding succesfully")
    233         else
    234             mp.osd_message("Failed to encode, check the log")
    235         end
    236     end
    237 end
    238 
    239 function clear_timestamp()
    240     timer:kill()
    241     start_timestamp = nil
    242     profile_start = ""
    243     mp.remove_key_binding("encode-ESC")
    244     mp.remove_key_binding("encode-ENTER")
    245     mp.osd_message("", 0)
    246 end
    247 
    248 function set_timestamp(profile)
    249     if not mp.get_property("path") then
    250         mp.osd_message("No file currently playing")
    251         return
    252     end
    253     if not mp.get_property_bool("seekable") then
    254         mp.osd_message("Cannot encode non-seekable media")
    255         return
    256     end
    257 
    258     if not start_timestamp or profile ~= profile_start then
    259         profile_start = profile
    260         start_timestamp = mp.get_property_number("time-pos")
    261         local msg = function()
    262             mp.osd_message(
    263                 string.format("encode [%s]: waiting for end timestamp", profile or "default"),
    264                 timer_duration
    265             )
    266         end
    267         msg()
    268         timer = mp.add_periodic_timer(timer_duration, msg)
    269         mp.add_forced_key_binding("ESC", "encode-ESC", clear_timestamp)
    270         mp.add_forced_key_binding("ENTER", "encode-ENTER", function() set_timestamp(profile) end)
    271     else
    272         local from = start_timestamp
    273         local to = mp.get_property_number("time-pos")
    274         if to <= from then
    275             mp.osd_message("Second timestamp cannot be before the first", timer_duration)
    276             timer:kill()
    277             timer:resume()
    278             return
    279         end
    280         clear_timestamp()
    281         mp.osd_message(string.format("Encoding from %s to %s"
    282             , seconds_to_time_string(from, false)
    283             , seconds_to_time_string(to, false)
    284         ), timer_duration)
    285         -- include the current frame into the extract
    286         local fps = mp.get_property_number("container-fps") or 30
    287         to = to + 1 / fps / 2
    288         local settings = {
    289             detached = true,
    290             container = "",
    291             only_active_tracks = false,
    292             preserve_filters = true,
    293             append_filter = "",
    294             codec = "-an -sn -c:v libvpx -crf 10 -b:v 1000k",
    295             output_format = "$f_$n.webm",
    296             output_directory = "",
    297             ffmpeg_command = "ffmpeg",
    298             print = true,
    299         }
    300         if profile then
    301             options.read_options(settings, profile)
    302             if settings.container ~= "" then
    303                 msg.warn("The 'container' setting is deprecated, use 'output_format' now")
    304                 settings.output_format = settings.output_format .. "." .. settings.container
    305             end
    306             settings.profile = profile
    307         else
    308             settings.profile = "default"
    309         end
    310         start_encoding(from, to, settings)
    311     end
    312 end
    313 
    314 mp.add_key_binding(nil, "set-timestamp", set_timestamp)