Files
ArtPlayer/packages/artplayer-proxy-mediabunny/ARCHITECTURE.md

24 KiB

MediaBunny proxy maintenance

The public entry is src/index.ts: an optional-options factory returns the existing ArtPlayer proxy callback and an HTMLCanvasElement. Public declarations remain in types/artplayer-proxy-mediabunny.d.ts, with a separate ESM declaration entry. The exact optional factory and HTMLCanvasElement result remain unchanged. The media view in types/media.d.ts is opt-in and checked against the real VideoShim class.

Current module map

Module Responsibility
index.ts Optional factory, Canvas construction, alias installation and setup rollback
canvas-bridge.ts Historical own/direct-prototype forwarding and native Canvas receiver binding
entry-lifecycle.ts Entry resize/metadata/destroy subscriptions, alias ownership and shim cleanup
cleanup.ts Attempt every release and preserve the first failure
input.ts String/Blob/ReadableStream normalization, SDK Source/SourceRef pass-through, HLS detection and Input construction
media.ts Internal SDK-backed media and track-mode types; not public consumer types
tracks.ts Primary video and pairable audio selection, metadata/computed/live duration
hls-state.ts HLS track snapshots, bitrate/name/language values and actual selected track identity
preflight.ts Optional HEAD Range check, historical warning/error policy, cancellation and stale-result guard
load-session.ts Ownership of the pending/active SDK Input, HEAD AbortController, deferred load events and deadline
MediaBunnyEngine.ts Load/readiness coordinator, source identity, errors and terminal teardown
playback.ts Playback intent and cancellable play/seek/replacement operations
readiness.ts One metadata publication after both participants, guarded event sequences
hls-selection.ts Latest selection intent, stable-media pairing queries and active replacement errors
VideoEngine.ts Video capability/setup, source and seek ownership, first frames and terminal cleanup
video-frames.ts Shared pending iterator reads, idempotent iterator release and existing frame timing policy
video-renderer.ts Owned RAF, stale frame rejection, clock-driven draw/stall/end events
video-poster.ts Per-request image callbacks, source guards and handler cleanup
AudioEngine.ts Audio load/play generation, clock anchors and decoder-error forwarding
audio-clock.ts Existing media-clock and future/partially-late buffer scheduling formulas
audio-context.ts Context/gain setup, sample-rate fallback, shared native resume and terminal close
audio-nodes.ts Scheduled source nodes, rate, ended handlers and complete stop/disconnect
audio-pump.ts SDK buffer iterator, bounded batches, backpressure and stale-result guards
audio-task.ts Cancelable waits and owned batch/backpressure timers
VideoShim.ts / EventTarget.ts Video-like descriptors, synchronous/Promise forwarding and owned event lifecycle
shim-values.ts Existing volume coercion and synthetic TimeRanges values
shim-frames.ts Synthetic frame callbacks, per-instance RAF ownership and terminal cleanup
engine-types.ts / engine-ports.ts Internal shim, coordinator and decoder interfaces
m3u8.ts HLS refresh/selection revisions, source guards, events and terminal cleanup
m3u8-model.ts Display names, selected IDs, label deduplication and sorted selector models
m3u8-menu.ts Owned control/setting surfaces and stale callback guards
m3u8-types.ts Narrow internal UI/shim contracts without changing public factory declarations

The thirty-four production TypeScript modules use strict checking with skipLibCheck: false and no ambient Node types. The coordinator and both decoder engines are checked implementations; there are no remaining adjacent JavaScript declaration bridges. All owned production JavaScript has been migrated. PKG-MB-08 also checks public consumer compatibility and distinguishes supported partial media from wholly undecodable inputs.

The entry preserves native Canvas method priority, detached createElement invocation, two reads of enumerable native method accessors, direct-prototype forwarding, bound shim methods and strict assignment errors for getter-only properties. Avoid replacing these with generic Reflect forwarding or a broad HTMLVideoElement cast. Entry bookkeeping stays off the canvas surface. Destroy unregisters its callbacks and only removes art.mediabunny when the alias still belongs to that entry. A custom historical host without off keeps inert callbacks; a real Artplayer host removes them. Partial setup attempts all cleanup while preserving the original setup error. HLS cleanup cannot strand the remaining entry resources.

