myetv-player 1.7.0 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +461 -154
- package/css/myetv-player.css +36 -0
- package/css/myetv-player.min.css +1 -1
- package/dist/myetv-player.js +415 -89
- package/dist/myetv-player.min.js +338 -60
- package/package.json +2 -1
- package/plugins/autosub/myetv-autosub-plugin.js +545 -188
- package/plugins/autosub/readme.md +156 -66
- package/plugins/radio/api.php +154 -0
- package/plugins/radio/icons/bolt-lightning-solid.png +0 -0
- package/plugins/radio/icons/compact-disc-solid.png +0 -0
- package/plugins/radio/icons/guitar-solid.png +0 -0
- package/plugins/radio/icons/headphones-solid.png +0 -0
- package/plugins/radio/icons/medal-solid.png +0 -0
- package/plugins/radio/icons/music-solid.png +0 -0
- package/plugins/radio/icons/newspaper-solid.png +0 -0
- package/plugins/radio/icons/radio-solid.png +0 -0
- package/plugins/radio/icons/readme.md +1 -0
- package/plugins/radio/myetv-radio-plugin.js +1655 -0
- package/plugins/radio/php/data/readme.md +1 -0
- package/plugins/radio/php/radio_stats.php +203 -0
- package/plugins/radio/proxy.php +26 -0
- package/plugins/radio/readme.md +176 -0
- package/plugins/radio/station_example.json +47 -0
- package/plugins/video-loading/myetv-video-loading-plugin.js +157 -0
- package/plugins/video-loading/readme.md +87 -0
package/dist/myetv-player.js
CHANGED
|
@@ -674,6 +674,9 @@ constructor(videoElement, options = {}) {
|
|
|
674
674
|
showPictureInPicture: true, // Enable PiP button
|
|
675
675
|
showSubtitles: true, // Enable subtitles button
|
|
676
676
|
subtitlesEnabled: false, // Enable subtitles by default if available
|
|
677
|
+
externalSubtitleTracks: [],// external subtitles track from url
|
|
678
|
+
externalSubtitleLabel: 'Default Subtitles', //label for the subtitle track url
|
|
679
|
+
externalSubtitleLang: 'en', // language of the subtitles track url
|
|
677
680
|
showSettingsMenu: true, // Show settings menu in top bar
|
|
678
681
|
autoHide: true, // auto-hide controls when idle
|
|
679
682
|
autoHideDelay: 3000, // hide controls after ... seconds of inactivity (specificed in milliseconds)
|
|
@@ -700,6 +703,7 @@ constructor(videoElement, options = {}) {
|
|
|
700
703
|
brandLogoUrl: '', // URL for brand logo image
|
|
701
704
|
brandLogoLinkUrl: '', // Optional URL to open when clicking the logo
|
|
702
705
|
brandLogoTooltipText: '', // Tooltip text for brand logo
|
|
706
|
+
loadingLogo: null, // URL image to show inside the loading circle
|
|
703
707
|
playlistEnabled: true, // Enable/disable playlist detection
|
|
704
708
|
playlistAutoPlay: true, // Auto-play next video when current ends
|
|
705
709
|
playlistLoop: false, // Loop playlist when reaching the end
|
|
@@ -840,9 +844,33 @@ constructor(videoElement, options = {}) {
|
|
|
840
844
|
if (this.options.language && this.isI18nAvailable()) {
|
|
841
845
|
VideoPlayerTranslations.setLanguage(this.options.language);
|
|
842
846
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
847
|
+
|
|
848
|
+
this.savedAutoplayIntent = options.autoplay || false;
|
|
849
|
+
this.options.autoplay = false;
|
|
850
|
+
|
|
851
|
+
if (this.video) {
|
|
852
|
+
if (this.video.hasAttribute('autoplay')) {
|
|
853
|
+
this.savedAutoplayIntent = true;
|
|
854
|
+
this.video.removeAttribute('autoplay');
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Physically stop the browser engine from pre-loading and playing
|
|
858
|
+
this.video.pause();
|
|
859
|
+
this.video.preload = 'none';
|
|
860
|
+
|
|
861
|
+
if (this.video.hasAttribute('poster')) {
|
|
862
|
+
this.savedPoster = this.video.getAttribute('poster');
|
|
863
|
+
this.video.setAttribute('poster', 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7');
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
this.video.pause();
|
|
867
|
+
this.video.preload = 'none';
|
|
868
|
+
|
|
869
|
+
this.video.removeAttribute('src');
|
|
870
|
+
this.video.load();
|
|
871
|
+
|
|
872
|
+
this.video.style.opacity = '0';
|
|
873
|
+
this.video.style.visibility = 'hidden';
|
|
846
874
|
}
|
|
847
875
|
|
|
848
876
|
try {
|
|
@@ -934,17 +962,8 @@ constructor(videoElement, options = {}) {
|
|
|
934
962
|
this.updateVolumeSliderVisual();
|
|
935
963
|
this.initVolumeTooltip();
|
|
936
964
|
this.updateTooltips();
|
|
937
|
-
this.markPlayerReady();
|
|
938
|
-
this.initializePluginSystem();
|
|
939
|
-
this.restoreSourcesAsync();
|
|
940
965
|
|
|
941
|
-
this.
|
|
942
|
-
this.initializeQualityMonitoring();
|
|
943
|
-
|
|
944
|
-
this.initializeResolution();
|
|
945
|
-
this.initializeChapters();
|
|
946
|
-
this.initializePoster();
|
|
947
|
-
this.initializeWatermark();
|
|
966
|
+
this.initializePluginSystem();
|
|
948
967
|
|
|
949
968
|
} catch (error) {
|
|
950
969
|
if (this.options.debug) console.error('Video player initialization error:', error);
|
|
@@ -1077,6 +1096,8 @@ interceptAutoLoading() {
|
|
|
1077
1096
|
|
|
1078
1097
|
this.hideNativePlayer();
|
|
1079
1098
|
|
|
1099
|
+
this.video.load();
|
|
1100
|
+
|
|
1080
1101
|
if (this.options.debug) console.log('📁 Sources temporarily disabled to prevent blocking');
|
|
1081
1102
|
}
|
|
1082
1103
|
|
|
@@ -1245,6 +1266,7 @@ markPlayerReady() {
|
|
|
1245
1266
|
this.video.style.visibility = '';
|
|
1246
1267
|
this.video.style.opacity = '';
|
|
1247
1268
|
this.video.style.pointerEvents = '';
|
|
1269
|
+
this.video.style.display = '';
|
|
1248
1270
|
}
|
|
1249
1271
|
|
|
1250
1272
|
// UPDATE SETTINGS MENU VISIBILITY IF APPLICABLE
|
|
@@ -1337,24 +1359,40 @@ createPlayerStructure() {
|
|
|
1337
1359
|
createInitialLoading() {
|
|
1338
1360
|
const initialLoader = document.createElement('div');
|
|
1339
1361
|
initialLoader.className = 'initial-loading';
|
|
1340
|
-
initialLoader.innerHTML =
|
|
1362
|
+
initialLoader.innerHTML = `
|
|
1363
|
+
<div class="loading-spinner-wrap">
|
|
1364
|
+
<div class="loading-spinner"></div>
|
|
1365
|
+
${this.options.loadingLogo
|
|
1366
|
+
? `<img class="loading-spinner-logo" src="${this.options.loadingLogo}" alt="" />`
|
|
1367
|
+
: ''}
|
|
1368
|
+
</div>
|
|
1369
|
+
<div class="loading-text"></div>
|
|
1370
|
+
`;
|
|
1341
1371
|
this.container.appendChild(initialLoader);
|
|
1342
1372
|
this.initialLoading = initialLoader;
|
|
1343
1373
|
}
|
|
1344
1374
|
|
|
1345
|
-
collectVideoQualities() {
|
|
1346
|
-
if (this.options.debug) console.log('📁 Video qualities will be loaded with restored sources');
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
1375
|
createLoadingOverlay() {
|
|
1350
1376
|
const overlay = document.createElement('div');
|
|
1351
1377
|
overlay.className = 'loading-overlay';
|
|
1352
|
-
overlay.id =
|
|
1353
|
-
overlay.innerHTML =
|
|
1378
|
+
overlay.id = `loadingOverlay-${this.getUniqueId()}`;
|
|
1379
|
+
overlay.innerHTML = `
|
|
1380
|
+
<div class="loading-spinner-wrap">
|
|
1381
|
+
<div class="loading-spinner"></div>
|
|
1382
|
+
${this.options.loadingLogo
|
|
1383
|
+
? `<img class="loading-spinner-logo" src="${this.options.loadingLogo}" alt="" />`
|
|
1384
|
+
: ''}
|
|
1385
|
+
</div>
|
|
1386
|
+
<div class="loading-text"></div>
|
|
1387
|
+
`;
|
|
1354
1388
|
this.container.appendChild(overlay);
|
|
1355
1389
|
this.loadingOverlay = overlay;
|
|
1356
1390
|
}
|
|
1357
1391
|
|
|
1392
|
+
collectVideoQualities() {
|
|
1393
|
+
if (this.options.debug) console.log('📁 Video qualities will be loaded with restored sources');
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1358
1396
|
updateTooltips() {
|
|
1359
1397
|
if (!this.controls) return;
|
|
1360
1398
|
|
|
@@ -2013,6 +2051,12 @@ updateSeekTooltip(e) {
|
|
|
2013
2051
|
}
|
|
2014
2052
|
|
|
2015
2053
|
play() {
|
|
2054
|
+
if (!this.isPlayerReady) {
|
|
2055
|
+
this.savedAutoplayIntent = true;
|
|
2056
|
+
if (this.options.debug) console.log('🛑 Play requested but player is booting. Intent saved for later.');
|
|
2057
|
+
return Promise ? Promise.resolve() : undefined;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2016
2060
|
if (!this.video || this.isChangingQuality) return;
|
|
2017
2061
|
|
|
2018
2062
|
this.video.play().catch(err => {
|
|
@@ -2022,7 +2066,6 @@ play() {
|
|
|
2022
2066
|
if (this.playIcon) this.playIcon.classList.add('hidden');
|
|
2023
2067
|
if (this.pauseIcon) this.pauseIcon.classList.remove('hidden');
|
|
2024
2068
|
|
|
2025
|
-
// Trigger event played
|
|
2026
2069
|
this.triggerEvent('played', {
|
|
2027
2070
|
currentTime: this.getCurrentTime(),
|
|
2028
2071
|
duration: this.getDuration()
|
|
@@ -2030,6 +2073,11 @@ play() {
|
|
|
2030
2073
|
}
|
|
2031
2074
|
|
|
2032
2075
|
pause() {
|
|
2076
|
+
if (!this.isPlayerReady) {
|
|
2077
|
+
this.savedAutoplayIntent = false;
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2033
2081
|
if (!this.video) return;
|
|
2034
2082
|
|
|
2035
2083
|
this.video.pause();
|
|
@@ -4399,6 +4447,53 @@ populateSettingsMenu() {
|
|
|
4399
4447
|
settingsMenu.innerHTML = menuHTML;
|
|
4400
4448
|
}
|
|
4401
4449
|
|
|
4450
|
+
/**
|
|
4451
|
+
* Updates specific UI elements inside the settings menu
|
|
4452
|
+
* without recreating the entire DOM (prevents accordions from closing)
|
|
4453
|
+
*/
|
|
4454
|
+
updateSettingsMenuUI() {
|
|
4455
|
+
const settingsMenu = this.container?.querySelector('.settings-menu');
|
|
4456
|
+
if (!settingsMenu) return;
|
|
4457
|
+
|
|
4458
|
+
if (this.options.showSubtitles) {
|
|
4459
|
+
const subtitlesTrigger = settingsMenu.querySelector('[data-action="subtitles_expand"]');
|
|
4460
|
+
|
|
4461
|
+
if (subtitlesTrigger) {
|
|
4462
|
+
// 1. Update the label text (e.g. "Subtitles: English")
|
|
4463
|
+
const label = subtitlesTrigger.querySelector('.settings-option-label');
|
|
4464
|
+
if (label) {
|
|
4465
|
+
const subtitlesLabel = this.t('subtitles') || 'Subtitles';
|
|
4466
|
+
const currentTrack = this.currentSubtitleTrack;
|
|
4467
|
+
|
|
4468
|
+
let currentLabelText = this.t('subtitlesoff') || 'Off';
|
|
4469
|
+
if (this.subtitlesEnabled && currentTrack) {
|
|
4470
|
+
currentLabelText = currentTrack.label || 'Unknown';
|
|
4471
|
+
}
|
|
4472
|
+
|
|
4473
|
+
label.innerHTML = `${subtitlesLabel} <strong>${currentLabelText}</strong>`;
|
|
4474
|
+
}
|
|
4475
|
+
|
|
4476
|
+
// 2. Update the "active" classes in the dropdown list
|
|
4477
|
+
const submenuContent = subtitlesTrigger.nextElementSibling;
|
|
4478
|
+
if (submenuContent && submenuContent.classList.contains('settings-expandable-content')) {
|
|
4479
|
+
submenuContent.querySelectorAll('.settings-suboption').forEach(opt => {
|
|
4480
|
+
const trackData = opt.getAttribute('data-track');
|
|
4481
|
+
opt.classList.remove('active');
|
|
4482
|
+
|
|
4483
|
+
if (trackData === 'off' && !this.subtitlesEnabled) {
|
|
4484
|
+
opt.classList.add('active');
|
|
4485
|
+
} else if (this.subtitlesEnabled && trackData !== 'off') {
|
|
4486
|
+
const trackIndex = parseInt(trackData);
|
|
4487
|
+
if (this.textTracks[trackIndex] && this.textTracks[trackIndex].track === this.currentSubtitleTrack) {
|
|
4488
|
+
opt.classList.add('active');
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
});
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
|
|
4402
4497
|
/**
|
|
4403
4498
|
* Add scrollbar to settings menu on mobile
|
|
4404
4499
|
*/
|
|
@@ -6080,6 +6175,33 @@ createCustomSubtitleOverlay() {
|
|
|
6080
6175
|
if (this.options.debug) console.log('✅ Custom subtitle overlay created with responsive settings');
|
|
6081
6176
|
}
|
|
6082
6177
|
|
|
6178
|
+
// Universal time parser for both SRT (00:00:00,000) and VTT (00:00.000)
|
|
6179
|
+
parseTimeToSecondsUniversal(timeString) {
|
|
6180
|
+
if (!timeString) return 0;
|
|
6181
|
+
|
|
6182
|
+
// Normalize comma to dot for consistency
|
|
6183
|
+
let normalized = timeString.replace(',', '.');
|
|
6184
|
+
let parts = normalized.split('.');
|
|
6185
|
+
|
|
6186
|
+
let timeParts = parts[0].split(':');
|
|
6187
|
+
let ms = parts[1] ? parseInt(parts[1], 10) / 1000 : 0;
|
|
6188
|
+
|
|
6189
|
+
let hours = 0, minutes = 0, seconds = 0;
|
|
6190
|
+
|
|
6191
|
+
if (timeParts.length === 3) {
|
|
6192
|
+
// SRT Format: HH:MM:SS
|
|
6193
|
+
hours = parseInt(timeParts[0], 10);
|
|
6194
|
+
minutes = parseInt(timeParts[1], 10);
|
|
6195
|
+
seconds = parseInt(timeParts[2], 10);
|
|
6196
|
+
} else if (timeParts.length === 2) {
|
|
6197
|
+
// VTT Short Format: MM:SS
|
|
6198
|
+
minutes = parseInt(timeParts[0], 10);
|
|
6199
|
+
seconds = parseInt(timeParts[1], 10);
|
|
6200
|
+
}
|
|
6201
|
+
|
|
6202
|
+
return (hours * 3600) + (minutes * 60) + seconds + ms;
|
|
6203
|
+
}
|
|
6204
|
+
|
|
6083
6205
|
customTimeToSeconds(timeString) {
|
|
6084
6206
|
if (!timeString) return 0;
|
|
6085
6207
|
|
|
@@ -6105,6 +6227,33 @@ customTimeToSeconds(timeString) {
|
|
|
6105
6227
|
return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000;
|
|
6106
6228
|
}
|
|
6107
6229
|
|
|
6230
|
+
// Universal time parser for both SRT (00:00:00,000) and VTT (00:00.000)
|
|
6231
|
+
parseTimeToSecondsUniversal(timeString) {
|
|
6232
|
+
if (!timeString) return 0;
|
|
6233
|
+
|
|
6234
|
+
// Normalize comma to dot for consistency
|
|
6235
|
+
const normalized = timeString.replace(',', '.');
|
|
6236
|
+
const parts = normalized.split('.');
|
|
6237
|
+
|
|
6238
|
+
const timeParts = parts[0].split(':');
|
|
6239
|
+
const ms = parts[1] ? parseInt(parts[1], 10) / 1000 : 0;
|
|
6240
|
+
|
|
6241
|
+
let hours = 0, minutes = 0, seconds = 0;
|
|
6242
|
+
|
|
6243
|
+
if (timeParts.length === 3) {
|
|
6244
|
+
// Full format: HH:MM:SS
|
|
6245
|
+
hours = parseInt(timeParts[0], 10);
|
|
6246
|
+
minutes = parseInt(timeParts[1], 10);
|
|
6247
|
+
seconds = parseInt(timeParts[2], 10);
|
|
6248
|
+
} else if (timeParts.length === 2) {
|
|
6249
|
+
// Short format (common in VTT): MM:SS
|
|
6250
|
+
minutes = parseInt(timeParts[0], 10);
|
|
6251
|
+
seconds = parseInt(timeParts[1], 10);
|
|
6252
|
+
}
|
|
6253
|
+
|
|
6254
|
+
return (hours * 3600) + (minutes * 60) + seconds + ms;
|
|
6255
|
+
}
|
|
6256
|
+
|
|
6108
6257
|
parseCustomSRT(srtText) {
|
|
6109
6258
|
var subtitles = [];
|
|
6110
6259
|
var normalizedText = srtText.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
@@ -6138,6 +6287,63 @@ parseCustomSRT(srtText) {
|
|
|
6138
6287
|
return subtitles;
|
|
6139
6288
|
}
|
|
6140
6289
|
|
|
6290
|
+
// Method to inject multiple external subtitle tracks
|
|
6291
|
+
injectExternalSubtitleTracks(tracksArray) {
|
|
6292
|
+
// Check if video exists and if tracksArray is actually an array
|
|
6293
|
+
if (!this.video || !Array.isArray(tracksArray) || tracksArray.length === 0) return;
|
|
6294
|
+
|
|
6295
|
+
this.video.crossOrigin = "anonymous";
|
|
6296
|
+
let loadedCount = 0;
|
|
6297
|
+
|
|
6298
|
+
// Loop through each track object in the array
|
|
6299
|
+
tracksArray.forEach(trackData => {
|
|
6300
|
+
const trackEl = document.createElement('track');
|
|
6301
|
+
trackEl.kind = 'subtitles';
|
|
6302
|
+
trackEl.label = trackData.label;
|
|
6303
|
+
trackEl.srclang = trackData.lang;
|
|
6304
|
+
trackEl.src = trackData.src;
|
|
6305
|
+
|
|
6306
|
+
// Set default if specified in the JSON
|
|
6307
|
+
if (trackData.default) {
|
|
6308
|
+
trackEl.default = true;
|
|
6309
|
+
trackEl.setAttribute('default', 'default');
|
|
6310
|
+
trackEl.setAttribute('data-default', 'true');
|
|
6311
|
+
}
|
|
6312
|
+
|
|
6313
|
+
// Listen for the load event on EACH track
|
|
6314
|
+
trackEl.addEventListener('load', () => {
|
|
6315
|
+
if (trackEl.track) {
|
|
6316
|
+
// Only show the default one, keep others hidden but ready
|
|
6317
|
+
trackEl.track.mode = trackData.default ? 'showing' : 'hidden';
|
|
6318
|
+
}
|
|
6319
|
+
|
|
6320
|
+
loadedCount++;
|
|
6321
|
+
|
|
6322
|
+
// Re-initialize UI ONLY when ALL tracks have finished loading
|
|
6323
|
+
if (loadedCount === tracksArray.length) {
|
|
6324
|
+
console.log(`MYETV Player: ${loadedCount} external subtitle tracks loaded.`);
|
|
6325
|
+
|
|
6326
|
+
if (typeof this.initializeSubtitles === 'function') {
|
|
6327
|
+
this.initializeSubtitles();
|
|
6328
|
+
} else if (typeof this.initializeCustomSubtitles === 'function') {
|
|
6329
|
+
this.initializeCustomSubtitles();
|
|
6330
|
+
} else if (typeof this.loadCustomSubtitleTracks === 'function') {
|
|
6331
|
+
this.loadCustomSubtitleTracks();
|
|
6332
|
+
}
|
|
6333
|
+
}
|
|
6334
|
+
});
|
|
6335
|
+
|
|
6336
|
+
trackEl.addEventListener('error', () => {
|
|
6337
|
+
console.warn(`MYETV Player: Failed to load track ${trackData.lang}`);
|
|
6338
|
+
// Increment anyway so we don't block the UI initialization
|
|
6339
|
+
loadedCount++;
|
|
6340
|
+
});
|
|
6341
|
+
|
|
6342
|
+
// Append to DOM
|
|
6343
|
+
this.video.appendChild(trackEl);
|
|
6344
|
+
});
|
|
6345
|
+
}
|
|
6346
|
+
|
|
6141
6347
|
loadCustomSubtitleTracks() {
|
|
6142
6348
|
var self = this;
|
|
6143
6349
|
var tracks = this.video.querySelectorAll('track[kind="subtitles"]');
|
|
@@ -6148,7 +6354,8 @@ loadCustomSubtitleTracks() {
|
|
|
6148
6354
|
var label = track.getAttribute('label') || 'Unknown';
|
|
6149
6355
|
var srclang = track.getAttribute('srclang') || '';
|
|
6150
6356
|
|
|
6151
|
-
|
|
6357
|
+
var isDefault = track.default || track.hasAttribute('default');
|
|
6358
|
+
|
|
6152
6359
|
var trackObj = {
|
|
6153
6360
|
label: label,
|
|
6154
6361
|
language: srclang,
|
|
@@ -6162,43 +6369,83 @@ loadCustomSubtitleTracks() {
|
|
|
6162
6369
|
return response.text();
|
|
6163
6370
|
})
|
|
6164
6371
|
.then(function (srtText) {
|
|
6372
|
+
// Normalize newlines
|
|
6165
6373
|
var normalizedText = srtText.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
6374
|
+
|
|
6375
|
+
// Split into blocks by double newline
|
|
6166
6376
|
var blocks = normalizedText.trim().split('\n\n');
|
|
6167
6377
|
|
|
6168
6378
|
for (var i = 0; i < blocks.length; i++) {
|
|
6169
6379
|
var block = blocks[i].trim();
|
|
6170
|
-
if (!block) continue;
|
|
6380
|
+
if (!block || block.toUpperCase().startsWith('WEBVTT')) continue;
|
|
6381
|
+
|
|
6171
6382
|
var lines = block.split('\n');
|
|
6383
|
+
var timeMatch = null;
|
|
6384
|
+
var textLines = [];
|
|
6385
|
+
|
|
6386
|
+
for (var j = 0; j < lines.length; j++) {
|
|
6387
|
+
var line = lines[j].trim();
|
|
6388
|
+
|
|
6389
|
+
// Regex matches both SRT and VTT formats (with or without hours, dot or comma)
|
|
6390
|
+
if (!timeMatch) {
|
|
6391
|
+
var match = line.match(/(\d{2,}:\d{2}:\d{2}[.,]\d{3}|\d{2}:\d{2}[.,]\d{3})\s*-->\s*(\d{2,}:\d{2}:\d{2}[.,]\d{3}|\d{2}:\d{2}[.,]\d{3})/);
|
|
6392
|
+
if (match) {
|
|
6393
|
+
timeMatch = match;
|
|
6394
|
+
continue;
|
|
6395
|
+
}
|
|
6396
|
+
}
|
|
6172
6397
|
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6398
|
+
// Skip numeric IDs common in SRT files
|
|
6399
|
+
if (!timeMatch && /^\d+$/.test(line)) {
|
|
6400
|
+
continue;
|
|
6401
|
+
}
|
|
6176
6402
|
|
|
6403
|
+
// Everything after the timestamp is the actual subtitle text
|
|
6177
6404
|
if (timeMatch) {
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
}
|
|
6405
|
+
textLines.push(line);
|
|
6406
|
+
}
|
|
6407
|
+
}
|
|
6408
|
+
|
|
6409
|
+
// Store the parsed subtitle block
|
|
6410
|
+
if (timeMatch && textLines.length > 0) {
|
|
6411
|
+
var startTime = self.parseTimeToSecondsUniversal(timeMatch[1]);
|
|
6412
|
+
var endTime = self.parseTimeToSecondsUniversal(timeMatch[2]);
|
|
6413
|
+
var text = textLines.join('\n').trim().replace(/<[^>]*>/g, '');
|
|
6414
|
+
|
|
6415
|
+
if (text && !isNaN(startTime) && !isNaN(endTime) && startTime < endTime) {
|
|
6416
|
+
trackObj.subtitles.push({
|
|
6417
|
+
start: startTime,
|
|
6418
|
+
end: endTime,
|
|
6419
|
+
text: text
|
|
6420
|
+
});
|
|
6195
6421
|
}
|
|
6196
6422
|
}
|
|
6197
6423
|
}
|
|
6198
6424
|
|
|
6199
6425
|
if (self.options.debug) {
|
|
6200
|
-
console.log('✅
|
|
6426
|
+
console.log('✅ Custom parser loaded ' + trackObj.subtitles.length + ' subtitles for ' + label);
|
|
6427
|
+
}
|
|
6428
|
+
|
|
6429
|
+
// =========================================================
|
|
6430
|
+
// AUTO-ENABLE SUBTITLES
|
|
6431
|
+
// =========================================================
|
|
6432
|
+
var isDefault = track.default || track.hasAttribute('default') || track.getAttribute('data-default') === 'true';
|
|
6433
|
+
|
|
6434
|
+
if (isDefault) {
|
|
6435
|
+
if (self.options.debug) {
|
|
6436
|
+
console.log('⏳ Waiting 500ms for UI alignment... track:', label);
|
|
6437
|
+
}
|
|
6438
|
+
|
|
6439
|
+
// Delay execution to ensure the player has finished
|
|
6440
|
+
// building the menu and populating this.textTracks array!
|
|
6441
|
+
setTimeout(function () {
|
|
6442
|
+
if (self.options.debug) {
|
|
6443
|
+
console.log('🎯 Forced auto-enable triggered now!');
|
|
6444
|
+
}
|
|
6445
|
+
self.enableSubtitleTrack(index);
|
|
6446
|
+
}, 500); // 500 milliseconds of delay
|
|
6201
6447
|
}
|
|
6448
|
+
|
|
6202
6449
|
})
|
|
6203
6450
|
.catch(function (error) {
|
|
6204
6451
|
console.error('❌ Error loading ' + label + ':', error);
|
|
@@ -6317,39 +6564,38 @@ detectTextTracks() {
|
|
|
6317
6564
|
enableSubtitleTrack(trackIndex) {
|
|
6318
6565
|
if (trackIndex < 0 || trackIndex >= this.textTracks.length) return;
|
|
6319
6566
|
|
|
6320
|
-
// Disable all tracks first
|
|
6567
|
+
// 1. Disable all tracks first to prevent overlapping
|
|
6321
6568
|
this.disableAllTracks();
|
|
6322
6569
|
|
|
6323
|
-
// Enable ONLY the custom subtitle system
|
|
6570
|
+
// 2. Enable ONLY the custom WebView-safe subtitle system
|
|
6324
6571
|
var success = this.enableCustomSubtitleTrack(trackIndex);
|
|
6325
6572
|
|
|
6326
6573
|
if (success) {
|
|
6327
6574
|
this.currentSubtitleTrack = this.textTracks[trackIndex].track;
|
|
6328
6575
|
this.subtitlesEnabled = true;
|
|
6329
6576
|
|
|
6330
|
-
//
|
|
6577
|
+
// 3. Kill the native browser track to avoid double rendering or crashes
|
|
6331
6578
|
if (this.video.textTracks && this.video.textTracks[trackIndex]) {
|
|
6332
|
-
this.video.textTracks[trackIndex].mode = 'disabled';
|
|
6579
|
+
this.video.textTracks[trackIndex].mode = 'disabled';
|
|
6333
6580
|
}
|
|
6334
6581
|
|
|
6335
6582
|
this.updateSubtitlesButton();
|
|
6336
6583
|
this.populateSubtitlesMenu();
|
|
6337
6584
|
|
|
6585
|
+
if (typeof this.updateSettingsMenuUI === 'function') {
|
|
6586
|
+
this.updateSettingsMenuUI();
|
|
6587
|
+
}
|
|
6588
|
+
|
|
6338
6589
|
if (this.options.debug) {
|
|
6339
|
-
console.log('✅ Custom subtitles
|
|
6590
|
+
console.log('✅ Custom UI subtitles strictly enforced for track', trackIndex);
|
|
6340
6591
|
}
|
|
6341
6592
|
|
|
6342
|
-
// Trigger subtitle change event
|
|
6343
6593
|
this.triggerEvent('subtitlechange', {
|
|
6344
6594
|
enabled: true,
|
|
6345
6595
|
trackIndex: trackIndex,
|
|
6346
6596
|
trackLabel: this.textTracks[trackIndex].label,
|
|
6347
6597
|
trackLanguage: this.textTracks[trackIndex].language
|
|
6348
6598
|
});
|
|
6349
|
-
} else {
|
|
6350
|
-
if (this.options.debug) {
|
|
6351
|
-
console.error('❌ Failed to enable custom subtitles for track', trackIndex);
|
|
6352
|
-
}
|
|
6353
6599
|
}
|
|
6354
6600
|
}
|
|
6355
6601
|
|
|
@@ -6363,6 +6609,11 @@ disableSubtitles() {
|
|
|
6363
6609
|
this.updateSubtitlesButton();
|
|
6364
6610
|
this.populateSubtitlesMenu();
|
|
6365
6611
|
|
|
6612
|
+
// 👉 L'AGGIUNTA FONDAMENTALE: Sincronizza il menu Settings!
|
|
6613
|
+
if (typeof this.updateSettingsMenuUI === 'function') {
|
|
6614
|
+
this.updateSettingsMenuUI();
|
|
6615
|
+
}
|
|
6616
|
+
|
|
6366
6617
|
if (this.options.debug) console.log('📝 Subtitles disabled');
|
|
6367
6618
|
|
|
6368
6619
|
this.triggerEvent('subtitlechange', {
|
|
@@ -8617,48 +8868,123 @@ setDashQuality(qualityIndex) {
|
|
|
8617
8868
|
// CLASS METHODS - Will be placed INSIDE the class
|
|
8618
8869
|
// ===================================================================
|
|
8619
8870
|
|
|
8620
|
-
/**
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
'beforeInit': [],
|
|
8631
|
-
'afterInit': [],
|
|
8632
|
-
'beforePlay': [],
|
|
8633
|
-
'afterPlay': [],
|
|
8634
|
-
'beforePause': [],
|
|
8635
|
-
'afterPause': [],
|
|
8636
|
-
'beforeQualityChange': [],
|
|
8637
|
-
'afterQualityChange': [],
|
|
8638
|
-
'beforeDestroy': [],
|
|
8639
|
-
'afterDestroy': []
|
|
8640
|
-
};
|
|
8871
|
+
/**
|
|
8872
|
+
* Initialize plugin system for player instance
|
|
8873
|
+
*/
|
|
8874
|
+
initializePluginSystem() {
|
|
8875
|
+
this.plugins = {};
|
|
8876
|
+
this.pluginHooks = {
|
|
8877
|
+
'beforeInit': [], 'afterInit': [], 'beforePlay': [], 'afterPlay': [],
|
|
8878
|
+
'beforePause': [], 'afterPause': [], 'beforeQualityChange': [],
|
|
8879
|
+
'afterQualityChange': [], 'beforeDestroy': [], 'afterDestroy': []
|
|
8880
|
+
};
|
|
8641
8881
|
|
|
8642
|
-
|
|
8643
|
-
|
|
8882
|
+
if (this.options.debug) console.log('🔌 Plugin system initialized');
|
|
8883
|
+
|
|
8884
|
+
// Load plugins sequentially if present, otherwise finalize player immediately
|
|
8885
|
+
if (this.options.plugins && typeof this.options.plugins === 'object') {
|
|
8886
|
+
this.loadPluginsSequentially(this.options.plugins);
|
|
8887
|
+
} else {
|
|
8888
|
+
this.finalizePlayerSetup();
|
|
8889
|
+
}
|
|
8644
8890
|
}
|
|
8645
8891
|
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8892
|
+
/**
|
|
8893
|
+
* Loads plugins one by one based on their 'order' option.
|
|
8894
|
+
* Prevents any plugin from loading until the previous blocking plugin finishes.
|
|
8895
|
+
* @param {Object} pluginsConfig
|
|
8896
|
+
*/
|
|
8897
|
+
async loadPluginsSequentially(pluginsConfig) {
|
|
8898
|
+
// 1. Sort plugins by order (default 99 means load last)
|
|
8899
|
+
const pluginList = Object.keys(pluginsConfig).map(pluginName => {
|
|
8900
|
+
return {
|
|
8901
|
+
name: pluginName,
|
|
8902
|
+
config: pluginsConfig[pluginName],
|
|
8903
|
+
order: pluginsConfig[pluginName] && pluginsConfig[pluginName].order !== undefined
|
|
8904
|
+
? pluginsConfig[pluginName].order : 99
|
|
8905
|
+
};
|
|
8906
|
+
}).sort((a, b) => a.order - b.order);
|
|
8907
|
+
|
|
8908
|
+
// 2. Process each plugin sequentially
|
|
8909
|
+
for (const pluginData of pluginList) {
|
|
8910
|
+
if (this.options.debug) console.log(`🔌 Loading plugin: ${pluginData.name} (order: ${pluginData.order})`);
|
|
8911
|
+
|
|
8912
|
+
// Instantiate the plugin NOW (plugins with higher order do not exist yet)
|
|
8913
|
+
const pluginInstance = this.usePlugin(pluginData.name, pluginData.config);
|
|
8914
|
+
|
|
8915
|
+
// If the plugin has an async barrier, pause the ENTIRE loop
|
|
8916
|
+
if (pluginInstance && typeof pluginInstance.waitForCompletion === 'function') {
|
|
8917
|
+
if (this.options.debug) console.log(`🔌 Pausing queue. Waiting for ${pluginData.name} to complete...`);
|
|
8918
|
+
await pluginInstance.waitForCompletion();
|
|
8919
|
+
if (this.options.debug) console.log(`🔌 ${pluginData.name} completed. Resuming queue.`);
|
|
8920
|
+
}
|
|
8649
8921
|
}
|
|
8922
|
+
|
|
8923
|
+
// 3. All plugins processed. Now we can safely boot the main HTML5 player
|
|
8924
|
+
this.finalizePlayerSetup();
|
|
8650
8925
|
}
|
|
8651
8926
|
|
|
8652
|
-
/**
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8927
|
+
/**
|
|
8928
|
+
* Boots up the core HTML5 player features after all blocking plugins have finished.
|
|
8929
|
+
* This function ensures that the main player components (like poster, sources, and watermark)
|
|
8930
|
+
* are initialized only after the plugin queue (including the loader) is fully resolved.
|
|
8931
|
+
*/
|
|
8932
|
+
finalizePlayerSetup() {
|
|
8933
|
+
if (this.options.debug) {
|
|
8934
|
+
console.log('🚀 Finalizing main player setup...');
|
|
8935
|
+
}
|
|
8936
|
+
|
|
8937
|
+
// Restore the autoplay intent BEFORE initializing the poster.
|
|
8938
|
+
// This ensures initializePoster() knows we are autoplaying and won't flash the image.
|
|
8939
|
+
if (this.savedAutoplayIntent) {
|
|
8940
|
+
this.options.autoplay = true;
|
|
8941
|
+
}
|
|
8942
|
+
|
|
8943
|
+
// Trigger the global 'playerready' event.
|
|
8944
|
+
// Firing it ONLY NOW guarantees that third-party plugins (like YouTube)
|
|
8945
|
+
// safely instantiate their iframes only after the loader has finished its countdown.
|
|
8946
|
+
this.markPlayerReady();
|
|
8947
|
+
|
|
8948
|
+
this.restoreSourcesAsync();
|
|
8949
|
+
this.initializeSubtitles();
|
|
8950
|
+
this.initializeQualityMonitoring();
|
|
8951
|
+
this.initializeResolution();
|
|
8952
|
+
this.initializeChapters();
|
|
8953
|
+
|
|
8954
|
+
// Prevent the final poster flash if a third-party iframe (YouTube/Vimeo/Twitch) is active.
|
|
8955
|
+
const hasExternalIframe = this.options.plugins && (
|
|
8956
|
+
this.options.plugins.youtube ||
|
|
8957
|
+
this.options.plugins.vimeo ||
|
|
8958
|
+
this.options.plugins.twitch
|
|
8959
|
+
);
|
|
8960
|
+
|
|
8961
|
+
if (!hasExternalIframe) {
|
|
8962
|
+
this.initializePoster();
|
|
8963
|
+
}
|
|
8964
|
+
|
|
8965
|
+
this.initializeWatermark();
|
|
8966
|
+
|
|
8967
|
+
// Check if external subtitle tracks were provided and the array is not empty
|
|
8968
|
+
if (this.options.externalSubtitleTracks && this.options.externalSubtitleTracks.length > 0) {
|
|
8969
|
+
this.injectExternalSubtitleTracks(this.options.externalSubtitleTracks);
|
|
8970
|
+
}
|
|
8971
|
+
|
|
8972
|
+
// 🚀 Restore Autoplay ONLY AFTER everything is fully loaded and the queue is unlocked
|
|
8973
|
+
if (this.savedAutoplayIntent) {
|
|
8974
|
+
if (this.options.debug) console.log('🚀 Triggering delayed autoplay...');
|
|
8975
|
+
|
|
8976
|
+
if (this.video) {
|
|
8977
|
+
this.video.setAttribute('autoplay', '');
|
|
8978
|
+
this.video.style.opacity = '1';
|
|
8979
|
+
this.video.style.visibility = '';
|
|
8661
8980
|
}
|
|
8981
|
+
|
|
8982
|
+
setTimeout(() => {
|
|
8983
|
+
this.play();
|
|
8984
|
+
}, 300);
|
|
8985
|
+
} else if (this.video) {
|
|
8986
|
+
this.video.style.opacity = '1';
|
|
8987
|
+
this.video.style.visibility = '';
|
|
8662
8988
|
}
|
|
8663
8989
|
}
|
|
8664
8990
|
|