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,181 @@
1
+ # Traceable beta release
2
+
3
+ A release candidate is one archive from a clean tagged revision, built and tested
4
+ as recorded below. A deterministic tar command alone is not an engine build test.
5
+ One clean build of all three engines is required for this developer beta. Universal
6
+ bit-for-bit reproducibility and the historical Linux baseline are separate claims.
7
+
8
+ ## Prerequisites
9
+
10
+ Install native Python 3, CMake, Ninja, pkg-config, Git, curl and Node.js/npm. On the
11
+ reference macOS host, install the pinned Python build dependencies in the checkout:
12
+
13
+ ```sh
14
+ python3 -m venv build/venv
15
+ build/venv/bin/python -m pip install meson==1.7.2 Jinja2==3.1.6 MarkupSafe==3.0.2
16
+ npm ci
17
+ python3 scripts/fetch-sources.py
18
+ cp -R build/sources/emsdk build/emsdk-4.0.14
19
+ build/emsdk-4.0.14/emsdk install 4.0.14
20
+ build/emsdk-4.0.14/emsdk activate 4.0.14
21
+ ```
22
+
23
+ The installer archive and all library archives are verified against
24
+ `sources.lock.json`. The SDK installs the platform's 4.0.14 compiler tools. This
25
+ macOS recipe does not claim that the historical Linux `toolchain.lock.json` is a
26
+ macOS package lock. The new build record captures actual host tool versions and
27
+ hashes; retain it. `prepare-beta-toolchain.py` generates absolute SDK paths and a
28
+ fresh compiler cache, without inheriting an edited SDK configuration.
29
+
30
+ For a clean build use a new checkout of the reviewed tag, e.g. a fresh clone with
31
+ `git checkout --detach <tag>`. Install npm/Python dependencies there as above. An
32
+ already installed SDK and verified download archive cache may be shared with that
33
+ checkout, but never share extracted library sources, objects, prefixes or the
34
+ Emscripten cache. Do not run `fetch-sources.py` in that checkout before `--clean`;
35
+ use the prerequisite installation checkout to provision the SDK first.
36
+
37
+ ```sh
38
+ DEMUXE_SDK=/absolute/path/to/installed/emsdk-4.0.14 \
39
+ bash scripts/build-beta-engines.sh --clean > build/clean-build.log 2>&1
40
+ ```
41
+
42
+ Create `build/` before redirecting the log. The clean flag rejects existing engine
43
+ outputs, extracted sources, dependency prefixes, objects or compiler cache. The
44
+ build verifies locked archives, applies the complete patch series, builds all
45
+ static dependencies and all three engines, and records actual configuration and
46
+ input/output hashes in `build/beta-build.json`. Compiler file-prefix maps and
47
+ normalized generated configuration headers use `/demuxe/` as a virtual build root;
48
+ the npm packager rejects leaked host paths in any runtime file, including Wasm.
49
+ Full host paths remain only in build evidence and the source companion. Inputs may not change during the
50
+ build. Generated tracked bindings must match the tag; otherwise fix the source,
51
+ review and make a new candidate revision.
52
+
53
+ ## Assemble, test and identify the same bytes
54
+
55
+ Resolve the original-code license and inspect `docs/LICENSING.md` before tagging.
56
+ Use a new version/tag for changed candidate bytes. The release packaging option
57
+ requires a clean tagged revision, clean-build evidence, the original license and
58
+ matching input/configuration/engine hashes. It also produces the source companion.
59
+
60
+ ```sh
61
+ python3 scripts/package-beta.py --release-tag <tag> --output build/release
62
+ BETA_ARCHIVE=/absolute/path/to/build/release/demuxe-<version>.tgz \
63
+ node tests/beta-consumer.mjs
64
+ BROWSER=firefox BETA_ARCHIVE=/absolute/path/to/build/release/demuxe-<version>.tgz \
65
+ node tests/beta-consumer.mjs
66
+ BETA_ARCHIVE=/absolute/path/to/build/release/demuxe-<version>.tgz \
67
+ node tests/beta-streaming.mjs
68
+ BROWSER=firefox BETA_ARCHIVE=/absolute/path/to/build/release/demuxe-<version>.tgz \
69
+ node tests/beta-streaming.mjs
70
+ ```
71
+
72
+ The browser tests require Playwright's Firefox and local Chrome, and repository
73
+ fixtures created by the documented fixture generators. The focused streaming test
74
+ uses `build/fixtures/playback-performance/bbb-stream.mp4` by default, or
75
+ `STREAMING_FIXTURE` pointing to a seekable H.264/AAC MP4 longer than 500 seconds.
76
+ It records that fixture's hash. Keep fixture-generation/source provenance with the
77
+ results. Tests use only the extracted runtime archive, and verify all manifest
78
+ hashes before running. Consumer results include the archive's SHA-256.
79
+
80
+ Extract that same archive and run the deterministic timeout regressions against it:
81
+
82
+ ```sh
83
+ mkdir -p build/release/extracted
84
+ tar -xzf build/release/demuxe-<version>.tgz -C build/release/extracted
85
+ RANGE_READER_MODULE="$PWD/build/release/extracted/package/web/range-reader.js" \
86
+ node --test tests/range-reader-deadline.mjs
87
+ ```
88
+
89
+ Record result paths and archive hashes in the release verification record. Recheck
90
+ both archive hashes immediately before distribution. Publish the tested runtime,
91
+ matching source companion, SHA256SUMS and verification record together. Packaging
92
+ creates a candidate; publication still requires the test results to pass and the
93
+ actual distribution/license arrangement to be settled. Never rebuild an archive
94
+ after testing and reuse the earlier test results for it.
95
+
96
+ Use the verifier to require both complete browser suites and run the deadline tests
97
+ against the archived reader, then write the final verification record:
98
+
99
+ ```sh
100
+ python3 scripts/verify-beta-release.py \
101
+ --archive build/release/demuxe-<version>.tgz \
102
+ --source build/release/demuxe-<version>-source.tar.gz \
103
+ --consumer <chrome-consumer-result.json> <firefox-consumer-result.json> \
104
+ --streaming <chrome-streaming-result.json> <firefox-streaming-result.json> \
105
+ --extra <release-extra-result.json>
106
+ ```
107
+
108
+ A changed runtime hash, source companion, tagged test harness, failed test, filtered
109
+ suite, or missing browser result prevents verification. Archive assembly does not
110
+ publish anything; distribute the verified files without running the packager again.
111
+
112
+ ## Optional preparation and ASS runtimes
113
+
114
+ When shipping the optional runtimes, pass their verified build directories to the
115
+ same tagged packager with `--adaptation-build <engine-directory>` and
116
+ `--ass-build <runtime-directory>`. Keep both optional source companions and their
117
+ hashes alongside the standard source companion. See
118
+ [Runtime assets](RUNTIME-ASSETS.md) for the pinned builds and asset-copy contract.
119
+
120
+ Run the optional matrix against the immutable tagged archive:
121
+
122
+ ```sh
123
+ python3 scripts/qualify-optional-runtime.py \
124
+ --archive <tagged-runtime.tgz> \
125
+ --adaptation-build <engine-directory> \
126
+ --ass-build <runtime-directory> \
127
+ --output <fresh-qualification-directory>
128
+ ```
129
+
130
+ Supply `--optional <fresh-qualification-directory>/qualification.json` to
131
+ `verify-beta-release.py`, alongside all standard consumer, transport and extra
132
+ results. Optional evidence from an earlier untagged archive cannot qualify a new
133
+ release archive. Qualification preserves the documented browser and source gates;
134
+ it does not admit Firefox Native long unequal tails or staged streaming adaptation.
135
+
136
+ ## Current component and npm consumer qualification
137
+
138
+ After the four archive suites above, run the full additional qualification against
139
+ that same archive. This installs into an empty project, invokes the npm executable,
140
+ checks package metadata and imports without a DOM, records npm's pack inventory,
141
+ and runs CLI collision/hash checks, TypeScript/static/bundled consumers, public API,
142
+ component, and menu regressions. The UI/API test server serves runtime code strictly
143
+ from the installed archive; only test pages and media come from the tagged source.
144
+
145
+ ```sh
146
+ BETA_ARCHIVE="$PWD/build/release/demuxe-0.3.0-beta.3.tgz" node tests/release-extra.mjs
147
+ ```
148
+
149
+ Pass `--extra <release-extra-result.json>` to `verify-beta-release.py`, in addition
150
+ to its archive/source/consumer/streaming arguments. It requires all extra cases and
151
+ checks their harness hashes against the source companion. Provision esbuild 0.28.2
152
+ in `build/public-api-tooling` for the bundled consumer checks. Component fixtures
153
+ use `fixtures/example.mp4`; the menu suite also runs in automated WebKit without
154
+ claiming Safari or physical mobile qualification.
155
+
156
+ ## First npm publication
157
+
158
+ The root `package.json` deliberately remains `private: true`. Never publish from
159
+ the source root. Publish only the runtime archive identified by `verification.json`;
160
+ do not rebuild or repack it after qualification. Keep the source companion,
161
+ `SHA256SUMS`, clean build record and verification record with the public release.
162
+ Before npm publication, make the matching source companion downloadable from the
163
+ GitHub release for the recorded tag; the npm package alone is not that source offer.
164
+
165
+ 1. Confirm `npm whoami`, account publishing access/2FA, and `demuxe` name
166
+ availability or ownership (`npm view demuxe name version maintainers`).
167
+ 2. Check archive metadata and `SHA256SUMS` against `verification.json`.
168
+ 3. Run `npm publish ./build/release/demuxe-0.3.0-beta.3.tgz --tag beta --access public --dry-run`.
169
+ 4. Only after all verification gates pass, explicitly publish:
170
+
171
+ ```sh
172
+ npm publish ./build/release/demuxe-0.3.0-beta.3.tgz --tag beta --access public
173
+ ```
174
+
175
+ Do not use `latest` for this beta. After publication, install `demuxe@beta` into
176
+ a brand-new temporary project, run `npx demuxe copy-assets public/assets/demuxe`,
177
+ import both `demuxe` and `demuxe/player`, and smoke-test one Native direct source
178
+ and one Hybrid/Software or Native-remux source using the installed runtime assets.
179
+ Check `npm view demuxe@beta version dist` and download the registry tarball to
180
+ compare its bytes/hash to the qualified archive. Once this first version exists,
181
+ configure npm trusted publishing for the exact GitHub workflow used for later releases.
@@ -0,0 +1,127 @@
1
+ # Runtime assets and package integration
2
+
3
+ The assetBase/copy-assets interfaces are implemented in this working candidate.
4
+ The project is not yet published to npm. Install the locally assembled archive:
5
+
6
+ ```sh
7
+ npm run build
8
+ python3 scripts/package-beta.py --output build/my-candidate
9
+ # In a clean application:
10
+ npm install /absolute/path/to/demuxe-0.3.0-beta.3.tgz
11
+ npx demuxe copy-assets public/assets/demuxe
12
+ ```
13
+
14
+ ```js
15
+ import {Player} from 'demuxe';
16
+ const player = new Player(container, {assetBase:'/assets/demuxe/'});
17
+ // Optional, separate UI entry:
18
+ import {definePlayerElement} from 'demuxe/player';
19
+ definePlayerElement();
20
+ ```
21
+
22
+ ```html
23
+ <demuxe-player controls asset-base="/assets/demuxe/"></demuxe-player>
24
+ ```
25
+
26
+ assetBase is the package runtime root containing web/, fixtures/, third_party/
27
+ and LICENSE. Its trailing slash is normalized. It is resolved against the page
28
+ base URL; same-origin HTTP(S) is required. The old static-directory installation
29
+ works without assetBase when generated modules retain their package paths.
30
+ Bundled applications should always provide assetBase. All inspector/worker entry
31
+ points, nested workers, engine modules/Wasm, AudioWorklet and fonts follow the
32
+ copied tree. Route-dependent loading is retained; core import and construction
33
+ perform no engine downloads. Import during SSR is safe; construction is browser-only.
34
+ The UI entry does not register anything until definePlayerElement is called.
35
+
36
+ copy-assets validates every package manifest hash before copying runtime assets,
37
+ font and notices. It writes demuxe-runtime.json with version and hashes. It never
38
+ deletes destination files; unrelated collisions and symlink paths reject. Updates
39
+ may replace only previous manifest-owned unmodified assets. Retain the generated
40
+ manifest with the installation. Use core and runtime from the same archive; mixing
41
+ releases or experimental presenters is unsupported. Failed copies should be rerun
42
+ from an intact package; use a versioned destination for atomic application rollout.
43
+
44
+ Serve JS/mjs as text/javascript, Wasm as application/wasm and fonts with font/ttf.
45
+ Worker routes require a secure context and COOP: same-origin plus COEP: require-corp.
46
+ Native direct can work without isolation. Media must satisfy CORS, range/identity
47
+ and source allowlist requirements. CSP must permit same-origin module scripts and
48
+ workers, Wasm compilation (wasm-unsafe-eval), same-origin worker-owner iframes,
49
+ AudioWorklet, fonts and authorized media/connect origins. Native local/remux media
50
+ needs media-src blob:. Component styles use a shadow style element; deployments
51
+ with strict style-src need an appropriate hash or policy for those shipped styles.
52
+ No consumer service worker is installed. The Pages isolation worker is demo-only.
53
+ Arbitrary CDN worker roots, Safari/mobile, PiP/casting and physical output fidelity
54
+ remain separate qualification gates. See LICENSING.md and RELEASE.md for source
55
+ and clean-engine-build obligations; this integration does not close them.
56
+
57
+ ## Optional experimental audio preparation
58
+
59
+ Local candidates built with `scripts/package-beta.py --adaptation-build <versioned-engine-dir>`
60
+ include `web/engine-adaptation/remux.mjs`, its matching `remux.wasm`, and a hash/inputs
61
+ manifest. The standard asset-copy CLI validates and copies them with the rest of
62
+ the runtime. Default packages omit these assets; ordinary Native playback does not
63
+ load them. A separately hashed `demuxe-audio-adaptation-source.tar.gz` accompanies
64
+ that candidate. This profile follows the saved FFmpeg modernization pins and does
65
+ not reuse the historical scratch LibAV binary. Preparation ABI 2 requires matching
66
+ worker, JavaScript and Wasm; mixing earlier optional binaries is rejected. Release
67
+ verification requires clean source correspondence and exact-archive optional tests;
68
+ see [current profiles, qualification and source limitations](OPTIMIZATION-COMPLETION.md).
69
+ Opus-enabled builds declare that profile in their manifest; Player still requires
70
+ explicit lossy permission. No package option enables automatic adaptation.
71
+
72
+
73
+ ## Optional external Native ASS
74
+
75
+ Pass `--ass-build <versioned-ass-dir>` for the pinned libass wrapper and matching
76
+ `web/engine-ass/subtitles.mjs` / `subtitles.wasm`. The worker is lazy, and the same
77
+ asset-copy CLI copies and verifies its manifest, default font and notices. The
78
+ separate `demuxe-native-ass-source.tar.gz` contains wrapper/build inputs and preferred
79
+ library sources. Both optional source companions are listed in SHA256SUMS when
80
+ packaged together. Their presence alone is not proof of source correspondence. Packaging verifies the
81
+ clean library/source record, and release verification requires the exact-archive
82
+ optional matrix plus all existing clean tagged-source and standard release gates. See the current optimization coverage report.
83
+
84
+ ### Isolated Native ASS build
85
+
86
+ `scripts/build-native-ass.py` builds only the four pinned subtitle libraries and
87
+ links the optional worker in a fresh output directory. It verifies the input
88
+ archives against `sources.lock.json`, uses a private Emscripten cache and prefix,
89
+ and records commands, source/library hashes and host tool identities. Existing
90
+ checkouts and library archives are read-only inputs; no playback engine is rebuilt.
91
+
92
+ ```sh
93
+ python3 scripts/build-native-ass.py --sdk /path/to/emsdk-4.0.14 \
94
+ --archives /path/to/verified-downloads --meson /path/to/meson \
95
+ --output build/native-ass-clean
96
+ python3 scripts/verify-native-ass-build.py build/native-ass-clean/runtime
97
+ python3 scripts/package-beta.py --output build/optional-candidate \
98
+ --ass-build build/native-ass-clean/runtime
99
+ python3 tests/native-ass-source-build.py
100
+ ```
101
+
102
+ The archive directory must contain `freetype.tar.gz`, `fribidi.tar.gz`,
103
+ `harfbuzz.tar.gz`, and `libass.tar.gz` matching the unchanged source lock. The
104
+ builder refuses an existing output directory. Use a second fresh output to compare
105
+ runtime and library hashes. The historical Linux toolchain lock is not evidence
106
+ for a macOS build; the source-build record declares that distinction.
107
+
108
+ For clean builds, packaging verifies the source-build record before copying assets
109
+ and includes it and the isolated builder in the source companion. Installed asset
110
+ tests verify the companion's preferred sources and wrapper against those hashes.
111
+ A passing local correspondence check does not authorize release packaging; the
112
+ existing tagged-source, full-consumer and streaming release gates still apply.
113
+
114
+ ## Exact-archive optional qualification
115
+
116
+ Run `scripts/qualify-optional-runtime.py --archive <candidate.tgz>
117
+ --ass-build <clean-ass/runtime> --adaptation-build <clean-preparation/engine>
118
+ --output <fresh-directory>`. It verifies matching source builds and tests installed
119
+ assets, consumers, automatic admission, subtitles, filters/gain, FLAC/Opus fidelity,
120
+ lifecycle and the documented unequal-tail browser policy. Failed runs are retained.
121
+
122
+ The standard `scripts/verify-beta-release.py` requires `--optional
123
+ <qualification.json>` whenever either optional engine is packaged. It checks the
124
+ archive, runtime inventory, required Chrome/Firefox matrix, tagged harness hashes,
125
+ log hashes and source-companion hashes. This adds a gate; it does not replace clean
126
+ source/tag, engine build, streaming, consumer or extra release requirements. No
127
+ qualification script creates tags or publishes. See the current closeout evidence.