The shim keeps its existing own-property order and direct prototype surface because the entry copies those descriptors onto the canvas. Resource bookkeeping lives in a WeakMap rather than adding accidental canvas properties. Frame callback IDs include zero; destruction cancels pending callbacks and closes scheduling. The callback metadata remains synthetic (presentedFrames: 0), not a claim of decoded frame accuracy.

EventTarget retains duplicate listeners, first-match removal, live-array forEach mutation semantics, detail identity and listener exceptions during normal dispatch. Its terminal state stops callbacks later in a dispatch when a listener destroys the shim. Shim teardown closes events in finally, even if engine cleanup throws, and subsequent teardown is inert. Engine teardown attempts input, audio and video cleanup even when an earlier release throws, then preserves the first failure.

Input and cancellation flow

Each engine.load invalidates the previous generation and cancels its session. The new session owns an Input immediately after construction, before asynchronous track selection. engine.input and engine.media become visible only after selection. The session continues to own the Input during playback; these fields are aliases, not separate disposal owners. Repeated cancellation/disposal is harmless.

Source replacement, timeout and destroy release that Input through SDK Input.dispose(). MediaBunny controls its media fetch cancellation; do not pass a signal in UrlSource's requestInit (the SDK explicitly excludes it). The separate HEAD check uses its own AbortController. Without that constructor, stale HEAD results/errors are still ignored, although the network request cannot be physically aborted by this adapter.

Cancellation settles the old load Promise without waiting for a stalled read. Its late fulfillment/rejection remains observed but cannot update the current load. Active errors retain code: 4 and the SDK message; active timeout retains Load timeout and networkState 3. Successful completion clears its deadline. Normal deferred waiting/loadstart ordering remains; cancellation clears both callbacks. Destroy makes future loads inert.

Range behavior remains opt-in: no/none accept-ranges emits an error with a RangeNotSupported detail and returns false, without setting the engine error property. Network failure warns and continues; HLS strings and non-string sources skip HEAD. HLS matching preserves the old case-insensitive .m3u8 suffix/query/hash rule. One explicit InputOptions assertion leaves unsupported values to SDK validation, preserving the old error owner rather than silently converting arbitrary inputs.

Playback and readiness ownership

Playback intent is separate from the temporary paused state during seek. Explicit pause cancels pending play and prevents automatic resume; a newer seek settles the old operation without allowing its completion or rejection to overwrite current state. Pending play waits for the seek completion signal, resolved before optional resume to avoid a cycle. A pending AudioContext resume is shared across play-pause-play; only the current operation starts video and emits play/playing. Source change and destroy invalidate both operations. Cancellation settles the public operation while observing the underlying late result; video and audio ownership are detailed below. Native resume coalescing belongs to the context owner: each new playback intent calls audio.play(), but shares the same pending native resume. Sharing a canceled upper-level play Promise would incorrectly start video before the latest audio intent is active.

Normal play, pause, seek/resume and HLS replacement event order is retained. Before each event the coordinator rechecks ownership, since a listener may synchronously pause, seek, load another source or destroy. Active play errors reject their Promise and restore paused state; active seek errors additionally publish one media error. The synchronous currentTime setter observes its otherwise unreturnable rejection; direct engine.seek still rejects. Active replacement errors remain visible even after the media object changes. HLS queries and replacement errors also carry a selection intent; obsolete intents cannot replace newer results or surface an old failure. A still-current query retries if its media snapshot changed.

Metadata publishes once after both audio/video participants report; repeated or obsolete callbacks cannot ready the current source. A parsed container with neither track fails with code 4 and message Input has no audio or video tracks. before decoder/AudioContext setup. This corrects the old false-ready state; normal valid input event order is preserved. Existing-track unsupported-codec handling remains a decoder capability task, not evidence that every capability failure has been fixed.

