demuxe 0.3.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/LICENSE +352 -0
  2. package/README.md +75 -0
  3. package/bin/demuxe.mjs +30 -0
  4. package/docs/API-MIGRATION.md +54 -0
  5. package/docs/BETA.md +98 -0
  6. package/docs/BRANDING-MIGRATION.md +77 -0
  7. package/docs/COMPATIBILITY-EXPANSION.md +181 -0
  8. package/docs/LICENSING.md +83 -0
  9. package/docs/OPTIMIZATION-COMPLETION.md +511 -0
  10. package/docs/OPTIMIZATION-FLAC.md +178 -0
  11. package/docs/OPTIMIZATION-INTEGRATION.md +221 -0
  12. package/docs/OPTIMIZATION-REVIEW-FIXES.md +78 -0
  13. package/docs/PLAYER-COMPONENT.md +247 -0
  14. package/docs/PUBLIC-API-VALIDATION.md +163 -0
  15. package/docs/PUBLIC-API.md +245 -0
  16. package/docs/RELEASE.md +181 -0
  17. package/docs/RUNTIME-ASSETS.md +127 -0
  18. package/engine-build.json +8781 -0
  19. package/examples/custom-controls.html +14 -0
  20. package/examples/player-element.html +5 -0
  21. package/fixtures/DejaVuSans.ttf +0 -0
  22. package/fixtures/FONT-LICENSE.txt +187 -0
  23. package/index.d.ts +1 -0
  24. package/index.js +1 -0
  25. package/package.json +46 -0
  26. package/player.d.ts +1 -0
  27. package/player.js +1 -0
  28. package/release-manifest.json +541 -0
  29. package/sources.lock.json +96 -0
  30. package/third_party/notices/dav1d/COPYING +23 -0
  31. package/third_party/notices/emscripten/LICENSE +102 -0
  32. package/third_party/notices/emscripten/system/lib/compiler-rt/LICENSE.TXT +311 -0
  33. package/third_party/notices/emscripten/system/lib/libc/musl/COPYRIGHT +193 -0
  34. package/third_party/notices/emscripten/system/lib/libcxx/LICENSE.TXT +311 -0
  35. package/third_party/notices/emscripten/system/lib/libcxxabi/LICENSE.TXT +311 -0
  36. package/third_party/notices/emsdk/LICENSE +27 -0
  37. package/third_party/notices/ffmpeg/COPYING.GPLv2 +339 -0
  38. package/third_party/notices/ffmpeg/COPYING.GPLv3 +674 -0
  39. package/third_party/notices/ffmpeg/COPYING.LGPLv2.1 +502 -0
  40. package/third_party/notices/ffmpeg/LICENSE.md +129 -0
  41. package/third_party/notices/ffmpeg-adaptation/COPYING.LGPLv2.1 +502 -0
  42. package/third_party/notices/ffmpeg-adaptation/LICENSE.md +127 -0
  43. package/third_party/notices/freetype/LICENSE.TXT +46 -0
  44. package/third_party/notices/freetype/docs/FTL.TXT +169 -0
  45. package/third_party/notices/freetype/docs/GPLv2.TXT +340 -0
  46. package/third_party/notices/fribidi/COPYING +504 -0
  47. package/third_party/notices/harfbuzz/COPYING +42 -0
  48. package/third_party/notices/libass/COPYING +15 -0
  49. package/third_party/notices/libplacebo/LICENSE +458 -0
  50. package/third_party/notices/libxml2/Copyright +23 -0
  51. package/third_party/notices/libxml2/dict.c.notice +17 -0
  52. package/third_party/notices/libxml2/list.c.notice +16 -0
  53. package/third_party/notices/mpv/Copyright +78 -0
  54. package/third_party/notices/mpv/LICENSE.GPL +338 -0
  55. package/third_party/notices/mpv/LICENSE.LGPL +501 -0
  56. package/third_party/notices/vulkan-headers/LICENSE.txt +202 -0
  57. package/third_party/notices/zimg/COPYING +14 -0
  58. package/third_party/notices/zlib/LICENSE +22 -0
  59. package/third_party/notices.json +166 -0
  60. package/toolchain.lock.json +204 -0
  61. package/web/audio-worklet.js +45 -0
  62. package/web/cheap-mp4-probe.js +82 -0
  63. package/web/engine-adaptation/manifest.json +75 -0
  64. package/web/engine-adaptation/remux.mjs +2 -0
  65. package/web/engine-adaptation/remux.wasm +0 -0
  66. package/web/engine-ass/manifest.json +55 -0
  67. package/web/engine-ass/subtitles.mjs +2 -0
  68. package/web/engine-ass/subtitles.wasm +0 -0
  69. package/web/engine-hybrid/player.mjs +2 -0
  70. package/web/engine-hybrid/player.wasm +0 -0
  71. package/web/engine-remux/remux.mjs +2 -0
  72. package/web/engine-remux/remux.wasm +0 -0
  73. package/web/engine-software-full/player.mjs +2 -0
  74. package/web/engine-software-full/player.wasm +0 -0
  75. package/web/file-reader.js +34 -0
  76. package/web/filter-retained-engine-worker.js +292 -0
  77. package/web/generated/index.d.ts +6 -0
  78. package/web/generated/index.js +4 -0
  79. package/web/generated/internal/assets.d.ts +2 -0
  80. package/web/generated/internal/assets.js +7 -0
  81. package/web/generated/internal/backend.d.ts +27 -0
  82. package/web/generated/internal/backend.js +1 -0
  83. package/web/generated/internal/errors.d.ts +13 -0
  84. package/web/generated/internal/errors.js +54 -0
  85. package/web/generated/internal/native-ass.d.ts +38 -0
  86. package/web/generated/internal/native-ass.js +206 -0
  87. package/web/generated/internal/native-player.d.ts +95 -0
  88. package/web/generated/internal/native-player.js +502 -0
  89. package/web/generated/internal/playback-plans.d.ts +526 -0
  90. package/web/generated/internal/playback-plans.js +110 -0
  91. package/web/generated/internal/selection.d.ts +45 -0
  92. package/web/generated/internal/selection.js +45 -0
  93. package/web/generated/internal/state.d.ts +8 -0
  94. package/web/generated/internal/state.js +56 -0
  95. package/web/generated/internal/wasm-player.d.ts +127 -0
  96. package/web/generated/internal/wasm-player.js +372 -0
  97. package/web/generated/player/index.d.ts +198 -0
  98. package/web/generated/player/index.js +873 -0
  99. package/web/generated/player/interaction.d.ts +6 -0
  100. package/web/generated/player/interaction.js +14 -0
  101. package/web/generated/player/styles.d.ts +1 -0
  102. package/web/generated/player/styles.js +86 -0
  103. package/web/generated/types.d.ts +227 -0
  104. package/web/generated/types.js +2 -0
  105. package/web/generated/unified-player.d.ts +129 -0
  106. package/web/generated/unified-player.js +1074 -0
  107. package/web/io-worker.js +65 -0
  108. package/web/native-ass-worker.js +26 -0
  109. package/web/native-remux-player.js +301 -0
  110. package/web/native-remux-source-worker.js +25 -0
  111. package/web/native-remux-worker.js +46 -0
  112. package/web/range-reader.js +130 -0
  113. package/web/remux-packaging.js +11 -0
  114. package/web/resource-loader.js +173 -0
  115. package/web/retained-decoder-worker.js +200 -0
  116. package/web/retained-video.js +15 -0
  117. package/web/segmented-subtitles.js +37 -0
  118. package/web/software-full-engine-worker.js +234 -0
  119. package/web/source-probe.js +28 -0
  120. package/web/split-mp4.js +159 -0
  121. package/web/streaming-manifest.js +136 -0
  122. package/web/subtitle-overlay.js +26 -0
  123. package/web/video-codec-config.js +59 -0
  124. package/web/vod-manifest.js +54 -0
