@mediafox/core 1.2.8 → 1.2.10

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 (48) hide show
  1. package/dist/compositor/compositor.d.ts.map +1 -1
  2. package/dist/compositor-worker.js +1 -1
  3. package/dist/index.js +1 -1
  4. package/package.json +4 -3
  5. package/src/compositor/audio-manager.ts +411 -0
  6. package/src/compositor/compositor-worker.ts +158 -0
  7. package/src/compositor/compositor.ts +931 -0
  8. package/src/compositor/index.ts +19 -0
  9. package/src/compositor/source-pool.ts +450 -0
  10. package/src/compositor/types.ts +103 -0
  11. package/src/compositor/worker-client.ts +139 -0
  12. package/src/compositor/worker-types.ts +67 -0
  13. package/src/core/player-core.ts +273 -0
  14. package/src/core/state-facade.ts +98 -0
  15. package/src/core/track-switcher.ts +127 -0
  16. package/src/events/emitter.ts +137 -0
  17. package/src/events/types.ts +24 -0
  18. package/src/index.ts +124 -0
  19. package/src/mediafox.ts +642 -0
  20. package/src/playback/audio.ts +361 -0
  21. package/src/playback/controller.ts +446 -0
  22. package/src/playback/renderer.ts +1176 -0
  23. package/src/playback/renderers/canvas2d.ts +128 -0
  24. package/src/playback/renderers/factory.ts +172 -0
  25. package/src/playback/renderers/index.ts +5 -0
  26. package/src/playback/renderers/types.ts +57 -0
  27. package/src/playback/renderers/webgl.ts +373 -0
  28. package/src/playback/renderers/webgpu.ts +395 -0
  29. package/src/playlist/manager.ts +268 -0
  30. package/src/plugins/context.ts +93 -0
  31. package/src/plugins/index.ts +15 -0
  32. package/src/plugins/manager.ts +482 -0
  33. package/src/plugins/types.ts +243 -0
  34. package/src/sources/manager.ts +285 -0
  35. package/src/sources/source.ts +84 -0
  36. package/src/sources/types.ts +17 -0
  37. package/src/state/store.ts +389 -0
  38. package/src/state/types.ts +18 -0
  39. package/src/tracks/manager.ts +421 -0
  40. package/src/tracks/types.ts +30 -0
  41. package/src/types/jassub.d.ts +1 -0
  42. package/src/types.ts +235 -0
  43. package/src/utils/async-lock.ts +26 -0
  44. package/src/utils/dispose.ts +28 -0
  45. package/src/utils/equal.ts +33 -0
  46. package/src/utils/errors.ts +74 -0
  47. package/src/utils/logger.ts +50 -0
  48. package/src/utils/time.ts +157 -0