Video frame ownership

VideoEngine keeps source/load generation separate from seek iteration and renderer activity. A new source calls cancelPending before track selection starts, so an old seek cannot draw while the new source is still parsing. Seek during capability detection waits for that load without canceling its metadata; only the latest seek may install or draw its frames. Ordinary paused seek still displays its first target frame and queues the second.

Iterator detachment is synchronous. The old return Promise is observed without blocking a new load on obsolete reads; repeated release calls return the same cleanup Promise. Late completion cannot clear the current iterator or its fetching flag. Stop/start shares an already pending next() for the same iterator instead of issuing concurrent reads. Pause invalidates render work; it does not cancel a legitimate paused seek's target frame.

Each seek after the initial iterator creates a fresh CanvasSink with its own two-canvas pool. Concurrent obsolete decoders can otherwise overwrite the pixels of a queued frame even when the proxy discards their returned objects. Sink identity is internal and may change on seek; public canvas identity stays stable. Tests intercept sink prototypes to follow new instances. CanvasSink retains poolSize 2, contain fit and track transparency. Its implementation closes VideoSample after drawing into the pooled canvas. The proxy releases iterators and references; WrappedCanvas is not a VideoFrame and must not receive close(). Default dropLateFrames remains false, with the same tolerance/rate formula when enabled. Start preserves the queued second frame until it is consumed instead of replacing it through premature prefetch. There is no added frame copy.

Each renderer owns one RAF, including ID zero. Stop/destroy and event-listener reentry make queued callbacks inert; an instance that never scheduled a frame cannot cancel another instance's frame. Normal timeupdate, stall recovery and ended ordering is preserved, while reentrant pause/source/destroy/restart stops the obsolete event sequence. Poster image callbacks have source and request identity and are detached on replacement, draw or destroy.

Background decoder failure stops rendering and reports one media error through the coordinator, even if audio cleanup fails. Iterator cleanup failures are observed and warned; explicit stopIterator still rejects. Missing 2D context fails at use with a descriptive error, rather than becoming falsely ready. Existing but undecodable tracks may use the historical no-video/no-audio fallback only when at least one selected decoder is usable.

readiness.requireDecoder checks the actual audio/video sinks after their existing SDK capability checks. It does not issue duplicate canDecode queries or change decoder load return values. The metadata barrier checks before publishing metadata, and the load completion checks before publishing data/canplay. Track replacement checks after decoder setup and before seeking or publishing its success sequence. Without either sink, readyState returns to zero and the existing code-4 error channel reports Input has no decodable audio or video tracks. Active SDK query errors retain their own messages. Valid audio-only/video-only and mixed inputs with one usable track retain normal readiness/playback; canceled queries cannot report against a newer source or destroyed player.

Audio clock and resource ownership

AudioEngine keeps source generation separate from pump IDs and cancellable play intent. Pause invalidates a suspended resume even when the audio clock has not started; source replacement invalidates audio before track parsing. Late resume, capability checks and buffer reads cannot install a sink, schedule nodes or resurrect a destroyed instance. Native AudioContext.resume is coalesced per context, while canceled callers settle early and late rejections stay observed. A valid no-audio-track source retains the Web Audio clock fallback; this is distinct from rejecting a container with neither track.

The clock retains the existing anchor/rate formula and squared gain with mute. Pause captures media time; seek/rate changes cancel the old iterator and stop/disconnect its scheduled nodes before rebuilding the queue at the new anchor. The renderer's ended event freezes the audio clock and releases remaining nodes. Source load resets position. Fully expired buffers do not allocate nodes; partially late buffers retain the old offset calculation. Audio playbackRate changes still change pitch through native source playbackRate; no new pitch-preservation or acoustic synchronization guarantee is introduced.

