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,372 @@
1
+ import { PlayerError } from './errors.js';
2
+ /** One isolated software engine per player; bounded remote ranges and local File reads; ArrayBuffer inputs remain capped. */
3
+ export class WasmPlayer extends EventTarget {
4
+ loading = new AbortController();
5
+ worker;
6
+ workerOwner;
7
+ audioContext;
8
+ audioNode;
9
+ analyser;
10
+ gainNode;
11
+ gainValue = 1;
12
+ timing;
13
+ lastTiming;
14
+ nextId = 100;
15
+ pending = new Map();
16
+ destroyed = false;
17
+ destruction;
18
+ onDestroyed;
19
+ readyTimer;
20
+ rejectReady;
21
+ eventWaiters = new Set();
22
+ hasFile = false;
23
+ opening = false;
24
+ refreshAuthorization;
25
+ audioHeader;
26
+ outputChannels;
27
+ requestedOutput;
28
+ deviceChannels;
29
+ diagnostics;
30
+ browserCodecsAbsent = false;
31
+ properties = new Map();
32
+ ready;
33
+ constructor(canvas, { disableBrowserCodecs = false, measureOutput = false, mode = 'software', softwarePresenter = 'rgb', audioOutput = 'stereo', audioFallback = 'stereo', resourceLimits = {}, fonts = [], assetBase = new URL('../../../', import.meta.url) } = {}) {
34
+ super();
35
+ const decoder = mode === 'hybrid' ? 'webcodecs' : 'software';
36
+ if (!crossOriginIsolated)
37
+ throw new Error('This player requires a secure, cross-origin isolated page.');
38
+ this.audioContext = new AudioContext({ latencyHint: 'interactive' });
39
+ this.requestedOutput = audioOutput;
40
+ this.deviceChannels = this.audioContext.destination.maxChannelCount;
41
+ const wanted = audioOutput === 'auto' ? (this.deviceChannels >= 8 ? 8 : this.deviceChannels >= 6 ? 6 : 2) : audioOutput === '7.1' ? 8 : audioOutput === '5.1' ? 6 : 2;
42
+ if (wanted > this.deviceChannels && audioFallback === 'reject') {
43
+ void this.audioContext.close();
44
+ throw Error('Requested audio layout is unavailable on this output device');
45
+ }
46
+ this.outputChannels = wanted <= this.deviceChannels ? wanted : 2;
47
+ try {
48
+ this.audioContext.destination.channelCount = this.outputChannels;
49
+ }
50
+ catch (error) {
51
+ void this.audioContext.close();
52
+ throw error;
53
+ }
54
+ this.audioContext.destination.channelCountMode = 'explicit';
55
+ // A disposable same-origin owner gives the browser a complete worker-tree
56
+ // teardown boundary, including native pthread workers and decoder resources.
57
+ this.workerOwner = canvas.ownerDocument.createElement('iframe');
58
+ this.workerOwner.hidden = true;
59
+ this.workerOwner.setAttribute('aria-hidden', 'true');
60
+ canvas.ownerDocument.body.append(this.workerOwner);
61
+ const owner = this.workerOwner.contentWindow;
62
+ try {
63
+ this.worker = new owner.Worker(new URL(mode === 'hybrid' ? 'web/filter-retained-engine-worker.js?mode=retained' : 'web/software-full-engine-worker.js', assetBase), { type: 'module' });
64
+ }
65
+ catch (error) {
66
+ this.workerOwner.remove();
67
+ void this.audioContext.close();
68
+ throw error;
69
+ }
70
+ const audio = new SharedArrayBuffer(64 + 8192 * this.outputChannels * 4);
71
+ this.audioHeader = new Int32Array(audio, 0, 16);
72
+ this.ready = new Promise((resolve, reject) => {
73
+ this.rejectReady = reject;
74
+ const timeout = this.readyTimer = setTimeout(() => reject(new Error('Player initialization timed out')), 60000);
75
+ this.worker.onerror = event => { clearTimeout(timeout); reject(new PlayerError('ASSET_LOAD_FAILED', 'Playback engine initialization failed: ' + event.message, null, null, 'operation', true)); this.fail(new Error(event.message)); };
76
+ this.worker.onmessage = ({ data }) => {
77
+ if (data.type === 'ready') {
78
+ clearTimeout(timeout);
79
+ this.browserCodecsAbsent = data.browserCodecsAbsent;
80
+ this.sendTiming(true);
81
+ resolve();
82
+ }
83
+ else if (data.type === 'error') {
84
+ clearTimeout(timeout);
85
+ const error = new Error(data.message);
86
+ reject(new PlayerError('ASSET_LOAD_FAILED', 'Playback engine initialization failed: ' + error.message, null, null, 'operation', true));
87
+ this.fail(error, data.id);
88
+ }
89
+ else if (data.type === 'destroyed') {
90
+ if (this.diagnostics) {
91
+ this.diagnostics.decoderStats = data.decoderStats;
92
+ if (data.presentation)
93
+ this.diagnostics.presentation = data.presentation;
94
+ }
95
+ this.onDestroyed?.();
96
+ }
97
+ else if (data.type === 'refresh') {
98
+ void this.refreshAuthorization?.(data.resource).then(update => { if (!this.destroyed)
99
+ this.worker.postMessage({ type: 'refreshed', id: data.id, update }); }, () => { if (!this.destroyed)
100
+ this.worker.postMessage({ type: 'refreshed', id: data.id, error: true }); });
101
+ }
102
+ else if (data.type === 'output')
103
+ this.dispatchEvent(new CustomEvent('output', { detail: data.data }));
104
+ else if (data.type === 'source')
105
+ this.dispatchEvent(new CustomEvent('source', { detail: data.info }));
106
+ else if (data.type === 'diagnostics')
107
+ this.diagnostics = data.data;
108
+ else if (data.type === 'log')
109
+ this.dispatchEvent(new CustomEvent('log', { detail: data.message }));
110
+ else if (data.type === 'event') {
111
+ const event = data.event;
112
+ if (event.event === 'start-file')
113
+ this.hasFile = true;
114
+ if (event.event === 'end-file')
115
+ this.hasFile = false;
116
+ if (event.event === 'property-change' && event.name === 'track-list' && Array.isArray(event.data))
117
+ event.data = event.data.map(track => ({ ...track, id: String(track.id) }));
118
+ if (event.event === 'command-reply' && event.id) {
119
+ const pending = this.pending.get(event.id);
120
+ if (pending) {
121
+ clearTimeout(pending.timer);
122
+ this.pending.delete(event.id);
123
+ event.error ? pending.reject(new Error(event.error)) : pending.resolve(event.result);
124
+ }
125
+ }
126
+ if (event.event === 'property-change' && event.name === 'track-list' && Array.isArray(event.data)) {
127
+ let external = 0;
128
+ event.data = event.data.map(t => t.external ? { ...t, 'external-index': ++external } : t);
129
+ }
130
+ if (event.event === 'property-change' && event.name)
131
+ this.properties.set(event.name, event.data);
132
+ this.dispatchEvent(new CustomEvent('mpv', { detail: event }));
133
+ }
134
+ };
135
+ void (async () => {
136
+ await this.audioContext.audioWorklet.addModule(new URL('web/audio-worklet.js', assetBase));
137
+ if (this.destroyed)
138
+ throw new Error('Player destroyed during initialization');
139
+ this.audioNode = new AudioWorkletNode(this.audioContext, 'demuxe-pcm', { numberOfInputs: 0, numberOfOutputs: 1, outputChannelCount: [this.outputChannels], channelCount: this.outputChannels, channelCountMode: 'explicit', processorOptions: { buffer: audio, capacity: 8192, channels: this.outputChannels, measureOutput } });
140
+ this.audioNode.port.onmessage = ({ data }) => { const stamp = this.audioContext.getOutputTimestamp(); const wallTime = stamp.performanceTime !== undefined && stamp.contextTime !== undefined ? performance.timeOrigin + stamp.performanceTime + (data.audioFrame / data.sampleRate - stamp.contextTime) * 1000 : null; this.dispatchEvent(new CustomEvent('output', { detail: { ...data, wallTime, stamp } })); };
141
+ this.analyser = this.audioContext.createAnalyser();
142
+ this.audioNode.connect(this.analyser);
143
+ this.audioNode.connect(this.audioContext.destination);
144
+ const response = await fetch(new URL('fixtures/DejaVuSans.ttf', assetBase), { signal: this.loading.signal });
145
+ if (!response.ok)
146
+ throw new Error('Could not load the bundled subtitle font');
147
+ const font = await response.arrayBuffer();
148
+ if (this.destroyed)
149
+ throw new Error('Player destroyed during initialization');
150
+ const offscreen = canvas.transferControlToOffscreen();
151
+ this.worker.postMessage({ type: 'init', canvas: offscreen, audio, font, fonts, audioChannels: this.outputChannels, maxDecodePixels: resourceLimits.maxDecodePixels, maxAllocationBytes: resourceLimits.maxAllocationBytes, sampleRate: this.audioContext.sampleRate, disableBrowserCodecs, measureOutput, decoder, softwarePresenter, decoderFaultAfter: 0 }, [offscreen, font]);
152
+ this.timing = setInterval(() => this.sendTiming(), 20);
153
+ this.sendTiming();
154
+ })().catch(error => { clearTimeout(timeout); reject(new PlayerError('ASSET_LOAD_FAILED', 'Playback engine initialization failed: ' + String(error), null, null, 'operation', true)); });
155
+ });
156
+ }
157
+ sendTiming(force = false) {
158
+ if (this.destroyed)
159
+ return;
160
+ // Fallback latency estimate, explicitly not an independent A/V sync measurement.
161
+ const latency = (this.audioContext.baseLatency || 0) + (this.audioContext.outputLatency || 0);
162
+ const latencyUs = Math.round(latency * 1e6), running = this.audioContext.state === 'running';
163
+ if (!force && this.lastTiming?.latencyUs === latencyUs && this.lastTiming.running === running)
164
+ return;
165
+ this.lastTiming = { latencyUs, running };
166
+ this.worker.postMessage({ type: 'timing', latencyUs, running });
167
+ }
168
+ fail(error, id, report = true) {
169
+ for (const [key, p] of this.pending)
170
+ if (!id || key === id) {
171
+ clearTimeout(p.timer);
172
+ p.reject(error);
173
+ this.pending.delete(key);
174
+ }
175
+ if (report)
176
+ for (const cancel of this.eventWaiters)
177
+ cancel(error);
178
+ if (report)
179
+ this.dispatchEvent(new CustomEvent('error', { detail: error.message }));
180
+ }
181
+ request(message, transfer = []) {
182
+ if (this.destroyed)
183
+ return Promise.reject(new Error('Player is destroyed'));
184
+ if (this.pending.size >= 128)
185
+ return Promise.reject(new Error('Command queue is full'));
186
+ const id = this.nextId++;
187
+ return new Promise((resolve, reject) => {
188
+ const timer = setTimeout(() => { this.pending.delete(id); reject(new Error('Command timed out')); }, 15000);
189
+ this.pending.set(id, { resolve, reject, timer });
190
+ this.worker.postMessage({ ...message, id }, transfer);
191
+ });
192
+ }
193
+ async open(file, options = {}) {
194
+ if (this.destroyed)
195
+ throw new Error('Player is destroyed');
196
+ if (this.opening)
197
+ throw new Error('Another open is in progress');
198
+ this.opening = true;
199
+ try {
200
+ await this.openLocal(file, options);
201
+ }
202
+ finally {
203
+ this.opening = false;
204
+ }
205
+ }
206
+ async openRemote(source) {
207
+ if (this.destroyed)
208
+ throw new Error('Player is destroyed');
209
+ if (this.opening)
210
+ throw new Error('Another open is in progress');
211
+ this.opening = true;
212
+ try {
213
+ await this.ready;
214
+ if (this.hasFile)
215
+ await Promise.all([this.waitForEvent(e => e.event === 'end-file'), this.command('stop')]);
216
+ else
217
+ await this.command('stop');
218
+ if (source.demuxer && !/^[a-z0-9_]{1,64}$/.test(source.demuxer))
219
+ throw Error('Invalid demuxer hint');
220
+ await this.command('set', 'demuxer-lavf-format', source.demuxer ?? '');
221
+ const { refreshAuthorization, ...options } = source;
222
+ this.refreshAuthorization = refreshAuthorization;
223
+ const loaded = this.waitForEvent(e => e.event === 'file-loaded' || (e.event === 'end-file' && e.reason === 'error' ? new Error(String(e.file_error)) : false));
224
+ await Promise.all([loaded, this.request({ type: 'open-remote', options, canRefresh: !!refreshAuthorization })]);
225
+ }
226
+ finally {
227
+ this.opening = false;
228
+ }
229
+ }
230
+ waitForEvent(predicate) {
231
+ return new Promise((resolve, reject) => {
232
+ const finish = (error) => { clearTimeout(timeout); this.removeEventListener('mpv', listener); this.eventWaiters.delete(cancel); error ? reject(error) : resolve(); };
233
+ const cancel = (error) => finish(error);
234
+ const listener = (event) => { const result = predicate(event.detail); if (result)
235
+ finish(result instanceof Error ? result : undefined); };
236
+ const timeout = setTimeout(() => finish(new Error('Media operation timed out')), 25000);
237
+ this.eventWaiters.add(cancel);
238
+ this.addEventListener('mpv', listener);
239
+ });
240
+ }
241
+ async openLocal(file, options) {
242
+ await this.ready;
243
+ const size = file instanceof File ? file.size : file.byteLength;
244
+ if (!(file instanceof File) && size > 32 * 1024 * 1024)
245
+ throw new Error('ArrayBuffer sources are limited to 32 MiB');
246
+ if (this.hasFile)
247
+ await Promise.all([this.waitForEvent(event => event.event === 'end-file'), this.command('stop')]);
248
+ else
249
+ await this.command('stop');
250
+ const suffix = file instanceof File ? file.name.split('.').at(-1)?.toLowerCase() : undefined;
251
+ const demuxer = options.demuxer ?? (suffix === 'sbc' || suffix === 'msbc' ? 'sbc' : '');
252
+ if (demuxer && !/^[a-z0-9_]{1,64}$/.test(demuxer))
253
+ throw Error('Invalid demuxer hint');
254
+ await this.command('set', 'demuxer-lavf-format', demuxer);
255
+ const loaded = this.waitForEvent(event => event.event === 'file-loaded' || (event.event === 'end-file' && event.reason === 'error' ? new Error(String(event.file_error)) : false));
256
+ if (file instanceof File)
257
+ await Promise.all([loaded, this.request({ type: 'open-file', file })]);
258
+ else {
259
+ const bytes = file.slice(0);
260
+ await Promise.all([loaded, this.request({ type: 'open', bytes }, [bytes])]);
261
+ }
262
+ }
263
+ async inspectMetadata() {
264
+ const value = await this.request({ type: 'command', args: ['expand-text', '${seekable}'] });
265
+ if (value === 'yes' || value === 'no')
266
+ this.properties.set('seekable', value === 'yes');
267
+ }
268
+ async command(...args) { await this.ready; return this.request({ type: 'command', args }); }
269
+ async setPause(paused) {
270
+ if (this.properties.get('pause') === paused) {
271
+ await this.command('set', 'pause', paused ? 'yes' : 'no');
272
+ return;
273
+ }
274
+ await Promise.all([this.waitForEvent(e => e.event === 'property-change' && e.name === 'pause' && e.data === paused), this.command('set', 'pause', paused ? 'yes' : 'no')]);
275
+ }
276
+ async play() {
277
+ const resume = this.audioContext.resume();
278
+ void resume.catch(() => { });
279
+ if (this.audioContext.state === 'suspended' && !navigator.userActivation?.isActive)
280
+ throw new DOMException('Playback needs a user gesture', 'NotAllowedError');
281
+ await resume;
282
+ this.sendTiming();
283
+ await this.setPause(false);
284
+ }
285
+ pause() { return this.setPause(true); }
286
+ seek(seconds) { if (!Number.isFinite(seconds) || seconds < 0)
287
+ throw new Error('Invalid seek time'); Atomics.store(this.audioHeader, 2, 0); return this.ready.then(() => this.request({ type: 'seek', seconds })); }
288
+ rate(rate) { if (!Number.isFinite(rate) || rate < 0.5 || rate > 2)
289
+ throw new Error('Playback rate must be 0.5 to 2'); return this.command('set', 'speed', String(rate)); }
290
+ volume(percent) { if (!Number.isFinite(percent) || percent < 0 || percent > 100)
291
+ throw new Error('Invalid volume'); return this.command('set', 'volume', String(percent)); }
292
+ async gain(value) {
293
+ if (!Number.isFinite(value) || value < 0 || value > 1)
294
+ throw new Error('Gain must be between 0 and 1');
295
+ await this.ready;
296
+ if (this.destroyed)
297
+ throw new Error('Player is destroyed');
298
+ if (!this.gainNode && value !== 1) {
299
+ const gain = this.audioContext.createGain();
300
+ gain.channelCount = this.outputChannels;
301
+ gain.channelCountMode = 'explicit';
302
+ gain.channelInterpretation = 'discrete';
303
+ gain.gain.setValueAtTime(value, this.audioContext.currentTime);
304
+ this.audioNode.disconnect();
305
+ this.audioNode.connect(gain);
306
+ gain.connect(this.audioContext.destination);
307
+ gain.connect(this.analyser);
308
+ this.gainNode = gain;
309
+ }
310
+ this.gainNode?.gain.setValueAtTime(value, this.audioContext.currentTime);
311
+ this.gainValue = value;
312
+ }
313
+ selectTrack(type, id) {
314
+ if (!['audio', 'sub'].includes(type) || !/^(?:[1-9][0-9]*|auto|no)$/.test(id))
315
+ throw new Error('Invalid track selection');
316
+ return this.command('set', type === 'audio' ? 'aid' : 'sid', id);
317
+ }
318
+ async addSubtitle(subtitle) {
319
+ await this.ready;
320
+ const bytes = subtitle.bytes.slice(0);
321
+ const previous = (this.properties.get('track-list') ?? []).filter(t => t.external).length;
322
+ // Command acceptance can precede the track-list event. Selection must wait
323
+ // for the new source-scoped external identity to become observable.
324
+ const listed = this.waitForEvent(e => e.event === 'property-change' && e.name === 'track-list' && Array.isArray(e.data) && e.data.filter(t => t.external).length > previous);
325
+ await Promise.all([listed, this.request({ type: 'subtitle', ...subtitle, bytes }, [bytes])]);
326
+ }
327
+ subtitleVisible(visible) { return this.command('set', 'sub-visibility', visible ? 'yes' : 'no'); }
328
+ resize(width, height) { if (this.destroyed)
329
+ throw new Error('Player is destroyed'); if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width > 1920 || height > 1080)
330
+ throw new Error('Invalid output dimensions'); this.worker.postMessage({ type: 'resize', width, height }); }
331
+ audioDiagnostics() {
332
+ const samples = new Float32Array(this.analyser?.fftSize || 2048);
333
+ this.analyser?.getFloatTimeDomainData(samples);
334
+ return { gain: this.gainValue, gainStage: this.gainNode ? 'web-audio' : 'none', requestedOutput: this.requestedOutput, outputChannels: this.outputChannels, deviceChannels: this.deviceChannels, channelLayout: this.outputChannels === 8 ? '7.1' : this.outputChannels === 6 ? '5.1' : 'stereo', state: this.audioContext.state, sampleRate: this.audioContext.sampleRate, mediaFrames: Atomics.load(this.audioHeader, 5), underruns: Atomics.load(this.audioHeader, 6), rms: Math.sqrt(samples.reduce((sum, v) => sum + v * v, 0) / samples.length), latencyConfidence: 'reported-latency estimate' };
335
+ }
336
+ destroy() {
337
+ if (this.destruction)
338
+ return this.destruction;
339
+ this.destroyed = true;
340
+ this.loading.abort();
341
+ this.refreshAuthorization = undefined;
342
+ clearTimeout(this.readyTimer);
343
+ this.rejectReady?.(new Error('Player destroyed'));
344
+ for (const cancel of this.eventWaiters)
345
+ cancel(new Error('Player destroyed'));
346
+ this.fail(new Error('Player destroyed'), undefined, false);
347
+ clearInterval(this.timing);
348
+ Atomics.store(this.audioHeader, 2, 0);
349
+ this.audioNode?.port.postMessage('close');
350
+ this.audioNode?.disconnect();
351
+ this.audioNode?.port.close();
352
+ this.analyser?.disconnect();
353
+ this.gainNode?.disconnect();
354
+ this.destruction = (async () => {
355
+ let timeout;
356
+ try {
357
+ await new Promise((resolve, reject) => {
358
+ this.onDestroyed = resolve;
359
+ timeout = setTimeout(() => reject(new Error('Native cleanup timed out; worker containment applied')), 10000);
360
+ this.worker.postMessage({ type: 'destroy' });
361
+ });
362
+ }
363
+ finally {
364
+ clearTimeout(timeout);
365
+ this.worker.terminate();
366
+ this.workerOwner.remove();
367
+ await this.audioContext.close();
368
+ }
369
+ })();
370
+ return this.destruction;
371
+ }
372
+ }
@@ -0,0 +1,198 @@
1
+ import { Player } from '../unified-player.js';
2
+ import type { MediaSourceInput, OpenOptions, SubtitleOptions } from '../types.js';
3
+ declare const Base: typeof HTMLElement;
4
+ export declare const defaultLabels: Readonly<{
5
+ diagnostics: "Session diagnostics";
6
+ moreOptions: "More options";
7
+ back: "Seek backward 10 seconds";
8
+ forward: "Seek forward 10 seconds";
9
+ play: "Play";
10
+ pause: "Pause";
11
+ mute: "Mute";
12
+ unmute: "Unmute";
13
+ seek: "Playback position";
14
+ volume: "Volume";
15
+ settings: "Playback settings";
16
+ closeSettings: "Close settings";
17
+ speed: "Playback speed";
18
+ audio: "Audio";
19
+ subtitles: "Subtitles";
20
+ automatic: "Automatic";
21
+ off: "Off";
22
+ fullscreen: "Fullscreen";
23
+ exitFullscreen: "Exit fullscreen";
24
+ open: "Open media";
25
+ addSubtitle: "Add subtitles";
26
+ empty: "Something good to watch?";
27
+ drop: "Open a video or audio file from your device.";
28
+ loading: "Opening media…";
29
+ switching: "Updating playback…";
30
+ seeking: "Seeking…";
31
+ buffering: "Buffering…";
32
+ live: "LIVE";
33
+ unknown: "Unknown duration";
34
+ retry: "Retry";
35
+ resume: "Press Play to continue";
36
+ shortcuts: "K / Space: play · ← → / J L: seek · ↑ ↓: volume · M: mute · C: subtitles · [ ]: speed · 0–9 / Home / End: position · F: fullscreen";
37
+ noFullscreen: "Fullscreen is unavailable here. Open this page in a browser tab.";
38
+ noWindow: "Live playback · seek window unavailable";
39
+ openURL: "Open URL";
40
+ closeMedia: "Close media";
41
+ url: "Media URL";
42
+ format: "Source format";
43
+ streamLive: "Live stream";
44
+ addFiles: "Add files";
45
+ queue: "Queue";
46
+ clearQueue: "Clear queue";
47
+ previous: "Previous file";
48
+ next: "Next file";
49
+ remove: "Remove";
50
+ unnamed: "Unnamed media";
51
+ mediaFile: "Media file";
52
+ noMedia: "No media loaded";
53
+ loadedMedia: "Media loaded";
54
+ subtitleFile: "Subtitle file";
55
+ }>;
56
+ export type PlayerLabels = Partial<Record<keyof typeof defaultLabels, string>>;
57
+ export type PlayerTitleMode = 'auto' | 'custom' | 'source' | 'none';
58
+ export declare class DemuxePlayerElement extends Base {
59
+ static observedAttributes: string[];
60
+ private core?;
61
+ private queueItems;
62
+ private queueIndex;
63
+ private queueOperation?;
64
+ private queuePlayIntent?;
65
+ private queueSourceId;
66
+ private queueEndedId;
67
+ private queueSignature;
68
+ private queueRevision;
69
+ private queueRenderSignature;
70
+ private queueItem;
71
+ private resetQueue;
72
+ private activateQueue;
73
+ private addFiles;
74
+ private selectQueue;
75
+ private removeQueueItem;
76
+ private advanceQueue;
77
+ private renderQueue;
78
+ private sourceName;
79
+ private sourceNameId;
80
+ private sourceControls;
81
+ private diagnosticsControl;
82
+ private fileDrop;
83
+ private seekSeconds;
84
+ private autoHideDelay;
85
+ get titleMode(): PlayerTitleMode;
86
+ set titleMode(value: PlayerTitleMode);
87
+ get showSourceControls(): boolean;
88
+ set showSourceControls(value: boolean);
89
+ get showDiagnostics(): boolean;
90
+ set showDiagnostics(value: boolean);
91
+ get allowFileDrop(): boolean;
92
+ set allowFileDrop(value: boolean);
93
+ get seekStep(): number;
94
+ set seekStep(value: number);
95
+ get controlsAutoHideDelay(): number;
96
+ set controlsAutoHideDelay(value: number);
97
+ private updateTitle;
98
+ private updateSourceLabel;
99
+ private updateUtilities;
100
+ private terminal;
101
+ private cleanup;
102
+ private connecting?;
103
+ private connection;
104
+ private unsubscribe?;
105
+ private sourceAbort?;
106
+ private sourceVersion;
107
+ private lastSource?;
108
+ private lastOptions?;
109
+ private resolveReady;
110
+ private rejectReady;
111
+ private readiness;
112
+ private overrides;
113
+ private menuTrigger;
114
+ private seekPreviewTimer?;
115
+ private hideTimer?;
116
+ private revealControls;
117
+ private playFromControls;
118
+ private hideControls;
119
+ private dismissMenu;
120
+ private stageWasIdle;
121
+ private isScreenPress;
122
+ private wasSeeking;
123
+ private diagnosticsUpdated;
124
+ private dragging;
125
+ private dimensions;
126
+ private trackSignature;
127
+ private reflected;
128
+ private attributeScheduled;
129
+ private configuredAsset;
130
+ private resizeObserver?;
131
+ private lastAnnouncement;
132
+ private lastFailure?;
133
+ private fullscreenChanged;
134
+ constructor();
135
+ private newReady;
136
+ get ready(): Promise<Player>;
137
+ get player(): Player | undefined;
138
+ get src(): string;
139
+ set src(value: string);
140
+ get controls(): boolean;
141
+ set controls(value: boolean);
142
+ get autoplay(): boolean;
143
+ set autoplay(value: boolean);
144
+ get muted(): boolean;
145
+ set muted(value: boolean);
146
+ get poster(): string;
147
+ set poster(value: string);
148
+ get assetBase(): string | undefined;
149
+ set assetBase(value: string | undefined);
150
+ get labels(): Record<keyof typeof defaultLabels, string>;
151
+ set labels(value: PlayerLabels);
152
+ private $;
153
+ private input;
154
+ connectedCallback(): void;
155
+ disconnectedCallback(): void;
156
+ attributeChangedCallback(name: string, old: string | null, value: string | null): void;
157
+ private openFromControls;
158
+ private scheduleSource;
159
+ private waitReady;
160
+ open(source: MediaSourceInput, options?: OpenOptions): Promise<void>;
161
+ private openSource;
162
+ close(): Promise<void>;
163
+ play(): Promise<void>;
164
+ pause(): Promise<void>;
165
+ seek(seconds: number): Promise<void>;
166
+ setVolume(value: number): Promise<void>;
167
+ setMuted(value: boolean): Promise<void>;
168
+ setPlaybackRate(value: number): Promise<void>;
169
+ selectAudioTrack(id: string | null): Promise<void>;
170
+ selectSubtitleTrack(id: string | null): Promise<void>;
171
+ addSubtitle(file: File, options?: SubtitleOptions): Promise<void>;
172
+ destroy(): Promise<void>;
173
+ private run;
174
+ private componentError;
175
+ private showError;
176
+ private clearError;
177
+ private announce;
178
+ private geometry;
179
+ private update;
180
+ private setDiagnostics;
181
+ private updateDiagnostics;
182
+ private bufferedProgress;
183
+ private timelineProgress;
184
+ private skip;
185
+ private trackOptions;
186
+ private settings;
187
+ private fullscreen;
188
+ private iconButton;
189
+ private labelControls;
190
+ private renderShell;
191
+ }
192
+ export declare function definePlayerElement(name?: string): typeof DemuxePlayerElement;
193
+ declare global {
194
+ interface HTMLElementTagNameMap {
195
+ 'demuxe-player': DemuxePlayerElement;
196
+ }
197
+ }
198
+ export {};