@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/README.md CHANGED
@@ -81,6 +81,114 @@ See the complete example app on Vercel: <https://github.com/dmitritz/gcore-video
81
81
 
82
82
  [Example codepen](https://codepen.io/dmitritz/pen/OPLdEab)
83
83
 
84
+ ## Protected-content streams — automatic token refresh
85
+
86
+ Gcore protected-content streams embed a security token and expiry timestamp
87
+ directly in the URL path:
88
+
89
+ ```
90
+ https://host/videos/{video-id}/{token}/{expires}/master.m3u8
91
+ ```
92
+
93
+ Once a token expires the CDN returns HTTP 401 for every segment request.
94
+ `TokenRefreshPlugin` handles renewal transparently: it fires a background timer
95
+ before the token expires, calls your `getToken()` function, and rewrites the
96
+ `/{token}/{expires}/` segment in every outgoing hls.js or dash.js request URL —
97
+ playback continues without buffering or interruption.
98
+
99
+ ### Supported playback engines
100
+
101
+ | Engine | Mechanism | Interruption |
102
+ |---|---|---|
103
+ | **hls.js** | Custom loader rewrites every request URL before XHR `open()` | None |
104
+ | **dash.js** | `addRequestInterceptor` rewrites every request URL | None |
105
+ | **Native `<video>`** (older Safari) | Source reload + seek restore | Brief |
106
+
107
+ For fully seamless refresh on older Safari, register `example/token-refresh-sw.js`
108
+ as a Service Worker — it intercepts all CDN fetch requests and rewrites the token
109
+ even for native media elements.
110
+
111
+ ### Usage
112
+
113
+ ```ts
114
+ import { Player, TokenRefreshPlugin } from '@gcorevideo/player'
115
+
116
+ // Register once before creating any player instance.
117
+ Player.registerPlugin(TokenRefreshPlugin)
118
+
119
+ const player = new Player({
120
+ sources: [{
121
+ // The token API returns a ready-to-use URL with the token embedded in the path.
122
+ // TokenRefreshPlugin reads the initial {token}/{expires} from this URL at startup.
123
+ source: 'https://host/videos/{id}/{token}/{expires}/master.m3u8',
124
+ mimeType: 'application/x-mpegURL',
125
+ }],
126
+
127
+ tokenRefresh: {
128
+ /**
129
+ * Called automatically ~refreshLeadSeconds before the current token expires.
130
+ * Must return a Promise resolving to a TokenResponse object.
131
+ */
132
+ getToken: () => fetch('https://your-token-api/token').then(r => r.json()),
133
+
134
+ /**
135
+ * Set to true to use IP-bound tokens (token_ip / url_ip).
136
+ * All CDN requests must then originate from the same IP as the first response.
137
+ * Default: false.
138
+ */
139
+ ipBound: false,
140
+
141
+ /**
142
+ * How many seconds before expiry to pre-fetch the new token.
143
+ * Rule of thumb: refreshLeadSeconds < tokenLifetime / 2.
144
+ * Default: 5.
145
+ */
146
+ refreshLeadSeconds: 5,
147
+
148
+ /** Optional callback fired after each successful token refresh. */
149
+ onTokenRefreshed(data) {
150
+ console.log('token refreshed, new expiry:', new Date(data.expires * 1000))
151
+ },
152
+ },
153
+ })
154
+
155
+ player.attachTo(document.getElementById('player'))
156
+ ```
157
+
158
+ ### TokenResponse shape
159
+
160
+ Your `getToken()` function must return an object with this structure:
161
+
162
+ ```ts
163
+ interface TokenResponse {
164
+ token: string // plain (any-IP) token
165
+ token_ip: string // IP-bound token
166
+ client_ip: string // client IP the token server observed
167
+ expires: number // Unix timestamp (seconds) when both tokens expire
168
+ url: string // full HLS master URL with plain token in path
169
+ url_ip: string // full HLS master URL with IP-bound token in path
170
+ }
171
+ ```
172
+
173
+ ### Pausing and resuming refresh
174
+
175
+ Use the plugin instance to suspend and resume the refresh cycle at runtime:
176
+
177
+ ```ts
178
+ const plugin = player.getPlugin('token_refresh')
179
+
180
+ plugin.pause() // stop the timer; existing token stays active until CDN rejects it
181
+ plugin.resume() // restart the timer; fetches immediately if token already expired
182
+ console.log(plugin.isPaused) // → true | false
183
+ ```
184
+
185
+ ### Working demo
186
+
187
+ See [`example/protected-content.html`](../../example/protected-content.html) and
188
+ [`example/protected-content.js`](../../example/protected-content.js) for a fully
189
+ annotated end-to-end integration, including UI feedback, IP-bound token switching,
190
+ a live countdown, and Service Worker integration notes.
191
+
84
192
  ## Documentation
85
193
 
86
194
  - [API reference](./docs/api/index.md)
package/dist/core.js CHANGED
@@ -12963,6 +12963,7 @@ class DashPlayback extends BasePlayback {
12963
12963
  this._dash = dash;
12964
12964
  this._dash.initialize();
12965
12965
  if (this.options.dash) {
12966
+ const { requestInterceptor, ...dashSettings } = this.options.dash;
12966
12967
  const settings = $.extend(true, {
12967
12968
  streaming: {
12968
12969
  text: {
@@ -12974,8 +12975,11 @@ class DashPlayback extends BasePlayback {
12974
12975
  // dispatchForManualRendering: true, // TODO only when useNativeSubtitles is not true?
12975
12976
  },
12976
12977
  },
12977
- }, this.options.dash);
12978
+ }, dashSettings);
12978
12979
  this._dash.updateSettings(settings);
12980
+ if (typeof requestInterceptor === 'function') {
12981
+ this._dash.addRequestInterceptor(requestInterceptor);
12982
+ }
12979
12983
  }
12980
12984
  this._dash.attachView(this.el);
12981
12985
  this._dash.setAutoPlay(false);
@@ -49996,6 +50000,48 @@ Hls.defaultConfig = void 0;
49996
50000
  // export const CLAPPR_VERSION: string = process.env.CLAPPR_VERSION || '0.11.3';
49997
50001
  const CLAPPR_VERSION = '0.13.0';
49998
50002
 
50003
+ class RangesList {
50004
+ // TODO write an efficient implementation
50005
+ items = [];
50006
+ insert(start, end, value) {
50007
+ const index = this.findIndex((start + end) / 2);
50008
+ this.items.splice(index, 0, [start, end, value]);
50009
+ }
50010
+ find(position) {
50011
+ const index = this.findIndex(position);
50012
+ const item = this.items[index];
50013
+ if (!item || item[0] > position || item[1] < position) {
50014
+ return null;
50015
+ }
50016
+ return item[2];
50017
+ }
50018
+ findIndex(position) {
50019
+ let low = 0;
50020
+ let high = this.items.length;
50021
+ let index = 0;
50022
+ while (low < high) {
50023
+ index = low + Math.floor((high - low) / 2);
50024
+ const item = this.items[index];
50025
+ if (item[0] > position) {
50026
+ if (index === low) {
50027
+ return index;
50028
+ }
50029
+ high = index;
50030
+ continue;
50031
+ }
50032
+ if (item[1] <= position) {
50033
+ if (index === high - 1) {
50034
+ return index + 1;
50035
+ }
50036
+ low = index + 1;
50037
+ continue;
50038
+ }
50039
+ break;
50040
+ }
50041
+ return index;
50042
+ }
50043
+ }
50044
+
49999
50045
  // Copyright 2014 Globo.com Player authors. All rights reserved.
50000
50046
  // Use of this source code is governed by a BSD-style
50001
50047
  // license that can be found on https://github.com/clappr/hlsjs-playback/blob/main/LICENSE
@@ -50005,7 +50051,6 @@ const DEFAULT_RECOVER_ATTEMPTS = 16;
50005
50051
  Events$1.register('PLAYBACK_FRAGMENT_PARSING_METADATA');
50006
50052
  const T$2 = 'playback.hls';
50007
50053
  class HlsPlayback extends BasePlayback {
50008
- _ccTracksUpdated = false;
50009
50054
  _currentFragment = null;
50010
50055
  _currentLevel = null;
50011
50056
  _durationExcludesAfterLiveSyncPoint = false;
@@ -50030,7 +50075,8 @@ class HlsPlayback extends BasePlayback {
50030
50075
  _timeUpdateTimer = null;
50031
50076
  oncueenter = null;
50032
50077
  oncueexit = null;
50033
- cues = []; // TODO check the list size and use BST if needed
50078
+ cues = null;
50079
+ cuesByTrack = {};
50034
50080
  currentCueId = null;
50035
50081
  /**
50036
50082
  * @internal
@@ -50227,7 +50273,6 @@ class HlsPlayback extends BasePlayback {
50227
50273
  }
50228
50274
  this._manifestParsed = false;
50229
50275
  // this._ccIsSetup = false
50230
- this._ccTracksUpdated = false;
50231
50276
  this._setInitialState();
50232
50277
  this._hls.destroy();
50233
50278
  this._hls = null;
@@ -50281,12 +50326,20 @@ class HlsPlayback extends BasePlayback {
50281
50326
  this._hls.on(Events.AUDIO_TRACK_SWITCHED, (evt, data) => this._onAudioTrackSwitched(evt, data));
50282
50327
  this._hls.on(Events.CUES_PARSED, (evt, data) => {
50283
50328
  data.cues?.forEach((cue) => {
50284
- this.cues.push({
50329
+ if (!this.cues) {
50330
+ const trackId = this._hls.subtitleTrack;
50331
+ if (!this.cuesByTrack[trackId]) {
50332
+ this.cuesByTrack[trackId] = new RangesList();
50333
+ }
50334
+ this.cues = this.cuesByTrack[trackId];
50335
+ }
50336
+ const cueInfo = {
50285
50337
  id: cue.id,
50286
50338
  start: cue.startTime,
50287
50339
  end: cue.endTime,
50288
50340
  text: cue.text,
50289
- });
50341
+ };
50342
+ this.cues.insert(cue.startTime, cue.endTime, cueInfo);
50290
50343
  });
50291
50344
  });
50292
50345
  this.bindCustomListeners();
@@ -50513,7 +50566,7 @@ class HlsPlayback extends BasePlayback {
50513
50566
  }
50514
50567
  }
50515
50568
  reload() {
50516
- this.cues = [];
50569
+ this.cues = null;
50517
50570
  this.currentCueId = null;
50518
50571
  this._hls?.startLoad(-1);
50519
50572
  }
@@ -50578,9 +50631,7 @@ class HlsPlayback extends BasePlayback {
50578
50631
  }
50579
50632
  triggerCues() {
50580
50633
  const currentTime = this.getCurrentTime();
50581
- // const cues = Object.values(this.cues)
50582
- // TODO build a search tree
50583
- const cue = this.cues.find((cue) => currentTime >= cue.start && currentTime <= cue.end);
50634
+ const cue = this.cues?.find(currentTime);
50584
50635
  if (cue) {
50585
50636
  this.currentCueId = cue.id;
50586
50637
  this.oncueenter?.(cue);
@@ -50623,20 +50674,14 @@ class HlsPlayback extends BasePlayback {
50623
50674
  destroy() {
50624
50675
  this._stopTimeUpdateTimer();
50625
50676
  this._destroyHLSInstance();
50677
+ this.cues = null;
50678
+ this.cuesByTrack = {};
50626
50679
  return super.destroy();
50627
50680
  }
50628
50681
  _updatePlaybackType(evt, data) {
50629
50682
  const prevPlaybackType = this._playbackType;
50630
50683
  this._playbackType = (data.details.live ? Playback.LIVE : Playback.VOD);
50631
50684
  this._onLevelUpdated(evt, data);
50632
- // Live stream subtitle tracks detection hack (may not immediately available)
50633
- // if (
50634
- // this._ccTracksUpdated &&
50635
- // this._playbackType === Playback.LIVE &&
50636
- // this.hasClosedCaptionsTracks
50637
- // ) {
50638
- // this._onSubtitleLoaded()
50639
- // }
50640
50685
  if (prevPlaybackType !== this._playbackType) {
50641
50686
  this._updateSettings();
50642
50687
  }
@@ -50857,7 +50902,10 @@ class HlsPlayback extends BasePlayback {
50857
50902
  return;
50858
50903
  }
50859
50904
  this._hls.subtitleTrack = id;
50860
- this.cues = [];
50905
+ if (!this.cuesByTrack[id]) {
50906
+ this.cuesByTrack[id] = new RangesList();
50907
+ }
50908
+ this.cues = this.cuesByTrack[id];
50861
50909
  }
50862
50910
  /**
50863
50911
  * @override
@@ -50866,7 +50914,7 @@ class HlsPlayback extends BasePlayback {
50866
50914
  return this.getTextTracks();
50867
50915
  }
50868
50916
  getTextTracks() {
50869
- return this._hls?.subtitleTracks.map((t) => ({
50917
+ return (this._hls?.subtitleTracks.map((t) => ({
50870
50918
  id: t.id,
50871
50919
  name: t.name,
50872
50920
  track: {
@@ -50874,7 +50922,7 @@ class HlsPlayback extends BasePlayback {
50874
50922
  label: t.name,
50875
50923
  language: t.lang,
50876
50924
  },
50877
- })) || [];
50925
+ })) || []);
50878
50926
  }
50879
50927
  }
50880
50928
  HlsPlayback.canPlay = function (resource, mimeType) {
@@ -51206,6 +51254,17 @@ class Player {
51206
51254
  }
51207
51255
  this.player?.load(ms, ms[0].mimeType ?? '');
51208
51256
  }
51257
+ /**
51258
+ * Returns a registered core plugin instance by name, or `null` if not found.
51259
+ *
51260
+ * @example
51261
+ * ```ts
51262
+ * const tokenRefresh = player.getPlugin('token_refresh') as TokenRefreshPlugin | null
51263
+ * ```
51264
+ */
51265
+ getPlugin(name) {
51266
+ return this.player?.core.getPlugin(name) ?? null;
51267
+ }
51209
51268
  /**
51210
51269
  * Mutes the sound of the video.
51211
51270
  */
@@ -51477,7 +51536,7 @@ class Player {
51477
51536
  }
51478
51537
  }
51479
51538
 
51480
- var version$1 = "2.29.0";
51539
+ var version$1 = "2.30.1";
51481
51540
 
51482
51541
  var packages = {
51483
51542
  "node_modules/@clappr/core": {