@codenkay/video-nsgplayer-core 1.0.0 → 3.0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - Fix minor issues
8
+
9
+ ## 2.0.1
10
+
11
+ - Auto-wire `AuthManager.getAccessToken()` as default `streaming.getKeyAccessToken` so AES `enc.key` can load with Bearer when no BFF key proxy is configured.
12
+ - When `keyProxyUrlBuilder` returns 401/403/502/CORS (e.g. Cloudflare-blocked BFF), automatically fall back to browser Bearer `enc.key` fetch.
13
+ - On Bearer `enc.key` fetch network/CORS failure, fall back to signed-query XHR (CDN hosts).
14
+
15
+ ## 2.0.0
16
+
17
+ ### Minor Changes
18
+
19
+ - Add `@codenkay/video-nsgplayer-ui` control chrome package; emit timeupdate/progress and text/audio track Runtime APIs from core for host UI parity.
20
+
21
+ ## 1.1.0
22
+
23
+ ### Minor Changes
24
+
25
+ - Emit `timeupdate` and `progress` from the mounted media element for host UI seek bars.
26
+ - Add text/audio track Runtime API: `getTextTracks`, `getSelectedTextTrack`, `setTextTrack`, `getAudioTracks`, `getSelectedAudioTrack`, `setAudioTrack`, plus `texttrackchange` / `audiotrackchange` events.
27
+ - Add `setFullscreenElement` for host chrome fullscreen targeting. Zoom remains UI-only (CSS transform).
28
+
3
29
  ## 1.0.0
4
30
 
5
31
  ### Major Changes
package/dist/index.cjs CHANGED
@@ -212,7 +212,7 @@ var AuthManager = class {
212
212
 
213
213
  // src/config/defaults.ts
214
214
  var PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2];
