MPV Pro — fork for professionals
MPV is the best lightweight video player that I want to improve for video professionals. The current version is to geeky for non-programmers who are not familiar with CLI. It hard to install and default config is not optimal.
What I want to change in MPV
Easy installation for macOS/Windows
Right not the only way to install MPV is to deal with CLI. I want to create a regular .app package that can be install just my drag-n-drop like any other applications on macOS or even upload it to App Store and Windows Store.
Valid signature
All modern OS require a signature. I can provide a signature for macOS and Windows from my company to sign the binaries for macOS and WIndows
Convert any video to H264 or DHNxHD for editing?
A simple option in GUI to convert any strange video format to most popular video codec ready to publish to the web or social media. Or decompress to the best editing codec.
Built-in cut feature
Easy to use cut feature to cut any part of video and export it to source codec without re-encoding or h264. Maybe this plugin https://github.com/familyfriendlymikey/mpv-cut
Suggestion for default settings
Default settings is not optimal for simple and fast operating. I suggest to change it to this:
.mpv/config
# resize window in case it's larger than screen autofit-larger=90%x90% # keep the player open when a file's end is reached keep-open=yes # disable interlace deinterlace=no # much faster fullscreen on macOS than native no-native-fs # correct window resizing with mouse wheel on hidpi macbooks no-hidpi-window-scale # improve playing performance at the cost of video quality vd-lavc-fast vd-lavc-skiploopfilter=all
.mpv/input.conf
🐛 The keyboard shortcuts works only with English keyboard layout. This bug should be fixed.
# Resize window with mouse scroll WHEEL_UP add window-scale +0.0625 WHEEL_DOWN add window-scale -0.0625 # Move back/forward by one frame , frame-back-step ;show-text "${playback-time/full} / ${duration} (${percent-pos}%)\nframe: ${estimated-frame-number} / ${estimated-frame-count}" . frame-step ; show-text "${playback-time/full} / ${duration} (${percent-pos}%)\nframe: ${estimated-frame-number} / ${estimated-frame-count}" # Apply color filter in cycle c-1 vf set "format=gamma=v-log:colorlevels=full:primaries=v-gamut" ; show-text "Panasonic V-Log" c-2 set contrast 58 ; set brightness 0 ; set gamma -12 ; set saturation 10 ; show-text "My BMPCC4K" c-0 vf clr "" ; set contrast 0 ; set brightness 0 ; set gamma 0 ; set saturation 0 ; show-text "Color cleared" # Duplicate russian layout hotkeys because of MPV bug # This bug should be fixed й keypress "q" ц keypress "w" у keypress "e" к keypress "r" е keypress "t" н keypress "y" г keypress "u" ш keypress "i" щ keypress "o" з keypress "p" х keypress "[" ъ keypress "]" ф keypress "a" ы keypress "s" в keypress "d" а keypress "f" п keypress "g" р keypress "h" о keypress "j" л keypress "k" д keypress "l" ж keypress ";" э keypress "'" я keypress "z" ч keypress "x" с keypress "c" м keypress "v" и keypress "b" т keypress "n" ь keypress "m" б keypress "," ю keypress "."
./scripts/control.lua
-- Control 1.0.5 -- https://github.com/oe-d/control -- See control.conf for settings and key binds options = require 'mp.options' u = require 'mp.utils' o = { audio_device = 0, pause_minimized = 'no', play_restored = 'no', show_info = 'yes', info_duration = 1000, step_method = 'seek', step_delay = -1, step_rate = 0, step_mute = 'auto', htp_speed = 2.5, htp_keep_dir = 'no', end_rewind = 'file', end_exit_fs = 'no', audio_symbol='🔊 ', audio_muted_symbol='🔈 ', image_symbol='🖼 ', music_symbol='🎵 ', video_symbol='🎞 ' } function init() options.read_options(o, 'control') if o.step_delay == -1 then o.step_delay = get('input-ar-delay') end if o.step_rate == -1 then o.step_rate = get('input-ar-rate') end if o.end_rewind == 'file' then mp.set_property('keep-open', 'always') end if o.show_info == 'start' then o.show_info = 'yes' osd:toggle() end osd.default_msg = function() if media.type == 'image' then return o.image_symbol elseif media.type == 'audio' then return o.music_symbol else local frame = get('frame') local frames = get('frames') if not frame or not frames then return o.video_symbol else frame = frame + 1 end local progress = math.floor(frame / frames * 100) return o.video_symbol..math.min(frame, frames)..' / '..frames..' ('..progress..'%)\n' ..format(math.max(get('pos') or 0, 0))..'\n' ..round(fps.fps, 3)..' fps ('..round(get('speed'), 2)..'x)' end end osd.msg_timer:kill() osd.osd_timer:kill() step.delay_timer:kill() step.delay_timer.timeout = o.step_delay / 1000 step.hwdec_timer:kill() if o.audio_device > 0 then audio:set(o.audio_device) end mp.register_event('file-loaded', function() media:get_type() end) mp.observe_property('window-minimized', 'bool', function(_, v) if o.pause_minimized == 'yes' or o.pause_minimized == media:get_type() then if v then media.playback:on_minimize() elseif o.play_restored == 'yes' then media.playback:on_restore() end end end) mp.observe_property('playback-time', 'number', function(_, _) if osd.show then fps:tick() osd:set(nil, o.info_duration / 1000) end end) mp.observe_property('play-dir', 'string', function(_, v) if v == 'forward' and step.prev_hwdec then step.dir_frame = get('frame') step.hwdec_timer:resume() end end) mp.observe_property('eof-reached', 'bool', function(_, v) media.playback.eof = v if v and not step.played then if o.end_rewind ~= 'no' then local pos = tonumber(o.end_rewind) if pos then mp.set_property('playlist-pos-1', math.min(pos, get('playlist-count'))) end mp.add_timeout(0.01, function() media.playback.rewind(true) end) end if o.end_exit_fs == 'yes' then mp.command('set fullscreen no') end end end) end function split(string, pattern) local str = {} for i in string.gmatch(string, pattern) do table.insert(str, i) end return str end function round(number, decimals) decimals = decimals or 0 return math.floor(number * 10 ^ decimals + 0.5) / 10 ^ decimals end function format(time) time = time or 0 local h = math.floor(time / 3600) local m = math.floor(time % 3600 / 60) local s = time % 60 return string.format('%02d:%02d:%06.03f', h, m, s) end function get(property) local props = { drops = 'frame-drop-count', e_fps = 'estimated-vf-fps', fps = 'container-fps', frame = 'estimated-frame-number', frames = 'estimated-frame-count', pos = 'playback-time' } for k, v in pairs(props) do if k == property then property = v end end return mp.get_property_native(property) end media = { type = nil, get_type = function(self) if get('track-list/0/type') == 'video' and get('frames') == 0 then self.type = 'image' elseif get('track-list/0/type') == 'audio' or get('track-list/0/albumart') == 'yes' then self.type = 'audio' else self.type = 'video' end return self.type end, playback = { eof = false, prev_pause = false, pause = function(self) if self.eof then self.rewind() else if get('pause') and step.stepped then mp.commandv('seek', 0, 'relative+exact') step.stepped = false end mp.command('set pause '..(get('pause') and 'no' or 'yes')) end end, rewind = function(pause) mp.commandv('seek', 0, 'absolute') mp.command('set pause '..(pause and 'yes' or 'no')) end, on_minimize = function(self) self.prev_pause = get('pause') mp.command('set pause yes') end, on_restore = function(self) if not self.prev_pause then mp.command('set pause no') end self.prev_pause = false end } } audio = { osd = true, prev_list = '', i = 0, set_prev_vol = false, prev_mute = false, prev_vol = 0, valid = true, get = function(self, index) local list = get('audio-device-list') if index and (index < 1 or index > table.getn(list)) then self.valid = false list[1].name = 'Invalid device index ('..index..')' list[1].description = list[1].name index = 1 end return index and list[index] or list end, set = function(self, index) local name = self:get(index).name if self.valid then mp.command('no-osd set audio-device '..name) end end, list = function(self, list, show_index, duration) local msg = '' for i, v in ipairs(list) do local symbol = '' if v.name == get('audio-device') then symbol = (get('mute') or get('volume') == 0) and o.audio_muted_symbol or o.audio_symbol end i = show_index and i..': ' or '' msg = msg..i..symbol..string.gsub(v.description, 'Autoselect', 'Default')..'\n' end if self.osd then osd:set(msg, duration) end end, cycle = function(self, list) if u.to_string(list) ~= self.prev_list then self.i = 0 end self.prev_list = u.to_string(list) self.i = self.i == table.getn(list) and 1 or self.i + 1 local remember_vol = false local index = 0 local set_vol = false local vol = 0 for i, v in ipairs(list) do local iv = split(v, '%d+') if i == (self.i > 1 and self.i - 1 or table.getn(list)) and string.find(v, 'r') then self.set_prev_vol = true remember_vol = true end if i == self.i then index = tonumber(iv[1]) if iv[2] then set_vol = true vol = iv[2] end end list[i] = self:get(tonumber(iv[1])) end if remember_vol then self.prev_mute = get('mute') self.prev_vol = get('volume') end self.valid = true self:set(index) if set_vol then mp.command('no-osd set mute no') mp.command('no-osd set volume '..vol) elseif self.set_prev_vol then mp.command('no-osd set mute '..(self.prev_mute and 'yes' or 'no')) mp.command('no-osd set volume '..self.prev_vol) self.set_prev_vol = false end self:list(list, false, 2) end, msg_handler = function(self, cmd, ...) if cmd == 'list' then self.osd = true self:list(self:get(), true, 4) elseif cmd == 'cycle' then local args = {...} if args[1] == 'no-osd' then table.remove(args, 1) self.osd = false else self.osd = true end self:cycle(args) end end } fps = { interval = 0.5, fps = 0, prev_time = 0, prev_pos = 0, prev_drops = 0, prev_vop_dur = 0, vop_dur = 0, frames = 0, tick = function(self) local vop = get('vo-passes') or {fresh = {}} for _, v in ipairs(vop.fresh) do self.vop_dur = self.vop_dur + v.last end if self.vop_dur ~= self.prev_vop_dur then self.frames = self.frames + 1 end self.prev_vop_dur = self.vop_dur self.vop_dur = 0 local fps = get('e_fps') local t_delta = mp.get_time() - self.prev_time if not fps or t_delta < self.interval then return end local spd = get('speed') local pos_delta = math.abs((get('pos') or 0) - (self.prev_pos or 0)) local drops = (get('drops') or 0) - (self.prev_drops or 0) local mult = self.interval / t_delta local function hot_mess(speed) if drops > 0 and self.frames * mult < fps * speed / math.max(fps / 30, 1) * self.interval * 0.95 then self.fps = round(self.frames * mult, 2) else self.fps = fps * spd end end if spd > 1 then if drops > 0 and (pos_delta * mult > 2 or pos_delta * mult / self.interval > spd * 0.95 and self.frames * mult > 18 * self.interval) then self.fps = round(fps * pos_delta * mult / self.interval, 2) else hot_mess(1) end else hot_mess(spd) end self.prev_time = mp.get_time() self.prev_pos = get('pos') self.prev_drops = get('drops') self.frames = 0 end } osd = { default_msg = nil, msg = '', show = false, toggled = false, osd_timer = mp.add_timeout(1e8, function() mp.set_property('osd-msg1', '') end), msg_timer = mp.add_timeout(1e8, function() osd.msg = osd.default_msg() end), set = function(self, msg, duration) if msg or not self.toggled or (self.toggled and self.osd_timer.timeout ~= 1e8) then self.osd_timer:kill() self.osd_timer.timeout = self.toggled and 1e8 or duration self.osd_timer:resume() mp.set_property('osd-level', 1) end if msg then self.msg = msg self.msg_timer:kill() self.msg_timer.timeout = duration self.msg_timer:resume() mp.add_timeout(0.1, function() mp.set_property('osd-msg1', self.msg) end) elseif not self.msg_timer:is_enabled() then self.msg = self.default_msg() mp.set_property('osd-msg1', self.msg) else mp.set_property('osd-msg1', self.msg) end end, toggle = function(self) self.toggled = not self.toggled self.show = self.toggled self:set(nil, 0) end } fullscreen = { prev_time = 0, clicks = 0, x = 0, click = function(self) if mp.get_time() - self.prev_time > 0.3 then self.clicks = 0 end if self.clicks == 1 and mp.get_time() - self.prev_time < 0.3 and math.abs(mp.get_mouse_pos() - self.x) < 5 then self.clicks = 2 else self.x = mp.get_mouse_pos() self.clicks = 1 end self.prev_time = mp.get_time() end, cycle = function(self, e) if self.clicks == 2 and mp.get_time() - self.prev_time < 0.3 then if (e == 'down' and get('fs')) or (e == 'up' and not get('fs')) then mp.command('cycle fullscreen') self.clicks = 0 end end end, key_handler = function(self, e) if e.key_name == 'MBTN_LEFT_DBL' then osd:set('Bind to MBTN_LEFT. Not MBTN_LEFT_DBL.', 4) elseif e.event == 'press' then osd:set('Received a key press event.\n' ..'Key down/up events are required.\n' ..'Make sure nothing else is bound to the key.', 4) elseif e.event == 'down' then self:click() self:cycle(e.event) elseif e.event == 'up' then self:cycle(e.event) end end } step = { e_msg = false, direction = nil, prev_hwdec = nil, dir_frame = 0, paused = false, muted = false, prev_speed = 1, prev_pos = 0, play_speed = 1, stepped = false, played = false, delay_timer = mp.add_timeout(1e8, function() step:play() end), hwdec_timer = mp.add_periodic_timer(1 / 60, function() if get('play-dir') == 'forward' and not get('pause') and get('frame') ~= step.dir_frame then mp.command('no-osd set hwdec '..step.prev_hwdec) step.hwdec_timer:kill() step.prev_hwdec = nil end end), play = function(self) self.played = true if self.direction == 'backward' then mp.command('no-osd set hwdec no') end mp.command('no-osd set play-dir '..self.direction) mp.command('no-osd set speed '..self.play_speed) if o.step_mute == 'auto' and not self.muted then mp.command('no-osd set mute no') elseif o.step_mute == 'hold' then mp.command('no-osd set mute yes') end mp.commandv('seek', 0, 'relative+exact') mp.command('set pause no') end, start = function(self, dir, htp) self.direction = dir self.prev_hwdec = self.prev_hwdec or get('hwdec') self.paused = get('pause') self.muted = get('mute') self.prev_speed = get('speed') self.prev_pos = get('pos') if o.show_info == 'yes' then osd.show = true end if htp then self.play_speed = o.htp_speed self:play() else self.play_speed = o.step_rate == 0 and 1 or o.step_rate / get('fps') self.delay_timer:resume() mp.command('set pause yes') if dir == 'forward' and o.step_method == 'step' then if o.step_mute ~= 'no' then mp.command('no-osd set mute yes') end mp.command('frame-step') self.stepped = true elseif dir == 'backward' or get('time-pos') < get('duration') then mp.commandv('seek', (dir == 'forward' and 1 or -1) / get('fps'), 'relative+exact') end end end, stop = function(self, dir, htp) self.delay_timer:kill() if dir == 'backward' and get('frame') > 0 and not self.played and get('pos') == self.prev_pos then mp.command('frame-back-step') print('Backward seek failed. Reverted to backstep.') end if not htp or o.htp_keep_dir == 'no' then mp.command('no-osd set play-dir forward') end mp.command('no-osd set speed '..self.prev_speed) if not self.muted then mp.command('no-osd set mute no') end mp.command('set pause yes') if self.played then mp.commandv('seek', 0, 'relative+exact') end if htp and not self.paused and not (media.playback.eof and get('keep-open-pause')) then mp.command('set pause no') end self.played = false if not osd.toggled then osd.show = false end end, on_press = function(self, dir, htp) local msg = 'Received a key press event.\n' ..(htp and 'Key down/up events are required.\n' or 'Only single frame steps will work.\n') ..'Make sure nothing else is bound to the key.' if htp then osd:set(msg, 4) return else if not self.e_msg then print(msg) self.e_msg = true end self:start(dir, false) mp.add_timeout(0.1, function() self:stop(dir, false) end) end end, key_handler = function(self, e, dir, htp) if media.type ~= 'video' then return elseif e.event == 'press' then self:on_press(dir, htp) elseif e.event == 'down' then self:start(dir, htp) elseif e.event == 'up' then self:stop(dir, htp) end end } init() mp.register_script_message('list-audio-devices', function() audio:msg_handler('list') end) mp.register_script_message('set-audio-device', function(...) audio:msg_handler('cycle', ...) end) mp.register_script_message('cycle-audio-devices', function(...) audio:msg_handler('cycle', ...) end) mp.add_key_binding(nil, 'toggle-info', function() osd:toggle() end) mp.add_key_binding(nil, 'cycle-pause', function() media.playback:pause() end) mp.add_key_binding(nil, 'cycle-fullscreen', function(e) fullscreen:key_handler(e) end, {complex = true}) mp.add_key_binding(nil, 'step', function(e) step:key_handler(e, 'forward') end, {complex = true}) mp.add_key_binding(nil, 'step-back', function(e) step:key_handler(e, 'backward') end, {complex = true}) mp.add_key_binding(nil, 'htp', function(e) step:key_handler(e, 'forward', true) end, {complex = true}) mp.add_key_binding(nil, 'htp-back', function(e) step:key_handler(e, 'backward', true) end, {complex = true})