@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.
- package/dist/compositor/compositor.d.ts.map +1 -1
- package/dist/compositor-worker.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +4 -3
- package/src/compositor/audio-manager.ts +411 -0
- package/src/compositor/compositor-worker.ts +158 -0
- package/src/compositor/compositor.ts +931 -0
- package/src/compositor/index.ts +19 -0
- package/src/compositor/source-pool.ts +450 -0
- package/src/compositor/types.ts +103 -0
- package/src/compositor/worker-client.ts +139 -0
- package/src/compositor/worker-types.ts +67 -0
- package/src/core/player-core.ts +273 -0
- package/src/core/state-facade.ts +98 -0
- package/src/core/track-switcher.ts +127 -0
- package/src/events/emitter.ts +137 -0
- package/src/events/types.ts +24 -0
- package/src/index.ts +124 -0
- package/src/mediafox.ts +642 -0
- package/src/playback/audio.ts +361 -0
- package/src/playback/controller.ts +446 -0
- package/src/playback/renderer.ts +1176 -0
- package/src/playback/renderers/canvas2d.ts +128 -0
- package/src/playback/renderers/factory.ts +172 -0
- package/src/playback/renderers/index.ts +5 -0
- package/src/playback/renderers/types.ts +57 -0
- package/src/playback/renderers/webgl.ts +373 -0
- package/src/playback/renderers/webgpu.ts +395 -0
- package/src/playlist/manager.ts +268 -0
- package/src/plugins/context.ts +93 -0
- package/src/plugins/index.ts +15 -0
- package/src/plugins/manager.ts +482 -0
- package/src/plugins/types.ts +243 -0
- package/src/sources/manager.ts +285 -0
- package/src/sources/source.ts +84 -0
- package/src/sources/types.ts +17 -0
- package/src/state/store.ts +389 -0
- package/src/state/types.ts +18 -0
- package/src/tracks/manager.ts +421 -0
- package/src/tracks/types.ts +30 -0
- package/src/types/jassub.d.ts +1 -0
- package/src/types.ts +235 -0
- package/src/utils/async-lock.ts +26 -0
- package/src/utils/dispose.ts +28 -0
- package/src/utils/equal.ts +33 -0
- package/src/utils/errors.ts +74 -0
- package/src/utils/logger.ts +50 -0
- 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
|
+
}
|