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,27 @@
1
+ import type { RemoteSource, TextTrackSource, TrackType, SubtitleAsset, MediaInputOptions } from '../types.js';
2
+ export interface Backend extends EventTarget {
3
+ readonly ready: Promise<void>;
4
+ readonly properties: Map<string, unknown>;
5
+ readonly diagnostics?: object;
6
+ open(file: File | ArrayBuffer, options?: MediaInputOptions): Promise<void>;
7
+ openRemote(source: RemoteSource): Promise<void>;
8
+ play(): Promise<void>;
9
+ pause(): Promise<void>;
10
+ seek(seconds: number): Promise<void>;
11
+ rate(value: number): Promise<void>;
12
+ volume(value: number): Promise<void>;
13
+ gain?(value: number): Promise<void>;
14
+ selectTrack(type: TrackType, id: string): Promise<void>;
15
+ subtitleVisible(visible: boolean): Promise<void>;
16
+ resize(width: number, height: number): void;
17
+ command?(...args: string[]): Promise<void>;
18
+ addTextTrack?(track: TextTrackSource): Promise<void>;
19
+ addSubtitle?(subtitle: SubtitleAsset): Promise<void>;
20
+ audioDiagnostics(): object;
21
+ destroy(): Promise<void>;
22
+ }
23
+ export type Session = {
24
+ backend: Backend;
25
+ surface: HTMLCanvasElement | HTMLVideoElement;
26
+ error?: Error;
27
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { PlayerErrorCode, SessionError, OperationKind } from '../types.js';
2
+ /** Public text intentionally omits all URL queries, fragments and userinfo. */
3
+ export declare function redact(value: unknown): any;
4
+ export declare class PlayerError extends Error implements SessionError {
5
+ readonly code: PlayerErrorCode;
6
+ readonly operationId: number | null;
7
+ readonly operation: OperationKind | null;
8
+ readonly scope: 'operation' | 'session';
9
+ readonly retryable: boolean;
10
+ constructor(code: PlayerErrorCode, message: string, operationId?: number | null, operation?: OperationKind | null, scope?: 'operation' | 'session', retryable?: boolean);
11
+ toJSON(): SessionError;
12
+ }
13
+ export declare function playerError(error: unknown, id?: number | null, operation?: OperationKind | null, scope?: 'operation' | 'session'): PlayerError;
@@ -0,0 +1,54 @@
1
+ /** Public text intentionally omits all URL queries, fragments and userinfo. */
2
+ export function redact(value) {
3
+ if (typeof value === 'string')
4
+ return value.replace(/https?:\/\/[^\s<>"']+/gi, text => {
5
+ try {
6
+ const u = new URL(text);
7
+ return u.origin + u.pathname + (u.search || u.hash ? '?[redacted]' : '');
8
+ }
9
+ catch {
10
+ return '[redacted URL]';
11
+ }
12
+ }).replace(/\b(authorization|proxy-authorization|cookie|set-cookie|x-api-key)\s*[:=]\s*[^\r\n]+/gi, '$1: [redacted]').replace(/\b(Bearer|Basic)\s+[^\s,;]+/gi, '$1 [redacted]');
13
+ if (Array.isArray(value))
14
+ return value.map(redact);
15
+ if (value && typeof value === 'object')
16
+ return Object.fromEntries(Object.entries(value).map(([key, v]) => [key, /authorization|cookie|token|secret|password|headers/i.test(key) ? '[redacted]' : redact(v)]));
17
+ return value;
18
+ }
19
+ export class PlayerError extends Error {
20
+ code;
21
+ operationId;
22
+ operation;
23
+ scope;
24
+ retryable;
25
+ constructor(code, message, operationId = null, operation = null, scope = 'operation', retryable = false) {
26
+ super(redact(message));
27
+ this.code = code;
28
+ this.operationId = operationId;
29
+ this.operation = operation;
30
+ this.scope = scope;
31
+ this.retryable = retryable;
32
+ this.name = 'PlayerError';
33
+ }
34
+ toJSON() { return { code: this.code, message: this.message, operationId: this.operationId, operation: this.operation, scope: this.scope, retryable: this.retryable }; }
35
+ }
36
+ export function playerError(error, id = null, operation = null, scope = 'operation') {
37
+ if (error instanceof PlayerError)
38
+ return new PlayerError(error.code, error.message, id ?? error.operationId, operation ?? error.operation, scope, error.retryable);
39
+ if (error instanceof Error && 'code' in error && error.code === 'UNSUPPORTED_TIMELINE')
40
+ return new PlayerError('UNSUPPORTED_TIMELINE', error.message, id, operation, scope);
41
+ const message = error instanceof Error ? error.message : String(error);
42
+ const name = error instanceof Error ? error.name : '';
43
+ const code = name === 'AbortError' || /^(?:Operation aborted|Open aborted|Player (?:element )?(?:is )?destroyed|Player element disconnected)|cancelled/i.test(message) ? 'ABORTED'
44
+ : name === 'NotAllowedError' || /autoplay|user gesture|audio context.*suspended/i.test(message) ? 'AUTOPLAY_BLOCKED'
45
+ : /cross.origin isolat|secure.*isolated/i.test(message) ? 'ISOLATION_REQUIRED'
46
+ : /representation changed|changed length|Source changed/i.test(message) ? 'SOURCE_CHANGED'
47
+ : /401|403|permission|origin.*not allowed|authorization/i.test(message) ? 'SOURCE_PERMISSION'
48
+ : /timed? ?out|deadline/i.test(message) ? 'NETWORK_TIMEOUT'
49
+ : /fetch.*module|load.*font|\.wasm|initialization|worker.*failed|import.*module|Aborted\(.*fetch|wasm.*failed|WebAssembly.*(?:compile|instantiate)/i.test(message) ? 'ASSET_LOAD_FAILED'
50
+ : /Invalid|Expected|must be|limited to|queue.*full|No source/i.test(message) ? 'INVALID_ARGUMENT'
51
+ : /preserve.*track|unknown.*track|require.*mode|unsupported.*feature|not supported.*source|filters require|cannot.*discard|external.*require/i.test(message) ? 'UNSUPPORTED_FEATURE'
52
+ : /unsupported|no playback route|no browser bridge/i.test(message) ? 'UNSUPPORTED_MEDIA' : 'DECODE_FAILED';
53
+ return new PlayerError(code, message, id, operation, scope, ['NETWORK_TIMEOUT', 'ASSET_LOAD_FAILED', 'AUTOPLAY_BLOCKED'].includes(code));
54
+ }
@@ -0,0 +1,38 @@
1
+ import type { FontAsset, SubtitleAsset } from '../types.js';
2
+ /** External ASS rendering on the accepted media timeline. One bounded RPC at a time. */
3
+ export declare class NativeASS {
4
+ private video;
5
+ private time;
6
+ private failed;
7
+ readonly canvas: HTMLCanvasElement;
8
+ private worker;
9
+ private pending;
10
+ private sequence;
11
+ private revision;
12
+ private stopped;
13
+ private enabled;
14
+ private busy;
15
+ private changingTrack;
16
+ private frame;
17
+ private last;
18
+ private lastRevision;
19
+ private loading;
20
+ private observer?;
21
+ private handlers;
22
+ readonly ready: Promise<void>;
23
+ readonly stats: {
24
+ renders: number;
25
+ bitmapUpdates: number;
26
+ bytes: number;
27
+ peakBytes: number;
28
+ discarded: number;
29
+ };
30
+ constructor(video: HTMLVideoElement, time: () => number, base: URL, fonts: FontAsset[], failed: (e: Error) => void);
31
+ private request;
32
+ private fail;
33
+ load(asset: SubtitleAsset): Promise<void>;
34
+ visible(value: boolean): void;
35
+ private invalidate;
36
+ private tick;
37
+ destroy(): void;
38
+ }
@@ -0,0 +1,206 @@
1
+ /** External ASS rendering on the accepted media timeline. One bounded RPC at a time. */
2
+ export class NativeASS {
3
+ video;
4
+ time;
5
+ failed;
6
+ canvas = document.createElement('canvas');
7
+ worker;
8
+ pending = new Map();
9
+ sequence = 0;
10
+ revision = 0;
11
+ stopped = false;
12
+ enabled = false;
13
+ busy = false;
14
+ changingTrack = false;
15
+ frame = 0;
16
+ last = '';
17
+ lastRevision = -1;
18
+ loading = new AbortController();
19
+ observer;
20
+ handlers = [];
21
+ ready;
22
+ stats = { renders: 0, bitmapUpdates: 0, bytes: 0, peakBytes: 0, discarded: 0 };
23
+ constructor(video, time, base, fonts, failed) {
24
+ this.video = video;
25
+ this.time = time;
26
+ this.failed = failed;
27
+ if (!crossOriginIsolated)
28
+ throw Error('Native ASS requires cross-origin isolation');
29
+ this.canvas.className = 'demuxe-native-ass';
30
+ this.canvas.style.cssText = 'position:absolute;pointer-events:none;display:none';
31
+ if (!video.parentElement)
32
+ throw Error('Missing Native presentation container');
33
+ // Worker construction can synchronously fail (CSP, URL or allocation).
34
+ // Do not attach a canvas until its owner exists.
35
+ this.worker = new Worker(new URL('web/native-ass-worker.js', base), { type: 'module' });
36
+ const parent = video.parentElement, prior = { position: parent.style.position, fit: video.style.objectFit, pip: video.disablePictureInPicture, remote: video.disableRemotePlayback };
37
+ try {
38
+ parent.style.position = 'relative';
39
+ parent.append(this.canvas);
40
+ video.style.objectFit = 'contain';
41
+ video.disablePictureInPicture = true;
42
+ video.disableRemotePlayback = true;
43
+ this.worker.onmessage = ({ data }) => { const p = this.pending.get(data.id); if (!p)
44
+ return; clearTimeout(p.timer); this.pending.delete(data.id); data.error ? p.reject(Error(data.error)) : p.resolve(data); };
45
+ this.worker.onerror = e => { e.preventDefault(); this.fail(Error(e.message || 'Subtitle worker failed')); };
46
+ this.worker.onmessageerror = () => this.fail(Error('Subtitle worker message failure'));
47
+ this.observer = new ResizeObserver(() => this.invalidate());
48
+ this.observer.observe(video);
49
+ for (const event of ['seeked', 'seeking', 'pause', 'play', 'ratechange', 'loadedmetadata']) {
50
+ const listener = () => this.invalidate();
51
+ video.addEventListener(event, listener);
52
+ this.handlers.push(() => video.removeEventListener(event, listener));
53
+ }
54
+ const fullscreen = () => { if (document.fullscreenElement === video)
55
+ this.fail(Error('Native ASS requires fullscreen on the player container, not the video element'));
56
+ else
57
+ this.invalidate(); };
58
+ document.addEventListener('fullscreenchange', fullscreen);
59
+ this.handlers.push(() => document.removeEventListener('fullscreenchange', fullscreen));
60
+ this.ready = (async () => {
61
+ const deadline = setTimeout(() => this.loading.abort(), 10000);
62
+ try {
63
+ const response = await fetch(new URL('fixtures/DejaVuSans.ttf', base), { signal: this.loading.signal });
64
+ if (!response.ok)
65
+ throw Error('Subtitle default font unavailable');
66
+ const bytes = await response.arrayBuffer();
67
+ if (bytes.byteLength > 8 * 1024 * 1024)
68
+ throw Error('Subtitle font budget exceeded');
69
+ if (this.stopped)
70
+ throw Error('Subtitle renderer destroyed');
71
+ await this.request('init', { fonts: [{ name: 'DejaVuSans.ttf', bytes }, ...fonts] });
72
+ }
73
+ finally {
74
+ clearTimeout(deadline);
75
+ }
76
+ })();
77
+ this.ready.catch(() => { });
78
+ }
79
+ catch (error) {
80
+ this.destroy();
81
+ parent.style.position = prior.position;
82
+ video.style.objectFit = prior.fit;
83
+ video.disablePictureInPicture = prior.pip;
84
+ video.disableRemotePlayback = prior.remote;
85
+ throw error;
86
+ }
87
+ }
88
+ request(type, data = {}) {
89
+ if (this.stopped)
90
+ return Promise.reject(Error('Subtitle renderer destroyed'));
91
+ const id = ++this.sequence;
92
+ return new Promise((resolve, reject) => {
93
+ const timer = setTimeout(() => this.fail(Error('Subtitle worker deadline exceeded')), 10000);
94
+ this.pending.set(id, { resolve, reject, timer });
95
+ try {
96
+ this.worker.postMessage({ id, type, ...data });
97
+ }
98
+ catch (error) {
99
+ clearTimeout(timer);
100
+ this.pending.delete(id);
101
+ reject(error);
102
+ }
103
+ });
104
+ }
105
+ fail(error) { if (this.stopped)
106
+ return; this.destroy(); this.failed(error); }
107
+ async load(asset) {
108
+ await this.ready;
109
+ // Keep the accepted bitmap visible until parsing succeeds. Prevent render
110
+ // requests from racing the track transaction; rejected parses retain the old
111
+ // native track and force a fresh render on the same presentation timeline.
112
+ this.changingTrack = true;
113
+ this.revision++;
114
+ try {
115
+ await this.request('load', { bytes: asset.bytes });
116
+ }
117
+ finally {
118
+ this.changingTrack = false;
119
+ this.invalidate();
120
+ }
121
+ }
122
+ visible(value) { this.enabled = value; this.canvas.style.display = value ? 'block' : 'none'; this.invalidate(); }
123
+ invalidate() { this.revision++; this.last = ''; if (!this.stopped && !this.frame)
124
+ this.frame = requestAnimationFrame(() => this.tick()); }
125
+ tick() {
126
+ this.frame = 0;
127
+ if (this.stopped || !this.enabled || this.changingTrack)
128
+ return;
129
+ const rect = this.video.getBoundingClientRect(), parent = this.video.parentElement.getBoundingClientRect();
130
+ const ratio = this.video.videoWidth / this.video.videoHeight;
131
+ let width = rect.width, height = rect.height;
132
+ if (Number.isFinite(ratio)) {
133
+ if (width / height > ratio)
134
+ width = height * ratio;
135
+ else
136
+ height = width / ratio;
137
+ }
138
+ if (width < 1 || height < 1) {
139
+ this.frame = requestAnimationFrame(() => this.tick());
140
+ return;
141
+ }
142
+ this.canvas.style.left = `${rect.left - parent.left + (rect.width - width) / 2}px`;
143
+ this.canvas.style.top = `${rect.top - parent.top + (rect.height - height) / 2}px`;
144
+ this.canvas.style.width = `${width}px`;
145
+ this.canvas.style.height = `${height}px`;
146
+ const scale = Math.min(1, 1920 / width, 1080 / height), w = Math.max(1, Math.round(width * scale)), h = Math.max(1, Math.round(height * scale));
147
+ const sourceWidth = this.video.videoWidth, sourceHeight = this.video.videoHeight;
148
+ const seconds = this.time(), key = `${w}:${h}:${sourceWidth}:${sourceHeight}:${seconds}`, revision = this.revision;
149
+ if (!this.busy && key !== this.last) {
150
+ this.busy = true;
151
+ this.request('render', { seconds, width: w, height: h, sourceWidth, sourceHeight, force: this.lastRevision !== revision }).then(({ tiles, size, unchanged }) => {
152
+ if (this.stopped || !this.enabled || revision !== this.revision) {
153
+ this.stats.discarded++;
154
+ return;
155
+ }
156
+ this.lastRevision = revision;
157
+ if (unchanged) {
158
+ this.stats.renders++;
159
+ this.last = key;
160
+ return;
161
+ }
162
+ // Resize only when a complete accepted replacement bitmap is ready. A
163
+ // paused resize must redraw active cues even if libass reports unchanged.
164
+ this.canvas.width = w;
165
+ this.canvas.height = h;
166
+ const context = this.canvas.getContext('2d');
167
+ for (const t of tiles) {
168
+ const pixels = new ImageData(t.w, t.h);
169
+ for (let i = 0; i < t.bytes.length; i++) {
170
+ pixels.data[i * 4] = t.color >>> 24;
171
+ pixels.data[i * 4 + 1] = (t.color >>> 16) & 255;
172
+ pixels.data[i * 4 + 2] = (t.color >>> 8) & 255;
173
+ pixels.data[i * 4 + 3] = Math.round(t.bytes[i] * (255 - (t.color & 255)) / 255);
174
+ }
175
+ const tile = new OffscreenCanvas(t.w, t.h);
176
+ tile.getContext('2d').putImageData(pixels, 0, 0);
177
+ context.drawImage(tile, t.x, t.y);
178
+ }
179
+ this.stats.renders++;
180
+ this.stats.bitmapUpdates++;
181
+ this.stats.bytes += size;
182
+ this.stats.peakBytes = Math.max(this.stats.peakBytes, size);
183
+ this.last = key;
184
+ }, error => this.fail(error)).finally(() => { this.busy = false; });
185
+ }
186
+ if (!this.video.paused || this.busy || this.last !== key)
187
+ this.frame = requestAnimationFrame(() => this.tick());
188
+ }
189
+ destroy() {
190
+ if (this.stopped)
191
+ return;
192
+ this.stopped = true;
193
+ this.revision++;
194
+ this.loading.abort();
195
+ cancelAnimationFrame(this.frame);
196
+ this.observer?.disconnect();
197
+ this.handlers.forEach(f => f());
198
+ this.worker.terminate();
199
+ for (const p of this.pending.values()) {
200
+ clearTimeout(p.timer);
201
+ p.reject(Error('Subtitle renderer destroyed'));
202
+ }
203
+ this.pending.clear();
204
+ this.canvas.remove();
205
+ }
206
+ }
@@ -0,0 +1,95 @@
1
+ import type { RemoteSource, TextTrackSource, TrackType, SubtitleAsset, FontAsset } from '../types.js';
2
+ import type { Backend } from './backend.js';
3
+ /** Browser media ownership, including listeners, pending loads and object URLs. */
4
+ export declare class NativePlayer extends EventTarget implements Backend {
5
+ private video;
6
+ private remuxPolicy;
7
+ private assetBase;
8
+ private bufferedSeeks;
9
+ private audioAdaptation?;
10
+ private initialAudioTrack?;
11
+ private nativeASS;
12
+ private fonts;
13
+ readonly ready: Promise<void>;
14
+ readonly properties: Map<string, unknown>;
15
+ private stopped;
16
+ private ass?;
17
+ private assAssets;
18
+ private assIndex;
19
+ private gainContext?;
20
+ private gainSource?;
21
+ private gainNode?;
22
+ private gainValue;
23
+ gain(value: number): Promise<void>;
24
+ private resumeGain;
25
+ private destruction?;
26
+ private opening;
27
+ private remux?;
28
+ private adapted;
29
+ private remuxSource?;
30
+ private directFailure?;
31
+ private shiftedCues;
32
+ private sourceTime;
33
+ private sourceDuration;
34
+ private objectURL?;
35
+ private selectedSub;
36
+ private subsVisible;
37
+ private cancelers;
38
+ private listeners;
39
+ constructor(video: HTMLVideoElement, remuxPolicy?: 'auto' | 'never' | 'always', assetBase?: URL, bufferedSeeks?: boolean, audioAdaptation?: "flac" | "opus" | undefined, initialAudioTrack?: number | undefined, nativeASS?: boolean, fonts?: FontAsset[]);
40
+ private emit;
41
+ private assertActive;
42
+ private wait;
43
+ private refresh;
44
+ get diagnostics(): {
45
+ path: string;
46
+ plan: string;
47
+ subtitleOverlay: {
48
+ renders: number;
49
+ bitmapUpdates: number;
50
+ bytes: number;
51
+ peakBytes: number;
52
+ discarded: number;
53
+ component: string;
54
+ scope: string;
55
+ destination: string;
56
+ } | undefined;
57
+ audioProcessing: {
58
+ component: string;
59
+ gain: number;
60
+ contextState: AudioContextState | undefined;
61
+ baseLatency: number | undefined;
62
+ };
63
+ directFailure: string | undefined;
64
+ remux: Record<string, unknown> | undefined;
65
+ position: number;
66
+ rendered: number;
67
+ dropped: number;
68
+ readyState: number;
69
+ };
70
+ private load;
71
+ private startRemux;
72
+ private loadPlan;
73
+ open(file: File | ArrayBuffer): Promise<void>;
74
+ openRemote(source: RemoteSource): Promise<void>;
75
+ play(): Promise<void>;
76
+ pause(): Promise<void>;
77
+ seek(seconds: number): Promise<void>;
78
+ private seekPresented;
79
+ rate(value: number): Promise<void>;
80
+ volume(value: number): Promise<void>;
81
+ selectTrack(type: TrackType, id: string): Promise<void>;
82
+ private applySubtitles;
83
+ subtitleVisible(visible: boolean): Promise<void>;
84
+ addSubtitle(asset: SubtitleAsset): Promise<void>;
85
+ addTextTrack(source: TextTrackSource): Promise<void>;
86
+ private shiftTextTrack;
87
+ resize(width: number, height: number): void;
88
+ audioDiagnostics(): {
89
+ state: string;
90
+ source: string;
91
+ decodedSampleCountersAvailable: boolean;
92
+ };
93
+ destroy(): Promise<void>;
94
+ private dispose;
95
+ }