Demo

Mediabunny Adapter Sync

A frame-aware preview surface where Mediabunny owns the playback clock and Canvas Timeline maps playhead time to source media time.

Media syncIntermediateMediaTimelineSync.tsx

Live demo

Code example

import {
TimelineEngine,
TimelineProvider,
Timeline,
useTimeline,
useTimelinePlayheadTime,
CanvasRenderer,
fromSeconds,
toSeconds,
} from '@techsquidtv/canvas-timeline';
import { formatMediabunnyTime } from '@techsquidtv/canvas-timeline-mediabunny-adapter';
import { useMediabunnyTimelineMedia } from '@techsquidtv/canvas-timeline-mediabunny-adapter/react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
demoMarkers,
demoTracks,
sampleDurationSeconds,
sampleMediaUrls,
sampleSourceId,
} from './timeline-demo-data';
import type { DemoMetrics } from '../demo-instrumentation';
import '@techsquidtv/canvas-timeline/styles.css';
// Demo configuration
const playbackRates = [0.5, 1, 2] as const;
const previewLayerSelectors = {
visuals: { trackKind: 'visual', sourceId: sampleSourceId },
audio: { trackKind: 'audio', sourceId: sampleSourceId },
} as const;
// Display helpers
function formatRenderedFrame(seconds: number | null) {
return seconds === null ? 'Pending' : formatMediabunnyTime(seconds);
}
// Timeline chrome
function TimelineLayers() {
const { state } = useTimeline();
return (
<>
<Timeline.PlayheadArea />
<Timeline.PlayheadGrabber />
<Timeline.TrackList className="timeline-track-list-overlay">
{state.tracks.map((track) => (
<Timeline.Track key={track.id} trackId={track.id} />
))}
</Timeline.TrackList>
<Timeline.ClipInteractionLayer />
<Timeline.RangeSelector />
</>
);
}
// Mediabunny preview and timeline sync
function MediaSyncSurface({ metrics }: { metrics?: DemoMetrics }) {
const playheadTime = useTimelinePlayheadTime();
const canvasRef = useRef<HTMLCanvasElement>(null);
const mediaLoadStartedAtRef = useRef(performance.now());
const decodeMetricReportedRef = useRef(false);
const [playbackError, setPlaybackError] = useState<string | null>(null);
// Adapter setup
const sources = useMemo(() => sampleMediaUrls.map((url) => ({ id: sampleSourceId, url })), []);
const {
ready,
status,
durationBySourceId,
lastFrameTime,
playing,
playbackRate,
play,
pause,
setPlaybackRate,
} = useMediabunnyTimelineMedia({
canvasRef,
sources,
layers: previewLayerSelectors,
onError: (message) => {
metrics?.onMediaLoadFailed?.({
demoId: 'media-sync',
adapter: 'mediabunny',
mediaType: 'video_audio',
});
setPlaybackError(message);
},
});
useEffect(() => {
if (!ready || decodeMetricReportedRef.current) {
return;
}
decodeMetricReportedRef.current = true;
metrics?.onMediaDecodeTime?.(
{
demoId: 'media-sync',
adapter: 'mediabunny',
mediaType: 'video_audio',
},
performance.now() - mediaLoadStartedAtRef.current
);
}, [metrics, ready]);
// Transport controls
const handlePlayPause = useCallback(async () => {
if (playing) {
pause();
setPlaybackError(null);
} else {
const result = await play();
setPlaybackError(result.ok ? null : result.message);
if (!result.ok) {
metrics?.onMediaLoadFailed?.({
demoId: 'media-sync',
adapter: 'mediabunny',
mediaType: 'video_audio',
});
}
}
}, [metrics, pause, play, playing]);
const mediaDuration = durationBySourceId.get(sampleSourceId) ?? null;
return (
<div className="media-sync-demo">
<div className="media-sync-preview">
<div className="media-sync-monitor">
<canvas ref={canvasRef} className="media-sync-canvas" width={1280} height={720} />
<button
type="button"
className="media-sync-button media-sync-play-button"
onClick={handlePlayPause}
disabled={!ready}
>
{playing ? 'Pause' : 'Play'}
</button>
</div>
<section className="media-sync-panel" aria-label="Media sync controls">
<h3>Mediabunny Adapter Sync</h3>
<p className="media-sync-status">{playbackError ?? status}</p>
<dl className="media-sync-readout">
<dt>Timeline position</dt>
<dd>{formatMediabunnyTime(toSeconds(playheadTime))}</dd>
<dt>Nearest frame</dt>
<dd>{formatRenderedFrame(lastFrameTime)}</dd>
<dt>Source duration</dt>
<dd>{mediaDuration === null ? 'Loading' : formatMediabunnyTime(mediaDuration)}</dd>
</dl>
<div className="media-sync-controls">
{playbackRates.map((rate) => (
<button
key={rate}
type="button"
className={`media-sync-button${
playbackRate === rate ? ' media-sync-button-active' : ''
}`}
onClick={() => setPlaybackRate(rate)}
disabled={!ready}
>
{rate}x
</button>
))}
</div>
</section>
</div>
<div className="timeline-shell">
<div className="timeline-stage">
<Timeline.Root className="timeline-fill">
<CanvasRenderer />
<TimelineLayers />
</Timeline.Root>
</div>
<div className="timeline-scrollbar-row">
<Timeline.ViewportScrollbar>
<Timeline.ViewportScrollbarThumb>
<Timeline.ViewportScrollbarHandle side="start" />
<Timeline.ViewportScrollbarHandle side="end" />
</Timeline.ViewportScrollbarThumb>
</Timeline.ViewportScrollbar>
</div>
</div>
</div>
);
}
// Demo entrypoint
export function MediaTimelineSync({ metrics }: { metrics?: DemoMetrics }) {
const engine = useMemo(
() =>
new TimelineEngine({
duration: fromSeconds(sampleDurationSeconds),
playheadTime: fromSeconds(0),
zoomScale: 12,
tracks: demoTracks,
markers: demoMarkers,
}),
[]
);
return (
<TimelineProvider engine={engine}>
<MediaSyncSurface metrics={metrics} />
</TimelineProvider>
);
}