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:
pnpm add mediabunny@^1.56.0Custom 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.