@openplayerjs/player 3.1.0 → 3.1.2
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 +9 -11
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.js +223 -41
- package/dist/index.js.map +1 -1
- package/dist/openplayer.js +1 -1
- package/dist/openplayer.js.map +1 -1
- package/dist/types/controls/captions.d.ts.map +1 -1
- package/dist/types/controls/play.d.ts.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getOverlayManager, EVENT_OPTIONS, isAudio, isMobile, DisposableStore, formatTime, generateISODateTime, offset } from '@openplayerjs/core';
|
|
1
|
+
import { getOverlayManager, EVENT_OPTIONS, isAudio, isMobile, DisposableStore, getCaptionTrackProvider, formatTime, generateISODateTime, offset } from '@openplayerjs/core';
|
|
2
2
|
|
|
3
3
|
const defaultUIConfiguration = {
|
|
4
4
|
step: 0,
|
|
@@ -21,6 +21,7 @@ const defaultLabels = Object.freeze({
|
|
|
21
21
|
play: 'Play',
|
|
22
22
|
progressRail: 'Time Rail',
|
|
23
23
|
progressSlider: 'Time Slider',
|
|
24
|
+
restart: 'Restart',
|
|
24
25
|
settings: 'Player Settings',
|
|
25
26
|
speed: 'Speed',
|
|
26
27
|
speedNormal: 'Normal',
|
|
@@ -253,7 +254,7 @@ function bindCenterOverlay(core, keyTarget, bindings) {
|
|
|
253
254
|
if (upVolume > 0)
|
|
254
255
|
lastNonZeroVolume = upVolume;
|
|
255
256
|
const el = getActiveMedia(core);
|
|
256
|
-
if (el && el !== core.
|
|
257
|
+
if (el && el !== core.surface) {
|
|
257
258
|
try {
|
|
258
259
|
el.volume = upVolume;
|
|
259
260
|
el.muted = !(upVolume > 0);
|
|
@@ -273,7 +274,7 @@ function bindCenterOverlay(core, keyTarget, bindings) {
|
|
|
273
274
|
if (downVolume > 0)
|
|
274
275
|
lastNonZeroVolume = downVolume;
|
|
275
276
|
const el = getActiveMedia(core);
|
|
276
|
-
if (el && el !== core.
|
|
277
|
+
if (el && el !== core.surface) {
|
|
277
278
|
try {
|
|
278
279
|
el.volume = downVolume;
|
|
279
280
|
el.muted = !(downVolume > 0);
|
|
@@ -315,7 +316,7 @@ function bindCenterOverlay(core, keyTarget, bindings) {
|
|
|
315
316
|
lastNonZeroVolume = core.volume;
|
|
316
317
|
core.volume = 0;
|
|
317
318
|
core.muted = true;
|
|
318
|
-
if (el && el !== core.
|
|
319
|
+
if (el && el !== core.surface) {
|
|
319
320
|
try {
|
|
320
321
|
el.volume = 0;
|
|
321
322
|
el.muted = true;
|
|
@@ -329,7 +330,7 @@ function bindCenterOverlay(core, keyTarget, bindings) {
|
|
|
329
330
|
const restore = lastNonZeroVolume > 0 ? lastNonZeroVolume : 1;
|
|
330
331
|
core.volume = restore;
|
|
331
332
|
core.muted = false;
|
|
332
|
-
if (el && el !== core.
|
|
333
|
+
if (el && el !== core.surface) {
|
|
333
334
|
try {
|
|
334
335
|
el.volume = restore;
|
|
335
336
|
el.muted = false;
|
|
@@ -1058,7 +1059,7 @@ function isRelevantKind(kind) {
|
|
|
1058
1059
|
function trackLabel(t, index) {
|
|
1059
1060
|
return (t.label && t.label.trim()) || (t.language && t.language.trim().toUpperCase()) || `Track ${index + 1}`;
|
|
1060
1061
|
}
|
|
1061
|
-
function
|
|
1062
|
+
function listNativeTracks(media) {
|
|
1062
1063
|
const list = media.textTracks ?? null;
|
|
1063
1064
|
if (!list)
|
|
1064
1065
|
return [];
|
|
@@ -1073,21 +1074,27 @@ function listRelevantTracks(media) {
|
|
|
1073
1074
|
}
|
|
1074
1075
|
return out;
|
|
1075
1076
|
}
|
|
1076
|
-
function
|
|
1077
|
-
const
|
|
1078
|
-
for (const x of tracks) {
|
|
1077
|
+
function getNativeShowingIndex(media) {
|
|
1078
|
+
for (const x of listNativeTracks(media)) {
|
|
1079
1079
|
if (x.track.mode === 'showing')
|
|
1080
1080
|
return x.index;
|
|
1081
1081
|
}
|
|
1082
1082
|
return 'off';
|
|
1083
1083
|
}
|
|
1084
|
-
function
|
|
1085
|
-
for (const x of
|
|
1084
|
+
function setNativeAllOff(media) {
|
|
1085
|
+
for (const x of listNativeTracks(media))
|
|
1086
1086
|
x.track.mode = 'disabled';
|
|
1087
1087
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1088
|
+
// For ad video: use 'hidden' instead of 'disabled' so the browser keeps the
|
|
1089
|
+
// VTT data loaded; 'disabled' discards cue data and the re-fetch on re-enable
|
|
1090
|
+
// can silently fail, making captions unrecoverable until the ad restarts.
|
|
1091
|
+
function setNativeAllHidden(media) {
|
|
1092
|
+
for (const x of listNativeTracks(media))
|
|
1093
|
+
x.track.mode = 'hidden';
|
|
1094
|
+
}
|
|
1095
|
+
function selectNativeIndex(media, index, offMode = 'disabled') {
|
|
1096
|
+
for (const x of listNativeTracks(media)) {
|
|
1097
|
+
x.track.mode = x.index === index ? 'showing' : offMode;
|
|
1091
1098
|
}
|
|
1092
1099
|
}
|
|
1093
1100
|
class CaptionsControl extends BaseControl {
|
|
@@ -1117,6 +1124,20 @@ class CaptionsControl extends BaseControl {
|
|
|
1117
1124
|
writable: true,
|
|
1118
1125
|
value: null
|
|
1119
1126
|
});
|
|
1127
|
+
// Separate from lastSelectedIndex (which tracks content-media state) so a
|
|
1128
|
+
// content-video pref can't bleed into ad-video track selection.
|
|
1129
|
+
Object.defineProperty(this, "lastAdTrackIndex", {
|
|
1130
|
+
enumerable: true,
|
|
1131
|
+
configurable: true,
|
|
1132
|
+
writable: true,
|
|
1133
|
+
value: null
|
|
1134
|
+
});
|
|
1135
|
+
Object.defineProperty(this, "lastSelectedProviderId", {
|
|
1136
|
+
enumerable: true,
|
|
1137
|
+
configurable: true,
|
|
1138
|
+
writable: true,
|
|
1139
|
+
value: null
|
|
1140
|
+
});
|
|
1120
1141
|
}
|
|
1121
1142
|
build() {
|
|
1122
1143
|
const core = this.core;
|
|
@@ -1128,30 +1149,87 @@ class CaptionsControl extends BaseControl {
|
|
|
1128
1149
|
this.button.className = 'op-controls__captions';
|
|
1129
1150
|
setA11yLabel(this.button, buttonLabel);
|
|
1130
1151
|
this.button.setAttribute('aria-pressed', 'false');
|
|
1152
|
+
const getProvider = () => getCaptionTrackProvider(core);
|
|
1153
|
+
const getAdVideo = () => {
|
|
1154
|
+
const el = this.activeOverlay?.fullscreenVideoEl;
|
|
1155
|
+
return el instanceof HTMLVideoElement ? el : null;
|
|
1156
|
+
};
|
|
1131
1157
|
const refresh = () => {
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1158
|
+
const adVideo = getAdVideo();
|
|
1159
|
+
if (this.activeOverlay) {
|
|
1160
|
+
// During an ad overlay, show caption button only if the ad video has tracks.
|
|
1161
|
+
if (adVideo) {
|
|
1162
|
+
const adTracks = listNativeTracks(adVideo);
|
|
1163
|
+
this.button.style.display = adTracks.length > 0 ? '' : 'none';
|
|
1164
|
+
if (adTracks.length > 0) {
|
|
1165
|
+
const on = getNativeShowingIndex(adVideo) !== 'off';
|
|
1166
|
+
this.button.classList.toggle('op-controls__captions--on', on);
|
|
1167
|
+
this.button.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
else {
|
|
1171
|
+
this.button.style.display = 'none';
|
|
1172
|
+
}
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
const provider = getProvider();
|
|
1176
|
+
const nativeTracks = listNativeTracks(core.media);
|
|
1177
|
+
const providerTracks = provider?.getTracks() ?? [];
|
|
1178
|
+
const hasTracks = nativeTracks.length > 0 || providerTracks.length > 0;
|
|
1179
|
+
this.button.style.display = hasTracks ? '' : 'none';
|
|
1180
|
+
const on = provider ? provider.getActiveTrack() !== null : getNativeShowingIndex(core.media) !== 'off';
|
|
1139
1181
|
this.button.classList.toggle('op-controls__captions--on', on);
|
|
1140
1182
|
this.button.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
1141
1183
|
};
|
|
1142
1184
|
// Toggle only (on/off)
|
|
1143
1185
|
this.listen(this.button, 'click', (e) => {
|
|
1144
1186
|
const me = e;
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1187
|
+
const adVideo = getAdVideo();
|
|
1188
|
+
if (this.activeOverlay && adVideo) {
|
|
1189
|
+
// Toggle captions on the ad video. Use 'hidden' (not 'disabled') so
|
|
1190
|
+
// the browser keeps VTT data; 'disabled' discards it and re-enabling
|
|
1191
|
+
// silently fails.
|
|
1192
|
+
const showing = getNativeShowingIndex(adVideo);
|
|
1193
|
+
if (showing === 'off') {
|
|
1194
|
+
const tracks = listNativeTracks(adVideo);
|
|
1195
|
+
// Use lastAdTrackIndex (ad-specific) — not lastSelectedIndex which
|
|
1196
|
+
// tracks content-media state and may point to a wrong index.
|
|
1197
|
+
const idx = this.lastAdTrackIndex ?? tracks[0]?.index;
|
|
1198
|
+
if (typeof idx === 'number')
|
|
1199
|
+
selectNativeIndex(adVideo, idx, 'hidden');
|
|
1200
|
+
}
|
|
1201
|
+
else {
|
|
1202
|
+
setNativeAllHidden(adVideo);
|
|
1203
|
+
}
|
|
1152
1204
|
}
|
|
1153
1205
|
else {
|
|
1154
|
-
|
|
1206
|
+
const provider = getProvider();
|
|
1207
|
+
if (provider) {
|
|
1208
|
+
const active = provider.getActiveTrack();
|
|
1209
|
+
if (active !== null) {
|
|
1210
|
+
provider.setTrack(null);
|
|
1211
|
+
}
|
|
1212
|
+
else {
|
|
1213
|
+
const tracks = provider.getTracks();
|
|
1214
|
+
const id = this.lastSelectedProviderId ?? tracks[0]?.id ?? null;
|
|
1215
|
+
provider.setTrack(id);
|
|
1216
|
+
if (id) {
|
|
1217
|
+
this.lastSelectedProviderId = id;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
else {
|
|
1222
|
+
const showing = getNativeShowingIndex(core.media);
|
|
1223
|
+
if (showing === 'off') {
|
|
1224
|
+
const tracks = listNativeTracks(core.media);
|
|
1225
|
+
const idx = this.lastSelectedIndex ?? tracks[0]?.index;
|
|
1226
|
+
if (typeof idx === 'number')
|
|
1227
|
+
selectNativeIndex(core.media, idx);
|
|
1228
|
+
}
|
|
1229
|
+
else {
|
|
1230
|
+
setNativeAllOff(core.media);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1155
1233
|
}
|
|
1156
1234
|
refresh();
|
|
1157
1235
|
me.preventDefault();
|
|
@@ -1161,11 +1239,77 @@ class CaptionsControl extends BaseControl {
|
|
|
1161
1239
|
id: 'captions',
|
|
1162
1240
|
label,
|
|
1163
1241
|
getSubmenu: () => {
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
1242
|
+
const adVideo = getAdVideo();
|
|
1243
|
+
if (this.activeOverlay && adVideo) {
|
|
1244
|
+
// Show ad video's caption tracks in the submenu
|
|
1245
|
+
const adTracks = listNativeTracks(adVideo);
|
|
1246
|
+
if (!adTracks.length)
|
|
1247
|
+
return null;
|
|
1248
|
+
const showing = getNativeShowingIndex(adVideo);
|
|
1249
|
+
return {
|
|
1250
|
+
id: 'captions',
|
|
1251
|
+
label,
|
|
1252
|
+
items: [
|
|
1253
|
+
{
|
|
1254
|
+
id: 'off',
|
|
1255
|
+
label: labels.off,
|
|
1256
|
+
checked: showing === 'off',
|
|
1257
|
+
onSelect: () => {
|
|
1258
|
+
setNativeAllHidden(adVideo);
|
|
1259
|
+
refresh();
|
|
1260
|
+
},
|
|
1261
|
+
},
|
|
1262
|
+
...adTracks.map((x) => ({
|
|
1263
|
+
id: String(x.index),
|
|
1264
|
+
label: trackLabel(x.track, x.index),
|
|
1265
|
+
checked: x.index === showing,
|
|
1266
|
+
onSelect: () => {
|
|
1267
|
+
selectNativeIndex(adVideo, x.index, 'hidden');
|
|
1268
|
+
this.lastAdTrackIndex = x.index;
|
|
1269
|
+
refresh();
|
|
1270
|
+
},
|
|
1271
|
+
})),
|
|
1272
|
+
],
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
if (this.activeOverlay)
|
|
1276
|
+
return null;
|
|
1277
|
+
const captionProvider = getProvider();
|
|
1278
|
+
const nativeTracks = listNativeTracks(core.media);
|
|
1279
|
+
if (captionProvider) {
|
|
1280
|
+
const providerTracks = captionProvider.getTracks();
|
|
1281
|
+
if (!providerTracks.length)
|
|
1282
|
+
return null;
|
|
1283
|
+
const active = captionProvider.getActiveTrack();
|
|
1284
|
+
return {
|
|
1285
|
+
id: 'captions',
|
|
1286
|
+
label,
|
|
1287
|
+
items: [
|
|
1288
|
+
{
|
|
1289
|
+
id: 'off',
|
|
1290
|
+
label: labels.off,
|
|
1291
|
+
checked: active === null,
|
|
1292
|
+
onSelect: () => {
|
|
1293
|
+
captionProvider.setTrack(null);
|
|
1294
|
+
refresh();
|
|
1295
|
+
},
|
|
1296
|
+
},
|
|
1297
|
+
...providerTracks.map((t) => ({
|
|
1298
|
+
id: t.id,
|
|
1299
|
+
label: t.label || t.language || t.id,
|
|
1300
|
+
checked: t.id === active,
|
|
1301
|
+
onSelect: () => {
|
|
1302
|
+
captionProvider.setTrack(t.id);
|
|
1303
|
+
this.lastSelectedProviderId = t.id;
|
|
1304
|
+
refresh();
|
|
1305
|
+
},
|
|
1306
|
+
})),
|
|
1307
|
+
],
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
if (!nativeTracks.length)
|
|
1167
1311
|
return null;
|
|
1168
|
-
const showing =
|
|
1312
|
+
const showing = getNativeShowingIndex(core.media);
|
|
1169
1313
|
return {
|
|
1170
1314
|
id: 'captions',
|
|
1171
1315
|
label,
|
|
@@ -1175,16 +1319,16 @@ class CaptionsControl extends BaseControl {
|
|
|
1175
1319
|
label: labels.off,
|
|
1176
1320
|
checked: showing === 'off',
|
|
1177
1321
|
onSelect: () => {
|
|
1178
|
-
|
|
1322
|
+
setNativeAllOff(core.media);
|
|
1179
1323
|
refresh();
|
|
1180
1324
|
},
|
|
1181
1325
|
},
|
|
1182
|
-
...
|
|
1326
|
+
...nativeTracks.map((x) => ({
|
|
1183
1327
|
id: String(x.index),
|
|
1184
1328
|
label: trackLabel(x.track, x.index),
|
|
1185
1329
|
checked: x.index === showing,
|
|
1186
1330
|
onSelect: () => {
|
|
1187
|
-
|
|
1331
|
+
selectNativeIndex(core.media, x.index);
|
|
1188
1332
|
this.lastSelectedIndex = x.index;
|
|
1189
1333
|
refresh();
|
|
1190
1334
|
},
|
|
@@ -1194,8 +1338,26 @@ class CaptionsControl extends BaseControl {
|
|
|
1194
1338
|
},
|
|
1195
1339
|
};
|
|
1196
1340
|
getSettingsRegistry(core).register(provider);
|
|
1197
|
-
this.dispose.add(this.overlayMgr.bus.on('overlay:changed',
|
|
1198
|
-
|
|
1341
|
+
this.dispose.add(this.overlayMgr.bus.on('overlay:changed', (ov) => {
|
|
1342
|
+
if (ov) {
|
|
1343
|
+
// Defer: mountAdVideo attaches caption tracks after activate() fires overlay:changed.
|
|
1344
|
+
Promise.resolve().then(() => refresh());
|
|
1345
|
+
}
|
|
1346
|
+
else {
|
|
1347
|
+
// Ad ended — reset the per-ad track preference so the next ad starts fresh.
|
|
1348
|
+
this.lastAdTrackIndex = null;
|
|
1349
|
+
refresh();
|
|
1350
|
+
}
|
|
1351
|
+
}));
|
|
1352
|
+
this.onPlayer('loadedmetadata', () => {
|
|
1353
|
+
const captionProvider = getProvider();
|
|
1354
|
+
refresh();
|
|
1355
|
+
// If the engine exposes a subscribe hook, wire it up so it can push
|
|
1356
|
+
// track-list updates (e.g. YouTube captions module loads after onReady).
|
|
1357
|
+
if (captionProvider?.subscribe) {
|
|
1358
|
+
this.dispose.add(captionProvider.subscribe(() => refresh()));
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1199
1361
|
refresh();
|
|
1200
1362
|
return this.button;
|
|
1201
1363
|
}
|
|
@@ -1400,25 +1562,36 @@ class FullscreenControl extends BaseControl {
|
|
|
1400
1562
|
const resize = (width, height) => {
|
|
1401
1563
|
const container = this.resolveFullscreenContainer();
|
|
1402
1564
|
const video = this.resolveFullscreenVideoEl();
|
|
1565
|
+
// For iframe engines the video element is hidden; also resize its parent (.op-media)
|
|
1566
|
+
// so the iframe (position:absolute inside it) fills the fullscreen viewport.
|
|
1567
|
+
const mediaContainer = video?.parentElement ?? null;
|
|
1403
1568
|
if (width) {
|
|
1404
1569
|
container.style.width = '100%';
|
|
1405
1570
|
if (video)
|
|
1406
1571
|
video.style.width = '100%';
|
|
1572
|
+
if (mediaContainer && mediaContainer !== container)
|
|
1573
|
+
mediaContainer.style.width = '100%';
|
|
1407
1574
|
}
|
|
1408
1575
|
else {
|
|
1409
1576
|
container.style.removeProperty('width');
|
|
1410
1577
|
if (video)
|
|
1411
1578
|
video.style.removeProperty('width');
|
|
1579
|
+
if (mediaContainer && mediaContainer !== container)
|
|
1580
|
+
mediaContainer.style.removeProperty('width');
|
|
1412
1581
|
}
|
|
1413
1582
|
if (height) {
|
|
1414
1583
|
container.style.height = '100%';
|
|
1415
1584
|
if (video)
|
|
1416
1585
|
video.style.height = '100%';
|
|
1586
|
+
if (mediaContainer && mediaContainer !== container)
|
|
1587
|
+
mediaContainer.style.height = '100%';
|
|
1417
1588
|
}
|
|
1418
1589
|
else {
|
|
1419
1590
|
container.style.removeProperty('height');
|
|
1420
1591
|
if (video)
|
|
1421
1592
|
video.style.removeProperty('height');
|
|
1593
|
+
if (mediaContainer && mediaContainer !== container)
|
|
1594
|
+
mediaContainer.style.removeProperty('height');
|
|
1422
1595
|
}
|
|
1423
1596
|
};
|
|
1424
1597
|
const sync = () => {
|
|
@@ -1524,10 +1697,19 @@ class PlayControl extends BaseControl {
|
|
|
1524
1697
|
btn.setAttribute('aria-pressed', playing ? 'true' : 'false');
|
|
1525
1698
|
setA11yLabel(btn, isEnded && !playing ? restartLabel : playing ? pauseLabel : playLabel);
|
|
1526
1699
|
};
|
|
1527
|
-
this.onPlayer('play', () => {
|
|
1528
|
-
|
|
1700
|
+
this.onPlayer('play', () => {
|
|
1701
|
+
isEnded = false;
|
|
1702
|
+
setPlaying(true);
|
|
1703
|
+
});
|
|
1704
|
+
this.onPlayer('playing', () => {
|
|
1705
|
+
isEnded = false;
|
|
1706
|
+
setPlaying(true);
|
|
1707
|
+
});
|
|
1529
1708
|
this.onPlayer('pause', () => setPlaying(false));
|
|
1530
|
-
this.onPlayer('ended', () => {
|
|
1709
|
+
this.onPlayer('ended', () => {
|
|
1710
|
+
isEnded = true;
|
|
1711
|
+
setPlaying(false);
|
|
1712
|
+
});
|
|
1531
1713
|
return btn;
|
|
1532
1714
|
}
|
|
1533
1715
|
}
|