AudioPump owns 16-buffer batches, a zero-delay yield and the existing one-second/50ms backpressure policy. Its timers include ID zero and are canceled with its task. Iterator detachment is synchronous and return is observed once; obsolete cleanup cannot clear a new iterator. Native AudioBuffers have no close() contract: the SDK closes its AudioSamples, while the proxy releases buffer/node/iterator references. Every scheduled source is owned before start, cleared on ended, stopped/disconnected on cancellation, and has its callback removed. Cleanup attempts all nodes and context resources before throwing the first error; background cleanup failures are observed, and do not replace an active decoder failure.

ContextOwner installs context/gain references only after successful setup. Setup failure releases partial resources, destroy nulls references and observes the asynchronous close, and repeated destroy is inert. Real-browser teardown assertions poll the native closed state. Tests deliberately delay native resume completion or decoded buffer delivery, without substituting fake audio decoding for native acceptance.

HLS selection and UI ownership

Quality and audio requests share a per-engine intent token. A newer request supersedes an unresolved older query, including cross-kind requests, before it can call replaceTracks. Queries capture source generation and Input identity. If a prior replacement installs new media while the latest query is awaiting track metadata or pairing, recompute against the current media instead of dropping the latest intent or restoring an old counterpart. The resolver rechecks cancellation between SDK calls; active failures still reject the caller. An already-started replacement remains governed by Playback until the next replacement begins. This does not claim that issuing a selection synchronously cancels SDK decoding. Pairing preserves the current compatible counterpart and its mode; fallback chooses the SDK primary pairable track and resets only the counterpart mode to auto. Invalid IDs retain the historical current-track fallback. Media state getters suppress stale source results.

The UI has separate refresh revisions, selection intent and source identity. Old reads cannot overwrite newer models; callbacks from replaced menus or a previous source are inert. The same visible menu accepts a newer click while a prior selection is pending. Its own loadedmetadata refresh does not cancel that valid selection's completion. Active failures refresh actual track highlights before rejecting; stale failures, including a failure superseded during recovery, cannot disturb the current intent. Background event-driven refreshes observe failures through a warning instead of unhandled Promise rejections.

Models retain the existing menu names, title/Auto/getName defaults, sort order and notice format. Auto remains an action, while the actual selected track ID controls the highlighted item and displayed track label. Label deduplication is retained; if labels collide, keep the selected ID as that label's representative so it is not hidden by the first item. Height lookups use a map rather than repeated array searches during sorting. User getName callbacks are guarded against synchronous source/destroy reentry between calls and before UI writes.

Each menu owns control and setting independently. Disabling either removes that surface; video-only/single-audio topology removes audio menus, non-HLS clears both, and loadstart/error/ destroy invalidate pending work and clear owned UI. Teardown attempts every surface and listener even after one failure, and cannot prevent the entry's shim destroy callback. Control/setting callbacks retain their Promise and selected-label result; no new public methods or declarations are exposed by the internal modules.

Types and dependencies

The locked runtime SDK remains MediaBunny 1.56.1. Its pinned WebCodecs declaration 0.1.13 duplicates types now present in TypeScript 5.9.3's DOM library. The root Yarn resolution selects @types/dom-webcodecs@0.1.19 for both dependency paths; the SDK itself is unchanged. This is a build-time declaration correction, not a browser polyfill. The package typecheck adds ESNext.Disposable for SDK declarations while keeping the ES2020 target; production source does not use using or require Symbol.dispose. Public declarations do not import SDK types or modern DOM VideoFrame callback names. The optional media view names the synthetic frame metadata directly; SDK track objects are unknown until consumers narrow them using their own SDK version. VideoShim implements MediaBunnyShim checks that this view matches implementation rather than masking JS with an adjacent declaration. Explicit getter return types retain coercive unknown setters internally while giving consumers numeric/boolean reads.

Default Result remains exactly HTMLCanvasElement, allowing callers to substitute a plain canvas initializer or an entire historical factory. MediaBunnyCanvas combines the opt-in media view with native Canvas methods winning collisions. MediaBunnyPlayer has an optional mediabunny alias because it is absent before initialization and after destruction; there is no global augmentation of Artplayer. Named exports are types only.

