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)