Skip to main content

Video sync

A YouTube guitar lesson synced to a printable score โ€” the video plays and the notation scrolls alongside โ€” has become a standard learning format. Video sync uses the same infrastructure as backing-track audio, with one twist: alphaTab does not own the media element. You implement IExternalMediaHandler, alphaTab calls it for play, pause, seek, volume, and playback rate, and you feed the current time back through player.output.updatePosition. The same handler shape drives an HTML5 <video> tag and the YouTube IFrame Player API alike.

The demo below pairs the Bach BWV 999 score with a matching MP4 whose visualization is generated from the same audio stream โ€” the on-screen waveform is the sound you hear, so nothing drifts between what you see and what plays. Press play on the video; the alphaTab cursor tracks the video's clock via the external-media handler shown further down.

The two-way contractโ€‹

External-media mode plugs two pieces together:

  • IExternalMediaHandler โ€” the object you implement. It exposes backingTrackDuration, playbackRate, masterVolume, seekTo, play, and pause. alphaTab calls into it whenever the score's transport changes.
  • api.player.output.updatePosition(ms) โ€” the call you make on every media time update, roughly every 50 ms during playback and after every seek. This is how alphaTab moves the cursor.

Set player.playerMode to PlayerMode.EnabledExternalMedia and assign your handler to api.player.output.handler.

YouTube quirkโ€‹

The YouTube IFrame API resumes playback whenever seekTo is called on a non-playing player. alphaTab issues an initial seek to position zero during load, so a naive handler starts the video the moment the page mounts. The workaround is to defer the initial seek until the user actually presses play โ€” check player.getPlayerState() inside your seekTo and queue the position if the state is UNSTARTED or CUED.

Show it in codeโ€‹

import * as alphaTab from '@coderline/alphatab';

const container = getAlphaTabContainer();
const video = document.getElementById('video');

const api = new alphaTab.AlphaTabApi(container, {
core: { file: '/scores/song.gp' },
player: {
playerMode: alphaTab.PlayerMode.EnabledExternalMedia,
soundFont: '/soundfont/sonivox.sf2',
},
});

const handler = {
get backingTrackDuration() {
const d = video.duration;
return Number.isFinite(d) ? d * 1000 : 0;
},
get playbackRate() { return video.playbackRate; },
set playbackRate(value) { video.playbackRate = value; },
get masterVolume() { return video.volume; },
set masterVolume(value) { video.volume = value; },
seekTo(timeMs) { video.currentTime = timeMs / 1000; },
play() { video.play(); },
pause() { video.pause(); },
};

api.playerReady.on(() => {
api.player.output.handler = handler;
});

let updateTimer = 0;
const pushPosition = () => api.player.output.updatePosition(video.currentTime * 1000);

video.addEventListener('play', () => {
api.play();
updateTimer = window.setInterval(pushPosition, 50);
});
video.addEventListener('pause', () => { api.pause(); clearInterval(updateTimer); });
video.addEventListener('seeked', pushPosition);

getAlphaTabContainer() is a placeholder for the platform-specific step of obtaining the render target โ€” a <div> on Web, an AlphaTab control on .NET, or an AlphaTabView on Android.

Where to look nextโ€‹