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