@@ -0,0 +1,421 @@
1
+ import type { Input, InputAudioTrack, InputVideoTrack } from 'mediabunny';
2
+
3
+ import type { AudioTrackInfo, SubtitleTrackInfo, VideoTrackInfo } from '../types';
4
+
5
+ import type { SubtitleTrackRegistration, SubtitleTrackResource, TrackManagerState, TrackSelectionEvent } from './types';
6
+
7
+ export class TrackManager {
8
+ private input: Input | null = null;
9
+ private videoTracks: Map<string, InputVideoTrack> = new Map();
10
+ private audioTracks: Map<string, InputAudioTrack> = new Map();
11
+ private videoTrackInfos: VideoTrackInfo[] = [];
12
+ private audioTrackInfos: AudioTrackInfo[] = [];
13
+ private subtitleTrackInfos: SubtitleTrackInfo[] = [];
14
+ private subtitleProviders: Map<string, SubtitleTrackRegistration[]> = new Map();
15
+ private subtitleTrackResolvers: Map<string, () => Promise<SubtitleTrackResource>> = new Map();
16
+ private selectedVideoTrack: string | null = null;
17
+ private selectedAudioTrack: string | null = null;
18
+ private selectedSubtitleTrack: string | null = null;
19
+ private onTrackChange?: (event: TrackSelectionEvent) => void;
20
+
21
+ async initialize(input: Input): Promise<void> {
22
+ // Clear old tracks before loading new ones
23
+ this.videoTracks.clear();
24
+ this.audioTracks.clear();
25
+ this.videoTrackInfos = [];
26
+ this.audioTrackInfos = [];
27
+ this.selectedVideoTrack = null;
28
+ this.selectedAudioTrack = null;
29
+
30
+ this.input = input;
31
+ await this.loadTracks();
32
+ }
33
+
34
+ private async loadTracks(): Promise<void> {
35
+ if (!this.input) return;
36
+
37
+ // Load video tracks
38
+ const videoTracks = await this.input.getVideoTracks();
39
+ this.videoTrackInfos = await Promise.all(
40
+ videoTracks.map(async (track) => {
41
+ const id = `video-${track.id}`;
42
+ this.videoTracks.set(id, track);
43
+
44
+ // Get packet statistics for bitrate and frame rate
45
+ let frameRate = 0;
46
+ let bitrate = 0;
47
+ try {
48
+ const stats = await track.computePacketStats(100);
49
+ frameRate = stats.averagePacketRate;
50
+ bitrate = stats.averageBitrate;
51
+ } catch {
52
+ // Ignore errors in stats computation
53
+ }
54
+
55
+ const info: VideoTrackInfo = {
56
+ id,
57
+ codec: track.codec,
58
+ language: track.languageCode,
59
+ name: track.name,
60
+ width: track.codedWidth,
61
+ height: track.codedHeight,
62
+ frameRate,
63
+ bitrate,
64
+ rotation: track.rotation,
65
+ selected: false,
66
+ decodable: await track.canDecode(),
67
+ };
68
+
69
+ return info;
70
+ })
71
+ );
72
+
73
+ // Load audio tracks
74
+ const audioTracks = await this.input.getAudioTracks();
75
+ this.audioTrackInfos = await Promise.all(
76
+ audioTracks.map(async (track) => {
77
+ const id = `audio-${track.id}`;
78
+ this.audioTracks.set(id, track);
79
+
80
+ // Get packet statistics for bitrate
81
+ let bitrate = 0;
82
+ try {
83
+ const stats = await track.computePacketStats(100);
84
+ bitrate = stats.averageBitrate;
85
+ } catch {
86
+ // Ignore errors in stats computation
87
+ }
88
+
89
+ const info: AudioTrackInfo = {
90
+ id,
91
+ codec: track.codec,
92
+ language: track.languageCode,
93
+ name: track.name,
94
+ channels: track.numberOfChannels,
95
+ sampleRate: track.sampleRate,
96
+ bitrate,
97
+ selected: false,
98
+ decodable: await track.canDecode(),
99
+ };
100
+
101
+ return info;
102
+ })
103
+ );
104
+
105
+ // Auto-select first decodable tracks
106
+ if (this.videoTrackInfos.length > 0) {
107
+ const firstDecodable = this.videoTrackInfos.find((t) => t.decodable);
108
+ if (firstDecodable) {
109
+ this.selectVideoTrack(firstDecodable.id);
110
+ }
111
+ }
112
+
113
+ if (this.audioTrackInfos.length > 0) {
114
+ const firstDecodable = this.audioTrackInfos.find((t) => t.decodable);
115
+ if (firstDecodable) {
116
+ this.selectAudioTrack(firstDecodable.id);
117
+ }
118
+ }
119
+ }
120
+
121
+ getVideoTracks(): VideoTrackInfo[] {
122
+ return [...this.videoTrackInfos];
123
+ }
124
+
125
+ getAudioTracks(): AudioTrackInfo[] {
126
+ return [...this.audioTrackInfos];
127
+ }
128
+
129
+ getSubtitleTracks(): SubtitleTrackInfo[] {
130
+ return [...this.subtitleTrackInfos];
131
+ }
132
+
133
+ getSelectedVideoTrack(): InputVideoTrack | null {
134
+ if (!this.selectedVideoTrack) return null;
135
+ return this.videoTracks.get(this.selectedVideoTrack) ?? null;
136
+ }
137
+
138
+ getSelectedAudioTrack(): InputAudioTrack | null {
139
+ if (!this.selectedAudioTrack) return null;
140
+ return this.audioTracks.get(this.selectedAudioTrack) ?? null;
141
+ }
142
+
143
+ getSelectedVideoTrackInfo(): VideoTrackInfo | null {
144
+ if (!this.selectedVideoTrack) return null;
145
+ return this.videoTrackInfos.find((t) => t.id === this.selectedVideoTrack) ?? null;
146
+ }
147
+
148
+ getSelectedAudioTrackInfo(): AudioTrackInfo | null {
149
+ if (!this.selectedAudioTrack) return null;
150
+ return this.audioTrackInfos.find((t) => t.id === this.selectedAudioTrack) ?? null;
151
+ }
152
+
153
+ getSelectedSubtitleTrackInfo(): SubtitleTrackInfo | null {
154
+ if (!this.selectedSubtitleTrack) return null;
155
+ return this.subtitleTrackInfos.find((t) => t.id === this.selectedSubtitleTrack) ?? null;
156
+ }
157
+
158
+ selectVideoTrack(trackId: string | null): boolean {
159
+ if (trackId === this.selectedVideoTrack) return true;
160
+
161
+ if (trackId && !this.videoTracks.has(trackId)) {
162
+ return false;
163
+ }
164
+
165
+ const previousId = this.selectedVideoTrack;
166
+
167
+ // Update selection state
168
+ this.videoTrackInfos.forEach((track) => {
169
+ track.selected = track.id === trackId;
170
+ });
171
+
172
+ this.selectedVideoTrack = trackId;
173
+
174
+ // Notify change
175
+ if (this.onTrackChange) {
176
+ this.onTrackChange({
177
+ type: 'video',
178
+ previousTrackId: previousId,
179
+ newTrackId: trackId,
180
+ });
181
+ }
182
+
183
+ return true;
184
+ }
185
+
186
+ selectAudioTrack(trackId: string | null): boolean {
187
+ if (trackId === this.selectedAudioTrack) return true;
188
+
189
+ if (trackId && !this.audioTracks.has(trackId)) {
190
+ return false;
191
+ }
192
+
193
+ const previousId = this.selectedAudioTrack;
194
+
195
+ // Update selection state
196
+ this.audioTrackInfos.forEach((track) => {
197
+ track.selected = track.id === trackId;
198
+ });
199
+
200
+ this.selectedAudioTrack = trackId;
201
+
202
+ // Notify change
203
+ if (this.onTrackChange) {
204
+ this.onTrackChange({
205
+ type: 'audio',
206
+ previousTrackId: previousId,
207
+ newTrackId: trackId,
208
+ });
209
+ }
210
+
211
+ return true;
212
+ }
213
+
214
+ selectSubtitleTrack(trackId: string | null): boolean {
215
+ if (trackId === this.selectedSubtitleTrack) return true;
216
+
217
+ if (trackId && !this.subtitleTrackResolvers.has(trackId)) {
218
+ return false;
219
+ }
220
+
221
+ const previousId = this.selectedSubtitleTrack;
222
+
223
+ // Update selection state
224
+ this.subtitleTrackInfos.forEach((track) => {
225
+ track.selected = track.id === trackId;
226
+ });
227
+
228
+ this.selectedSubtitleTrack = trackId;
229
+
230
+ // Notify change
231
+ if (this.onTrackChange) {
232
+ this.onTrackChange({
233
+ type: 'subtitle',
234
+ previousTrackId: previousId,
235
+ newTrackId: trackId,
236
+ });
237
+ }
238
+
239
+ return true;
240
+ }
241
+
242
+ registerSubtitleTracks(sourceId: string, entries: SubtitleTrackRegistration[]): void {
243
+ this.subtitleProviders.set(sourceId, entries);
244
+ this.rebuildSubtitleTracks();
245
+ }
246
+
247
+ unregisterSubtitleTracks(sourceId: string): void {
248
+ if (!this.subtitleProviders.delete(sourceId)) {
249
+ return;
250
+ }
251
+ this.rebuildSubtitleTracks();
252
+ }
253
+
254
+ async getSubtitleTrackResource(trackId: string | null): Promise<SubtitleTrackResource | null> {
255
+ if (!trackId) return null;
256
+ const resolver = this.subtitleTrackResolvers.get(trackId);
257
+ if (!resolver) return null;
258
+ return resolver();
259
+ }
260
+
261
+ private rebuildSubtitleTracks(): void {
262
+ const previousSelected = this.selectedSubtitleTrack;
263
+
264
+ this.subtitleTrackInfos = [];
265
+ this.subtitleTrackResolvers.clear();
266
+
267
+ for (const entries of this.subtitleProviders.values()) {
268
+ for (const entry of entries) {
269
+ const info: SubtitleTrackInfo = {
270
+ ...entry.info,
271
+ selected: false,
272
+ };
273
+ this.subtitleTrackInfos.push(info);
274
+ this.subtitleTrackResolvers.set(info.id, entry.resolver);
275
+ }
276
+ }
277
+
278
+ let nextSelected = previousSelected;
279
+ if (!nextSelected || !this.subtitleTrackResolvers.has(nextSelected)) {
280
+ nextSelected = this.subtitleTrackInfos[0]?.id ?? null;
281
+ }
282
+
283
+ this.selectedSubtitleTrack = nextSelected;
284
+ this.subtitleTrackInfos.forEach((track) => {
285
+ track.selected = track.id === this.selectedSubtitleTrack;
286
+ });
287
+
288
+ if (previousSelected !== this.selectedSubtitleTrack && this.onTrackChange) {
289
+ this.onTrackChange({
290
+ type: 'subtitle',
291
+ previousTrackId: previousSelected,
292
+ newTrackId: this.selectedSubtitleTrack,
293
+ });
294
+ }
295
+ }
296
+
297
+ setTrackChangeListener(listener: (event: TrackSelectionEvent) => void): void {
298
+ this.onTrackChange = listener;
299
+ }
300
+
301
+ getState(): TrackManagerState {
302
+ return {
303
+ videoTracks: this.getVideoTracks(),
304
+ audioTracks: this.getAudioTracks(),
305
+ subtitleTracks: this.getSubtitleTracks(),
306
+ selectedVideoTrack: this.selectedVideoTrack,
307
+ selectedAudioTrack: this.selectedAudioTrack,
308
+ selectedSubtitleTrack: this.selectedSubtitleTrack,
309
+ };
310
+ }
311
+
312
+ getPrimaryVideoTrack(): InputVideoTrack | null {
313
+ if (this.videoTracks.size === 0) return null;
314
+ return this.selectedVideoTrack
315
+ ? (this.videoTracks.get(this.selectedVideoTrack) ?? null)
316
+ : (this.videoTracks.values().next().value ?? null);
317
+ }
318
+
319
+ getPrimaryAudioTrack(): InputAudioTrack | null {
320
+ if (this.audioTracks.size === 0) return null;
321
+ return this.selectedAudioTrack
322
+ ? (this.audioTracks.get(this.selectedAudioTrack) ?? null)
323
+ : (this.audioTracks.values().next().value ?? null);
324
+ }
325
+
326
+ hasVideo(): boolean {
327
+ return this.videoTrackInfos.length > 0;
328
+ }
329
+
330
+ hasAudio(): boolean {
331
+ return this.audioTrackInfos.length > 0;
332
+ }
333
+
334
+ hasSubtitles(): boolean {
335
+ return this.subtitleTrackInfos.length > 0;
336
+ }
337
+
338
+ dispose(): void {
339
+ this.videoTracks.clear();
340
+ this.audioTracks.clear();
341
+ this.videoTrackInfos = [];
342
+ this.audioTrackInfos = [];
343
+ this.subtitleTrackInfos = [];
344
+ this.subtitleProviders.clear();
345
+ this.subtitleTrackResolvers.clear();
346
+ this.selectedVideoTrack = null;
347
+ this.selectedAudioTrack = null;
348
+ this.selectedSubtitleTrack = null;
349
+ this.input = null;
350
+ this.onTrackChange = undefined;
351
+ }
352
+
353
+ // Replace an audio track (by original input track id) with a new InputAudioTrack
354
+ async replaceAudioTrackByInputId(originalInputId: number, newTrack: InputAudioTrack): Promise<void> {
355
+ // Find map key for original track
356
+ let key: string | null = null;
357
+ for (const [k, t] of this.audioTracks.entries()) {
358
+ if (t.id === originalInputId) {
359
+ key = k;
360
+ break;
361
+ }
362
+ }
363
+ if (!key) return;
364
+
365
+ // Update map
366
+ this.audioTracks.set(key, newTrack);
367
+
368
+ // Update info
369
+ const idx = this.audioTrackInfos.findIndex((i) => i.id === key);
370
+ if (idx !== -1) {
371
+ let bitrate = 0;
372
+ try {
373
+ const stats = await newTrack.computePacketStats(100);
374
+ bitrate = stats.averageBitrate;
375
+ } catch {}
376
+ this.audioTrackInfos[idx] = {
377
+ ...this.audioTrackInfos[idx],
378
+ codec: newTrack.codec,
379
+ channels: newTrack.numberOfChannels,
380
+ sampleRate: newTrack.sampleRate,
381
+ bitrate,
382
+ decodable: await newTrack.canDecode(),
383
+ };
384
+ }
385
+ }
386
+
387
+ // Replace a video track (by original input track id) with a new InputVideoTrack
388
+ async replaceVideoTrackByInputId(originalInputId: number, newTrack: InputVideoTrack): Promise<void> {
389
+ let key: string | null = null;
390
+ for (const [k, t] of this.videoTracks.entries()) {
391
+ if (t.id === originalInputId) {
392
+ key = k;
393
+ break;
394
+ }
395
+ }
396
+ if (!key) return;
397
+
398
+ this.videoTracks.set(key, newTrack);
399
+
400
+ const idx = this.videoTrackInfos.findIndex((i) => i.id === key);
401
+ if (idx !== -1) {
402
+ let frameRate = 0;
403
+ let bitrate = 0;
404
+ try {
405
+ const stats = await newTrack.computePacketStats(100);
406
+ frameRate = stats.averagePacketRate;
407
+ bitrate = stats.averageBitrate;
408
+ } catch {}
409
+ this.videoTrackInfos[idx] = {
410
+ ...this.videoTrackInfos[idx],
411
+ codec: newTrack.codec,
412
+ width: newTrack.codedWidth,
413
+ height: newTrack.codedHeight,
414
+ rotation: newTrack.rotation,
415
+ frameRate,
416
+ bitrate,
417
+ decodable: await newTrack.canDecode(),
418
+ };
419
+ }
420
+ }
421
+ }
@@ -0,0 +1,30 @@
1
+ import type { AudioTrackInfo, SubtitleTrackInfo, VideoTrackInfo } from '../types';
2
+
3
+ export type SubtitleTrackFormat = 'ass' | 'srt' | 'vtt';
4
+
5
+ export interface SubtitleTrackResource {
6
+ format: SubtitleTrackFormat;
7
+ content?: string;
8
+ url?: string;
9
+ fonts?: Array<string | Uint8Array>;
10
+ }
11
+
12
+ export interface SubtitleTrackRegistration {
13
+ info: SubtitleTrackInfo;
14
+ resolver: () => Promise<SubtitleTrackResource>;
15
+ }
16
+
17
+ export interface TrackManagerState {
18
+ videoTracks: VideoTrackInfo[];
19
+ audioTracks: AudioTrackInfo[];
20
+ subtitleTracks: SubtitleTrackInfo[];
21
+ selectedVideoTrack: string | null;
22
+ selectedAudioTrack: string | null;
23
+ selectedSubtitleTrack: string | null;
24
+ }
25
+
26
+ export interface TrackSelectionEvent {
27
+ type: 'video' | 'audio' | 'subtitle';
28
+ previousTrackId: string | null;
29
+ newTrackId: string | null;
30
+ }
@@ -0,0 +1 @@
1
+ declare module 'jassub';
package/src/types.ts ADDED
@@ -0,0 +1,235 @@
1
+ import type { AudioCodec, MetadataTags, SubtitleCodec, VideoCodec } from 'mediabunny';
2
+
3
+ export type MediaSource = File | Blob | string | URL | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array>;
4
+
5
+ export type PlayerState = 'idle' | 'loading' | 'ready' | 'playing' | 'paused' | 'ended' | 'error';
6
+
7
+ export type PlaybackMode = 'normal' | 'loop' | 'loop-one';
8
+
9
+ export type Rotation = 0 | 90 | 180 | 270;
10
+
11
+ export type RendererType = 'webgpu' | 'webgl' | 'canvas2d';
12
+
13
+ export interface PlayerOptions {
14
+ renderTarget?: HTMLCanvasElement | OffscreenCanvas;
15
+ audioContext?: AudioContext;
16
+ volume?: number;
17
+ muted?: boolean;
18
+ playbackRate?: number;
19
+ autoplay?: boolean;
20
+ preload?: 'none' | 'metadata' | 'auto';
21
+ crossOrigin?: string;
22
+ maxCacheSize?: number;
23
+ renderer?: RendererType;
24
+ }
25
+
26
+ export interface MediaInfo {
27
+ duration: number;
28
+ format: string;
29
+ mimeType: string;
30
+ metadata: MetadataTags;
31
+ hasVideo: boolean;
32
+ hasAudio: boolean;
33
+ hasSubtitles: boolean;
34
+ }
35
+
36
+ export interface VideoTrackInfo {
37
+ id: string;
38
+ codec: VideoCodec | null;
39
+ language: string;
40
+ name: string | null;
41
+ width: number;
42
+ height: number;
43
+ frameRate: number;
44
+ bitrate: number;
45
+ rotation: 0 | 90 | 180 | 270;
46
+ selected: boolean;
47
+ decodable: boolean;
48
+ }
49
+
50
+ export interface AudioTrackInfo {
51
+ id: string;
52
+ codec: AudioCodec | null;
53
+ language: string;
54
+ name: string | null;
55
+ channels: number;
56
+ sampleRate: number;
57
+ bitrate: number;
58
+ selected: boolean;
59
+ decodable: boolean;
60
+ }
61
+
62
+ export interface SubtitleTrackInfo {
63
+ id: string;
64
+ codec: SubtitleCodec | null;
65
+ language: string;
66
+ name: string | null;
67
+ selected: boolean;
68
+ }
69
+
70
+ export interface PlayerStateData {
71
+ state: PlayerState;
72
+ currentTime: number;
73
+ duration: number;
74
+ buffered: TimeRange[];
75
+ volume: number;
76
+ muted: boolean;
77
+ playbackRate: number;
78
+ playing: boolean;
79
+ paused: boolean;
80
+ ended: boolean;
81
+ seeking: boolean;
82
+ waiting: boolean;
83
+ error: Error | null;
84
+ mediaInfo: MediaInfo | null;
85
+ videoTracks: VideoTrackInfo[];
86
+ audioTracks: AudioTrackInfo[];
87
+ subtitleTracks: SubtitleTrackInfo[];
88
+ selectedVideoTrack: string | null;
89
+ selectedAudioTrack: string | null;
90
+ selectedSubtitleTrack: string | null;
91
+ canPlay: boolean;
92
+ canPlayThrough: boolean;
93
+ isLive: boolean;
94
+ rendererType: RendererType;
95
+ playlist: Playlist;
96
+ currentPlaylistIndex: number | null;
97
+ playlistMode: PlaylistMode;
98
+ rotation: Rotation;
99
+ displaySize: { width: number; height: number };
100
+ }
101
+
102
+ export interface TimeRange {
103
+ start: number;
104
+ end: number;
105
+ }
106
+
107
+ export interface SeekOptions {
108
+ precise?: boolean;
109
+ keyframe?: boolean;
110
+ }
111
+
112
+ export interface LoadOptions {
113
+ autoplay?: boolean;
114
+ startTime?: number;
115
+ replacePlaylist?: boolean;
116
+ /** Playlist item id for prefetch optimization */
117
+ playlistItemId?: string;
118
+ }
119
+
120
+ export interface ScreenshotOptions {
121
+ format?: 'png' | 'jpeg' | 'webp';
122
+ quality?: number;
123
+ width?: number;
124
+ height?: number;
125
+ fit?: 'fill' | 'contain' | 'cover';
126
+ }
127
+
128
+ export interface QualityLevel {
129
+ id: string;
130
+ label: string;
131
+ width?: number;
132
+ height?: number;
133
+ bitrate?: number;
134
+ codec?: VideoCodec;
135
+ auto?: boolean;
136
+ }
137
+
138
+ export type PlayerEventMap = {
139
+ statechange: PlayerStateData;
140
+ loadstart: undefined;
141
+ loadedmetadata: MediaInfo;
142
+ loadeddata: undefined;
143
+ canplay: undefined;
144
+ canplaythrough: undefined;
145
+ play: undefined;
146
+ pause: undefined;
147
+ playing: undefined;
148
+ ended: undefined;
149
+ timeupdate: { currentTime: number };
150
+ durationchange: { duration: number };
151
+ volumechange: { volume: number; muted: boolean };
152
+ ratechange: { playbackRate: number };
153
+ seeking: { currentTime: number };
154
+ seeked: { currentTime: number };
155
+ waiting: undefined;
156
+ progress: { buffered: TimeRange[] };
157
+ error: Error;
158
+ warning: {
159
+ type: string;
160
+ message: string;
161
+ error?: Error;
162
+ };
163
+ trackchange: {
164
+ type: 'video' | 'audio' | 'subtitle';
165
+ trackId: string | null;
166
+ };
167
+ qualitychange: {
168
+ qualityId: string;
169
+ auto: boolean;
170
+ };
171
+ resize: {
172
+ width: number;
173
+ height: number;
174
+ };
175
+ rotationchange: {
176
+ rotation: Rotation;
177
+ displaySize: { width: number; height: number };
178
+ };
179
+ rendererchange: RendererType;
180
+ rendererfallback: {
181
+ from: RendererType;
182
+ to: RendererType;
183
+ };
184
+ playlistchange: { playlist: Playlist };
185
+ playlistitemchange: { index: number; item: PlaylistItem; previousIndex?: number };
186
+ playlistend: undefined;
187
+ playlistadd: { item: PlaylistItem; index: number };
188
+ playlistremove: { index: number };
189
+ playlistitemerror: { index: number; error: Error };
190
+ };
191
+
192
+ export type PlayerEventListener<K extends keyof PlayerEventMap> = (event: PlayerEventMap[K]) => void;
193
+
194
+ export interface Subscription {
195
+ unsubscribe(): void;
196
+ }
197
+
198
+ export interface PerformanceMetrics {
199
+ droppedFrames: number;
200
+ totalFrames: number;
201
+ decodedFrames: number;
202
+ currentFPS: number;
203
+ averageFPS: number;
204
+ bufferHealth: number;
205
+ latency: number;
206
+ bandwidth: number;
207
+ }
208
+
209
+ export interface ChapterInfo {
210
+ id: string;
211
+ title: string;
212
+ startTime: number;
213
+ endTime: number;
214
+ thumbnail?: string;
215
+ }
216
+
217
+ export interface CuePoint {
218
+ id: string;
219
+ time: number;
220
+ type: string;
221
+ data?: unknown;
222
+ }
223
+
224
+ export type PlaylistMode = 'sequential' | 'manual' | 'repeat' | 'repeat-one' | null;
225
+
226
+ export interface PlaylistItem {
227
+ id: string;
228
+ mediaSource: MediaSource;
229
+ title?: string;
230
+ poster?: string;
231
+ savedPosition: number | null;
232
+ duration: number | null;
233
+ }
234
+
235
+ export type Playlist = PlaylistItem[];
@@ -0,0 +1,26 @@
1
+ export class KeyedLock {
2
+ private chains = new Map<string, Promise<unknown>>();
3
+
4
+ async run<T>(key: string, fn: () => Promise<T>): Promise<T> {
5
+ const prev = this.chains.get(key) ?? Promise.resolve();
6
+ let resolveNext: (() => void) | undefined;
7
+ const next = new Promise<void>((r) => {
8
+ resolveNext = r;
9
+ });
10
+ this.chains.set(
11
+ key,
12
+ prev.then(() => next)
13
+ );
14
+
15
+ try {
16
+ // Wait previous task for this key
17
+ await prev;
18
+ // Execute guarded function
19
+ return await fn();
20
+ } finally {
21
+ // Release lock for next queued task
22
+ resolveNext?.();
23
+ // If the current next promise is the tail, we can optionally prune later
24
+ }
25
+ }
26
+ }