@openplayerjs/core 3.0.2 → 3.1.1

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 CHANGED
@@ -1,9 +1,110 @@
1
1
  const DVR_THRESHOLD = 120;
2
2
  const EVENT_OPTIONS = { passive: false };
3
3
 
4
+ /**
5
+ * Wrap a native HTMLMediaElement inside the normalized MediaSurface contract.
6
+ */
7
+ class HtmlMediaSurface {
8
+ constructor(media) {
9
+ Object.defineProperty(this, "media", {
10
+ enumerable: true,
11
+ configurable: true,
12
+ writable: true,
13
+ value: media
14
+ });
15
+ Object.defineProperty(this, "element", {
16
+ enumerable: true,
17
+ configurable: true,
18
+ writable: true,
19
+ value: void 0
20
+ });
21
+ this.element = media;
22
+ }
23
+ get currentSrc() {
24
+ return this.media.currentSrc || this.media.src || '';
25
+ }
26
+ get currentTime() {
27
+ return this.media.currentTime || 0;
28
+ }
29
+ set currentTime(value) {
30
+ this.media.currentTime = value;
31
+ }
32
+ get duration() {
33
+ return this.media.duration;
34
+ }
35
+ set duration(_value) {
36
+ // Native media duration is read-only.
37
+ }
38
+ get volume() {
39
+ return this.media.volume;
40
+ }
41
+ set volume(value) {
42
+ this.media.volume = value;
43
+ }
44
+ get muted() {
45
+ return this.media.muted;
46
+ }
47
+ set muted(value) {
48
+ this.media.muted = value;
49
+ }
50
+ get playbackRate() {
51
+ return this.media.playbackRate;
52
+ }
53
+ set playbackRate(value) {
54
+ this.media.playbackRate = value;
55
+ }
56
+ get paused() {
57
+ return this.media.paused;
58
+ }
59
+ get ended() {
60
+ return this.media.ended;
61
+ }
62
+ load(source) {
63
+ if (source?.src && this.media.src !== source.src) {
64
+ this.media.src = source.src;
65
+ }
66
+ this.media.load();
67
+ }
68
+ play() {
69
+ return this.media.play();
70
+ }
71
+ pause() {
72
+ this.media.pause();
73
+ }
74
+ on(event, handler, options) {
75
+ const wrapped = ((evt) => {
76
+ if (event === 'error') {
77
+ handler((this.media.error ?? evt));
78
+ return;
79
+ }
80
+ handler(undefined);
81
+ });
82
+ this.media.addEventListener(event, wrapped, options);
83
+ return () => this.media.removeEventListener(event, wrapped, options);
84
+ }
85
+ }
86
+ function bridgeSurfaceEvents(surface, events) {
87
+ return [
88
+ surface.on('loadstart', () => events.emit('loadstart'), EVENT_OPTIONS),
89
+ surface.on('loadedmetadata', () => events.emit('loadedmetadata'), EVENT_OPTIONS),
90
+ surface.on('durationchange', () => events.emit('durationchange'), EVENT_OPTIONS),
91
+ surface.on('timeupdate', () => events.emit('timeupdate'), EVENT_OPTIONS),
92
+ surface.on('waiting', () => events.emit('waiting'), EVENT_OPTIONS),
93
+ surface.on('seeking', () => events.emit('seeking'), EVENT_OPTIONS),
94
+ surface.on('seeked', () => events.emit('seeked'), EVENT_OPTIONS),
95
+ surface.on('ended', () => events.emit('ended'), EVENT_OPTIONS),
96
+ surface.on('error', (error) => events.emit('error', error), EVENT_OPTIONS),
97
+ surface.on('play', () => events.emit('play'), EVENT_OPTIONS),
98
+ surface.on('playing', () => events.emit('playing'), EVENT_OPTIONS),
99
+ surface.on('pause', () => events.emit('pause'), EVENT_OPTIONS),
100
+ surface.on('volumechange', () => events.emit('volumechange'), EVENT_OPTIONS),
101
+ surface.on('ratechange', () => events.emit('ratechange'), EVENT_OPTIONS),
102
+ ];
103
+ }
104
+
4
105
  class BaseMediaEngine {
5
106
  constructor() {
6
- Object.defineProperty(this, "media", {
107
+ Object.defineProperty(this, "surface", {
7
108
  enumerable: true,
8
109
  configurable: true,
9
110
  writable: true,
@@ -21,88 +122,94 @@ class BaseMediaEngine {
21
122
  writable: true,
22
123
  value: []
23
124
  });
24
- Object.defineProperty(this, "mediaListeners", {
125
+ Object.defineProperty(this, "surfaceListeners", {
25
126
  enumerable: true,
26
127
  configurable: true,
27
128
  writable: true,
28
129
  value: []
29
130
  });
30
131
  }
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;
132
+ bindSurfaceEvents(surface, events) {
133
+ this.surface = surface;
37
134
  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);
135
+ this.surfaceListeners = bridgeSurfaceEvents(surface, events);
136
+ }
137
+ unbindSurfaceEvents() {
138
+ this.surfaceListeners.forEach((off) => off());
139
+ this.surfaceListeners = [];
140
+ }
141
+ bindMediaEvents(media, events) {
142
+ const shim = {
143
+ get currentTime() { return media.currentTime; },
144
+ set currentTime(v) { media.currentTime = v; },
145
+ get duration() { return media.duration; },
146
+ set duration(_v) { },
147
+ get volume() { return media.volume; },
148
+ set volume(v) { media.volume = v; },
149
+ get muted() { return media.muted; },
150
+ set muted(v) { media.muted = v; },
151
+ get playbackRate() { return media.playbackRate; },
152
+ set playbackRate(v) { media.playbackRate = v; },
153
+ get paused() { return media.paused; },
154
+ get ended() { return media.ended; },
155
+ play: () => media.play(),
156
+ pause: () => media.pause(),
157
+ on: (event, handler) => {
158
+ const wrapped = () => handler();
159
+ media.addEventListener(event, wrapped);
160
+ return () => media.removeEventListener(event, wrapped);
161
+ },
162
+ };
163
+ this.surfaceListeners = bridgeSurfaceEvents(shim, events);
64
164
  }
65
165
  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 = [];
166
+ this.unbindSurfaceEvents();
72
167
  }
73
- addMediaListener(media, type, handler, options) {
74
- media.addEventListener(type, handler, options);
75
- this.mediaListeners.push({ type, handler, options });
168
+ addMediaListener(media, event, handler, options) {
169
+ media.addEventListener(event, handler, options);
170
+ this.surfaceListeners.push(() => media.removeEventListener(event, handler, options));
76
171
  }
77
172
  canHandlePlayback(ctx) {
78
173
  const owner = ctx.core.leases.owner('playback');
79
174
  return !owner || owner === this.name;
80
175
  }
81
- /**
82
- * Commands are explicit and separate from notifications.
83
- */
84
176
  bindCommands(ctx) {
85
- const { media, events } = ctx;
177
+ const { events } = ctx;
86
178
  this.commands.push(events.on('cmd:seek', (t) => {
87
179
  if (!this.canHandlePlayback(ctx))
88
180
  return;
89
181
  try {
90
- media.currentTime = t;
182
+ ctx.surface.currentTime = t;
91
183
  }
92
184
  catch {
93
185
  // ignore
94
186
  }
95
187
  }));
96
188
  this.commands.push(events.on('cmd:setVolume', (v) => {
97
- media.volume = v;
189
+ try {
190
+ ctx.surface.volume = v;
191
+ }
192
+ catch {
193
+ // ignore
194
+ }
98
195
  }));
99
196
  this.commands.push(events.on('cmd:setMuted', (m) => {
100
- media.muted = m;
197
+ try {
198
+ ctx.surface.muted = m;
199
+ }
200
+ catch {
201
+ // ignore
202
+ }
101
203
  }));
102
204
  this.commands.push(events.on('cmd:setRate', (r) => {
103
205
  if (!this.canHandlePlayback(ctx))
104
206
  return;
105
- media.playbackRate = r;
207
+ try {
208
+ ctx.surface.playbackRate = r;
209
+ }
210
+ catch {
211
+ // ignore
212
+ }
106
213
  }));
107
214
  this.bindPlayPauseCommands(ctx);
108
215
  }
@@ -112,25 +219,28 @@ class BaseMediaEngine {
112
219
  this.commands = [];
113
220
  }
114
221
  bindPlayPauseCommands(ctx) {
115
- const { media, events } = ctx;
222
+ const { events } = ctx;
116
223
  this.commands.push(events.on('cmd:play', () => {
117
224
  if (!this.canHandlePlayback(ctx))
118
225
  return;
119
- this.playImpl(media);
226
+ this.playImpl(ctx.surface);
120
227
  }));
121
228
  this.commands.push(events.on('cmd:pause', () => {
122
229
  if (!this.canHandlePlayback(ctx))
123
230
  return;
124
- this.pauseImpl(media);
231
+ this.pauseImpl(ctx.surface);
125
232
  }));
126
233
  }
127
- playImpl(media) {
128
- void media.play().catch(() => {
129
- // ignore
130
- });
234
+ playImpl(surface) {
235
+ const maybePromise = surface.play();
236
+ if (maybePromise && typeof maybePromise.catch === 'function') {
237
+ void maybePromise.catch(() => {
238
+ // ignore
239
+ });
240
+ }
131
241
  }
132
- pauseImpl(media) {
133
- media.pause();
242
+ pauseImpl(surface) {
243
+ surface.pause();
134
244
  }
135
245
  }
136
246
 
@@ -167,28 +277,27 @@ class DefaultMediaEngine extends BaseMediaEngine {
167
277
  return media.canPlayType(source.type || '') !== '';
168
278
  }
169
279
  attach(ctx) {
170
- if (ctx.activeSource?.src && ctx.media.src !== ctx.activeSource.src) {
171
- ctx.media.src = ctx.activeSource.src;
172
- }
280
+ const surface = ctx.resetSurface();
173
281
  try {
174
- ctx.media.load();
282
+ surface.load?.(ctx.activeSource);
175
283
  }
176
284
  catch {
177
285
  // ignore
178
286
  }
179
- this.bindMediaEvents(ctx.media, ctx.events);
287
+ this.bindSurfaceEvents(surface, ctx.events);
180
288
  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
289
  this.commands.push(ctx.events.on('cmd:startLoad', () => {
184
290
  const s = ctx.core.state.current;
185
291
  if (['ready', 'playing', 'paused', 'waiting', 'seeking', 'ended'].includes(s))
186
292
  return;
293
+ // ctx.media.preload is intentionally read/written here: it is a DOM attribute
294
+ // with no surface equivalent. The surface abstraction covers playback operations
295
+ // only; loading hints are DOM-level concerns that must be set directly on the element.
187
296
  if (ctx.media.preload !== 'none')
188
297
  return;
189
298
  ctx.media.preload = 'metadata';
190
299
  try {
191
- ctx.media.load();
300
+ surface.load?.(ctx.activeSource);
192
301
  }
193
302
  catch {
194
303
  // ignore
@@ -197,7 +306,7 @@ class DefaultMediaEngine extends BaseMediaEngine {
197
306
  }
198
307
  detach() {
199
308
  this.unbindCommands();
200
- this.unbindMediaEvents();
309
+ this.unbindSurfaceEvents();
201
310
  }
202
311
  }
203
312
 
@@ -411,7 +520,15 @@ function isMobile() {
411
520
  /android/i.test(window.navigator.userAgent));
412
521
  }
413
522
  function predictMimeType(media, url) {
414
- const fragments = new URL(url).pathname.split('.');
523
+ let pathname;
524
+ try {
525
+ pathname = new URL(url).pathname;
526
+ }
527
+ catch {
528
+ // Non-URL string (e.g. a bare video ID): no extension can be determined.
529
+ return isAudio(media) ? 'audio/mp3' : 'video/mp4';
530
+ }
531
+ const fragments = pathname.split('.');
415
532
  const extension = fragments.length > 1 ? fragments.pop().toLowerCase() : '';
416
533
  // If no extension found, check if media is a vendor iframe
417
534
  if (!extension)
@@ -505,6 +622,18 @@ class Core {
505
622
  writable: true,
506
623
  value: false
507
624
  });
625
+ Object.defineProperty(this, "nativeSurface", {
626
+ enumerable: true,
627
+ configurable: true,
628
+ writable: true,
629
+ value: void 0
630
+ });
631
+ Object.defineProperty(this, "activeSurface", {
632
+ enumerable: true,
633
+ configurable: true,
634
+ writable: true,
635
+ value: void 0
636
+ });
508
637
  Object.defineProperty(this, "interactionUnsubs", {
509
638
  enumerable: true,
510
639
  configurable: true,
@@ -623,30 +752,35 @@ class Core {
623
752
  else {
624
753
  this.media = media;
625
754
  }
755
+ this.nativeSurface = new HtmlMediaSurface(this.media);
756
+ this.activeSurface = this.nativeSurface;
626
757
  this.registerPlugin(new DefaultMediaEngine());
627
758
  this.config = { ...defaultConfiguration, ...config };
628
759
  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;
760
+ this._currentTime = this.config.startTime || this.activeSurface.currentTime;
761
+ this._duration = this.config.duration || this.activeSurface.duration;
762
+ const initialVolume = clamp01(this.config.startVolume ?? this.activeSurface.volume);
763
+ this.activeSurface.volume = initialVolume;
633
764
  this._volume = initialVolume;
634
765
  if (this.config.startVolume !== undefined) {
635
- this.media.muted = initialVolume <= 0;
766
+ this.activeSurface.muted = initialVolume <= 0;
636
767
  this._muted = initialVolume <= 0;
637
768
  }
638
769
  else {
639
- this._muted = this.media.muted;
770
+ this._muted = this.activeSurface.muted;
640
771
  }
641
- this.media.playbackRate = this.config.startPlaybackRate || this.media.playbackRate;
642
- this._playbackRate = this.config.startPlaybackRate || this.media.playbackRate;
772
+ this.activeSurface.playbackRate = this.config.startPlaybackRate || this.activeSurface.playbackRate;
773
+ this._playbackRate = this.config.startPlaybackRate || this.activeSurface.playbackRate;
643
774
  (this.config.plugins || []).forEach((p) => this.registerPlugin(p));
644
775
  this.bindStateTransitions();
645
- this.bindMediaSync();
776
+ this.bindSurfaceSync();
646
777
  this.bindLeaseSync();
647
778
  this.bindFirstInteraction();
648
779
  queueMicrotask(() => this.maybeAutoLoad());
649
780
  }
781
+ get surface() {
782
+ return this.activeSurface;
783
+ }
650
784
  on(event, cb) {
651
785
  // Keep the surface flexible (plugins may emit custom events).
652
786
  return this.events.on(event, cb);
@@ -739,13 +873,7 @@ class Core {
739
873
  const sources = this.detectedSources ?? this.readMediaSources(this.media);
740
874
  this.detectedSources = sources;
741
875
  const { engine, source: activeSource } = this.resolveMediaEngine(sources);
742
- this.playerContext = {
743
- media: this.media,
744
- events: this.events,
745
- config: this.config,
746
- activeSource,
747
- core: this,
748
- };
876
+ this.playerContext = this.createPlayerContext(activeSource);
749
877
  this.activeEngine?.detach?.(this.playerContext);
750
878
  this.activeEngine = engine;
751
879
  this.emit('loadstart');
@@ -1008,35 +1136,32 @@ class Core {
1008
1136
  }
1009
1137
  });
1010
1138
  }
1011
- bindMediaSync() {
1139
+ bindSurfaceSync() {
1012
1140
  const read = () => {
1013
- // time + duration
1014
1141
  try {
1015
- this._currentTime = this.media.currentTime || 0;
1142
+ this._currentTime = this.activeSurface.currentTime || 0;
1016
1143
  }
1017
1144
  catch {
1018
1145
  // ignore
1019
1146
  }
1020
1147
  try {
1021
- const d = this.media.duration;
1148
+ const d = this.config.duration || this.activeSurface.duration;
1022
1149
  this._duration = d;
1023
1150
  }
1024
1151
  catch {
1025
1152
  // ignore
1026
1153
  }
1027
- // volume + mute
1028
1154
  try {
1029
- this._muted = Boolean(this.media.muted);
1030
- const v = this.media.volume;
1155
+ this._muted = Boolean(this.activeSurface.muted);
1156
+ const v = this.activeSurface.volume;
1031
1157
  if (Number.isFinite(v))
1032
1158
  this._volume = v;
1033
1159
  }
1034
1160
  catch {
1035
1161
  // ignore
1036
1162
  }
1037
- // rate
1038
1163
  try {
1039
- const r = this.media.playbackRate;
1164
+ const r = this.activeSurface.playbackRate;
1040
1165
  if (Number.isFinite(r))
1041
1166
  this._playbackRate = r;
1042
1167
  }
@@ -1050,6 +1175,33 @@ class Core {
1050
1175
  this.events.on('volumechange', () => read());
1051
1176
  this.events.on('ratechange', () => read());
1052
1177
  }
1178
+ createPlayerContext(activeSource) {
1179
+ const ctx = {
1180
+ media: this.media,
1181
+ container: (this.media.parentElement ?? this.media),
1182
+ events: this.events,
1183
+ config: this.config,
1184
+ activeSource,
1185
+ core: this,
1186
+ surface: this.activeSurface, // overridden below with a live getter
1187
+ setSurface: (surface) => {
1188
+ this.activeSurface = surface;
1189
+ return this.activeSurface;
1190
+ },
1191
+ resetSurface: () => {
1192
+ this.activeSurface = this.nativeSurface;
1193
+ return this.nativeSurface;
1194
+ },
1195
+ };
1196
+ // Replace the snapshot with a live getter so ctx.surface always reflects the
1197
+ // current active surface even after setSurface() is called (e.g. by an iframe engine).
1198
+ Object.defineProperty(ctx, 'surface', {
1199
+ get: () => this.activeSurface,
1200
+ enumerable: true,
1201
+ configurable: true,
1202
+ });
1203
+ return ctx;
1204
+ }
1053
1205
  bindLeaseSync() {
1054
1206
  this.leases.onChange((cap) => {
1055
1207
  if (cap !== 'playback')
@@ -1237,5 +1389,306 @@ function getOverlayManager(player) {
1237
1389
  return mgr;
1238
1390
  }
1239
1391
 
1240
- export { BaseMediaEngine, Core, DVR_THRESHOLD, DefaultMediaEngine, DisposableStore, EVENT_OPTIONS, EventBus, Lease, PluginRegistry, StateManager, formatTime, generateISODateTime, getOverlayManager, isAudio, isMobile, offset };
1392
+ // ─── IframeMediaSurface ───────────────────────────────────────────────────────
1393
+ /**
1394
+ * A normalized MediaSurface backed by an IframeMediaAdapter.
1395
+ *
1396
+ * Usage inside an engine's attach():
1397
+ * const surface = new IframeMediaSurface(adapter, { pollIntervalMs: 250 });
1398
+ * ctx.setSurface(surface);
1399
+ * this.bindSurfaceEvents(surface, ctx.events); // wires internal bus → EventBus
1400
+ * this.bindCommands(ctx);
1401
+ * surface.mount(ctx.container);
1402
+ */
1403
+ class IframeMediaSurface {
1404
+ constructor(adapter, options = {}) {
1405
+ Object.defineProperty(this, "kind", {
1406
+ enumerable: true,
1407
+ configurable: true,
1408
+ writable: true,
1409
+ value: 'iframe'
1410
+ });
1411
+ Object.defineProperty(this, "element", {
1412
+ enumerable: true,
1413
+ configurable: true,
1414
+ writable: true,
1415
+ value: null
1416
+ });
1417
+ Object.defineProperty(this, "currentSrc", {
1418
+ enumerable: true,
1419
+ configurable: true,
1420
+ writable: true,
1421
+ value: void 0
1422
+ });
1423
+ Object.defineProperty(this, "internalBus", {
1424
+ enumerable: true,
1425
+ configurable: true,
1426
+ writable: true,
1427
+ value: new EventBus()
1428
+ });
1429
+ Object.defineProperty(this, "adapter", {
1430
+ enumerable: true,
1431
+ configurable: true,
1432
+ writable: true,
1433
+ value: void 0
1434
+ });
1435
+ Object.defineProperty(this, "pollIntervalMs", {
1436
+ enumerable: true,
1437
+ configurable: true,
1438
+ writable: true,
1439
+ value: void 0
1440
+ });
1441
+ Object.defineProperty(this, "pollTimer", {
1442
+ enumerable: true,
1443
+ configurable: true,
1444
+ writable: true,
1445
+ value: null
1446
+ });
1447
+ // Normalized state — updated by adapter events and polling
1448
+ Object.defineProperty(this, "_paused", {
1449
+ enumerable: true,
1450
+ configurable: true,
1451
+ writable: true,
1452
+ value: true
1453
+ });
1454
+ Object.defineProperty(this, "_ended", {
1455
+ enumerable: true,
1456
+ configurable: true,
1457
+ writable: true,
1458
+ value: false
1459
+ });
1460
+ Object.defineProperty(this, "_duration", {
1461
+ enumerable: true,
1462
+ configurable: true,
1463
+ writable: true,
1464
+ value: NaN
1465
+ });
1466
+ Object.defineProperty(this, "_currentTime", {
1467
+ enumerable: true,
1468
+ configurable: true,
1469
+ writable: true,
1470
+ value: 0
1471
+ });
1472
+ Object.defineProperty(this, "_volume", {
1473
+ enumerable: true,
1474
+ configurable: true,
1475
+ writable: true,
1476
+ value: 1
1477
+ });
1478
+ Object.defineProperty(this, "_muted", {
1479
+ enumerable: true,
1480
+ configurable: true,
1481
+ writable: true,
1482
+ value: false
1483
+ });
1484
+ Object.defineProperty(this, "_playbackRate", {
1485
+ enumerable: true,
1486
+ configurable: true,
1487
+ writable: true,
1488
+ value: 1
1489
+ });
1490
+ this.adapter = adapter;
1491
+ this.pollIntervalMs = options.pollIntervalMs ?? 250;
1492
+ this.onAdapterReady = this.onAdapterReady.bind(this);
1493
+ this.onAdapterState = this.onAdapterState.bind(this);
1494
+ this.onAdapterError = this.onAdapterError.bind(this);
1495
+ this.adapter.on('ready', this.onAdapterReady);
1496
+ this.adapter.on('state', this.onAdapterState);
1497
+ this.adapter.on('error', this.onAdapterError);
1498
+ // Wire optional push events
1499
+ this.adapter.on('timeupdate', ((t) => this.applyTime(t)));
1500
+ this.adapter.on('durationchange', ((d) => this.applyDuration(d)));
1501
+ this.adapter.on('ratechange', ((r) => this.applyRate(r)));
1502
+ this.adapter.on('volumechange', ((v, m) => this.applyVolume(v, m)));
1503
+ this.adapter.on('ended', (() => this.applyEnded()));
1504
+ }
1505
+ // ─── MediaSurface properties (getter + setter adjacent) ──────────────────
1506
+ get currentTime() {
1507
+ return this._currentTime;
1508
+ }
1509
+ set currentTime(value) {
1510
+ this._currentTime = value;
1511
+ this.internalBus.emit('seeking');
1512
+ void Promise.resolve(this.adapter.seekTo(value)).then(() => {
1513
+ this.internalBus.emit('seeked');
1514
+ });
1515
+ }
1516
+ get duration() {
1517
+ return this._duration;
1518
+ }
1519
+ set duration(_value) {
1520
+ }
1521
+ get volume() {
1522
+ return this._volume;
1523
+ }
1524
+ set volume(value) {
1525
+ const clamped = Math.min(1, Math.max(0, value));
1526
+ this.adapter.setVolume(clamped);
1527
+ this.applyVolume(clamped, this._muted);
1528
+ }
1529
+ get muted() {
1530
+ return this._muted;
1531
+ }
1532
+ set muted(value) {
1533
+ if (value)
1534
+ this.adapter.mute();
1535
+ else
1536
+ this.adapter.unmute();
1537
+ this.applyVolume(this._volume, value);
1538
+ }
1539
+ get playbackRate() {
1540
+ return this._playbackRate;
1541
+ }
1542
+ set playbackRate(value) {
1543
+ this.adapter.setPlaybackRate(value);
1544
+ this.applyRate(value);
1545
+ }
1546
+ get paused() {
1547
+ return this._paused;
1548
+ }
1549
+ get ended() {
1550
+ return this._ended;
1551
+ }
1552
+ // ─── MediaSurface methods ─────────────────────────────────────────────────
1553
+ load(_source) {
1554
+ // No-op: iframe surfaces are initialized with a fixed source at construction time.
1555
+ }
1556
+ play() {
1557
+ return Promise.resolve(this.adapter.play()).then(() => undefined);
1558
+ }
1559
+ pause() {
1560
+ void Promise.resolve(this.adapter.pause());
1561
+ }
1562
+ /**
1563
+ * Subscribe to surface events — satisfies MediaSurface.on() and is used by
1564
+ * bridgeSurfaceEvents / BaseMediaEngine.bindSurfaceEvents to forward events
1565
+ * onto the shared EventBus.
1566
+ */
1567
+ on(event, handler, _options) {
1568
+ return this.internalBus.on(event, handler);
1569
+ }
1570
+ // ─── Extra lifecycle (beyond MediaSurface contract) ───────────────────────
1571
+ /** Mount the adapter iframe into a DOM container. Resolves once the adapter is ready. */
1572
+ async mount(container) {
1573
+ await this.adapter.mount(container);
1574
+ // Polling starts in onAdapterReady() after the player signals it is initialized.
1575
+ }
1576
+ destroy() {
1577
+ this.stopPolling();
1578
+ this.adapter.off('ready', this.onAdapterReady);
1579
+ this.adapter.off('state', this.onAdapterState);
1580
+ this.adapter.off('error', this.onAdapterError);
1581
+ this.adapter.destroy();
1582
+ this.internalBus.clear();
1583
+ }
1584
+ // ─── Adapter → internal bus ───────────────────────────────────────────────
1585
+ onAdapterReady() {
1586
+ this.applyDuration(this.safeNum(this.adapter.getDuration()));
1587
+ this.applyTime(this.safeNum(this.adapter.getCurrentTime()));
1588
+ if (this.adapter.getVolume)
1589
+ this.applyVolume(this.safeNum(this.adapter.getVolume()), this._muted);
1590
+ if (this.adapter.isMuted)
1591
+ this.applyVolume(this._volume, !!this.adapter.isMuted());
1592
+ if (this.adapter.getPlaybackRate)
1593
+ this.applyRate(this.safeNum(this.adapter.getPlaybackRate()));
1594
+ if (this.adapter.getElement) {
1595
+ this.element = this.adapter.getElement();
1596
+ }
1597
+ this.startPolling();
1598
+ this.internalBus.emit('loadedmetadata');
1599
+ }
1600
+ onAdapterState(s) {
1601
+ switch (s) {
1602
+ case 'loading':
1603
+ case 'buffering':
1604
+ this.internalBus.emit('waiting');
1605
+ break;
1606
+ case 'playing':
1607
+ this._paused = false;
1608
+ this._ended = false;
1609
+ this.internalBus.emit('play');
1610
+ this.internalBus.emit('playing');
1611
+ break;
1612
+ case 'paused':
1613
+ this._paused = true;
1614
+ this.internalBus.emit('pause');
1615
+ break;
1616
+ case 'ended':
1617
+ this._paused = true;
1618
+ this._ended = true;
1619
+ this.internalBus.emit('ended');
1620
+ break;
1621
+ }
1622
+ }
1623
+ onAdapterError(err) {
1624
+ this.internalBus.emit('error', err);
1625
+ }
1626
+ applyTime(t) {
1627
+ if (!Number.isFinite(t))
1628
+ return;
1629
+ this._currentTime = t;
1630
+ this.internalBus.emit('timeupdate');
1631
+ }
1632
+ applyDuration(d) {
1633
+ if (!Number.isFinite(d) || d <= 0)
1634
+ return;
1635
+ this._duration = d;
1636
+ this.internalBus.emit('durationchange');
1637
+ }
1638
+ applyRate(r) {
1639
+ if (!Number.isFinite(r) || r <= 0)
1640
+ return;
1641
+ this._playbackRate = r;
1642
+ this.internalBus.emit('ratechange');
1643
+ }
1644
+ applyVolume(v, muted) {
1645
+ if (!Number.isFinite(v))
1646
+ return;
1647
+ this._volume = Math.min(1, Math.max(0, v));
1648
+ this._muted = !!muted;
1649
+ this.internalBus.emit('volumechange');
1650
+ }
1651
+ applyEnded() {
1652
+ this._ended = true;
1653
+ this._paused = true;
1654
+ this.internalBus.emit('ended');
1655
+ }
1656
+ // ─── Polling ──────────────────────────────────────────────────────────────
1657
+ startPolling() {
1658
+ if (this.pollTimer != null)
1659
+ return;
1660
+ const tick = () => {
1661
+ this.applyTime(this.safeNum(this.adapter.getCurrentTime()));
1662
+ this.applyDuration(this.safeNum(this.adapter.getDuration()));
1663
+ if (this.adapter.getVolume)
1664
+ this.applyVolume(this.safeNum(this.adapter.getVolume()), this._muted);
1665
+ if (this.adapter.isMuted)
1666
+ this.applyVolume(this._volume, !!this.adapter.isMuted());
1667
+ if (this.adapter.getPlaybackRate)
1668
+ this.applyRate(this.safeNum(this.adapter.getPlaybackRate()));
1669
+ this.pollTimer = window.setTimeout(tick, this.pollIntervalMs);
1670
+ };
1671
+ this.pollTimer = window.setTimeout(tick, this.pollIntervalMs);
1672
+ }
1673
+ stopPolling() {
1674
+ if (this.pollTimer != null) {
1675
+ window.clearTimeout(this.pollTimer);
1676
+ this.pollTimer = null;
1677
+ }
1678
+ }
1679
+ safeNum(n) {
1680
+ const v = Number(n);
1681
+ return Number.isFinite(v) ? v : NaN;
1682
+ }
1683
+ }
1684
+
1685
+ const CAPTION_KEY = Symbol.for('openplayerjs.caption.provider');
1686
+ function getCaptionTrackProvider(core) {
1687
+ return core[CAPTION_KEY] ?? null;
1688
+ }
1689
+ function setCaptionTrackProvider(core, provider) {
1690
+ core[CAPTION_KEY] = provider;
1691
+ }
1692
+
1693
+ export { BaseMediaEngine, Core, DVR_THRESHOLD, DefaultMediaEngine, DisposableStore, EVENT_OPTIONS, EventBus, HtmlMediaSurface, IframeMediaSurface, Lease, PluginRegistry, StateManager, formatTime, generateISODateTime, getCaptionTrackProvider, getOverlayManager, isAudio, isMobile, offset, setCaptionTrackProvider };
1241
1694
  //# sourceMappingURL=index.js.map