@gcorevideo/player 2.28.36 → 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 +108 -0
- package/assets/media-control/media-control.scss +8 -6
- package/assets/multi-camera/multicamera.ejs +27 -23
- package/assets/multi-camera/style.scss +7 -34
- package/assets/style/main.scss +2 -2
- package/dist/core.js +24 -7
- package/dist/index.css +324 -346
- package/dist/index.embed.js +24 -46
- package/dist/index.js +471 -245
- package/docs/api/player.md +22 -9
- package/docs/api/player.mediacontrol.setkeepvisible.md +56 -0
- package/docs/api/player.multicamera.md +0 -28
- package/docs/api/player.multiccamerasourceinfo.md +27 -0
- package/docs/api/{player.multicamera.unbindevents.md → player.multisourcesmode.md} +4 -7
- package/docs/api/player.sourcecontroller.md +0 -37
- package/lib/Player.d.ts +9 -0
- package/lib/Player.d.ts.map +1 -1
- package/lib/Player.js +11 -0
- package/lib/index.plugins.d.ts +1 -0
- package/lib/index.plugins.d.ts.map +1 -1
- package/lib/index.plugins.js +1 -0
- package/lib/playback/dash-playback/DashPlayback.d.ts +2 -1
- package/lib/playback/dash-playback/DashPlayback.d.ts.map +1 -1
- package/lib/playback/dash-playback/DashPlayback.js +5 -1
- package/lib/playback/hls-playback/HlsPlayback.d.ts +2 -1
- package/lib/playback/hls-playback/HlsPlayback.d.ts.map +1 -1
- package/lib/playback/types.d.ts +9 -0
- package/lib/playback/types.d.ts.map +1 -1
- package/lib/playback.types.d.ts +0 -6
- package/lib/playback.types.d.ts.map +1 -1
- package/lib/plugins/multi-camera/MultiCamera.d.ts +21 -4
- package/lib/plugins/multi-camera/MultiCamera.d.ts.map +1 -1
- package/lib/plugins/multi-camera/MultiCamera.js +70 -134
- package/lib/plugins/source-controller/SourceController.d.ts +0 -39
- package/lib/plugins/source-controller/SourceController.d.ts.map +1 -1
- package/lib/plugins/source-controller/SourceController.js +0 -39
- package/lib/plugins/token-refresh/TokenRefreshPlugin.d.ts +119 -0
- package/lib/plugins/token-refresh/TokenRefreshPlugin.d.ts.map +1 -0
- package/lib/plugins/token-refresh/TokenRefreshPlugin.js +318 -0
- package/lib/plugins/token-refresh/index.d.ts +2 -0
- package/lib/plugins/token-refresh/index.d.ts.map +1 -0
- package/lib/plugins/token-refresh/index.js +1 -0
- package/lib/utils/mediaSources.d.ts +4 -0
- package/lib/utils/mediaSources.d.ts.map +1 -1
- package/lib/utils/mediaSources.js +8 -6
- package/package.json +1 -1
- package/src/Player.ts +12 -0
- package/src/index.plugins.ts +1 -0
- package/src/playback/dash-playback/DashPlayback.ts +7 -3
- package/src/playback/hls-playback/HlsPlayback.ts +1 -1
- package/src/playback/types.ts +10 -0
- package/src/playback.types.ts +0 -6
- package/src/plugins/multi-camera/MultiCamera.ts +103 -166
- package/src/plugins/source-controller/SourceController.ts +0 -39
- package/src/plugins/subtitles/ClosedCaptions.ts +1 -1
- package/src/plugins/token-refresh/TokenRefreshPlugin.ts +425 -0
- package/src/plugins/token-refresh/index.ts +5 -0
- package/src/utils/mediaSources.ts +10 -6
- package/tsconfig.tsbuildinfo +1 -1
- package/docs/api/player.multicamera.activebyid.md +0 -67
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)
|
|
@@ -186,6 +186,7 @@
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
.media-control-indicator {
|
|
189
|
+
|
|
189
190
|
&[data-position],
|
|
190
191
|
&[data-duration] {
|
|
191
192
|
display: flex;
|
|
@@ -394,6 +395,7 @@
|
|
|
394
395
|
}
|
|
395
396
|
}
|
|
396
397
|
}
|
|
398
|
+
|
|
397
399
|
.seek-time {
|
|
398
400
|
height: 26px;
|
|
399
401
|
line-height: 26px;
|
|
@@ -472,37 +474,37 @@
|
|
|
472
474
|
font-size: var(--gplayer-mc-font-size-dropdown);
|
|
473
475
|
text-align: right;
|
|
474
476
|
height: 30px;
|
|
475
|
-
|
|
477
|
+
|
|
476
478
|
a {
|
|
477
479
|
display: block;
|
|
478
480
|
text-decoration: none;
|
|
479
481
|
text-overflow: ellipsis;
|
|
480
482
|
overflow: hidden;
|
|
481
483
|
white-space: nowrap;
|
|
482
|
-
|
|
484
|
+
|
|
483
485
|
// height: 30px;
|
|
484
486
|
padding: 5px 10px;
|
|
485
487
|
line-height: 20px;
|
|
486
488
|
color: #fffffe; // TODO color var
|
|
487
|
-
|
|
489
|
+
|
|
488
490
|
&:hover {
|
|
489
491
|
text-decoration: none;
|
|
490
492
|
background-color: rgb(0 0 0 / 40%);
|
|
491
493
|
color: var(--gplayer-mc-text-color);
|
|
492
494
|
}
|
|
493
495
|
}
|
|
494
|
-
|
|
496
|
+
|
|
495
497
|
&.current a {
|
|
496
498
|
color: #f00;
|
|
497
499
|
}
|
|
498
|
-
|
|
500
|
+
|
|
499
501
|
&:first-child {
|
|
500
502
|
a {
|
|
501
503
|
border-bottom-left-radius: 4px;
|
|
502
504
|
border-bottom-right-radius: 4px;
|
|
503
505
|
}
|
|
504
506
|
}
|
|
505
|
-
|
|
507
|
+
|
|
506
508
|
&:last-child {
|
|
507
509
|
a {
|
|
508
510
|
border-top-left-radius: 4px;
|
|
@@ -1,29 +1,33 @@
|
|
|
1
|
-
<button data-multicamera-button class='gcore-skin-button-color'>
|
|
1
|
+
<button data-multicamera-button class='gcore-skin-button-color media-control-button'>
|
|
2
2
|
<span class="multicamera-icon"></span>
|
|
3
3
|
</button>
|
|
4
4
|
|
|
5
5
|
<ul class="gcore-skin-bg-color">
|
|
6
|
-
<% for (var i
|
|
7
|
-
<% if(!streams[i].live && multisources_mode
|
|
6
|
+
<% for (var i=0; i < streams.length; i++) { %>
|
|
7
|
+
<% if(!streams[i].live && multisources_mode==='only_live' ) { %>
|
|
8
8
|
<% continue; %>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
<% } %>
|
|
10
|
+
<li>
|
|
11
|
+
<div class="multicamera-item" data-multicamera-selector-live="<%= streams[i].live %>"
|
|
12
|
+
data-multicamera-selector-select="<%= streams[i].id %>">
|
|
13
|
+
<div class="multicamera-screenshot">
|
|
14
|
+
<% if (streams[i].screenshot) { %>
|
|
15
|
+
<img src="<%= streams[i].screenshot %>" alt="<%= streams[i].title %>" />
|
|
16
|
+
<% } %>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="multicamera-text gcore-skin-text-color">
|
|
19
|
+
<% if (streams[i].title) { %>
|
|
20
|
+
<div class="multicamera-title gcore-skin-text-color">
|
|
21
|
+
<%= streams[i].title %>
|
|
22
|
+
</div>
|
|
23
|
+
<% } %>
|
|
24
|
+
<% if (streams[i].description) { %>
|
|
25
|
+
<div class="multicamera-description gcore-skin-text-color">
|
|
26
|
+
<%= streams[i].description %>
|
|
27
|
+
</div>
|
|
28
|
+
<% } %>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</li>
|
|
16
32
|
<% } %>
|
|
17
|
-
|
|
18
|
-
<div class="multicamera-text gcore-skin-text-color">
|
|
19
|
-
<% if (streams[i].title) { %>
|
|
20
|
-
<div class="multicamera-title gcore-skin-text-color"><%= streams[i].title %></div>
|
|
21
|
-
<% } %>
|
|
22
|
-
<% if (streams[i].description) { %>
|
|
23
|
-
<div class="multicamera-description gcore-skin-text-color"><%= streams[i].description %></div>
|
|
24
|
-
<% } %>
|
|
25
|
-
</div>
|
|
26
|
-
</div>
|
|
27
|
-
</li>
|
|
28
|
-
<% } %>
|
|
29
|
-
</ul>
|
|
33
|
+
</ul>
|
|
@@ -5,11 +5,8 @@
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
.multicamera[data-multicamera] {
|
|
8
|
-
float: right;
|
|
9
|
-
margin-top: 4px;
|
|
10
8
|
position: relative;
|
|
11
|
-
|
|
12
|
-
width: 20px;
|
|
9
|
+
order: 80;
|
|
13
10
|
|
|
14
11
|
button {
|
|
15
12
|
background-color: transparent;
|
|
@@ -20,10 +17,15 @@
|
|
|
20
17
|
font-size: 14px;
|
|
21
18
|
padding: 0;
|
|
22
19
|
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
width: 24px;
|
|
24
|
+
height: 20px;
|
|
25
|
+
|
|
23
26
|
svg {
|
|
24
27
|
height: 20px;
|
|
25
28
|
position: relative;
|
|
26
|
-
margin-top: 6px;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
&:hover {
|
|
@@ -33,13 +35,6 @@
|
|
|
33
35
|
&.changing {
|
|
34
36
|
animation: pulse 0.5s infinite alternate;
|
|
35
37
|
}
|
|
36
|
-
|
|
37
|
-
span.quality-arrow {
|
|
38
|
-
width: 9px;
|
|
39
|
-
height: 6px;
|
|
40
|
-
margin-top: 11px;
|
|
41
|
-
margin-left: 5px;
|
|
42
|
-
}
|
|
43
38
|
}
|
|
44
39
|
|
|
45
40
|
&>ul {
|
|
@@ -135,27 +130,5 @@
|
|
|
135
130
|
}
|
|
136
131
|
}
|
|
137
132
|
}
|
|
138
|
-
|
|
139
|
-
a {
|
|
140
|
-
color: #444;
|
|
141
|
-
padding: 2px 10px;
|
|
142
|
-
display: block;
|
|
143
|
-
text-decoration: none;
|
|
144
|
-
|
|
145
|
-
&:hover {
|
|
146
|
-
background-color: #555;
|
|
147
|
-
color: white;
|
|
148
|
-
|
|
149
|
-
a {
|
|
150
|
-
color: white;
|
|
151
|
-
text-decoration: none;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
&.current a {
|
|
157
|
-
color: #f00;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
133
|
}
|
|
161
134
|
}
|
package/assets/style/main.scss
CHANGED
package/dist/core.js
CHANGED
|
@@ -12686,6 +12686,9 @@ var PlayerEvent;
|
|
|
12686
12686
|
PlayerEvent["VolumeUpdate"] = "volumeupdate";
|
|
12687
12687
|
})(PlayerEvent || (PlayerEvent = {}));
|
|
12688
12688
|
|
|
12689
|
+
const MIME_TYPES_HLS = ['application/x-mpegurl', 'application/vnd.apple.mpegurl'];
|
|
12690
|
+
const MIME_TYPE_HLS = MIME_TYPES_HLS[0];
|
|
12691
|
+
const MIME_TYPE_DASH = 'application/dash+xml';
|
|
12689
12692
|
// TODO rewrite using the Playback classes and canPlay static methods
|
|
12690
12693
|
function buildMediaSourcesList(sources, priorityTransport = 'dash') {
|
|
12691
12694
|
const playbacks = Loader.registeredPlaybacks;
|
|
@@ -12725,22 +12728,21 @@ function wrapSource(s) {
|
|
|
12725
12728
|
}
|
|
12726
12729
|
function guessMimeType(s) {
|
|
12727
12730
|
if (s.endsWith('.mpd')) {
|
|
12728
|
-
return
|
|
12731
|
+
return MIME_TYPE_DASH;
|
|
12729
12732
|
}
|
|
12730
12733
|
if (s.endsWith('.m3u8')) {
|
|
12731
|
-
|
|
12732
|
-
return 'application/x-mpegurl';
|
|
12734
|
+
return MIME_TYPE_HLS;
|
|
12733
12735
|
}
|
|
12734
12736
|
}
|
|
12735
12737
|
function isDashSource(source, mimeType) {
|
|
12736
12738
|
if (mimeType) {
|
|
12737
|
-
return mimeType ===
|
|
12739
|
+
return mimeType === MIME_TYPE_DASH; // TODO consider video/mp4
|
|
12738
12740
|
}
|
|
12739
12741
|
return source.endsWith('.mpd');
|
|
12740
12742
|
}
|
|
12741
12743
|
function isHlsSource(source, mimeType) {
|
|
12742
12744
|
if (mimeType) {
|
|
12743
|
-
return
|
|
12745
|
+
return MIME_TYPES_HLS.includes(mimeType.toLowerCase());
|
|
12744
12746
|
}
|
|
12745
12747
|
return source.endsWith('.m3u8');
|
|
12746
12748
|
}
|
|
@@ -12961,6 +12963,7 @@ class DashPlayback extends BasePlayback {
|
|
|
12961
12963
|
this._dash = dash;
|
|
12962
12964
|
this._dash.initialize();
|
|
12963
12965
|
if (this.options.dash) {
|
|
12966
|
+
const { requestInterceptor, ...dashSettings } = this.options.dash;
|
|
12964
12967
|
const settings = $.extend(true, {
|
|
12965
12968
|
streaming: {
|
|
12966
12969
|
text: {
|
|
@@ -12972,8 +12975,11 @@ class DashPlayback extends BasePlayback {
|
|
|
12972
12975
|
// dispatchForManualRendering: true, // TODO only when useNativeSubtitles is not true?
|
|
12973
12976
|
},
|
|
12974
12977
|
},
|
|
12975
|
-
},
|
|
12978
|
+
}, dashSettings);
|
|
12976
12979
|
this._dash.updateSettings(settings);
|
|
12980
|
+
if (typeof requestInterceptor === 'function') {
|
|
12981
|
+
this._dash.addRequestInterceptor(requestInterceptor);
|
|
12982
|
+
}
|
|
12977
12983
|
}
|
|
12978
12984
|
this._dash.attachView(this.el);
|
|
12979
12985
|
this._dash.setAutoPlay(false);
|
|
@@ -51204,6 +51210,17 @@ class Player {
|
|
|
51204
51210
|
}
|
|
51205
51211
|
this.player?.load(ms, ms[0].mimeType ?? '');
|
|
51206
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
|
+
}
|
|
51207
51224
|
/**
|
|
51208
51225
|
* Mutes the sound of the video.
|
|
51209
51226
|
*/
|
|
@@ -51475,7 +51492,7 @@ class Player {
|
|
|
51475
51492
|
}
|
|
51476
51493
|
}
|
|
51477
51494
|
|
|
51478
|
-
var version$1 = "2.
|
|
51495
|
+
var version$1 = "2.30.0";
|
|
51479
51496
|
|
|
51480
51497
|
var packages = {
|
|
51481
51498
|
"node_modules/@clappr/core": {
|