Open Source
Keeping a browser video and a locally processed audio track in sync — and the 0.2s leak that broke it
smsm Dev.to (EN Zone)
3 views
We build [HaramLite](https://haramlite.com), a desktop app that removes music and
instrumentals from video and audio on the user's own machine — no uploads, no
accounts, no cloud. This is the story of a sync bug in its browser side, because
the fix is a nice illustration of a class of bug that is easy to ship and hard to
see.
## The setup
The browser extension never re-encodes anything. When you watch a YouTube video
with the music removed, it does two things:
1. mutes the page video element, and
2. plays a second `<audio>` element — the file the desktop app produced — and
keeps the two in step.
The audio file is not the same length as the video, because the stretches that
held only music were *cut out* of it. So the timeline is compressed, and we keep
a map of the ranges that survived: `kept = [[0,10],[12,20]]` reads as "seconds
0–10 and 12–20 of the video exist in the audio".
Whenever the video enters a stretch that is missing from the audio, we jump the
**picture** forward over it:
js
const target = skipVideoGaps(video.currentTime, kept); // full timeline
if (Math.abs(target - video.currentTime) > 0.15) {
video.currentTime = target;
// ...and the audio has to follow, or it keeps playing content that
// belongs after the skip.
}
## The bug
The audio was re-anchored **only** when a hold flag was set, which happened only
when a seek had landed inside a removed stretch. On an ordinary jump the picture
moved and the sound did not — for about two tenths of a second. The user heard
the first word of the next sentence twice: once from the tail of the stretch that
should have been skipped, then again when the audio finally landed in the right
place.
Why did nothing correct it? There *is* a periodic drift corrector. It runs every
second and fixes the audio when it has drifted more than **0.35s**. The leak was
**0.20s** — comfortably inside the tolerance, so it was never corrected. It only
snapped back once leaks accumulated past the threshold.
## Two details that matter in the fix
**Compute from the value you just seeked to, not from the element.** A media
seek is asynchronous; reading `video.currentTime` immediately after assigning it
is unreliable. The old code re-derived the audio position from
`video.currentTime`, which could read stale. The fix maps the *target* instead:
js
const reanchorAudio = (fullT) => {
const want = clamp(mapFullToCut(fullT, kept), 0, audio.duration - 0.05);
if (Math.abs(audio.currentTime - want) <= 0.05) return;
// mute across the seek so the fragment is inaudible, unmute on 'seeked'
audio.muted = true;
audio.currentTime = want;
audio.addEventListener('seeked', unmute, { once: true });
setTimeout(unmute, 120); // fallback if 'seeked' never arrives
};
**Keep the wide tolerance for the periodic corrector, tighten only at the jump.**
The 0.35s window exists so the corrector does not fight the player's own seeks.
Loosening or removing it globally trades one bug for another. The jump path uses
its own 0.05s threshold, because there we know exactly where the audio should be.
## Prove it without a browser
The mapping functions are pure, so the fix can be demonstrated arithmetically —
no autoplay policies, no headless quirks, no flaky test. Extracting the real
functions from the shipped file and running the case above:
plaintext
words 0-10, removed 10-12, words 12-20
audio drifted to 10.20s during the gap
expected after the jump 10.00s
error 0.20s < 0.35s tolerance => never corrected
new re-anchor threshold 0.05s => corrects 10.20 to 10.00
Sixteen assertions, including the boundary cases: no map means no change, a
0.1s jump does not trigger the skip, past the last range the video stays put.
## Three things I would tell my past self
1. **A tolerance window silently hides every error below it.** If your corrector
has a threshold, ask what *small persistent* error it permits — ours was 0.2s
of audible audio belonging to the wrong moment.
2. **Media elements are asynchronous.** Never derive a new position by reading
an element you have just written to.
3. **Extract the pure logic and test it.** The interesting part of a media bug is
usually the arithmetic around it, and that part needs no browser at all.
The app is open source (Tauri v2 + Rust on the desktop side, ONNX Runtime with
UVR-MDX-NET for the separation itself), and the extension is plain JavaScript:
- Project and source: https://github.com/SMSMy/HaramLite
- Site and guides: https://haramlite.com
If you have shipped a similar sync problem — subtitles, karaoke, dubbed audio,
anything where two media elements have to agree on where "now" is — I would like
to hear how you handled the drift window.
Read original: https://dev.to/smsmy/keeping-a-browser-video-and-a-locally-processed-audio-track-in-sync-and-the-02s-leak-that-broke-26fe
← Previous
Why Go + Python Is One of the Most Practical Language Pairings in Modern Software
Next →
Suggestions for front-end hackathon
Related
252 affiliate programs, and 38% of the terms are not published
Open Source
7
Dev.to (EN Zone)
gh api --paginate --slurp --jq 'length' Counts Pages, Not Issues
Open Source
7
DEV Community
Milestone Reached: YINI Syntax Highlighting Is Now on the VS Code Marketplace
Open Source
5
DEV Community
I built a CSV chart page that shows a SHA of your file so the chart can’t invent numbers
Open Source
7
Dev.to (EN Zone)
Comments0
No comments yet — be the first