videoclip/videoclip.lua

365 lines
9.9 KiB
Lua
Raw Normal View History

2020-08-21 04:19:06 +00:00
local mpopt = require('mp.options')
-- Options can be changed here or in a separate config file.
-- Config path: ~/.config/mpv/script-opts/videoclip.conf
local config = {
-- absolute paths
2020-09-19 23:47:10 +00:00
-- relative paths (e.g. ~ for home dir) do NOT work.
video_folder_path = string.format('%s/Videos/', os.getenv("HOME")),
2020-09-19 23:47:10 +00:00
audio_folder_path = string.format('%s/Music/', os.getenv("HOME")),
2020-08-21 04:19:06 +00:00
2020-08-21 04:47:46 +00:00
font_size = 24,
2020-08-21 04:19:06 +00:00
audio_bitrate = '32k',
-- The range of the CRF scale is 051, where 0 is lossless,
-- 23 is the default, and 51 is worst quality possible.
2020-09-20 00:43:41 +00:00
-- Insane values like 9999 still work but produce the worst quality.
2020-08-21 04:19:06 +00:00
video_quality = 23,
-- Use the slowest preset that you have patience for.
-- https://trac.ffmpeg.org/wiki/Encode/H.264
preset = 'faster',
video_width = -2,
video_height = 480,
}
mpopt.read_options(config, 'videoclip')
local menu
local encoder
local OSD
local Timings
2020-08-21 04:19:06 +00:00
2020-09-24 18:16:14 +00:00
local allowed_presets = {
ultrafast = true,
superfast = true,
veryfast = true,
faster = true,
fast = true,
medium = true,
slow = true,
slower = true,
veryslow = true,
}
2020-08-21 04:19:06 +00:00
------------------------------------------------------------
-- utility functions
2020-08-27 07:13:48 +00:00
function string:endswith(suffix)
2020-09-24 18:00:33 +00:00
return suffix == "" or self:sub(-#suffix) == suffix
2020-08-27 07:13:48 +00:00
end
local function remove_extension(filename)
2020-09-19 23:47:10 +00:00
return filename:gsub('%.%w+$', '')
2020-08-21 04:19:06 +00:00
end
local function remove_text_in_brackets(str)
2020-09-19 23:47:10 +00:00
return str:gsub('%b[]', '')
2020-08-21 04:19:06 +00:00
end
local function remove_special_characters(str)
2020-09-19 23:47:10 +00:00
return str:gsub('[%c%p%s]', '')
2020-08-21 04:19:06 +00:00
end
local function human_readable_time(seconds)
if type(seconds) ~= 'number' or seconds < 0 then
2020-08-21 04:19:06 +00:00
return 'empty'
end
local parts = {}
2020-08-21 04:19:06 +00:00
2020-09-19 23:47:10 +00:00
parts.h = math.floor(seconds / 3600)
parts.m = math.floor(seconds / 60) % 60
parts.s = math.floor(seconds % 60)
parts.ms = math.floor((seconds * 1000) % 1000)
local ret = string.format("%02dm%02ds%03dms", parts.m, parts.s, parts.ms)
if parts.h > 0 then
ret = string.format('%dh%s', parts.h, ret)
2020-08-21 04:19:06 +00:00
end
return ret
2020-08-21 04:19:06 +00:00
end
local function construct_filename()
2020-08-21 04:19:06 +00:00
local filename = mp.get_property("filename") -- filename without path
filename = remove_extension(filename)
filename = remove_text_in_brackets(filename)
filename = remove_special_characters(filename)
filename = string.format(
2020-09-13 20:02:45 +00:00
'%s_(%s-%s)',
filename,
human_readable_time(menu.timings['start']),
human_readable_time(menu.timings['end'])
2020-08-21 04:19:06 +00:00
)
return filename
end
local function subprocess(args)
return mp.command_native {
2020-08-27 03:29:23 +00:00
name = "subprocess",
playback_only = false,
capture_stdout = true,
capture_stderr = true,
2020-08-27 03:29:23 +00:00
args = args
}
2020-08-21 04:19:06 +00:00
end
------------------------------------------------------------
-- provides interface for creating audio/video clips
encoder = {}
encoder.create_videoclip = function(clip_filename)
local clip_path = table.concat { config.video_folder_path, clip_filename, '.mp4' }
return subprocess {
'mpv',
mp.get_property('path'),
2020-09-26 22:35:36 +00:00
'--loop-file=no',
'--no-ocopy-metadata',
'--no-sub',
'--audio-channels=2',
'--oac=libopus',
'--ovc=libx264',
'--oacopts-add=vbr=on',
'--oacopts-add=application=voip',
'--oacopts-add=compression_level=10',
table.concat { '--start=', menu.timings['start'] },
table.concat { '--end=', menu.timings['end'] },
table.concat { '--aid=', mp.get_property("aid") }, -- track number
table.concat { '--oacopts-add=b=', config.audio_bitrate },
table.concat { '--ovcopts-add=crf=', config.video_quality },
table.concat { '--ovcopts-add=preset=', config.preset },
table.concat { '--vf-add=scale=', config.video_width, ':', config.video_height },
table.concat { '-o=', clip_path }
2020-08-21 04:19:06 +00:00
}
end
encoder.create_audioclip = function(clip_filename)
local clip_path = table.concat { config.audio_folder_path, clip_filename, '.ogg' }
return subprocess {
'mpv',
mp.get_property('path'),
'--loop-file=no',
'--no-ocopy-metadata',
'--no-sub',
'--audio-channels=2',
2020-09-26 22:35:36 +00:00
'--video=no',
'--oac=libopus',
'--oacopts-add=vbr=on',
'--oacopts-add=application=voip',
'--oacopts-add=compression_level=10',
table.concat { '--start=', menu.timings['start'] },
table.concat { '--end=', menu.timings['end'] },
table.concat { '--aid=', mp.get_property("aid") }, -- track number
table.concat { '--oacopts-add=b=', config.audio_bitrate },
table.concat { '-o=', clip_path }
2020-08-22 00:44:37 +00:00
}
end
encoder.create_clip = function(clip_type)
if clip_type == nil then
return
end
2020-08-22 00:44:37 +00:00
if not menu.timings:validate() then
2020-08-22 00:44:37 +00:00
mp.osd_message("Wrong timings. Aborting.", 2)
return
end
local clip_filename = construct_filename()
2020-08-27 03:29:23 +00:00
mp.osd_message("Please wait...", 9999)
local ret
local location
2020-08-27 03:29:23 +00:00
2020-08-22 00:44:37 +00:00
if clip_type == 'video' then
location = config.video_folder_path
ret = encoder.create_videoclip(clip_filename)
2020-08-22 00:44:37 +00:00
elseif clip_type == 'audio' then
location = config.audio_folder_path
ret = encoder.create_audioclip(clip_filename)
2020-08-27 03:29:23 +00:00
end
2020-09-20 01:11:33 +00:00
if ret.status ~= 0 or string.match(ret.stdout, "could not open") then
mp.osd_message(string.format("Error: couldn't create the clip.\nDoes %s exist?", location), 5)
2020-09-20 00:21:39 +00:00
else
mp.osd_message(string.format("Clip saved to %s.", location), 2)
2020-08-22 00:44:37 +00:00
end
end
2020-08-21 04:19:06 +00:00
------------------------------------------------------------
-- main menu
menu = {}
2020-09-20 00:01:38 +00:00
menu.overlay = mp.create_osd_overlay('ass-events')
menu.overlay_draw = function(text)
menu.overlay.data = text
menu.overlay:update()
end
2020-08-21 04:19:06 +00:00
menu.keybinds = {
{ key = 's', fn = function() menu.set_time('start') end },
{ key = 'e', fn = function() menu.set_time('end') end },
{ key = 'S', fn = function() menu.set_time_sub('start') end },
{ key = 'E', fn = function() menu.set_time_sub('end') end },
{ key = 'c', fn = function() menu.close(); encoder.create_clip('video'); menu.timings:reset() end },
{ key = 'a', fn = function() menu.close(); encoder.create_clip('audio'); menu.timings:reset() end },
2020-08-21 04:19:06 +00:00
{ key = 'o', fn = function() mp.commandv('run', 'xdg-open', 'https://streamable.com/') end },
{ key = 'ESC', fn = function() menu.close() end },
}
menu.set_time = function(property)
menu.timings[property] = mp.get_property_number('time-pos')
2020-08-21 04:19:06 +00:00
menu.update()
end
menu.set_time_sub = function(property)
local sub_delay = mp.get_property_native("sub-delay")
local time_pos = mp.get_property_number(string.format("sub-%s", property))
if time_pos == nil then
2020-09-06 16:01:32 +00:00
mp.osd_message("Warning: No subtitles visible.", 2)
2020-08-21 04:19:06 +00:00
return
end
menu.timings[property] = time_pos + sub_delay
2020-08-21 04:19:06 +00:00
menu.update()
end
2020-09-06 16:01:32 +00:00
menu.update = function()
2020-09-06 15:51:36 +00:00
local osd = OSD:new():size(config.font_size):align(4)
osd:bold('Clip creator'):newline():newline()
2020-08-21 04:19:06 +00:00
osd:bold('Start time: '):append(human_readable_time(menu.timings['start'])):newline()
osd:bold('End time: '):append(human_readable_time(menu.timings['end'])):newline()
osd:newline()
2020-08-21 04:19:06 +00:00
osd:bold('Bindings:'):newline()
osd:tab():bold('s: '):append('Set start time'):newline()
osd:tab():bold('e: '):append('Set end time'):newline()
osd:newline()
osd:tab():bold('S: '):append('Set start time based on subtitles'):newline()
osd:tab():bold('E: '):append('Set end time based on subtitles'):newline()
osd:newline()
osd:tab():bold('c: '):append('Create video clip'):newline()
2020-08-22 00:44:37 +00:00
osd:tab():bold('a: '):append('Create audio clip'):newline()
2020-08-21 04:19:06 +00:00
osd:tab():bold('o: '):append('Open `streamable.com`'):newline()
osd:tab():bold('ESC: '):append('Close'):newline()
menu.overlay_draw(osd:get_text())
2020-08-21 04:19:06 +00:00
end
menu.close = function()
for _, val in pairs(menu.keybinds) do
mp.remove_key_binding(val.key)
end
2020-09-20 00:01:38 +00:00
menu.overlay:remove()
2020-08-21 04:19:06 +00:00
end
menu.open = function()
menu.timings = Timings:new()
2020-08-21 04:19:06 +00:00
for _, val in pairs(menu.keybinds) do
mp.add_key_binding(val.key, val.key, val.fn)
end
menu.update()
end
------------------------------------------------------------
-- Helper class for styling OSD messages
2020-09-06 15:51:52 +00:00
-- http://docs.aegisub.org/3.2/ASS_Tags/
2020-08-21 04:19:06 +00:00
OSD = {}
OSD.__index = OSD
function OSD:new()
return setmetatable({ text = {} }, self)
2020-08-21 04:19:06 +00:00
end
function OSD:append(s)
table.insert(self.text, s)
2020-08-21 04:19:06 +00:00
return self
end
function OSD:bold(s)
2020-09-20 00:02:51 +00:00
return self:append(string.format([[{\b1}%s{\b0}]], s))
2020-08-21 04:19:06 +00:00
end
2020-09-27 23:54:17 +00:00
function OSD:italics(s)
return self:append('{\\i1}'):append(s):append('{\\i0}')
end
2020-08-21 04:19:06 +00:00
function OSD:newline()
2020-09-20 00:02:51 +00:00
return self:append([[\N]])
2020-08-21 04:19:06 +00:00
end
function OSD:tab()
2020-09-20 00:02:51 +00:00
return self:append([[\h\h\h\h]])
2020-08-21 04:19:06 +00:00
end
function OSD:size(size)
2020-09-20 00:02:51 +00:00
return self:append(string.format([[{\fs%s}]], size))
2020-08-21 04:19:06 +00:00
end
2020-09-06 15:51:36 +00:00
function OSD:align(number)
2020-09-20 00:02:51 +00:00
return self:append(string.format([[{\an%s}]], number))
2020-08-21 04:19:06 +00:00
end
function OSD:get_text()
return table.concat(self.text)
end
2020-08-21 04:19:06 +00:00
------------------------------------------------------------
-- Timings class
Timings = {
['start'] = -1,
['end'] = -1,
}
function Timings:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
function Timings:reset()
self['start'] = -1
2020-09-19 23:47:10 +00:00
self['end'] = -1
2020-08-21 04:19:06 +00:00
end
function Timings:validate()
return self['start'] >= 0 and self['start'] < self['end']
2020-08-21 04:19:06 +00:00
end
2020-08-27 07:13:48 +00:00
------------------------------------------------------------
-- Validate config
if not config.video_folder_path:endswith('/') then
config.video_folder_path = config.video_folder_path .. '/'
end
if not config.audio_folder_path:endswith('/') then
config.audio_folder_path = config.audio_folder_path .. '/'
end
if not config.audio_bitrate:endswith('k') then
config.audio_bitrate = config.audio_bitrate .. 'k'
end
2020-09-24 18:16:14 +00:00
if not allowed_presets[config.preset] then
config.preset = 'faster'
end
2020-08-21 04:19:06 +00:00
------------------------------------------------------------
-- Finally, set an 'entry point' in mpv
2020-08-31 19:45:35 +00:00
mp.add_key_binding('c', 'videoclip-menu-open', menu.open)