Documentation

Media adapter migration

Migrate HTML and Mediabunny integrations to resolved sources, input fallbacks, shared playback ranges, and runtime diagnostics.

Browse documentation

Mediabunny 1.56.0 minimum

BREAKING: The Mediabunny adapter now requires mediabunny@^1.56.0. Upgrade the peer dependency alongside Canvas Timeline to enable background read error recovery for adapter-created URL and Blob sources:

Terminal window
pnpm add mediabunny@^1.56.0

Custom urlSourceOptions.handleUnhandledError callbacks still receive the original error, alongside the adapter’s automatic recovery. Supplied inputs and input factories retain responsibility for configuring their source callbacks.

Breaking change warning

The media adapters now accept one app-resolved media choice for each logical timeline source. The old flat descriptors, duplicate sourceId fallback convention, adapter-owned original/proxy selection, durationBySourceId, and blocking resumeClock() contract have been removed. There are no compatibility aliases.

For the finalized API rather than the old-to-new mapping, read the current Media adapters guide.

Media contracts such as TimelineMediaSyncAdapter, TimelineMediaSourceTiming, and TimelineMediaError now come from the framework-free core package. HTML React hooks moved to the adapter’s ./react export, matching the Mediabunny package boundary.

import { useHTMLTimelineMedia } from '@techsquidtv/canvas-timeline-html-media-adapter/react';
import type { HTMLMediaSourceInput } from '@techsquidtv/canvas-timeline-html-media-adapter';

HTMLMediaAdapterSource was removed; use HTMLMediaSourceInput. The adapter package root is imperative and no longer exports React hooks.

The common case stays concise: { sourceId, input }. Use fallbacks only for equivalent transport or cache alternatives. Keep original, proxy, preview, and export policy in the application’s media library.

Migrate HTML media sources

Replace the source record with an array of logical sources. Put the normal URL, blob, or file in input and optional load failovers in fallbacks.

const sources = [
{
sourceId: 'intro',
input: cdnUrl,
fallbacks: [fallbackFile],
},
];
const media = useHTMLTimelineMedia({
sources,
layers,
});
return <video ref={media.mediaRef} />;

Read sourceStateById to inspect lifecycle status, selectedInputIndex, and ordered attempts. Use media.adapter.retrySource(sourceId) to retry inputs or media.adapter.replaceSource(source) to replace the resolved media without recreating the adapter. Both commands return an explicit configured or ready success state instead of a boolean.

useHTMLTimelineMedia().ready now means its returned mediaRef connected a native element. The hook owns that callback ref; passing an object ref in its options is no longer supported. It does not claim that active source metadata has loaded; inspect sourceStateById.get(sourceId)?.status for that lifecycle.

React hooks reconcile string sources by value, so an inline array of ordinary URL descriptors no longer disposes and recreates the adapter on every render. Keep Blob and File objects stable when they represent the same local media.

Migrate Mediabunny sources

The high-level hook now owns and returns canvasRef; attach it directly to the preview canvas. The low-level useMediabunnyAdapter accepts the resolved canvas element instead of an object ref.

const media = useMediabunnyTimelineMedia({ sources, layers });
return <canvas ref={media.canvasRef} />;

Pass a normal URL string, URL, Request, Blob, or File directly as input. Use an object descriptor when you need formats, UrlSource options, a supplied input, or an input factory. Put equivalent direct-media failover in fallbacks.

import { HLS_FORMATS } from 'mediabunny';
const sources = [
{
sourceId: 'camera-a',
input: {
kind: 'url',
url: manifestUrl,
formats: HLS_FORMATS,
urlSourceOptions: { maxCacheSize: 32 * 1024 * 1024 },
},
fallbacks: [
{
kind: 'url',
url: mp4Url,
},
],
},
];

Use selectTracks when primary tracks are not the desired video or audio tracks. Source state now includes detected timing, dimensions, frame rate, and audio metadata alongside input-attempt diagnostics.

Ordinary URL descriptors are also reconciled by value. Keep Request, Blob, File, input factories, track selectors, custom URL option objects, and other callback configuration stable because their identity represents a resource or executable policy.

