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,110 @@
1
+ /** Finite execution plans; qualification is local to a feature, not a browser claim. */
2
+ export const PLAYBACK_PLANS = Object.freeze([
3
+ { id: 'native-direct', mode: 'native', video: 'browser', audio: 'original', qualification: 'existing' },
4
+ { id: 'native-remux', mode: 'native', video: 'packet-copy', audio: 'packet-copy', qualification: 'existing' },
5
+ { id: 'native-direct-gain', mode: 'native', video: 'browser', audio: 'web-audio-gain', qualification: 'experimental' },
6
+ { id: 'native-remux-gain', mode: 'native', video: 'packet-copy', audio: 'web-audio-gain', qualification: 'experimental' },
7
+ { id: 'native-flac', mode: 'native', video: 'packet-copy', audio: 'flac-lossless', qualification: 'experimental' },
8
+ { id: 'native-flac-gain', mode: 'native', video: 'packet-copy', audio: 'flac-lossless+web-audio-gain', qualification: 'experimental' },
9
+ { id: 'native-direct-ass', mode: 'native', video: 'browser', audio: 'original', qualification: 'experimental' },
10
+ { id: 'native-direct-ass-gain', mode: 'native', video: 'browser', audio: 'original+web-audio-gain', qualification: 'experimental' },
11
+ { id: 'native-remux-ass', mode: 'native', video: 'packet-copy', audio: 'packet-copy', qualification: 'experimental' },
12
+ { id: 'native-remux-ass-gain', mode: 'native', video: 'packet-copy', audio: 'packet-copy+web-audio-gain', qualification: 'experimental' },
13
+ { id: 'native-flac-ass', mode: 'native', video: 'packet-copy', audio: 'flac-lossless', qualification: 'experimental' },
14
+ { id: 'native-flac-ass-gain', mode: 'native', video: 'packet-copy', audio: 'flac-lossless+web-audio-gain', qualification: 'experimental' },
15
+ { id: 'native-opus', mode: 'native', video: 'packet-copy', audio: 'opus-lossy', qualification: 'experimental' },
16
+ { id: 'native-opus-gain', mode: 'native', video: 'packet-copy', audio: 'opus-lossy+web-audio-gain', qualification: 'experimental' },
17
+ { id: 'hybrid', mode: 'hybrid', video: 'webcodecs', audio: 'mpv', qualification: 'existing' },
18
+ { id: 'hybrid-audio-filter', mode: 'hybrid', video: 'webcodecs', audio: 'mpv-filter', qualification: 'experimental' },
19
+ { id: 'hybrid-gain', mode: 'hybrid', video: 'webcodecs', audio: 'mpv+web-audio-gain', qualification: 'experimental' },
20
+ { id: 'hybrid-audio-filter-gain', mode: 'hybrid', video: 'webcodecs', audio: 'mpv-filter+web-audio-gain', qualification: 'experimental' },
21
+ { id: 'software-gain', mode: 'software', video: 'ffmpeg', audio: 'mpv+web-audio-gain', qualification: 'experimental' },
22
+ { id: 'software', mode: 'software', video: 'ffmpeg', audio: 'mpv', qualification: 'existing' },
23
+ ].map(plan => Object.freeze({ ...plan,
24
+ source: (plan.id.startsWith('native-remux') || plan.id.startsWith('native-flac') || plan.id.startsWith('native-opus')) ? 'qualified random-access file and selected codec packaging' : plan.mode === 'native' ? 'browser-supported source and selected tracks' : 'existing mpv source/track contract',
25
+ prerequisites: (plan.id.startsWith('native-remux') || plan.id.startsWith('native-flac') || plan.id.startsWith('native-opus')) ? 'MSE, cross-origin isolation, qualified MIME' : plan.mode === 'native' ? 'HTMLMediaElement' + (plan.id.endsWith('gain') ? ', Web Audio and CORS-clean media' : '') : 'cross-origin isolation' + (plan.mode === 'hybrid' ? ', supported complete WebCodecs configuration' : ''),
26
+ subtitles: plan.id.includes('-ass') ? 'external ASS/SSA via pinned libass; container presentation only' : plan.mode === 'native' ? 'browser text tracks' : 'mpv/libass',
27
+ fidelity: plan.id.startsWith('native-opus') ? 'Explicitly permitted lossy audio; no resampling/downmix; video copied' : plan.id.startsWith('native-flac') ? 'Selected integer audio encoded losslessly as FLAC; video copied; no downmix/resample' : 'No audio encoding, downmix or resampling added by route selection; existing backend output contracts apply',
28
+ resources: plan.mode === 'native' ? 'existing bounded remux buffers when used; browser decoder allocations are opaque' : 'existing mpv allocation, PCM ring and retained-frame limits',
29
+ fallback: plan.mode === 'software' ? 'terminal' : 'existing diagnosed-path fallback with source and user intent preserved',
30
+ })));
31
+ // Only the demonstrated scalar filter is admitted. Expressions, chains, channel
32
+ // remapping, resampling, latency-changing filters and video filters are excluded.
33
+ export function qualifiedAudioFilter(chain) {
34
+ const match = /^(?:volume=([0-9]+(?:\.[0-9]+)?)|lavfi=\[volume=([0-9]+(?:\.[0-9]+)?)\])$/.exec(chain);
35
+ return !!match && Number(match[1] ?? match[2]) <= 1;
36
+ }
37
+ export function featureRejection(mode, features) {
38
+ if (mode === 'software')
39
+ return;
40
+ if (features.vf || features.toneMapping !== 'off')
41
+ return 'Video filters and tone mapping require Software. Clear filters before leaving software mode.';
42
+ if (!features.af)
43
+ return;
44
+ if (mode === 'native')
45
+ return 'Audio filters require mpv. Clear filters before selecting Native.';
46
+ if (!features.hybridAudioFilters)
47
+ return 'Hybrid audio filters require explicit experimental admission';
48
+ if (!qualifiedAudioFilter(features.af))
49
+ return 'This audio filter has not been qualified for Hybrid';
50
+ }
51
+ export function executionPlan(mode, packaging, audioFilter, gain = 1, nativeASS = false) {
52
+ let id = (mode === 'native' ? packaging === 'adapted-opus' ? gain !== 1 ? 'native-opus-gain' : 'native-opus' : packaging === 'adapted-flac' ? gain !== 1 ? 'native-flac-gain' : 'native-flac' : packaging === 'remux' ? gain !== 1 ? 'native-remux-gain' : 'native-remux' : gain !== 1 ? 'native-direct-gain' : 'native-direct' : mode === 'hybrid' ? audioFilter ? gain !== 1 ? 'hybrid-audio-filter-gain' : 'hybrid-audio-filter' : gain !== 1 ? 'hybrid-gain' : 'hybrid' : gain !== 1 ? 'software-gain' : 'software');
53
+ if (mode === 'native' && nativeASS)
54
+ id = gain !== 1 ? id.replace(/-gain$/, '-ass-gain') : id + '-ass';
55
+ return PLAYBACK_PLANS.find(plan => plan.id === id);
56
+ }
57
+ /** Admission is executable and deliberately finite. Runtime output verification
58
+ * still owns acceptance; browser capability signals cannot prove presentation. */
59
+ export function planAdmission(f) {
60
+ return PLAYBACK_PLANS.map(plan => {
61
+ let code, reason;
62
+ const reject = (c, r) => { code = c; reason = r; };
63
+ const gain = plan.id.endsWith('-gain'), ass = plan.id.includes('-ass'), flac = plan.id.startsWith('native-flac'), opus = plan.id.startsWith('native-opus');
64
+ const effect = featureRejection(plan.mode, { ...f });
65
+ if (effect)
66
+ reject('FEATURE_UNSUPPORTED', effect);
67
+ else if (gain !== (f.gain !== 1))
68
+ reject('PLAN_NOT_REQUESTED', 'Gain stage does not match the requested presentation');
69
+ else if (gain && !f.webAudio)
70
+ reject('DEPLOYMENT_UNAVAILABLE', 'Web Audio is unavailable');
71
+ else if (plan.mode !== 'native' && !f.isolated)
72
+ reject('ISOLATION_REQUIRED', 'mpv deployment requires cross-origin isolation');
73
+ else if (plan.mode === 'hybrid' && !f.webCodecs)
74
+ reject('DEPLOYMENT_UNAVAILABLE', 'WebCodecs video decoding is unavailable');
75
+ else if (plan.mode === 'hybrid' && plan.id.includes('audio-filter') !== !!f.af)
76
+ reject('PLAN_NOT_REQUESTED', 'mpv scalar filter stage does not match the request');
77
+ else if (plan.mode !== 'native' && f.browserTextTracks)
78
+ reject('FEATURE_UNSUPPORTED', 'External browser text tracks cannot be silently discarded');
79
+ else if (plan.mode === 'native') {
80
+ const prepared = plan.id.startsWith('native-remux') || flac || opus;
81
+ if (f.audioOutput !== 'stereo')
82
+ reject('FEATURE_UNSUPPORTED', 'Explicit PCM layout requires mpv');
83
+ else if (ass !== !!f.externalFormats.length)
84
+ reject('PLAN_NOT_REQUESTED', 'Subtitle component does not match the requested presentation');
85
+ else if (ass && (!f.nativeASS || f.externalFormats.some(format => !['ass', 'ssa'].includes(format))))
86
+ reject('FEATURE_UNSUPPORTED', 'External subtitle format requires mpv or explicit Native ASS admission');
87
+ else if (ass && f.manifest)
88
+ reject('QUALIFICATION_REQUIRED', 'Native ASS is qualified only on file presentations');
89
+ else if (ass && !f.isolated)
90
+ reject('ISOLATION_REQUIRED', 'Native libass requires cross-origin isolation');
91
+ else if (prepared && f.manifest)
92
+ reject('QUALIFICATION_REQUIRED', 'File preparation is not qualified for manifest sources');
93
+ else if (prepared && !f.isolated)
94
+ reject('ISOLATION_REQUIRED', 'Native preparation requires cross-origin isolation');
95
+ else if (prepared && (f.nativeRemux === 'never' || !f.mse))
96
+ reject('DEPLOYMENT_UNAVAILABLE', 'Native preparation requires permitted MSE and cross-origin isolation');
97
+ else if (!prepared && (f.nativeRemux === 'always' || f.requiresRemux))
98
+ reject('SOURCE_UNSUPPORTED', 'This source policy requires controlled remux transport');
99
+ else if ((flac || opus) && (f.automatic ? (!flac || !f.automaticLossless) : f.adaptation !== (flac ? 'flac' : 'opus')))
100
+ reject('QUALIFICATION_REQUIRED', 'Audio adaptation requires explicit profile or qualified automatic lossless policy');
101
+ else if (opus && !f.allowLossy)
102
+ reject('POLICY_PROHIBITS_TRANSFORM', 'Lossy audio permission is absent');
103
+ else if (flac && f.automatic && !f.adaptationSourceQualified)
104
+ reject('SOURCE_UNSUPPORTED', f.adaptationSourceRejection ?? 'Automatic FLAC source has not been qualified');
105
+ else if (f.nativeSourceRejection && !(flac && f.automatic && f.automaticLossless))
106
+ reject('SOURCE_UNSUPPORTED', f.nativeSourceRejection);
107
+ }
108
+ return { id: plan.id, mode: plan.mode, eligible: !code, ...(code ? { code, reason } : {}) };
109
+ });
110
+ }
@@ -0,0 +1,45 @@
1
+ import type { PlaybackMode } from '../types.js';
2
+ export type ProbeTrack = {
3
+ id: string;
4
+ index: number;
5
+ type: string;
6
+ codec: string;
7
+ codecString?: string;
8
+ default?: boolean;
9
+ forced?: boolean;
10
+ channels?: number;
11
+ aacObject?: number;
12
+ attachedPicture?: boolean;
13
+ sampleRate?: number;
14
+ bits?: number;
15
+ startTime?: number;
16
+ endTime?: number;
17
+ width?: number;
18
+ height?: number;
19
+ };
20
+ export type Probe = {
21
+ tracks: ProbeTrack[];
22
+ duration: number;
23
+ format?: string;
24
+ identity?: {
25
+ size: string;
26
+ etag?: string;
27
+ };
28
+ };
29
+ /** Narrow file-only automatic FLAC admission. Unknown or unequal ends are rejected.
30
+ * The runtime still verifies packets, samples, actual MSE output and work bounds. */
31
+ export declare function losslessAdaptationRejection(probe: Probe, settings: {
32
+ aid: string;
33
+ sid: string;
34
+ subtitles: boolean;
35
+ }): string | undefined;
36
+ export type SelectionAttempt = {
37
+ mode: PlaybackMode | 'probe';
38
+ outcome: 'skipped' | 'failed' | 'selected';
39
+ reason: string;
40
+ };
41
+ export declare function nativeRejection(probe: Probe, settings: {
42
+ aid: string;
43
+ sid: string;
44
+ subtitles: boolean;
45
+ }, video: HTMLVideoElement): string | undefined;
@@ -0,0 +1,45 @@
1
+ /** Narrow file-only automatic FLAC admission. Unknown or unequal ends are rejected.
2
+ * The runtime still verifies packets, samples, actual MSE output and work bounds. */
3
+ export function losslessAdaptationRejection(probe, settings) {
4
+ if (!probe.format?.includes('matroska'))
5
+ return 'Automatic FLAC requires inspected Matroska';
6
+ const selected = (type, id = 'auto') => { const tracks = probe.tracks.filter(t => t.type === type && !t.attachedPicture); return id === 'no' ? undefined : id === 'auto' ? (tracks.find(t => t.default) || tracks[0]) : tracks.find(t => t.id === id); };
7
+ if (settings.subtitles && selected('sub', settings.sid))
8
+ return 'Embedded subtitles require mpv rendering';
9
+ const v = selected('video'), a = selected('audio', settings.aid);
10
+ if (!v || v.codec !== 'h264' || !v.width || !v.height || v.width * v.height > 1920 * 1080)
11
+ return 'Automatic FLAC requires H264 up to 1080p';
12
+ if (!a || !['pcm_s16le', 'pcm_s24le'].includes(a.codec) || a.sampleRate !== 48000 || ![1, 2].includes(a.channels ?? 0) || ![16, 24].includes(a.bits ?? 0))
13
+ return 'Automatic FLAC requires selected 48 kHz mono/stereo PCM16/24';
14
+ if ([v, a].some(t => !Number.isFinite(t.startTime) || t.startTime < 0 || !Number.isFinite(t.endTime) || t.endTime <= t.startTime))
15
+ return 'Automatic FLAC requires known selected-track bounds';
16
+ if (Math.abs(v.startTime - a.startTime) > .05 || Math.abs(v.endTime - a.endTime) > .05)
17
+ return 'Automatic FLAC selected-track offsets or tails exceed qualification';
18
+ if (!Number.isFinite(probe.duration) || probe.duration <= 0)
19
+ return 'Automatic FLAC requires a finite duration';
20
+ }
21
+ export function nativeRejection(probe, settings, video) {
22
+ const selected = (type, id = 'auto') => { const tracks = probe.tracks.filter(t => t.type === type && !t.attachedPicture); return id === 'no' ? undefined : id === 'auto' ? (tracks.find(t => t.default) || tracks[0]) : tracks.find(t => t.id === id); };
23
+ // Browser text-track exposure cannot reliably prove embedded subtitle delivery.
24
+ if (settings.subtitles && selected('sub', settings.sid))
25
+ return 'Embedded subtitles require mpv rendering';
26
+ const v = selected('video'), a = selected('audio', settings.aid);
27
+ if (!['auto', 'no'].includes(settings.aid) && !a)
28
+ return 'Requested audio track was not found';
29
+ const codecs = { h264: 'avc1', hevc: 'hev1', vp8: 'vp8', vp9: 'vp09.00.10.08', av1: 'av01.0.04M.08', aac: 'mp4a.40.2', mp3: 'mp3', opus: 'opus', vorbis: 'vorbis', flac: 'flac', ac3: 'ac-3', eac3: 'ec-3' };
30
+ if (!v && !a)
31
+ return 'No selected playable streams';
32
+ for (const t of [v, a])
33
+ if (t && !codecs[t.codec])
34
+ return `Native has no qualified ${t.type} mapping for ${t.codec}`;
35
+ if (a?.codec === 'aac') {
36
+ if (!a.aacObject || a.aacObject < 1 || a.aacObject >= 31)
37
+ return 'Native AAC profile is unavailable';
38
+ codecs.aac = `mp4a.40.${a.aacObject}`;
39
+ }
40
+ // Try each relevant packaging contract. A WebM rejection does not rule out MP4
41
+ // (for example VP9 plus AAC). Actual remux configuration and playback still gate.
42
+ const mimes = ['mp4', 'webm'].map(container => `${v ? 'video' : 'audio'}/${container}; codecs="${[v, a].filter(Boolean).map(t => container === 'webm' && t.codec === 'vp9' ? 'vp9' : t.codecString ?? codecs[t.codec]).join(',')}"`);
43
+ if (!mimes.some(mime => video.canPlayType(mime)))
44
+ return `Browser does not report support for ${mimes.join(' or ')}`;
45
+ }
@@ -0,0 +1,8 @@
1
+ import type { MediaInfo, MediaTrack, TimeRange, PlaybackMode } from '../types.js';
2
+ export declare function freeze<T>(value: T): T;
3
+ export declare function ranges(value: unknown): readonly TimeRange[] | null;
4
+ export type RawTrack = Record<string, any>;
5
+ export declare function usesRemuxTracks(plan?: string): boolean;
6
+ export declare function trackKey(track: RawTrack, mode: PlaybackMode, plan?: string): string;
7
+ export declare function tracks(raw: RawTrack[], sourceId: number, mode: PlaybackMode, plan?: string): MediaTrack[];
8
+ export declare function mediaInfo(properties: ReadonlyMap<string, unknown>, mode: PlaybackMode, surface: HTMLCanvasElement | HTMLVideoElement | undefined, list: MediaTrack[]): MediaInfo;
@@ -0,0 +1,56 @@
1
+ export function freeze(value) {
2
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
3
+ Object.values(value).forEach(freeze);
4
+ Object.freeze(value);
5
+ }
6
+ return value;
7
+ }
8
+ export function ranges(value) {
9
+ if (!Array.isArray(value))
10
+ return null;
11
+ const out = value.map(r => ({ start: Number(r.start), end: Number(r.end) }));
12
+ return out.every(r => Number.isFinite(r.start) && Number.isFinite(r.end) && r.start >= 0 && r.end >= r.start) ? out : null;
13
+ }
14
+ export function usesRemuxTracks(plan) { return plan === 'remux' || plan === 'adapted-flac' || plan === 'adapted-opus'; }
15
+ export function trackKey(track, mode, plan) {
16
+ const type = track.type;
17
+ // ff-index belongs to each demuxer: separate subtitle files commonly all use 0.
18
+ // Attachments are replayed in the same order on replacement, retaining mpv IDs.
19
+ if (track.external)
20
+ return `${type}:external:${track['external-index'] ?? track.id}`;
21
+ const index = Number.isInteger(track['ff-index']) ? track['ff-index'] : mode === 'native' && usesRemuxTracks(plan) && type === 'audio' ? Number(track.id) - 1 : undefined;
22
+ return index !== undefined ? `${type}:stream:${index}` : `${type}:${mode === 'native' ? 'native' : 'mpv'}:${track.id}`;
23
+ }
24
+ export function tracks(raw, sourceId, mode, plan) {
25
+ return raw.filter(t => ['audio', 'sub', 'video'].includes(t.type)).map(t => ({ id: `${sourceId}:${trackKey(t, mode, plan)}`, type: t.type === 'sub' ? 'subtitle' : t.type,
26
+ label: String(t.title || t.lang || `${t.type === 'sub' ? 'Subtitle' : t.type === 'audio' ? 'Audio' : 'Video'} ${t.id}`), language: t.lang ? String(t.lang) : null,
27
+ codec: t.codec ? String(t.codec) : null, selected: !!t.selected, external: !!t.external }));
28
+ }
29
+ const positive = (value) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null;
30
+ export function mediaInfo(properties, mode, surface, list) {
31
+ const raw = properties.get('track-list')?.find(t => t.type === 'video' && t.selected);
32
+ let width = null, height = null, rotation = null;
33
+ if (surface?.tagName === 'VIDEO') {
34
+ width = positive(surface.videoWidth);
35
+ height = positive(surface.videoHeight);
36
+ }
37
+ else {
38
+ const p = (mode === 'hybrid' ? undefined : properties.get('video-out-params') || properties.get('video-params'));
39
+ width = positive(p?.dw);
40
+ height = positive(p?.dh);
41
+ if (!width || !height) {
42
+ width = positive(p?.w) || positive(raw?.['demux-w']);
43
+ height = positive(p?.h) || positive(raw?.['demux-h']);
44
+ if (width)
45
+ width *= positive(p?.par) || positive(raw?.['demux-par']) || 1;
46
+ }
47
+ const angle = p?.rotate ?? raw?.['demux-rotation'];
48
+ rotation = typeof angle === 'number' ? angle : null;
49
+ if (width && height) {
50
+ const a = (rotation || 0) * Math.PI / 180, c = Math.abs(Math.cos(a)), s = Math.abs(Math.sin(a));
51
+ [width, height] = [width * c + height * s, width * s + height * c];
52
+ }
53
+ }
54
+ return { displayWidth: width, displayHeight: height, aspectRatio: width && height ? width / height : null, rotation,
55
+ video: list.find(t => t.type === 'video' && t.selected) ?? null, audio: list.find(t => t.type === 'audio' && t.selected) ?? null, subtitle: list.find(t => t.type === 'subtitle' && t.selected) ?? null };
56
+ }
@@ -0,0 +1,127 @@
1
+ import type { AudioOutput, FontAsset, ResourceLimits, SubtitleAsset, MediaInputOptions, StreamingOptions } from '../types.js';
2
+ export type PlayerEvent = {
3
+ event: string;
4
+ id?: number;
5
+ name?: string;
6
+ data?: unknown;
7
+ error?: string;
8
+ [key: string]: unknown;
9
+ };
10
+ export type RemoteSource = MediaInputOptions & {
11
+ streaming?: StreamingOptions;
12
+ url: string;
13
+ format?: 'file' | 'hls' | 'dash';
14
+ headers?: Record<string, string>;
15
+ credentials?: RequestCredentials;
16
+ allowedOrigins?: string[];
17
+ immutable?: boolean;
18
+ refreshAuthorization?: (resource?: {
19
+ url: string;
20
+ }) => Promise<{
21
+ url?: string;
22
+ headers?: Record<string, string>;
23
+ }>;
24
+ };
25
+ export type PlayerDiagnostics = {
26
+ path: 'wasm';
27
+ presentation?: {
28
+ position?: number;
29
+ pts?: number[];
30
+ retained?: number;
31
+ pending?: number;
32
+ received?: number;
33
+ closed?: number;
34
+ };
35
+ decoder?: 'software' | 'webcodecs';
36
+ decoderStats?: Record<string, number | boolean>;
37
+ rendered: number;
38
+ heapBytes: number;
39
+ queuedFrames: number;
40
+ epoch: number;
41
+ io?: Record<string, number | string>;
42
+ seeking?: boolean;
43
+ position?: number;
44
+ presentedPosition?: number;
45
+ ioPending?: boolean;
46
+ interruptions?: number;
47
+ renderMs?: number;
48
+ copyMs?: number;
49
+ };
50
+ /** One isolated software engine per player; bounded remote ranges and local File reads; ArrayBuffer inputs remain capped. */
51
+ export declare class WasmPlayer extends EventTarget {
52
+ private loading;
53
+ private worker;
54
+ private workerOwner;
55
+ private audioContext;
56
+ private audioNode?;
57
+ private analyser?;
58
+ private gainNode?;
59
+ private gainValue;
60
+ private timing?;
61
+ private lastTiming?;
62
+ private nextId;
63
+ private pending;
64
+ private destroyed;
65
+ private destruction?;
66
+ private onDestroyed?;
67
+ private readyTimer?;
68
+ private rejectReady?;
69
+ private eventWaiters;
70
+ private hasFile;
71
+ private opening;
72
+ private refreshAuthorization?;
73
+ private audioHeader;
74
+ private outputChannels;
75
+ private requestedOutput;
76
+ private deviceChannels;
77
+ diagnostics?: PlayerDiagnostics;
78
+ browserCodecsAbsent: boolean;
79
+ properties: Map<string, unknown>;
80
+ readonly ready: Promise<void>;
81
+ constructor(canvas: HTMLCanvasElement, { disableBrowserCodecs, measureOutput, mode, softwarePresenter, audioOutput, audioFallback, resourceLimits, fonts, assetBase }?: {
82
+ assetBase?: URL;
83
+ audioOutput?: AudioOutput;
84
+ audioFallback?: 'stereo' | 'reject';
85
+ resourceLimits?: ResourceLimits;
86
+ fonts?: FontAsset[];
87
+ disableBrowserCodecs?: boolean;
88
+ measureOutput?: boolean;
89
+ mode?: 'hybrid' | 'software';
90
+ softwarePresenter?: 'rgb' | 'experimental-yuv';
91
+ });
92
+ private sendTiming;
93
+ private fail;
94
+ private request;
95
+ open(file: File | ArrayBuffer, options?: MediaInputOptions): Promise<void>;
96
+ openRemote(source: RemoteSource): Promise<void>;
97
+ private waitForEvent;
98
+ private openLocal;
99
+ inspectMetadata(): Promise<void>;
100
+ command(...args: string[]): Promise<void>;
101
+ private setPause;
102
+ play(): Promise<void>;
103
+ pause(): Promise<void>;
104
+ seek(seconds: number): Promise<any>;
105
+ rate(rate: number): Promise<void>;
106
+ volume(percent: number): Promise<void>;
107
+ gain(value: number): Promise<void>;
108
+ selectTrack(type: 'audio' | 'sub', id: string): Promise<void>;
109
+ addSubtitle(subtitle: SubtitleAsset): Promise<void>;
110
+ subtitleVisible(visible: boolean): Promise<void>;
111
+ resize(width: number, height: number): void;
112
+ audioDiagnostics(): {
113
+ gain: number;
114
+ gainStage: string;
115
+ requestedOutput: AudioOutput;
116
+ outputChannels: number;
117
+ deviceChannels: number;
118
+ channelLayout: string;
119
+ state: AudioContextState;
120
+ sampleRate: number;
121
+ mediaFrames: number;
122
+ underruns: number;
123
+ rms: number;
124
+ latencyConfidence: string;
125
+ };
126
+ destroy(): Promise<void>;
127
+ }