@openplayerjs/core 3.0.0-alpha.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/LICENSE +21 -0
- package/README.md +285 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/index.js +1241 -0
- package/dist/index.js.map +1 -0
- package/dist/types/core/configuration.d.ts +9 -0
- package/dist/types/core/configuration.d.ts.map +1 -0
- package/dist/types/core/constants.d.ts +5 -0
- package/dist/types/core/constants.d.ts.map +1 -0
- package/dist/types/core/dispose.d.ts +10 -0
- package/dist/types/core/dispose.d.ts.map +1 -0
- package/dist/types/core/events.d.ts +42 -0
- package/dist/types/core/events.d.ts.map +1 -0
- package/dist/types/core/index.d.ts +78 -0
- package/dist/types/core/index.d.ts.map +1 -0
- package/dist/types/core/lease.d.ts +10 -0
- package/dist/types/core/lease.d.ts.map +1 -0
- package/dist/types/core/media.d.ts +23 -0
- package/dist/types/core/media.d.ts.map +1 -0
- package/dist/types/core/overlay.d.ts +37 -0
- package/dist/types/core/overlay.d.ts.map +1 -0
- package/dist/types/core/plugin.d.ts +37 -0
- package/dist/types/core/plugin.d.ts.map +1 -0
- package/dist/types/core/state.d.ts +8 -0
- package/dist/types/core/state.d.ts.map +1 -0
- package/dist/types/core/utils.d.ts +10 -0
- package/dist/types/core/utils.d.ts.map +1 -0
- package/dist/types/engines/base.d.ts +35 -0
- package/dist/types/engines/base.d.ts.map +1 -0
- package/dist/types/engines/html5.d.ts +12 -0
- package/dist/types/engines/html5.d.ts.map +1 -0
- package/dist/types/index.d.ts +25 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1241 @@
|
|
|
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.media.playbackRate = this.config.startPlaybackRate || this.media.playbackRate;
|
|
642
|
+
this._playbackRate = this.config.startPlaybackRate || this.media.playbackRate;
|
|
643
|
+
(this.config.plugins || []).forEach((p) => this.registerPlugin(p));
|
|
644
|
+
this.bindStateTransitions();
|
|
645
|
+
this.bindMediaSync();
|
|
646
|
+
this.bindLeaseSync();
|
|
647
|
+
this.bindFirstInteraction();
|
|
648
|
+
queueMicrotask(() => this.maybeAutoLoad());
|
|
649
|
+
}
|
|
650
|
+
on(event, cb) {
|
|
651
|
+
// Keep the surface flexible (plugins may emit custom events).
|
|
652
|
+
return this.events.on(event, cb);
|
|
653
|
+
}
|
|
654
|
+
emit(event, payload) {
|
|
655
|
+
this.events.emit(event, payload);
|
|
656
|
+
this.plugins
|
|
657
|
+
.all()
|
|
658
|
+
.filter((p) => !p.capabilities?.includes('media-engine'))
|
|
659
|
+
.forEach((p) => {
|
|
660
|
+
p.onEvent?.(event, payload);
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
registerPlugin(plugin) {
|
|
664
|
+
this.plugins.register(plugin);
|
|
665
|
+
const dispose = new DisposableStore();
|
|
666
|
+
this.pluginDisposables.set(plugin, dispose);
|
|
667
|
+
plugin.setup?.({
|
|
668
|
+
core: this,
|
|
669
|
+
media: this.media,
|
|
670
|
+
events: this.events,
|
|
671
|
+
state: this.state,
|
|
672
|
+
leases: this.leases,
|
|
673
|
+
dispose,
|
|
674
|
+
add: (d) => dispose.add(d ?? undefined),
|
|
675
|
+
on: (event, cb) => dispose.add(this.events.on(event, cb)),
|
|
676
|
+
listen: (target, type, handler, options) => dispose.addEventListener(target, type, handler, options),
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
set src(value) {
|
|
680
|
+
this._src = value;
|
|
681
|
+
if (value) {
|
|
682
|
+
this.detectedSources = [{ src: value, type: predictMimeType(this.media, value) }];
|
|
683
|
+
this.emit('source:set', value);
|
|
684
|
+
// Tear down any active engine so that load() can re-initialise cleanly
|
|
685
|
+
// with the new source, even when the player is already in a non-idle state.
|
|
686
|
+
if (this.playerContext) {
|
|
687
|
+
this.activeEngine?.detach?.(this.playerContext);
|
|
688
|
+
this.activeEngine = undefined;
|
|
689
|
+
this.playerContext = null;
|
|
690
|
+
}
|
|
691
|
+
this.state.transition('idle');
|
|
692
|
+
this.readyPromise = undefined;
|
|
693
|
+
this.readyResolve = undefined;
|
|
694
|
+
this.readyReject = undefined;
|
|
695
|
+
this.playRequestPromise = undefined;
|
|
696
|
+
queueMicrotask(() => this.load());
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
get src() {
|
|
700
|
+
return this._src;
|
|
701
|
+
}
|
|
702
|
+
get volume() {
|
|
703
|
+
return this._volume;
|
|
704
|
+
}
|
|
705
|
+
set volume(v) {
|
|
706
|
+
const next = clamp01(v);
|
|
707
|
+
this._volume = next;
|
|
708
|
+
this.emit('cmd:setVolume', next);
|
|
709
|
+
}
|
|
710
|
+
get muted() {
|
|
711
|
+
return this._muted;
|
|
712
|
+
}
|
|
713
|
+
set muted(muted) {
|
|
714
|
+
this._muted = muted;
|
|
715
|
+
this.emit('cmd:setMuted', muted);
|
|
716
|
+
}
|
|
717
|
+
set playbackRate(rate) {
|
|
718
|
+
this._playbackRate = rate;
|
|
719
|
+
this.emit('cmd:setRate', rate);
|
|
720
|
+
}
|
|
721
|
+
get playbackRate() {
|
|
722
|
+
return this._playbackRate;
|
|
723
|
+
}
|
|
724
|
+
set currentTime(time) {
|
|
725
|
+
this._currentTime = time;
|
|
726
|
+
this.emit('cmd:seek', time);
|
|
727
|
+
}
|
|
728
|
+
get currentTime() {
|
|
729
|
+
return this._currentTime;
|
|
730
|
+
}
|
|
731
|
+
get duration() {
|
|
732
|
+
return this._duration;
|
|
733
|
+
}
|
|
734
|
+
load() {
|
|
735
|
+
if (this.state.current !== 'idle')
|
|
736
|
+
return;
|
|
737
|
+
this.emit('cmd:startLoad');
|
|
738
|
+
this.createReadyPromise();
|
|
739
|
+
const sources = this.detectedSources ?? this.readMediaSources(this.media);
|
|
740
|
+
this.detectedSources = sources;
|
|
741
|
+
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
|
+
};
|
|
749
|
+
this.activeEngine?.detach?.(this.playerContext);
|
|
750
|
+
this.activeEngine = engine;
|
|
751
|
+
this.emit('loadstart');
|
|
752
|
+
this.emit('cmd:load');
|
|
753
|
+
this.activeEngine.attach(this.playerContext);
|
|
754
|
+
this.emit('cmd:setVolume', this._volume);
|
|
755
|
+
this.emit('cmd:setMuted', this._muted);
|
|
756
|
+
this.emit('cmd:setRate', this._playbackRate);
|
|
757
|
+
if (this._currentTime)
|
|
758
|
+
this.emit('cmd:seek', this._currentTime);
|
|
759
|
+
}
|
|
760
|
+
async whenReady() {
|
|
761
|
+
if (this.state.current === 'ready' ||
|
|
762
|
+
this.state.current === 'playing' ||
|
|
763
|
+
this.state.current === 'paused' ||
|
|
764
|
+
this.state.current === 'waiting' ||
|
|
765
|
+
this.state.current === 'seeking' ||
|
|
766
|
+
this.state.current === 'ended') {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (!this.activeEngine)
|
|
770
|
+
this.load();
|
|
771
|
+
this.createReadyPromise();
|
|
772
|
+
return this.readyPromise ?? Promise.resolve();
|
|
773
|
+
}
|
|
774
|
+
async play() {
|
|
775
|
+
if (this.playRequestPromise)
|
|
776
|
+
return this.playRequestPromise;
|
|
777
|
+
if (!this.activeEngine)
|
|
778
|
+
this.load();
|
|
779
|
+
// Emit cmd:play synchronously while the user-gesture task is still active.
|
|
780
|
+
// Browsers (especially Safari) require media.play() to be called in the same
|
|
781
|
+
// microtask/task as the user interaction; any await before this would cause
|
|
782
|
+
// the autoplay policy to reject the play() call on unmuted media.
|
|
783
|
+
this.emit('cmd:play');
|
|
784
|
+
// Await readiness for state-machine consistency (does not re-emit cmd:play).
|
|
785
|
+
this.playRequestPromise = this.whenReady().finally(() => {
|
|
786
|
+
this.playRequestPromise = undefined;
|
|
787
|
+
});
|
|
788
|
+
return this.playRequestPromise;
|
|
789
|
+
}
|
|
790
|
+
async determineAutoplaySupport() {
|
|
791
|
+
if (this.autoplaySupport)
|
|
792
|
+
return this.autoplaySupport;
|
|
793
|
+
if (this.autoplaySupportPromise)
|
|
794
|
+
return this.autoplaySupportPromise;
|
|
795
|
+
// Gate on readiness, but don't fail detection if readiness never arrives.
|
|
796
|
+
await this.whenReady().catch(() => { });
|
|
797
|
+
const media = this.media;
|
|
798
|
+
const defaultVol = media.volume;
|
|
799
|
+
const defaultMuted = media.muted;
|
|
800
|
+
const restore = () => {
|
|
801
|
+
try {
|
|
802
|
+
media.volume = defaultVol;
|
|
803
|
+
}
|
|
804
|
+
catch {
|
|
805
|
+
// ignore
|
|
806
|
+
}
|
|
807
|
+
try {
|
|
808
|
+
media.muted = defaultMuted;
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
// ignore
|
|
812
|
+
}
|
|
813
|
+
// Keep Player state consistent with the underlying element.
|
|
814
|
+
this._volume = defaultVol;
|
|
815
|
+
this._muted = defaultMuted;
|
|
816
|
+
};
|
|
817
|
+
this.autoplaySupportPromise = (async () => {
|
|
818
|
+
try {
|
|
819
|
+
// Attempt unmuted autoplay first.
|
|
820
|
+
try {
|
|
821
|
+
const playPromise = media.play();
|
|
822
|
+
if (playPromise !== undefined)
|
|
823
|
+
await playPromise;
|
|
824
|
+
try {
|
|
825
|
+
media.pause();
|
|
826
|
+
}
|
|
827
|
+
catch {
|
|
828
|
+
// ignore
|
|
829
|
+
}
|
|
830
|
+
this.canAutoplay = true;
|
|
831
|
+
this.canAutoplayMuted = false;
|
|
832
|
+
return { autoplay: true, muted: false };
|
|
833
|
+
}
|
|
834
|
+
catch {
|
|
835
|
+
// Unmuted autoplay failed; retry muted autoplay.
|
|
836
|
+
try {
|
|
837
|
+
media.volume = 0;
|
|
838
|
+
media.muted = true;
|
|
839
|
+
this._volume = 0;
|
|
840
|
+
this._muted = true;
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
// ignore
|
|
844
|
+
}
|
|
845
|
+
try {
|
|
846
|
+
const playPromiseMuted = media.play();
|
|
847
|
+
if (playPromiseMuted !== undefined)
|
|
848
|
+
await playPromiseMuted;
|
|
849
|
+
try {
|
|
850
|
+
media.pause();
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
// ignore
|
|
854
|
+
}
|
|
855
|
+
this.canAutoplay = true;
|
|
856
|
+
this.canAutoplayMuted = true;
|
|
857
|
+
return { autoplay: true, muted: true };
|
|
858
|
+
}
|
|
859
|
+
catch {
|
|
860
|
+
// Autoplay is blocked even when muted.
|
|
861
|
+
this.canAutoplay = false;
|
|
862
|
+
this.canAutoplayMuted = false;
|
|
863
|
+
return { autoplay: false, muted: false };
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
finally {
|
|
868
|
+
restore();
|
|
869
|
+
}
|
|
870
|
+
})();
|
|
871
|
+
this.autoplaySupport = await this.autoplaySupportPromise;
|
|
872
|
+
return this.autoplaySupportPromise;
|
|
873
|
+
}
|
|
874
|
+
pause() {
|
|
875
|
+
if (this.state.current === 'idle' || this.state.current === 'loading') {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
this.emit('cmd:pause');
|
|
879
|
+
}
|
|
880
|
+
destroy() {
|
|
881
|
+
this.events.emit('player:destroy');
|
|
882
|
+
if (this.playerContext)
|
|
883
|
+
this.activeEngine?.detach?.(this.playerContext);
|
|
884
|
+
this.playerContext = null;
|
|
885
|
+
this.plugins.all().forEach((p) => {
|
|
886
|
+
// Always dispose tracked resources first.
|
|
887
|
+
try {
|
|
888
|
+
this.pluginDisposables.get(p)?.dispose();
|
|
889
|
+
}
|
|
890
|
+
catch {
|
|
891
|
+
// ignore
|
|
892
|
+
}
|
|
893
|
+
try {
|
|
894
|
+
p.destroy?.();
|
|
895
|
+
}
|
|
896
|
+
catch {
|
|
897
|
+
// ignore
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
this.interactionUnsubs.forEach((u) => u());
|
|
901
|
+
this.interactionUnsubs = [];
|
|
902
|
+
this.events.clear();
|
|
903
|
+
}
|
|
904
|
+
addCaptions(args) {
|
|
905
|
+
const track = document.createElement('track');
|
|
906
|
+
track.kind = args.kind || 'captions';
|
|
907
|
+
track.src = args.src;
|
|
908
|
+
if (args.srclang)
|
|
909
|
+
track.srclang = args.srclang;
|
|
910
|
+
if (args.label)
|
|
911
|
+
track.label = args.label;
|
|
912
|
+
if (args.default)
|
|
913
|
+
track.default = true;
|
|
914
|
+
this.media.appendChild(track);
|
|
915
|
+
this.emit('texttrack:add', track);
|
|
916
|
+
this.emit('texttrack:listchange');
|
|
917
|
+
return track;
|
|
918
|
+
}
|
|
919
|
+
getPlugin(name) {
|
|
920
|
+
return this.plugins.all().find((p) => p?.name === name);
|
|
921
|
+
}
|
|
922
|
+
extend(extension) {
|
|
923
|
+
if (!extension || typeof extension !== 'object')
|
|
924
|
+
return this;
|
|
925
|
+
for (const key of Object.keys(extension)) {
|
|
926
|
+
if (this[key] === undefined) {
|
|
927
|
+
this[key] = extension[key];
|
|
928
|
+
}
|
|
929
|
+
else if (this[key] &&
|
|
930
|
+
typeof this[key] === 'object' &&
|
|
931
|
+
extension[key] &&
|
|
932
|
+
typeof extension[key] === 'object') {
|
|
933
|
+
const target = this[key];
|
|
934
|
+
const source = extension[key];
|
|
935
|
+
for (const k of Object.keys(source)) {
|
|
936
|
+
if (target[k] === undefined)
|
|
937
|
+
target[k] = source[k];
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return this;
|
|
942
|
+
}
|
|
943
|
+
bindFirstInteraction() {
|
|
944
|
+
const doc = typeof document !== 'undefined' ? document : null;
|
|
945
|
+
if (!doc)
|
|
946
|
+
return;
|
|
947
|
+
const mark = () => {
|
|
948
|
+
if (this.userInteracted)
|
|
949
|
+
return;
|
|
950
|
+
this.userInteracted = true;
|
|
951
|
+
this.emit('player:interacted');
|
|
952
|
+
this.interactionUnsubs.forEach((u) => u());
|
|
953
|
+
this.interactionUnsubs = [];
|
|
954
|
+
};
|
|
955
|
+
const opts = { capture: true, passive: true };
|
|
956
|
+
const removeOpts = { capture: true };
|
|
957
|
+
const on = (type) => {
|
|
958
|
+
doc.addEventListener(type, mark, opts);
|
|
959
|
+
this.interactionUnsubs.push(() => doc.removeEventListener(type, mark, removeOpts));
|
|
960
|
+
};
|
|
961
|
+
on('pointerdown');
|
|
962
|
+
on('mousedown');
|
|
963
|
+
on('touchstart');
|
|
964
|
+
on('keydown');
|
|
965
|
+
}
|
|
966
|
+
resolveMediaEngine(sources) {
|
|
967
|
+
if (sources.length === 0)
|
|
968
|
+
throw new Error('Player cannot resolve media with an empty source');
|
|
969
|
+
const engines = this.plugins
|
|
970
|
+
.all()
|
|
971
|
+
.filter((p) => !!(!!p && p.capabilities?.includes('media-engine') && typeof p.canPlay === 'function'));
|
|
972
|
+
// Don't mutate registry order.
|
|
973
|
+
const sortedEngines = [...engines].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0) || a.name.localeCompare(b.name));
|
|
974
|
+
for (const source of sources) {
|
|
975
|
+
for (const engine of sortedEngines) {
|
|
976
|
+
if (engine.canPlay?.(source)) {
|
|
977
|
+
return { engine, source };
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
throw new Error('No compatible media engine found');
|
|
982
|
+
}
|
|
983
|
+
bindStateTransitions() {
|
|
984
|
+
// Load lifecycle
|
|
985
|
+
this.events.on('loadstart', () => this.state.transition('loading'));
|
|
986
|
+
// Resolve "ready" when metadata is available (closest HTML5 analogue to previous playback:ready).
|
|
987
|
+
this.events.on('loadedmetadata', () => {
|
|
988
|
+
this.state.transition('ready');
|
|
989
|
+
if (this.readyResolve) {
|
|
990
|
+
this.readyResolve();
|
|
991
|
+
this.readyResolve = undefined;
|
|
992
|
+
this.readyReject = undefined;
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
// IMPORTANT: state transitions only on observed playback events (not on commands).
|
|
996
|
+
this.events.on('playing', () => this.state.transition('playing'));
|
|
997
|
+
this.events.on('pause', () => this.state.transition('paused'));
|
|
998
|
+
this.events.on('waiting', () => this.state.transition('waiting'));
|
|
999
|
+
this.events.on('seeking', () => this.state.transition('seeking'));
|
|
1000
|
+
this.events.on('seeked', () => this.state.transition('ready'));
|
|
1001
|
+
this.events.on('ended', () => this.state.transition('ended'));
|
|
1002
|
+
this.events.on('error', (e) => {
|
|
1003
|
+
this.state.transition('error');
|
|
1004
|
+
if (this.readyReject) {
|
|
1005
|
+
this.readyReject(e);
|
|
1006
|
+
this.readyResolve = undefined;
|
|
1007
|
+
this.readyReject = undefined;
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
bindMediaSync() {
|
|
1012
|
+
const read = () => {
|
|
1013
|
+
// time + duration
|
|
1014
|
+
try {
|
|
1015
|
+
this._currentTime = this.media.currentTime || 0;
|
|
1016
|
+
}
|
|
1017
|
+
catch {
|
|
1018
|
+
// ignore
|
|
1019
|
+
}
|
|
1020
|
+
try {
|
|
1021
|
+
const d = this.media.duration;
|
|
1022
|
+
this._duration = d;
|
|
1023
|
+
}
|
|
1024
|
+
catch {
|
|
1025
|
+
// ignore
|
|
1026
|
+
}
|
|
1027
|
+
// volume + mute
|
|
1028
|
+
try {
|
|
1029
|
+
this._muted = Boolean(this.media.muted);
|
|
1030
|
+
const v = this.media.volume;
|
|
1031
|
+
if (Number.isFinite(v))
|
|
1032
|
+
this._volume = v;
|
|
1033
|
+
}
|
|
1034
|
+
catch {
|
|
1035
|
+
// ignore
|
|
1036
|
+
}
|
|
1037
|
+
// rate
|
|
1038
|
+
try {
|
|
1039
|
+
const r = this.media.playbackRate;
|
|
1040
|
+
if (Number.isFinite(r))
|
|
1041
|
+
this._playbackRate = r;
|
|
1042
|
+
}
|
|
1043
|
+
catch {
|
|
1044
|
+
// ignore
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
this.events.on('loadedmetadata', () => read());
|
|
1048
|
+
this.events.on('durationchange', () => read());
|
|
1049
|
+
this.events.on('timeupdate', () => read());
|
|
1050
|
+
this.events.on('volumechange', () => read());
|
|
1051
|
+
this.events.on('ratechange', () => read());
|
|
1052
|
+
}
|
|
1053
|
+
bindLeaseSync() {
|
|
1054
|
+
this.leases.onChange((cap) => {
|
|
1055
|
+
if (cap !== 'playback')
|
|
1056
|
+
return;
|
|
1057
|
+
queueMicrotask(() => {
|
|
1058
|
+
this.emit('cmd:setVolume', this._volume);
|
|
1059
|
+
this.emit('cmd:setMuted', this._muted);
|
|
1060
|
+
this.emit('cmd:setRate', this._playbackRate);
|
|
1061
|
+
if (this._currentTime)
|
|
1062
|
+
this.emit('cmd:seek', this._currentTime);
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
createReadyPromise() {
|
|
1067
|
+
if (this.readyPromise)
|
|
1068
|
+
return;
|
|
1069
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
1070
|
+
this.readyResolve = resolve;
|
|
1071
|
+
this.readyReject = reject;
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
readMediaSources(media) {
|
|
1075
|
+
const sources = [];
|
|
1076
|
+
if (media.src) {
|
|
1077
|
+
sources.push({ src: media.src, type: predictMimeType(media, media.src) });
|
|
1078
|
+
}
|
|
1079
|
+
try {
|
|
1080
|
+
media.querySelectorAll('source').forEach((el) => {
|
|
1081
|
+
sources.push({ src: el.src, type: el.type || predictMimeType(media, el.src) });
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
catch {
|
|
1085
|
+
// ignore
|
|
1086
|
+
}
|
|
1087
|
+
return sources;
|
|
1088
|
+
}
|
|
1089
|
+
maybeAutoLoad() {
|
|
1090
|
+
if (this.state.current !== 'idle')
|
|
1091
|
+
return;
|
|
1092
|
+
const hasEngines = this.plugins.all().some((p) => p.capabilities?.includes('media-engine'));
|
|
1093
|
+
if (!hasEngines)
|
|
1094
|
+
return;
|
|
1095
|
+
const sources = this.readMediaSources(this.media);
|
|
1096
|
+
if (sources.length === 0)
|
|
1097
|
+
return;
|
|
1098
|
+
this.detectedSources = sources;
|
|
1099
|
+
// Capture autoplay intent before we clear the src attribute.
|
|
1100
|
+
const wantsAutoplay = this.media.autoplay;
|
|
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
|
+
if (wantsAutoplay) {
|
|
1110
|
+
// Programmatic media.load() above resets the element and causes some browsers
|
|
1111
|
+
// (notably Chromium) not to re-trigger native autoplay after the src is restored.
|
|
1112
|
+
// Disable the autoplay attribute to prevent a native-vs-plugin race, then
|
|
1113
|
+
// propagate the play intent through Core's event system so plugins (e.g. AdsPlugin)
|
|
1114
|
+
// can intercept the preroll before content begins.
|
|
1115
|
+
this.media.autoplay = false;
|
|
1116
|
+
queueMicrotask(() => this.emit('cmd:play'));
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
catch {
|
|
1120
|
+
// best effort; don't block attach
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
class OverlayBus {
|
|
1126
|
+
constructor(bus) {
|
|
1127
|
+
Object.defineProperty(this, "bus", {
|
|
1128
|
+
enumerable: true,
|
|
1129
|
+
configurable: true,
|
|
1130
|
+
writable: true,
|
|
1131
|
+
value: bus
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
on(event, cb) {
|
|
1135
|
+
return this.bus.on(event, cb);
|
|
1136
|
+
}
|
|
1137
|
+
emit(event, ...data) {
|
|
1138
|
+
this.bus.emit(event, ...data);
|
|
1139
|
+
}
|
|
1140
|
+
clear() {
|
|
1141
|
+
this.bus.clear();
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
const OVERLAY_MANAGER_KEY = '__op::overlay::manager';
|
|
1145
|
+
class OverlayManager {
|
|
1146
|
+
constructor() {
|
|
1147
|
+
Object.defineProperty(this, "bus", {
|
|
1148
|
+
enumerable: true,
|
|
1149
|
+
configurable: true,
|
|
1150
|
+
writable: true,
|
|
1151
|
+
value: void 0
|
|
1152
|
+
});
|
|
1153
|
+
Object.defineProperty(this, "active", {
|
|
1154
|
+
enumerable: true,
|
|
1155
|
+
configurable: true,
|
|
1156
|
+
writable: true,
|
|
1157
|
+
value: null
|
|
1158
|
+
});
|
|
1159
|
+
Object.defineProperty(this, "overlays", {
|
|
1160
|
+
enumerable: true,
|
|
1161
|
+
configurable: true,
|
|
1162
|
+
writable: true,
|
|
1163
|
+
value: new Map()
|
|
1164
|
+
});
|
|
1165
|
+
this.bus = new OverlayBus(new EventBus());
|
|
1166
|
+
}
|
|
1167
|
+
dispose() {
|
|
1168
|
+
this.overlays.clear();
|
|
1169
|
+
this.active = null;
|
|
1170
|
+
this.bus.clear();
|
|
1171
|
+
}
|
|
1172
|
+
activate(state) {
|
|
1173
|
+
this.overlays.set(state.id, state);
|
|
1174
|
+
this.recomputeAndEmit();
|
|
1175
|
+
}
|
|
1176
|
+
update(id, patch) {
|
|
1177
|
+
const cur = this.overlays.get(id);
|
|
1178
|
+
if (!cur)
|
|
1179
|
+
return;
|
|
1180
|
+
const next = { ...cur, ...patch, id: cur.id };
|
|
1181
|
+
this.overlays.set(id, next);
|
|
1182
|
+
this.recomputeAndEmit();
|
|
1183
|
+
}
|
|
1184
|
+
deactivate(id) {
|
|
1185
|
+
const existed = this.overlays.delete(id);
|
|
1186
|
+
if (!existed)
|
|
1187
|
+
return;
|
|
1188
|
+
this.recomputeAndEmit();
|
|
1189
|
+
}
|
|
1190
|
+
recomputeAndEmit() {
|
|
1191
|
+
const next = this.pickActive();
|
|
1192
|
+
this.active = next;
|
|
1193
|
+
this.bus.emit('overlay:changed', this.active);
|
|
1194
|
+
}
|
|
1195
|
+
pickActive() {
|
|
1196
|
+
let best = null;
|
|
1197
|
+
for (const s of this.overlays.values()) {
|
|
1198
|
+
if (!best || s.priority > best.priority)
|
|
1199
|
+
best = s;
|
|
1200
|
+
}
|
|
1201
|
+
return best;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
function getOverlayManager(player) {
|
|
1205
|
+
if (player[OVERLAY_MANAGER_KEY])
|
|
1206
|
+
return player[OVERLAY_MANAGER_KEY];
|
|
1207
|
+
const mgr = new OverlayManager();
|
|
1208
|
+
player[OVERLAY_MANAGER_KEY] = mgr;
|
|
1209
|
+
try {
|
|
1210
|
+
if (player?.events?.on && player?.events?.emit) {
|
|
1211
|
+
const off = mgr.bus.on('overlay:changed', (active) => player.events.emit('overlay:changed', active));
|
|
1212
|
+
player.events.on('player:destroy', () => {
|
|
1213
|
+
try {
|
|
1214
|
+
off();
|
|
1215
|
+
}
|
|
1216
|
+
catch {
|
|
1217
|
+
// ignore
|
|
1218
|
+
}
|
|
1219
|
+
try {
|
|
1220
|
+
mgr.dispose();
|
|
1221
|
+
}
|
|
1222
|
+
catch {
|
|
1223
|
+
// ignore
|
|
1224
|
+
}
|
|
1225
|
+
try {
|
|
1226
|
+
delete player[OVERLAY_MANAGER_KEY];
|
|
1227
|
+
}
|
|
1228
|
+
catch {
|
|
1229
|
+
// ignore
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
catch {
|
|
1235
|
+
// ignore
|
|
1236
|
+
}
|
|
1237
|
+
return mgr;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
export { BaseMediaEngine, Core, DVR_THRESHOLD, DefaultMediaEngine, DisposableStore, EVENT_OPTIONS, EventBus, Lease, PluginRegistry, StateManager, formatTime, generateISODateTime, getOverlayManager, isAudio, isMobile, offset };
|
|
1241
|
+
//# sourceMappingURL=index.js.map
|