Mediabunny sources are registered immediately and loaded only when their clips become active. ready therefore means the adapter can resolve configured media; it does not mean every project asset has been opened. Use preloadSource(sourceId) for likely next clips and unloadSource(sourceId) to release decoder resources while keeping the definition registered.

Model editing proxies in the media library

A proxy is an application-level media relationship, not a decoder fallback. An editor may select it for scrubbing while retaining the original for high-quality preview, conform, relink, and export. Model those purposes in media-library state and resolve one adapter source from the current editing policy.

const cameraA = {
sourceId: 'camera-a',
original: { input: originalFile },
proxies: {
editing: {
input: proxyUrl,
fallbacks: [cachedProxy],
timing: {
sourceTimeSeconds: cameraTimestamp,
mediaTimeSeconds: 0,
},
},
},
} as const;
function resolvePreviewSource(useProxy: boolean) {
const resolved = useProxy ? cameraA.proxies.editing : cameraA.original;
return { sourceId: cameraA.sourceId, ...resolved };
}
await media.adapter.replaceSource(resolvePreviewSource(true));

The adapter does not need to know why that media was chosen. The timing anchor maps logical source time to the resolved media time and is needed only when a proxy or transcode uses a different container timestamp origin. Switching back uses replaceSource(resolvePreviewSource(false)); timeline clips keep the same sourceId throughout.

This keeps policy extensible without changing adapter types: an application can also resolve offline cache entries, HLS/direct delivery, color-managed review media, remote collaboration previews, or export originals through the same source boundary.

Update transport and audio controls

Pass the same playbackOptions used by engine playback when an external media clock drives the timeline. External ticks now enforce in/out boundaries and looping through the engine’s shared playback policy.

Clock activation is intentionally non-blocking. The high-level hooks request it when transport starts; imperative integrations can call requestClockActivation() directly. Observe audio activation state separately from video transport. Use setVolume() and setMuted() for runtime gain changes; neither operation reloads sources.

Caller-supplied AudioContext instances remain caller-owned. The Mediabunny adapter closes only contexts it creates, and it does not create an audio graph for sources without a decodable audio track. Audio activation requests made before lazy loading are deferred until a selected decodable audio track exists; video-only sources do not resume a supplied context.

MediabunnyAdapter now implements TimelineMediaSyncAdapter directly. Pass the adapter itself to useTimelineMediaSync(); the former nested syncAdapter facade is removed without an alias. The adapter transport clock is no longer owned by an active source controller, so lazy source changes and runtime input recovery preserve the running timeline position.

Error handlers now receive TimelineMediaError with a stable reason and a human-readable message:

useMediabunnyTimelineMedia({
sources,
layers,
onError: (error) => reportPreviewFailure(error.reason, error.message),
});

This replaces string-only onError callbacks. Play still returns a discriminated TimelineMediaPlayResult for command-local handling. External rendering and scheduling failures use sync-failed, including rejected async iterator work, and pause both the media clock and timeline.

Low-level media play() and media-aware setPlaybackRate() commands are now asynchronous because the adapter synchronization contract permits promises. Await their command results when product UI needs to surface startup, decoder, or scheduling failures. Paused preview seeks, startup, external ticks, loop realignment, pause cleanup, and rate changes are ordered through adapter operation queues, so an older decoder operation cannot apply after a newer timeline position. Concurrent high-level play requests share one startup. Calling pause() during startup resolves that pending play result with reason: 'cancelled'; cancellation is a normal transport outcome and is not delivered to onError.

Packaged hooks coordinate adapter replacement automatically. Custom useTimelineMediaSync() integrations should pass the underlying controller as adapterIdentity when replacing it must cancel pending startup, stop its owned clock, and prime the new paused preview. Omit adapterIdentity for an inline callback facade; it may be recreated on render without useMemo.

Native HTML startup now keeps one play command attached to equivalent input fallbacks. When the preferred input fails during initial play(), the command resolves from the selected fallback instead of failing while fallback playback continues in a detached callback.

At the low-level useTimelineMediaPlayback boundary, provide loop as the clock-realignment callback. Its presence enables looping, so an external clock cannot be configured to loop without a way to seek back to the range start.