@openplayerjs/core 3.0.0-beta.0

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/index.js ADDED
@@ -0,0 +1,1232 @@
1
+ const DVR_THRESHOLD = 120;
2
+ const EVENT_OPTIONS = { passive: false };
3
+
4
+ class BaseMediaEngine {
5
+ constructor() {
6
+ Object.defineProperty(this, "media", {
7
+ enumerable: true,
8
+ configurable: true,
9
+ writable: true,
10
+ value: null
11
+ });
12
+ Object.defineProperty(this, "events", {
13
+ enumerable: true,
14
+ configurable: true,
15
+ writable: true,
16
+ value: null
17
+ });
18
+ Object.defineProperty(this, "commands", {
19
+ enumerable: true,
20
+ configurable: true,
21
+ writable: true,
22
+ value: []
23
+ });
24
+ Object.defineProperty(this, "mediaListeners", {
25
+ enumerable: true,
26
+ configurable: true,
27
+ writable: true,
28
+ value: []
29
+ });
30
+ }
31
+ /**
32
+ * Bridge real HTMLMediaElement events into the player EventBus using
33
+ * HTML5 event names (no payload; consumers read from player/media).
34
+ */
35
+ bindMediaEvents(media, events) {
36
+ this.media = media;
37
+ this.events = events;
38
+ const onLoadedMetadata = () => events.emit('loadedmetadata');
39
+ const onDurationChange = () => events.emit('durationchange');
40
+ const onTimeUpdate = () => events.emit('timeupdate');
41
+ const onWaiting = () => events.emit('waiting');
42
+ const onSeeking = () => events.emit('seeking');
43
+ const onSeeked = () => events.emit('seeked');
44
+ const onEnded = () => events.emit('ended');
45
+ const onError = () => events.emit('error', media.error);
46
+ const onPlay = () => events.emit('play');
47
+ const onPlaying = () => events.emit('playing');
48
+ const onPause = () => events.emit('pause');
49
+ const onVolumeChange = () => events.emit('volumechange');
50
+ const onRateChange = () => events.emit('ratechange');
51
+ this.addMediaListener(media, 'loadedmetadata', onLoadedMetadata, EVENT_OPTIONS);
52
+ this.addMediaListener(media, 'durationchange', onDurationChange, EVENT_OPTIONS);
53
+ this.addMediaListener(media, 'timeupdate', onTimeUpdate, EVENT_OPTIONS);
54
+ this.addMediaListener(media, 'waiting', onWaiting, EVENT_OPTIONS);
55
+ this.addMediaListener(media, 'seeking', onSeeking, EVENT_OPTIONS);
56
+ this.addMediaListener(media, 'seeked', onSeeked, EVENT_OPTIONS);
57
+ this.addMediaListener(media, 'ended', onEnded, EVENT_OPTIONS);
58
+ this.addMediaListener(media, 'error', onError, EVENT_OPTIONS);
59
+ this.addMediaListener(media, 'playing', onPlaying, EVENT_OPTIONS);
60
+ this.addMediaListener(media, 'pause', onPause, EVENT_OPTIONS);
61
+ this.addMediaListener(media, 'play', onPlay, EVENT_OPTIONS);
62
+ this.addMediaListener(media, 'volumechange', onVolumeChange, EVENT_OPTIONS);
63
+ this.addMediaListener(media, 'ratechange', onRateChange, EVENT_OPTIONS);
64
+ }
65
+ unbindMediaEvents() {
66
+ if (!this.media)
67
+ return;
68
+ for (const l of this.mediaListeners) {
69
+ this.media.removeEventListener(l.type, l.handler, l.options);
70
+ }
71
+ this.mediaListeners = [];
72
+ }
73
+ addMediaListener(media, type, handler, options) {
74
+ media.addEventListener(type, handler, options);
75
+ this.mediaListeners.push({ type, handler, options });
76
+ }
77
+ canHandlePlayback(ctx) {
78
+ const owner = ctx.core.leases.owner('playback');
79
+ return !owner || owner === this.name;
80
+ }
81
+ /**
82
+ * Commands are explicit and separate from notifications.
83
+ */
84
+ bindCommands(ctx) {
85
+ const { media, events } = ctx;
86
+ this.commands.push(events.on('cmd:seek', (t) => {
87
+ if (!this.canHandlePlayback(ctx))
88
+ return;
89
+ try {
90
+ media.currentTime = t;
91
+ }
92
+ catch {
93
+ // ignore
94
+ }
95
+ }));
96
+ this.commands.push(events.on('cmd:setVolume', (v) => {
97
+ media.volume = v;
98
+ }));
99
+ this.commands.push(events.on('cmd:setMuted', (m) => {
100
+ media.muted = m;
101
+ }));
102
+ this.commands.push(events.on('cmd:setRate', (r) => {
103
+ if (!this.canHandlePlayback(ctx))
104
+ return;
105
+ media.playbackRate = r;
106
+ }));
107
+ this.bindPlayPauseCommands(ctx);
108
+ }
109
+ unbindCommands() {
110
+ for (const off of this.commands)
111
+ off();
112
+ this.commands = [];
113
+ }
114
+ bindPlayPauseCommands(ctx) {
115
+ const { media, events } = ctx;
116
+ this.commands.push(events.on('cmd:play', () => {
117
+ if (!this.canHandlePlayback(ctx))
118
+ return;
119
+ this.playImpl(media);
120
+ }));
121
+ this.commands.push(events.on('cmd:pause', () => {
122
+ if (!this.canHandlePlayback(ctx))
123
+ return;
124
+ this.pauseImpl(media);
125
+ }));
126
+ }
127
+ playImpl(media) {
128
+ void media.play().catch(() => {
129
+ // ignore
130
+ });
131
+ }
132
+ pauseImpl(media) {
133
+ media.pause();
134
+ }
135
+ }
136
+
137
+ class DefaultMediaEngine extends BaseMediaEngine {
138
+ constructor() {
139
+ super(...arguments);
140
+ Object.defineProperty(this, "name", {
141
+ enumerable: true,
142
+ configurable: true,
143
+ writable: true,
144
+ value: 'default-engine'
145
+ });
146
+ Object.defineProperty(this, "version", {
147
+ enumerable: true,
148
+ configurable: true,
149
+ writable: true,
150
+ value: '1.0.0'
151
+ });
152
+ Object.defineProperty(this, "capabilities", {
153
+ enumerable: true,
154
+ configurable: true,
155
+ writable: true,
156
+ value: ['media-engine']
157
+ });
158
+ Object.defineProperty(this, "priority", {
159
+ enumerable: true,
160
+ configurable: true,
161
+ writable: true,
162
+ value: 0
163
+ });
164
+ }
165
+ canPlay(source) {
166
+ const media = document.createElement('video');
167
+ return media.canPlayType(source.type || '') !== '';
168
+ }
169
+ attach(ctx) {
170
+ if (ctx.activeSource?.src && ctx.media.src !== ctx.activeSource.src) {
171
+ ctx.media.src = ctx.activeSource.src;
172
+ }
173
+ try {
174
+ ctx.media.load();
175
+ }
176
+ catch {
177
+ // ignore
178
+ }
179
+ this.bindMediaEvents(ctx.media, ctx.events);
180
+ this.bindCommands(ctx);
181
+ // When preload="none" and play is requested, bump preload so the browser
182
+ // will actually fetch resource metadata and fire loadedmetadata.
183
+ this.commands.push(ctx.events.on('cmd:startLoad', () => {
184
+ const s = ctx.core.state.current;
185
+ if (['ready', 'playing', 'paused', 'waiting', 'seeking', 'ended'].includes(s))
186
+ return;
187
+ if (ctx.media.preload !== 'none')
188
+ return;
189
+ ctx.media.preload = 'metadata';
190
+ try {
191
+ ctx.media.load();
192
+ }
193
+ catch {
194
+ // ignore
195
+ }
196
+ }));
197
+ }
198
+ detach() {
199
+ this.unbindCommands();
200
+ this.unbindMediaEvents();
201
+ }
202
+ }
203
+
204
+ const defaultConfiguration = {
205
+ startTime: 0,
206
+ duration: 0,
207
+ };
208
+
209
+ class DisposableStore {
210
+ constructor() {
211
+ Object.defineProperty(this, "disposers", {
212
+ enumerable: true,
213
+ configurable: true,
214
+ writable: true,
215
+ value: []
216
+ });
217
+ Object.defineProperty(this, "disposed", {
218
+ enumerable: true,
219
+ configurable: true,
220
+ writable: true,
221
+ value: false
222
+ });
223
+ }
224
+ get isDisposed() {
225
+ return this.disposed;
226
+ }
227
+ add(disposer) {
228
+ const d = (disposer ?? (() => { }));
229
+ if (this.disposed) {
230
+ try {
231
+ d();
232
+ }
233
+ catch {
234
+ // best-effort cleanup
235
+ }
236
+ return () => { };
237
+ }
238
+ this.disposers.push(d);
239
+ return d;
240
+ }
241
+ addEventListener(target, type, handler, options) {
242
+ target.addEventListener(type, handler, options);
243
+ return this.add(() => target.removeEventListener(type, handler, options));
244
+ }
245
+ dispose() {
246
+ if (this.disposed)
247
+ return;
248
+ this.disposed = true;
249
+ for (let i = this.disposers.length - 1; i >= 0; i--) {
250
+ try {
251
+ this.disposers[i]();
252
+ }
253
+ catch {
254
+ // best-effort cleanup
255
+ }
256
+ }
257
+ this.disposers = [];
258
+ }
259
+ }
260
+
261
+ class EventBus {
262
+ constructor() {
263
+ Object.defineProperty(this, "listeners", {
264
+ enumerable: true,
265
+ configurable: true,
266
+ writable: true,
267
+ value: new Map()
268
+ });
269
+ }
270
+ on(event, cb) {
271
+ if (!this.listeners.has(event)) {
272
+ this.listeners.set(event, new Set());
273
+ }
274
+ this.listeners.get(event).add(cb);
275
+ return () => this.listeners.get(event).delete(cb);
276
+ }
277
+ emit(event, payload) {
278
+ this.listeners.get(event)?.forEach((cb) => cb(payload));
279
+ }
280
+ listenerCount(event) {
281
+ return this.listeners.get(event)?.size ?? 0;
282
+ }
283
+ clear() {
284
+ this.listeners.clear();
285
+ }
286
+ }
287
+
288
+ class Lease {
289
+ constructor() {
290
+ Object.defineProperty(this, "owners", {
291
+ enumerable: true,
292
+ configurable: true,
293
+ writable: true,
294
+ value: new Map()
295
+ });
296
+ Object.defineProperty(this, "listeners", {
297
+ enumerable: true,
298
+ configurable: true,
299
+ writable: true,
300
+ value: []
301
+ });
302
+ }
303
+ onChange(cb) {
304
+ this.listeners.push(cb);
305
+ return () => {
306
+ this.listeners = this.listeners.filter((x) => x !== cb);
307
+ };
308
+ }
309
+ notify(capability) {
310
+ const owner = this.owners.get(capability);
311
+ for (const cb of this.listeners) {
312
+ try {
313
+ cb(capability, owner);
314
+ }
315
+ catch {
316
+ // ignore
317
+ }
318
+ }
319
+ }
320
+ acquire(capability, owner) {
321
+ if (this.owners.has(capability))
322
+ return false;
323
+ this.owners.set(capability, owner);
324
+ this.notify(capability);
325
+ return true;
326
+ }
327
+ release(capability, owner) {
328
+ if (this.owners.get(capability) === owner) {
329
+ this.owners.delete(capability);
330
+ this.notify(capability);
331
+ }
332
+ }
333
+ owner(capability) {
334
+ return this.owners.get(capability);
335
+ }
336
+ }
337
+
338
+ class PluginRegistry {
339
+ constructor() {
340
+ Object.defineProperty(this, "plugins", {
341
+ enumerable: true,
342
+ configurable: true,
343
+ writable: true,
344
+ value: []
345
+ });
346
+ }
347
+ register(plugin) {
348
+ this.plugins.push(plugin);
349
+ }
350
+ all() {
351
+ return this.plugins;
352
+ }
353
+ }
354
+
355
+ class StateManager {
356
+ constructor(state) {
357
+ Object.defineProperty(this, "state", {
358
+ enumerable: true,
359
+ configurable: true,
360
+ writable: true,
361
+ value: state
362
+ });
363
+ }
364
+ get current() {
365
+ return this.state;
366
+ }
367
+ transition(next) {
368
+ this.state = next;
369
+ }
370
+ }
371
+
372
+ function formatTime(seconds, frameRate) {
373
+ const f = Math.floor((seconds % 1) * (frameRate || 0));
374
+ let s = Math.floor(seconds);
375
+ let m = Math.floor(s / 60);
376
+ const h = Math.floor(m / 60);
377
+ const wrap = (value) => {
378
+ const formattedVal = value.toString();
379
+ if (value < 10) {
380
+ if (value <= 0) {
381
+ return '00';
382
+ }
383
+ return `0${formattedVal}`;
384
+ }
385
+ return formattedVal;
386
+ };
387
+ m %= 60;
388
+ s %= 60;
389
+ return `${h > 0 ? `${wrap(h)}:` : ''}${wrap(m)}:${wrap(s)}${f ? `:${wrap(f)}` : ''}`;
390
+ }
391
+ const generateISODateTime = (d) => {
392
+ const duration = Number.isFinite(d) ? Math.max(0, d) : 0;
393
+ const totalSeconds = Math.floor(duration);
394
+ const hours = Math.floor(totalSeconds / 3600);
395
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
396
+ const seconds = totalSeconds % 60;
397
+ return `PT${hours ? `${hours}H` : ''}${minutes ? `${minutes}M` : ''}${seconds}S`;
398
+ };
399
+ function offset(el) {
400
+ const rect = el.getBoundingClientRect();
401
+ return {
402
+ left: rect.left + (window.pageXOffset || document.documentElement.scrollLeft),
403
+ top: rect.top + (window.pageYOffset || document.documentElement.scrollTop),
404
+ };
405
+ }
406
+ function isAudio(element) {
407
+ return element instanceof HTMLAudioElement;
408
+ }
409
+ function isMobile() {
410
+ return ((/ipad|iphone|ipod/i.test(window.navigator.userAgent) && !window.MSStream) ||
411
+ /android/i.test(window.navigator.userAgent));
412
+ }
413
+ function predictMimeType(media, url) {
414
+ const fragments = new URL(url).pathname.split('.');
415
+ const extension = fragments.length > 1 ? fragments.pop().toLowerCase() : '';
416
+ // If no extension found, check if media is a vendor iframe
417
+ if (!extension)
418
+ return isAudio(media) ? 'audio/mp3' : 'video/mp4';
419
+ // Check native media types
420
+ switch (extension) {
421
+ case 'm3u8':
422
+ case 'm3u':
423
+ return 'application/x-mpegURL';
424
+ case 'mpd':
425
+ return 'application/dash+xml';
426
+ case 'mp4':
427
+ return isAudio(media) ? 'audio/mp4' : 'video/mp4';
428
+ case 'mp3':
429
+ return 'audio/mp3';
430
+ case 'webm':
431
+ return isAudio(media) ? 'audio/webm' : 'video/webm';
432
+ case 'ogg':
433
+ return isAudio(media) ? 'audio/ogg' : 'video/ogg';
434
+ case 'ogv':
435
+ return 'video/ogg';
436
+ case 'oga':
437
+ return 'audio/ogg';
438
+ case '3gp':
439
+ return 'audio/3gpp';
440
+ case 'wav':
441
+ return 'audio/wav';
442
+ case 'aac':
443
+ return 'audio/aac';
444
+ case 'flac':
445
+ return 'audio/flac';
446
+ default:
447
+ return isAudio(media) ? 'audio/mp3' : 'video/mp4';
448
+ }
449
+ }
450
+
451
+ const clamp01 = (n) => Math.min(1, Math.max(0, n));
452
+ class Core {
453
+ constructor(media, config = {}) {
454
+ Object.defineProperty(this, "media", {
455
+ enumerable: true,
456
+ configurable: true,
457
+ writable: true,
458
+ value: void 0
459
+ });
460
+ Object.defineProperty(this, "isLive", {
461
+ enumerable: true,
462
+ configurable: true,
463
+ writable: true,
464
+ value: false
465
+ });
466
+ Object.defineProperty(this, "events", {
467
+ enumerable: true,
468
+ configurable: true,
469
+ writable: true,
470
+ value: new EventBus()
471
+ });
472
+ Object.defineProperty(this, "leases", {
473
+ enumerable: true,
474
+ configurable: true,
475
+ writable: true,
476
+ value: new Lease()
477
+ });
478
+ Object.defineProperty(this, "state", {
479
+ enumerable: true,
480
+ configurable: true,
481
+ writable: true,
482
+ value: new StateManager('idle')
483
+ });
484
+ Object.defineProperty(this, "config", {
485
+ enumerable: true,
486
+ configurable: true,
487
+ writable: true,
488
+ value: void 0
489
+ });
490
+ Object.defineProperty(this, "userInteracted", {
491
+ enumerable: true,
492
+ configurable: true,
493
+ writable: true,
494
+ value: false
495
+ });
496
+ Object.defineProperty(this, "canAutoplay", {
497
+ enumerable: true,
498
+ configurable: true,
499
+ writable: true,
500
+ value: false
501
+ });
502
+ Object.defineProperty(this, "canAutoplayMuted", {
503
+ enumerable: true,
504
+ configurable: true,
505
+ writable: true,
506
+ value: false
507
+ });
508
+ Object.defineProperty(this, "interactionUnsubs", {
509
+ enumerable: true,
510
+ configurable: true,
511
+ writable: true,
512
+ value: []
513
+ });
514
+ Object.defineProperty(this, "plugins", {
515
+ enumerable: true,
516
+ configurable: true,
517
+ writable: true,
518
+ value: new PluginRegistry()
519
+ });
520
+ Object.defineProperty(this, "pluginDisposables", {
521
+ enumerable: true,
522
+ configurable: true,
523
+ writable: true,
524
+ value: new WeakMap()
525
+ });
526
+ Object.defineProperty(this, "_src", {
527
+ enumerable: true,
528
+ configurable: true,
529
+ writable: true,
530
+ value: void 0
531
+ });
532
+ Object.defineProperty(this, "_volume", {
533
+ enumerable: true,
534
+ configurable: true,
535
+ writable: true,
536
+ value: 1
537
+ });
538
+ Object.defineProperty(this, "_playbackRate", {
539
+ enumerable: true,
540
+ configurable: true,
541
+ writable: true,
542
+ value: 1
543
+ });
544
+ Object.defineProperty(this, "_currentTime", {
545
+ enumerable: true,
546
+ configurable: true,
547
+ writable: true,
548
+ value: 0
549
+ });
550
+ Object.defineProperty(this, "_muted", {
551
+ enumerable: true,
552
+ configurable: true,
553
+ writable: true,
554
+ value: false
555
+ });
556
+ Object.defineProperty(this, "_duration", {
557
+ enumerable: true,
558
+ configurable: true,
559
+ writable: true,
560
+ value: 0
561
+ });
562
+ Object.defineProperty(this, "detectedSources", {
563
+ enumerable: true,
564
+ configurable: true,
565
+ writable: true,
566
+ value: void 0
567
+ });
568
+ Object.defineProperty(this, "activeEngine", {
569
+ enumerable: true,
570
+ configurable: true,
571
+ writable: true,
572
+ value: void 0
573
+ });
574
+ Object.defineProperty(this, "playerContext", {
575
+ enumerable: true,
576
+ configurable: true,
577
+ writable: true,
578
+ value: null
579
+ });
580
+ Object.defineProperty(this, "autoplaySupport", {
581
+ enumerable: true,
582
+ configurable: true,
583
+ writable: true,
584
+ value: void 0
585
+ });
586
+ Object.defineProperty(this, "autoplaySupportPromise", {
587
+ enumerable: true,
588
+ configurable: true,
589
+ writable: true,
590
+ value: void 0
591
+ });
592
+ Object.defineProperty(this, "readyPromise", {
593
+ enumerable: true,
594
+ configurable: true,
595
+ writable: true,
596
+ value: void 0
597
+ });
598
+ Object.defineProperty(this, "readyResolve", {
599
+ enumerable: true,
600
+ configurable: true,
601
+ writable: true,
602
+ value: void 0
603
+ });
604
+ Object.defineProperty(this, "readyReject", {
605
+ enumerable: true,
606
+ configurable: true,
607
+ writable: true,
608
+ value: void 0
609
+ });
610
+ Object.defineProperty(this, "playRequestPromise", {
611
+ enumerable: true,
612
+ configurable: true,
613
+ writable: true,
614
+ value: void 0
615
+ });
616
+ if (typeof media === 'string') {
617
+ const el = document.querySelector(media);
618
+ if (!el || !(el instanceof HTMLMediaElement)) {
619
+ throw new Error(`OpenPlayer: could not find media element for selector: ${media}`);
620
+ }
621
+ this.media = el;
622
+ }
623
+ else {
624
+ this.media = media;
625
+ }
626
+ this.registerPlugin(new DefaultMediaEngine());
627
+ this.config = { ...defaultConfiguration, ...config };
628
+ this.media.currentTime = this.config.startTime || this.media.currentTime;
629
+ this._currentTime = this.config.startTime || this.media.currentTime;
630
+ this._duration = this.config.duration || this.media.duration;
631
+ const initialVolume = clamp01(this.config.startVolume ?? this.media.volume);
632
+ this.media.volume = initialVolume;
633
+ this._volume = initialVolume;
634
+ if (this.config.startVolume !== undefined) {
635
+ this.media.muted = initialVolume <= 0;
636
+ this._muted = initialVolume <= 0;
637
+ }
638
+ else {
639
+ this._muted = this.media.muted;
640
+ }
641
+ this._volume = initialVolume;
642
+ this._muted = initialVolume <= 0;
643
+ this.media.playbackRate = this.config.startPlaybackRate || this.media.playbackRate;
644
+ this._playbackRate = this.config.startPlaybackRate || this.media.playbackRate;
645
+ (this.config.plugins || []).forEach((p) => this.registerPlugin(p));
646
+ this.bindStateTransitions();
647
+ this.bindMediaSync();
648
+ this.bindLeaseSync();
649
+ this.bindFirstInteraction();
650
+ this.maybeAutoLoad();
651
+ }
652
+ on(event, cb) {
653
+ // Keep the surface flexible (plugins may emit custom events).
654
+ return this.events.on(event, cb);
655
+ }
656
+ emit(event, payload) {
657
+ this.events.emit(event, payload);
658
+ this.plugins
659
+ .all()
660
+ .filter((p) => !p.capabilities?.includes('media-engine'))
661
+ .forEach((p) => {
662
+ p.onEvent?.(event, payload);
663
+ });
664
+ }
665
+ registerPlugin(plugin) {
666
+ this.plugins.register(plugin);
667
+ const dispose = new DisposableStore();
668
+ this.pluginDisposables.set(plugin, dispose);
669
+ plugin.setup?.({
670
+ core: this,
671
+ media: this.media,
672
+ events: this.events,
673
+ state: this.state,
674
+ leases: this.leases,
675
+ dispose,
676
+ add: (d) => dispose.add(d ?? undefined),
677
+ on: (event, cb) => dispose.add(this.events.on(event, cb)),
678
+ listen: (target, type, handler, options) => dispose.addEventListener(target, type, handler, options),
679
+ });
680
+ }
681
+ set src(value) {
682
+ this._src = value;
683
+ if (value) {
684
+ this.detectedSources = [{ src: value, type: predictMimeType(this.media, value) }];
685
+ this.emit('source:set', value);
686
+ // Tear down any active engine so that load() can re-initialise cleanly
687
+ // with the new source, even when the player is already in a non-idle state.
688
+ if (this.playerContext) {
689
+ this.activeEngine?.detach?.(this.playerContext);
690
+ this.activeEngine = undefined;
691
+ this.playerContext = null;
692
+ }
693
+ this.state.transition('idle');
694
+ this.readyPromise = undefined;
695
+ this.readyResolve = undefined;
696
+ this.readyReject = undefined;
697
+ this.playRequestPromise = undefined;
698
+ queueMicrotask(() => this.load());
699
+ }
700
+ }
701
+ get src() {
702
+ return this._src;
703
+ }
704
+ get volume() {
705
+ return this._volume;
706
+ }
707
+ set volume(v) {
708
+ const next = clamp01(v);
709
+ this._volume = next;
710
+ this.emit('cmd:setVolume', next);
711
+ }
712
+ get muted() {
713
+ return this._muted;
714
+ }
715
+ set muted(muted) {
716
+ this._muted = muted;
717
+ this.emit('cmd:setMuted', muted);
718
+ }
719
+ set playbackRate(rate) {
720
+ this._playbackRate = rate;
721
+ this.emit('cmd:setRate', rate);
722
+ }
723
+ get playbackRate() {
724
+ return this._playbackRate;
725
+ }
726
+ set currentTime(time) {
727
+ this._currentTime = time;
728
+ this.emit('cmd:seek', time);
729
+ }
730
+ get currentTime() {
731
+ return this._currentTime;
732
+ }
733
+ get duration() {
734
+ return this._duration;
735
+ }
736
+ load() {
737
+ if (this.state.current !== 'idle')
738
+ return;
739
+ this.emit('cmd:startLoad');
740
+ this.createReadyPromise();
741
+ const sources = this.detectedSources ?? this.readMediaSources(this.media);
742
+ this.detectedSources = sources;
743
+ const { engine, source: activeSource } = this.resolveMediaEngine(sources);
744
+ this.playerContext = {
745
+ media: this.media,
746
+ events: this.events,
747
+ config: this.config,
748
+ activeSource,
749
+ core: this,
750
+ };
751
+ this.activeEngine?.detach?.(this.playerContext);
752
+ this.activeEngine = engine;
753
+ this.emit('loadstart');
754
+ this.emit('cmd:load');
755
+ this.activeEngine.attach(this.playerContext);
756
+ this.emit('cmd:setVolume', this._volume);
757
+ this.emit('cmd:setMuted', this._muted);
758
+ this.emit('cmd:setRate', this._playbackRate);
759
+ if (this._currentTime)
760
+ this.emit('cmd:seek', this._currentTime);
761
+ }
762
+ async whenReady() {
763
+ if (this.state.current === 'ready' ||
764
+ this.state.current === 'playing' ||
765
+ this.state.current === 'paused' ||
766
+ this.state.current === 'waiting' ||
767
+ this.state.current === 'seeking' ||
768
+ this.state.current === 'ended') {
769
+ return;
770
+ }
771
+ if (!this.activeEngine)
772
+ this.load();
773
+ this.createReadyPromise();
774
+ return this.readyPromise ?? Promise.resolve();
775
+ }
776
+ async play() {
777
+ if (this.playRequestPromise)
778
+ return this.playRequestPromise;
779
+ if (!this.activeEngine)
780
+ this.load();
781
+ // Emit cmd:play synchronously while the user-gesture task is still active.
782
+ // Browsers (especially Safari) require media.play() to be called in the same
783
+ // microtask/task as the user interaction; any await before this would cause
784
+ // the autoplay policy to reject the play() call on unmuted media.
785
+ this.emit('cmd:play');
786
+ // Await readiness for state-machine consistency (does not re-emit cmd:play).
787
+ this.playRequestPromise = this.whenReady().finally(() => {
788
+ this.playRequestPromise = undefined;
789
+ });
790
+ return this.playRequestPromise;
791
+ }
792
+ async determineAutoplaySupport() {
793
+ if (this.autoplaySupport)
794
+ return this.autoplaySupport;
795
+ if (this.autoplaySupportPromise)
796
+ return this.autoplaySupportPromise;
797
+ // Gate on readiness, but don't fail detection if readiness never arrives.
798
+ await this.whenReady().catch(() => { });
799
+ const media = this.media;
800
+ const defaultVol = media.volume;
801
+ const defaultMuted = media.muted;
802
+ const restore = () => {
803
+ try {
804
+ media.volume = defaultVol;
805
+ }
806
+ catch {
807
+ // ignore
808
+ }
809
+ try {
810
+ media.muted = defaultMuted;
811
+ }
812
+ catch {
813
+ // ignore
814
+ }
815
+ // Keep Player state consistent with the underlying element.
816
+ this._volume = defaultVol;
817
+ this._muted = defaultMuted;
818
+ };
819
+ this.autoplaySupportPromise = (async () => {
820
+ try {
821
+ // Attempt unmuted autoplay first.
822
+ try {
823
+ const playPromise = media.play();
824
+ if (playPromise !== undefined)
825
+ await playPromise;
826
+ try {
827
+ media.pause();
828
+ }
829
+ catch {
830
+ // ignore
831
+ }
832
+ this.canAutoplay = true;
833
+ this.canAutoplayMuted = false;
834
+ return { autoplay: true, muted: false };
835
+ }
836
+ catch {
837
+ // Unmuted autoplay failed; retry muted autoplay.
838
+ try {
839
+ media.volume = 0;
840
+ media.muted = true;
841
+ this._volume = 0;
842
+ this._muted = true;
843
+ }
844
+ catch {
845
+ // ignore
846
+ }
847
+ try {
848
+ const playPromiseMuted = media.play();
849
+ if (playPromiseMuted !== undefined)
850
+ await playPromiseMuted;
851
+ try {
852
+ media.pause();
853
+ }
854
+ catch {
855
+ // ignore
856
+ }
857
+ this.canAutoplay = true;
858
+ this.canAutoplayMuted = true;
859
+ return { autoplay: true, muted: true };
860
+ }
861
+ catch {
862
+ // Autoplay is blocked even when muted.
863
+ this.canAutoplay = false;
864
+ this.canAutoplayMuted = false;
865
+ return { autoplay: false, muted: false };
866
+ }
867
+ }
868
+ }
869
+ finally {
870
+ restore();
871
+ }
872
+ })();
873
+ this.autoplaySupport = await this.autoplaySupportPromise;
874
+ return this.autoplaySupportPromise;
875
+ }
876
+ pause() {
877
+ if (this.state.current === 'idle' || this.state.current === 'loading') {
878
+ return;
879
+ }
880
+ this.emit('cmd:pause');
881
+ }
882
+ destroy() {
883
+ this.events.emit('player:destroy');
884
+ if (this.playerContext)
885
+ this.activeEngine?.detach?.(this.playerContext);
886
+ this.playerContext = null;
887
+ this.plugins.all().forEach((p) => {
888
+ // Always dispose tracked resources first.
889
+ try {
890
+ this.pluginDisposables.get(p)?.dispose();
891
+ }
892
+ catch {
893
+ // ignore
894
+ }
895
+ try {
896
+ p.destroy?.();
897
+ }
898
+ catch {
899
+ // ignore
900
+ }
901
+ });
902
+ this.interactionUnsubs.forEach((u) => u());
903
+ this.interactionUnsubs = [];
904
+ this.events.clear();
905
+ }
906
+ addCaptions(args) {
907
+ const track = document.createElement('track');
908
+ track.kind = args.kind || 'captions';
909
+ track.src = args.src;
910
+ if (args.srclang)
911
+ track.srclang = args.srclang;
912
+ if (args.label)
913
+ track.label = args.label;
914
+ if (args.default)
915
+ track.default = true;
916
+ this.media.appendChild(track);
917
+ this.emit('texttrack:add', track);
918
+ this.emit('texttrack:listchange');
919
+ return track;
920
+ }
921
+ getPlugin(name) {
922
+ return this.plugins.all().find((p) => p?.name === name);
923
+ }
924
+ extend(extension) {
925
+ if (!extension || typeof extension !== 'object')
926
+ return this;
927
+ for (const key of Object.keys(extension)) {
928
+ if (this[key] === undefined) {
929
+ this[key] = extension[key];
930
+ }
931
+ else if (this[key] &&
932
+ typeof this[key] === 'object' &&
933
+ extension[key] &&
934
+ typeof extension[key] === 'object') {
935
+ const target = this[key];
936
+ const source = extension[key];
937
+ for (const k of Object.keys(source)) {
938
+ if (target[k] === undefined)
939
+ target[k] = source[k];
940
+ }
941
+ }
942
+ }
943
+ return this;
944
+ }
945
+ bindFirstInteraction() {
946
+ const doc = typeof document !== 'undefined' ? document : null;
947
+ if (!doc)
948
+ return;
949
+ const mark = () => {
950
+ if (this.userInteracted)
951
+ return;
952
+ this.userInteracted = true;
953
+ this.emit('player:interacted');
954
+ this.interactionUnsubs.forEach((u) => u());
955
+ this.interactionUnsubs = [];
956
+ };
957
+ const opts = { capture: true, passive: true };
958
+ const removeOpts = { capture: true };
959
+ const on = (type) => {
960
+ doc.addEventListener(type, mark, opts);
961
+ this.interactionUnsubs.push(() => doc.removeEventListener(type, mark, removeOpts));
962
+ };
963
+ on('pointerdown');
964
+ on('mousedown');
965
+ on('touchstart');
966
+ on('keydown');
967
+ }
968
+ resolveMediaEngine(sources) {
969
+ if (sources.length === 0)
970
+ throw new Error('Player cannot resolve media with an empty source');
971
+ const engines = this.plugins
972
+ .all()
973
+ .filter((p) => !!(!!p && p.capabilities?.includes('media-engine') && typeof p.canPlay === 'function'));
974
+ // Don't mutate registry order.
975
+ const sortedEngines = [...engines].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0) || a.name.localeCompare(b.name));
976
+ for (const source of sources) {
977
+ for (const engine of sortedEngines) {
978
+ if (engine.canPlay?.(source)) {
979
+ return { engine, source };
980
+ }
981
+ }
982
+ }
983
+ throw new Error('No compatible media engine found');
984
+ }
985
+ bindStateTransitions() {
986
+ // Load lifecycle
987
+ this.events.on('loadstart', () => this.state.transition('loading'));
988
+ // Resolve "ready" when metadata is available (closest HTML5 analogue to previous playback:ready).
989
+ this.events.on('loadedmetadata', () => {
990
+ this.state.transition('ready');
991
+ if (this.readyResolve) {
992
+ this.readyResolve();
993
+ this.readyResolve = undefined;
994
+ this.readyReject = undefined;
995
+ }
996
+ });
997
+ // IMPORTANT: state transitions only on observed playback events (not on commands).
998
+ this.events.on('playing', () => this.state.transition('playing'));
999
+ this.events.on('pause', () => this.state.transition('paused'));
1000
+ this.events.on('waiting', () => this.state.transition('waiting'));
1001
+ this.events.on('seeking', () => this.state.transition('seeking'));
1002
+ this.events.on('seeked', () => this.state.transition('ready'));
1003
+ this.events.on('ended', () => this.state.transition('ended'));
1004
+ this.events.on('error', (e) => {
1005
+ this.state.transition('error');
1006
+ if (this.readyReject) {
1007
+ this.readyReject(e);
1008
+ this.readyResolve = undefined;
1009
+ this.readyReject = undefined;
1010
+ }
1011
+ });
1012
+ }
1013
+ bindMediaSync() {
1014
+ const read = () => {
1015
+ // time + duration
1016
+ try {
1017
+ this._currentTime = this.media.currentTime || 0;
1018
+ }
1019
+ catch {
1020
+ // ignore
1021
+ }
1022
+ try {
1023
+ const d = this.media.duration;
1024
+ this._duration = d;
1025
+ }
1026
+ catch {
1027
+ // ignore
1028
+ }
1029
+ // volume + mute
1030
+ try {
1031
+ this._muted = Boolean(this.media.muted);
1032
+ const v = this.media.volume;
1033
+ if (Number.isFinite(v))
1034
+ this._volume = v;
1035
+ }
1036
+ catch {
1037
+ // ignore
1038
+ }
1039
+ // rate
1040
+ try {
1041
+ const r = this.media.playbackRate;
1042
+ if (Number.isFinite(r))
1043
+ this._playbackRate = r;
1044
+ }
1045
+ catch {
1046
+ // ignore
1047
+ }
1048
+ };
1049
+ this.events.on('loadedmetadata', () => read());
1050
+ this.events.on('durationchange', () => read());
1051
+ this.events.on('timeupdate', () => read());
1052
+ this.events.on('volumechange', () => read());
1053
+ this.events.on('ratechange', () => read());
1054
+ }
1055
+ bindLeaseSync() {
1056
+ this.leases.onChange((cap) => {
1057
+ if (cap !== 'playback')
1058
+ return;
1059
+ queueMicrotask(() => {
1060
+ this.emit('cmd:setVolume', this._volume);
1061
+ this.emit('cmd:setMuted', this._muted);
1062
+ this.emit('cmd:setRate', this._playbackRate);
1063
+ if (this._currentTime)
1064
+ this.emit('cmd:seek', this._currentTime);
1065
+ });
1066
+ });
1067
+ }
1068
+ createReadyPromise() {
1069
+ if (this.readyPromise)
1070
+ return;
1071
+ this.readyPromise = new Promise((resolve, reject) => {
1072
+ this.readyResolve = resolve;
1073
+ this.readyReject = reject;
1074
+ });
1075
+ }
1076
+ readMediaSources(media) {
1077
+ const sources = [];
1078
+ if (media.src) {
1079
+ sources.push({ src: media.src, type: predictMimeType(media, media.src) });
1080
+ }
1081
+ try {
1082
+ media.querySelectorAll('source').forEach((el) => {
1083
+ sources.push({ src: el.src, type: el.type || predictMimeType(media, el.src) });
1084
+ });
1085
+ }
1086
+ catch {
1087
+ // ignore
1088
+ }
1089
+ return sources;
1090
+ }
1091
+ maybeAutoLoad() {
1092
+ if (this.state.current !== 'idle')
1093
+ return;
1094
+ const hasEngines = this.plugins.all().some((p) => p.capabilities?.includes('media-engine'));
1095
+ if (!hasEngines)
1096
+ return;
1097
+ const sources = this.readMediaSources(this.media);
1098
+ if (sources.length === 0)
1099
+ return;
1100
+ this.detectedSources = sources;
1101
+ try {
1102
+ const sources = this.media.querySelectorAll('source');
1103
+ sources.forEach((s) => s.remove());
1104
+ if (this.media.getAttribute('src'))
1105
+ this.media.removeAttribute('src');
1106
+ if (this.media.src)
1107
+ this.media.src = '';
1108
+ this.load();
1109
+ }
1110
+ catch {
1111
+ // best effort; don't block attach
1112
+ }
1113
+ }
1114
+ }
1115
+
1116
+ class OverlayBus {
1117
+ constructor(bus) {
1118
+ Object.defineProperty(this, "bus", {
1119
+ enumerable: true,
1120
+ configurable: true,
1121
+ writable: true,
1122
+ value: bus
1123
+ });
1124
+ }
1125
+ on(event, cb) {
1126
+ return this.bus.on(event, cb);
1127
+ }
1128
+ emit(event, ...data) {
1129
+ this.bus.emit(event, ...data);
1130
+ }
1131
+ clear() {
1132
+ this.bus.clear();
1133
+ }
1134
+ }
1135
+ const OVERLAY_MANAGER_KEY = '__op::overlay::manager';
1136
+ class OverlayManager {
1137
+ constructor() {
1138
+ Object.defineProperty(this, "bus", {
1139
+ enumerable: true,
1140
+ configurable: true,
1141
+ writable: true,
1142
+ value: void 0
1143
+ });
1144
+ Object.defineProperty(this, "active", {
1145
+ enumerable: true,
1146
+ configurable: true,
1147
+ writable: true,
1148
+ value: null
1149
+ });
1150
+ Object.defineProperty(this, "overlays", {
1151
+ enumerable: true,
1152
+ configurable: true,
1153
+ writable: true,
1154
+ value: new Map()
1155
+ });
1156
+ this.bus = new OverlayBus(new EventBus());
1157
+ }
1158
+ dispose() {
1159
+ this.overlays.clear();
1160
+ this.active = null;
1161
+ this.bus.clear();
1162
+ }
1163
+ activate(state) {
1164
+ this.overlays.set(state.id, state);
1165
+ this.recomputeAndEmit();
1166
+ }
1167
+ update(id, patch) {
1168
+ const cur = this.overlays.get(id);
1169
+ if (!cur)
1170
+ return;
1171
+ const next = { ...cur, ...patch, id: cur.id };
1172
+ this.overlays.set(id, next);
1173
+ this.recomputeAndEmit();
1174
+ }
1175
+ deactivate(id) {
1176
+ const existed = this.overlays.delete(id);
1177
+ if (!existed)
1178
+ return;
1179
+ this.recomputeAndEmit();
1180
+ }
1181
+ recomputeAndEmit() {
1182
+ const next = this.pickActive();
1183
+ this.active = next;
1184
+ this.bus.emit('overlay:changed', this.active);
1185
+ }
1186
+ pickActive() {
1187
+ let best = null;
1188
+ for (const s of this.overlays.values()) {
1189
+ if (!best || s.priority > best.priority)
1190
+ best = s;
1191
+ }
1192
+ return best;
1193
+ }
1194
+ }
1195
+ function getOverlayManager(player) {
1196
+ if (player[OVERLAY_MANAGER_KEY])
1197
+ return player[OVERLAY_MANAGER_KEY];
1198
+ const mgr = new OverlayManager();
1199
+ player[OVERLAY_MANAGER_KEY] = mgr;
1200
+ try {
1201
+ if (player?.events?.on && player?.events?.emit) {
1202
+ const off = mgr.bus.on('overlay:changed', (active) => player.events.emit('overlay:changed', active));
1203
+ player.events.on('player:destroy', () => {
1204
+ try {
1205
+ off();
1206
+ }
1207
+ catch {
1208
+ // ignore
1209
+ }
1210
+ try {
1211
+ mgr.dispose();
1212
+ }
1213
+ catch {
1214
+ // ignore
1215
+ }
1216
+ try {
1217
+ delete player[OVERLAY_MANAGER_KEY];
1218
+ }
1219
+ catch {
1220
+ // ignore
1221
+ }
1222
+ });
1223
+ }
1224
+ }
1225
+ catch {
1226
+ // ignore
1227
+ }
1228
+ return mgr;
1229
+ }
1230
+
1231
+ export { BaseMediaEngine, Core, DVR_THRESHOLD, DefaultMediaEngine, DisposableStore, EVENT_OPTIONS, EventBus, Lease, PluginRegistry, StateManager, formatTime, generateISODateTime, getOverlayManager, isAudio, isMobile, offset };
1232
+ //# sourceMappingURL=index.js.map