@gcorevideo/player 2.29.0 → 2.30.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/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);
@@ -51206,6 +51210,17 @@ class Player {
51206
51210
  }
51207
51211
  this.player?.load(ms, ms[0].mimeType ?? '');
51208
51212
  }
51213
+ /**
51214
+ * Returns a registered core plugin instance by name, or `null` if not found.
51215
+ *
51216
+ * @example
51217
+ * ```ts
51218
+ * const tokenRefresh = player.getPlugin('token_refresh') as TokenRefreshPlugin | null
51219
+ * ```
51220
+ */
51221
+ getPlugin(name) {
51222
+ return this.player?.core.getPlugin(name) ?? null;
51223
+ }
51209
51224
  /**
51210
51225
  * Mutes the sound of the video.
51211
51226
  */
@@ -51477,7 +51492,7 @@ class Player {
51477
51492
  }
51478
51493
  }
51479
51494
 
51480
- var version$1 = "2.29.0";
51495
+ var version$1 = "2.30.0";
51481
51496
 
51482
51497
  var packages = {
51483
51498
  "node_modules/@clappr/core": {
package/dist/index.css CHANGED
@@ -122,6 +122,55 @@
122
122
  .gplayer-lite-btn::-moz-focus-inner {
123
123
  border: 0;
124
124
  padding: 0;
125
+ }.big-mute-icon-wrapper[data-big-mute] {
126
+ position: absolute;
127
+ z-index: 9998;
128
+ background-color: transparent;
129
+ display: flex;
130
+ justify-content: center;
131
+ width: 100%;
132
+ height: calc(100% - 50px);
133
+ margin: 0 auto;
134
+ opacity: 0.75;
135
+ transition: opacity 0.1s ease;
136
+ pointer-events: auto;
137
+ }
138
+ .big-mute-icon-wrapper[data-big-mute].hide {
139
+ display: none;
140
+ }
141
+ .big-mute-icon-wrapper[data-big-mute]:hover {
142
+ cursor: pointer;
143
+ }
144
+
145
+ .big-mute-icon[data-big-mute-icon] {
146
+ display: flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ align-self: center;
150
+ width: 120px;
151
+ height: 120px;
152
+ border: 2px solid white;
153
+ border-radius: 50%;
154
+ filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=60)";
155
+ filter: alpha(opacity=60);
156
+ opacity: 1;
157
+ box-shadow: 0 0 1px 0 white;
158
+ background: rgba(240, 243, 247, 0.9411764706);
159
+ z-index: 10000;
160
+ }
161
+ .big-mute-icon[data-big-mute-icon] svg {
162
+ margin-left: 5px;
163
+ width: 80px;
164
+ height: 80px;
165
+ }
166
+ .big-mute-icon[data-big-mute-icon] svg path {
167
+ fill: #1f1e1e !important;
168
+ }
169
+ .big-mute-icon[data-big-mute-icon]:hover {
170
+ background: rgba(240, 243, 247, 0.8784313725);
171
+ }
172
+ .big-mute-icon[data-big-mute-icon]:hover svg path {
173
+ fill: #151515 !important;
125
174
  }*,
126
175
  :focus,
127
176
  :visited {
@@ -230,78 +279,6 @@
230
279
  }
231
280
  .media-control-skin-1 .media-control-item.media-control-gear .gear-option_value {
232
281
  flex: 1 0 auto;
233
- }.big-mute-icon-wrapper[data-big-mute] {
234
- position: absolute;
235
- z-index: 9998;
236
- background-color: transparent;
237
- display: flex;
238
- justify-content: center;
239
- width: 100%;
240
- height: calc(100% - 50px);
241
- margin: 0 auto;
242
- opacity: 0.75;
243
- transition: opacity 0.1s ease;
244
- pointer-events: auto;
245
- }
246
- .big-mute-icon-wrapper[data-big-mute].hide {
247
- display: none;
248
- }
249
- .big-mute-icon-wrapper[data-big-mute]:hover {
250
- cursor: pointer;
251
- }
252
-
253
- .big-mute-icon[data-big-mute-icon] {
254
- display: flex;
255
- align-items: center;
256
- justify-content: center;
257
- align-self: center;
258
- width: 120px;
259
- height: 120px;
260
- border: 2px solid white;
261
- border-radius: 50%;
262
- filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=60)";
263
- filter: alpha(opacity=60);
264
- opacity: 1;
265
- box-shadow: 0 0 1px 0 white;
266
- background: rgba(240, 243, 247, 0.9411764706);
267
- z-index: 10000;
268
- }
269
- .big-mute-icon[data-big-mute-icon] svg {
270
- margin-left: 5px;
271
- width: 80px;
272
- height: 80px;
273
- }
274
- .big-mute-icon[data-big-mute-icon] svg path {
275
- fill: #1f1e1e !important;
276
- }
277
- .big-mute-icon[data-big-mute-icon]:hover {
278
- background: rgba(240, 243, 247, 0.8784313725);
279
- }
280
- .big-mute-icon[data-big-mute-icon]:hover svg path {
281
- fill: #151515 !important;
282
- }@charset "UTF-8";
283
- .gplayer-mc-clips {
284
- display: flex;
285
- gap: 6px;
286
- }
287
- .gplayer-mc-clips .gplayer-mc-clips-text {
288
- text-overflow: ellipsis;
289
- white-space: nowrap;
290
- overflow: hidden;
291
- display: inline-block;
292
- text-overflow: ellipsis;
293
- color: white;
294
- cursor: default;
295
- line-height: var(--bottom-panel);
296
- position: relative;
297
- max-width: 150px;
298
- }
299
- .gplayer-mc-clips .gplayer-mc-clips-text::before {
300
- content: "•";
301
- padding-right: 6px;
302
- }
303
- .gplayer-mc-clips .gplayer-mc-clips-text.compact {
304
- max-width: 100px;
305
282
  }:root {
306
283
  --primary-background-color: #000;
307
284
  --secondary-background-color: #262626;
@@ -666,6 +643,65 @@
666
643
  .fullscreen .clappr-nerd-stats .stats-box {
667
644
  top: unset;
668
645
  }
646
+ }.context-menu {
647
+ z-index: 999;
648
+ position: absolute;
649
+ top: 0;
650
+ left: 0;
651
+ text-align: center;
652
+ }
653
+ .context-menu .context-menu-list {
654
+ font-family: "Proxima Nova", sans-serif;
655
+ font-size: 12px;
656
+ line-height: 12px;
657
+ list-style-type: none;
658
+ text-align: left;
659
+ padding: 5px;
660
+ margin-left: auto;
661
+ margin-right: auto;
662
+ background-color: rgba(0, 0, 0, 0.75);
663
+ border: 1px solid #666;
664
+ border-radius: 4px;
665
+ }
666
+ .context-menu .context-menu-list-item button {
667
+ border: none;
668
+ background-color: transparent;
669
+ padding: 0;
670
+ color: white;
671
+ display: flex;
672
+ gap: 8px;
673
+ align-items: center;
674
+ justify-content: center;
675
+ cursor: pointer;
676
+ padding: 5px;
677
+ width: 100%;
678
+ }
679
+ .context-menu .context-menu-list-item_icon {
680
+ width: 20px;
681
+ height: 20px;
682
+ }@charset "UTF-8";
683
+ .gplayer-mc-clips {
684
+ display: flex;
685
+ gap: 6px;
686
+ }
687
+ .gplayer-mc-clips .gplayer-mc-clips-text {
688
+ text-overflow: ellipsis;
689
+ white-space: nowrap;
690
+ overflow: hidden;
691
+ display: inline-block;
692
+ text-overflow: ellipsis;
693
+ color: white;
694
+ cursor: default;
695
+ line-height: var(--bottom-panel);
696
+ position: relative;
697
+ max-width: 150px;
698
+ }
699
+ .gplayer-mc-clips .gplayer-mc-clips-text::before {
700
+ content: "•";
701
+ padding-right: 6px;
702
+ }
703
+ .gplayer-mc-clips .gplayer-mc-clips-text.compact {
704
+ max-width: 100px;
669
705
  }.dvr-controls {
670
706
  --disabled-opacity: 0.3;
671
707
  --circle-radius: 5px;
@@ -723,42 +759,6 @@
723
759
  .dvr-controls .live-button:hover {
724
760
  opacity: 1;
725
761
  text-shadow: rgba(255, 255, 255, 0.75) 0 0 5px;
726
- }.context-menu {
727
- z-index: 999;
728
- position: absolute;
729
- top: 0;
730
- left: 0;
731
- text-align: center;
732
- }
733
- .context-menu .context-menu-list {
734
- font-family: "Proxima Nova", sans-serif;
735
- font-size: 12px;
736
- line-height: 12px;
737
- list-style-type: none;
738
- text-align: left;
739
- padding: 5px;
740
- margin-left: auto;
741
- margin-right: auto;
742
- background-color: rgba(0, 0, 0, 0.75);
743
- border: 1px solid #666;
744
- border-radius: 4px;
745
- }
746
- .context-menu .context-menu-list-item button {
747
- border: none;
748
- background-color: transparent;
749
- padding: 0;
750
- color: white;
751
- display: flex;
752
- gap: 8px;
753
- align-items: center;
754
- justify-content: center;
755
- cursor: pointer;
756
- padding: 5px;
757
- width: 100%;
758
- }
759
- .context-menu .context-menu-list-item_icon {
760
- width: 20px;
761
- height: 20px;
762
762
  }div.player-error-screen, [data-player] div.player-error-screen {
763
763
  color: #CCCACA;
764
764
  position: absolute;
@@ -914,39 +914,6 @@ div.player-error-screen__reload, [data-player] div.player-error-screen__reload {
914
914
  }
915
915
  .media-control-skin-1 .media-control-item.media-control-pip button svg {
916
916
  height: 20px;
917
- }.seek-time {
918
- position: absolute;
919
- white-space: nowrap;
920
- height: 20px;
921
- line-height: 20px;
922
- font-size: 0;
923
- left: -100%;
924
- bottom: 55px;
925
- background-color: rgba(2, 2, 2, 0.5);
926
- z-index: 9999;
927
- transition: opacity 0.1s ease;
928
- }
929
- .seek-time.hidden {
930
- opacity: 0;
931
- }
932
- .seek-time .seek-time__pos {
933
- display: inline-block;
934
- color: white;
935
- font-size: 10px;
936
- padding-left: 7px;
937
- padding-right: 7px;
938
- vertical-align: top;
939
- }
940
- .seek-time .seek-time__duration {
941
- display: inline-block;
942
- color: rgba(255, 255, 255, 0.5);
943
- font-size: 10px;
944
- padding-right: 7px;
945
- vertical-align: top;
946
- }
947
- .seek-time .seek-time__duration::before {
948
- content: "|";
949
- margin-right: 7px;
950
917
  }[data-player] {
951
918
  --bottom-panel: 40px;
952
919
  }
@@ -1608,6 +1575,39 @@ div.player-error-screen__reload, [data-player] div.player-error-screen__reload {
1608
1575
  }
1609
1576
  .quality-levels li.current {
1610
1577
  background-color: #000;
1578
+ }.seek-time {
1579
+ position: absolute;
1580
+ white-space: nowrap;
1581
+ height: 20px;
1582
+ line-height: 20px;
1583
+ font-size: 0;
1584
+ left: -100%;
1585
+ bottom: 55px;
1586
+ background-color: rgba(2, 2, 2, 0.5);
1587
+ z-index: 9999;
1588
+ transition: opacity 0.1s ease;
1589
+ }
1590
+ .seek-time.hidden {
1591
+ opacity: 0;
1592
+ }
1593
+ .seek-time .seek-time__pos {
1594
+ display: inline-block;
1595
+ color: white;
1596
+ font-size: 10px;
1597
+ padding-left: 7px;
1598
+ padding-right: 7px;
1599
+ vertical-align: top;
1600
+ }
1601
+ .seek-time .seek-time__duration {
1602
+ display: inline-block;
1603
+ color: rgba(255, 255, 255, 0.5);
1604
+ font-size: 10px;
1605
+ padding-right: 7px;
1606
+ vertical-align: top;
1607
+ }
1608
+ .seek-time .seek-time__duration::before {
1609
+ content: "|";
1610
+ margin-right: 7px;
1611
1611
  }.share_plugin[data-share] {
1612
1612
  pointer-events: auto;
1613
1613
  z-index: 5;
@@ -1712,6 +1712,44 @@ div.player-error-screen__reload, [data-player] div.player-error-screen__reload {
1712
1712
  .mc-skip-time .skip-container .skip-item {
1713
1713
  flex: 1 0 0px;
1714
1714
  height: 100%;
1715
+ }.spinner-three-bounce[data-spinner] {
1716
+ position: absolute;
1717
+ width: 70px;
1718
+ text-align: center;
1719
+ z-index: 999;
1720
+ left: 0;
1721
+ right: 0;
1722
+ margin: 0 auto;
1723
+ margin-left: auto;
1724
+ margin-right: auto;
1725
+ /* center vertically */
1726
+ top: 50%;
1727
+ transform: translateY(-50%);
1728
+ }
1729
+ .spinner-three-bounce[data-spinner] > div {
1730
+ width: 18px;
1731
+ height: 18px;
1732
+ background-color: #FFF;
1733
+ border-radius: 100%;
1734
+ display: inline-block;
1735
+ animation: bouncedelay 1.4s infinite ease-in-out;
1736
+ /* Prevent first frame from flickering when animation starts */
1737
+ animation-fill-mode: both;
1738
+ }
1739
+ .spinner-three-bounce[data-spinner] [data-bounce1] {
1740
+ animation-delay: -0.32s;
1741
+ }
1742
+ .spinner-three-bounce[data-spinner] [data-bounce2] {
1743
+ animation-delay: -0.16s;
1744
+ }
1745
+
1746
+ @keyframes bouncedelay {
1747
+ 0%, 80%, 100% {
1748
+ transform: scale(0);
1749
+ }
1750
+ 40% {
1751
+ transform: scale(1);
1752
+ }
1715
1753
  }.media-control-skin-1 .media-control-cc button.media-control-button {
1716
1754
  display: flex;
1717
1755
  justify-content: center;
@@ -1836,44 +1874,6 @@ div.player-error-screen__reload, [data-player] div.player-error-screen__reload {
1836
1874
  }
1837
1875
  .scrub-thumbnails .backdrop .carousel img {
1838
1876
  width: auto;
1839
- }.spinner-three-bounce[data-spinner] {
1840
- position: absolute;
1841
- width: 70px;
1842
- text-align: center;
1843
- z-index: 999;
1844
- left: 0;
1845
- right: 0;
1846
- margin: 0 auto;
1847
- margin-left: auto;
1848
- margin-right: auto;
1849
- /* center vertically */
1850
- top: 50%;
1851
- transform: translateY(-50%);
1852
- }
1853
- .spinner-three-bounce[data-spinner] > div {
1854
- width: 18px;
1855
- height: 18px;
1856
- background-color: #FFF;
1857
- border-radius: 100%;
1858
- display: inline-block;
1859
- animation: bouncedelay 1.4s infinite ease-in-out;
1860
- /* Prevent first frame from flickering when animation starts */
1861
- animation-fill-mode: both;
1862
- }
1863
- .spinner-three-bounce[data-spinner] [data-bounce1] {
1864
- animation-delay: -0.32s;
1865
- }
1866
- .spinner-three-bounce[data-spinner] [data-bounce2] {
1867
- animation-delay: -0.16s;
1868
- }
1869
-
1870
- @keyframes bouncedelay {
1871
- 0%, 80%, 100% {
1872
- transform: scale(0);
1873
- }
1874
- 40% {
1875
- transform: scale(1);
1876
- }
1877
1877
  }.player-logo[data-logo] {
1878
1878
  position: absolute;
1879
1879
  z-index: 2;
@@ -12830,6 +12830,7 @@ class DashPlayback extends BasePlayback {
12830
12830
  this._dash = dash;
12831
12831
  this._dash.initialize();
12832
12832
  if (this.options.dash) {
12833
+ const { requestInterceptor, ...dashSettings } = this.options.dash;
12833
12834
  const settings = $.extend(true, {
12834
12835
  streaming: {
12835
12836
  text: {
@@ -12841,8 +12842,11 @@ class DashPlayback extends BasePlayback {
12841
12842
  // dispatchForManualRendering: true, // TODO only when useNativeSubtitles is not true?
12842
12843
  },
12843
12844
  },
12844
- }, this.options.dash);
12845
+ }, dashSettings);
12845
12846
  this._dash.updateSettings(settings);
12847
+ if (typeof requestInterceptor === 'function') {
12848
+ this._dash.addRequestInterceptor(requestInterceptor);
12849
+ }
12846
12850
  }
12847
12851
  this._dash.attachView(this.el);
12848
12852
  this._dash.setAutoPlay(false);
@@ -51067,6 +51071,17 @@ class Player {
51067
51071
  }
51068
51072
  this.player?.load(ms, ms[0].mimeType ?? '');
51069
51073
  }
51074
+ /**
51075
+ * Returns a registered core plugin instance by name, or `null` if not found.
51076
+ *
51077
+ * @example
51078
+ * ```ts
51079
+ * const tokenRefresh = player.getPlugin('token_refresh') as TokenRefreshPlugin | null
51080
+ * ```
51081
+ */
51082
+ getPlugin(name) {
51083
+ return this.player?.core.getPlugin(name) ?? null;
51084
+ }
51070
51085
  /**
51071
51086
  * Mutes the sound of the video.
51072
51087
  */