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,234 @@
1
+ let audioChannels=2;
2
+ let YUVPresenter,uploader,gpuPauseIntent;
3
+ function installPresenter(canvas,prior){
4
+ uploader=new YUVPresenter(canvas);
5
+ if(prior)uploader.stats={...prior,liveTextures:4,contextRestores:(prior.contextRestores||0)+1};
6
+ uploader.onLost=()=>{gpuPauseIntent=paused;internalCommand(['set','pause','yes'],()=>{});post({type:'output',data:{kind:'gpu-context-lost'}});};
7
+ uploader.onRestore=()=>{if(closing)return;const old=uploader,stats={...old.stats};old.destroy(true);const intent=gpuPauseIntent;gpuPauseIntent=undefined;installPresenter(canvas,stats);internalCommand(['set','pause',intent?'yes':'no'],()=>{});force=true;schedulePump(0);post({type:'output',data:{kind:'gpu-context-restored'}});};
8
+ }
9
+ let decoderWorker,decoderStats;
10
+
11
+
12
+ let engine, canvas, context, timer, audio, pcm, nativeAudio, epoch = -1, forwarded = 0;
13
+ let renderMs=0,copyMs=0,maxRenderMs=0;
14
+ let rendered = 0, sourceRendered = 0, ticks = 0, force = true, closing = false, presentedPosition=0, frameImage, measureOutput=false, wasWhite=false;
15
+ let ioWorker,ioStats,ioReady,ioClose,ioSession=0,pendingTarget=null,restarted=false,position=0;
16
+ let internalId=0x80000000,demuxFormat='',seekPrerollSeconds=0;
17
+ const internalCommands=new Map();
18
+ function internalCommand(args,done){const id=internalId++;internalCommands.set(id,done);submit(id,args);}
19
+ const CAPACITY = 8192;
20
+ let paused=true,busyUntil=0,pumpFailed=false,nextDiagnostics=0;
21
+ function schedulePump(delay=5){
22
+ clearTimeout(timer);if(closing||pumpFailed||!engine)return;
23
+ timer=setTimeout(()=>{
24
+ schedulePump(paused&&pendingTarget===null&&performance.now()>=busyUntil?100:5);
25
+ tick();
26
+ },delay);
27
+ }
28
+
29
+ function releaseSeek(){if(pendingTarget!==null&&restarted&&Math.abs(position-pendingTarget)<0.15){pendingTarget=null;force=true;post({type:'seek-complete',position});}}
30
+ async function closeIO(){if(!ioWorker)return;const old=ioWorker;engine._web_io_cancel();await new Promise(resolve=>{ioClose=resolve;old.postMessage({type:'close'});setTimeout(resolve,1500);});old.terminate();ioWorker=null;ioClose=null;engine.ccall('web_io_root',null,['number','string'],[0,'']);}
31
+ async function openRemote(data){
32
+ sourceRendered=0;
33
+ await closeIO();
34
+ const pointer=engine._web_io_ptr();
35
+ ioWorker=new Worker(new URL('./io-worker.js',import.meta.url),{type:'module'});
36
+ const info=await new Promise((resolve,reject)=>{
37
+ const timeout=setTimeout(()=>reject(Error('Remote open timed out')),20000);
38
+ ioWorker.onmessage=({data:message})=>{
39
+ if(message.type==='ready'){clearTimeout(timeout);resolve(message.info);}
40
+ else if(message.type==='error'){clearTimeout(timeout);reject(Error(message.message));post({type:'error',id:data.id,message:message.message});}
41
+ else if(message.type==='stats')ioStats=message.stats;
42
+ else if(message.type==='refresh')post({type:'refresh',id:message.id,resource:message.resource});
43
+ else if(message.type==='closed')ioClose?.();
44
+ };
45
+ ioWorker.onerror=event=>{clearTimeout(timeout);reject(Error(event.message));};
46
+ ioWorker.postMessage({type:'init',memory:engine.HEAPU8.buffer,pointer,options:data.options??{},file:data.file,canRefresh:data.canRefresh});
47
+ });
48
+ if(closing)throw Error('Player closed');
49
+ engine._web_io_configure(++ioSession,BigInt(info.size));
50
+ engine.ccall('web_io_root',null,['number','string'],[info.resource??0,info.url??'']);
51
+ post({type:'source',info});submit(data.id,['loadfile','brange://source','replace']);
52
+ }
53
+ const post = message => self.postMessage(message);
54
+ function pumpAudio() {
55
+ // Refresh views after every possible Wasm memory growth.
56
+ const h = engine.HEAPU32;
57
+ const at = nativeAudio >>> 2;
58
+ const nextEpoch = Atomics.load(h, at + 3);
59
+ if (nextEpoch & 1) return;
60
+ if (epoch !== nextEpoch) {
61
+ epoch = nextEpoch;
62
+ forwarded = 0;
63
+ Atomics.store(audio, 2, 0);
64
+ Atomics.store(audio, 0, 0);
65
+ Atomics.store(audio, 3, epoch);
66
+ return;
67
+ }
68
+ if (Atomics.load(audio, 4) !== epoch) return;
69
+ const consumed = Atomics.load(audio, 1) >>> 0;
70
+ Atomics.store(h, at + 7, 0);
71
+ Atomics.store(h, at + 1, consumed);
72
+ Atomics.store(h, at + 7, epoch);
73
+ const written = Atomics.load(h, at);
74
+ const count = (written - forwarded) >>> 0;
75
+ if (Atomics.load(h, at + 3) !== epoch) return;
76
+ if (count > CAPACITY) throw new Error('PCM capacity invariant violated');
77
+ const source = (nativeAudio + 32) >>> 2;
78
+ for (let i = 0; i < count; i++) {
79
+ const index = ((forwarded + i) % CAPACITY) * audioChannels;
80
+ for(let c=0;c<audioChannels;c++)pcm[index+c]=engine.HEAPF32[source+index+c];
81
+ }
82
+ // A reset during the copy discards this batch before publishing it.
83
+ if (Atomics.load(h, at + 3) !== epoch) return;
84
+ forwarded = written;
85
+ Atomics.store(audio, 0, written);
86
+ Atomics.store(audio, 2, pendingTarget===null?Atomics.load(h, at + 2):0);
87
+ }
88
+ function tick() {
89
+ if (closing) return;
90
+ try {
91
+ pumpAudio();
92
+ for (let i = 0; i < 64; i++) {
93
+ const ptr = engine._web_event();
94
+ if (!ptr) break;
95
+ const event = JSON.parse(engine.UTF8ToString(ptr));
96
+ engine._free(ptr);
97
+ if(event.event==='command-reply'&&internalCommands.has(event.id)){
98
+ const done=internalCommands.get(event.id);internalCommands.delete(event.id);
99
+ if(event.error)throw Error(`Playback configuration failed: ${event.error}`);
100
+ done(event.result);continue;
101
+ }
102
+ if(event.event==='property-change'&&event.name==='pause'){paused=!!event.data;busyUntil=performance.now()+300;}
103
+ if(event.event==='file-loaded'){
104
+ // Configure from mpv's detected format before resolving the host's open.
105
+ internalCommand(['expand-text','${file-format}'],format=>{
106
+ demuxFormat=String(format);seekPrerollSeconds=1;
107
+ internalCommand(['set','hr-seek-demuxer-offset',String(seekPrerollSeconds)],()=>post({type:'event',event}));
108
+ });continue;
109
+ }
110
+ // Let mpv discard obsolete demux packets after this bounded read completes.
111
+ // Interrupting stream_cb read_fn mid-packet returns a truncated packet to FFmpeg.
112
+ // Source replacement/destroy still cancel I/O through closeIO().
113
+ if(event.event==='property-change'&&event.name==='time-pos'){position=event.data;releaseSeek();}
114
+ if(event.event==='playback-restart'){restarted=true;releaseSeek();}
115
+ if(event.event==='log-message')post({type:'log',message:event.prefix+': '+event.text});
116
+ post({type:'event', event});
117
+ }
118
+ if(uploader?.lost)return;
119
+ const renderStart=performance.now();
120
+ const ptr = engine._web_render(canvas.width, canvas.height, +force);
121
+ const renderDuration=performance.now()-renderStart;
122
+ force = false;
123
+ if (ptr && pendingTarget===null) {
124
+ renderMs+=renderDuration;maxRenderMs=Math.max(maxRenderMs,renderDuration);const copyStart=performance.now();
125
+ if(!uploader){
126
+ if(!frameImage||frameImage.width!==canvas.width||frameImage.height!==canvas.height)frameImage=new ImageData(canvas.width,canvas.height);
127
+ const bytes=frameImage.data;bytes.set(engine.HEAPU8.subarray(ptr,ptr+bytes.length));
128
+ for (let i = 3; i < bytes.length; i += 4) bytes[i] = 255;
129
+ context.putImageData(frameImage, 0, 0);
130
+ if(measureOutput){const at=(8*canvas.width+8)*4;const white=bytes[at]>225&&bytes[at+1]>225&&bytes[at+2]>225;if(white&&!wasWhite)post({type:'output',data:{kind:'flash',wallTime:performance.timeOrigin+performance.now(),position}});wasWhite=white;}
131
+ }
132
+ copyMs+=performance.now()-copyStart;engine._web_presented();
133
+ rendered++;sourceRendered++;presentedPosition=position;
134
+ }
135
+ ticks++;
136
+ if(performance.now()>=nextDiagnostics||(ptr&&sourceRendered<=5)){nextDiagnostics=performance.now()+200;post({type:'diagnostics', data:{softwarePresenter:uploader?'experimental-yuv':'rgb',yuv:uploader?{...uploader.stats}:undefined,pumpTicks:ticks,rendered,renderMs,copyMs,maxRenderMs, heapBytes:engine.HEAPU8.byteLength, epoch, path:'wasm', decoder:decoderStats?.active?'webcodecs':'software',decoderStats, demuxFormat,seekPrerollSeconds,presentedPosition, ioPending:(Atomics.load(engine.HEAPU32,engine._web_io_ptr()>>>2)&7)===1, ioSerial:Atomics.load(engine.HEAPU32,(engine._web_io_ptr()>>>2)+1), interruptions:Atomics.load(engine.HEAPU32,(engine._web_io_ptr()>>>2)+14), io:ioStats, seeking:pendingTarget!==null, position, queuedFrames:(Atomics.load(audio,0)-Atomics.load(audio,1))>>>0}});}
137
+ } catch (error) {pumpFailed=true;clearInterval(timer);post({type:'error',message:String(error.stack || error)}); }
138
+ }
139
+ self.onmessage = async ({data}) => {
140
+ try {
141
+ if (data.type === 'init') {
142
+ if (data.disableBrowserCodecs) for (const name of ['VideoDecoder','AudioDecoder','VideoFrame']) Object.defineProperty(globalThis,name,{value:undefined, configurable:true});
143
+ measureOutput=!!data.measureOutput;
144
+ canvas = data.canvas;
145
+ if(data.softwarePresenter==='experimental-yuv'){({YUVPresenter}=await import('./yuv-presenter.js'));installPresenter(canvas);}else context=canvas.getContext('2d',{alpha:false});
146
+ audio = new Int32Array(data.audio, 0, 16);
147
+ pcm = new Float32Array(data.audio, 64);
148
+ if(data.decoder!=='software')throw Error('This build supports software decoding only');
149
+ const createEngine=(await import(uploader?'./engine-software-yuv/player.mjs':'./engine-software-full/player.mjs')).default;
150
+ engine = await createEngine({printErr:message=>post({type:'log',message}),print:message=>post({type:'log',message})});
151
+ if(uploader){engine.failOutput=message=>{pumpFailed=true;post({type:'error',message});};engine.drawYUV=d=>{try{uploader.draw(engine,d);}catch(e){engine.failOutput(String(e));}};engine.drawRGB=(...a)=>{try{uploader.drawRGB(engine,...a);}catch(e){engine.failOutput(String(e));}};}
152
+ if (closing) return;
153
+ engine.FS.mkdir('/fonts');
154
+ engine.FS.writeFile('/fonts/DejaVuSans.ttf', new Uint8Array(data.font));
155
+ for(const font of data.fonts??[])engine.FS.writeFile('/fonts/'+font.name,new Uint8Array(font.bytes));
156
+ const fontSize=engine.FS.stat('/fonts/DejaVuSans.ttf').size;
157
+ if(Number(fontSize) !== data.font.byteLength) throw new Error('Subtitle font write failed');
158
+ if(data.decoder==='webcodecs'){
159
+ decoderWorker=new Worker(new URL('./browser-decoder-worker.js',import.meta.url),{type:'module'});
160
+ await new Promise((resolve,reject)=>{
161
+ const deadline=setTimeout(()=>reject(Error('Decoder service initialization timed out')),5000);
162
+ decoderWorker.onmessage=({data:message})=>{
163
+ if(message.ready){clearTimeout(deadline);resolve();}
164
+ if(message.stats)decoderStats=message.stats;
165
+ if(message.wakeup&&!closing)engine._web_decoder_wakeup();
166
+ if(message.error)post({type:'log',message:message.error});
167
+ };
168
+ decoderWorker.onerror=error=>{clearTimeout(deadline);reject(Error(error.message));};
169
+ decoderWorker.postMessage({memory:engine.HEAPU8.buffer,pointer:engine._web_decoder_ptr(),disabled:data.disableBrowserCodecs,faultAfter:data.decoderFaultAfter});
170
+ });
171
+ engine._web_decoder_enable(1);
172
+ }
173
+ audioChannels=data.audioChannels??2;
174
+ if(engine._web_audio_configure(audioChannels)<0)throw Error("Invalid output channel count");
175
+ if(engine._web_configure(data.maxDecodePixels??8294400,data.maxAllocationBytes??134217728)<0)throw Error("Invalid decode resource limits");
176
+ const result = engine._web_create(data.sampleRate);
177
+ if (result < 0) throw new Error(`mpv initialization failed: ${result}`);
178
+ nativeAudio = engine._web_audio_ptr();
179
+ schedulePump();
180
+ post({type:'ready', browserCodecsAbsent:['VideoDecoder','AudioDecoder','VideoFrame'].every(name=>typeof globalThis[name]==='undefined')});
181
+ } else if(data.type==='experimental-context-loss'&&uploader){const ext=uploader.gl.getExtension('WEBGL_lose_context');if(!ext)throw Error('Context loss test unavailable');ext.loseContext();setTimeout(()=>{if(!closing)ext.restoreContext();},250);
182
+ } else if (data.type === 'timing' && engine) {
183
+ Atomics.store(engine.HEAPU32, (nativeAudio >>> 2) + 5, data.latencyUs);
184
+ Atomics.store(engine.HEAPU32, (nativeAudio >>> 2) + 6, +data.running);
185
+ } else if (data.type === 'open-remote' || data.type === 'open-file') {await openRemote(data);
186
+ } else if(data.type==='refreshed'){ioWorker?.postMessage(data);
187
+ } else if(data.type==='seek'){sourceRendered=0;pendingTarget=data.seconds;restarted=false;Atomics.store(audio,2,0);submit(data.id,['seek',String(data.seconds),'absolute+exact']);
188
+ } else if (data.type === 'open') {
189
+ sourceRendered=0;
190
+ await closeIO();
191
+ if (data.bytes.byteLength > 32 * 1024 * 1024) throw new Error('M0 local fixture limit is 32 MiB');
192
+ try {engine.FS.unlink('/media.mkv');} catch { /* First open. */ }
193
+ engine.FS.writeFile('/media.mkv', new Uint8Array(data.bytes));
194
+ if(Number(engine.FS.stat('/media.mkv').size) !== data.bytes.byteLength) throw new Error('Local media write failed');
195
+ submit(data.id, ['loadfile','/media.mkv','replace']);
196
+ } else if(data.type==='subtitle'){
197
+ const path='/subtitle-'+data.id+'.'+data.format;
198
+ engine.FS.writeFile(path,new Uint8Array(data.bytes));
199
+ const result=engine.ccall('web_add_subtitle','number',['number','string','string','string','number'],[data.id,path,data.label??'',data.language??'',+data.select]);
200
+ if(result<0)throw Error('Could not add subtitle: '+result);
201
+ busyUntil=performance.now()+300;schedulePump(0);
202
+ } else if (data.type === 'command') {if(uploader?.lost&&data.args[0]==='set'&&data.args[1]==='pause')gpuPauseIntent=data.args[2]==='yes';submit(data.id,data.args);}
203
+ else if (data.type === 'resize') {busyUntil=performance.now()+300;schedulePump(0);canvas.width=data.width;canvas.height=data.height;force=true;}
204
+ else if (data.type === 'destroy') {
205
+ closing = true;
206
+ internalCommands.clear();
207
+ clearInterval(timer);
208
+ if (audio) Atomics.store(audio,2,0);
209
+ await closeIO();
210
+ decoderWorker?.postMessage({type:'cancel'});
211
+ engine?._web_destroy();if(uploader){uploader.destroy();post({type:'output',data:{kind:'yuv-cleanup',...uploader.stats}});}
212
+ // Native joins precede the queued pthread pool-return messages.
213
+ const deadline=performance.now()+2000;
214
+ while(engine?.PThread.runningWorkers.length&&performance.now()<deadline)
215
+ await new Promise(resolve=>setTimeout(resolve,10));
216
+ if(engine?.PThread.runningWorkers.length)throw Error('Native thread cleanup did not settle');
217
+ engine?.PThread.terminateAllThreads();
218
+ decoderWorker?.terminate();decoderWorker=null;
219
+ // Let child termination and queued cleanup run before closing their owner.
220
+ await new Promise(resolve=>setTimeout(resolve,50));
221
+ post({type:'destroyed',decoderStats});
222
+ self.close();
223
+ }
224
+ } catch (error) {post({type:'error',id:data.id,message:String(error.stack || error)});}
225
+ };
226
+ function submit(id,args) {
227
+ if (!engine || closing) throw new Error('Player is unavailable');
228
+ if (args.length > 4 || !args.length) throw new Error('Invalid command arity');
229
+ busyUntil=performance.now()+300;schedulePump(0);
230
+ const padded = [...args];
231
+ while (padded.length < 4) padded.push(null);
232
+ const result = engine.ccall('web_command_args','number',['number','string','string','string','string'],[id,...padded]);
233
+ if (result < 0) throw new Error(`mpv command failed: ${result}`);
234
+ }
@@ -0,0 +1,28 @@
1
+ // Packet-only metadata preflight. No decoded audio or video is produced.
2
+ export async function probeSource(source,signal,audioAdaptation){
3
+ if(!crossOriginIsolated)throw Error('Automatic source inspection requires cross-origin isolation');
4
+ if(signal.aborted)throw new DOMException('Aborted','AbortError');
5
+ const mailbox=new SharedArrayBuffer(64+262144),workers=[];let abort;
6
+ try{return await new Promise((resolve,reject)=>{
7
+ const timer=setTimeout(()=>reject(Error('Source inspection timed out')),20000);
8
+ const finish=(error,value)=>{clearTimeout(timer);error?reject(error):resolve(value);};
9
+ abort=()=>finish(new DOMException('Aborted','AbortError'));signal.addEventListener('abort',abort,{once:true});
10
+ const make=file=>{const w=new Worker(new URL(file,import.meta.url),{type:'module'});workers.push(w);w.onerror=e=>{e.preventDefault();finish(Error('Source inspection worker failed: '+e.message));};w.onmessageerror=()=>finish(Error('Source inspection message error'));return w;};
11
+ const reader=make('./native-remux-source-worker.js');
12
+ reader.onmessage=({data})=>{
13
+ if(signal.aborted)return;
14
+ if(data.type==='error')finish(Error('Source transport: '+data.message));
15
+ if(data.type==='refresh')Promise.resolve().then(()=>{if(!source.refreshAuthorization)throw Error('Authorization refresh unavailable');return source.refreshAuthorization(data.resource);}).then(update=>{if(!signal.aborted)reader.postMessage({type:'refreshed',update});},error=>{if(!signal.aborted)reader.postMessage({type:'refreshed',error:String(error)});});
16
+ if(data.type==='ready'){
17
+ const probe=make('./native-remux-worker.js');probe.onmessage=({data:message})=>{
18
+ if(message.type==='error')finish(Error(message.message));
19
+ if(message.type==='probed')finish(null,{tracks:message.tracks,duration:message.duration,format:message.format,identity:data.identity});
20
+ };probe.postMessage({type:'probe',size:data.size,mailbox,audioAdaptation});
21
+ }
22
+ };
23
+ const {refreshAuthorization,...transport}=source;reader.postMessage({type:'init',mailbox,...transport});
24
+ });}finally{
25
+ signal.removeEventListener('abort',abort);const h=new Int32Array(mailbox,0,16);Atomics.store(h,4,1);Atomics.store(h,0,3);Atomics.notify(h,0);
26
+ for(const worker of workers)worker.terminate();
27
+ }
28
+ }
@@ -0,0 +1,159 @@
1
+ // Packaging-only split of the maintained muxer's clear, default-base-is-moof
2
+ // fragments. Sample bytes, decode times, composition offsets and sample metadata
3
+ // are preserved. Other BMFF addressing schemes are rejected, never guessed.
4
+ const text=b=>String.fromCharCode(...b);
5
+ function boxes(bytes){
6
+ const result=[],view=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength);
7
+ for(let at=0;at<bytes.length;){
8
+ if(result.length>=4096)throw Error('MP4 box budget exceeded');
9
+ if(at+8>bytes.length)throw Error('Truncated MP4 box');
10
+ const size=view.getUint32(at);if(size<8||at+size>bytes.length)throw Error('Unsupported or truncated MP4 box size');
11
+ result.push({type:text(bytes.subarray(at+4,at+8)),at,bytes:bytes.subarray(at,at+size)});at+=size;
12
+ }
13
+ return result;
14
+ }
15
+ const u32=(b,n)=>new DataView(b.buffer,b.byteOffset,b.byteLength).getUint32(n);
16
+ function join(parts){const out=new Uint8Array(parts.reduce((n,p)=>n+p.length,0));let at=0;for(const part of parts){out.set(part,at);at+=part.length;}return out;}
17
+ function box(type,parts){const payload=join(parts),out=new Uint8Array(payload.length+8);new DataView(out.buffer).setUint32(0,out.length);out.set([...type].map(c=>c.charCodeAt(0)),4);out.set(payload,8);return out;}
18
+ const children=b=>boxes(b.subarray(8));
19
+ export class SplitMP4 {
20
+ constructor(){this.ids=[];this.defaults=new Map();}
21
+ split(input){
22
+ if(input.byteLength>8*1024*1024)throw Error('Split MP4 input budget exceeded');
23
+ const bytes=new Uint8Array(input),top=boxes(bytes),out=[[],[]];
24
+ for(let n=0;n<top.length;n++){
25
+ const current=top[n];
26
+ if(current.type==='ftyp'){for(const lane of out)lane.push(current.bytes);}
27
+ else if(current.type==='moov'){
28
+ if(this.ids.length)throw Error('Unexpected MP4 reinitialization');
29
+ const parts=children(current.bytes),tracks=parts.filter(p=>p.type==='trak');
30
+ if(tracks.length!==2)throw Error('Split MP4 requires exactly two tracks');
31
+ const roles=new Map();
32
+ for(const track of tracks){
33
+ const sub=children(track.bytes),tkhd=sub.find(p=>p.type==='tkhd')?.bytes,mdia=sub.find(p=>p.type==='mdia')?.bytes;
34
+ if(!tkhd||!mdia)throw Error('Missing track identity');
35
+ const handler=children(mdia).find(p=>p.type==='hdlr')?.bytes;
36
+ if(!handler)throw Error('Missing track handler');
37
+ const role=text(handler.subarray(16,20)),id=u32(tkhd,tkhd[8]===1?28:20);
38
+ if(!['vide','soun'].includes(role)||roles.has(role))throw Error('Unsupported track roles');
39
+ roles.set(role,{id,bytes:track.bytes});
40
+ }
41
+ this.ids=['vide','soun'].map(role=>roles.get(role).id);
42
+ const mvex=parts.find(p=>p.type==='mvex');if(!mvex)throw Error('Missing fragment defaults');
43
+ for(const p of children(mvex.bytes))if(p.type==='trex')this.defaults.set(u32(p.bytes,12),u32(p.bytes,24));
44
+ for(let lane=0;lane<2;lane++){
45
+ const track=roles.get(lane?'soun':'vide');
46
+ out[lane].push(box('moov',parts.flatMap(p=>p.type==='trak'?(p.bytes===track.bytes?[p.bytes]:[]):p.type==='mvex'?[box('mvex',children(p.bytes).filter(c=>c.type!=='trex'||u32(c.bytes,12)===track.id).map(c=>c.bytes))]:[p.bytes])));
47
+ }
48
+ }else if(current.type==='moof'){
49
+ if(this.ids.length!==2)throw Error('Fragment before track initialization');
50
+ const data=top[++n];if(data?.type!=='mdat')throw Error('Expected adjacent media data');
51
+ const used=[],parts=children(current.bytes),header=parts.find(p=>p.type==='mfhd');if(!header)throw Error('Missing fragment sequence');
52
+ for(const traf of parts.filter(p=>p.type==='traf')){
53
+ const fields=children(traf.bytes),tfhd=fields.find(p=>p.type==='tfhd')?.bytes;
54
+ if(!tfhd)throw Error('Missing fragment track identity');
55
+ const flags=u32(tfhd,8)&0xffffff,id=u32(tfhd,12),lane=this.ids.indexOf(id);
56
+ if(lane<0||!(flags&0x020000)||(flags&1))throw Error('Unsupported fragment addressing');
57
+ let at=16;if(flags&2)at+=4;if(flags&8)at+=4;
58
+ const defaultSize=flags&16?u32(tfhd,at):this.defaults.get(id)??0;
59
+ const runs=[],payload=[];
60
+ for(const field of fields){
61
+ if(!['tfhd','tfdt','trun','sgpd','sbgp'].includes(field.type))throw Error('Unsupported fragment auxiliary data');
62
+ if(field.type!=='trun')continue;
63
+ const trun=field.bytes,f=u32(trun,8)&0xffffff,count=u32(trun,12);
64
+ if(!(f&1)||count>20000)throw Error('Unsupported sample run addressing or count');
65
+ const start=current.at+new DataView(trun.buffer,trun.byteOffset,trun.length).getInt32(16);
66
+ let cursor=20+(f&4?4:0),length=0;
67
+ for(let i=0;i<count;i++){
68
+ if(f&0x100)cursor+=4;
69
+ const size=f&0x200?u32(trun,cursor):defaultSize;
70
+ if(f&0x200)cursor+=4;if(f&0x400)cursor+=4;if(f&0x800)cursor+=4;
71
+ if(cursor>trun.length||!size)throw Error('Invalid sample description');length+=size;
72
+ }
73
+ if(cursor!==trun.length||start<data.at+8||start+length>data.at+data.bytes.length)throw Error('Sample run exceeds media data');
74
+ if(used.some(([a,b])=>start<b&&start+length>a))throw Error('Overlapping sample runs');
75
+ used.push([start,start+length]);
76
+ const copy=trun.slice();runs.push(copy);payload.push(bytes.subarray(start,start+length));field.bytes=copy;
77
+ }
78
+ const newTraf=box('traf',fields.map(p=>p.bytes));let offset=8+header.bytes.length+newTraf.length+8;
79
+ for(let i=0;i<runs.length;i++){new DataView(runs[i].buffer).setInt32(16,offset);offset+=payload[i].length;}
80
+ out[lane].push(box('moof',[header.bytes,box('traf',fields.map(p=>p.bytes))]),box('mdat',payload));
81
+ }
82
+ if(used.reduce((sum,[a,b])=>sum+b-a,0)!==data.bytes.length-8)throw Error('Unaccounted media data');
83
+ }else if(current.type!=='mfra'&&current.type!=='free')throw Error('Unsupported top-level MP4 box: '+current.type);
84
+ // mfra is the old combined-file seek index. The source demuxer owns seeking;
85
+ // MSE does not consume this now-invalid byte-offset index.
86
+ }
87
+ return out.map(parts=>join(parts).buffer);
88
+ }
89
+ }
90
+
91
+ // Read the maintained muxer's actual presentation intervals. In B-frame MP4,
92
+ // duration follows decode-order deltas and can differ from rounded input packet
93
+ // durations. Do not infer the presented frame solely from the latest input PTS.
94
+ export class MP4VideoTiming {
95
+ constructor(){this.track=undefined;this.scale=0;this.shift=0;this.defaults=new Map();}
96
+ read(input){
97
+ if(input.byteLength>8*1024*1024)throw Error('MP4 timing input budget exceeded');
98
+ const frames=[];
99
+ for(const top of boxes(new Uint8Array(input))){
100
+ if(top.type==='moov'){
101
+ if(this.track!==undefined)throw Error('Unexpected timing reinitialization');
102
+ const parts=children(top.bytes),movie=parts.find(p=>p.type==='mvhd')?.bytes;
103
+ const movieScale=movie?u32(movie,movie[8]===1?28:20):0;
104
+ for(const p of parts){
105
+ if(p.type==='mvex')for(const d of children(p.bytes))if(d.type==='trex')this.defaults.set(u32(d.bytes,12),u32(d.bytes,20));
106
+ if(p.type!=='trak')continue;
107
+ const fields=children(p.bytes),header=fields.find(p=>p.type==='tkhd')?.bytes,mdia=fields.find(p=>p.type==='mdia')?.bytes;
108
+ if(!header||!mdia)throw Error('Missing timing track');
109
+ const media=children(mdia),handler=media.find(p=>p.type==='hdlr')?.bytes,time=media.find(p=>p.type==='mdhd')?.bytes;
110
+ if(!handler||!time)throw Error('Missing media time base');
111
+ if(text(handler.subarray(16,20))!=='vide')continue;
112
+ if(this.track!==undefined)throw Error('Multiple timing video tracks');
113
+ this.track=u32(header,header[8]===1?28:20);this.scale=u32(time,time[8]===1?28:20);
114
+ if(!this.scale)throw Error('Invalid media time base');
115
+ const edits=fields.find(p=>p.type==='edts')?.bytes;
116
+ if(edits){
117
+ const list=children(edits).find(p=>p.type==='elst')?.bytes;
118
+ if(!list||!movieScale||list[8]>1)throw Error('Unsupported video edit timing');
119
+ const count=u32(list,12),view=new DataView(list.buffer,list.byteOffset,list.length);let cursor=16,movieTime=0,mediaFound=false;
120
+ if(!count||count>2)throw Error('Unsupported video edit count');
121
+ for(let i=0;i<count;i++){
122
+ const wide=list[8]===1,length=wide?Number(view.getBigUint64(cursor)):u32(list,cursor);cursor+=wide?8:4;
123
+ const time=wide?Number(view.getBigInt64(cursor)):view.getInt32(cursor);cursor+=wide?8:4;
124
+ if(!Number.isSafeInteger(length)||!Number.isSafeInteger(time)||view.getInt16(cursor)!==1||view.getInt16(cursor+2)!==0)throw Error('Unsupported video edit rate or time');cursor+=4;
125
+ if(time===-1&&!mediaFound)movieTime+=length/movieScale;
126
+ else if(time>=0&&!mediaFound){this.shift=movieTime-time/this.scale;mediaFound=true;if(length)throw Error('Finite video edit not qualified for progressive timing');}
127
+ else throw Error('Unsupported video edit sequence');
128
+ }
129
+ if(!mediaFound||cursor!==list.length)throw Error('Invalid video edit timing');
130
+ }
131
+ }
132
+ }
133
+ if(top.type!=='moof')continue;
134
+ if(this.track===undefined)throw Error('Timing fragment before initialization');
135
+ for(const p of children(top.bytes).filter(p=>p.type==='traf')){
136
+ const fields=children(p.bytes),header=fields.find(p=>p.type==='tfhd')?.bytes,base=fields.find(p=>p.type==='tfdt')?.bytes;
137
+ if(!header||!base)throw Error('Missing fragment timing');
138
+ if(u32(header,12)!==this.track)continue;
139
+ const flags=u32(header,8)&0xffffff;let at=16+(flags&1?8:0)+(flags&2?4:0);
140
+ const duration=flags&8?u32(header,at):this.defaults.get(this.track);
141
+ let dts=base[8]===1?Number(new DataView(base.buffer,base.byteOffset,base.length).getBigUint64(12)):u32(base,12);
142
+ if(!Number.isSafeInteger(dts))throw Error('Unrepresentable fragment decode time');
143
+ for(const run of fields.filter(p=>p.type==='trun')){
144
+ const b=run.bytes,f=u32(b,8)&0xffffff,count=u32(b,12);at=16+(f&1?4:0)+(f&4?4:0);
145
+ if(count+frames.length>4096)throw Error('Video timing sample budget exceeded');
146
+ for(let i=0;i<count;i++){
147
+ const length=f&0x100?u32(b,at):duration;if(f&0x100)at+=4;
148
+ if(f&0x200)at+=4;if(f&0x400)at+=4;
149
+ const cts=f&0x800?(b[8]===1?new DataView(b.buffer,b.byteOffset,b.length).getInt32(at):u32(b,at)):0;if(f&0x800)at+=4;
150
+ if(!length||at>b.length||!Number.isSafeInteger(dts+cts+length))throw Error('Invalid video presentation interval');
151
+ frames.push([(dts+cts)/this.scale+this.shift,length/this.scale]);dts+=length;
152
+ }
153
+ if(at!==b.length)throw Error('Unexpected video timing fields');
154
+ }
155
+ }
156
+ }
157
+ return frames;
158
+ }
159
+ }
@@ -0,0 +1,136 @@
1
+ // Bounded manifest selection and finite DASH-period adaptation. Compressed media
2
+ // is never decoded here; FFmpeg still demuxes segments and owns packet timelines.
3
+ const enc=new TextEncoder();
4
+ const fail=message=>{throw Error(message);};
5
+ const escape=s=>String(s).replaceAll('&','&amp;').replaceAll('"','&quot;').replaceAll('<','&lt;');
6
+ const entities=s=>s.replace(/&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos);/gi,(_,v)=>v[0]==='#'?String.fromCodePoint(v[1].toLowerCase()==='x'?parseInt(v.slice(2),16):Number(v.slice(1))):({amp:'&',lt:'<',gt:'>',quot:'"',apos:"'"})[v]);
7
+ export function parseXML(text){
8
+ if(/<!DOCTYPE|<!ENTITY|<!\[CDATA\[/i.test(text))fail('Unsupported DASH XML declaration');
9
+ const root={name:'',attrs:{},children:[],text:''},stack=[root];let count=0;
10
+ const tokens=text.match(/<!--[\s\S]*?-->|<\?[\s\S]*?\?>|<[^>]*>|[^<]+/g)??[];
11
+ if(tokens.join('')!==text)fail('Malformed DASH XML');
12
+ for(const token of tokens){
13
+ if(token.startsWith('<!--')||token.startsWith('<?'))continue;
14
+ if(!token.startsWith('<')){stack.at(-1).text+=entities(token);continue;}
15
+ if(token.startsWith('</')){if(stack.length===1||stack.pop().name!==token.slice(2,-1).trim())fail('Malformed DASH XML');continue;}
16
+ const name=/^<([\w:.-]+)/.exec(token)?.[1];if(!name||++count>50000||stack.length>32)fail('Invalid DASH XML bounds');
17
+ const attrs={};let rest=token.slice(name.length+1).replace(/\/?\s*>$/,'');
18
+ while(rest.trim()){
19
+ const m=/^\s+([\w:.-]+)\s*=\s*(["'])([\s\S]*?)\2/.exec(rest);if(!m||Object.hasOwn(attrs,m[1]))fail('Malformed DASH attributes');
20
+ attrs[m[1]]=entities(m[3]);rest=rest.slice(m[0].length);
21
+ }
22
+ const node={name,attrs,children:[],text:''};stack.at(-1).children.push(node);if(!/\/\s*>$/.test(token))stack.push(node);
23
+ }
24
+ if(stack.length!==1||root.children.length!==1||root.children[0].name!=='MPD')fail('Malformed DASH MPD');return root.children[0];
25
+ }
26
+ const children=(n,name)=>n.children.filter(c=>c.name===name);
27
+ const child=(n,name)=>children(n,name)[0];
28
+ const xml=n=>`<${n.name}${Object.entries(n.attrs).map(([k,v])=>` ${k}="${escape(v)}"`).join('')}>${escape(n.text)}${n.children.map(xml).join('')}</${n.name}>`;
29
+ export function isoDuration(s){const m=/^P(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/.exec(s??'');if(!m)return NaN;return Number(m[1]??0)*86400+Number(m[2]??0)*3600+Number(m[3]??0)*60+Number(m[4]??0);}
30
+ export function chooseVariant(items,options={}){
31
+ if(!items.length)fail('No streaming representation');
32
+ if(options.representation!==undefined){const exact=items.find(i=>i.id===String(options.representation));if(!exact)fail('Requested streaming representation is unavailable');return exact;}
33
+ const limit=options.maxBandwidth??Infinity;if(!(limit>0))fail('Invalid streaming bandwidth limit');
34
+ const sorted=[...items].sort((a,b)=>a.bandwidth-b.bandwidth);return sorted.filter(i=>i.bandwidth<=limit).at(-1)??sorted[0];
35
+ }
36
+ const attrs=line=>Object.fromEntries([...line.matchAll(/(?:^|[:,])([A-Z0-9-]+)=("[^"]*"|[^,]*)/g)].map(m=>[m[1],m[2].replace(/^"|"$/g,'')]));
37
+ export function selectHLS(text,options={}){
38
+ const lines=text.split(/\r?\n/),variants=[];
39
+ for(let i=0;i<lines.length;i++)if(lines[i].startsWith('#EXT-X-STREAM-INF:')){
40
+ let j=i+1;while(j<lines.length&&!lines[j].trim())j++;
41
+ if(!lines[j]||lines[j].startsWith('#'))fail('Missing HLS variant URI');
42
+ variants.push({id:String(variants.length),bandwidth:Number(attrs(lines[i]).BANDWIDTH)||0,line:i,uri:j});
43
+ }
44
+ if(variants.length>64)fail('HLS variant limit exceeded');
45
+ if(!variants.length)return text;
46
+ const chosen=chooseVariant(variants,options),drop=new Set(variants.filter(v=>v!==chosen).flatMap(v=>[v.line,v.uri]));
47
+ const chosenAttrs=attrs(lines[chosen.line]);
48
+ return lines.filter((line,i)=>{
49
+ if(drop.has(i))return false;
50
+ if(line.startsWith('#EXT-X-MEDIA:')){const a=attrs(line);return !['AUDIO','SUBTITLES','VIDEO','CLOSED-CAPTIONS'].includes(a.TYPE)||chosenAttrs[a.TYPE]===a['GROUP-ID'];}
51
+ return true;
52
+ }).join('\n');
53
+ }
54
+ const baseAt=(node,base)=>child(node,'BaseURL')?new URL(child(node,'BaseURL').text.trim(),base).href:base;
55
+ function templateURL(pattern,rep,time,number){
56
+ if(!pattern)fail('DASH segment URL is missing');
57
+ return pattern.replace(/\$\$|\$(RepresentationID|Bandwidth|Time|Number)(?:%0(\d+)d)?\$/g,(all,key,width)=>key?String(key==='RepresentationID'?rep.attrs.id:key==='Bandwidth'?rep.attrs.bandwidth:key==='Time'?time:number).padStart(Number(width)||0,'0'):'$');
58
+ }
59
+ function segmentsFor(rep,adaptation,period,base,duration){
60
+ base=baseAt(rep,baseAt(adaptation,base));
61
+ const inherit=name=>{const nodes=[period,adaptation,rep].map(n=>child(n,name)).filter(Boolean);return nodes.length?{...nodes.at(-1),attrs:Object.assign({},...nodes.map(n=>n.attrs)),children:[...new Map(nodes.flatMap(n=>n.children).map(n=>[n.name,n])).values()]}:undefined;};
62
+ const template=inherit('SegmentTemplate');
63
+ const list=child(rep,'SegmentList')??child(adaptation,'SegmentList')??child(period,'SegmentList');
64
+ const spec=template??list;if(!spec)fail('Multiple DASH periods require SegmentTemplate or SegmentList');
65
+ const scale=Number(spec.attrs.timescale??1),offset=Number(spec.attrs.presentationTimeOffset??0);if(!Number.isSafeInteger(scale)||scale<=0||!Number.isSafeInteger(offset))fail('Invalid DASH timescale');
66
+ let time=0,number=Number(spec.attrs.startNumber??1);const timeline=child(spec,'SegmentTimeline'),entries=[];
67
+ if(timeline){
68
+ const nodes=children(timeline,'S');
69
+ for(let i=0;i<nodes.length;i++){
70
+ const s=nodes[i];time=s.attrs.t===undefined?time:Number(s.attrs.t);const d=Number(s.attrs.d);let r=Number(s.attrs.r??0);
71
+ if(r===-1){const next=nodes[i+1]?.attrs.t;r=Math.ceil(((next===undefined?duration*scale+offset:Number(next))-time)/d)-1;}
72
+ if(!Number.isSafeInteger(time)||!Number.isSafeInteger(d)||d<=0||!Number.isInteger(r)||r<0||entries.length+r+1>10000)fail('Invalid or unbounded DASH timeline');
73
+ for(let j=0;j<=r;j++){entries.push({time,duration:d/scale,number:number++});time+=d;}
74
+ }
75
+ }else{
76
+ const d=Number(spec.attrs.duration);if(!(d>0&&duration>0)||Math.ceil(duration*scale/d)>10000)fail('DASH period needs a bounded segment duration');
77
+ for(let n=0;n<Math.ceil(duration*scale/d);n++)entries.push({time:offset+n*d,duration:Math.min(d/scale,duration-n*d/scale),number:number++});
78
+ }
79
+ if(!entries.length)fail('Empty DASH period');
80
+ const init=template?new URL(templateURL(template.attrs.initialization,rep,0,0),base).href:new URL(child(list,'Initialization')?.attrs.sourceURL??fail('Missing DASH initialization'),base).href;
81
+ const urls=list?children(list,'SegmentURL'):[];
82
+ if(list&&urls.length!==entries.length)fail('DASH SegmentList timeline mismatch');
83
+ if(list&&(child(list,'Initialization')?.attrs.range||urls.some(n=>n.attrs.mediaRange)))fail('Multi-period byte-range DASH requires separate segment resources');
84
+ return {init,entries:entries.map((e,i)=>({...e,url:new URL(template?templateURL(template.attrs.media,rep,e.time,e.number):urls[i].attrs.media,base).href})),offset:entries[0].time/scale-offset/scale};
85
+ }
86
+ export function prepareDASH(text,url,options={}){
87
+ const mpd=parseXML(text),periods=children(mpd,'Period');
88
+ if(!periods.length||periods.length>64)fail('DASH period count limit exceeded');
89
+ const all=[];const visit=n=>{all.push(n);n.children.forEach(visit);};visit(mpd);
90
+ if(all.some(n=>['ContentProtection','Location','PatchLocation','EventStream'].includes(n.name)||Object.keys(n.attrs).some(k=>k.startsWith('xlink:'))))fail('Unsupported DASH extension');
91
+ for(const period of periods)for(const adaptation of children(period,'AdaptationSet')){
92
+ const reps=children(adaptation,'Representation');if(reps.length>64)fail('DASH representation limit exceeded');
93
+ const type=adaptation.attrs.contentType??(reps[0]?.attrs.mimeType??adaptation.attrs.mimeType??'').split('/')[0];
94
+ const selected=chooseRepresentation(reps,type==='video'?options:{});
95
+ adaptation.children=adaptation.children.filter(n=>n.name!=='Representation'||n===selected);
96
+ }
97
+ if(periods.length===1)return {bytes:enc.encode(xml(mpd)),format:'dash',resources:new Map()};
98
+ if(mpd.attrs.type!=='static')fail('Multiple live DASH periods are unsupported');
99
+ const resources=new Map(),tracks=new Map(),rootBase=baseAt(mpd,url);let expectedStart=0;
100
+ for(let i=0;i<periods.length;i++){
101
+ const period=periods[i],start=period.attrs.start===undefined?expectedStart:isoDuration(period.attrs.start);
102
+ let duration=isoDuration(period.attrs.duration);
103
+ if(!Number.isFinite(duration))duration=(i+1<periods.length?isoDuration(periods[i+1].attrs.start):isoDuration(mpd.attrs.mediaPresentationDuration))-start;
104
+ if(!Number.isFinite(start)||Math.abs(start-expectedStart)>.01||!(duration>0))fail('DASH periods must have contiguous finite durations');
105
+ const present=new Set();
106
+ for(const [a,adaptation] of children(period,'AdaptationSet').entries()){
107
+ const rep=child(adaptation,'Representation'),type=adaptation.attrs.contentType??(rep?.attrs.mimeType??adaptation.attrs.mimeType??'').split('/')[0];
108
+ if(!rep||!['video','audio'].includes(type))fail('Multi-period DASH requires audio/video fMP4 representations');
109
+ const key=type+':'+(adaptation.attrs.id??a),mime=rep.attrs.mimeType??adaptation.attrs.mimeType;
110
+ if(mime&&!mime.endsWith('/mp4'))fail('Multi-period DASH requires fMP4 segments');
111
+ const info=segmentsFor(rep,adaptation,period,baseAt(period,rootBase),duration);
112
+ if(Math.abs(info.offset)>.01)fail('DASH period starts must align with their first segment');
113
+ const language=adaptation.attrs.lang??'und';if(!/^[a-zA-Z0-9-]{1,64}$/.test(language))fail('Invalid DASH language tag');
114
+ const track=tracks.get(key)??{type,language,parts:[],bandwidth:Number(rep.attrs.bandwidth)||1000000};
115
+ if(i&&!tracks.has(key))fail('DASH track identity changed across periods');
116
+ track.parts.push({duration,...info});tracks.set(key,track);present.add(key);
117
+ }
118
+ if(present.size!==tracks.size)fail('DASH tracks cannot disappear across periods');expectedStart=start+duration;
119
+ }
120
+ const prefix=new URL('__demuxe_periods__/',url).href;let audio=[],video=[];
121
+ for(const [key,t] of tracks){
122
+ const uri=prefix+encodeURIComponent(key)+'.m3u8';let body='#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-TARGETDURATION:'+Math.ceil(Math.max(...t.parts.flatMap(p=>p.entries.map(e=>e.duration))))+'\n#EXT-X-PLAYLIST-TYPE:VOD\n';
123
+ for(const [i,p] of t.parts.entries()){
124
+ if(i)body+='#EXT-X-DISCONTINUITY\n';body+='#EXT-X-MAP:URI="'+p.init+'"\n';
125
+ let remaining=p.duration;for(const e of p.entries){if(remaining<=.000001)break;const d=Math.min(e.duration,remaining);body+=`#EXTINF:${d},\n${e.url}\n`;remaining-=d;}
126
+ if(remaining>.05)fail('DASH segments do not cover period duration');
127
+ }
128
+ body+='#EXT-X-ENDLIST\n';resources.set(uri,enc.encode(body));(t.type==='audio'?audio:video).push({...t,uri,key});
129
+ }
130
+ if(video.length>1)fail('Choose one DASH video adaptation set');
131
+ let master='#EXTM3U\n';for(const [i,a] of audio.entries())master+=`#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="audio-${i}",LANGUAGE="${a.language}",DEFAULT=${i?'NO':'YES'},AUTOSELECT=YES,URI="${a.uri}"\n`;
132
+ const main=video[0]??audio[0];if(!main)fail('Empty DASH presentation');
133
+ master+=`#EXT-X-STREAM-INF:BANDWIDTH=${main.bandwidth}${audio.length&&video.length?',AUDIO="audio"':''}\n${main.uri}\n`;
134
+ return {bytes:enc.encode(master),format:'hls',url:prefix+'master.m3u8',resources};
135
+ }
136
+ function chooseRepresentation(reps,options){return chooseVariant(reps.map(node=>({node,id:node.attrs.id,bandwidth:Number(node.attrs.bandwidth)||0})),options).node;}
@@ -0,0 +1,26 @@
1
+ // Decode bounded mpv A8/color or premultiplied BGRA tiles into cached browser surfaces.
2
+ export class SubtitleOverlay {
3
+ constructor(){this.serial=-1;this.snapshot={parts:[]};this.stats={updates:0,bytes:0,peakBytes:0,parts:0,renders:0};}
4
+ read(engine){
5
+ const ptr=engine._web_subtitle_ptr(),h=new Int32Array(engine.HEAPU8.buffer,ptr,8);
6
+ this.stats.renders=h[6];if(h[3])throw Error('Subtitle bitmap budget exceeded');
7
+ if(this.serial===h[0])return this.snapshot;
8
+ if(h[1]<0||h[1]>512||h[2]<0||h[2]>2*1024*1024)throw Error('Invalid subtitle packet');
9
+ const parts=[];
10
+ for(let i=0;i<h[1];i++){
11
+ const d=new Int32Array(engine.HEAPU8.buffer,ptr+32+i*36,9),[x,y,w,height,dw,dh,format,color,offset]=d;
12
+ const bpp=format===1?1:4,size=w*height*bpp;
13
+ if(![1,2].includes(format)||w<=0||height<=0||dw<=0||dh<=0||offset<0||offset+size>h[2])throw Error('Invalid subtitle tile');
14
+ const input=new Uint8Array(engine.HEAPU8.buffer,ptr+32+512*36+offset,size),image=new ImageData(w,height),rgba=image.data;
15
+ for(let j=0;j<w*height;j++){
16
+ if(format===1){rgba[j*4]=color>>>24;rgba[j*4+1]=(color>>>16)&255;rgba[j*4+2]=(color>>>8)&255;rgba[j*4+3]=Math.round(input[j]*(255-(color&255))/255);}
17
+ else{const a=input[j*4+3];rgba[j*4]=a?Math.min(255,Math.round(input[j*4+2]*255/a)):0;rgba[j*4+1]=a?Math.min(255,Math.round(input[j*4+1]*255/a)):0;rgba[j*4+2]=a?Math.min(255,Math.round(input[j*4]*255/a)):0;rgba[j*4+3]=a;}
18
+ }
19
+ const tile=new OffscreenCanvas(w,height);tile.getContext('2d').putImageData(image,0,0);parts.push({tile,x,y,dw,dh});
20
+ }
21
+ this.serial=h[0];this.stats.updates++;this.stats.bytes+=h[2];this.stats.peakBytes=Math.max(this.stats.peakBytes,h[2]);this.stats.parts=h[1];
22
+ return this.snapshot={parts};
23
+ }
24
+ draw(context,snapshot){for(const p of snapshot.parts)context.drawImage(p.tile,p.x,p.y,p.dw,p.dh);}
25
+ clear(){this.serial=-1;this.snapshot={parts:[]};this.stats.parts=0;}
26
+ }