@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/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.media) {
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.media) {
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.media) {
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.media) {
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 listRelevantTracks(media) {
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 getShowingIndex(media) {
1077
- const tracks = listRelevantTracks(media);
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 setAllOff(media) {
1085
- for (const x of listRelevantTracks(media))
1084
+ function setNativeAllOff(media) {
1085
+ for (const x of listNativeTracks(media))
1086
1086
  x.track.mode = 'disabled';
1087
1087
  }
1088
- function selectIndex(media, index) {
1089
- for (const x of listRelevantTracks(media)) {
1090
- x.track.mode = x.index === index ? 'showing' : 'disabled';
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 media = getActiveMedia(core);
1133
- const tracks = listRelevantTracks(media);
1134
- this.button.style.display = tracks.length ? '' : 'none';
1135
- const showing = getShowingIndex(media);
1136
- const on = showing !== 'off';
1137
- if (typeof showing === 'number')
1138
- this.lastSelectedIndex = showing;
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 media = getActiveMedia(core);
1146
- const showing = getShowingIndex(media);
1147
- if (showing === 'off') {
1148
- const tracks = listRelevantTracks(media);
1149
- const idx = this.lastSelectedIndex ?? tracks[0]?.index;
1150
- if (typeof idx === 'number')
1151
- selectIndex(media, idx);
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
- setAllOff(media);
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 media = getActiveMedia(core);
1165
- const tracks = listRelevantTracks(media);
1166
- if (!tracks.length)
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 = getShowingIndex(media);
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
- setAllOff(media);
1322
+ setNativeAllOff(core.media);
1179
1323
  refresh();
1180
1324
  },
1181
1325
  },
1182
- ...tracks.map((x) => ({
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
- selectIndex(media, x.index);
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', refresh));
1198
- this.onPlayer('loadedmetadata', refresh);
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', () => { isEnded = false; setPlaying(true); });
1528
- this.onPlayer('playing', () => { isEnded = false; setPlaying(true); });
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', () => { isEnded = true; setPlaying(false); });
1709
+ this.onPlayer('ended', () => {
1710
+ isEnded = true;
1711
+ setPlaying(false);
1712
+ });
1531
1713
  return btn;
1532
1714
  }
1533
1715
  }