unified-video-framework 1.4.242 → 1.4.244
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.
|
@@ -1116,13 +1116,8 @@ export class WebPlayer extends BasePlayer {
|
|
|
1116
1116
|
this.video.poster = metadata.thumbnail;
|
|
1117
1117
|
}
|
|
1118
1118
|
|
|
1119
|
-
//
|
|
1120
|
-
|
|
1121
|
-
const embedUrl = YouTubeExtractor.getEmbedUrl(videoId);
|
|
1122
|
-
|
|
1123
|
-
// Use iframe approach - this is the most reliable method
|
|
1124
|
-
// We'll load the video through a special method that extracts the stream
|
|
1125
|
-
await this.loadYouTubeIframe(videoId, embedUrl);
|
|
1119
|
+
// Create YouTube iframe player with custom controls integration
|
|
1120
|
+
await this.createYouTubePlayer(videoId);
|
|
1126
1121
|
|
|
1127
1122
|
this.debugLog('✅ YouTube video loaded successfully');
|
|
1128
1123
|
} catch (error) {
|
|
@@ -1131,99 +1126,273 @@ export class WebPlayer extends BasePlayer {
|
|
|
1131
1126
|
}
|
|
1132
1127
|
}
|
|
1133
1128
|
|
|
1134
|
-
private
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
try {
|
|
1138
|
-
// First, try to load using YouTube's nocookie embed with special handling
|
|
1139
|
-
// This approach uses a proxy service to get direct MP4 URLs
|
|
1140
|
-
const proxyUrl = `https://www.youtube.com/watch?v=${videoId}`;
|
|
1141
|
-
|
|
1142
|
-
// Attempt 1: Try using a CORS proxy to fetch the page and extract HLS URL
|
|
1143
|
-
try {
|
|
1144
|
-
const response = await fetch(`https://cors-anywhere.herokuapp.com/${proxyUrl}`, {
|
|
1145
|
-
method: 'GET',
|
|
1146
|
-
headers: {
|
|
1147
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
|
|
1148
|
-
}
|
|
1149
|
-
});
|
|
1150
|
-
|
|
1151
|
-
if (response.ok) {
|
|
1152
|
-
const html = await response.text();
|
|
1153
|
-
// Try to extract streamingData from the HTML
|
|
1154
|
-
const match = html.match(/"streamingData"\s*:\s*\{([^}]*"url"[^}]*)/);
|
|
1155
|
-
if (match) {
|
|
1156
|
-
this.debugLog('✅ Found HLS stream in YouTube page');
|
|
1157
|
-
// Continue with fallback
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
} catch (proxyError) {
|
|
1161
|
-
this.debugWarn('CORS proxy attempt failed, trying fallback:', proxyError);
|
|
1162
|
-
}
|
|
1129
|
+
private youtubePlayer: any = null;
|
|
1130
|
+
private youtubePlayerReady: boolean = false;
|
|
1131
|
+
private youtubeIframe: HTMLIFrameElement | null = null;
|
|
1163
1132
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
<source
|
|
1169
|
-
src="https://www.youtube.com/embed/${videoId}?fs=1"
|
|
1170
|
-
type="text/html"
|
|
1171
|
-
/>
|
|
1172
|
-
`;
|
|
1173
|
-
|
|
1174
|
-
// Alternative: Create iframe overlay and sync with player
|
|
1175
|
-
this.createYouTubeIframeProxy(videoId);
|
|
1176
|
-
} catch (error) {
|
|
1177
|
-
this.debugWarn('YouTube iframe loading fallback:', error);
|
|
1178
|
-
// Final fallback: Use the standard YouTube URL (won't work with custom controls)
|
|
1179
|
-
// but at least something will play
|
|
1180
|
-
if (this.video) {
|
|
1181
|
-
// For the custom player UI to work, we need actual video stream
|
|
1182
|
-
// If neither method works, inform user
|
|
1183
|
-
throw new Error(
|
|
1184
|
-
'YouTube embedding requires either: 1) Backend service to extract streams, or 2) YouTube API key configuration. ' +
|
|
1185
|
-
'See documentation for setup instructions.'
|
|
1186
|
-
);
|
|
1187
|
-
}
|
|
1133
|
+
private async createYouTubePlayer(videoId: string): Promise<void> {
|
|
1134
|
+
const container = this.playerWrapper || this.video?.parentElement;
|
|
1135
|
+
if (!container) {
|
|
1136
|
+
throw new Error('No container found for YouTube player');
|
|
1188
1137
|
}
|
|
1189
|
-
}
|
|
1190
1138
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
if (!container) return;
|
|
1139
|
+
// Hide the regular video element
|
|
1140
|
+
if (this.video) {
|
|
1141
|
+
this.video.style.display = 'none';
|
|
1142
|
+
}
|
|
1196
1143
|
|
|
1197
|
-
// Create iframe
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1144
|
+
// Create iframe container
|
|
1145
|
+
const iframeContainer = document.createElement('div');
|
|
1146
|
+
iframeContainer.id = `youtube-player-${videoId}`;
|
|
1147
|
+
iframeContainer.style.cssText = `
|
|
1201
1148
|
position: absolute;
|
|
1202
1149
|
top: 0;
|
|
1203
1150
|
left: 0;
|
|
1204
1151
|
width: 100%;
|
|
1205
1152
|
height: 100%;
|
|
1206
|
-
border: none;
|
|
1207
1153
|
z-index: 1;
|
|
1208
|
-
opacity: 0;
|
|
1209
|
-
pointer-events: none;
|
|
1210
1154
|
`;
|
|
1211
|
-
|
|
1212
|
-
iframe.src = `https://www.youtube.com/embed/${videoId}?enablejsapi=1&modestbranding=1&rel=0&controls=0`;
|
|
1213
|
-
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
|
|
1214
1155
|
|
|
1215
|
-
|
|
1156
|
+
// Remove existing YouTube player if any
|
|
1157
|
+
const existingPlayer = container.querySelector(`#youtube-player-${videoId}`);
|
|
1158
|
+
if (existingPlayer) {
|
|
1159
|
+
existingPlayer.remove();
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
container.appendChild(iframeContainer);
|
|
1216
1163
|
|
|
1217
|
-
// Load YouTube
|
|
1164
|
+
// Load YouTube IFrame API if not loaded
|
|
1218
1165
|
if (!window.YT) {
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1166
|
+
await this.loadYouTubeAPI();
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// Wait for API to be ready
|
|
1170
|
+
await this.waitForYouTubeAPI();
|
|
1171
|
+
|
|
1172
|
+
// Create YouTube player
|
|
1173
|
+
this.youtubePlayer = new window.YT.Player(iframeContainer.id, {
|
|
1174
|
+
videoId: videoId,
|
|
1175
|
+
width: '100%',
|
|
1176
|
+
height: '100%',
|
|
1177
|
+
playerVars: {
|
|
1178
|
+
controls: 0, // Hide YouTube controls
|
|
1179
|
+
disablekb: 1, // Disable keyboard controls
|
|
1180
|
+
fs: 0, // Hide fullscreen button
|
|
1181
|
+
iv_load_policy: 3, // Hide annotations
|
|
1182
|
+
modestbranding: 1, // Minimal YouTube branding
|
|
1183
|
+
rel: 0, // Don't show related videos
|
|
1184
|
+
showinfo: 0, // Hide video info
|
|
1185
|
+
autoplay: this.config.autoPlay ? 1 : 0,
|
|
1186
|
+
mute: this.config.muted ? 1 : 0
|
|
1187
|
+
},
|
|
1188
|
+
events: {
|
|
1189
|
+
onReady: () => this.onYouTubePlayerReady(),
|
|
1190
|
+
onStateChange: (event: any) => this.onYouTubePlayerStateChange(event),
|
|
1191
|
+
onError: (event: any) => this.onYouTubePlayerError(event)
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
this.debugLog('YouTube player created');
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
private async loadYouTubeAPI(): Promise<void> {
|
|
1199
|
+
return new Promise((resolve) => {
|
|
1200
|
+
if (window.YT) {
|
|
1201
|
+
resolve();
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Set up the callback for when API loads
|
|
1206
|
+
(window as any).onYouTubeIframeAPIReady = () => {
|
|
1207
|
+
this.debugLog('YouTube IFrame API loaded');
|
|
1208
|
+
resolve();
|
|
1209
|
+
};
|
|
1210
|
+
|
|
1211
|
+
// Load the API script
|
|
1212
|
+
const script = document.createElement('script');
|
|
1213
|
+
script.src = 'https://www.youtube.com/iframe_api';
|
|
1214
|
+
script.async = true;
|
|
1215
|
+
document.body.appendChild(script);
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
private async waitForYouTubeAPI(): Promise<void> {
|
|
1220
|
+
return new Promise((resolve) => {
|
|
1221
|
+
const checkAPI = () => {
|
|
1222
|
+
if (window.YT && window.YT.Player) {
|
|
1223
|
+
resolve();
|
|
1224
|
+
} else {
|
|
1225
|
+
setTimeout(checkAPI, 100);
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
checkAPI();
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
private onYouTubePlayerReady(): void {
|
|
1233
|
+
this.youtubePlayerReady = true;
|
|
1234
|
+
this.debugLog('YouTube player ready');
|
|
1235
|
+
|
|
1236
|
+
// Set initial volume
|
|
1237
|
+
if (this.youtubePlayer) {
|
|
1238
|
+
const volume = this.config.volume ? this.config.volume * 100 : 100;
|
|
1239
|
+
this.youtubePlayer.setVolume(volume);
|
|
1240
|
+
|
|
1241
|
+
if (this.config.muted) {
|
|
1242
|
+
this.youtubePlayer.mute();
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Start time tracking
|
|
1247
|
+
this.startYouTubeTimeTracking();
|
|
1248
|
+
|
|
1249
|
+
this.emit('onReady');
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
private onYouTubePlayerStateChange(event: any): void {
|
|
1253
|
+
const state = event.data;
|
|
1254
|
+
|
|
1255
|
+
switch (state) {
|
|
1256
|
+
case window.YT.PlayerState.PLAYING:
|
|
1257
|
+
this.state.isPlaying = true;
|
|
1258
|
+
this.state.isPaused = false;
|
|
1259
|
+
this.state.isBuffering = false;
|
|
1260
|
+
this.updateYouTubeUI('playing');
|
|
1261
|
+
this.emit('onPlay');
|
|
1262
|
+
break;
|
|
1263
|
+
|
|
1264
|
+
case window.YT.PlayerState.PAUSED:
|
|
1265
|
+
this.state.isPlaying = false;
|
|
1266
|
+
this.state.isPaused = true;
|
|
1267
|
+
this.state.isBuffering = false;
|
|
1268
|
+
this.updateYouTubeUI('paused');
|
|
1269
|
+
this.emit('onPause');
|
|
1270
|
+
break;
|
|
1271
|
+
|
|
1272
|
+
case window.YT.PlayerState.BUFFERING:
|
|
1273
|
+
this.state.isBuffering = true;
|
|
1274
|
+
this.updateYouTubeUI('buffering');
|
|
1275
|
+
this.emit('onBuffering', true);
|
|
1276
|
+
break;
|
|
1277
|
+
|
|
1278
|
+
case window.YT.PlayerState.ENDED:
|
|
1279
|
+
this.state.isPlaying = false;
|
|
1280
|
+
this.state.isPaused = true;
|
|
1281
|
+
this.state.isEnded = true;
|
|
1282
|
+
this.updateYouTubeUI('ended');
|
|
1283
|
+
this.emit('onEnded');
|
|
1284
|
+
break;
|
|
1285
|
+
|
|
1286
|
+
case window.YT.PlayerState.CUED:
|
|
1287
|
+
this.state.duration = this.youtubePlayer.getDuration();
|
|
1288
|
+
this.updateYouTubeUI('cued');
|
|
1289
|
+
break;
|
|
1222
1290
|
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
private updateYouTubeUI(state: string): void {
|
|
1294
|
+
const playIcon = document.getElementById('uvf-play-icon');
|
|
1295
|
+
const pauseIcon = document.getElementById('uvf-pause-icon');
|
|
1296
|
+
const centerPlay = document.getElementById('uvf-center-play');
|
|
1297
|
+
|
|
1298
|
+
if (state === 'playing' || state === 'buffering') {
|
|
1299
|
+
if (playIcon) playIcon.style.display = 'none';
|
|
1300
|
+
if (pauseIcon) pauseIcon.style.display = 'block';
|
|
1301
|
+
if (centerPlay) centerPlay.classList.add('hidden');
|
|
1302
|
+
} else if (state === 'paused' || state === 'cued' || state === 'ended') {
|
|
1303
|
+
if (playIcon) playIcon.style.display = 'block';
|
|
1304
|
+
if (pauseIcon) pauseIcon.style.display = 'none';
|
|
1305
|
+
if (centerPlay) centerPlay.classList.remove('hidden');
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
private onYouTubePlayerError(event: any): void {
|
|
1310
|
+
const errorCode = event.data;
|
|
1311
|
+
let errorMessage = 'YouTube player error';
|
|
1312
|
+
|
|
1313
|
+
switch (errorCode) {
|
|
1314
|
+
case 2:
|
|
1315
|
+
errorMessage = 'Invalid video ID';
|
|
1316
|
+
break;
|
|
1317
|
+
case 5:
|
|
1318
|
+
errorMessage = 'HTML5 player error';
|
|
1319
|
+
break;
|
|
1320
|
+
case 100:
|
|
1321
|
+
errorMessage = 'Video not found or private';
|
|
1322
|
+
break;
|
|
1323
|
+
case 101:
|
|
1324
|
+
case 150:
|
|
1325
|
+
errorMessage = 'Video cannot be embedded';
|
|
1326
|
+
break;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
this.handleError({
|
|
1330
|
+
code: 'YOUTUBE_ERROR',
|
|
1331
|
+
message: errorMessage,
|
|
1332
|
+
type: 'media',
|
|
1333
|
+
fatal: true,
|
|
1334
|
+
details: { errorCode }
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
private youtubeTimeTrackingInterval: NodeJS.Timeout | null = null;
|
|
1339
|
+
|
|
1340
|
+
private startYouTubeTimeTracking(): void {
|
|
1341
|
+
if (this.youtubeTimeTrackingInterval) {
|
|
1342
|
+
clearInterval(this.youtubeTimeTrackingInterval);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
this.youtubeTimeTrackingInterval = setInterval(() => {
|
|
1346
|
+
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
1347
|
+
try {
|
|
1348
|
+
const currentTime = this.youtubePlayer.getCurrentTime();
|
|
1349
|
+
const duration = this.youtubePlayer.getDuration();
|
|
1350
|
+
const buffered = this.youtubePlayer.getVideoLoadedFraction() * 100;
|
|
1351
|
+
|
|
1352
|
+
this.state.currentTime = currentTime || 0;
|
|
1353
|
+
this.state.duration = duration || 0;
|
|
1354
|
+
this.state.bufferedPercentage = buffered || 0;
|
|
1355
|
+
|
|
1356
|
+
// Update UI progress bar
|
|
1357
|
+
this.updateYouTubeProgressBar(currentTime, duration, buffered);
|
|
1358
|
+
|
|
1359
|
+
this.emit('onTimeUpdate', this.state.currentTime);
|
|
1360
|
+
this.emit('onProgress', this.state.bufferedPercentage);
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
// Ignore errors during tracking
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}, 250); // Update every 250ms
|
|
1366
|
+
}
|
|
1223
1367
|
|
|
1224
|
-
|
|
1368
|
+
private updateYouTubeProgressBar(currentTime: number, duration: number, buffered: number): void {
|
|
1369
|
+
if (!duration || duration === 0) return;
|
|
1370
|
+
|
|
1371
|
+
const percent = (currentTime / duration) * 100;
|
|
1372
|
+
|
|
1373
|
+
// Update progress filled
|
|
1374
|
+
const progressFilled = document.getElementById('uvf-progress-filled') as HTMLElement;
|
|
1375
|
+
if (progressFilled && !this.isDragging) {
|
|
1376
|
+
progressFilled.style.width = percent + '%';
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// Update progress handle
|
|
1380
|
+
const progressHandle = document.getElementById('uvf-progress-handle') as HTMLElement;
|
|
1381
|
+
if (progressHandle && !this.isDragging) {
|
|
1382
|
+
progressHandle.style.left = percent + '%';
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// Update buffered progress
|
|
1386
|
+
const progressBuffered = document.getElementById('uvf-progress-buffered') as HTMLElement;
|
|
1387
|
+
if (progressBuffered) {
|
|
1388
|
+
progressBuffered.style.width = buffered + '%';
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// Update time display
|
|
1392
|
+
this.updateTimeDisplay();
|
|
1225
1393
|
}
|
|
1226
1394
|
|
|
1395
|
+
|
|
1227
1396
|
protected loadScript(src: string): Promise<void> {
|
|
1228
1397
|
return new Promise((resolve, reject) => {
|
|
1229
1398
|
const script = document.createElement('script');
|
|
@@ -1708,6 +1877,12 @@ export class WebPlayer extends BasePlayer {
|
|
|
1708
1877
|
}
|
|
1709
1878
|
|
|
1710
1879
|
async play(): Promise<void> {
|
|
1880
|
+
// Handle YouTube player
|
|
1881
|
+
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
1882
|
+
this.youtubePlayer.playVideo();
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1711
1886
|
if (!this.video) throw new Error('Video element not initialized');
|
|
1712
1887
|
|
|
1713
1888
|
// Prevent playback when in fallback poster mode (no valid sources)
|
|
@@ -1773,6 +1948,12 @@ export class WebPlayer extends BasePlayer {
|
|
|
1773
1948
|
}
|
|
1774
1949
|
|
|
1775
1950
|
pause(): void {
|
|
1951
|
+
// Handle YouTube player
|
|
1952
|
+
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
1953
|
+
this.youtubePlayer.pauseVideo();
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1776
1957
|
if (!this.video) return;
|
|
1777
1958
|
|
|
1778
1959
|
const now = Date.now();
|
|
@@ -1840,6 +2021,14 @@ export class WebPlayer extends BasePlayer {
|
|
|
1840
2021
|
}
|
|
1841
2022
|
|
|
1842
2023
|
seek(time: number): void {
|
|
2024
|
+
// Handle YouTube player
|
|
2025
|
+
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
2026
|
+
this.youtubePlayer.seekTo(time, true);
|
|
2027
|
+
this.emit('onSeeking');
|
|
2028
|
+
setTimeout(() => this.emit('onSeeked'), 500);
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
1843
2032
|
if (!this.video) return;
|
|
1844
2033
|
|
|
1845
2034
|
// Validate input time
|
|
@@ -1866,18 +2055,34 @@ export class WebPlayer extends BasePlayer {
|
|
|
1866
2055
|
}
|
|
1867
2056
|
|
|
1868
2057
|
setVolume(level: number): void {
|
|
2058
|
+
// Handle YouTube player
|
|
2059
|
+
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
2060
|
+
const volumePercent = level * 100;
|
|
2061
|
+
this.youtubePlayer.setVolume(volumePercent);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
1869
2064
|
if (!this.video) return;
|
|
1870
2065
|
this.video.volume = Math.max(0, Math.min(1, level));
|
|
1871
2066
|
super.setVolume(level);
|
|
1872
2067
|
}
|
|
1873
2068
|
|
|
1874
2069
|
mute(): void {
|
|
2070
|
+
// Handle YouTube player
|
|
2071
|
+
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
2072
|
+
this.youtubePlayer.mute();
|
|
2073
|
+
}
|
|
2074
|
+
|
|
1875
2075
|
if (!this.video) return;
|
|
1876
2076
|
this.video.muted = true;
|
|
1877
2077
|
super.mute();
|
|
1878
2078
|
}
|
|
1879
2079
|
|
|
1880
2080
|
unmute(): void {
|
|
2081
|
+
// Handle YouTube player
|
|
2082
|
+
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
2083
|
+
this.youtubePlayer.unMute();
|
|
2084
|
+
}
|
|
2085
|
+
|
|
1881
2086
|
if (!this.video) return;
|
|
1882
2087
|
this.video.muted = false;
|
|
1883
2088
|
super.unmute();
|
|
@@ -1885,6 +2090,11 @@ export class WebPlayer extends BasePlayer {
|
|
|
1885
2090
|
|
|
1886
2091
|
|
|
1887
2092
|
getCurrentTime(): number {
|
|
2093
|
+
// Handle YouTube player
|
|
2094
|
+
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
2095
|
+
return this.youtubePlayer.getCurrentTime();
|
|
2096
|
+
}
|
|
2097
|
+
|
|
1888
2098
|
if (this.video && typeof this.video.currentTime === 'number') {
|
|
1889
2099
|
return this.video.currentTime;
|
|
1890
2100
|
}
|
|
@@ -6591,6 +6801,9 @@ export class WebPlayer extends BasePlayer {
|
|
|
6591
6801
|
const fullscreenBtn = document.getElementById('uvf-fullscreen-btn');
|
|
6592
6802
|
const settingsBtn = document.getElementById('uvf-settings-btn');
|
|
6593
6803
|
|
|
6804
|
+
// Get the event target (video element or YouTube player)
|
|
6805
|
+
const getEventTarget = () => this.youtubePlayer && this.youtubePlayerReady ? this.youtubePlayer : this.video;
|
|
6806
|
+
|
|
6594
6807
|
// Disable right-click context menu
|
|
6595
6808
|
this.video.addEventListener('contextmenu', (e) => {
|
|
6596
6809
|
e.preventDefault();
|