@@ -0,0 +1,247 @@
1
+ # Optional player component
2
+
3
+ This is the target contract; qualification is recorded separately. Import
4
+ `definePlayerElement` from `demuxe/player` and call it once (repeat calls with the
5
+ same implementation are harmless). Core import does not import UI or register a
6
+ tag. SSR imports are safe; registration and construction require a browser.
7
+
8
+ `<demuxe-player controls asset-base="/assets/demuxe/" poster="/preview.jpg">`
9
+ creates one Player on connection. ready resolves with that core; player exposes it
10
+ read-only after initialization. open, close, destroy and playback conveniences
11
+ delegate to it. The component contains no route selector or playback scheduler.
12
+
13
+ src is a remote URL. Changing it cancels a previous pending source open; only the
14
+ latest accepted change wins. Removing src closes media. Programmatic open accepts
15
+ all core source types. autoplay requests play only after acceptance and reports
16
+ normal browser policy rejection without fallback. muted reflects configured mute;
17
+ controls toggles controls and closes open settings/stats overlays when disabled;
18
+ poster is an idle/loading preview. asset-base is fixed
19
+ after initialization and rejects changes. Set it before connection. Pre-upgrade
20
+ properties are replayed on upgrade. Boolean attributes follow HTML presence rules.
21
+
22
+ A microtask grace period preserves playback during synchronous DOM moves. Actual
23
+ removal aborts work and destroys the owned core; reconnect waits for that cleanup
24
+ before creating the next core. Explicit destroy is terminal, including reinsertion.
25
+ ready waits for connection; destroy before connection rejects it.
26
+
27
+ The open shadow root includes stage, controls, settings and status parts, CSS
28
+ public Demuxe theme variables, and before-controls /
29
+ after-controls slots. Labels can be overridden before or after connection.
30
+ Controls use semantic buttons/ranges/selects, visible focus, scoped keyboard
31
+ shortcuts, local drag preview, and an aria-live status that excludes time updates.
32
+ Settings restore focus on close. Fullscreen requests the component container in
33
+ the user gesture, keeping controls and subtitles together. PiP/casting are not
34
+ qualified. Loading, live windows and errors use core state only.
35
+
36
+ Forwarded core events are dispatched once with unchanged detail, bubbles:false;
37
+ listen directly on the element. No event-name aliases are generated. Component
38
+ lifecycle failures use error with a structured operation-scoped detail.
39
+
40
+ ## Multiple files and queue
41
+
42
+ The component's media picker accepts multiple files. Selecting or dropping files
43
+ adds them in the supplied order, preserving duplicate filenames. If nothing is
44
+ queued, the first item opens; otherwise files append without interrupting playback.
45
+ The initial item follows `autoplay` (paused by default).
46
+
47
+ The folder menu shows the queue, with the current item highlighted. Select a file
48
+ to switch, remove individual items, or use **Clear queue** to stop and release all
49
+ queued sources. Removing the current item opens the next available item, or the
50
+ previous one if it was last. If that replacement fails, the removed media is
51
+ closed and the remaining queue stays available for retry or selection. Removing
52
+ other items leaves playback untouched.
53
+
54
+ Previous/next buttons and an item count appear beside the timeline controls when
55
+ there is more than one item. Manual changes preserve play/pause intent; an explicit
56
+ play or pause during loading overrides the intent captured when switching. Reaching
57
+ the end advances and plays the next file; the final item stops without looping.
58
+ An opening failure stops on that item and uses the existing error/retry UI;
59
+ it does not silently skip through the queue.
60
+
61
+ `element.open(source)`, `src` changes, and URL submissions replace the queue with
62
+ one source. `close()`, destruction and actual disconnection clear the queue and
63
+ release its file references. Synchronous DOM moves keep it. No queued item is
64
+ preloaded, and all opens delegate to the same core `Player`.
65
+
66
+ `showSourceControls = false` hides and disables queue management along with file
67
+ input. Previous/next playback controls still work. `allowFileDrop` independently
68
+ controls file drops. Files in a batch are treated as media; use the separate
69
+ subtitle picker to attach subtitles.
70
+
71
+ The queue is built-in UI state, not a second core playback API. Applications
72
+ needing their own playlist policies can use the core `Player` and manage sources
73
+ themselves.
74
+
75
+ ## Customization
76
+
77
+ ```html
78
+ <demuxe-player
79
+ controls
80
+ title="Movie Night"
81
+ asset-base="/assets/demuxe/">
82
+ </demuxe-player>
83
+ ```
84
+
85
+ The standard HTML `title` property/attribute updates the displayed title live.
86
+ `titleMode` (also `title-mode`) has four policies:
87
+
88
+ * `auto` (default): a nonempty custom `title`, otherwise the accepted source
89
+ filename, otherwise nothing.
90
+ * `custom`: only `title`; an empty string hides it.
91
+ * `source`: only the accepted source filename, even when a custom title exists.
92
+ * `none`: hide the title.
93
+
94
+ Local files use `File.name`. HTTP(S)/file URLs, including remote source
95
+ descriptors, use the decoded final pathname filename only. Origins, credentials,
96
+ query strings and fragments are excluded. Directory URLs, ArrayBuffers, opaque
97
+ URLs such as blob/data URLs, and unnamed sources show nothing. Source titles
98
+ track successful `element.open()` / `src` opens and clear when that source closes
99
+ or is replaced through the core; applications opening directly through
100
+ `element.player` should manage a custom title. Titles are plain text and truncate
101
+ visually to fit the player.
102
+
103
+ ```ts
104
+ const element = document.querySelector("demuxe-player");
105
+ element.titleMode = "source";
106
+ ```
107
+
108
+ For an embedded app, disable utility UI while retaining playback controls:
109
+
110
+ ```ts
111
+ element.showSourceControls = false;
112
+ element.showDiagnostics = false;
113
+ element.allowFileDrop = false;
114
+ await element.open(source); // The host application owns source selection.
115
+ ```
116
+
117
+ These three properties default to `true` and update live. Source controls include
118
+ the empty-state opener, folder menu, URL form and local media/subtitle pickers.
119
+ Disabling them closes their menu and disables the input handlers. Programmatic
120
+ `open()` and `addSubtitle()`, plus existing subtitle track selection, still work.
121
+ Disabling diagnostics closes its overlay; `element.player.diagnostics` remains
122
+ available. Disabling file drop leaves browser drag/drop defaults alone. Drop
123
+ handling is independent of source-control visibility: disable both when the host
124
+ owns all source input. Focus moves to the stage if a focused utility is disabled.
125
+ These options are JavaScript properties, not HTML boolean attributes.
126
+
127
+ `seekStep` defaults to 10 seconds and accepts a positive finite number. It
128
+ controls both seek buttons, their default accessible labels/numerals, and J/L.
129
+ Arrow keys retain their five-second steps. Explicit localized `labels.back` and
130
+ `labels.forward` override the default labels; keep them consistent with your step.
131
+ `controlsAutoHideDelay` defaults to 2800 milliseconds; zero disables inactivity
132
+ hiding, and positive values up to 2147483647 set the delay. Changing it restarts
133
+ the timer. Playing with the play button/shortcut still hides controls immediately;
134
+ screen taps still toggle visibility. Invalid numeric property values throw
135
+ `INVALID_ARGUMENT`.
136
+
137
+ For entirely custom controls, import `Player` from `demuxe` and build your own
138
+ layout. The optional component delegates to that same core and adds no playback
139
+ modes or routing policy.
140
+
141
+ ```js
142
+ import {definePlayerElement} from 'demuxe/player';
143
+ definePlayerElement();
144
+ const element = document.querySelector('demuxe-player');
145
+ element.labels = {play: 'Lire', pause: 'Pause', settings: 'Réglages'};
146
+ const core = await element.ready;
147
+ const unsubscribe = core.subscribe(state => console.log(state.status));
148
+ await element.open(file, {signal: controller.signal});
149
+ // Later: await element.close() to reuse, or await element.destroy() to finish.
150
+ ```
151
+
152
+ Methods also include play, pause, seek, setVolume, setMuted, setPlaybackRate,
153
+ selectAudioTrack, selectSubtitleTrack and addSubtitle. Advanced font, filter,
154
+ tone-mapping and source policy methods are available through the read-only player
155
+ reference. Replacing that reference is unsupported. Use a new element for a
156
+ new asset-base; the old one must finish cleanup first. An asset-base attribute
157
+ change after initialization is reverted and reports INVALID_ARGUMENT.
158
+
159
+ ```css
160
+ demuxe-player {
161
+ --demuxe-background: #121318;
162
+ --demuxe-stage-background: #090a10;
163
+ --demuxe-foreground: #f2f1f7;
164
+ --demuxe-muted-foreground: #bbb8ca;
165
+ --demuxe-panel-background: #20212a;
166
+ --demuxe-control-background: #30313e;
167
+ --demuxe-overlay-background: #171824bb;
168
+ --demuxe-accent: #b7a0ff;
169
+ --demuxe-border: #393941;
170
+ --demuxe-radius: 16px;
171
+ }
172
+ demuxe-player::part(controls) { padding-inline: 20px; }
173
+ demuxe-player::part(title) { font-weight: 600; }
174
+ demuxe-player::part(timeline) { height: 32px; }
175
+ ```
176
+
177
+ For a light skin, set the same surface variables to light colors, foregrounds to
178
+ dark colors, and `color-scheme: light` on the element for native form controls.
179
+ All public theme names use `--demuxe-*`; there are no legacy branding aliases.
180
+
181
+ Stable parts: container, stage, controls, settings, error, status, title, topbar,
182
+ transport, timeline, volume. The volume part wraps the mute button and slider. Limited slots:
183
+ before-controls, after-controls and source-actions. The source-actions slot adds
184
+ host-owned buttons to the source menu; activating a button closes the menu and
185
+ focuses the stage. The playground uses it for its example media. Shadow IDs/classes are implementation details.
186
+ Air controls overlay the video: an open-media action at the top, a large unboxed
187
+ play/pause icon between backward/forward seek buttons (ten seconds by default) at the center,
188
+ and a thin full-width timeline with elapsed and total times at opposite ends.
189
+ Seek buttons clamp to available seek ranges and disable while an operation is
190
+ pending or no seek window exists. Volume shares the timestamp row below the
191
+ timeline; settings and fullscreen sit at the top right. The gear opens compact playback settings; the folder opens file/URL
192
+ inputs and subtitle upload separately. The eye toggles a live session diagnostics
193
+ text overlay that remains visible when playback controls hide. The overlay is
194
+ scrollable and keyboard-focusable; navigation keys scroll it, and Escape closes
195
+ it and returns focus to the eye. Top-right icons
196
+ use motion on hover and a stronger accent stroke when active. Buffering displays
197
+ a small central ring even with hidden controls. Timeline shading uses reported
198
+ buffered ranges (currently Native only); unavailable ranges are not estimated.
199
+ Control sizes and compact layouts follow the player container width, including
200
+ small embeds in wide browser windows. Pointer controls retain 36px targets and
201
+ coarse-pointer devices use 44px targets. On narrow players, opening a menu hides
202
+ the central controls to avoid overlap. Play hides controls immediately; mouse
203
+ movement reveals them, and inactivity fades them out. Leaving the player with
204
+ the mouse also hides controls while playing (not while paused), unless a menu,
205
+ pending operation, timeline drag, or keyboard-visible focus needs them. Losing
206
+ focus alone does not hide controls; touch input keeps its existing tap behavior. Seeking with hidden
207
+ controls briefly shows only the timeline and times, for 800 ms after completion. Tapping the video toggles controls
208
+ in playing or paused playback. Clicking outside a menu dismisses it without
209
+ stealing focus; Escape returns focus to its trigger. Hiding controls preserves
210
+ focus on the stage; keyboard navigation reveals controls. Keyboard shortcuts apply to
211
+ focus inside the component; inputs keep their native keys, and buttons retain
212
+ Space/Enter activation, except top-right icons reserve Space for playback
213
+ and use Enter for activation. Playback keys: Space/K, arrows, J/L, M, C, brackets, digits,
214
+ Home/End, F and ?. Inputs, selects, editable content and modified keys
215
+ retain their normal handling. C uses the core's subtitle visibility setting;
216
+ it does not select a different language. Scrubbing previews locally; release
217
+ commits one seek. Settings are an accessible disclosure and restore trigger focus.
218
+
219
+ Local file/subtitle pickers are available in settings; opening files never uploads
220
+ them. File drop and a URL form (File, HLS, DASH, and live input)
221
+ are also built into the component. File selection, URL submission, and file drop
222
+ focus the stage so Space controls playback; the public close() method closes media. The component contains no example media, engine selector,
223
+ raw filters or memory metrics. The playground keeps automatic engine selection
224
+ enabled and shows a compact engine/media summary below the player. A live stream with no known seek window shows LIVE
225
+ and disables the finite seek control. Browser fullscreen denial produces a message;
226
+ no fake fullscreen, PiP or casting fallback is applied.
227
+
228
+ Experimental playback optimizations use the existing Player owner and do not add
229
+ public modes or automatic component controls. See [the integration stage](OPTIMIZATION-INTEGRATION.md)
230
+ for constructor flags, scalar gain, and current qualification limits.
231
+
232
+ The programmatic Player option `experimentalAudioAdaptation: 'flac'` is an explicit
233
+ Native trial gate. No new component attribute or automatic route is introduced.
234
+ See [the FLAC qualification report](OPTIMIZATION-FLAC.md) for required optional assets,
235
+ track/fidelity limits and unsupported subtitle combinations.
236
+
237
+
238
+ ## Optional playback processing
239
+
240
+ The component delegates playback, subtitle selection and visibility to its core
241
+ Player; it owns no separate optimization route or audio graph. Applications using
242
+ an explicitly configured core can call `setAudioGain` repeatedly without replacing
243
+ the video session. Ordinary component volume/mute controls retain their existing
244
+ meaning. The component fullscreen container includes subtitle canvases. Native
245
+ ASS and adaptation require explicit core constructor options and optional assets;
246
+ there are no new component attributes or silently enabled experimental defaults.
247
+ See [core options and restrictions](PUBLIC-API.md#experimental-playback-optimizations).
@@ -0,0 +1,163 @@
1
+ # Public API and component qualification
2
+
3
+ Implementation baseline: working files matched demo-source 8bb451b while main was
4
+ f9a8f9c. No branch was merged or published. The pre-change source snapshot is
5
+ build/public-api-baseline/source-before.tar.gz. Historical results and failed
6
+ artifacts are retained.
7
+
8
+ ## Milestone status
9
+
10
+ | Milestone | Implementation | Qualification boundary |
11
+ | --- | --- | --- |
12
+ | M0 | Public contract, component contract, migration notes | Source-reviewed against current transaction/engine owners |
13
+ | M1 | Immutable snapshots, subscriptions, typed events, media geometry/tracks, capabilities | Real Native/Hybrid/Software tests; unknown/live-window mappings have separate adapter-injection evidence |
14
+ | M2 | Additive commands, stable IDs, cancellation, close/reopen, idempotent teardown, structured/redacted errors | Real rollback, stale-event, mute, track transition, seek and cleanup checks |
15
+ | M3 | Same-origin runtime base, verified copy CLI, static and bundled entry points, SSR-safe imports | Exact archives installed into independent static and esbuild apps at two runtime paths |
16
+ | M4 | Optional custom element and migrated playground, same core | Two independent instances, source changes, removal/reconnect and actual fullscreen |
17
+ | M5 | Keyboard, semantic controls, accessible names, focus restoration, live announcements, styling hooks | Chrome/Firefox keyboard and accessibility-tree checks; manual screen-reader and physical touch sessions remain open |
18
+ | M6 | Tests, examples, package evidence, overhead samples | Local candidate qualification only; clean three-engine builds, Safari/mobile, physical output and release gates remain open |
19
+
20
+ Archive-specific qualification is generated **after** assembly, alongside the
21
+ archive as qualification.json. An embedded copy of this document describes the
22
+ build-time status; it cannot contain its own archive hash. Consult the accompanying
23
+ record and the repository results for final pass/fail status and test hashes.
24
+
25
+ ## Reproducible commands
26
+
27
+ ```sh
28
+ npm run build
29
+ node --test tests/public-api-state.mjs
30
+ node tests/public-api.mjs
31
+ BROWSER=firefox node tests/public-api.mjs
32
+ node tests/player-component.mjs
33
+ BROWSER=firefox node tests/player-component.mjs
34
+ node tests/autoplay-policy.mjs
35
+ BROWSER=firefox node tests/autoplay-policy.mjs
36
+ node tests/player-api.mjs
37
+ node scripts/media-server.mjs # separate terminal; synthetic origins 4180/4181
38
+ node --test tests/range-reader-deadline.mjs tests/range-reader.mjs \
39
+ tests/audio-worklet.mjs tests/resource-loader.mjs tests/streaming-expansion.mjs \
40
+ tests/remux-buffering.mjs tests/video-codec-config.mjs \
41
+ tests/retained-codec-worker.mjs tests/native-selection.mjs
42
+ ONLY='Live,External,HLS,Finite DASH,Audio device' node tests/compatibility-expansion.mjs
43
+ node tests/player-ui-overhead.mjs
44
+ npm install --prefix build/public-api-tooling --no-audit --no-fund esbuild@0.28.2
45
+ python3 scripts/package-beta.py --output build/public-api-release-candidate
46
+ BETA_ARCHIVE=build/public-api-release-candidate/demuxe-0.3.0-beta.3.tgz node --test tests/copy-assets.mjs
47
+ BETA_ARCHIVE=build/public-api-release-candidate/demuxe-0.3.0-beta.3.tgz node tests/public-api-consumer.mjs
48
+ BETA_ARCHIVE=build/public-api-release-candidate/demuxe-0.3.0-beta.3.tgz BROWSER=firefox node tests/public-api-consumer.mjs
49
+ BETA_ARCHIVE=build/public-api-release-candidate/demuxe-0.3.0-beta.3.tgz node tests/beta-streaming.mjs
50
+ BETA_ARCHIVE=build/public-api-release-candidate/demuxe-0.3.0-beta.3.tgz BROWSER=firefox node tests/beta-streaming.mjs
51
+ ```
52
+
53
+ Results go to timestamped results/public-api/, results/player-component/,
54
+ results/public-api-consumer/, results/beta/ and existing regression directories.
55
+ The archive tests record SHA-256, browser version, consumer root and case results.
56
+ The esbuild application imports only shipped package entry points and copies its
57
+ runtime via the shipped command. No repository-specific browser code is needed.
58
+
59
+ ## Interpreting evidence
60
+
61
+ The prior 21-case API suite, 73 reader/routing/audio unit checks and focused HLS,
62
+ DASH, subtitle/font and audio-device regressions passed during this work. New
63
+ normalized-state and component suites exercise real engines, while explicitly
64
+ labeled adapter injection tests cover unknown metadata and live-window mapping.
65
+ They do not certify every live demuxer or codec. Browser isolation and missing
66
+ assets are tested as distinct errors. Missing assets must be rejected at the HTTP
67
+ origin because worker fetch interception differs across automation engines.
68
+
69
+ Autoplay checks must not run evaluation polling while the initial play request
70
+ is pending: automation evaluation can grant transient activation. Chrome rejected
71
+ Native and Hybrid and resumed after a click. Firefox rejected Native but allowed
72
+ the Web Audio Hybrid path in the observed environment; that difference is not
73
+ reported as a codec failure or an invented autoplay rejection.
74
+
75
+ The UI adds no engine polling. The short overhead harness compares the same
76
+ example.mp4, mode and 1920×1080 output for four seconds, using browser main-thread
77
+ ScriptDuration. Those samples describe UI/event work only, not full engine CPU,
78
+ real-time output quality, sustained playback or a performance improvement.
79
+
80
+ The in-app browser plugin could not connect because an installed module was
81
+ missing. Qualification used standalone real Chrome/Firefox automation. No manual
82
+ VoiceOver/NVDA session, physical touch, Safari, PiP, casting, physical HDR/surround
83
+ fidelity or independent clean build of all three engines is claimed. GPL/source
84
+ obligations and the release gate in RELEASE.md remain intact. The public Pages
85
+ site has not been updated by this work.
86
+
87
+ ## Archive-specific record
88
+
89
+ The final archive hash and exact result paths are recorded after assembly in the
90
+ accompanying qualification.json and in the working repository's final report.
91
+ This embedded build-time statement deliberately contains no self-referential
92
+ archive hash. Earlier candidates, failures and their evidence remain preserved.
93
+
94
+
95
+ ## Final local candidate — 2026-09-12
96
+
97
+ Artifact: `build/public-api-release-candidate/demuxe-0.3.0-beta.3.tgz` (17,845,779 bytes).
98
+
99
+ SHA-256: `2b7d99731bb05c33d8d24648550fafda90c4ad13968d2dbbdb777ef48421bb4a`.
100
+
101
+ The package assembled alongside `build/pages-api-release-candidate` is byte-identical.
102
+ Every packaged manifest entry was verified. Generated core/component files match the
103
+ working implementation; all three shipped Wasm binaries match the previous Pages
104
+ manifest. These are existing engine binaries, not newly qualified clean engine builds.
105
+ The final report is external to the archive to avoid a self-referential archive hash.
106
+
107
+ | Suite | Chrome 152.0.7977.83 | Firefox 146.0.1 | Evidence scope |
108
+ | --- | --- | --- | --- |
109
+ | Normalized core | 14/14 | 14/14 | Current core source hashes |
110
+ | Component | 14/14 | 14/14 | Current component source hashes, including release of retry sources |
111
+ | Clean static and esbuild consumers | 8/8 | 8/8 | Exact archive; two runtime bases, all three modes, SSR/types/import isolation |
112
+ | Streaming/seek | 6/6 | 6/6 | Exact archive; packet integrity, absolute deadline, cleanup |
113
+ | Local Pages assembly | 8/8 | 8/8 | Matching packaged runtime manifest; no public deployment |
114
+ | Autoplay policy | 2/2 | 2/2 | Recorded browser policy behavior, including Firefox allowing Hybrid |
115
+
116
+ Additional checks: 4 asset-copy tests, 5 pure state/error tests, 73 existing unit
117
+ checks, 21 existing API cases and 7 focused compatibility cases passed. The actual
118
+ `npx --offline demuxe copy-assets npx-runtime` command copied 96 verified assets
119
+ from a clean installation of this archive. Exact paths, result hashes, CLI output,
120
+ source/build-material archive hashes and qualification boundaries are recorded in
121
+ `build/public-api-release-candidate/qualification.json`.
122
+
123
+ A Pages harness failure is retained at
124
+ `results/pages/firefox-2026-09-12T14-06-43.067Z/result.json`.
125
+ The harness could mistake the previous source's idle operation state for completion
126
+ of the asynchronously fetched example. The final harness explicitly waits for a
127
+ new accepted source identity. This test-only correction does not change package
128
+ bytes. Historical candidates and failures remain preserved.
129
+
130
+ Four-second matched playback samples measured core/component main-thread script
131
+ work of 4.027/6.377 ms (Native), 44.603/57.464 ms (Hybrid), and 19.553/32.696 ms
132
+ (Software). Notification counts were equal within each pair: 19, 129 and 129.
133
+ These earlier short samples are not engine CPU or sustained performance claims.
134
+
135
+ The implementation is complete for the stated scope. M5's manual screen-reader
136
+ and physical touch gate and M6's broader browser/clean-engine/release gates remain
137
+ open as listed above. No commit, push, merge or publication was performed.
138
+
139
+
140
+ ## Review corrections — 2026-09-12
141
+
142
+ The candidate above is historical and predates three review corrections: external
143
+ subtitle identities no longer alias their per-file stream indices; unrecovered
144
+ fatal mpv end-file events populate the structured session error before notification;
145
+ and caller cancellation is detached at source acceptance, before subscribers run.
146
+ Instance close/destroy retain their internal cancellation authority.
147
+
148
+ `tests/public-api.mjs` now includes real two-attachment selection and cross-mode
149
+ retention, fatal-event adapter injection for each public mode, and synchronous
150
+ abort-at-sourcechange checks in Native and Software. The existing pre-acceptance
151
+ cancellation, rollback, stale-event and bounded-close checks remain in that suite.
152
+ `tests/public-api-state.mjs` additionally checks external/embedded ID separation.
153
+ New timestamped results and a separate review-fix archive preserve the previous
154
+ candidate and its evidence. The review does not change the release gates above.
155
+
156
+ Review-fix validation: Chrome 152.0.7977.83 and Firefox 146.0.1 each passed
157
+ 20 normalized API cases and 8 clean static/bundled consumer cases. The 6 state
158
+ unit tests and TypeScript build passed. The archive is
159
+ `build/public-api-review-fixes/demuxe-0.3.0-beta.3.tgz`, SHA-256
160
+ `806b07a4c7fc7df8c96781cc69b0f0fa77984f062ec0a51bf8ab0caef700bc42`. Its manifest hashes and generated runtime
161
+ match the working build; the Wasm engines are unchanged. Detailed result hashes
162
+ are in the accompanying `qualification.json`. Broader historical suites were
163
+ not rerun for these targeted core corrections.
@@ -0,0 +1,245 @@
1
+ # Public API contract
2
+
3
+ Contract for the public API milestone work. Implementation and qualification are
4
+ tracked separately in PUBLIC-API-VALIDATION.md; this document is not test evidence.
5
+ The baseline is demo-source 8bb451b, present in the working tree on main f9a8f9c.
6
+
7
+ Player is the sole playback owner. It owns its video/canvas surface, bounded
8
+ source adapters, routing, synchronization, recovery and teardown. There are
9
+ exactly three modes: native, hybrid, software. UI and core imports are separate.
10
+ No arbitrary mpv command interface is exposed.
11
+
12
+ ## Compatibility and additions
13
+
14
+ Existing: ready, open(File | ArrayBuffer), openRemote, play, pause, seek,
15
+ volume(0–100), rate(0.5–2), selectTrack with backend IDs, subtitleVisible,
16
+ addSubtitle, addTextTrack, addFont, setMode, setAutomaticSelection, filter and
17
+ tone-mapping methods, resize, destroy, properties, diagnostics, mpv events,
18
+ modechange and selectionchange.
19
+
20
+ Additions: state, subscribe, mediaInfo, typed high-level events, feature
21
+ availability, open(string | URL | RemoteSource), AbortSignal open options,
22
+ setVolume(0–1), setMuted, setPlaybackRate, stable public track selections, close,
23
+ assetBase. These are additive; legacy volume and rate units do not change.
24
+
25
+ ## State and events
26
+
27
+ state is a deeply frozen snapshot. Its identity remains unchanged until a value
28
+ changes. subscribe(listener) calls the listener immediately and returns an
29
+ idempotent unsubscribe. Updates happen before subscribers and high-level events.
30
+ Ordinary backend property bursts are coalesced in a microtask, with no new worker
31
+ polling. Subscribers must not throw; observer failures do not change playback.
32
+
33
+ status describes observed idle/paused/playing/buffering/ended/error state;
34
+ playbackIntent describes requested play/pause independently. pendingOperation has
35
+ an increasing instance-local ID and opening/seeking/switching/closing kind.
36
+ activeMode is null without accepted media. Legacy mode remains the configured
37
+ route while idle. Candidate events never replace accepted state. Failed
38
+ replacement reports an operation error and preserves a working session.
39
+
40
+ Seconds are used throughout. Unknown duration and ranges are null; [] means
41
+ known empty. Byte-cache statistics are never converted into buffered time.
42
+ Native ranges use the browser timeline translated out of remux bias. mpv
43
+ buffered time is unknown; seek ranges use observed demuxer seekable ranges for
44
+ live sources, and confirmed demuxer seekability plus duration for VOD. An unknown
45
+ live window disables a finite seek slider. mediaInfo includes display geometry,
46
+ source rotation when reported, display aspect and selected streams; absent
47
+ metadata stays null. No UI needs mpv property names.
48
+
49
+ High-level event detail contains the committed state. Source acceptance emits
50
+ sourcechange; subsequent changed fields emit durationchange, trackschange,
51
+ capabilitieschange, volumechange, ratechange and timeupdate. play expresses
52
+ accepted play intent; playing requires observed playback progress, waiting
53
+ observed starvation, pause observed pause, ended EOF. seeking precedes the
54
+ backend seek; seeked occurs only after native seeked or mpv presentation settles.
55
+ statechange follows snapshot publication. modechange retains loading/ready/failed
56
+ candidate phases and selectionchange retains route-attempt detail.
57
+
58
+ ## Completion, cancellation and ownership
59
+
60
+ open resolves after an accepted paused session loads and its initial output is
61
+ ready. File references are retained; ArrayBuffers are copied and limited to
62
+ 32 MiB. Remote options are copied. open accepts {signal}; abort before acceptance
63
+ cancels inspection/candidate work and preserves the previous usable session.
64
+ Abort after acceptance has no effect. Caller-owned text-track URLs remain alive
65
+ until close/source replacement. Fonts have existing bounded instance budgets.
66
+
67
+ play resolves when the backend accepts playback, not at the first time update.
68
+ Call from a user gesture; resume is initiated in that gesture when no operation
69
+ is pending. Autoplay denial is AUTOPLAY_BLOCKED and never triggers codec fallback.
70
+ pause resolves at observed pause. seek resolves after output settles; it does not
71
+ interrupt active packet reads. There are at most 32 pending core operations.
72
+ Controls preview scrubbing locally and submit a seek on release.
73
+
74
+ close aborts older queued/in-flight operations, disposes media-specific resources
75
+ and attachments, and leaves a reusable idle instance. It retains configured
76
+ volume/mute/rate, mode policy, filters, tone mapping, output policy and bounded
77
+ fonts. destroy is terminal and idempotent, cancels work, disposes all instance
78
+ resources and removes owned DOM. Pending promises reject predictably. Await
79
+ cleanup before discarding the owner.
80
+
81
+ Stable track IDs are scoped to a source generation and mapped to backend IDs.
82
+ Source stream indices preserve identity across routes where reported. External
83
+ subtitle files use a separate attachment namespace: their demuxers can share the
84
+ same stream index. Replaying the retained attachments preserves their track IDs. Identity
85
+ that exists only within a backend cannot be guessed across Native/mpv routes;
86
+ explicit selection causes an unsupported-transition rejection. New source IDs
87
+ invalidate previous IDs. null means disabled; 'auto' delegates initial selection.
88
+ Legacy selectTrack retains its documented backend-ID contract.
89
+
90
+ Errors are PlayerError with code, operationId, operation, scope, retryable and
91
+ redacted message. Session errors alone populate state.error. Failed controls or
92
+ replacement errors are operation-scoped. Codes: INVALID_ARGUMENT, ABORTED,
93
+ AUTOPLAY_BLOCKED, SOURCE_PERMISSION, SOURCE_CHANGED, NETWORK_TIMEOUT,
94
+ UNSUPPORTED_MEDIA, UNSUPPORTED_TIMELINE, UNSUPPORTED_FEATURE, ASSET_LOAD_FAILED, ISOLATION_REQUIRED,
95
+ DECODE_FAILED. Normal events/diagnostics redact authorization fields, URL userinfo,
96
+ queries and fragments; raw mpv properties remain an advanced inspection surface.
97
+
98
+ ## Using normalized state
99
+
100
+ ```ts
101
+ const player = new Player(host, {assetBase: '/assets/demuxe/'});
102
+ const unsubscribe = player.subscribe(state => {
103
+ render({
104
+ status: state.status,
105
+ seconds: state.currentTime,
106
+ duration: state.duration,
107
+ seekWindow: state.seekable,
108
+ audio: state.audioTracks,
109
+ subtitles: state.subtitleTracks,
110
+ aspect: state.mediaInfo.aspectRatio,
111
+ seek: state.capabilities.features.seek,
112
+ });
113
+ });
114
+ await player.open(file, {signal});
115
+ await player.selectSubtitleTrack(player.state.subtitleTracks[0]?.id ?? null);
116
+ await player.setVolume(.75);
117
+ await player.setMuted(true);
118
+ await player.close();
119
+ unsubscribe();
120
+ await player.destroy();
121
+ ```
122
+
123
+ state.volume is normalized 0–1, configured independently from mute. Legacy
124
+ properties.volume still reflects backend 0–100 output, including zero while
125
+ muted. subtitlesVisible exposes the retained visibility setting independently
126
+ from the selected subtitle identity. player.capabilities includes legacy booleans,
127
+ deployment flags and feature availability; the same object is in the snapshot.
128
+ Feature availability describes request/route eligibility, not a guarantee that
129
+ an untested codec, asset or output device will work. Source-specific timeline and
130
+ track availability remain unknown until established.
131
+
132
+ The core emits structured operation errors for failed queued controls, while
133
+ synchronous legacy argument validation throws PlayerError. Rejected opens due
134
+ to invalid arguments or a signal already canceled may reject without an event;
135
+ callers must catch returned promises. Observer exceptions are reported through
136
+ the browser's error reporting and cannot veto a committed session. Close reserves
137
+ one teardown slot beyond the 32 ordinary pending-operation limit, so a full queue
138
+ cannot prevent cleanup. Browser resume requests initiated during another queued
139
+ operation may require a fresh user gesture when that operation completes.
140
+
141
+ Native exposes display dimensions; source rotation/codec metadata can remain null
142
+ when the browser does not report them. Its selected video entry is an opaque
143
+ source-scoped identity and does not imply a browser video-track switching API.
144
+ Live mpv seek windows use the demuxer's reported cached seekable timestamps; a
145
+ byte cache alone never creates a buffered-time claim. High-level time/state
146
+ notifications reuse backend events; only existing seek/presentation settlement
147
+ and engine health mechanisms retain their preexisting polling.
148
+
149
+ ## Experimental playback optimizations
150
+
151
+ `experimentalBufferedNativeSeeks` and `experimentalHybridAudioFilters` are opt-in
152
+ constructor options, both false by default. Hybrid currently admits only scalar
153
+ `volume=N` / `lavfi=[volume=N]` attenuation for N in [0, 1]. Other filters retain
154
+ Software routing or explicit-mode rejection.
155
+
156
+ `audioGain` (default 1) and `setAudioGain(value)` request experimental scalar
157
+ attenuation in [0, 1]. All three modes use a dedicated, lazy Web Audio gain stage.
158
+ The setter updates that stage in place, preserving the engine, source, position,
159
+ and pause intent. Hybrid/Software gain follows the mpv PCM output without changing
160
+ the user audio filter chain. Ordinary volume and mute remain separate. Unity on a
161
+ fresh player allocates no gain stage; restoration retains an existing stage.
162
+ Transactional replacement is reserved for backends without an in-place gain method.
163
+ Diagnostics expose `plan` and `audioGain`; feature capabilities include `audioGain`.
164
+ See [current qualification and limitations](OPTIMIZATION-COMPLETION.md).
165
+
166
+ ### Experimental selected-audio FLAC preparation
167
+
168
+ `experimentalAudioAdaptation: 'flac'` enables explicit Native trials after ordinary
169
+ Native/direct or packet-copy audio is unsuitable. It does not enable automatic
170
+ adaptation. The optional separately built preparation assets must be packaged.
171
+ The tested subset is integer PCM16/24 mono/stereo at 44.1/48 kHz; unsupported
172
+ precision/layouts reject instead of quantizing, downmixing or resampling. Video
173
+ packets are copied. No Opus permission is implied. It supports the separately
174
+ requested `audioGain` operation and external Native ASS, including their combination.
175
+ Long unequal tails have a separate qualified experimental FLAC path for desktop
176
+ Chrome, H264 and PCM16/24. It preserves the final video image while longer audio
177
+ continues, and silence after shorter audio ends. Distant seeks regenerate bounded
178
+ real preroll. Firefox rejects this Native timeline with `UNSUPPORTED_TIMELINE`;
179
+ use an eligible mpv route. On Firefox, a distant seek after video EOF can
180
+ recover from Hybrid through Software, with the failed seek disclosed in diagnostics.
181
+ Use Software explicitly when continuous ASS behavior through a Firefox video-EOF
182
+ audio tail is required; that Hybrid combination is not qualified.
183
+ Opus long tails remain unsupported. See the
184
+ [current qualification and reproductions](../results/optimization-final/README.md).
185
+
186
+ ### Automatic lossless adaptation policy
187
+
188
+ ```js
189
+ const player = new Player(container, {automaticAudioAdaptation: 'lossless'});
190
+ await player.open(file);
191
+ ```
192
+
193
+ This optional policy permits Native FLAC only for inspected local Matroska with
194
+ H264 up to 1080p and selected 48 kHz PCM16/24 mono/stereo audio, with established
195
+ selected-track starts and ends within 50 ms. Original direct/copy playback remains
196
+ preferred. Unsupported sources retain eligible Hybrid/Software alternatives;
197
+ explicit modes remain authoritative. Selecting a different audio track repeats
198
+ admission while preserving its source identity and user intent. External Native
199
+ ASS and gain use their existing options and must form a supported complete plan.
200
+
201
+ This does not enable Opus, resampling, downmixing, embedded Native subtitles,
202
+ remote automatic adaptation or streaming adaptation. FLAC preserves admitted
203
+ integer decoded samples; it does not restore an earlier lossy recording.
204
+ Diagnostics disclose rejected alternatives and the accepted transformation.
205
+
206
+ ### Experimental Native ASS/SSA
207
+
208
+ Set `experimentalNativeASS: true` to use external ASS/SSA with Native direct,
209
+ remux or qualified FLAC playback. Use the existing `addSubtitle(file)`,
210
+ `addFont(file)`, `selectSubtitleTrack(id)` and `subtitleVisible(value)` methods.
211
+ Fonts added before opening avoid a later transactional font refresh. Attachments
212
+ are source-scoped and reset on opening a new source. Source-scoped external IDs
213
+ follow attachment order across Native and mpv; they are distinct from embedded
214
+ stream indices. Other subtitle formats and embedded ASS still require mpv.
215
+
216
+ Install the optional libass assets using the package asset-copy command from a
217
+ package built with `--ass-build`. This feature requires cross-origin isolation.
218
+ Use container fullscreen; video-only fullscreen, PiP and casting cannot carry the
219
+ external overlay. Native PiP/remote-playback controls are disabled for this plan.
220
+ See [current coverage and limits](OPTIMIZATION-COMPLETION.md).
221
+
222
+ ### Experimental selected-audio Opus preparation
223
+
224
+ `experimentalAudioAdaptation: 'opus'` requires `allowLossyAudio: true` and explicit
225
+ Native mode. It is a separate profile, never a fallback from failed FLAC. The
226
+ qualified input subset is copied H.264 video with selected PCM16/24 mono/stereo at
227
+ 48 kHz. Other sample rates/layouts/codecs reject; permission to encode lossily does
228
+ not authorize resampling or downmixing. Original compressed audio remains preferred
229
+ when it can be copied. Opus+gain is supported; Opus+Native ASS is not admitted.
230
+ The matching optional preparation engine includes the pinned FFmpeg Opus encoder.
231
+ No sample-exact claim applies to Opus. See the current coverage document for delay,
232
+ padding, timestamp and marker tests and the unresolved long-tail limit.
233
+
234
+ ### Complete-plan admission diagnostics
235
+
236
+ `diagnostics.plan` identifies the accepted internal plan without adding a public
237
+ mode. `diagnostics.planAdmission` lists finite candidates with `eligible`, typed
238
+ rejection `code`, and `reason`. Eligibility is permission to attempt the plan;
239
+ actual codec preparation and meaningful startup output must still succeed.
240
+ Automatic routing retains Native copy, authorized qualified Native FLAC, eligible Hybrid, then Software. It
241
+ uses requested effects, source inspection, selected tracks, transport constraints
242
+ and browser prerequisites. The experimental adaptation option remains explicit-only;
243
+ automatic FLAC uses the separate lossless policy above. Route ranking
244
+ does not use prototype benchmark percentages. Explicit mode selection remains
245
+ authoritative. In-place gain updates refresh plan diagnostics without rerouting.