Skip to content

Instantly share code, notes, and snippets.

@onyx-nxt
Last active January 21, 2026 09:46
Show Gist options
  • Select an option

  • Save onyx-nxt/750578ac505dc3560ea80c15bb21112b to your computer and use it in GitHub Desktop.

Select an option

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…
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)
@onyx-nxt
Copy link
Author

onyx-nxt commented Dec 24, 2025

Full description:

MPV script that fixes video sync on some broken videos where both the audio and video PTS are incorrect by a constant factor (like what -itsscale 2 ... -c copy does in ffmpeg). 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 video tagged as 30 FPS but actually 120 FPS, it would advance 4 seconds in 1 actual second, so we can calculate to find the factor to multiply the PTS by. This script includes some sanity checks to ensure the correct factor is used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment