@gcorevideo/player 2.29.0 → 2.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +108 -0
  2. package/dist/core.js +81 -22
  3. package/dist/index.css +370 -370
  4. package/dist/index.embed.js +80 -23
  5. package/dist/index.js +459 -87
  6. package/docs/api/player.md +37 -0
  7. package/docs/api/player.player.getplugin.md +59 -0
  8. package/docs/api/player.player.md +14 -0
  9. package/docs/api/player.tokenrefreshoptions.gettoken.md +13 -0
  10. package/docs/api/player.tokenrefreshoptions.ipbound.md +13 -0
  11. package/docs/api/player.tokenrefreshoptions.md +115 -0
  12. package/docs/api/player.tokenrefreshoptions.ontokenrefreshed.md +13 -0
  13. package/docs/api/player.tokenrefreshoptions.refreshleadseconds.md +13 -0
  14. package/docs/api/player.tokenrefreshplugin.md +50 -0
  15. package/docs/api/player.tokenresponse.client_ip.md +13 -0
  16. package/docs/api/player.tokenresponse.expires.md +13 -0
  17. package/docs/api/player.tokenresponse.md +153 -0
  18. package/docs/api/player.tokenresponse.token.md +13 -0
  19. package/docs/api/player.tokenresponse.token_ip.md +13 -0
  20. package/docs/api/player.tokenresponse.url.md +13 -0
  21. package/docs/api/player.tokenresponse.url_ip.md +13 -0
  22. package/lib/Player.d.ts +9 -0
  23. package/lib/Player.d.ts.map +1 -1
  24. package/lib/Player.js +11 -0
  25. package/lib/index.plugins.d.ts +1 -0
  26. package/lib/index.plugins.d.ts.map +1 -1
  27. package/lib/index.plugins.js +1 -0
  28. package/lib/playback/dash-playback/DashPlayback.d.ts.map +1 -1
  29. package/lib/playback/dash-playback/DashPlayback.js +5 -1
  30. package/lib/playback/hls-playback/HlsPlayback.d.ts +1 -1
  31. package/lib/playback/hls-playback/HlsPlayback.d.ts.map +1 -1
  32. package/lib/playback/hls-playback/HlsPlayback.js +23 -20
  33. package/lib/playback/hls-playback/RangesList.d.ts +7 -0
  34. package/lib/playback/hls-playback/RangesList.d.ts.map +1 -0
  35. package/lib/playback/hls-playback/RangesList.js +41 -0
  36. package/lib/plugins/subtitles/ClosedCaptions.d.ts.map +1 -1
  37. package/lib/plugins/subtitles/ClosedCaptions.js +0 -2
  38. package/lib/plugins/token-refresh/TokenRefreshPlugin.d.ts +119 -0
  39. package/lib/plugins/token-refresh/TokenRefreshPlugin.d.ts.map +1 -0
  40. package/lib/plugins/token-refresh/TokenRefreshPlugin.js +318 -0
  41. package/lib/plugins/token-refresh/index.d.ts +2 -0
  42. package/lib/plugins/token-refresh/index.d.ts.map +1 -0
  43. package/lib/plugins/token-refresh/index.js +1 -0
  44. package/package.json +1 -1
  45. package/src/Player.ts +12 -0
  46. package/src/index.plugins.ts +1 -0
  47. package/src/playback/dash-playback/DashPlayback.ts +6 -1
  48. package/src/playback/hls-playback/HlsPlayback.ts +40 -37
  49. package/src/playback/hls-playback/RangesList.ts +45 -0
  50. package/src/playback/hls-playback/__tests__/RangesList.test.ts +60 -0
  51. package/src/plugins/subtitles/ClosedCaptions.ts +0 -5
  52. package/src/plugins/token-refresh/TokenRefreshPlugin.ts +425 -0
  53. package/src/plugins/token-refresh/index.ts +5 -0
  54. package/tsconfig.tsbuildinfo +1 -1
package/dist/index.js CHANGED
@@ -12922,7 +12922,7 @@ var PlaybackEvents;
12922
12922
  // https://github.com/clappr/clappr/blob/8752995ea439321ac7ca3cd35e8c64de7a3c3d17/LICENSE
12923
12923
  const AUTO$1 = -1;
12924
12924
  const { now: now$2 } = Utils;
