@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,446 @@
1
+ import type { InputAudioTrack, InputVideoTrack } from 'mediabunny';
2
+ import type { PluginManager } from '../plugins/manager';
3
+ import { AudioManager } from './audio';
4
+ import { VideoRenderer } from './renderer';
5
+ import type { RendererType, Rotation } from './renderers';
6
+
7
+ // Constants for timing intervals
8
+ const RENDER_INTERVAL_MS = 100; // Interval for background tab rendering
9
+ const SYNC_INTERVAL_MS = 250; // Interval for time update sync
10
+
11
+ export interface PlaybackControllerOptions {
12
+ canvas?: HTMLCanvasElement | OffscreenCanvas;
13
+ audioContext?: AudioContext;
14
+ volume?: number;
15
+ muted?: boolean;
16
+ playbackRate?: number;
17
+ rendererType?: RendererType;
18
+ }
19
+
20
+ export class PlaybackController {
21
+ private videoRenderer: VideoRenderer;
22
+ private audioManager: AudioManager;
23
+ private playing = false;
24
+ private currentTime = 0;
25
+ private duration = 0;
26
+ private playbackRate = 1;
27
+ private animationFrameId: number | null = null;
28
+ private lastFrameTime = 0;
29
+ private syncIntervalId: number | null = null;
30
+ private renderIntervalId: number | null = null;
31
+ private isWaiting = false;
32
+ private onTimeUpdate?: (time: number) => void;
33
+ private onEnded?: () => void;
34
+ private onWaiting?: () => void;
35
+ private onPlaying?: () => void;
36
+
37
+ constructor(options: PlaybackControllerOptions = {}) {
38
+ this.videoRenderer = new VideoRenderer({
39
+ canvas: options.canvas,
40
+ rendererType: options.rendererType,
41
+ });
42
+
43
+ this.audioManager = new AudioManager({
44
+ audioContext: options.audioContext,
45
+ volume: options.volume,
46
+ muted: options.muted,
47
+ });
48
+
49
+ this.playbackRate = options.playbackRate ?? 1;
50
+ }
51
+
52
+ async setVideoTrack(track: InputVideoTrack | null): Promise<void> {
53
+ if (!track) {
54
+ this.videoRenderer.dispose();
55
+ return;
56
+ }
57
+
58
+ await this.videoRenderer.setVideoTrack(track);
59
+ const duration = await track.computeDuration();
60
+ this.duration = Math.max(this.duration, duration);
61
+ }
62
+
63
+ /**
64
+ * Attempt to set video track without throwing. Returns true on success.
65
+ */
66
+ async trySetVideoTrack(track: InputVideoTrack | null): Promise<boolean> {
67
+ try {
68
+ await this.setVideoTrack(track);
69
+ return true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ async setAudioTrack(track: InputAudioTrack | null): Promise<void> {
76
+ const resumePlayback = this.playing;
77
+ const resumeTime = this.getCurrentTime();
78
+
79
+ if (!track) {
80
+ this.audioManager.dispose();
81
+ return;
82
+ }
83
+
84
+ const trackDuration = await track.computeDuration();
85
+ const clampedResumeTime = Math.max(0, Math.min(resumeTime, trackDuration));
86
+
87
+ await this.audioManager.setAudioTrack(track);
88
+ await this.audioManager.seek(clampedResumeTime);
89
+
90
+ if (resumePlayback) {
91
+ await this.audioManager.play(clampedResumeTime);
92
+ }
93
+
94
+ this.currentTime = clampedResumeTime;
95
+ this.duration = Math.max(this.duration, trackDuration);
96
+ }
97
+
98
+ /**
99
+ * Attempt to set audio track without throwing. Returns true on success.
100
+ */
101
+ async trySetAudioTrack(track: InputAudioTrack | null): Promise<boolean> {
102
+ try {
103
+ await this.setAudioTrack(track);
104
+ return true;
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ async setCanvas(canvas: HTMLCanvasElement | OffscreenCanvas): Promise<void> {
111
+ await this.videoRenderer.setCanvas(canvas);
112
+ }
113
+
114
+ async play(): Promise<void> {
115
+ if (this.playing) return;
116
+
117
+ this.playing = true;
118
+ this.lastFrameTime = performance.now();
119
+
120
+ // If we're at the end, restart from beginning
121
+ if (this.currentTime >= this.duration) {
122
+ this.currentTime = 0;
123
+ await this.videoRenderer.seek(0);
124
+ }
125
+
126
+ // Start audio playback
127
+ await this.audioManager.play(this.currentTime);
128
+
129
+ // Start video rendering loop
130
+ this.startRenderLoop();
131
+
132
+ // Start sync interval for time updates
133
+ this.startSyncInterval();
134
+ }
135
+
136
+ pause(): void {
137
+ if (!this.playing) return;
138
+
139
+ this.playing = false;
140
+
141
+ // Clear waiting state without emitting playing event (we're pausing, not playing)
142
+ this.isWaiting = false;
143
+
144
+ // Pause audio
145
+ this.audioManager.pause();
146
+
147
+ // Stop render loop
148
+ this.stopRenderLoop();
149
+
150
+ // Stop sync interval
151
+ this.stopSyncInterval();
152
+
153
+ // Update current time to audio time (most accurate)
154
+ if (this.audioManager.isPlaying()) {
155
+ this.currentTime = this.audioManager.getCurrentTime();
156
+ }
157
+ }
158
+
159
+ async seek(time: number): Promise<void> {
160
+ const clampedTime = Math.max(0, Math.min(time, this.duration));
161
+ this.currentTime = clampedTime;
162
+
163
+ // Seek video - this will start a new iterator
164
+ await this.videoRenderer.seek(clampedTime);
165
+
166
+ // Seek audio
167
+ await this.audioManager.seek(clampedTime);
168
+
169
+ // Notify time update
170
+ if (this.onTimeUpdate) {
171
+ this.onTimeUpdate(this.currentTime);
172
+ }
173
+ }
174
+
175
+ private startRenderLoop(): void {
176
+ if (this.animationFrameId !== null || this.renderIntervalId !== null) {
177
+ // Already running
178
+ return;
179
+ }
180
+
181
+ const render = (requestNextFrame = true) => {
182
+ if (!this.playing) return;
183
+
184
+ // Get accurate time from audio manager if available
185
+ if (this.audioManager.isPlaying()) {
186
+ this.currentTime = this.audioManager.getCurrentTime();
187
+ } else {
188
+ // Fallback to manual time tracking
189
+ const now = performance.now();
190
+ const deltaTime = (now - this.lastFrameTime) / 1000;
191
+ this.lastFrameTime = now;
192
+ this.currentTime += deltaTime * this.playbackRate;
193
+ }
194
+
195
+ // Check for end of playback
196
+ if (this.currentTime >= this.duration) {
197
+ this.handleEnded();
198
+ return;
199
+ }
200
+
201
+ // Update video frame synchronously
202
+ const { isStarving } = this.videoRenderer.updateFrame(this.currentTime);
203
+
204
+ // Handle buffering state changes
205
+ if (isStarving && !this.isWaiting) {
206
+ this.isWaiting = true;
207
+ if (this.onWaiting) {
208
+ this.onWaiting();
209
+ }
210
+ } else if (!isStarving && this.isWaiting) {
211
+ this.isWaiting = false;
212
+ if (this.onPlaying) {
213
+ this.onPlaying();
214
+ }
215
+ }
216
+
217
+ // Schedule next frame
218
+ if (requestNextFrame) {
219
+ this.animationFrameId = requestAnimationFrame(() => render());
220
+ }
221
+ };
222
+
223
+ // Start the render loop
224
+ this.animationFrameId = requestAnimationFrame(() => render());
225
+
226
+ // Also call render on an interval to ensure updates even when tab is not visible
227
+ if (this.renderIntervalId === null) {
228
+ this.renderIntervalId = window.setInterval(() => render(false), RENDER_INTERVAL_MS);
229
+ }
230
+ }
231
+
232
+ private stopRenderLoop(): void {
233
+ if (this.animationFrameId !== null) {
234
+ cancelAnimationFrame(this.animationFrameId);
235
+ this.animationFrameId = null;
236
+ }
237
+ if (this.renderIntervalId !== null) {
238
+ clearInterval(this.renderIntervalId);
239
+ this.renderIntervalId = null;
240
+ }
241
+ }
242
+
243
+ private startSyncInterval(): void {
244
+ if (this.syncIntervalId !== null) return;
245
+ // Sync for time updates
246
+ this.syncIntervalId = window.setInterval(() => {
247
+ if (this.playing && this.onTimeUpdate) {
248
+ this.onTimeUpdate(this.currentTime);
249
+ }
250
+ }, SYNC_INTERVAL_MS);
251
+ }
252
+
253
+ private stopSyncInterval(): void {
254
+ if (this.syncIntervalId !== null) {
255
+ clearInterval(this.syncIntervalId);
256
+ this.syncIntervalId = null;
257
+ }
258
+ }
259
+
260
+ private handleEnded(): void {
261
+ this.pause();
262
+ this.currentTime = this.duration;
263
+
264
+ if (this.onEnded) {
265
+ this.onEnded();
266
+ }
267
+ }
268
+
269
+ getCurrentTime(): number {
270
+ if (this.playing && this.audioManager.isPlaying()) {
271
+ return this.audioManager.getCurrentTime();
272
+ }
273
+ return this.currentTime;
274
+ }
275
+
276
+ getDuration(): number {
277
+ return this.duration;
278
+ }
279
+
280
+ setDuration(duration: number): void {
281
+ this.duration = duration;
282
+ }
283
+
284
+ isPlaying(): boolean {
285
+ return this.playing;
286
+ }
287
+
288
+ setVolume(volume: number): void {
289
+ this.audioManager.setVolume(volume);
290
+ }
291
+
292
+ getVolume(): number {
293
+ return this.audioManager.getVolume();
294
+ }
295
+
296
+ setMuted(muted: boolean): void {
297
+ this.audioManager.setMuted(muted);
298
+ }
299
+
300
+ isMuted(): boolean {
301
+ return this.audioManager.isMuted();
302
+ }
303
+
304
+ setPlaybackRate(rate: number): void {
305
+ const clamped = Math.max(0.25, Math.min(4, rate));
306
+ if (this.playbackRate === clamped) {
307
+ return;
308
+ }
309
+
310
+ this.playbackRate = clamped;
311
+ this.audioManager.setPlaybackRate(clamped);
312
+
313
+ // Reset manual timer to avoid jumps when running without audio
314
+ this.lastFrameTime = performance.now();
315
+ }
316
+
317
+ getPlaybackRate(): number {
318
+ return this.playbackRate;
319
+ }
320
+
321
+ setTimeUpdateCallback(callback: (time: number) => void): void {
322
+ this.onTimeUpdate = callback;
323
+ }
324
+
325
+ setEndedCallback(callback: () => void): void {
326
+ this.onEnded = callback;
327
+ }
328
+
329
+ setWaitingCallback(callback: () => void): void {
330
+ this.onWaiting = callback;
331
+ }
332
+
333
+ setPlayingCallback(callback: () => void): void {
334
+ this.onPlaying = callback;
335
+ }
336
+
337
+ isBuffering(): boolean {
338
+ return this.isWaiting;
339
+ }
340
+
341
+ async screenshot(options?: { format?: 'png' | 'jpeg' | 'webp'; quality?: number }): Promise<Blob | null> {
342
+ return this.videoRenderer.screenshot(this.currentTime, options);
343
+ }
344
+
345
+ getVideoRenderer(): VideoRenderer {
346
+ return this.videoRenderer;
347
+ }
348
+
349
+ getAudioManager(): AudioManager {
350
+ return this.audioManager;
351
+ }
352
+
353
+ async switchRenderer(type: RendererType): Promise<void> {
354
+ await this.videoRenderer.switchRenderer(type);
355
+ }
356
+
357
+ getRendererType(): RendererType {
358
+ return this.videoRenderer.getRendererType();
359
+ }
360
+
361
+ updateCanvasDimensions(): void {
362
+ this.videoRenderer.updateCanvasDimensions();
363
+ }
364
+
365
+ setRendererChangeCallback(callback: (type: RendererType) => void): void {
366
+ this.videoRenderer.setRendererChangeCallback(callback);
367
+ }
368
+
369
+ setRendererFallbackCallback(callback: (from: RendererType, to: RendererType) => void): void {
370
+ this.videoRenderer.setRendererFallbackCallback(callback);
371
+ }
372
+
373
+ setRotationChangeCallback(
374
+ callback: (rotation: Rotation, displaySize: { width: number; height: number }) => void
375
+ ): void {
376
+ this.videoRenderer.setRotationChangeCallback(callback);
377
+ }
378
+
379
+ setRotation(rotation: Rotation): void {
380
+ this.videoRenderer.setRotation(rotation);
381
+ }
382
+
383
+ getRotation(): Rotation {
384
+ return this.videoRenderer.getRotation();
385
+ }
386
+
387
+ getDisplaySize(): { width: number; height: number } {
388
+ return this.videoRenderer.getDisplaySize();
389
+ }
390
+
391
+ setPluginManager(pluginManager: PluginManager): void {
392
+ this.videoRenderer.setPluginManager(pluginManager);
393
+ this.audioManager.setPluginManager(pluginManager);
394
+ }
395
+
396
+ getCanvas(): HTMLCanvasElement | OffscreenCanvas | null {
397
+ return this.videoRenderer.getCanvas();
398
+ }
399
+
400
+ refreshOverlays(): void {
401
+ this.videoRenderer.refreshOverlays();
402
+ }
403
+
404
+ /**
405
+ * Rebuild the audio graph with current plugin hooks.
406
+ * Call this after installing/uninstalling audio plugins.
407
+ */
408
+ rebuildAudioGraph(): void {
409
+ this.audioManager.rebuildAudioGraph();
410
+ }
411
+
412
+ async reset(): Promise<void> {
413
+ // Stop playback completely
414
+ this.pause();
415
+
416
+ // Clear any pending animation frames/intervals first
417
+ this.stopRenderLoop();
418
+ this.stopSyncInterval();
419
+
420
+ // Reset time to beginning
421
+ this.currentTime = 0;
422
+ this.duration = 0;
423
+
424
+ // Clear iterators to stop in-flight async operations before input is disposed
425
+ await Promise.all([this.videoRenderer.clearIterators(), this.audioManager.clearIterators()]);
426
+
427
+ // Reset playback rate to default
428
+ this.playbackRate = 1;
429
+ this.lastFrameTime = 0;
430
+ }
431
+
432
+ dispose(): void {
433
+ this.pause();
434
+ this.videoRenderer.dispose();
435
+ this.audioManager.dispose();
436
+ this.onTimeUpdate = undefined;
437
+ this.onEnded = undefined;
438
+ this.onWaiting = undefined;
439
+ this.onPlaying = undefined;
440
+ }
441
+
442
+ destroy(): void {
443
+ this.dispose();
444
+ this.audioManager.destroy();
445
+ }
446
+ }