myetv-player 1.7.1 → 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.
@@ -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)
@@ -841,9 +844,33 @@ constructor(videoElement, options = {}) {
841
844
  if (this.options.language && this.isI18nAvailable()) {
842
845
  VideoPlayerTranslations.setLanguage(this.options.language);
843
846
  }
844
- // Apply autoplay if enabled
845
- if (options.autoplay) {
846
- this.video.autoplay = true;
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';
847
874
  }
848
875
 
849
876
  try {
@@ -935,17 +962,8 @@ constructor(videoElement, options = {}) {
935
962
  this.updateVolumeSliderVisual();
936
963
  this.initVolumeTooltip();
937
964
  this.updateTooltips();
938
- this.markPlayerReady();
939
- this.initializePluginSystem();
940
- this.restoreSourcesAsync();
941
-
942
- this.initializeSubtitles();
943
- this.initializeQualityMonitoring();
944
965
 
945
- this.initializeResolution();
946
- this.initializeChapters();
947
- this.initializePoster();
948
- this.initializeWatermark();
966
+ this.initializePluginSystem();
949
967
 
950
968
  } catch (error) {
951
969
  if (this.options.debug) console.error('Video player initialization error:', error);
@@ -1078,6 +1096,8 @@ interceptAutoLoading() {
1078
1096
 
1079
1097
  this.hideNativePlayer();
1080
1098
 
1099
+ this.video.load();
1100
+
1081
1101
  if (this.options.debug) console.log('📁 Sources temporarily disabled to prevent blocking');
1082
1102
  }
1083
1103
 
@@ -1246,6 +1266,7 @@ markPlayerReady() {
1246
1266
  this.video.style.visibility = '';
1247
1267
  this.video.style.opacity = '';
1248
1268
  this.video.style.pointerEvents = '';
1269
+ this.video.style.display = '';
1249
1270
  }
1250
1271
 
1251
1272
  // UPDATE SETTINGS MENU VISIBILITY IF APPLICABLE
@@ -2030,6 +2051,12 @@ updateSeekTooltip(e) {
2030
2051
  }
2031
2052
 
2032
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
+
2033
2060
  if (!this.video || this.isChangingQuality) return;
2034
2061
 
2035
2062
  this.video.play().catch(err => {
@@ -2039,7 +2066,6 @@ play() {
2039
2066
  if (this.playIcon) this.playIcon.classList.add('hidden');
2040
2067
  if (this.pauseIcon) this.pauseIcon.classList.remove('hidden');
2041
2068
 
2042
- // Trigger event played
2043
2069
  this.triggerEvent('played', {
2044
2070
  currentTime: this.getCurrentTime(),
2045
2071
  duration: this.getDuration()
@@ -2047,6 +2073,11 @@ play() {
2047
2073
  }
2048
2074
 
2049
2075
  pause() {
2076
+ if (!this.isPlayerReady) {
2077
+ this.savedAutoplayIntent = false;
2078
+ return;
2079
+ }
2080
+
2050
2081
  if (!this.video) return;
2051
2082
 
2052
2083
  this.video.pause();
@@ -4416,6 +4447,53 @@ populateSettingsMenu() {
4416
4447
  settingsMenu.innerHTML = menuHTML;
4417
4448
  }
4418
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
+
4419
4497
  /**
4420
4498
  * Add scrollbar to settings menu on mobile
4421
4499
  */
@@ -6097,6 +6175,33 @@ createCustomSubtitleOverlay() {
6097
6175
  if (this.options.debug) console.log('✅ Custom subtitle overlay created with responsive settings');
6098
6176
  }
6099
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
+
6100
6205
  customTimeToSeconds(timeString) {
6101
6206
  if (!timeString) return 0;
6102
6207
 
@@ -6122,6 +6227,33 @@ customTimeToSeconds(timeString) {
6122
6227
  return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000;
6123
6228
  }
6124
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
+
6125
6257
  parseCustomSRT(srtText) {
6126
6258
  var subtitles = [];
6127
6259
  var normalizedText = srtText.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
@@ -6155,6 +6287,63 @@ parseCustomSRT(srtText) {
6155
6287
  return subtitles;
6156
6288
  }
6157
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
+
6158
6347
  loadCustomSubtitleTracks() {
6159
6348
  var self = this;
6160
6349
  var tracks = this.video.querySelectorAll('track[kind="subtitles"]');
@@ -6165,7 +6354,8 @@ loadCustomSubtitleTracks() {
6165
6354
  var label = track.getAttribute('label') || 'Unknown';
6166
6355
  var srclang = track.getAttribute('srclang') || '';
6167
6356
 
6168
- // CREA L'OGGETTO PRIMA E AGGIUNGILO SUBITO
6357
+ var isDefault = track.default || track.hasAttribute('default');
6358
+
6169
6359
  var trackObj = {
6170
6360
  label: label,
6171
6361
  language: srclang,
@@ -6179,43 +6369,83 @@ loadCustomSubtitleTracks() {
6179
6369
  return response.text();
6180
6370
  })
6181
6371
  .then(function (srtText) {
6372
+ // Normalize newlines
6182
6373
  var normalizedText = srtText.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
6374
+
6375
+ // Split into blocks by double newline
6183
6376
  var blocks = normalizedText.trim().split('\n\n');
6184
6377
 
6185
6378
  for (var i = 0; i < blocks.length; i++) {
6186
6379
  var block = blocks[i].trim();
6187
- if (!block) continue;
6380
+ if (!block || block.toUpperCase().startsWith('WEBVTT')) continue;
6381
+
6188
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
+ }
6189
6397
 
6190
- if (lines.length >= 3) {
6191
- var timeLine = lines[1].trim();
6192
- var timeMatch = timeLine.match(/(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})/);
6398
+ // Skip numeric IDs common in SRT files
6399
+ if (!timeMatch && /^\d+$/.test(line)) {
6400
+ continue;
6401
+ }
6193
6402
 
6403
+ // Everything after the timestamp is the actual subtitle text
6194
6404
  if (timeMatch) {
6195
- var startParts = timeMatch[1].split(',');
6196
- var startTimeParts = startParts[0].split(':');
6197
- var startTime = parseInt(startTimeParts[0], 10) * 3600 + parseInt(startTimeParts[1], 10) * 60 + parseInt(startTimeParts[2], 10) + parseInt(startParts[1], 10) / 1000;
6198
-
6199
- var endParts = timeMatch[2].split(',');
6200
- var endTimeParts = endParts[0].split(':');
6201
- var endTime = parseInt(endTimeParts[0], 10) * 3600 + parseInt(endTimeParts[1], 10) * 60 + parseInt(endTimeParts[2], 10) + parseInt(endParts[1], 10) / 1000;
6202
-
6203
- var text = lines.slice(2).join('\n').trim().replace(/<[^>]*>/g, '');
6204
-
6205
- if (text && text.length > 0 && !isNaN(startTime) && !isNaN(endTime) && startTime < endTime) {
6206
- trackObj.subtitles.push({
6207
- start: startTime,
6208
- end: endTime,
6209
- text: text
6210
- });
6211
- }
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
+ });
6212
6421
  }
6213
6422
  }
6214
6423
  }
6215
6424
 
6216
6425
  if (self.options.debug) {
6217
- console.log('✅ Loaded ' + trackObj.subtitles.length + ' subtitles for ' + label);
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
6218
6447
  }
6448
+
6219
6449
  })
6220
6450
  .catch(function (error) {
6221
6451
  console.error('❌ Error loading ' + label + ':', error);
@@ -6334,39 +6564,38 @@ detectTextTracks() {
6334
6564
  enableSubtitleTrack(trackIndex) {
6335
6565
  if (trackIndex < 0 || trackIndex >= this.textTracks.length) return;
6336
6566
 
6337
- // Disable all tracks first
6567
+ // 1. Disable all tracks first to prevent overlapping
6338
6568
  this.disableAllTracks();
6339
6569
 
6340
- // Enable ONLY the custom subtitle system (not native browser)
6570
+ // 2. Enable ONLY the custom WebView-safe subtitle system
6341
6571
  var success = this.enableCustomSubtitleTrack(trackIndex);
6342
6572
 
6343
6573
  if (success) {
6344
6574
  this.currentSubtitleTrack = this.textTracks[trackIndex].track;
6345
6575
  this.subtitlesEnabled = true;
6346
6576
 
6347
- // Make sure native tracks stay DISABLED
6577
+ // 3. Kill the native browser track to avoid double rendering or crashes
6348
6578
  if (this.video.textTracks && this.video.textTracks[trackIndex]) {
6349
- this.video.textTracks[trackIndex].mode = 'disabled'; // Keep native disabled
6579
+ this.video.textTracks[trackIndex].mode = 'disabled';
6350
6580
  }
6351
6581
 
6352
6582
  this.updateSubtitlesButton();
6353
6583
  this.populateSubtitlesMenu();
6354
6584
 
6585
+ if (typeof this.updateSettingsMenuUI === 'function') {
6586
+ this.updateSettingsMenuUI();
6587
+ }
6588
+
6355
6589
  if (this.options.debug) {
6356
- console.log('✅ Custom subtitles enabled:', this.textTracks[trackIndex].label);
6590
+ console.log('✅ Custom UI subtitles strictly enforced for track', trackIndex);
6357
6591
  }
6358
6592
 
6359
- // Trigger subtitle change event
6360
6593
  this.triggerEvent('subtitlechange', {
6361
6594
  enabled: true,
6362
6595
  trackIndex: trackIndex,
6363
6596
  trackLabel: this.textTracks[trackIndex].label,
6364
6597
  trackLanguage: this.textTracks[trackIndex].language
6365
6598
  });
6366
- } else {
6367
- if (this.options.debug) {
6368
- console.error('❌ Failed to enable custom subtitles for track', trackIndex);
6369
- }
6370
6599
  }
6371
6600
  }
6372
6601
 
@@ -6380,6 +6609,11 @@ disableSubtitles() {
6380
6609
  this.updateSubtitlesButton();
6381
6610
  this.populateSubtitlesMenu();
6382
6611
 
6612
+ // 👉 L'AGGIUNTA FONDAMENTALE: Sincronizza il menu Settings!
6613
+ if (typeof this.updateSettingsMenuUI === 'function') {
6614
+ this.updateSettingsMenuUI();
6615
+ }
6616
+
6383
6617
  if (this.options.debug) console.log('📝 Subtitles disabled');
6384
6618
 
6385
6619
  this.triggerEvent('subtitlechange', {
@@ -8634,48 +8868,123 @@ setDashQuality(qualityIndex) {
8634
8868
  // CLASS METHODS - Will be placed INSIDE the class
8635
8869
  // ===================================================================
8636
8870
 
8637
- /**
8638
- * Initialize plugin system for player instance
8639
- * This method should be called in the player constructor
8640
- */
8641
- initializePluginSystem() {
8642
- // Plugin instances storage
8643
- this.plugins = {};
8644
-
8645
- // Plugin hooks for lifecycle events
8646
- this.pluginHooks = {
8647
- 'beforeInit': [],
8648
- 'afterInit': [],
8649
- 'beforePlay': [],
8650
- 'afterPlay': [],
8651
- 'beforePause': [],
8652
- 'afterPause': [],
8653
- 'beforeQualityChange': [],
8654
- 'afterQualityChange': [],
8655
- 'beforeDestroy': [],
8656
- 'afterDestroy': []
8657
- };
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
+ };
8658
8881
 
8659
- if (this.options.debug) {
8660
- console.log('🔌 Plugin system initialized');
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
+ }
8661
8890
  }
8662
8891
 
8663
- // Load plugins specified in options
8664
- if (this.options.plugins && typeof this.options.plugins === 'object') {
8665
- this.loadPlugins(this.options.plugins);
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
+ }
8666
8921
  }
8922
+
8923
+ // 3. All plugins processed. Now we can safely boot the main HTML5 player
8924
+ this.finalizePlayerSetup();
8667
8925
  }
8668
8926
 
8669
- /**
8670
- * Load multiple plugins from options
8671
- * @param {Object} pluginsConfig - Object with plugin names as keys and options as values
8672
- */
8673
- loadPlugins(pluginsConfig) {
8674
- for (const pluginName in pluginsConfig) {
8675
- if (pluginsConfig.hasOwnProperty(pluginName)) {
8676
- const pluginOptions = pluginsConfig[pluginName];
8677
- this.usePlugin(pluginName, pluginOptions);
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 = '';
8678
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 = '';
8679
8988
  }
8680
8989
  }
8681
8990