12925
- const T$e = 'playback.dash';
12925
+ const T$f = 'playback.dash';
12926
12926
  class DashPlayback extends BasePlayback {
12927
12927
  _levels = [];
12928
12928
  _currentLevel = AUTO$1;
@@ -13055,6 +13055,7 @@ class DashPlayback extends BasePlayback {
13055
13055
  this._dash = dash;
13056
13056
  this._dash.initialize();
13057
13057
  if (this.options.dash) {
13058
+ const { requestInterceptor, ...dashSettings } = this.options.dash;
13058
13059
  const settings = $.extend(true, {
13059
13060
  streaming: {
13060
13061
  text: {
@@ -13066,8 +13067,11 @@ class DashPlayback extends BasePlayback {
13066
13067
  // dispatchForManualRendering: true, // TODO only when useNativeSubtitles is not true?
13067
13068
  },
13068
13069
  },
13069
- }, this.options.dash);
13070
+ }, dashSettings);
13070
13071
  this._dash.updateSettings(settings);
13072
+ if (typeof requestInterceptor === 'function') {
13073
+ this._dash.addRequestInterceptor(requestInterceptor);
13074
+ }
13071
13075
  }
13072
13076
  this._dash.attachView(this.el);
13073
13077
  this._dash.setAutoPlay(false);
@@ -13195,7 +13199,7 @@ class DashPlayback extends BasePlayback {
13195
13199
  this.trigger(Events$1.PLAYBACK_SETTINGSUPDATE);
13196
13200
  }
13197
13201
  _onPlaybackError = (event) => {
13198
- trace(`${T$e} _onPlaybackError`, { type: event.type, code: event.error.code, message: event.error.message });
13202
+ trace(`${T$f} _onPlaybackError`, { type: event.type, code: event.error.code, message: event.error.message });
13199
13203
  };
13200
13204
  _onDASHJSSError = (event) => {
13201
13205
  this._stopTimeUpdateTimer();
@@ -50088,6 +50092,48 @@ Hls.defaultConfig = void 0;
50088
50092
  // export const CLAPPR_VERSION: string = process.env.CLAPPR_VERSION || '0.11.3';
50089
50093
  const CLAPPR_VERSION$1 = '0.13.0';
50090
50094
 
50095
+ class RangesList {
50096
+ // TODO write an efficient implementation
50097
+ items = [];
50098
+ insert(start, end, value) {
50099
+ const index = this.findIndex((start + end) / 2);
50100
+ this.items.splice(index, 0, [start, end, value]);
50101
+ }
50102
+ find(position) {
50103
+ const index = this.findIndex(position);
50104
+ const item = this.items[index];
50105
+ if (!item || item[0] > position || item[1] < position) {
50106
+ return null;
50107
+ }
50108
+ return item[2];
50109
+ }
50110
+ findIndex(position) {
50111
+ let low = 0;
50112
+ let high = this.items.length;
50113
+ let index = 0;
50114
+ while (low < high) {
50115
+ index = low + Math.floor((high - low) / 2);
50116
+ const item = this.items[index];
50117
+ if (item[0] > position) {
50118
+ if (index === low) {
50119
+ return index;
50120
+ }
50121
+ high = index;
50122
+ continue;
50123
+ }
50124
+ if (item[1] <= position) {
50125
+ if (index === high - 1) {
50126
+ return index + 1;
50127
+ }
50128
+ low = index + 1;
50129
+ continue;
50130
+ }
50131
+ break;
50132
+ }
50133
+ return index;
50134
+ }
50135
+ }
50136
+
50091
50137
  // Copyright 2014 Globo.com Player authors. All rights reserved.
50092
50138
  // Use of this source code is governed by a BSD-style
50093
50139
  // license that can be found on https://github.com/clappr/hlsjs-playback/blob/main/LICENSE
@@ -50095,9 +50141,8 @@ const { now } = Utils;
50095
50141
  const AUTO = -1;
50096
50142
  const DEFAULT_RECOVER_ATTEMPTS = 16;
50097
50143
  Events$1.register('PLAYBACK_FRAGMENT_PARSING_METADATA');
50098
- const T$d = 'playback.hls';
50144
+ const T$e = 'playback.hls';
50099
50145
  class HlsPlayback extends BasePlayback {
50100
- _ccTracksUpdated = false;
50101
50146
  _currentFragment = null;
50102
50147
  _currentLevel = null;
50103
50148
  _durationExcludesAfterLiveSyncPoint = false;
@@ -50122,7 +50167,8 @@ class HlsPlayback extends BasePlayback {
50122
50167
  _timeUpdateTimer = null;
50123
50168
  oncueenter = null;
50124
50169
  oncueexit = null;
50125
- cues = []; // TODO check the list size and use BST if needed
50170
+ cues = null;
50171
+ cuesByTrack = {};
50126
50172
  currentCueId = null;
50127
50173
  /**
50128
50174
  * @internal
@@ -50319,7 +50365,6 @@ class HlsPlayback extends BasePlayback {
50319
50365
  }
50320
50366
  this._manifestParsed = false;
50321
50367
  // this._ccIsSetup = false
50322
- this._ccTracksUpdated = false;
50323
50368
  this._setInitialState();
50324
50369
  this._hls.destroy();
50325
50370
  this._hls = null;
@@ -50373,12 +50418,20 @@ class HlsPlayback extends BasePlayback {
50373
50418
  this._hls.on(Events.AUDIO_TRACK_SWITCHED, (evt, data) => this._onAudioTrackSwitched(evt, data));
50374
50419
  this._hls.on(Events.CUES_PARSED, (evt, data) => {
50375
50420
  data.cues?.forEach((cue) => {
50376
- this.cues.push({
50421
+ if (!this.cues) {
50422
+ const trackId = this._hls.subtitleTrack;
50423
+ if (!this.cuesByTrack[trackId]) {
50424
+ this.cuesByTrack[trackId] = new RangesList();
50425
+ }
50426
+ this.cues = this.cuesByTrack[trackId];
50427
+ }
50428
+ const cueInfo = {
50377
50429
  id: cue.id,
50378
50430
  start: cue.startTime,
50379
50431
  end: cue.endTime,
50380
50432
  text: cue.text,
50381
- });
50433
+ };
50434
+ this.cues.insert(cue.startTime, cue.endTime, cueInfo);
50382
50435
  });
50383
50436
  });
50384
50437
  this.bindCustomListeners();
@@ -50420,7 +50473,7 @@ class HlsPlayback extends BasePlayback {
50420
50473
  }
50421
50474
  else {
50422
50475
  Log.error('hlsjs: failed to recover', { evt, data });
50423
- trace(`${T$d} _recover failed to recover`, {
50476
+ trace(`${T$e} _recover failed to recover`, {
50424
50477
  type: data.type,
50425
50478
  details: data.details,
50426
50479
  });
@@ -50507,7 +50560,7 @@ class HlsPlayback extends BasePlayback {
50507
50560
  this.trigger(Events$1.PLAYBACK_SETTINGSUPDATE);
50508
50561
  }
50509
50562
  _onHLSJSError(evt, data) {
50510
- trace(`${T$d} _onHLSJSError`, {
50563
+ trace(`${T$e} _onHLSJSError`, {
50511
50564
  fatal: data.fatal,
50512
50565
  type: data.type,
50513
50566
  details: data.details,
@@ -50555,7 +50608,7 @@ class HlsPlayback extends BasePlayback {
50555
50608
  evt,
50556
50609
  data,
50557
50610
  });
50558
- trace(`${T$d} _onHLSJSError trying to recover from network error`, {
50611
+ trace(`${T$e} _onHLSJSError trying to recover from network error`, {
50559
50612
  details: data.details,
50560
50613
  });
50561
50614
  error.level = PlayerError.Levels.WARN;
@@ -50568,7 +50621,7 @@ class HlsPlayback extends BasePlayback {
50568
50621
  evt,
50569
50622
  data,
50570
50623
  });
50571
- trace(`${T$d} _onHLSJSError trying to recover from media error`, {
50624
+ trace(`${T$e} _onHLSJSError trying to recover from media error`, {
50572
50625
  details: data.details,
50573
50626
  });
50574
50627
  error.level = PlayerError.Levels.WARN;
@@ -50598,14 +50651,14 @@ class HlsPlayback extends BasePlayback {
50598
50651
  return;
50599
50652
  }
50600
50653
  Log.warn('hlsjs: non-fatal error occurred', { evt, data });
50601
- trace(`${T$d} _onHLSJSError non-fatal error occurred`, {
50654
+ trace(`${T$e} _onHLSJSError non-fatal error occurred`, {
50602
50655
  type: data.type,
50603
50656
  details: data.details,
50604
50657
  });
50605
50658
  }
50606
50659
  }
50607
50660
  reload() {
50608
- this.cues = [];
50661
+ this.cues = null;
50609
50662
  this.currentCueId = null;
50610
50663
  this._hls?.startLoad(-1);
50611
50664
  }
@@ -50670,9 +50723,7 @@ class HlsPlayback extends BasePlayback {
50670
50723
  }
50671
50724
  triggerCues() {
50672
50725
  const currentTime = this.getCurrentTime();
50673
- // const cues = Object.values(this.cues)
50674
- // TODO build a search tree
50675
- const cue = this.cues.find((cue) => currentTime >= cue.start && currentTime <= cue.end);
50726
+ const cue = this.cues?.find(currentTime);
50676
50727
  if (cue) {
50677
50728
  this.currentCueId = cue.id;
50678
50729
  this.oncueenter?.(cue);
@@ -50715,20 +50766,14 @@ class HlsPlayback extends BasePlayback {
50715
50766
  destroy() {
50716
50767
  this._stopTimeUpdateTimer();
50717
50768
  this._destroyHLSInstance();
50769
+ this.cues = null;
50770
+ this.cuesByTrack = {};
50718
50771
  return super.destroy();
50719
50772
  }
50720
50773
  _updatePlaybackType(evt, data) {
50721
50774
  const prevPlaybackType = this._playbackType;
50722
50775
  this._playbackType = (data.details.live ? Playback.LIVE : Playback.VOD);
50723
50776
  this._onLevelUpdated(evt, data);
50724
- // Live stream subtitle tracks detection hack (may not immediately available)
50725
- // if (
50726
- // this._ccTracksUpdated &&
50727
- // this._playbackType === Playback.LIVE &&
50728
- // this.hasClosedCaptionsTracks
50729
- // ) {
50730
- // this._onSubtitleLoaded()
50731
- // }
50732
50777
  if (prevPlaybackType !== this._playbackType) {
50733
50778
  this._updateSettings();
50734
50779
  }
@@ -50939,7 +50984,7 @@ class HlsPlayback extends BasePlayback {
50939
50984
  this.trigger(Events$1.PLAYBACK_AUDIO_AVAILABLE, data.audioTracks.map(toClapprTrack));
50940
50985
  }
50941
50986
  _onAudioTrackSwitched(_, data) {
50942
- trace(`${T$d} onAudioTrackSwitched`);
50987
+ trace(`${T$e} onAudioTrackSwitched`);
50943
50988
  // @ts-ignore
50944
50989
  const track = this._hls.audioTracks[data.id];
50945
50990
  this.trigger(Events$1.PLAYBACK_AUDIO_CHANGED, toClapprTrack(track));
@@ -50949,7 +50994,10 @@ class HlsPlayback extends BasePlayback {
50949
50994
  return;
50950
50995
  }
50951
50996
  this._hls.subtitleTrack = id;
50952
- this.cues = [];
50997
+ if (!this.cuesByTrack[id]) {
50998
+ this.cuesByTrack[id] = new RangesList();
50999
+ }
51000
+ this.cues = this.cuesByTrack[id];
50953
51001
  }
50954
51002
  /**
50955
51003
  * @override
@@ -50958,7 +51006,7 @@ class HlsPlayback extends BasePlayback {
50958
51006
  return this.getTextTracks();
50959
51007
  }
50960
51008
  getTextTracks() {
50961
- return this._hls?.subtitleTracks.map((t) => ({
51009
+ return (this._hls?.subtitleTracks.map((t) => ({
50962
51010
  id: t.id,
50963
51011
  name: t.name,
50964
51012
  track: {
@@ -50966,7 +51014,7 @@ class HlsPlayback extends BasePlayback {
50966
51014
  label: t.name,
50967
51015
  language: t.lang,
50968
51016
  },
50969
- })) || [];
51017
+ })) || []);
50970
51018
  }
50971
51019
  }
50972
51020
  HlsPlayback.canPlay = function (resource, mimeType) {
@@ -50984,7 +51032,7 @@ function toClapprTrack(t) {
50984
51032
  };
50985
51033
  }
50986
51034
 
50987
- const T$c = 'playback.html5_video';
51035
+ const T$d = 'playback.html5_video';
50988
51036
  const STALL_TIMEOUT = 15000;
50989
51037
  class HTML5Video extends BasePlayback {
50990
51038
  stallTimerId = null;
@@ -51085,7 +51133,7 @@ class HTML5Video extends BasePlayback {
51085
51133
  switchAudioTrack(id) {
51086
51134
  const tracks = this.el.audioTracks;
51087
51135
  const supported = !!tracks;
51088
- trace(`${T$c} switchAudioTrack`, {
51136
+ trace(`${T$d} switchAudioTrack`, {
51089
51137
  supported,
51090
51138
  });
51091
51139
  if (supported) {
@@ -51104,7 +51152,7 @@ function registerPlaybacks() {
51104
51152
  Loader.registerPlayback(DashPlayback);
51105
51153
  }
51106
51154
 
51107
- const T$b = 'gplayer';
51155
+ const T$c = 'gplayer';
51108
51156
  const DEFAULT_OPTIONS = {
51109
51157
  autoPlay: false,
51110
51158
  debug: 'none',
@@ -51298,6 +51346,17 @@ class Player {
51298
51346
  }
51299
51347
  this.player?.load(ms, ms[0].mimeType ?? '');
51300
51348
  }
51349
+ /**
51350
+ * Returns a registered core plugin instance by name, or `null` if not found.
51351
+ *
51352
+ * @example
51353
+ * ```ts
51354
+ * const tokenRefresh = player.getPlugin('token_refresh') as TokenRefreshPlugin | null
51355
+ * ```
51356
+ */
51357
+ getPlugin(name) {
51358
+ return this.player?.core.getPlugin(name) ?? null;
51359
+ }
51301
51360
  /**
51302
51361
  * Mutes the sound of the video.
51303
51362
  */
@@ -51440,7 +51499,7 @@ class Player {
51440
51499
  }
51441
51500
  }
51442
51501
  triggerAutoPlay() {
51443
- trace(`${T$b} triggerAutoPlay`);
51502
+ trace(`${T$c} triggerAutoPlay`);
51444
51503
  setTimeout(() => {
51445
51504
  this.player?.play({
51446
51505
  autoPlay: true,
@@ -51458,7 +51517,7 @@ class Player {
51458
51517
  // TODO test
51459
51518
  events = {
51460
51519
  onReady: () => {
51461
- trace(`${T$b} onReady`, {
51520
+ trace(`${T$c} onReady`, {
51462
51521
  ready: this.ready,
51463
51522
  });
51464
51523
  if (this.ready) {
@@ -51492,7 +51551,7 @@ class Player {
51492
51551
  buildCoreOptions(rootNode) {
51493
51552
  const sources = this.buildMediaSourcesList();
51494
51553
  const source = sources[0];
51495
- trace(`${T$b} buildCoreOptions`, {
51554
+ trace(`${T$c} buildCoreOptions`, {
51496
51555
  source,
51497
51556
  sources,
51498
51557
  });
@@ -51569,7 +51628,7 @@ class Player {
51569
51628
  }
51570
51629
  }
51571
51630
 
51572
- var version$1 = "2.29.0";
51631
+ var version$1 = "2.30.1";
51573
51632
 
51574
51633
  var packages = {
51575
51634
  "node_modules/@clappr/core": {
@@ -52002,7 +52061,7 @@ const INITIAL_SETTINGS = {
52002
52061
  default: [],
52003
52062
  seekEnabled: false,
52004
52063
  };
52005
- const T$a = 'plugins.media_control';
52064
+ const T$b = 'plugins.media_control';
52006
52065
  /**
52007
52066
  * Extended events for the {@link MediaControl} plugin
52008
52067
  * @public
@@ -52295,7 +52354,7 @@ class MediaControl extends UICorePlugin {
52295
52354
  * Reenables the plugin disabled earlier with the {@link MediaControl.disable} method
52296
52355
  */
52297
52356
  enable() {
52298
- trace(`${T$a} enable`, {
52357
+ trace(`${T$b} enable`, {
52299
52358
  chromeless: this.options.chromeless,
52300
52359
  userDisabled: this.userDisabled,
52301
52360
  });
@@ -52452,7 +52511,7 @@ class MediaControl extends UICorePlugin {
52452
52511
  this.$el.removeClass('w370');
52453
52512
  this.$el.removeClass('w270');
52454
52513
  this.verticalVolume = false;
52455
- trace(`${T$a} playerResize`, {
52514
+ trace(`${T$b} playerResize`, {
52456
52515
  size,
52457
52516
  width: this.container.$el.width(),
52458
52517
  height: this.container.$el.height(),
@@ -53454,7 +53513,7 @@ class AudioTracks extends UICorePlugin {
53454
53513
 
53455
53514
  const templateHtml$2 = "<div class=\"big-mute-icon-wrapper\" data-big-mute id=\"gplayer-big-mute-button\">\n <div class=\"big-mute-icon gcore-skin-border-color\" data-big-mute-icon id=\"gplayer-big-mute-icon\"></div>\n</div>\n";
53456
53515
 
53457
- const T$9 = 'plugins.big_mute_button';
53516
+ const T$a = 'plugins.big_mute_button';
53458
53517
  // TODO rewrite as a container plugin
53459
53518
  /**
53460
53519
  * `PLUGIN` that displays a big mute button over the video when it's being played muted.
@@ -53517,7 +53576,7 @@ class BigMuteButton extends UICorePlugin {
53517
53576
  if (autoPlay) {
53518
53577
  this.autoPlay = true;
53519
53578
  }
53520
- trace(`${T$9} onPlay`, {
53579
+ trace(`${T$a} onPlay`, {
53521
53580
  autoPlay: this.autoPlay,
53522
53581
  wasMuted,
53523
53582
  volume,
@@ -53531,7 +53590,7 @@ class BigMuteButton extends UICorePlugin {
53531
53590
  }
53532
53591
  onStop(_, metadata) {
53533
53592
  const ui = metadata?.ui;
53534
- trace(`${T$9} onStop`, { ui });
53593
+ trace(`${T$a} onStop`, { ui });
53535
53594
  if (ui) {
53536
53595
  this.destroy();
53537
53596
  }
@@ -56495,7 +56554,7 @@ const PLAYBACK_NAMES = {
56495
56554
  hls: 'HLS.js',
56496
56555
  html5_video: 'Native',
56497
56556
  };
56498
- const T$8 = 'plugins.nerd_stats';
56557
+ const T$9 = 'plugins.nerd_stats';
56499
56558
  /**
56500
56559
  * `PLUGIN` that displays useful statistics regarding the playback as well as the network quality estimation.
56501
56560
  * @public
@@ -56632,7 +56691,7 @@ class NerdStats extends UICorePlugin {
56632
56691
  return super.destroy();
56633
56692
  }
56634
56693
  toggle = () => {
56635
- trace(`${T$8} toggle`, {
56694
+ trace(`${T$9} toggle`, {
56636
56695
  open: this.open,
56637
56696
  });
56638
56697
  if (this.open) {
@@ -56652,14 +56711,14 @@ class NerdStats extends UICorePlugin {
56652
56711
  })
56653
56712
  .catch((e) => {
56654
56713
  reportError(e);
56655
- trace(`${T$8} speedtest error`, {
56714
+ trace(`${T$9} speedtest error`, {
56656
56715
  error: e,
56657
56716
  });
56658
56717
  this.disable();
56659
56718
  });
56660
56719
  }
56661
56720
  hide() {
56662
- trace(`${T$8} hide`);
56721
+ trace(`${T$9} hide`);
56663
56722
  this.$el.hide();
56664
56723
  this.open = false;
56665
56724
  stopSpeedtest();
@@ -57392,7 +57451,7 @@ const reloadIcon = "<svg fill=\"#FFFFFF\" height=\"24\" viewBox=\"0 0 24 24\" wi
57392
57451
 
57393
57452
  const templateHtml = "<div class=\"player-error-screen__content\" data-error-screen>\n <% if (icon) { %>\n <div class=\"player-error-screen__icon\" data-error-screen><%= icon %></div>\n <% } %>\n <div class=\"player-error-screen__title\" data-error-screen><%= title %></div>\n <% if (message) { %>\n <div class=\"player-error-screen__message\" data-error-screen><%= message %></div>\n <% } %>\n <% if (code) { %>\n <div class=\"player-error-screen__code\" data-error-screen><%= i18n.t('error_code') %>: <%= code %></div>\n <% } %>\n <% if (reloadIcon) { %>\n <div class=\"player-error-screen__reload\" data-error-screen><%= reloadIcon %></div>\n <% } %>\n</div>\n";
57394
57453
 
57395
- const T$7 = 'plugins.error_screen';
57454
+ const T$8 = 'plugins.error_screen';
57396
57455
  /**
57397
57456
  * `PLUGIN` that displays fatal errors nicely in the overlay on top of the player.
57398
57457
  * @public
@@ -57444,11 +57503,11 @@ class ErrorScreen extends UICorePlugin {
57444
57503
  this.listenTo(this.core, Events$1.CORE_ACTIVE_CONTAINER_CHANGED, this.onActiveContainerChanged);
57445
57504
  }
57446
57505
  onPlay() {
57447
- trace(`${T$7} onPlay`);
57506
+ trace(`${T$8} onPlay`);
57448
57507
  this.unmount();
57449
57508
  }
57450
57509
  unmount() {
57451
- trace(`${T$7} unmount`);
57510
+ trace(`${T$8} unmount`);
57452
57511
  this.err = null;
57453
57512
  this.$el.remove();
57454
57513
  }
@@ -57461,7 +57520,7 @@ class ErrorScreen extends UICorePlugin {
57461
57520
  };
57462
57521
  }
57463
57522
  reload() {
57464
- trace(`${T$7} reload`);
57523
+ trace(`${T$8} reload`);
57465
57524
  setTimeout(() => {
57466
57525
  this.core.configure({
57467
57526
  reloading: true,
@@ -57484,7 +57543,7 @@ class ErrorScreen extends UICorePlugin {
57484
57543
  }
57485
57544
  }
57486
57545
  onError(err) {
57487
- trace(`${T$7} onError`, { err });
57546
+ trace(`${T$8} onError`, { err });
57488
57547
  if (err.UI) {
57489
57548
  if (this.err) {
57490
57549
  this.unmount();
@@ -57965,7 +58024,7 @@ const pluginHtml$3 = "<button data-multicamera-button class='gcore-skin-button-c
57965
58024
  const streamsIcon = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"20\" viewBox=\"0 0 24 20\">\n <path fill=\"#FFF\" fill-rule=\"nonzero\" d=\"M24 1.5v13a1.5 1.5 0 0 1-1.5 1.5h-1a.5.5 0 0 1-.5-.5v-10A2.5 2.5 0 0 0 18.5 3h-14a.5.5 0 0 1-.5-.5v-1A1.5 1.5 0 0 1 5.5 0h17A1.5 1.5 0 0 1 24 1.5M12.724 12.447l-5 2.5a.505.505 0 0 1-.487-.021A.503.503 0 0 1 7 14.5v-5c0-.173.09-.334.237-.426a.505.505 0 0 1 .487-.021l5 2.5a.5.5 0 0 1 0 .894M18.5 4h-17C.673 4 0 4.673 0 5.5v13c0 .827.673 1.5 1.5 1.5h17c.827 0 1.5-.673 1.5-1.5v-13c0-.827-.673-1.5-1.5-1.5\"/>\n</svg>\n";
57966
58025
 
57967
58026
  const VERSION$4 = '0.0.1';
57968
- const T$6 = 'plugins.multicamera';
58027
+ const T$7 = 'plugins.multicamera';
57969
58028
  /**
57970
58029
  * `PLUGIN` that adds support for loading multiple streams and switching between them using the media control UI.
57971
58030
  * @beta
@@ -58092,7 +58151,7 @@ class MultiCamera extends UICorePlugin {
58092
58151
  onCameraSelect(event) {
58093
58152
  const value = event.currentTarget.dataset
58094
58153
  .multicameraSelectorSelect;
58095
- trace(`${T$6} onCameraSelect`, { value });
58154
+ trace(`${T$7} onCameraSelect`, { value });
58096
58155
  if (value !== undefined) {
58097
58156
  this.changeById(parseInt(value, 10));
58098
58157
  }
@@ -58190,7 +58249,7 @@ class MultiCamera extends UICorePlugin {
58190
58249
  }
58191
58250
  }
58192
58251
  changeById(id) {
58193
- trace(`${T$6} changeById`, { id });
58252
+ trace(`${T$7} changeById`, { id });
58194
58253
  queueMicrotask(() => {
58195
58254
  const playbackOptions = this.core.options.playback || {};
58196
58255
  // TODO figure out if it's needed
@@ -58212,7 +58271,7 @@ class MultiCamera extends UICorePlugin {
58212
58271
  // TODO remove?
58213
58272
  // for html5 playback:
58214
58273
  this.options.dvrEnabled = this.currentCamera.dvr;
58215
- trace(`${T$6} changeById`, { currentCamera: this.currentCamera });
58274
+ trace(`${T$7} changeById`, { currentCamera: this.currentCamera });
58216
58275
  // TODO
58217
58276
  this.core.configure({
58218
58277
  playback: playbackOptions,
@@ -58275,7 +58334,7 @@ const pipIcon = "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"no
58275
58334
  const buttonHtml$2 = "<button class=\"gplayer-lite-btn gcore-skin-button-color\">\n <%= pipIcon %>\n</button>\n";
58276
58335
 
58277
58336
  const VERSION$3 = '0.0.1';
58278
- const T$5 = `plugins.pip`;
58337
+ const T$6 = `plugins.pip`;
58279
58338
  /**
58280
58339
  * `PLUGIN` that enables picture-in-picture mode.
58281
58340
  * @public
@@ -58333,7 +58392,7 @@ class PictureInPicture extends UICorePlugin {
58333
58392
  });
58334
58393
  }
58335
58394
  isPiPSupported() {
58336
- trace(`${T$5} isPiPSupported`, {
58395
+ trace(`${T$6} isPiPSupported`, {
58337
58396
  pictureInPictureEnabled: !!document.pictureInPictureEnabled,
58338
58397
  requestPictureInPicture: !!HTMLVideoElement.prototype.requestPictureInPicture,
58339
58398
  });
@@ -58355,7 +58414,7 @@ class PictureInPicture extends UICorePlugin {
58355
58414
  return this;
58356
58415
  }
58357
58416
  togglePictureInPicture() {
58358
- trace(`${T$5} togglePictureInPicture`);
58417
+ trace(`${T$6} togglePictureInPicture`);
58359
58418
  if (this.videoElement !== document.pictureInPictureElement) {
58360
58419
  this.requestPictureInPicture();
58361
58420
  }
@@ -58364,13 +58423,13 @@ class PictureInPicture extends UICorePlugin {
58364
58423
  }
58365
58424
  }
58366
58425
  requestPictureInPicture() {
58367
- trace(`${T$5} requestPictureInPicture`, {
58426
+ trace(`${T$6} requestPictureInPicture`, {
58368
58427
  videoElement: !!this.videoElement,
58369
58428
  });
58370
58429
  this.videoElement.requestPictureInPicture();
58371
58430
  }
58372
58431
  exitPictureInPicture() {
58373
- trace(`${T$5} exitPictureInPicture`);
58432
+ trace(`${T$6} exitPictureInPicture`);
58374
58433
  document.exitPictureInPicture();
58375
58434
  }
58376
58435
  }
@@ -58397,7 +58456,7 @@ const DEFAULT_PLAYBACK_RATES = [
58397
58456
  { value: 2.0, label: '2x' },
58398
58457
  ];
58399
58458
  const DEFAULT_PLAYBACK_RATE = 1;
58400
- const T$4 = 'plugins.playback_rate';
58459
+ const T$5 = 'plugins.playback_rate';
58401
58460
  /**
58402
58461
  * `PLUGIN` that allows changing the playback speed of the video.
58403
58462
  * @public
@@ -58483,7 +58542,7 @@ class PlaybackRate extends UICorePlugin {
58483
58542
  this.listenTo(this.core, Events$1.CORE_ACTIVE_CONTAINER_CHANGED, this.onActiveContainerChange);
58484
58543
  }
58485
58544
  onCoreReady() {
58486
- trace(`${T$4} onCoreReady`);
58545
+ trace(`${T$5} onCoreReady`);
58487
58546
  const mediaControl = this.core.getPlugin('media_control');
58488
58547
  assert(mediaControl, 'media_control plugin is required');
58489
58548
  const gear = this.core.getPlugin('bottom_gear');
@@ -58492,7 +58551,7 @@ class PlaybackRate extends UICorePlugin {
58492
58551
  this.listenTo(gear, GearEvents.RENDERED, this.onGearRendered);
58493
58552
  }
58494
58553
  onActiveContainerChange() {
58495
- trace(`${T$4} onActiveContainerChange`);
58554
+ trace(`${T$5} onActiveContainerChange`);
58496
58555
  this.metadataLoaded = false;
58497
58556
  this.listenTo(this.core.activePlayback, Events$1.PLAYBACK_STOP, this.onStop);
58498
58557
  this.listenTo(this.core.activePlayback, Events$1.PLAYBACK_PLAY, this.onPlay);
@@ -58500,15 +58559,15 @@ class PlaybackRate extends UICorePlugin {
58500
58559
  this.listenTo(this.core.activeContainer, Events$1.CONTAINER_LOADEDMETADATA, this.onMetaDataLoaded);
58501
58560
  }
58502
58561
  onMediaControlRendered() {
58503
- trace(`${T$4} onMediaControlRendered`);
58562
+ trace(`${T$5} onMediaControlRendered`);
58504
58563
  this.render();
58505
58564
  }
58506
58565
  onGearRendered() {
58507
- trace(`${T$4} onGearRendered`);
58566
+ trace(`${T$5} onGearRendered`);
58508
58567
  this.mount();
58509
58568
  }
58510
58569
  mount() {
58511
- trace(`${T$4} mount`, {
58570
+ trace(`${T$5} mount`, {
58512
58571
  shouldMount: this.shouldMount(),
58513
58572
  });
58514
58573
  if (!this.shouldMount()) {
@@ -58525,7 +58584,7 @@ class PlaybackRate extends UICorePlugin {
58525
58584
  })));
58526
58585
  }
58527
58586
  onMetaDataLoaded() {
58528
- trace(`${T$4} onMetaDataLoaded`, {
58587
+ trace(`${T$5} onMetaDataLoaded`, {
58529
58588
  playbackType: this.core.activePlayback.getPlaybackType(),
58530
58589
  dvrEnabled: this.core.activePlayback.dvrEnabled,
58531
58590
  });
@@ -58547,7 +58606,7 @@ class PlaybackRate extends UICorePlugin {
58547
58606
  this.core.activePlayback?.setPlaybackRate(this.selectedRate);
58548
58607
  }
58549
58608
  else {
58550
- trace(`${T$4} onPlaybackRateChange not steering to the selected rate, it is seemingly a catchup algorithm working`, {
58609
+ trace(`${T$5} onPlaybackRateChange not steering to the selected rate, it is seemingly a catchup algorithm working`, {
58551
58610
  playbackRate,
58552
58611
  selectedRate: this.selectedRate,
58553
58612
  });
@@ -58610,13 +58669,13 @@ class PlaybackRate extends UICorePlugin {
58610
58669
  }
58611
58670
  }
58612
58671
  syncRate() {
58613
- trace(`${T$4} syncRate`, {
58672
+ trace(`${T$5} syncRate`, {
58614
58673
  selectedRate: this.selectedRate,
58615
58674
  });
58616
58675
  this.core.activePlayback?.setPlaybackRate(this.selectedRate);
58617
58676
  }
58618
58677
  resetPlaybackRate() {
58619
- trace(`${T$4} resetPlaybackRate`, {
58678
+ trace(`${T$5} resetPlaybackRate`, {
58620
58679
  selectedRate: this.selectedRate,
58621
58680
  });
58622
58681
  this.core.activePlayback?.setPlaybackRate(DEFAULT_PLAYBACK_RATE);
@@ -58651,7 +58710,7 @@ class PlaybackRate extends UICorePlugin {
58651
58710
  ?.label || `x${rate}`);
58652
58711
  }
58653
58712
  highlightCurrentRate() {
58654
- trace(`${T$4} highlightCurrentRate`, {
58713
+ trace(`${T$5} highlightCurrentRate`, {
58655
58714
  selectedRate: this.selectedRate,
58656
58715
  });
58657
58716
  this.allRateElements().removeClass('current');
@@ -59687,7 +59746,7 @@ class SpinnerThreeBounce extends UIContainerPlugin {
59687
59746
  }
59688
59747
  }
59689
59748
 
59690
- const T$3 = 'plugins.source_controller';
59749
+ const T$4 = 'plugins.source_controller';
59691
59750
  const INITIAL_RETRY_DELAY = 1000;
59692
59751
  const MAX_RETRY_DELAY = 5000;
59693
59752
  const RETRY_DELAY_BLUR = 500;
@@ -59824,7 +59883,7 @@ class SourceController extends CorePlugin {
59824
59883
  }
59825
59884
  bindContainerEventListeners() {
59826
59885
  this.core.activePlayback.on(Events$1.PLAYBACK_ERROR, (error) => {
59827
- trace(`${T$3} on PLAYBACK_ERROR`, {
59886
+ trace(`${T$4} on PLAYBACK_ERROR`, {
59828
59887
  error: {
59829
59888
  code: error?.code,
59830
59889
  description: error?.description,
@@ -59848,7 +59907,7 @@ class SourceController extends CorePlugin {
59848
59907
  }
59849
59908
  });
59850
59909
  this.listenTo(this.core.activeContainer, Events$1.CONTAINER_PLAY, (_, { autoPlay }) => {
59851
- trace(`${T$3} onContainerPlay`, {
59910
+ trace(`${T$4} onContainerPlay`, {
59852
59911
  autoPlay,
59853
59912
  currentSource: this.sourcesList[this.currentSourceIndex],
59854
59913
  retrying: this.active,
@@ -59866,7 +59925,7 @@ class SourceController extends CorePlugin {
59866
59925
  this.sourcesDelay = {};
59867
59926
  }
59868
59927
  retryPlayback() {
59869
- trace(`${T$3} retryPlayback enter`, {
59928
+ trace(`${T$4} retryPlayback enter`, {
59870
59929
  currentSourceIndex: this.currentSourceIndex,
59871
59930
  currentSource: this.sourcesList[this.currentSourceIndex],
59872
59931
  });
@@ -59874,18 +59933,18 @@ class SourceController extends CorePlugin {
59874
59933
  this.switching = true;
59875
59934
  this.core.activeContainer?.getPlugin('spinner')?.show(0);
59876
59935
  this.getNextMediaSource().then((nextSource) => {
59877
- trace(`${T$3} retryPlayback syncing...`, {
59936
+ trace(`${T$4} retryPlayback syncing...`, {
59878
59937
  nextSource,
59879
59938
  });
59880
59939
  const rnd = Math.round(RETRY_DELAY_BLUR * Math.random());
59881
59940
  this.sync(() => {
59882
59941
  this.switching = false;
59883
59942
  this.core.load(nextSource.source, nextSource.mimeType);
59884
- trace(`${T$3} retryPlayback loaded`, {
59943
+ trace(`${T$4} retryPlayback loaded`, {
59885
59944
  nextSource,
59886
59945
  });
59887
59946
  setTimeout(() => {
59888
- trace(`${T$3} retryPlayback playing`, {
59947
+ trace(`${T$4} retryPlayback playing`, {
59889
59948
  autoPlay: this.autoPlay,
59890
59949
  nextSource,
59891
59950
  });
@@ -60243,8 +60302,6 @@ class ClosedCaptions extends UICorePlugin {
60243
60302
  // event.target does not exist for some reason in tests
60244
60303
  const id = Number((event.target ?? event.currentTarget).dataset?.item ??
60245
60304
  '-1');
60246
- // TODO review, make configurable, and emit event in addition
60247
- // localStorage.setItem(LOCAL_STORAGE_CC_ID, id) // TODO store language instead?
60248
60305
  this.userSelectedItemId = id;
60249
60306
  this.selectItem(this.findById(id));
60250
60307
  this.hideMenu();
@@ -60397,7 +60454,7 @@ class ClosedCaptions extends UICorePlugin {
60397
60454
  // An example implementation of client side performancestatistics
60398
60455
  const WATCH_CUTOFF = 5;
60399
60456
  const STALL_MEASURE_PERIOD = 10;
60400
- const T$2 = 'plugins.telemetry';
60457
+ const T$3 = 'plugins.telemetry';
60401
60458
  /**
60402
60459
  * Telemetry event type
60403
60460
  * @beta
@@ -60507,7 +60564,7 @@ class Telemetry extends ContainerPlugin {
60507
60564
  }
60508
60565
  onReady() {
60509
60566
  this.sendInit();
60510
- trace(`${T$2} onReady`, {
60567
+ trace(`${T$3} onReady`, {
60511
60568
  autoPlay: this.options.autoPlay,
60512
60569
  });
60513
60570
  if (this.options.autoPlay) {
@@ -63208,7 +63265,7 @@ function loadImageDimensions(url) {
63208
63265
  });
63209
63266
  }
63210
63267
 
63211
- const T$1 = 'plugins.thumbnails';
63268
+ const T$2 = 'plugins.thumbnails';
63212
63269
  /**
63213
63270
  * `PLUGIN` that displays the thumbnails of the video when available.
63214
63271
  * @public
@@ -63326,14 +63383,14 @@ class Thumbnails extends UICorePlugin {
63326
63383
  if (!this.options.thumbnails ||
63327
63384
  !this.options.thumbnails.sprite ||
63328
63385
  !this.options.thumbnails.vtt) {
63329
- trace(`${T$1} misconfigured: options.thumbnails.sprite and options.thumbnails.vtt are required`);
63386
+ trace(`${T$2} misconfigured: options.thumbnails.sprite and options.thumbnails.vtt are required`);
63330
63387
  this.destroy();
63331
63388
  return;
63332
63389
  }
63333
63390
  const { sprite: spriteSheet, vtt } = this.options.thumbnails;
63334
63391
  this.thumbs = this.buildSpriteConfig(parseVTT(vtt), spriteSheet);
63335
63392
  if (!this.thumbs.length) {
63336
- trace(`${T$1} failed to parse the sprite sheet`);
63393
+ trace(`${T$2} failed to parse the sprite sheet`);
63337
63394
  this.destroy();
63338
63395
  return;
63339
63396
  }
@@ -63582,6 +63639,321 @@ function parseVTT(vtt) {
63582
63639
  return cues;
63583
63640
  }
63584
63641
 
63642
+ const T$1 = 'plugins.token_refresh';
63643
+ /**
63644
+ * Matches the `/{token}/{expires}/` segment in a Gcore protected-content URL.
63645
+ * Token is base64url (letters, digits, `-`, `_`); expires is a ≥10-digit Unix timestamp.
63646
+ */
63647
+ const TOKEN_SEGMENT_RE = /\/([A-Za-z0-9_-]{6,})\/(1\d{9,})\//;
63648
+ function extractTokenState(url) {
63649
+ const m = url.match(TOKEN_SEGMENT_RE);
63650
+ if (!m)
63651
+ return null;
63652
+ return { token: m[1], expires: parseInt(m[2], 10) };
63653
+ }
63654
+ /** Replaces the exact `/{oldToken}/{oldExpires}/` segment in a URL. */
63655
+ function rewriteUrl(url, from, to) {
63656
+ const oldPart = `/${from.token}/${from.expires}/`;
63657
+ const newPart = `/${to.token}/${to.expires}/`;
63658
+ return url.includes(oldPart) ? url.replace(oldPart, newPart) : url;
63659
+ }
63660
+ /**
63661
+ * Normalises a URL by removing the `/{token}/{expires}/` segment so two URLs
63662
+ * for the same stream with different token pairs compare equal.
63663
+ * Returns `null` for unparseable input.
63664
+ */
63665
+ function streamKey(url) {
63666
+ try {
63667
+ const u = new URL(url);
63668
+ return u.origin + u.pathname.replace(TOKEN_SEGMENT_RE, '/');
63669
+ }
63670
+ catch {
63671
+ return null;
63672
+ }
63673
+ }
63674
+ /**
63675
+ * Returns a custom hls.js loader class that transparently rewrites the
63676
+ * token/expires path segments in every request URL.
63677
+ *
63678
+ * The returned class extends the default hls.js XhrLoader so all native
63679
+ * hls.js behaviour (retry, timeout, range requests …) is preserved.
63680
+ */
63681
+ function createTokenRewritingLoader(getOriginal, getCurrent) {
63682
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63683
+ const DefaultLoader = Hls.DefaultConfig.loader;
63684
+ return class TokenRewritingLoader extends DefaultLoader {
63685
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63686
+ load(context, config, callbacks) {
63687
+ const original = getOriginal();
63688
+ const current = getCurrent();
63689
+ if (original && current && context.url) {
63690
+ context.url = rewriteUrl(context.url, original, current);
63691
+ }
63692
+ super.load(context, config, callbacks);
63693
+ }
63694
+ };
63695
+ }
63696
+ /**
63697
+ * `PLUGIN` — automatic token refresh for Gcore protected-content streams.
63698
+ *
63699
+ * Supports all three playback engines:
63700
+ *
63701
+ * | Engine | Mechanism | Interruption |
63702
+ * |--------|-----------|--------------|
63703
+ * | **hls.js** | Custom loader rewrites every request URL | None |
63704
+ * | **dash.js** | `addRequestInterceptor` rewrites every request URL | None |
63705
+ * | **Native `<video>`** (Safari ≤ iOS 14.4) | Source reload + seek restore | Brief |
63706
+ *
63707
+ * @public
63708
+ * @remarks
63709
+ * Register the plugin once before creating any player instance:
63710
+ * ```ts
63711
+ * import { Player, TokenRefreshPlugin } from '@gcorevideo/player'
63712
+ * Player.registerPlugin(TokenRefreshPlugin)
63713
+ * ```
63714
+ *
63715
+ * Then pass `tokenRefresh` in `PlayerConfig`:
63716
+ * ```ts
63717
+ * const player = new Player({
63718
+ * sources: [{ source: initialUrl, mimeType: 'application/x-mpegURL' }],
63719
+ * tokenRefresh: {
63720
+ * getToken: () => fetch('https://…/token').then(r => r.json()),
63721
+ * ipBound: false,
63722
+ * refreshLeadSeconds: 5,
63723
+ * onTokenRefreshed: (data) => console.log('new token expires', data.expires),
63724
+ * },
63725
+ * })
63726
+ * ```
63727
+ *
63728
+ * @example
63729
+ * Safari native — opt-in Service Worker for fully seamless refresh:
63730
+ * ```js
63731
+ * // Register token-refresh-sw.js (see example/ directory)
63732
+ * // and omit tokenRefresh config — the SW handles rewriting.
63733
+ * ```
63734
+ */
63735
+ class TokenRefreshPlugin extends CorePlugin {
63736
+ /** @internal */
63737
+ static get type() {
63738
+ return 'core';
63739
+ }
63740
+ /** @internal */
63741
+ get name() {
63742
+ return 'token_refresh';
63743
+ }
63744
+ /** @internal */
63745
+ get supportedVersion() {
63746
+ return { min: CLAPPR_VERSION$1 };
63747
+ }
63748
+ /** Token state extracted from the currently-managed source URL */
63749
+ originalState = null;
63750
+ /** Latest token state (updated after each refresh) */
63751
+ currentState = null;
63752
+ /** Scheduled refresh timer handle */
63753
+ refreshTimer = null;
63754
+ /** Playback time (seconds) to restore after a native-video source reload */
63755
+ savedPosition = null;
63756
+ /** True when using native HTML5 Video playback (no request interception) */
63757
+ isNativePlayback = false;
63758
+ /** Set in destroy(); short-circuits late timer callbacks and getToken() resolutions */
63759
+ destroyed = false;
63760
+ /** @internal */
63761
+ bindEvents() {
63762
+ this.listenTo(this.core, Events$1.CORE_CONTAINERS_CREATED, this.onContainersCreated);
63763
+ }
63764
+ /** @internal */
63765
+ destroy() {
63766
+ this.destroyed = true;
63767
+ this.clearTimer();
63768
+ super.destroy();
63769
+ }
63770
+ onContainersCreated() {
63771
+ const container = this.core.containers[0];
63772
+ if (!container)
63773
+ return;
63774
+ const playbackName = container.playback.name;
63775
+ const src = container.playback.options?.src ?? '';
63776
+ trace(`${T$1} onContainersCreated`, { playbackName, src: src.slice(0, 80) });
63777
+ this.isNativePlayback = playbackName !== 'hls' && playbackName !== 'dash';
63778
+ const state = extractTokenState(src);
63779
+ if (!state) {
63780
+ // Active source has no token pattern — drop any refresh state we were
63781
+ // holding for a previous stream (e.g. SourceController rotated away).
63782
+ if (this.originalState) {
63783
+ trace(`${T$1} active source has no token pattern — clearing refresh state`);
63784
+ this.clearTimer();
63785
+ this.originalState = null;
63786
+ this.currentState = null;
63787
+ }
63788
+ return;
63789
+ }
63790
+ // Adopt the new token state if this is the first source we see, or if
63791
+ // the stream has changed (SourceController rotated, consumer called
63792
+ // player.load() with a different stream, or the plugin itself reloaded
63793
+ // with a refreshed URL in the native path).
63794
+ const isNewStream = !this.originalState ||
63795
+ state.token !== this.originalState.token ||
63796
+ state.expires !== this.originalState.expires;
63797
+ if (isNewStream) {
63798
+ trace(`${T$1} adopting source token state`, {
63799
+ token: state.token.slice(0, 8) + '…',
63800
+ expires: new Date(state.expires * 1000).toISOString(),
63801
+ });
63802
+ this.originalState = { ...state };
63803
+ this.currentState = { ...state };
63804
+ this.scheduleRefresh();
63805
+ }
63806
+ // Inject the appropriate interception mechanism for this playback engine.
63807
+ switch (playbackName) {
63808
+ case 'hls':
63809
+ this.injectHlsLoader(container);
63810
+ break;
63811
+ case 'dash':
63812
+ this.injectDashInterceptor(container);
63813
+ break;
63814
+ default:
63815
+ // Native HTML5 Video — no request hooks available.
63816
+ // Seek restore after a token-triggered reload.
63817
+ this.listenToOnce(this.core, Events$1.CORE_ACTIVE_CONTAINER_CHANGED, this.onActiveContainerChangedForNative);
63818
+ break;
63819
+ }
63820
+ }
63821
+ injectHlsLoader(container) {
63822
+ const getOriginal = () => this.originalState;
63823
+ const getCurrent = () => this.currentState;
63824
+ const TokenLoader = createTokenRewritingLoader(getOriginal, getCurrent);
63825
+ $.extend(true, container.playback.options, {
63826
+ playback: {
63827
+ hlsjsConfig: {
63828
+ loader: TokenLoader,
63829
+ },
63830
+ },
63831
+ });
63832
+ trace(`${T$1} HLS custom loader injected`);
63833
+ }
63834
+ injectDashInterceptor(container) {
63835
+ $.extend(true, container.playback.options, {
63836
+ dash: {
63837
+ requestInterceptor: (request) => {
63838
+ if (this.originalState && this.currentState) {
63839
+ request.url = rewriteUrl(request.url, this.originalState, this.currentState);
63840
+ }
63841
+ return Promise.resolve(request);
63842
+ },
63843
+ },
63844
+ });
63845
+ trace(`${T$1} DASH request interceptor injected`);
63846
+ }
63847
+ async reloadNativeSource(data) {
63848
+ const container = this.core.activeContainer;
63849
+ const playback = container?.playback;
63850
+ if (!playback)
63851
+ return;
63852
+ // SourceController (or any other actor) may have switched the active
63853
+ // playback to hls/dash while getToken() was in flight. Those engines
63854
+ // have their own request-interception path; a native reload would
63855
+ // undo the switch.
63856
+ if (playback.name === 'hls' || playback.name === 'dash') {
63857
+ trace(`${T$1} skipping native reload — active playback is ${playback.name}`);
63858
+ return;
63859
+ }
63860
+ if (!this.isNativePlayback) {
63861
+ trace(`${T$1} skipping native reload — no longer in native playback mode`);
63862
+ return;
63863
+ }
63864
+ // Verify the URL we're about to load belongs to the same stream that's
63865
+ // currently active. If SourceController rotated to a different stream
63866
+ // during the getToken() await, the refreshed URL would silently swap
63867
+ // us back, undoing SourceController's decision.
63868
+ const currentSrc = playback.options?.src ?? '';
63869
+ const newUrl = this.opts.ipBound ? data.url_ip : data.url;
63870
+ const activeKey = streamKey(currentSrc);
63871
+ const nextKey = streamKey(newUrl);
63872
+ if (!activeKey || !nextKey || activeKey !== nextKey) {
63873
+ trace(`${T$1} skipping native reload — active source differs from refresh URL`, {
63874
+ activeKey,
63875
+ nextKey,
63876
+ });
63877
+ return;
63878
+ }
63879
+ // Capture current playback position before tearing down the container.
63880
+ const mediaEl = playback.el;
63881
+ const currentTime = mediaEl?.currentTime ?? 0;
63882
+ this.savedPosition = currentTime > 0 ? currentTime : null;
63883
+ trace(`${T$1} native reload`, { newUrl: newUrl.slice(0, 80), savedPosition: this.savedPosition });
63884
+ // core.load() destroys and recreates all containers.
63885
+ this.core.load([{ source: newUrl, mimeType: this.core.options.mimeType ?? 'application/x-mpegURL' }]);
63886
+ }
63887
+ onActiveContainerChangedForNative() {
63888
+ if (this.savedPosition === null)
63889
+ return;
63890
+ const pos = this.savedPosition;
63891
+ this.savedPosition = null;
63892
+ // Wait for the new container to be fully ready before seeking.
63893
+ const container = this.core.activeContainer;
63894
+ if (!container)
63895
+ return;
63896
+ this.listenToOnce(container, Events$1.CONTAINER_READY, () => {
63897
+ trace(`${T$1} native: restoring position`, { pos });
63898
+ container.seek(pos);
63899
+ container.play();
63900
+ });
63901
+ }
63902
+ scheduleRefresh() {
63903
+ this.clearTimer();
63904
+ if (this.destroyed || !this.currentState)
63905
+ return;
63906
+ const leadMs = (this.opts.refreshLeadSeconds ?? 5) * 1000;
63907
+ const msUntilRefresh = this.currentState.expires * 1000 - Date.now() - leadMs;
63908
+ trace(`${T$1} next refresh in`, {
63909
+ seconds: Math.round(msUntilRefresh / 1000),
63910
+ expires: new Date(this.currentState.expires * 1000).toISOString(),
63911
+ });
63912
+ this.refreshTimer = setTimeout(() => this.performRefresh(), Math.max(msUntilRefresh, 1000));
63913
+ }
63914
+ async performRefresh() {
63915
+ trace(`${T$1} fetching new token`);
63916
+ try {
63917
+ const data = await this.opts.getToken();
63918
+ // Plugin may have been destroyed while getToken() was in flight; drop the result.
63919
+ if (this.destroyed)
63920
+ return;
63921
+ const newToken = this.opts.ipBound ? data.token_ip : data.token;
63922
+ const newState = { token: newToken, expires: data.expires };
63923
+ if (this.isNativePlayback) {
63924
+ // Must reload source because the <video> element has no request hook.
63925
+ await this.reloadNativeSource(data);
63926
+ }
63927
+ // originalState is never changed after init — it holds the token that was
63928
+ // baked into every URL in the initial manifest. hls.js/dash.js always
63929
+ // produces request URLs based on that manifest, so every segment URL
63930
+ // still contains the original token regardless of how many refreshes
63931
+ // have already happened. The loader replaces original→current on each
63932
+ // request, so updating only currentState is sufficient.
63933
+ this.currentState = newState;
63934
+ this.opts.onTokenRefreshed?.(data);
63935
+ trace(`${T$1} token refreshed`, {
63936
+ token: newToken.slice(0, 8) + '…',
63937
+ expires: new Date(data.expires * 1000).toISOString(),
63938
+ });
63939
+ }
63940
+ catch (err) {
63941
+ trace(`${T$1} token refresh failed`, { err });
63942
+ }
63943
+ // Always reschedule, even after an error (will retry near next expiry).
63944
+ this.scheduleRefresh();
63945
+ }
63946
+ get opts() {
63947
+ return this.options.tokenRefresh;
63948
+ }
63949
+ clearTimer() {
63950
+ if (this.refreshTimer !== null) {
63951
+ clearTimeout(this.refreshTimer);
63952
+ this.refreshTimer = null;
63953
+ }
63954
+ }
63955
+ }
63956
+
63585
63957
  /**
63586
63958
  * Events emitted by the VolumeFade plugin.
63587
63959
  * @public
@@ -63690,4 +64062,4 @@ class VolumeFade extends UICorePlugin {
63690
64062
  }
63691
64063
  }
63692
64064
 
63693
- export { AudioTracks as AudioSelector, AudioTracks, BigMuteButton, BottomGear, ChainedTracer, NerdStats as ClapprNerdStats, ClapprStats, ClapprStatsChronograph, ClapprStatsCounter, ClapprStatsEvents, ClickToPause, Clips, ClosedCaptions, CmcdConfig, ContextMenu, DvrControls, ErrorScreen, ExtendedEvents, Favicon, GearEvents, GoogleAnalytics, QualityLevels as LevelSelector, LogTracer, Logger$1 as Logger, Logo, MediaControl, MultiCamera, NerdStats, PictureInPicture, PlaybackErrorCode, PlaybackRate, Player, PlayerEvent, Poster, QualityLevels, RemoteTracer, SeekTime, SentryTracer, Share, SkipTime, SourceController, SpinnerThreeBounce as Spinner, SpinnerEvents, SpinnerThreeBounce, ClosedCaptions as Subtitles, Telemetry, TelemetryEvent, Thumbnails, VolumeFade, VolumeFadeEvents, reportError, setTracer, trace, version };
64065
+ export { AudioTracks as AudioSelector, AudioTracks, BigMuteButton, BottomGear, ChainedTracer, NerdStats as ClapprNerdStats, ClapprStats, ClapprStatsChronograph, ClapprStatsCounter, ClapprStatsEvents, ClickToPause, Clips, ClosedCaptions, CmcdConfig, ContextMenu, DvrControls, ErrorScreen, ExtendedEvents, Favicon, GearEvents, GoogleAnalytics, QualityLevels as LevelSelector, LogTracer, Logger$1 as Logger, Logo, MediaControl, MultiCamera, NerdStats, PictureInPicture, PlaybackErrorCode, PlaybackRate, Player, PlayerEvent, Poster, QualityLevels, RemoteTracer, SeekTime, SentryTracer, Share, SkipTime, SourceController, SpinnerThreeBounce as Spinner, SpinnerEvents, SpinnerThreeBounce, ClosedCaptions as Subtitles, Telemetry, TelemetryEvent, Thumbnails, TokenRefreshPlugin, VolumeFade, VolumeFadeEvents, reportError, setTracer, trace, version };