215
- var PACKAGE_VERSION = "0.1.8";
215
+ var PACKAGE_VERSION = "2.0.1";
216
216
  var DEFAULT_PLAYER_BEHAVIOR = {
217
217
  autoplay: true,
218
218
  startMuted: true,
@@ -981,9 +981,48 @@ function createAuthParamsLoader(HlsClass, store, options = {}) {
981
981
  const usingKeyProxy = Boolean(
982
982
  isKey && options.keyProxyUrlBuilder && options.videoId
983
983
  );
984
+ const signedKeyUrl = isKey ? rewriteHlsRequestUrl(originalUrl, store, {
985
+ ...options,
986
+ keyProxyUrlBuilder: void 0
987
+ }) : context.url;
984
988
  context.url = rewriteHlsRequestUrl(originalUrl, store, options);
985
989
  if (usingKeyProxy) {
986
- this.inner.load(context, config, callbacks);
990
+ const accessToken = options.getKeyAccessToken?.() ?? null;
991
+ if (!accessToken) {
992
+ this.inner.load(context, config, callbacks);
993
+ return;
994
+ }
995
+ const proxyCallbacks = {
996
+ ...callbacks,
997
+ onError: (error, errContext, networkDetails, stats) => {
998
+ const code = error?.code;
999
+ const retryable = code === 0 || code === 502 || code === 403 || code === 401 || /Cloudflare|HTML|DOCTYPE|blocked/i.test(error?.text ?? "");
1000
+ if (!retryable) {
1001
+ callbacks.onError(error, errContext, networkDetails, stats);
1002
+ return;
1003
+ }
1004
+ this.keyAbort?.abort();
1005
+ this.keyAbort = new AbortController();
1006
+ const bearerContext = { ...context, url: signedKeyUrl };
1007
+ void loadEncryptionKeyWithFetch(
1008
+ bearerContext,
1009
+ callbacks,
1010
+ accessToken,
1011
+ this.keyAbort.signal
1012
+ ).catch((fetchError) => {
1013
+ callbacks.onError(
1014
+ {
1015
+ code: 0,
1016
+ text: fetchError instanceof Error ? fetchError.message : error.text || "enc.key proxy and Bearer fetch failed"
1017
+ },
1018
+ bearerContext,
1019
+ null,
1020
+ stats
1021
+ );
1022
+ });
1023
+ }
1024
+ };
1025
+ this.inner.load(context, config, proxyCallbacks);
987
1026
  return;
988
1027
  }
989
1028
  if (isKey && typeof options.getKeyAccessToken === "function") {
@@ -1001,6 +1040,10 @@ function createAuthParamsLoader(HlsClass, store, options = {}) {
1001
1040
  ).catch((error) => {
1002
1041
  const stats = createLoaderStats();
1003
1042
  const aborted = error instanceof DOMException && error.name === "AbortError";
1043
+ if (!aborted && (error instanceof TypeError || error instanceof Error && /Failed to fetch|NetworkError|CORS/i.test(error.message))) {
1044
+ this.inner.load(context, config, callbacks);
1045
+ return;
1046
+ }
1004
1047
  callbacks.onError(
1005
1048
  {
1006
1049
  code: 0,
@@ -1037,6 +1080,11 @@ var HlsEngine = class {
1037
1080
  this.mediaRecoveryCount = 0;
1038
1081
  this.qualities = [];
1039
1082
  this.qualitySelection = "auto";
1083
+ this.textTracks = [];
1084
+ this.textTrackSelection = "off";
1085
+ this.audioTracks = [];
1086
+ this.audioTrackSelection = 0;
1087
+ this.nativeTextTrackCleanup = null;
1040
1088
  }
1041
1089
  async start() {
1042
1090
  const { videoEl, playbackUrl, startMuted, volume } = this.options;
@@ -1056,6 +1104,7 @@ var HlsEngine = class {
1056
1104
  this.options.callbacks.onSupportMode?.("native");
1057
1105
  videoEl.src = this.options.authParams.applyToUrl(playbackUrl);
1058
1106
  this.bindMediaEvents();
1107
+ this.bindNativeTextTracks();
1059
1108
  this.options.callbacks.onStateChange?.("ready");
1060
1109
  await this.applyStartPositionAndAutoplay();
1061
1110
  return;
@@ -1099,9 +1148,19 @@ var HlsEngine = class {
1099
1148
  label: level.height != null && level.height > 0 ? `${level.height}p` : `Level ${index}`
1100
1149
  }));
1101
1150
  callbacks.onQualitiesChange?.(this.qualities);
1151
+ this.syncHlsSubtitleTracks();
1152
+ this.syncHlsAudioTracks();
1102
1153
  callbacks.onStateChange?.("ready");
1103
1154
  void this.applyStartPositionAndAutoplay();
1104
1155
  });
1156
+ this.hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, () => {
1157
+ if (this.destroyed) return;
1158
+ this.syncHlsSubtitleTracks();
1159
+ });
1160
+ this.hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, () => {
1161
+ if (this.destroyed) return;
1162
+ this.syncHlsAudioTracks();
1163
+ });
1105
1164
  this.hls.on(Hls.Events.ERROR, (_event, data) => {
1106
1165
  this.handleHlsError(Hls, data);
1107
1166
  });
@@ -1227,12 +1286,120 @@ var HlsEngine = class {
1227
1286
  getQualities() {
1228
1287
  return this.qualities;
1229
1288
  }
1289
+ getTextTracks() {
1290
+ return this.textTracks;
1291
+ }
1292
+ getTextTrackSelection() {
1293
+ return this.textTrackSelection;
1294
+ }
1295
+ setTextTrack(selection) {
1296
+ if (this.destroyed) return;
1297
+ this.textTrackSelection = selection;
1298
+ this.options.callbacks.onTextTrackSelectionChange?.(selection);
1299
+ if (this.supportMode === "hls.js" && this.hls) {
1300
+ this.hls.subtitleDisplay = selection !== "off";
1301
+ this.hls.subtitleTrack = selection === "off" ? -1 : selection;
1302
+ return;
1303
+ }
1304
+ if (this.supportMode === "native" && this.options.videoEl) {
1305
+ const tracks = this.options.videoEl.textTracks;
1306
+ for (let i = 0; i < tracks.length; i += 1) {
1307
+ const track = tracks[i];
1308
+ track.mode = selection !== "off" && i === selection ? "showing" : "disabled";
1309
+ }
1310
+ }
1311
+ }
1312
+ getAudioTracks() {
1313
+ return this.audioTracks;
1314
+ }
1315
+ getAudioTrackSelection() {
1316
+ return this.audioTrackSelection;
1317
+ }
1318
+ setAudioTrack(index) {
1319
+ if (this.destroyed) return;
1320
+ if (!Number.isInteger(index) || index < 0 || this.audioTracks.length > 0 && index >= this.audioTracks.length) {
1321
+ return;
1322
+ }
1323
+ this.audioTrackSelection = index;
1324
+ this.options.callbacks.onAudioTrackSelectionChange?.(index);
1325
+ if (this.supportMode === "hls.js" && this.hls) {
1326
+ this.hls.audioTrack = index;
1327
+ return;
1328
+ }
1329
+ if (this.supportMode === "native" && this.options.videoEl) {
1330
+ const audioTracks = this.options.videoEl.audioTracks;
1331
+ if (audioTracks && audioTracks.length > 0) {
1332
+ for (let i = 0; i < audioTracks.length; i += 1) {
1333
+ audioTracks[i].enabled = i === index;
1334
+ }
1335
+ }
1336
+ }
1337
+ }
1230
1338
  getSupportMode() {
1231
1339
  return this.supportMode;
1232
1340
  }
1341
+ syncHlsSubtitleTracks() {
1342
+ if (!this.hls) return;
1343
+ const tracks = (this.hls.subtitleTracks ?? []).map(
1344
+ (track, index) => ({
1345
+ index,
1346
+ id: String(track.id ?? index),
1347
+ label: track.name || track.lang || `Subtitles ${index + 1}`,
1348
+ language: track.lang
1349
+ })
1350
+ );
1351
+ this.textTracks = tracks;
1352
+ this.options.callbacks.onTextTracksChange?.(tracks);
1353
+ }
1354
+ syncHlsAudioTracks() {
1355
+ if (!this.hls) return;
1356
+ const tracks = (this.hls.audioTracks ?? []).map(
1357
+ (track, index) => ({
1358
+ index,
1359
+ id: String(track.id ?? index),
1360
+ label: track.name || track.lang || `Audio ${index + 1}`,
1361
+ language: track.lang
1362
+ })
1363
+ );
1364
+ this.audioTracks = tracks;
1365
+ this.options.callbacks.onAudioTracksChange?.(tracks);
1366
+ if (tracks.length > 0) {
1367
+ this.audioTrackSelection = 0;
1368
+ this.options.callbacks.onAudioTrackSelectionChange?.(0);
1369
+ }
1370
+ }
1371
+ bindNativeTextTracks() {
1372
+ const { videoEl } = this.options;
1373
+ const syncFromElement = () => {
1374
+ if (this.destroyed || this.textTracks.length > 0) return;
1375
+ const list = videoEl.textTracks;
1376
+ const tracks = [];
1377
+ for (let i = 0; i < list.length; i += 1) {
1378
+ const track = list[i];
1379
+ if (track.kind !== "subtitles" && track.kind !== "captions") continue;
1380
+ tracks.push({
1381
+ index: tracks.length,
1382
+ id: track.id || `native-${tracks.length}`,
1383
+ label: track.label || track.language || `Track ${tracks.length + 1}`,
1384
+ language: track.language || void 0
1385
+ });
1386
+ }
1387
+ if (tracks.length === 0) return;
1388
+ this.textTracks = tracks;
1389
+ this.options.callbacks.onTextTracksChange?.(tracks);
1390
+ };
1391
+ const textTracks = videoEl.textTracks;
1392
+ textTracks.addEventListener?.("addtrack", syncFromElement);
1393
+ syncFromElement();
1394
+ this.nativeTextTrackCleanup = () => {
1395
+ textTracks.removeEventListener?.("addtrack", syncFromElement);
1396
+ };
1397
+ }
1233
1398
  destroy() {
1234
1399
  if (this.destroyed) return;
1235
1400
  this.destroyed = true;
1401
+ this.nativeTextTrackCleanup?.();
1402
+ this.nativeTextTrackCleanup = null;
1236
1403
  this.hls?.destroy();
1237
1404
  this.hls = null;
1238
1405
  const { videoEl } = this.options;
@@ -1289,11 +1456,40 @@ function createVideoPlayer(options) {
1289
1456
  let refreshManager = null;
1290
1457
  let fullscreenEl = null;
1291
1458
  let isLiveStream = false;
1459
+ let mediaCleanup = null;
1292
1460
  const setState = (next) => {
1293
1461
  if (destroyed && next !== "destroyed") return;
1294
1462
  state = next;
1295
1463
  events.emit("statechange", { state: next });
1296
1464
  };
1465
+ const bindMediaProgressEvents = (el) => {
1466
+ mediaCleanup?.();
1467
+ const onTimeUpdate = () => {
1468
+ if (destroyed) return;
1469
+ events.emit("timeupdate", {
1470
+ currentTime: el.currentTime,
1471
+ duration: Number.isFinite(el.duration) ? el.duration : 0
1472
+ });
1473
+ };
1474
+ const onProgress = () => {
1475
+ if (destroyed) return;
1476
+ let bufferedEnd = 0;
1477
+ try {
1478
+ if (el.buffered.length > 0) {
1479
+ bufferedEnd = el.buffered.end(el.buffered.length - 1);
1480
+ }
1481
+ } catch {
1482
+ bufferedEnd = 0;
1483
+ }
1484
+ events.emit("progress", { bufferedEnd });
1485
+ };
1486
+ el.addEventListener("timeupdate", onTimeUpdate);
1487
+ el.addEventListener("progress", onProgress);
1488
+ mediaCleanup = () => {
1489
+ el.removeEventListener("timeupdate", onTimeUpdate);
1490
+ el.removeEventListener("progress", onProgress);
1491
+ };
1492
+ };
1297
1493
  const authManager = new AuthManager(
1298
1494
  options.auth,
1299
1495
  options.auth?.refreshBeforeExpiryMs ?? 6e4,
@@ -1344,6 +1540,7 @@ function createVideoPlayer(options) {
1344
1540
  isLiveStream = Boolean(source.live);
1345
1541
  }
1346
1542
  const el = ensureVideoElement();
1543
+ bindMediaProgressEvents(el);
1347
1544
  engine?.destroy();
1348
1545
  engine = new HlsEngine({
1349
1546
  videoEl: el,
@@ -1355,12 +1552,11 @@ function createVideoPlayer(options) {
1355
1552
  volume: config.player.volume,
1356
1553
  videoId: options.videoId,
1357
1554
  keyProxyUrlBuilder: config.streaming.keyProxyUrlBuilder,
1358
- // Optional: browser Bearer for Video API–origin enc.key (CORS-dependent).
1359
- // CDN keys use signed query via the auth loader (no Bearer) — same as
1360
- // segments. Apps can pass getKeyAccessToken or rely on keyProxyUrlBuilder.
1361
- // When unset, defaults to AuthManager token only if explicitly requested
1362
- // via streaming.getKeyAccessToken; otherwise query-signed XHR.
1363
- getKeyAccessToken: config.streaming.getKeyAccessToken,
1555
+ // Enterprise AES key auth:
1556
+ // 1) keyProxyUrlBuilder (same-origin BFF) when configured
1557
+ // 2) else getKeyAccessToken / AuthManager Bearer via browser fetch
1558
+ // 3) else signed query + XHR (CDN hosts)
1559
+ getKeyAccessToken: config.streaming.getKeyAccessToken ?? (() => authManager.getAccessToken()),
1364
1560
  hlsConfig: config.streaming.hls,
1365
1561
  logger,
1366
1562
  callbacks: {
@@ -1402,6 +1598,30 @@ function createVideoPlayer(options) {
1402
1598
  label: typeof selection === "number" ? level?.label : "auto"
1403
1599
  });
1404
1600
  },
1601
+ onTextTracksChange: (tracks) => {
1602
+ events.emit("texttrackchange", {
1603
+ tracks,
1604
+ selection: engine?.getTextTrackSelection() ?? "off"
1605
+ });
1606
+ },
1607
+ onTextTrackSelectionChange: (selection) => {
1608
+ events.emit("texttrackchange", {
1609
+ tracks: engine?.getTextTracks() ?? [],
1610
+ selection
1611
+ });
1612
+ },
1613
+ onAudioTracksChange: (tracks) => {
1614
+ events.emit("audiotrackchange", {
1615
+ tracks,
1616
+ selection: engine?.getAudioTrackSelection() ?? 0
1617
+ });
1618
+ },
1619
+ onAudioTrackSelectionChange: (selection) => {
1620
+ events.emit("audiotrackchange", {
1621
+ tracks: engine?.getAudioTracks() ?? [],
1622
+ selection
1623
+ });
1624
+ },
1405
1625
  onEnded: () => events.emit("ended")
1406
1626
  }
1407
1627
  });
@@ -1560,6 +1780,24 @@ function createVideoPlayer(options) {
1560
1780
  getAvailableQualities() {
1561
1781
  return engine?.getQualities() ?? [];
1562
1782
  },
1783
+ getTextTracks() {
1784
+ return engine?.getTextTracks() ?? [];
1785
+ },
1786
+ getSelectedTextTrack() {
1787
+ return engine?.getTextTrackSelection() ?? "off";
1788
+ },
1789
+ setTextTrack(selection) {
1790
+ engine?.setTextTrack(selection);
1791
+ },
1792
+ getAudioTracks() {
1793
+ return engine?.getAudioTracks() ?? [];
1794
+ },
1795
+ getSelectedAudioTrack() {
1796
+ return engine?.getAudioTrackSelection() ?? 0;
1797
+ },
1798
+ setAudioTrack(index) {
1799
+ engine?.setAudioTrack(index);
1800
+ },
1563
1801
  async enterFullscreen() {
1564
1802
  const target = fullscreenEl ?? videoEl?.parentElement ?? videoEl;
1565
1803
  if (target && "requestFullscreen" in target) {
@@ -1576,6 +1814,9 @@ function createVideoPlayer(options) {
1576
1814
  isFullscreen() {
1577
1815
  return Boolean(document.fullscreenElement);
1578
1816
  },
1817
+ setFullscreenElement(el) {
1818
+ fullscreenEl = el;
1819
+ },
1579
1820
  async reload() {
1580
1821
  refreshManager?.stop();
1581
1822
  engine?.destroy();
@@ -1584,6 +1825,8 @@ function createVideoPlayer(options) {
1584
1825
  destroy() {
1585
1826
  if (destroyed) return;
1586
1827
  destroyed = true;
1828
+ mediaCleanup?.();
1829
+ mediaCleanup = null;
1587
1830
  refreshManager?.stop();
1588
1831
  refreshManager = null;
1589
1832
  engine?.destroy();
@@ -1631,7 +1874,7 @@ function createVideoPlayer(options) {
1631
1874
  engine: "hls",
1632
1875
  hls: { ...config.streaming.hls, ...partial?.streaming?.hls },
1633
1876
  keyProxyUrlBuilder: partial?.streaming?.keyProxyUrlBuilder ?? config.streaming.keyProxyUrlBuilder,
1634
- getKeyAccessToken: partial?.streaming?.getKeyAccessToken ?? config.streaming.getKeyAccessToken
1877
+ getKeyAccessToken: partial?.streaming?.getKeyAccessToken ?? config.streaming.getKeyAccessToken ?? (() => authManager.getAccessToken())
1635
1878
  },
1636
1879
  logging: { ...config.logging, ...partial?.logging },
1637
1880
  debug: { ...config.debug, ...partial?.debug },