The .d.mts wrapper fixes NodeNext ESM default resolution. CommonJS keeps the historical default declaration facade, and typesVersions resolves /legacy in TS 4.3/classic Node resolution. Runtime remains directly callable and adds a nonenumerable, writable default self alias for the 1.0.0 CommonJS usage. That property is deliberately not mandatory on the default factory type. Whole-factory assignments are tested bidirectionally against both frozen published declarations, in an actual isolated install as well as workspace consumers. Do not switch to export= or a required default member without those checks.

canPlayType() still always returns maybe; buffered/played/seekable and RAF metadata remain synthetic. None is proof that a codec or browser can play the input. Missing Web Audio or WebCodecs in Windows WebKit remains an unsupported-capability control. Existing but undecodable tracks no longer produce clock-only false readiness when all selected decoders are unusable. Partial playback retains the usable decoder. Native/long-play/device acceptance remains MB-09; no synthetic capability probe proves support on a physical Safari device.

Validation and remaining work

yarn test:mediabunny
yarn test:mediabunny-types-package
node node_modules/typescript/bin/tsc -p packages/artplayer-proxy-mediabunny/tsconfig.json
yarn build artplayer-proxy-mediabunny
yarn test:browser test/browser/mediabunny-inputs.spec.js test/browser/mediabunny.spec.js test/browser/mediabunny-load.spec.js
yarn test:browser test/browser/mediabunny-shim.spec.js
yarn test:browser test/browser/mediabunny-video.spec.js
yarn test:browser test/browser/mediabunny-audio.spec.js
yarn test:browser test/browser/mediabunny-hls.spec.js
yarn test:browser test/browser/mediabunny-entry.spec.js
yarn test:browser test/browser/mediabunny-capability.spec.js

Node tests use actual SDK parsing and controlled lifecycle interleavings. Browser tests include native decoding and separate pending ReadableStream cancellation with real timers. ARTPLAYER_MB_BASELINE=1 runs candidate load assertions against the immutable old main to reproduce failures. ARTPLAYER_MB_ARTIFACT selects a built UMD main/legacy bundle; ARTPLAYER_MB_BROWSER_CANDIDATE=1 restricts browser runs to that candidate. Browser reports record the implementation hash; archive the report and results before rerunning.

The source of historical truth is refactor/baselines/mb-release.json and its Git/npm inputs, not a rebuilt dist. Package-specific evidence and remaining tasks are in refactor/mb-validation.md. The full refactor plan is at the repository root.

PKG-MB-04 covers duplicate/trackless readiness, reentrant event sequences and pending play/seek coordination. PKG-MB-05/06 cover late frame and audio callbacks, including operations already inside decoders at cancellation. These bounded cases do not replace long-run synchronization and combination validation. PKG-MB-07 covers HLS UI/selection races using the actual new/old cores and SDK with controlled query ordering. PKG-MB-09 owns long-run AV sync, full core/proxy/plugin combinations and device validation. PKG-MB-10 owns the demo, installed package and MPL/source notice gate. Windows WebKit without WebCodecs/Web Audio is recorded as a capability failure, not successful playback or a claim about all Safari installations.

Installed browser validation

Run yarn test:package --browser, then pass its browser-artifacts.json as ARTPLAYER_BROWSER_ARTIFACTS to yarn test:browser:installed mediabunny. The shared roster includes this proxy and Document PiP. All ten MediaBunny browser files use the verified installed UMD, including its bundled SDK; they never silently rebuild a candidate when an installed map exists. Baseline and artifact overrides are rejected in installed mode; its runner also rejects the candidate-only flag so historical controls stay present. Source mode retains these explicit diagnostics.

Available-input attachments are an inventory, not proof that every listed input ran. Each observation identifies the selected implementation and its provenance; PiP combinations identify both packages. Local MP4/WebM/HLS fixtures remain test media, not npm assets. Actual decoding, controlled races/unsupported decoder branches, missing browser APIs and physical device evidence stay distinct. This CI coverage does not close PKG-MB-09/10 or establish long-run/acoustic acceptance.