@guardvideo/player-sdk 2.0.0 → 2.1.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/dist/core/player.d.ts +0 -16
- package/dist/core/player.d.ts.map +1 -1
- package/dist/core/types.d.ts +1 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.esm.js +307 -359
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +307 -359
- package/dist/index.js.map +1 -1
- package/dist/ui/PlayerUI.d.ts +12 -0
- package/dist/ui/PlayerUI.d.ts.map +1 -1
- package/dist/vanilla/guardvideo-player.js +307 -359
- package/dist/vanilla/guardvideo-player.js.map +1 -1
- package/dist/vanilla/guardvideo-player.min.js +1 -1
- package/dist/vanilla/guardvideo-player.min.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -35,23 +35,12 @@ const DEFAULT_SECURITY = {
|
|
|
35
35
|
class GuardVideoPlayer {
|
|
36
36
|
constructor(videoElement, videoId, config) {
|
|
37
37
|
this.videoId = videoId;
|
|
38
|
-
this.container = null;
|
|
39
38
|
this.hls = null;
|
|
40
39
|
this.state = exports.PlayerState.IDLE;
|
|
41
40
|
this.embedToken = null;
|
|
42
41
|
this.currentQuality = null;
|
|
43
|
-
this.ctxMenu = null;
|
|
44
|
-
this.ctxStyleTag = null;
|
|
45
|
-
this.watermarkEl = null;
|
|
46
|
-
this.watermarkObserver = null;
|
|
47
|
-
this._onCtx = this.handleContextMenu.bind(this);
|
|
48
|
-
this._onDocClick = this.hideContextMenu.bind(this);
|
|
49
|
-
this._onKeyDown = this.handleKeyDown.bind(this);
|
|
50
42
|
this._onRateChange = this.enforceMaxRate.bind(this);
|
|
51
|
-
this._onSelectStart = (e) => e.preventDefault();
|
|
52
|
-
this._onDragStart = (e) => e.preventDefault();
|
|
53
43
|
this.videoElement = videoElement;
|
|
54
|
-
this.container = videoElement.parentElement;
|
|
55
44
|
this.config = {
|
|
56
45
|
embedTokenEndpoint: config.embedTokenEndpoint,
|
|
57
46
|
apiBaseUrl: config.apiBaseUrl || '',
|
|
@@ -71,6 +60,7 @@ class GuardVideoPlayer {
|
|
|
71
60
|
onError: config.onError || (() => { }),
|
|
72
61
|
onQualityChange: config.onQualityChange || (() => { }),
|
|
73
62
|
onStateChange: config.onStateChange || (() => { }),
|
|
63
|
+
onWatermark: config.onWatermark || (() => { }),
|
|
74
64
|
};
|
|
75
65
|
this.log('Initializing GuardVideo Player', { videoId, config });
|
|
76
66
|
if (!this.checkAllowedDomain())
|
|
@@ -80,17 +70,17 @@ class GuardVideoPlayer {
|
|
|
80
70
|
}
|
|
81
71
|
log(message, data) {
|
|
82
72
|
if (this.config.debug) {
|
|
83
|
-
console.log(
|
|
73
|
+
console.log('[GuardVideoPlayer] ' + message, data || '');
|
|
84
74
|
}
|
|
85
75
|
}
|
|
86
76
|
error(message, data) {
|
|
87
|
-
console.error(
|
|
77
|
+
console.error('[GuardVideoPlayer] ' + message, data || '');
|
|
88
78
|
}
|
|
89
79
|
setState(newState) {
|
|
90
80
|
if (this.state !== newState) {
|
|
91
81
|
this.state = newState;
|
|
92
82
|
this.config.onStateChange(newState);
|
|
93
|
-
this.log(
|
|
83
|
+
this.log('State changed to: ' + newState);
|
|
94
84
|
}
|
|
95
85
|
}
|
|
96
86
|
checkAllowedDomain() {
|
|
@@ -98,11 +88,11 @@ class GuardVideoPlayer {
|
|
|
98
88
|
if (!domains || domains.length === 0)
|
|
99
89
|
return true;
|
|
100
90
|
const currentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
101
|
-
const allowed = domains.some((d) => currentOrigin === d || currentOrigin.endsWith(
|
|
91
|
+
const allowed = domains.some((d) => currentOrigin === d || currentOrigin.endsWith('.' + d.replace(/^https?:\/\//, '')));
|
|
102
92
|
if (!allowed) {
|
|
103
93
|
this.handleError({
|
|
104
94
|
code: 'DOMAIN_NOT_ALLOWED',
|
|
105
|
-
message:
|
|
95
|
+
message: 'This player is not authorized to run on ' + currentOrigin,
|
|
106
96
|
fatal: true,
|
|
107
97
|
});
|
|
108
98
|
return false;
|
|
@@ -111,294 +101,24 @@ class GuardVideoPlayer {
|
|
|
111
101
|
}
|
|
112
102
|
applySecurity() {
|
|
113
103
|
const sec = this.config.security;
|
|
114
|
-
const target = this.container || this.videoElement;
|
|
115
|
-
target.addEventListener('contextmenu', this._onCtx);
|
|
116
|
-
document.addEventListener('click', this._onDocClick);
|
|
117
|
-
if (sec.disableSelection) {
|
|
118
|
-
target.addEventListener('selectstart', this._onSelectStart);
|
|
119
|
-
target.style.userSelect = 'none';
|
|
120
|
-
target.style.webkitUserSelect = 'none';
|
|
121
|
-
}
|
|
122
|
-
if (sec.disableDrag) {
|
|
123
|
-
this.videoElement.addEventListener('dragstart', this._onDragStart);
|
|
124
|
-
this.videoElement.draggable = false;
|
|
125
|
-
}
|
|
126
104
|
if (sec.disablePiP) {
|
|
127
105
|
this.videoElement.disablePictureInPicture = true;
|
|
128
106
|
}
|
|
129
107
|
if (sec.disableScreenCapture) {
|
|
130
|
-
if ('mediaKeys' in this.videoElement &&
|
|
108
|
+
if ('mediaKeys' in this.videoElement &&
|
|
109
|
+
typeof navigator.requestMediaKeySystemAccess === 'function') {
|
|
131
110
|
this.log('Screen-capture protection: EME hint applied');
|
|
132
111
|
}
|
|
133
|
-
target.style.setProperty('-webkit-app-region', 'no-drag');
|
|
134
|
-
}
|
|
135
|
-
if (sec.blockDevTools) {
|
|
136
|
-
document.addEventListener('keydown', this._onKeyDown);
|
|
137
112
|
}
|
|
138
113
|
if (sec.maxPlaybackRate) {
|
|
139
114
|
this.videoElement.addEventListener('ratechange', this._onRateChange);
|
|
140
115
|
}
|
|
141
|
-
if (sec.enableWatermark && sec.watermarkText && this.container) {
|
|
142
|
-
this.createWatermark(sec.watermarkText);
|
|
143
|
-
}
|
|
144
|
-
this.injectProtectiveStyles();
|
|
145
|
-
}
|
|
146
|
-
handleContextMenu(e) {
|
|
147
|
-
e.preventDefault();
|
|
148
|
-
e.stopPropagation();
|
|
149
|
-
const sec = this.config.security;
|
|
150
|
-
if (sec.disableRightClick)
|
|
151
|
-
return;
|
|
152
|
-
const me = e;
|
|
153
|
-
this.showContextMenu(me.clientX, me.clientY);
|
|
154
|
-
}
|
|
155
|
-
showContextMenu(x, y) {
|
|
156
|
-
this.hideContextMenu();
|
|
157
|
-
const branding = this.config.branding;
|
|
158
|
-
const extraItems = this.config.contextMenuItems;
|
|
159
|
-
const menu = document.createElement('div');
|
|
160
|
-
menu.className = 'gv-ctx-menu';
|
|
161
|
-
menu.setAttribute('role', 'menu');
|
|
162
|
-
const header = document.createElement('a');
|
|
163
|
-
header.className = 'gv-ctx-header';
|
|
164
|
-
header.href = branding.url;
|
|
165
|
-
header.target = '_blank';
|
|
166
|
-
header.rel = 'noopener noreferrer';
|
|
167
|
-
header.setAttribute('role', 'menuitem');
|
|
168
|
-
if (branding.logoUrl) {
|
|
169
|
-
const logo = document.createElement('img');
|
|
170
|
-
logo.src = branding.logoUrl;
|
|
171
|
-
logo.alt = branding.name;
|
|
172
|
-
logo.className = 'gv-ctx-logo';
|
|
173
|
-
logo.width = 20;
|
|
174
|
-
logo.height = 20;
|
|
175
|
-
header.appendChild(logo);
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
179
|
-
svg.setAttribute('width', '18');
|
|
180
|
-
svg.setAttribute('height', '18');
|
|
181
|
-
svg.setAttribute('viewBox', '0 0 24 24');
|
|
182
|
-
svg.setAttribute('fill', 'none');
|
|
183
|
-
svg.setAttribute('stroke', branding.accentColor);
|
|
184
|
-
svg.setAttribute('stroke-width', '2');
|
|
185
|
-
svg.setAttribute('stroke-linecap', 'round');
|
|
186
|
-
svg.setAttribute('stroke-linejoin', 'round');
|
|
187
|
-
svg.innerHTML = '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>';
|
|
188
|
-
header.appendChild(svg);
|
|
189
|
-
}
|
|
190
|
-
const nameSpan = document.createElement('span');
|
|
191
|
-
nameSpan.className = 'gv-ctx-brand-name';
|
|
192
|
-
nameSpan.textContent = branding.name;
|
|
193
|
-
header.appendChild(nameSpan);
|
|
194
|
-
const tagSpan = document.createElement('span');
|
|
195
|
-
tagSpan.className = 'gv-ctx-tag';
|
|
196
|
-
tagSpan.textContent = 'Secure Video Player';
|
|
197
|
-
header.appendChild(tagSpan);
|
|
198
|
-
menu.appendChild(header);
|
|
199
|
-
if (extraItems.length > 0) {
|
|
200
|
-
extraItems.forEach((item) => {
|
|
201
|
-
if (item.separator) {
|
|
202
|
-
const sep = document.createElement('div');
|
|
203
|
-
sep.className = 'gv-ctx-sep';
|
|
204
|
-
menu.appendChild(sep);
|
|
205
|
-
}
|
|
206
|
-
const row = document.createElement('div');
|
|
207
|
-
row.className = 'gv-ctx-item';
|
|
208
|
-
row.setAttribute('role', 'menuitem');
|
|
209
|
-
row.tabIndex = 0;
|
|
210
|
-
if (item.icon) {
|
|
211
|
-
const ico = document.createElement('img');
|
|
212
|
-
ico.src = item.icon;
|
|
213
|
-
ico.width = 14;
|
|
214
|
-
ico.height = 14;
|
|
215
|
-
ico.className = 'gv-ctx-item-icon';
|
|
216
|
-
row.appendChild(ico);
|
|
217
|
-
}
|
|
218
|
-
const label = document.createElement('span');
|
|
219
|
-
label.textContent = item.label;
|
|
220
|
-
row.appendChild(label);
|
|
221
|
-
row.addEventListener('click', (ev) => {
|
|
222
|
-
ev.stopPropagation();
|
|
223
|
-
this.hideContextMenu();
|
|
224
|
-
if (item.onClick) {
|
|
225
|
-
item.onClick();
|
|
226
|
-
}
|
|
227
|
-
else if (item.href) {
|
|
228
|
-
window.open(item.href, '_blank', 'noopener,noreferrer');
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
menu.appendChild(row);
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
const sep = document.createElement('div');
|
|
235
|
-
sep.className = 'gv-ctx-sep';
|
|
236
|
-
menu.appendChild(sep);
|
|
237
|
-
const version = document.createElement('div');
|
|
238
|
-
version.className = 'gv-ctx-version';
|
|
239
|
-
version.textContent = `${branding.name} Player v1.0`;
|
|
240
|
-
menu.appendChild(version);
|
|
241
|
-
document.body.appendChild(menu);
|
|
242
|
-
const rect = menu.getBoundingClientRect();
|
|
243
|
-
const vw = window.innerWidth;
|
|
244
|
-
const vh = window.innerHeight;
|
|
245
|
-
menu.style.left = `${x + rect.width > vw ? vw - rect.width - 8 : x}px`;
|
|
246
|
-
menu.style.top = `${y + rect.height > vh ? vh - rect.height - 8 : y}px`;
|
|
247
|
-
this.ctxMenu = menu;
|
|
248
|
-
}
|
|
249
|
-
hideContextMenu() {
|
|
250
|
-
if (this.ctxMenu) {
|
|
251
|
-
this.ctxMenu.remove();
|
|
252
|
-
this.ctxMenu = null;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
injectProtectiveStyles() {
|
|
256
|
-
if (this.ctxStyleTag)
|
|
257
|
-
return;
|
|
258
|
-
const branding = this.config.branding;
|
|
259
|
-
const accent = branding.accentColor;
|
|
260
|
-
const css = `
|
|
261
|
-
/* GuardVideo branded context menu */
|
|
262
|
-
.gv-ctx-menu {
|
|
263
|
-
position: fixed;
|
|
264
|
-
z-index: 2147483647;
|
|
265
|
-
min-width: 220px;
|
|
266
|
-
background: rgba(18, 18, 22, 0.96);
|
|
267
|
-
backdrop-filter: blur(12px);
|
|
268
|
-
-webkit-backdrop-filter: blur(12px);
|
|
269
|
-
border: 1px solid rgba(255,255,255,0.08);
|
|
270
|
-
border-radius: 10px;
|
|
271
|
-
padding: 6px 0;
|
|
272
|
-
box-shadow: 0 8px 32px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.04);
|
|
273
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
274
|
-
font-size: 13px;
|
|
275
|
-
color: #e4e4e7;
|
|
276
|
-
user-select: none;
|
|
277
|
-
animation: gv-ctx-in 0.12s ease-out;
|
|
278
|
-
}
|
|
279
|
-
@keyframes gv-ctx-in {
|
|
280
|
-
from { opacity: 0; transform: scale(0.96); }
|
|
281
|
-
to { opacity: 1; transform: scale(1); }
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
.gv-ctx-header {
|
|
285
|
-
display: flex;
|
|
286
|
-
align-items: center;
|
|
287
|
-
gap: 8px;
|
|
288
|
-
padding: 8px 14px 8px 12px;
|
|
289
|
-
text-decoration: none;
|
|
290
|
-
color: inherit;
|
|
291
|
-
transition: background 0.15s;
|
|
292
|
-
border-radius: 6px 6px 0 0;
|
|
293
|
-
}
|
|
294
|
-
.gv-ctx-header:hover { background: rgba(255,255,255,0.06); }
|
|
295
|
-
|
|
296
|
-
.gv-ctx-logo { border-radius: 4px; }
|
|
297
|
-
|
|
298
|
-
.gv-ctx-brand-name {
|
|
299
|
-
font-weight: 600;
|
|
300
|
-
color: ${accent};
|
|
301
|
-
white-space: nowrap;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
.gv-ctx-tag {
|
|
305
|
-
margin-left: auto;
|
|
306
|
-
font-size: 10px;
|
|
307
|
-
color: rgba(255,255,255,0.35);
|
|
308
|
-
white-space: nowrap;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
.gv-ctx-sep {
|
|
312
|
-
height: 1px;
|
|
313
|
-
margin: 4px 10px;
|
|
314
|
-
background: rgba(255,255,255,0.07);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
.gv-ctx-item {
|
|
318
|
-
display: flex;
|
|
319
|
-
align-items: center;
|
|
320
|
-
gap: 8px;
|
|
321
|
-
padding: 7px 14px 7px 12px;
|
|
322
|
-
cursor: pointer;
|
|
323
|
-
transition: background 0.15s;
|
|
324
|
-
}
|
|
325
|
-
.gv-ctx-item:hover { background: rgba(255,255,255,0.06); }
|
|
326
|
-
.gv-ctx-item-icon { border-radius: 2px; }
|
|
327
|
-
|
|
328
|
-
.gv-ctx-version {
|
|
329
|
-
padding: 4px 14px 6px 12px;
|
|
330
|
-
font-size: 10px;
|
|
331
|
-
color: rgba(255,255,255,0.25);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/* Watermark overlay */
|
|
335
|
-
.gv-watermark {
|
|
336
|
-
position: absolute;
|
|
337
|
-
inset: 0;
|
|
338
|
-
pointer-events: none;
|
|
339
|
-
overflow: hidden;
|
|
340
|
-
z-index: 10;
|
|
341
|
-
}
|
|
342
|
-
.gv-watermark-text {
|
|
343
|
-
position: absolute;
|
|
344
|
-
white-space: nowrap;
|
|
345
|
-
font-size: 14px;
|
|
346
|
-
font-family: monospace;
|
|
347
|
-
color: rgba(255,255,255,0.07);
|
|
348
|
-
transform: rotate(-30deg);
|
|
349
|
-
user-select: none;
|
|
350
|
-
pointer-events: none;
|
|
351
|
-
}
|
|
352
|
-
`;
|
|
353
|
-
const tag = document.createElement('style');
|
|
354
|
-
tag.setAttribute('data-guardvideo', 'player-styles');
|
|
355
|
-
tag.textContent = css;
|
|
356
|
-
document.head.appendChild(tag);
|
|
357
|
-
this.ctxStyleTag = tag;
|
|
358
|
-
}
|
|
359
|
-
createWatermark(text) {
|
|
360
|
-
if (!this.container)
|
|
361
|
-
return;
|
|
362
|
-
const overlay = document.createElement('div');
|
|
363
|
-
overlay.className = 'gv-watermark';
|
|
364
|
-
for (let row = 0; row < 5; row++) {
|
|
365
|
-
for (let col = 0; col < 4; col++) {
|
|
366
|
-
const span = document.createElement('span');
|
|
367
|
-
span.className = 'gv-watermark-text';
|
|
368
|
-
span.textContent = text;
|
|
369
|
-
span.style.left = `${col * 28 + (row % 2) * 14}%`;
|
|
370
|
-
span.style.top = `${row * 22}%`;
|
|
371
|
-
overlay.appendChild(span);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
this.container.style.position = 'relative';
|
|
375
|
-
this.container.appendChild(overlay);
|
|
376
|
-
this.watermarkEl = overlay;
|
|
377
|
-
this.watermarkObserver = new MutationObserver(() => {
|
|
378
|
-
if (this.container && this.watermarkEl && !this.container.contains(this.watermarkEl)) {
|
|
379
|
-
this.container.appendChild(this.watermarkEl);
|
|
380
|
-
}
|
|
381
|
-
});
|
|
382
|
-
this.watermarkObserver.observe(this.container, { childList: true, subtree: false });
|
|
383
|
-
}
|
|
384
|
-
handleKeyDown(e) {
|
|
385
|
-
if (e.key === 'F12') {
|
|
386
|
-
e.preventDefault();
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
if (e.ctrlKey && e.shiftKey && ['I', 'J', 'C'].includes(e.key.toUpperCase())) {
|
|
390
|
-
e.preventDefault();
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
if (e.ctrlKey && e.key.toUpperCase() === 'U') {
|
|
394
|
-
e.preventDefault();
|
|
395
|
-
}
|
|
396
116
|
}
|
|
397
117
|
enforceMaxRate() {
|
|
398
118
|
const max = this.config.security.maxPlaybackRate;
|
|
399
119
|
if (this.videoElement.playbackRate > max) {
|
|
400
120
|
this.videoElement.playbackRate = max;
|
|
401
|
-
this.log(
|
|
121
|
+
this.log('Playback rate clamped to ' + max);
|
|
402
122
|
}
|
|
403
123
|
}
|
|
404
124
|
async initialize() {
|
|
@@ -423,38 +143,38 @@ class GuardVideoPlayer {
|
|
|
423
143
|
return;
|
|
424
144
|
try {
|
|
425
145
|
const tokenId = this.embedToken.tokenId;
|
|
426
|
-
const url =
|
|
146
|
+
const url = this.config.apiBaseUrl +
|
|
147
|
+
'/videos/stream/' + this.videoId +
|
|
148
|
+
'/viewer-config?token=' + encodeURIComponent(tokenId);
|
|
427
149
|
const resp = await fetch(url, { credentials: 'omit' });
|
|
428
150
|
if (!resp.ok) {
|
|
429
151
|
this.log('viewer-config fetch failed, falling back to SDK config', resp.status);
|
|
430
152
|
const sec = this.config.security;
|
|
431
|
-
if (sec.enableWatermark && sec.watermarkText
|
|
432
|
-
this.
|
|
153
|
+
if (sec.enableWatermark && sec.watermarkText) {
|
|
154
|
+
this.config.onWatermark?.(sec.watermarkText);
|
|
433
155
|
}
|
|
434
156
|
return;
|
|
435
157
|
}
|
|
436
158
|
const cfg = await resp.json();
|
|
437
159
|
this.log('Watermark config from server:', cfg);
|
|
438
|
-
if (cfg.enableWatermark && cfg.watermarkText
|
|
439
|
-
this.
|
|
160
|
+
if (cfg.enableWatermark && cfg.watermarkText) {
|
|
161
|
+
this.config.onWatermark?.(cfg.watermarkText);
|
|
440
162
|
}
|
|
441
163
|
}
|
|
442
164
|
catch (err) {
|
|
443
165
|
this.log('fetchAndApplyWatermark error (non-fatal):', err);
|
|
444
166
|
const sec = this.config.security;
|
|
445
|
-
if (sec.enableWatermark && sec.watermarkText
|
|
446
|
-
this.
|
|
167
|
+
if (sec.enableWatermark && sec.watermarkText) {
|
|
168
|
+
this.config.onWatermark?.(sec.watermarkText);
|
|
447
169
|
}
|
|
448
170
|
}
|
|
449
171
|
}
|
|
450
172
|
async fetchEmbedToken() {
|
|
451
|
-
const url =
|
|
173
|
+
const url = this.config.embedTokenEndpoint + '/' + this.videoId;
|
|
452
174
|
this.log('Fetching embed token from', url);
|
|
453
175
|
const response = await fetch(url, {
|
|
454
176
|
method: 'POST',
|
|
455
|
-
headers: {
|
|
456
|
-
'Content-Type': 'application/json',
|
|
457
|
-
},
|
|
177
|
+
headers: { 'Content-Type': 'application/json' },
|
|
458
178
|
body: JSON.stringify({
|
|
459
179
|
allowedDomain: window.location.origin,
|
|
460
180
|
expiresInMinutes: 120,
|
|
@@ -506,14 +226,11 @@ class GuardVideoPlayer {
|
|
|
506
226
|
this.hls.loadSource(playerUrl);
|
|
507
227
|
this.hls.attachMedia(this.videoElement);
|
|
508
228
|
this.hls.on(Hls.Events.MANIFEST_PARSED, (_event, data) => {
|
|
509
|
-
this.log('HLS manifest parsed', {
|
|
510
|
-
levels: data.levels.map((l) => `${l.height}p`),
|
|
511
|
-
});
|
|
229
|
+
this.log('HLS manifest parsed', { levels: data.levels.map((l) => l.height + 'p') });
|
|
512
230
|
this.setState(exports.PlayerState.READY);
|
|
513
231
|
this.config.onReady();
|
|
514
|
-
if (this.config.autoplay)
|
|
232
|
+
if (this.config.autoplay)
|
|
515
233
|
this.play();
|
|
516
|
-
}
|
|
517
234
|
});
|
|
518
235
|
this.hls.on(Hls.Events.LEVEL_SWITCHED, (_event, data) => {
|
|
519
236
|
const level = this.hls.levels[data.level];
|
|
@@ -522,10 +239,10 @@ class GuardVideoPlayer {
|
|
|
522
239
|
height: level.height,
|
|
523
240
|
width: level.width,
|
|
524
241
|
bitrate: level.bitrate,
|
|
525
|
-
name:
|
|
242
|
+
name: level.height + 'p',
|
|
526
243
|
};
|
|
527
244
|
this.currentQuality = quality;
|
|
528
|
-
this.log(
|
|
245
|
+
this.log('Quality switched to ' + quality.name);
|
|
529
246
|
this.config.onQualityChange(quality.name);
|
|
530
247
|
});
|
|
531
248
|
this.hls.on(Hls.Events.ERROR, (_event, data) => {
|
|
@@ -541,40 +258,28 @@ class GuardVideoPlayer {
|
|
|
541
258
|
this.hls?.recoverMediaError();
|
|
542
259
|
break;
|
|
543
260
|
default:
|
|
544
|
-
this.handleError({
|
|
545
|
-
code: data.type,
|
|
546
|
-
message: data.details,
|
|
547
|
-
fatal: true,
|
|
548
|
-
details: data,
|
|
549
|
-
});
|
|
550
|
-
break;
|
|
261
|
+
this.handleError({ code: data.type, message: data.details, fatal: true, details: data });
|
|
551
262
|
}
|
|
552
263
|
}
|
|
553
264
|
});
|
|
554
265
|
this.setupVideoEventListeners();
|
|
555
266
|
}
|
|
556
267
|
setupVideoEventListeners() {
|
|
557
|
-
this.videoElement.addEventListener('playing', () =>
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
this.videoElement.addEventListener('pause', () => {
|
|
561
|
-
this.setState(exports.PlayerState.PAUSED);
|
|
562
|
-
});
|
|
563
|
-
this.videoElement.addEventListener('waiting', () => {
|
|
564
|
-
this.setState(exports.PlayerState.BUFFERING);
|
|
565
|
-
});
|
|
268
|
+
this.videoElement.addEventListener('playing', () => this.setState(exports.PlayerState.PLAYING));
|
|
269
|
+
this.videoElement.addEventListener('pause', () => this.setState(exports.PlayerState.PAUSED));
|
|
270
|
+
this.videoElement.addEventListener('waiting', () => this.setState(exports.PlayerState.BUFFERING));
|
|
566
271
|
this.videoElement.addEventListener('error', () => {
|
|
567
272
|
const error = this.videoElement.error;
|
|
568
273
|
if (error) {
|
|
569
|
-
const
|
|
274
|
+
const msgs = {
|
|
570
275
|
1: 'Video loading aborted',
|
|
571
276
|
2: 'Network error',
|
|
572
277
|
3: 'Video decoding failed',
|
|
573
278
|
4: 'Video format not supported',
|
|
574
279
|
};
|
|
575
280
|
this.handleError({
|
|
576
|
-
code:
|
|
577
|
-
message:
|
|
281
|
+
code: 'MEDIA_ERROR_' + error.code,
|
|
282
|
+
message: msgs[error.code] || 'Unknown media error',
|
|
578
283
|
fatal: true,
|
|
579
284
|
details: error,
|
|
580
285
|
});
|
|
@@ -595,21 +300,11 @@ class GuardVideoPlayer {
|
|
|
595
300
|
throw err;
|
|
596
301
|
}
|
|
597
302
|
}
|
|
598
|
-
pause() {
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
}
|
|
604
|
-
seek(time) {
|
|
605
|
-
this.videoElement.currentTime = time;
|
|
606
|
-
}
|
|
607
|
-
getDuration() {
|
|
608
|
-
return this.videoElement.duration || 0;
|
|
609
|
-
}
|
|
610
|
-
getVolume() {
|
|
611
|
-
return this.videoElement.volume;
|
|
612
|
-
}
|
|
303
|
+
pause() { this.videoElement.pause(); }
|
|
304
|
+
getCurrentTime() { return this.videoElement.currentTime; }
|
|
305
|
+
seek(time) { this.videoElement.currentTime = time; }
|
|
306
|
+
getDuration() { return this.videoElement.duration || 0; }
|
|
307
|
+
getVolume() { return this.videoElement.volume; }
|
|
613
308
|
setVolume(volume) {
|
|
614
309
|
this.videoElement.volume = Math.max(0, Math.min(1, volume));
|
|
615
310
|
}
|
|
@@ -621,34 +316,20 @@ class GuardVideoPlayer {
|
|
|
621
316
|
height: level.height,
|
|
622
317
|
width: level.width,
|
|
623
318
|
bitrate: level.bitrate,
|
|
624
|
-
name:
|
|
319
|
+
name: level.height + 'p',
|
|
625
320
|
}));
|
|
626
321
|
}
|
|
627
|
-
getCurrentQuality() {
|
|
628
|
-
return this.currentQuality;
|
|
629
|
-
}
|
|
322
|
+
getCurrentQuality() { return this.currentQuality; }
|
|
630
323
|
setQuality(levelIndex) {
|
|
631
324
|
if (this.hls) {
|
|
632
325
|
this.hls.currentLevel = levelIndex;
|
|
633
|
-
this.log(
|
|
326
|
+
this.log('Quality set to level ' + levelIndex);
|
|
634
327
|
}
|
|
635
328
|
}
|
|
636
|
-
getState() {
|
|
637
|
-
return this.state;
|
|
638
|
-
}
|
|
329
|
+
getState() { return this.state; }
|
|
639
330
|
destroy() {
|
|
640
331
|
this.log('Destroying player');
|
|
641
|
-
const target = this.container || this.videoElement;
|
|
642
|
-
target.removeEventListener('contextmenu', this._onCtx);
|
|
643
|
-
target.removeEventListener('selectstart', this._onSelectStart);
|
|
644
|
-
this.videoElement.removeEventListener('dragstart', this._onDragStart);
|
|
645
332
|
this.videoElement.removeEventListener('ratechange', this._onRateChange);
|
|
646
|
-
document.removeEventListener('click', this._onDocClick);
|
|
647
|
-
document.removeEventListener('keydown', this._onKeyDown);
|
|
648
|
-
this.hideContextMenu();
|
|
649
|
-
this.watermarkObserver?.disconnect();
|
|
650
|
-
this.watermarkEl?.remove();
|
|
651
|
-
this.ctxStyleTag?.remove();
|
|
652
333
|
if (this.hls) {
|
|
653
334
|
this.hls.destroy();
|
|
654
335
|
this.hls = null;
|
|
@@ -735,6 +416,10 @@ function injectStyles() {
|
|
|
735
416
|
-ms-user-select: none;
|
|
736
417
|
user-select: none;
|
|
737
418
|
outline: none;
|
|
419
|
+
/* Reserve space at the bottom so the video is never covered by the controls bar.
|
|
420
|
+
Controls bar ≈ 10px top padding + seek(~20px) + btn row(~34px) + 14px bottom = ~88px.
|
|
421
|
+
We add this as padding-bottom so the video shrinks up rather than sitting behind the bar. */
|
|
422
|
+
padding-bottom: 90px;
|
|
738
423
|
|
|
739
424
|
/* Subtle inner vignette for cinema depth */
|
|
740
425
|
box-shadow:
|
|
@@ -1048,6 +733,7 @@ function injectStyles() {
|
|
|
1048
733
|
width: 0;
|
|
1049
734
|
max-width: 72px;
|
|
1050
735
|
height: 3px;
|
|
736
|
+
/* Default background — overridden by JS inline style for the fill gradient */
|
|
1051
737
|
background: rgba(255,255,255,0.18);
|
|
1052
738
|
border-radius: 99px;
|
|
1053
739
|
outline: none;
|
|
@@ -1065,6 +751,12 @@ function injectStyles() {
|
|
|
1065
751
|
width: 72px;
|
|
1066
752
|
opacity: 1;
|
|
1067
753
|
}
|
|
754
|
+
/* WebKit runnable track — gradient is set via inline style by JS */
|
|
755
|
+
.gvp-volume-slider::-webkit-slider-runnable-track {
|
|
756
|
+
height: 3px;
|
|
757
|
+
border-radius: 99px;
|
|
758
|
+
background: inherit; /* picks up the JS inline style gradient */
|
|
759
|
+
}
|
|
1068
760
|
/* WebKit thumb */
|
|
1069
761
|
.gvp-volume-slider::-webkit-slider-thumb {
|
|
1070
762
|
-webkit-appearance: none;
|
|
@@ -1072,9 +764,16 @@ function injectStyles() {
|
|
|
1072
764
|
border-radius: 50%;
|
|
1073
765
|
background: #fff;
|
|
1074
766
|
cursor: pointer;
|
|
767
|
+
margin-top: -4.5px; /* vertically centre over the 3px track */
|
|
1075
768
|
-webkit-box-shadow: 0 1px 4px rgba(0,0,0,0.4);
|
|
1076
769
|
box-shadow: 0 1px 4px rgba(0,0,0,0.4);
|
|
1077
770
|
}
|
|
771
|
+
/* Firefox — native filled track */
|
|
772
|
+
.gvp-volume-slider::-moz-range-progress {
|
|
773
|
+
background: var(--gvp-accent);
|
|
774
|
+
border-radius: 99px;
|
|
775
|
+
height: 3px;
|
|
776
|
+
}
|
|
1078
777
|
/* Firefox thumb */
|
|
1079
778
|
.gvp-volume-slider::-moz-range-thumb {
|
|
1080
779
|
width: 12px; height: 12px;
|
|
@@ -1348,6 +1047,81 @@ function injectStyles() {
|
|
|
1348
1047
|
.gvp-time { font-size: 11px; }
|
|
1349
1048
|
.gvp-controls-inner { padding: 8px 10px; }
|
|
1350
1049
|
}
|
|
1050
|
+
|
|
1051
|
+
/* ── Branded context menu ─────────────────────────────────────── */
|
|
1052
|
+
.gvp-ctx-menu {
|
|
1053
|
+
position: fixed;
|
|
1054
|
+
z-index: 2147483647;
|
|
1055
|
+
min-width: 220px;
|
|
1056
|
+
background: rgba(12,12,18,0.96);
|
|
1057
|
+
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
|
1058
|
+
backdrop-filter: blur(16px) saturate(180%);
|
|
1059
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
1060
|
+
border-radius: 12px;
|
|
1061
|
+
padding: 6px 0;
|
|
1062
|
+
-webkit-box-shadow: 0 12px 40px rgba(0,0,0,0.55), 0 0 0 0.5px rgba(255,255,255,0.04);
|
|
1063
|
+
box-shadow: 0 12px 40px rgba(0,0,0,0.55), 0 0 0 0.5px rgba(255,255,255,0.04);
|
|
1064
|
+
font-family: var(--gvp-font, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
|
|
1065
|
+
font-size: 13px;
|
|
1066
|
+
color: rgba(255,255,255,0.9);
|
|
1067
|
+
-webkit-user-select: none;
|
|
1068
|
+
-moz-user-select: none;
|
|
1069
|
+
-ms-user-select: none;
|
|
1070
|
+
user-select: none;
|
|
1071
|
+
-webkit-animation: gvp-ctx-in 0.12s cubic-bezier(0.22,1,0.36,1);
|
|
1072
|
+
animation: gvp-ctx-in 0.12s cubic-bezier(0.22,1,0.36,1);
|
|
1073
|
+
}
|
|
1074
|
+
@-webkit-keyframes gvp-ctx-in {
|
|
1075
|
+
from { opacity: 0; -webkit-transform: scale(0.95); transform: scale(0.95); }
|
|
1076
|
+
to { opacity: 1; -webkit-transform: none; transform: none; }
|
|
1077
|
+
}
|
|
1078
|
+
@keyframes gvp-ctx-in {
|
|
1079
|
+
from { opacity: 0; transform: scale(0.95); }
|
|
1080
|
+
to { opacity: 1; transform: none; }
|
|
1081
|
+
}
|
|
1082
|
+
.gvp-ctx-header {
|
|
1083
|
+
display: -webkit-box; display: -ms-flexbox; display: flex;
|
|
1084
|
+
-webkit-box-align: center; -ms-flex-align: center; align-items: center;
|
|
1085
|
+
gap: 8px;
|
|
1086
|
+
padding: 8px 14px 8px 12px;
|
|
1087
|
+
text-decoration: none;
|
|
1088
|
+
color: inherit;
|
|
1089
|
+
-webkit-transition: background 0.15s; transition: background 0.15s;
|
|
1090
|
+
border-radius: 8px 8px 0 0;
|
|
1091
|
+
}
|
|
1092
|
+
.gvp-ctx-header:hover { background: rgba(255,255,255,0.06); }
|
|
1093
|
+
.gvp-ctx-logo { border-radius: 4px; }
|
|
1094
|
+
.gvp-ctx-brand-name {
|
|
1095
|
+
font-weight: 600;
|
|
1096
|
+
color: var(--gvp-accent);
|
|
1097
|
+
white-space: nowrap;
|
|
1098
|
+
}
|
|
1099
|
+
.gvp-ctx-tag {
|
|
1100
|
+
margin-left: auto;
|
|
1101
|
+
font-size: 10px;
|
|
1102
|
+
color: rgba(255,255,255,0.3);
|
|
1103
|
+
white-space: nowrap;
|
|
1104
|
+
}
|
|
1105
|
+
.gvp-ctx-sep {
|
|
1106
|
+
height: 1px;
|
|
1107
|
+
margin: 4px 10px;
|
|
1108
|
+
background: rgba(255,255,255,0.07);
|
|
1109
|
+
}
|
|
1110
|
+
.gvp-ctx-item {
|
|
1111
|
+
display: -webkit-box; display: -ms-flexbox; display: flex;
|
|
1112
|
+
-webkit-box-align: center; -ms-flex-align: center; align-items: center;
|
|
1113
|
+
gap: 8px;
|
|
1114
|
+
padding: 7px 14px 7px 12px;
|
|
1115
|
+
cursor: pointer;
|
|
1116
|
+
-webkit-transition: background 0.15s; transition: background 0.15s;
|
|
1117
|
+
}
|
|
1118
|
+
.gvp-ctx-item:hover { background: rgba(255,255,255,0.06); }
|
|
1119
|
+
.gvp-ctx-item-icon { border-radius: 2px; }
|
|
1120
|
+
.gvp-ctx-version {
|
|
1121
|
+
padding: 4px 14px 6px 12px;
|
|
1122
|
+
font-size: 10px;
|
|
1123
|
+
color: rgba(255,255,255,0.25);
|
|
1124
|
+
}
|
|
1351
1125
|
`;
|
|
1352
1126
|
const tag = document.createElement('style');
|
|
1353
1127
|
tag.setAttribute('data-guardvideo', 'player-ui-styles-v2');
|
|
@@ -1365,6 +1139,12 @@ class PlayerUI {
|
|
|
1365
1139
|
this.openMenu = null;
|
|
1366
1140
|
this.hideTimer = null;
|
|
1367
1141
|
this.seekDragging = false;
|
|
1142
|
+
this._ctxMenu = null;
|
|
1143
|
+
this._ctxDocClickBound = () => { };
|
|
1144
|
+
this._ctxTargetBound = () => { };
|
|
1145
|
+
this._ctxKeyDownBound = () => { };
|
|
1146
|
+
this._watermarkObserver = null;
|
|
1147
|
+
this._watermarkText = '';
|
|
1368
1148
|
const accent = config.branding?.accentColor ?? '#00e5a0';
|
|
1369
1149
|
const brandName = config.branding?.name ?? 'GuardVideo';
|
|
1370
1150
|
injectStyles();
|
|
@@ -1512,11 +1292,13 @@ class PlayerUI {
|
|
|
1512
1292
|
};
|
|
1513
1293
|
this._seekTouchEndBound = () => this._endSeekDrag();
|
|
1514
1294
|
this._wireEvents(videoId, config);
|
|
1295
|
+
requestAnimationFrame(() => this._updateVolSliderFill(1));
|
|
1515
1296
|
if (config.forensicWatermark !== false) {
|
|
1516
1297
|
const wmText = config.viewerEmail || config.viewerName || '';
|
|
1517
1298
|
if (wmText)
|
|
1518
1299
|
this._renderWatermark(wmText);
|
|
1519
1300
|
}
|
|
1301
|
+
this._applySecurity(config);
|
|
1520
1302
|
}
|
|
1521
1303
|
_hexToRgba(hex, alpha) {
|
|
1522
1304
|
const clean = hex.replace('#', '');
|
|
@@ -1565,6 +1347,7 @@ class PlayerUI {
|
|
|
1565
1347
|
this._onStateChange(state);
|
|
1566
1348
|
config.onStateChange?.(state);
|
|
1567
1349
|
},
|
|
1350
|
+
onWatermark: (text) => this._renderWatermark(text),
|
|
1568
1351
|
});
|
|
1569
1352
|
video.addEventListener('timeupdate', () => {
|
|
1570
1353
|
config.onTimeUpdate?.(video.currentTime);
|
|
@@ -1600,6 +1383,7 @@ class PlayerUI {
|
|
|
1600
1383
|
this.volSlider.addEventListener('input', () => {
|
|
1601
1384
|
video.volume = parseFloat(this.volSlider.value);
|
|
1602
1385
|
video.muted = video.volume === 0;
|
|
1386
|
+
this._updateVolSliderFill(video.volume);
|
|
1603
1387
|
});
|
|
1604
1388
|
this.seekWrap.addEventListener('mousedown', (e) => {
|
|
1605
1389
|
e.preventDefault();
|
|
@@ -1717,6 +1501,14 @@ class PlayerUI {
|
|
|
1717
1501
|
this.playBtn.title = `${label} (k)`;
|
|
1718
1502
|
}
|
|
1719
1503
|
_toggleMute() { this.videoEl.muted = !this.videoEl.muted; }
|
|
1504
|
+
_updateVolSliderFill(vol) {
|
|
1505
|
+
const accent = getComputedStyle(this.root).getPropertyValue('--gvp-accent').trim() || '#00e5a0';
|
|
1506
|
+
const pct = Math.round(vol * 100);
|
|
1507
|
+
this.volSlider.style.background =
|
|
1508
|
+
`linear-gradient(to right,` +
|
|
1509
|
+
` ${accent} 0%, ${accent} ${pct}%,` +
|
|
1510
|
+
` rgba(255,255,255,0.18) ${pct}%, rgba(255,255,255,0.18) 100%)`;
|
|
1511
|
+
}
|
|
1720
1512
|
_onVolumeChange() {
|
|
1721
1513
|
const v = this.videoEl;
|
|
1722
1514
|
const vol = v.muted ? 0 : v.volume;
|
|
@@ -1727,6 +1519,7 @@ class PlayerUI {
|
|
|
1727
1519
|
this.volBtn.setAttribute('aria-label', muted ? 'Unmute' : 'Mute');
|
|
1728
1520
|
this.volBtn.title = muted ? 'Unmute (m)' : 'Mute (m)';
|
|
1729
1521
|
this.volSlider.value = String(vol);
|
|
1522
|
+
this._updateVolSliderFill(vol);
|
|
1730
1523
|
}
|
|
1731
1524
|
_startSeekDrag() {
|
|
1732
1525
|
this.seekDragging = true;
|
|
@@ -1913,6 +1706,9 @@ class PlayerUI {
|
|
|
1913
1706
|
this.controls.classList.add('gvp-hidden');
|
|
1914
1707
|
}
|
|
1915
1708
|
_renderWatermark(text) {
|
|
1709
|
+
if (!text)
|
|
1710
|
+
return;
|
|
1711
|
+
this._watermarkText = text;
|
|
1916
1712
|
this.watermarkDiv.innerHTML = '';
|
|
1917
1713
|
for (let i = 0; i < 20; i++) {
|
|
1918
1714
|
const span = el('span', 'gvp-watermark-text');
|
|
@@ -1921,6 +1717,40 @@ class PlayerUI {
|
|
|
1921
1717
|
span.style.top = `${Math.floor(i / 4) * 22}%`;
|
|
1922
1718
|
this.watermarkDiv.appendChild(span);
|
|
1923
1719
|
}
|
|
1720
|
+
this._mountWatermarkObserver();
|
|
1721
|
+
}
|
|
1722
|
+
_mountWatermarkObserver() {
|
|
1723
|
+
this._watermarkObserver?.disconnect();
|
|
1724
|
+
this._watermarkObserver = new MutationObserver((mutations) => {
|
|
1725
|
+
for (const m of mutations) {
|
|
1726
|
+
if (m.type === 'childList' && m.target === this.root) {
|
|
1727
|
+
if (!this.root.contains(this.watermarkDiv)) {
|
|
1728
|
+
this.root.appendChild(this.watermarkDiv);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
if (m.type === 'attributes' && m.target === this.watermarkDiv) {
|
|
1732
|
+
this.watermarkDiv.removeAttribute('style');
|
|
1733
|
+
this.watermarkDiv.className = 'gvp-watermark';
|
|
1734
|
+
this.watermarkDiv.setAttribute('aria-hidden', 'true');
|
|
1735
|
+
}
|
|
1736
|
+
if (m.type === 'childList' && m.target === this.watermarkDiv) {
|
|
1737
|
+
if (this.watermarkDiv.childElementCount < 20) {
|
|
1738
|
+
this._refillWatermark();
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
this._watermarkObserver.observe(this.root, { childList: true });
|
|
1744
|
+
this._watermarkObserver.observe(this.watermarkDiv, { attributes: true, childList: true });
|
|
1745
|
+
}
|
|
1746
|
+
_refillWatermark() {
|
|
1747
|
+
for (let i = this.watermarkDiv.childElementCount; i < 20; i++) {
|
|
1748
|
+
const span = el('span', 'gvp-watermark-text');
|
|
1749
|
+
span.textContent = this._watermarkText;
|
|
1750
|
+
span.style.left = `${(i % 4) * 26 + (Math.floor(i / 4) % 2) * 13}%`;
|
|
1751
|
+
span.style.top = `${Math.floor(i / 4) * 22}%`;
|
|
1752
|
+
this.watermarkDiv.appendChild(span);
|
|
1753
|
+
}
|
|
1924
1754
|
}
|
|
1925
1755
|
_addRipple(e) {
|
|
1926
1756
|
const rect = this.root.getBoundingClientRect();
|
|
@@ -1968,6 +1798,120 @@ class PlayerUI {
|
|
|
1968
1798
|
break;
|
|
1969
1799
|
}
|
|
1970
1800
|
}
|
|
1801
|
+
_applySecurity(config) {
|
|
1802
|
+
const sec = config.security;
|
|
1803
|
+
this.root.style.userSelect = 'none';
|
|
1804
|
+
this.root.style.webkitUserSelect = 'none';
|
|
1805
|
+
this.root.style.msUserSelect = 'none';
|
|
1806
|
+
if (sec?.disableDrag !== false) {
|
|
1807
|
+
this.videoEl.draggable = false;
|
|
1808
|
+
this.videoEl.addEventListener('dragstart', (e) => e.preventDefault());
|
|
1809
|
+
}
|
|
1810
|
+
if (sec?.blockDevTools) {
|
|
1811
|
+
this._ctxKeyDownBound = (e) => {
|
|
1812
|
+
if (e.key === 'F12') {
|
|
1813
|
+
e.preventDefault();
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
if (e.ctrlKey && e.shiftKey && ['I', 'J', 'C'].includes(e.key.toUpperCase())) {
|
|
1817
|
+
e.preventDefault();
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
if (e.ctrlKey && e.key.toUpperCase() === 'U')
|
|
1821
|
+
e.preventDefault();
|
|
1822
|
+
};
|
|
1823
|
+
document.addEventListener('keydown', this._ctxKeyDownBound);
|
|
1824
|
+
}
|
|
1825
|
+
this._ctxDocClickBound = () => this._hideContextMenu();
|
|
1826
|
+
document.addEventListener('click', this._ctxDocClickBound);
|
|
1827
|
+
this._ctxTargetBound = (e) => {
|
|
1828
|
+
e.preventDefault();
|
|
1829
|
+
e.stopPropagation();
|
|
1830
|
+
if (sec?.disableRightClick)
|
|
1831
|
+
return;
|
|
1832
|
+
const me = e;
|
|
1833
|
+
this._showContextMenu(me.clientX, me.clientY, config);
|
|
1834
|
+
};
|
|
1835
|
+
this.root.addEventListener('contextmenu', this._ctxTargetBound);
|
|
1836
|
+
}
|
|
1837
|
+
_showContextMenu(x, y, config) {
|
|
1838
|
+
this._hideContextMenu();
|
|
1839
|
+
const br = config.branding;
|
|
1840
|
+
const name = br?.name ?? 'GuardVideo';
|
|
1841
|
+
const url = br?.url ?? 'https://guardvid.com';
|
|
1842
|
+
const logoUrl = br?.logoUrl ?? '';
|
|
1843
|
+
const accent = br?.accentColor ?? '#00e5a0';
|
|
1844
|
+
const extras = config.contextMenuItems ?? [];
|
|
1845
|
+
const menu = el('div', 'gvp-ctx-menu');
|
|
1846
|
+
menu.setAttribute('role', 'menu');
|
|
1847
|
+
menu.style.setProperty('--gvp-accent', accent);
|
|
1848
|
+
const header = document.createElement('a');
|
|
1849
|
+
header.className = 'gvp-ctx-header';
|
|
1850
|
+
header.href = url;
|
|
1851
|
+
header.target = '_blank';
|
|
1852
|
+
header.rel = 'noopener noreferrer';
|
|
1853
|
+
header.setAttribute('role', 'menuitem');
|
|
1854
|
+
if (logoUrl) {
|
|
1855
|
+
const logo = el('img', 'gvp-ctx-logo');
|
|
1856
|
+
logo.src = logoUrl;
|
|
1857
|
+
logo.alt = name;
|
|
1858
|
+
logo.width = 20;
|
|
1859
|
+
logo.height = 20;
|
|
1860
|
+
header.appendChild(logo);
|
|
1861
|
+
}
|
|
1862
|
+
else {
|
|
1863
|
+
header.appendChild(svgEl(ICON.shield, 18, 18));
|
|
1864
|
+
}
|
|
1865
|
+
const nameSpan = el('span', 'gvp-ctx-brand-name');
|
|
1866
|
+
nameSpan.textContent = name;
|
|
1867
|
+
const tagSpan = el('span', 'gvp-ctx-tag');
|
|
1868
|
+
tagSpan.textContent = 'Secure Video Player';
|
|
1869
|
+
header.append(nameSpan, tagSpan);
|
|
1870
|
+
menu.appendChild(header);
|
|
1871
|
+
extras.forEach((item) => {
|
|
1872
|
+
if (item.separator) {
|
|
1873
|
+
menu.appendChild(el('div', 'gvp-ctx-sep'));
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
const row = el('div', 'gvp-ctx-item');
|
|
1877
|
+
row.setAttribute('role', 'menuitem');
|
|
1878
|
+
row.setAttribute('tabindex', '0');
|
|
1879
|
+
if (item.icon) {
|
|
1880
|
+
const ico = el('img', 'gvp-ctx-item-icon');
|
|
1881
|
+
ico.src = item.icon;
|
|
1882
|
+
ico.width = 14;
|
|
1883
|
+
ico.height = 14;
|
|
1884
|
+
row.appendChild(ico);
|
|
1885
|
+
}
|
|
1886
|
+
const lbl = el('span');
|
|
1887
|
+
lbl.textContent = item.label;
|
|
1888
|
+
row.appendChild(lbl);
|
|
1889
|
+
row.addEventListener('click', (ev) => {
|
|
1890
|
+
ev.stopPropagation();
|
|
1891
|
+
this._hideContextMenu();
|
|
1892
|
+
if (item.onClick)
|
|
1893
|
+
item.onClick();
|
|
1894
|
+
else if (item.href)
|
|
1895
|
+
window.open(item.href, '_blank', 'noopener,noreferrer');
|
|
1896
|
+
});
|
|
1897
|
+
menu.appendChild(row);
|
|
1898
|
+
});
|
|
1899
|
+
menu.appendChild(el('div', 'gvp-ctx-sep'));
|
|
1900
|
+
const ver = el('div', 'gvp-ctx-version');
|
|
1901
|
+
ver.textContent = `${name} Player v1.0`;
|
|
1902
|
+
menu.appendChild(ver);
|
|
1903
|
+
document.body.appendChild(menu);
|
|
1904
|
+
const rect = menu.getBoundingClientRect();
|
|
1905
|
+
menu.style.left = `${x + rect.width > window.innerWidth ? window.innerWidth - rect.width - 8 : x}px`;
|
|
1906
|
+
menu.style.top = `${y + rect.height > window.innerHeight ? window.innerHeight - rect.height - 8 : y}px`;
|
|
1907
|
+
this._ctxMenu = menu;
|
|
1908
|
+
}
|
|
1909
|
+
_hideContextMenu() {
|
|
1910
|
+
if (this._ctxMenu) {
|
|
1911
|
+
this._ctxMenu.remove();
|
|
1912
|
+
this._ctxMenu = null;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1971
1915
|
play() { return this.corePlayer.play(); }
|
|
1972
1916
|
pause() { return this.corePlayer.pause(); }
|
|
1973
1917
|
seek(t) { return this.corePlayer.seek(t); }
|
|
@@ -1991,6 +1935,10 @@ class PlayerUI {
|
|
|
1991
1935
|
window.removeEventListener('mouseup', this._seekMouseUpBound);
|
|
1992
1936
|
window.removeEventListener('touchmove', this._seekTouchMoveBound);
|
|
1993
1937
|
window.removeEventListener('touchend', this._seekTouchEndBound);
|
|
1938
|
+
document.removeEventListener('click', this._ctxDocClickBound);
|
|
1939
|
+
document.removeEventListener('keydown', this._ctxKeyDownBound);
|
|
1940
|
+
this._hideContextMenu();
|
|
1941
|
+
this._watermarkObserver?.disconnect();
|
|
1994
1942
|
this.corePlayer.destroy();
|
|
1995
1943
|
this.root.remove();
|
|
1996
1944
|
}
|