Last active
January 21, 2026 09:46
-
-
Save onyx-nxt/750578ac505dc3560ea80c15bb21112b to your computer and use it in GitHub Desktop.
MPV script that fixes video sync on some broken videos where both the audio and video PTS are incorrect by a constant factor. This is apparent in some videos uploaded to internet platforms to exploit and bypass max framerate checks. This works by monitoring the demuxer cache or audio PTS and measuring how much it increases over 1 second. In a vi…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| function fix_sync() | |
| -- Capture the first snapshot | |
| local pts_prev = math.floor(mp.get_property_native('demuxer-cache-state')['reader-pts'] or mp.get_property_number('audio-pts')) | |
| local sample_time = 1 | |
| local pts_prev_diff = 0 | |
| local sample_timer | |
| sample_timer = mp.add_periodic_timer(sample_time, function() | |
| local pts_cur = math.floor(mp.get_property_native('demuxer-cache-state')['reader-pts'] or mp.get_property_number('audio-pts')) | |
| local pts_diff = pts_cur - pts_prev | |
| pts_prev = pts_cur | |
| -- Only proceed if pts difference is stable | |
| if pts_diff <= 0 or pts_diff ~= pts_prev_diff then | |
| pts_prev_diff = pts_diff | |
| return | |
| end | |
| local fps = math.floor(mp.get_property_number('container-fps')) | |
| local mistimed_frames = mp.get_property_number('mistimed-frame-count') or 0 | |
| local dps_threshold = (fps * sample_time) + ((fps * sample_time) * 0.125) | |
| if mistimed_frames ~= 0 and mistimed_frames <= dps_threshold and fps > 0 then | |
| -- No significant number of frames were dropped, likely no sync issue | |
| sample_timer:kill() | |
| return | |
| end | |
| local sync_factor = sample_time / pts_diff | |
| local pts_multiplier = math.floor(sync_factor * 1000 + 0.5) / 1000 | |
| -- Avoid fractional or odd fps values | |
| local target_fps = fps / pts_multiplier | |
| local is_whole_even = (math.abs(target_fps - math.floor(target_fps + 0.5)) < 0.00001) and (math.floor(target_fps + 0.5) % 2 == 0) | |
| if not is_whole_even then | |
| local target_even = math.floor((target_fps / 2) + 0.5) * 2 | |
| if target_even == 0 then target_even = 2 end | |
| local ideal_multiplier = fps / target_even | |
| pts_multiplier = ideal_multiplier | |
| end | |
| if pts_multiplier == 1 then | |
| -- No adjustment needed | |
| sample_timer:kill() | |
| return | |
| end | |
| -- Apply settings to compensate for sync issue | |
| mp.commandv('vf', 'add', '@sync:setpts='.. pts_multiplier ..'*PTS') | |
| mp.commandv('af', 'add', '@sync:asetpts=' .. pts_multiplier .. '*PTS') | |
| mp.msg.info('Sync fix applied: A/V PTS multiplier set to ' .. pts_multiplier) | |
| sample_timer:kill() | |
| end) | |
| end | |
| local function wait_for_audio(name, pts) | |
| if pts ~= nil then | |
| mp.unobserve_property(wait_for_audio) | |
| fix_sync() | |
| end | |
| end | |
| local function wait_for_start(name, paused) | |
| if paused == false then | |
| -- The video is playing, wait for audio and start the sync fix | |
| mp.unobserve_property(wait_for_start) | |
| mp.observe_property('audio-pts', 'number', wait_for_audio) | |
| end | |
| end | |
| mp.register_event('file-loaded', function() | |
| -- Reset any previous adjustments | |
| for _, f in ipairs(mp.get_property_native('vf') or {}) do if f.label == 'sync' then mp.command('no-osd vf remove @sync') end end | |
| for _, f in ipairs(mp.get_property_native('af') or {}) do if f.label == 'sync' then mp.command('no-osd af remove @sync') end end | |
| -- Only run on videos | |
| local is_video = false | |
| for _, t in ipairs(mp.get_property_native('track-list') or {}) do | |
| if t.type == 'video' and not t.image then | |
| is_video = true | |
| break | |
| end | |
| end | |
| if not is_video then return end | |
| if mp.get_property_bool('pause') == false then | |
| -- If already playing, wait for audio and start the sync fix | |
| mp.observe_property('audio-pts', 'number', wait_for_audio) | |
| else | |
| -- If paused, wait for unpause. We need the video to be playing | |
| mp.observe_property('pause', 'bool', wait_for_start) | |
| end | |
| end) |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Full description: