myetv-player 1.0.8 → 1.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.
@@ -2,47 +2,104 @@
2
2
  * MYETV Player - Cloudflare Stream Plugin
3
3
  * File: myetv-player-cloudflare-stream-plugin.js
4
4
  * Integrates Cloudflare Stream videos with full API control
5
+ * Supports iframe player and direct HLS/DASH manifest URLs
6
+ * Auto-loads required libraries from cdnjs (hls.js and dash.js) on demand
5
7
  * Created by https://www.myetv.tv https://oskarcosimo.com
6
8
  */
7
9
 
8
10
  (function () {
9
11
  'use strict';
10
12
 
13
+ // CDN URLs for libraries (using Cloudflare CDN)
14
+ const LIBRARIES = {
15
+ hlsjs: 'https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.5.18/hls.min.js',
16
+ dashjs: 'https://cdnjs.cloudflare.com/ajax/libs/dashjs/5.0.3/modern/umd/dash.all.min.js'
17
+ };
18
+
19
+ // Track loaded libraries
20
+ const loadedLibraries = {
21
+ hlsjs: false,
22
+ dashjs: false
23
+ };
24
+
25
+ // Library loading promises
26
+ const loadingPromises = {};
27
+
28
+ /**
29
+ * Dynamically load a JavaScript library
30
+ */
31
+ function loadLibrary(name, url) {
32
+ if (loadingPromises[name]) {
33
+ return loadingPromises[name];
34
+ }
35
+
36
+ if (loadedLibraries[name]) {
37
+ return Promise.resolve();
38
+ }
39
+
40
+ if (name === 'hlsjs' && typeof Hls !== 'undefined') {
41
+ loadedLibraries[name] = true;
42
+ return Promise.resolve();
43
+ }
44
+ if (name === 'dashjs' && typeof dashjs !== 'undefined') {
45
+ loadedLibraries[name] = true;
46
+ return Promise.resolve();
47
+ }
48
+
49
+ loadingPromises[name] = new Promise((resolve, reject) => {
50
+ const script = document.createElement('script');
51
+ script.src = url;
52
+ script.async = true;
53
+
54
+ script.onload = () => {
55
+ loadedLibraries[name] = true;
56
+ console.log('☁️ Cloudflare Stream: ' + name + ' loaded successfully');
57
+ resolve();
58
+ };
59
+
60
+ script.onerror = () => {
61
+ const error = new Error('Failed to load ' + name + ' from ' + url);
62
+ console.error('☁️ Cloudflare Stream:', error);
63
+ reject(error);
64
+ };
65
+
66
+ document.head.appendChild(script);
67
+ });
68
+
69
+ return loadingPromises[name];
70
+ }
71
+
11
72
  class CloudflareStreamPlugin {
12
73
  constructor(player, options = {}) {
13
74
  this.player = player;
14
75
  this.options = {
15
- // Video source
16
- videoId: options.videoId || null, // Cloudflare Stream video ID
17
- videoUrl: options.videoUrl || null, // Full video URL
18
- signedUrl: options.signedUrl || null, // Signed URL for private videos
19
-
20
- // Account/Domain
21
- customerCode: options.customerCode || null, // Your Cloudflare account subdomain
22
-
23
- // Playback options
76
+ videoId: options.videoId || null,
77
+ videoUrl: options.videoUrl || null,
78
+ signedUrl: options.signedUrl || null,
79
+ manifestUrl: options.manifestUrl || null,
80
+ customerCode: options.customerCode || null,
81
+ useNativePlayer: options.useNativePlayer !== false,
82
+ preferIframe: options.preferIframe || false,
83
+ autoLoadLibraries: options.autoLoadLibraries !== false,
84
+ hlsLibraryUrl: options.hlsLibraryUrl || LIBRARIES.hlsjs,
85
+ dashLibraryUrl: options.dashLibraryUrl || LIBRARIES.dashjs,
24
86
  autoplay: options.autoplay || false,
25
87
  muted: options.muted || false,
26
88
  loop: options.loop || false,
27
- preload: options.preload || 'metadata', // 'none', 'metadata', 'auto'
89
+ preload: options.preload || 'auto',
28
90
  controls: options.controls !== false,
29
91
  defaultTextTrack: options.defaultTextTrack || null,
30
-
31
- // Player customization
32
- poster: options.poster || null, // Custom poster image
33
- primaryColor: options.primaryColor || null, // Custom player color
92
+ poster: options.poster || null,
93
+ primaryColor: options.primaryColor || null,
34
94
  letterboxColor: options.letterboxColor || 'black',
35
-
36
- // Advanced options
37
- startTime: options.startTime || 0, // Start position in seconds
38
- adUrl: options.adUrl || null, // VAST ad tag URL
39
-
40
- // Plugin options
95
+ startTime: options.startTime || 0,
96
+ adUrl: options.adUrl || null,
97
+ hlsConfig: options.hlsConfig || {},
98
+ defaultQuality: options.defaultQuality || 'auto',
41
99
  debug: options.debug || false,
42
100
  replaceNativePlayer: options.replaceNativePlayer !== false,
43
101
  autoLoadFromData: options.autoLoadFromData !== false,
44
102
  responsive: options.responsive !== false,
45
-
46
103
  ...options
47
104
  };
48
105
 
@@ -50,8 +107,15 @@
50
107
  this.streamIframe = null;
51
108
  this.streamContainer = null;
52
109
  this.isPlayerReady = false;
110
+ this.isUsingIframe = false;
111
+ this.isUsingManifest = false;
112
+ this.hlsInstance = null;
113
+ this.manifestType = null;
114
+ this.loadingCheckInterval = null;
115
+ this.availableQualities = [];
116
+ this.currentQuality = null;
117
+ this.qualityMonitorInterval = null;
53
118
 
54
- // Get plugin API
55
119
  this.api = player.getPluginAPI ? player.getPluginAPI() : {
56
120
  player: player,
57
121
  video: player.video,
@@ -70,19 +134,15 @@
70
134
  setup() {
71
135
  this.api.debug('Setup started');
72
136
 
73
- // Auto-detect from data attributes
74
137
  if (this.options.autoLoadFromData) {
75
138
  this.autoDetectSource();
76
139
  }
77
140
 
78
- // Create player if we have a source
79
- if (this.options.videoId || this.options.videoUrl || this.options.signedUrl) {
141
+ if (this.options.videoId || this.options.videoUrl || this.options.signedUrl || this.options.manifestUrl) {
80
142
  this.createStreamPlayer();
81
143
  }
82
144
 
83
- // Add custom methods
84
145
  this.addCustomMethods();
85
-
86
146
  this.api.debug('Setup completed');
87
147
  }
88
148
 
@@ -90,7 +150,6 @@
90
150
  * Auto-detect source from data attributes
91
151
  */
92
152
  autoDetectSource() {
93
- // Check data attributes
94
153
  const dataVideoId = this.api.video.getAttribute('data-cloudflare-video-id');
95
154
  const dataCustomerCode = this.api.video.getAttribute('data-cloudflare-customer');
96
155
  const dataVideoType = this.api.video.getAttribute('data-video-type');
@@ -104,19 +163,16 @@
104
163
  return;
105
164
  }
106
165
 
107
- // Check video src
108
166
  const src = this.api.video.src || this.api.video.currentSrc;
109
167
  if (src && this.isCloudflareUrl(src)) {
110
168
  this.extractFromUrl(src);
111
169
  return;
112
170
  }
113
171
 
114
- // Check source elements
115
172
  const sources = this.api.video.querySelectorAll('source');
116
173
  for (const source of sources) {
117
174
  const sourceSrc = source.getAttribute('src');
118
175
  const sourceType = source.getAttribute('type');
119
-
120
176
  if ((sourceType === 'video/cloudflare' || this.isCloudflareUrl(sourceSrc)) && sourceSrc) {
121
177
  this.extractFromUrl(sourceSrc);
122
178
  return;
@@ -124,25 +180,35 @@
124
180
  }
125
181
  }
126
182
 
127
- /**
128
- * Check if URL is a Cloudflare Stream URL
129
- */
130
183
  isCloudflareUrl(url) {
131
184
  if (!url) return false;
132
185
  return /cloudflarestream\.com|videodelivery\.net/.test(url);
133
186
  }
134
187
 
135
- /**
136
- * Extract video ID and customer code from URL
137
- */
188
+ isHLSManifest(url) {
189
+ if (!url) return false;
190
+ return /\.m3u8(\?.*)?$/.test(url) || /\/manifest\/video\.m3u8/.test(url);
191
+ }
192
+
193
+ isDASHManifest(url) {
194
+ if (!url) return false;
195
+ return /\.mpd(\?.*)?$/.test(url) || /\/manifest\/video\.mpd/.test(url);
196
+ }
197
+
138
198
  extractFromUrl(url) {
139
- // Extract video ID from various URL formats
140
- // https://customer-code.cloudflarestream.com/video-id/manifest/video.m3u8
141
- // https://videodelivery.net/video-id
199
+ if (this.isHLSManifest(url)) {
200
+ this.manifestType = 'hls';
201
+ this.options.manifestUrl = url;
202
+ this.api.debug('HLS manifest detected: ' + url);
203
+ } else if (this.isDASHManifest(url)) {
204
+ this.manifestType = 'dash';
205
+ this.options.manifestUrl = url;
206
+ this.api.debug('DASH manifest detected: ' + url);
207
+ }
142
208
 
143
209
  const match1 = url.match(/cloudflarestream\.com\/([a-f0-9]+)/);
144
210
  const match2 = url.match(/videodelivery\.net\/([a-f0-9]+)/);
145
- const match3 = url.match(/([a-z0-9-]+)\.cloudflarestream\.com/);
211
+ const match3 = url.match(/customer-([a-z0-9-]+)\.cloudflarestream\.com/);
146
212
 
147
213
  if (match1) {
148
214
  this.options.videoId = match1[1];
@@ -157,85 +223,1113 @@
157
223
  this.api.debug('Extracted - Video ID: ' + this.options.videoId + ', Customer: ' + this.options.customerCode);
158
224
  }
159
225
 
226
+ createStreamPlayer() {
227
+ if (!this.options.videoId && !this.options.videoUrl && !this.options.signedUrl && !this.options.manifestUrl) {
228
+ this.api.debug('No video source provided');
229
+ return;
230
+ }
231
+
232
+ const shouldUseManifest = (this.options.manifestUrl || this.manifestType) &&
233
+ this.options.useNativePlayer &&
234
+ !this.options.preferIframe;
235
+
236
+ if (shouldUseManifest) {
237
+ this.api.debug('Using native player with manifest');
238
+ this.createManifestPlayer();
239
+ } else {
240
+ this.api.debug('Using iframe player');
241
+ this.createIframePlayer();
242
+ }
243
+ }
244
+
245
+ async createManifestPlayer() {
246
+ this.isUsingManifest = true;
247
+
248
+ let manifestUrl = this.options.manifestUrl;
249
+ if (!manifestUrl && this.options.videoId) {
250
+ manifestUrl = this.buildManifestUrl();
251
+ }
252
+
253
+ if (!manifestUrl) {
254
+ this.api.debug('No manifest URL available');
255
+ return;
256
+ }
257
+
258
+ if (!this.manifestType) {
259
+ if (this.isHLSManifest(manifestUrl)) {
260
+ this.manifestType = 'hls';
261
+ } else if (this.isDASHManifest(manifestUrl)) {
262
+ this.manifestType = 'dash';
263
+ }
264
+ }
265
+
266
+ this.api.debug('Loading manifest: ' + manifestUrl + ' (type: ' + this.manifestType + ')');
267
+
268
+ const videoElement = this.api.video;
269
+ this.setupManifestEvents(videoElement);
270
+
271
+ if (this.options.muted) videoElement.muted = true;
272
+ if (this.options.loop) videoElement.loop = true;
273
+ if (this.options.poster) videoElement.poster = this.options.poster;
274
+ videoElement.preload = this.options.preload;
275
+
276
+ try {
277
+ if (this.manifestType === 'hls') {
278
+ await this.loadHLS(videoElement, manifestUrl);
279
+ } else if (this.manifestType === 'dash') {
280
+ await this.loadDASH(videoElement, manifestUrl);
281
+ } else {
282
+ videoElement.src = manifestUrl;
283
+ }
284
+
285
+ this.isPlayerReady = true;
286
+ this.forceReadyState();
287
+
288
+ if (this.options.autoplay) {
289
+ setTimeout(() => {
290
+ videoElement.play().catch(err => {
291
+ this.api.debug('Autoplay failed: ' + err.message);
292
+ });
293
+ }, 100);
294
+ }
295
+
296
+ this.api.triggerEvent('cloudflare:playerready', {
297
+ videoId: this.options.videoId,
298
+ mode: 'manifest',
299
+ type: this.manifestType
300
+ });
301
+
302
+ } catch (error) {
303
+ this.api.debug('Error loading manifest: ' + error.message);
304
+ this.api.triggerEvent('cloudflare:error', { error });
305
+ }
306
+ }
307
+
308
+ forceReadyState() {
309
+ const videoElement = this.api.video;
310
+
311
+ if (this.loadingCheckInterval) {
312
+ clearInterval(this.loadingCheckInterval);
313
+ }
314
+
315
+ let attempts = 0;
316
+ const maxAttempts = 50;
317
+
318
+ this.loadingCheckInterval = setInterval(() => {
319
+ attempts++;
320
+ const state = videoElement.readyState;
321
+
322
+ this.api.debug('ReadyState check #' + attempts + ': ' + state);
323
+
324
+ if (state >= 2) {
325
+ this.api.debug('Video ready! Triggering events...');
326
+
327
+ this.api.triggerEvent('loadstart', {});
328
+ this.api.triggerEvent('loadedmetadata', {});
329
+ this.api.triggerEvent('loadeddata', {});
330
+ this.api.triggerEvent('canplay', {});
331
+
332
+ if (state >= 3) {
333
+ this.api.triggerEvent('canplaythrough', {});
334
+ }
335
+
336
+ clearInterval(this.loadingCheckInterval);
337
+ this.loadingCheckInterval = null;
338
+ return;
339
+ }
340
+
341
+ if (attempts >= maxAttempts) {
342
+ this.api.debug('⚠️ Max attempts reached, forcing ready state anyway');
343
+
344
+ this.api.triggerEvent('loadedmetadata', {});
345
+ this.api.triggerEvent('canplay', {});
346
+
347
+ clearInterval(this.loadingCheckInterval);
348
+ this.loadingCheckInterval = null;
349
+ }
350
+ }, 100);
351
+ }
352
+
353
+ buildManifestUrl() {
354
+ if (!this.options.videoId) return null;
355
+
356
+ const baseUrl = this.options.customerCode
357
+ ? 'https://customer-' + this.options.customerCode + '.cloudflarestream.com/' + this.options.videoId
358
+ : 'https://videodelivery.net/' + this.options.videoId;
359
+
360
+ const manifestType = this.manifestType || 'hls';
361
+ const extension = manifestType === 'dash' ? 'video.mpd' : 'video.m3u8';
362
+
363
+ return baseUrl + '/manifest/' + extension;
364
+ }
365
+
366
+ async loadHLS(videoElement, url) {
367
+ // Native HLS support (Safari)
368
+ if (videoElement.canPlayType('application/vnd.apple.mpegurl')) {
369
+ this.api.debug('Using native HLS support');
370
+ videoElement.src = url;
371
+ videoElement.load();
372
+
373
+ // For native playback, qualities aren't accessible
374
+ this.availableQualities = [{
375
+ label: 'Auto',
376
+ value: 'auto',
377
+ active: true
378
+ }];
379
+ this.updateQualitySelector();
380
+ return;
381
+ }
382
+
383
+ if (this.options.autoLoadLibraries && typeof Hls === 'undefined') {
384
+ this.api.debug('Loading hls.js library...');
385
+ await loadLibrary('hlsjs', this.options.hlsLibraryUrl);
386
+ }
387
+
388
+ if (typeof Hls === 'undefined' || !Hls.isSupported()) {
389
+ this.api.debug('⚠️ hls.js not available/supported, using native playback');
390
+ videoElement.src = url;
391
+ videoElement.load();
392
+ return;
393
+ }
394
+
395
+ this.api.debug('Using hls.js for HLS playback');
396
+ const hlsConfig = {
397
+ debug: this.options.debug,
398
+ enableWorker: true,
399
+ lowLatencyMode: false,
400
+ backBufferLength: 90,
401
+ autoStartLoad: true,
402
+ startLevel: -1,
403
+ capLevelToPlayerSize: false,
404
+ ...this.options.hlsConfig
405
+ };
406
+
407
+ this.hlsInstance = new Hls(hlsConfig);
408
+
409
+ this.hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
410
+ this.api.debug('✓ HLS manifest parsed - ' + this.hlsInstance.levels.length + ' levels');
411
+
412
+ // Extract quality levels from HLS
413
+ this.extractHLSQualities();
414
+
415
+ this.api.triggerEvent('cloudflare:manifestloaded', {
416
+ levels: this.hlsInstance.levels,
417
+ qualities: this.availableQualities
418
+ });
419
+ });
420
+
421
+ this.hlsInstance.on(Hls.Events.LEVEL_SWITCHED, (event, data) => {
422
+ this.api.debug('HLS level switched to: ' + data.level);
423
+ this.updateCurrentQuality(data.level);
424
+ });
425
+
426
+ this.hlsInstance.on(Hls.Events.ERROR, (event, data) => {
427
+ this.api.debug('HLS error: ' + data.type + ' - ' + data.details);
428
+
429
+ if (data.fatal) {
430
+ switch (data.type) {
431
+ case Hls.ErrorTypes.NETWORK_ERROR:
432
+ this.api.debug('Network error, trying to recover...');
433
+ this.hlsInstance.startLoad();
434
+ break;
435
+ case Hls.ErrorTypes.MEDIA_ERROR:
436
+ this.api.debug('Media error, trying to recover...');
437
+ this.hlsInstance.recoverMediaError();
438
+ break;
439
+ default:
440
+ this.api.triggerEvent('cloudflare:error', data);
441
+ break;
442
+ }
443
+ }
444
+ });
445
+
446
+ this.hlsInstance.loadSource(url);
447
+ this.hlsInstance.attachMedia(videoElement);
448
+ }
449
+
160
450
  /**
161
- * Create Cloudflare Stream player
451
+ * Extract qualities from HLS levels
162
452
  */
163
- createStreamPlayer() {
453
+ extractHLSQualities() {
454
+ if (!this.hlsInstance) {
455
+ this.api.debug('No hlsInstance');
456
+ return;
457
+ }
458
+
459
+ const self = this;
460
+
461
+ setTimeout(() => {
462
+ try {
463
+ let levels = [];
464
+
465
+ if (self.hlsInstance.levels && self.hlsInstance.levels.length > 0) {
466
+ levels = self.hlsInstance.levels;
467
+ self.api.debug('HLS levels found: ' + levels.length);
468
+ }
469
+
470
+ if (levels.length === 0) {
471
+ self.api.debug('ERROR: No HLS levels found');
472
+ return;
473
+ }
474
+
475
+ levels.sort((a, b) => (a.height || 0) - (b.height || 0));
476
+
477
+ self.availableQualities = [];
478
+
479
+ self.availableQualities.push({
480
+ label: 'Auto',
481
+ value: -1,
482
+ height: null,
483
+ bitrate: null,
484
+ active: true
485
+ });
486
+
487
+ levels.forEach((level, index) => {
488
+ const h = level.height || 'Unknown';
489
+ const b = level.bitrate ? Math.round(level.bitrate / 1000) : null;
490
+
491
+ self.availableQualities.push({
492
+ label: h + 'p' + (b ? ' (' + b + 'k)' : ''),
493
+ value: index,
494
+ height: level.height,
495
+ width: level.width,
496
+ bitrate: level.bitrate,
497
+ active: false
498
+ });
499
+ });
500
+
501
+ self.api.debug('HLS Qualities extracted: ' + self.availableQualities.length);
502
+
503
+ if (self.api.player) {
504
+ self.api.player.qualities = self.availableQualities
505
+ .filter(q => q.value !== -1)
506
+ .map(q => ({
507
+ src: self.options.manifestUrl || self.buildManifestUrl(),
508
+ quality: q.label,
509
+ type: 'application/x-mpegURL',
510
+ height: q.height,
511
+ width: q.width,
512
+ bitrate: q.bitrate,
513
+ index: q.value
514
+ }));
515
+ }
516
+
517
+ self.updateQualitySelector();
518
+ self.createQualityControlButton();
519
+ self.startQualityMonitoring();
520
+
521
+ // Set initial quality based on defaultQuality
522
+ if (levels.length > 0) {
523
+ self.api.debug('🎯 Default quality option: ' + self.options.defaultQuality);
524
+
525
+ let targetIdx = -1;
526
+
527
+ if (self.options.defaultQuality === 'auto') {
528
+ targetIdx = -1;
529
+ self.api.debug('Starting with AUTO quality');
530
+ } else {
531
+ const targetQuality = self.availableQualities.find(q =>
532
+ q.label.toLowerCase().includes(self.options.defaultQuality.toLowerCase())
533
+ );
534
+
535
+ if (targetQuality && targetQuality.value !== -1) {
536
+ targetIdx = targetQuality.value;
537
+ self.api.debug('Starting with quality: ' + targetQuality.label);
538
+ } else {
539
+ targetIdx = levels.length - 1;
540
+ self.api.debug('Quality not found, using MAX: ' + levels[targetIdx].height + 'p');
541
+ }
542
+ }
543
+
544
+ try {
545
+ self.hlsInstance.currentLevel = targetIdx;
546
+ self.api.debug('✅ HLS quality set to: ' + (targetIdx === -1 ? 'auto' : levels[targetIdx].height + 'p'));
547
+ } catch (e) {
548
+ self.api.debug('❌ Error: ' + e.message);
549
+ }
550
+ }
551
+
552
+ } catch (error) {
553
+ self.api.debug('Extract error: ' + error.message);
554
+ }
555
+ }, 500);
556
+ }
557
+
558
+ async loadDASH(videoElement, url) {
559
+ if (this.options.autoLoadLibraries && typeof dashjs === 'undefined') {
560
+ this.api.debug('Loading dash.js library...');
561
+ await loadLibrary('dashjs', this.options.dashLibraryUrl);
562
+ }
563
+
564
+ if (typeof dashjs === 'undefined') {
565
+ this.api.debug('⚠️ dash.js not available, using native playback');
566
+ videoElement.src = url;
567
+ videoElement.load();
568
+ return;
569
+ }
570
+
571
+ this.api.debug('Using dash.js for DASH playback');
572
+
573
+ // Create dash.js player with initial configuration for high quality
574
+ const player = dashjs.MediaPlayer().create();
575
+
576
+ // CONFIGURE IMMEDIATELY to start with high quality
577
+ player.updateSettings({
578
+ streaming: {
579
+ abr: {
580
+ autoSwitchBitrate: {
581
+ video: false // Disable ABR initially
582
+ },
583
+ initialBitrate: {
584
+ video: 10000000 // 10 Mbps - force high quality at start
585
+ },
586
+ maxBitrate: {
587
+ video: 10000000
588
+ }
589
+ }
590
+ }
591
+ });
592
+
593
+ player.initialize(videoElement, url, this.options.autoplay);
594
+ this.streamPlayer = player;
595
+
596
+ player.on(dashjs.MediaPlayer.events.STREAM_INITIALIZED, () => {
597
+ this.api.debug('✓ DASH stream initialized');
598
+ this.extractDASHQualities();
599
+ });
600
+
601
+ player.on(dashjs.MediaPlayer.events.MANIFEST_LOADED, () => {
602
+ this.api.debug('✓ DASH manifest loaded');
603
+ this.api.triggerEvent('cloudflare:manifestloaded', {
604
+ qualities: this.availableQualities
605
+ });
606
+ });
607
+
608
+ player.on(dashjs.MediaPlayer.events.QUALITY_CHANGE_RENDERED, (e) => {
609
+ this.api.debug('DASH quality changed to: ' + e.newQuality);
610
+ this.updateCurrentQuality(e.newQuality);
611
+ });
612
+
613
+ player.on(dashjs.MediaPlayer.events.ERROR, (error) => {
614
+ this.api.debug('DASH error: ' + error.error);
615
+ this.api.triggerEvent('cloudflare:error', error);
616
+ });
617
+ }
618
+
619
+ /**
620
+ * Extract qualities from DASH bitrates
621
+ */
622
+ extractDASHQualities() {
623
+ if (!this.streamPlayer) {
624
+ this.api.debug('No streamPlayer');
625
+ return;
626
+ }
627
+
628
+ const self = this;
629
+
630
+ setTimeout(() => {
631
+ try {
632
+ let bitrates = [];
633
+
634
+ if (typeof self.streamPlayer.getTracksFor === 'function') {
635
+ const tracks = self.streamPlayer.getTracksFor('video');
636
+ self.api.debug('Tracks found: ' + tracks.length);
637
+
638
+ if (tracks && tracks.length > 0) {
639
+ const track = tracks[0];
640
+ if (track.bitrateList && track.bitrateList.length > 0) {
641
+ bitrates = track.bitrateList;
642
+ self.api.debug('Real qualities from bitrateList: ' + bitrates.length);
643
+ }
644
+ }
645
+ }
646
+
647
+ if (bitrates.length === 0) {
648
+ self.api.debug('ERROR: No qualities found in manifest');
649
+ return;
650
+ }
651
+
652
+ bitrates.sort((a, b) => (a.height || 0) - (b.height || 0));
653
+
654
+ self.availableQualities = [];
655
+
656
+ self.availableQualities.push({
657
+ label: 'Auto',
658
+ value: -1,
659
+ height: null,
660
+ bitrate: null,
661
+ active: true
662
+ });
663
+
664
+ bitrates.forEach((info, index) => {
665
+ const h = info.height;
666
+ const b = Math.round((info.bitrate || info.bandwidth || 0) / 1000);
667
+
668
+ self.availableQualities.push({
669
+ label: h + 'p (' + b + 'k)',
670
+ value: index,
671
+ height: info.height,
672
+ width: info.width,
673
+ bitrate: info.bitrate || info.bandwidth,
674
+ active: false
675
+ });
676
+ });
677
+
678
+ self.api.debug('Qualities extracted: ' + self.availableQualities.length);
679
+
680
+ if (self.api.player) {
681
+ self.api.player.qualities = self.availableQualities
682
+ .filter(q => q.value !== -1)
683
+ .map(q => ({
684
+ src: self.options.manifestUrl || self.buildManifestUrl(),
685
+ quality: q.label,
686
+ type: 'application/dash+xml',
687
+ height: q.height,
688
+ width: q.width,
689
+ bitrate: q.bitrate,
690
+ index: q.value
691
+ }));
692
+ }
693
+
694
+ self.updateQualitySelector();
695
+ self.createQualityControlButton();
696
+ self.startQualityMonitoring();
697
+
698
+ // Set initial quality based on defaultQuality
699
+ if (bitrates.length > 0) {
700
+ self.api.debug('🎯 Default quality option: ' + self.options.defaultQuality);
701
+
702
+ let targetIdx = -1;
703
+
704
+ if (self.options.defaultQuality === 'auto') {
705
+ targetIdx = -1;
706
+ self.api.debug('Starting with AUTO quality (ABR enabled)');
707
+ } else {
708
+ const targetQuality = self.availableQualities.find(q =>
709
+ q.label.toLowerCase().includes(self.options.defaultQuality.toLowerCase())
710
+ );
711
+
712
+ if (targetQuality && targetQuality.value !== -1) {
713
+ targetIdx = targetQuality.value;
714
+ self.api.debug('Starting with quality: ' + targetQuality.label);
715
+ } else {
716
+ targetIdx = bitrates.length - 1;
717
+ self.api.debug('Quality not found, using MAX: ' + bitrates[targetIdx].height + 'p');
718
+ }
719
+ }
720
+
721
+ try {
722
+ if (targetIdx === -1) {
723
+ self.streamPlayer.updateSettings({
724
+ streaming: {
725
+ abr: {
726
+ autoSwitchBitrate: { video: true }
727
+ }
728
+ }
729
+ });
730
+ self.api.debug('✅ ABR enabled for auto quality');
731
+ } else {
732
+ const targetBitrate = bitrates[targetIdx].bitrate || bitrates[targetIdx].bandwidth;
733
+
734
+ self.streamPlayer.updateSettings({
735
+ streaming: {
736
+ abr: {
737
+ autoSwitchBitrate: { video: false },
738
+ maxBitrate: { video: targetBitrate + 1000 },
739
+ minBitrate: { video: targetBitrate - 1000 },
740
+ initialBitrate: { video: targetBitrate }
741
+ }
742
+ }
743
+ });
744
+
745
+ self.api.debug('✅ Quality set to: ' + bitrates[targetIdx].height + 'p');
746
+ }
747
+
748
+ } catch (e) {
749
+ self.api.debug('❌ Error: ' + e.message);
750
+ }
751
+ }
752
+
753
+ } catch (error) {
754
+ self.api.debug('Extract error: ' + error.message);
755
+ }
756
+ }, 500);
757
+ }
758
+
759
+ /**
760
+ * Update current quality marker
761
+ */
762
+ updateCurrentQuality(qualityIndex) {
763
+ this.availableQualities.forEach(q => {
764
+ q.active = (q.value === qualityIndex);
765
+ });
766
+
767
+ this.currentQuality = qualityIndex;
768
+ this.updateQualitySelector();
769
+ }
770
+
771
+ /**
772
+ * Update quality selector in MYETV player
773
+ */
774
+ updateQualitySelector() {
775
+ if (this.availableQualities.length === 0) return;
776
+
777
+ // Trigger event for MYETV player to update quality selector
778
+ this.api.triggerEvent('qualitiesavailable', {
779
+ qualities: this.availableQualities,
780
+ current: this.currentQuality
781
+ });
782
+
783
+ this.api.debug('Quality selector updated with ' + this.availableQualities.length + ' qualities');
784
+ }
785
+
786
+ /**
787
+ * Change quality (called by MYETV player)
788
+ */
789
+
790
+ /**
791
+ * Force create quality button with multiple attempts
792
+ */
793
+ forceCreateQualityButton() {
794
+ this.api.debug('🔧 Forcing quality button creation');
795
+
796
+ this.createQualityButton();
797
+
798
+ setTimeout(() => {
799
+ this.api.debug('🔧 Retry #1');
800
+ this.createQualityButton();
801
+ }, 100);
802
+
803
+ setTimeout(() => {
804
+ this.api.debug('🔧 Retry #2');
805
+ this.createQualityButton();
806
+ }, 500);
807
+
808
+ setTimeout(() => {
809
+ this.api.debug('🔧 Final retry');
810
+ this.createQualityButton();
811
+ this.populateQualityMenu();
812
+ }, 1000);
813
+ }
814
+
815
+ /**
816
+ * Create quality button in controlbar
817
+ */
818
+ createQualityButton() {
819
+ this.api.debug('🎯 createQualityButton called');
820
+
821
+ if (!this.api.controls) {
822
+ this.api.debug('❌ Controls not found');
823
+ return false;
824
+ }
825
+
826
+ this.api.debug('✓ Controls found');
827
+
828
+ const existingBtn = this.api.controls.querySelector('.quality-btn');
829
+ if (existingBtn) {
830
+ this.api.debug('✓ Quality button exists');
831
+ const qualityControl = existingBtn.closest('.quality-control');
832
+ if (qualityControl) {
833
+ qualityControl.style.display = 'block';
834
+ }
835
+ return true;
836
+ }
837
+
838
+ this.api.debug('⚠️ Creating quality button');
839
+
840
+ const controlsRight = this.api.controls.querySelector('.controls-right');
841
+ if (!controlsRight) {
842
+ this.api.debug('❌ Controls-right not found');
843
+ return false;
844
+ }
845
+
846
+ const qualityControl = document.createElement('div');
847
+ qualityControl.className = 'quality-control';
848
+ qualityControl.style.display = 'block';
849
+
850
+ const qualityBtn = document.createElement('button');
851
+ qualityBtn.className = 'control-btn quality-btn';
852
+ qualityBtn.setAttribute('data-tooltip', 'videoquality');
853
+
854
+ const btnText = document.createElement('div');
855
+ btnText.className = 'quality-btn-text';
856
+
857
+ const selectedQuality = document.createElement('div');
858
+ selectedQuality.className = 'selected-quality';
859
+ selectedQuality.textContent = 'Auto';
860
+
861
+ const currentQuality = document.createElement('div');
862
+ currentQuality.className = 'current-quality';
863
+
864
+ btnText.appendChild(selectedQuality);
865
+ btnText.appendChild(currentQuality);
866
+ qualityBtn.appendChild(btnText);
867
+
868
+ const qualityMenu = document.createElement('div');
869
+ qualityMenu.className = 'quality-menu';
870
+ qualityMenu.style.display = 'none';
871
+
872
+ const autoOption = document.createElement('div');
873
+ autoOption.className = 'quality-option selected';
874
+ autoOption.setAttribute('data-quality', 'auto');
875
+ autoOption.textContent = 'Auto';
876
+ qualityMenu.appendChild(autoOption);
877
+
878
+ qualityControl.appendChild(qualityBtn);
879
+ qualityControl.appendChild(qualityMenu);
880
+
881
+ const fullscreenBtn = controlsRight.querySelector('.fullscreen-btn');
882
+ if (fullscreenBtn) {
883
+ controlsRight.insertBefore(qualityControl, fullscreenBtn);
884
+ } else {
885
+ controlsRight.appendChild(qualityControl);
886
+ }
887
+
888
+ this.api.debug('✅ Quality button created');
889
+
890
+ const self = this;
891
+
892
+ qualityBtn.addEventListener('click', function (e) {
893
+ e.stopPropagation();
894
+ const menu = this.nextElementSibling;
895
+ if (menu) {
896
+ menu.style.display = menu.style.display === 'block' ? 'none' : 'block';
897
+ }
898
+ });
899
+
900
+ qualityMenu.addEventListener('click', function (e) {
901
+ if (e.target.classList.contains('quality-option')) {
902
+ e.stopPropagation();
903
+ const quality = e.target.getAttribute('data-quality');
904
+
905
+ qualityMenu.querySelectorAll('.quality-option').forEach(function (opt) {
906
+ opt.classList.remove('selected');
907
+ });
908
+ e.target.classList.add('selected');
909
+
910
+ selectedQuality.textContent = e.target.textContent;
911
+ self.changeQuality(quality);
912
+ qualityMenu.style.display = 'none';
913
+ }
914
+ });
915
+
916
+ return true;
917
+ }
918
+
919
+ /**
920
+ * Populate quality menu
921
+ */
922
+ populateQualityMenu() {
923
+ const qualityMenu = this.api.controls?.querySelector('.quality-menu');
924
+ if (!qualityMenu) {
925
+ this.api.debug('❌ Menu not found');
926
+ return;
927
+ }
928
+
929
+ this.api.debug('📋 Populating: ' + this.availableQualities.length);
930
+
931
+ const existing = qualityMenu.querySelectorAll('.quality-option:not([data-quality="auto"])');
932
+ existing.forEach(function (opt) {
933
+ opt.remove();
934
+ });
935
+
936
+ this.availableQualities.forEach((quality) => {
937
+ if (quality.value === -1) return;
938
+
939
+ const option = document.createElement('div');
940
+ option.className = 'quality-option';
941
+ option.setAttribute('data-quality', quality.value.toString());
942
+ option.textContent = quality.label;
943
+
944
+ qualityMenu.appendChild(option);
945
+ });
946
+
947
+ this.api.debug('✅ Menu populated');
948
+ }
949
+
950
+ createQualityControlButton() {
951
+ const self = this;
952
+ let qualityControl = this.api.container.querySelector('.quality-control');
953
+
954
+ if (qualityControl) {
955
+ this.api.debug('Quality button exists');
956
+ return;
957
+ }
958
+
959
+ const controlsRight = this.api.container.querySelector('.controls-right');
960
+ if (!controlsRight) {
961
+ this.api.debug('No controls-right');
962
+ return;
963
+ }
964
+
965
+ const qualityHTML = `
966
+ <div class="quality-control">
967
+ <button class="control-btn quality-btn" data-tooltip="videoquality">
968
+ <div class="quality-btn-text">
969
+ <div class="selected-quality">Auto</div>
970
+ <div class="current-quality"></div>
971
+ </div>
972
+ </button>
973
+ <div class="quality-menu">
974
+ <div class="quality-option selected" data-quality="auto">Auto</div>
975
+ </div>
976
+ </div>
977
+ `;
978
+
979
+ const fullscreenBtn = controlsRight.querySelector('.fullscreen-btn');
980
+ if (fullscreenBtn) {
981
+ fullscreenBtn.insertAdjacentHTML('beforebegin', qualityHTML);
982
+ } else {
983
+ controlsRight.insertAdjacentHTML('beforeend', qualityHTML);
984
+ }
985
+
986
+ this.api.debug('✅ Quality button created');
987
+
988
+ // Popola il menu
989
+ setTimeout(() => {
990
+ const menu = this.api.container.querySelector('.quality-menu');
991
+ if (menu && this.availableQualities) {
992
+ this.availableQualities.forEach(q => {
993
+ if (q.value === -1) return;
994
+ const opt = document.createElement('div');
995
+ opt.className = 'quality-option';
996
+ opt.setAttribute('data-quality', q.value.toString());
997
+ opt.textContent = q.label;
998
+ menu.appendChild(opt);
999
+ });
1000
+ }
1001
+
1002
+ // Event listeners
1003
+ const btn = this.api.container.querySelector('.quality-btn');
1004
+ const qualityMenu = this.api.container.querySelector('.quality-menu');
1005
+
1006
+ if (btn) {
1007
+ btn.addEventListener('click', (e) => {
1008
+ e.stopPropagation();
1009
+ qualityMenu.classList.toggle('show');
1010
+ });
1011
+ }
1012
+
1013
+ if (qualityMenu) {
1014
+ qualityMenu.addEventListener('click', (e) => {
1015
+ if (e.target.classList.contains('quality-option')) {
1016
+ const quality = e.target.getAttribute('data-quality');
1017
+ self.changeQuality(quality);
1018
+
1019
+ qualityMenu.querySelectorAll('.quality-option').forEach(opt => {
1020
+ opt.classList.remove('selected');
1021
+ });
1022
+ e.target.classList.add('selected');
1023
+
1024
+ const selectedQuality = self.api.container.querySelector('.selected-quality');
1025
+ if (selectedQuality) {
1026
+ selectedQuality.textContent = e.target.textContent;
1027
+ }
1028
+
1029
+ qualityMenu.classList.remove('show');
1030
+ }
1031
+ });
1032
+ }
1033
+ }, 100);
1034
+ }
1035
+
1036
+ startQualityMonitoring() {
1037
+ if (this.qualityMonitorInterval) {
1038
+ clearInterval(this.qualityMonitorInterval);
1039
+ }
1040
+
1041
+ const self = this;
1042
+
1043
+ this.qualityMonitorInterval = setInterval(() => {
1044
+ let currentQualityIndex = -1;
1045
+ let currentQualityLabel = '';
1046
+
1047
+ // HLS
1048
+ if (self.hlsInstance) {
1049
+ currentQualityIndex = self.hlsInstance.currentLevel;
1050
+
1051
+ // Se è auto (-1), leggi quale livello sta effettivamente usando
1052
+ if (currentQualityIndex === -1 && self.hlsInstance.loadLevel !== -1) {
1053
+ currentQualityIndex = self.hlsInstance.loadLevel;
1054
+ }
1055
+
1056
+ if (currentQualityIndex >= 0 && self.hlsInstance.levels[currentQualityIndex]) {
1057
+ const level = self.hlsInstance.levels[currentQualityIndex];
1058
+ currentQualityLabel = level.height + 'p';
1059
+ }
1060
+ }
1061
+
1062
+ // DASH
1063
+ else if (self.streamPlayer) {
1064
+ try {
1065
+ // Try multiple methods to get current quality
1066
+
1067
+ // Metodo 1: getQualityFor
1068
+ if (typeof self.streamPlayer.getQualityFor === 'function') {
1069
+ currentQualityIndex = self.streamPlayer.getQualityFor('video');
1070
+ self.api.debug('DASH currentQuality index: ' + currentQualityIndex);
1071
+ }
1072
+
1073
+ // Metodo 2: Usa getBitrateInfoListFor per trovare quale bitrate è in uso
1074
+ if (currentQualityIndex === -1 || currentQualityIndex === undefined) {
1075
+ const settings = self.streamPlayer.getSettings();
1076
+ if (settings && settings.streaming && settings.streaming.abr) {
1077
+ // ABR è attivo, prova a leggere dal video stesso
1078
+ const videoEl = self.api.video;
1079
+ if (videoEl && videoEl.videoHeight) {
1080
+ // Find the quality closest to the current video height
1081
+ const tracks = self.streamPlayer.getTracksFor('video');
1082
+ if (tracks && tracks.length > 0 && tracks[0].bitrateList) {
1083
+ const bitrateList = tracks[0].bitrateList;
1084
+ for (let i = 0; i < bitrateList.length; i++) {
1085
+ if (bitrateList[i].height === videoEl.videoHeight) {
1086
+ currentQualityIndex = i;
1087
+ self.api.debug('Found quality by video height: ' + i);
1088
+ break;
1089
+ }
1090
+ }
1091
+ }
1092
+ }
1093
+ }
1094
+ }
1095
+
1096
+ // Converti l'indice in label
1097
+ if (currentQualityIndex >= 0) {
1098
+ const tracks = self.streamPlayer.getTracksFor('video');
1099
+ if (tracks && tracks.length > 0 && tracks[0].bitrateList) {
1100
+ const bitrateList = tracks[0].bitrateList;
1101
+ if (bitrateList[currentQualityIndex]) {
1102
+ currentQualityLabel = bitrateList[currentQualityIndex].height + 'p';
1103
+ self.api.debug('DASH quality label: ' + currentQualityLabel);
1104
+ }
1105
+ }
1106
+ }
1107
+ } catch (e) {
1108
+ self.api.debug('Error getting DASH quality: ' + e.message);
1109
+ }
1110
+ }
1111
+
1112
+ // Aggiorna il display solo se siamo in modalità Auto
1113
+ const selectedQualityDiv = self.api.container?.querySelector('.selected-quality');
1114
+ const currentQualityDiv = self.api.container?.querySelector('.current-quality');
1115
+
1116
+ if (selectedQualityDiv && currentQualityDiv) {
1117
+ const isAuto = selectedQualityDiv.textContent.trim() === 'Auto';
1118
+
1119
+ if (isAuto && currentQualityLabel) {
1120
+ currentQualityDiv.textContent = currentQualityLabel;
1121
+ currentQualityDiv.style.display = 'block';
1122
+ } else {
1123
+ currentQualityDiv.textContent = '';
1124
+ currentQualityDiv.style.display = 'none';
1125
+ }
1126
+ }
1127
+
1128
+ }, 1000);
1129
+
1130
+ this.api.debug('✅ Quality monitoring started');
1131
+ }
1132
+
1133
+ stopQualityMonitoring() {
1134
+ if (this.qualityMonitorInterval) {
1135
+ clearInterval(this.qualityMonitorInterval);
1136
+ this.qualityMonitorInterval = null;
1137
+ this.api.debug('⚠️ Quality monitoring stopped');
1138
+ }
1139
+ }
1140
+
1141
+ changeQuality(qualityValue) {
1142
+ this.api.debug('🔄 Change quality to: ' + qualityValue);
1143
+
1144
+ // Se è HLS, usa l'API di hls.js
1145
+ if (this.hlsInstance) {
1146
+ this.api.debug('🔄 HLS quality change');
1147
+
1148
+ // Auto
1149
+ if (qualityValue === 'auto' || qualityValue === '-1' || qualityValue === -1) {
1150
+ this.api.debug('🔄 HLS Auto quality');
1151
+ this.hlsInstance.currentLevel = -1; // -1 = auto in hls.js
1152
+ return;
1153
+ }
1154
+
1155
+ // Qualità specifica
1156
+ const qualityIndex = parseInt(qualityValue);
1157
+ this.api.debug('🎯 HLS quality index: ' + qualityIndex);
1158
+ this.hlsInstance.currentLevel = qualityIndex;
1159
+ this.api.debug('✅ HLS quality set');
1160
+ return;
1161
+ }
1162
+
1163
+
1164
+ if (!this.streamPlayer) {
1165
+ this.api.debug('❌ No streamPlayer');
1166
+ return;
1167
+ }
1168
+
1169
+ // Auto quality
1170
+ if (qualityValue === 'auto' || qualityValue === '-1' || qualityValue === -1) {
1171
+ this.api.debug('🔄 Enabling ABR (auto)');
1172
+ try {
1173
+ this.streamPlayer.updateSettings({
1174
+ streaming: {
1175
+ abr: {
1176
+ autoSwitchBitrate: {
1177
+ video: true
1178
+ },
1179
+ maxBitrate: {
1180
+ video: -1 // Rimuovi il limite
1181
+ },
1182
+ minBitrate: {
1183
+ video: -1
1184
+ }
1185
+ }
1186
+ }
1187
+ });
1188
+
1189
+ // Forza un piccolo seek per ricaricare
1190
+ const currentTime = this.api.video.currentTime;
1191
+ this.api.video.currentTime = currentTime + 0.1;
1192
+
1193
+ this.api.debug('✅ ABR enabled');
1194
+ } catch (e) {
1195
+ this.api.debug('❌ ABR error: ' + e.message);
1196
+ }
1197
+ return;
1198
+ }
1199
+
1200
+ // Qualità specifica
1201
+ const qualityIndex = parseInt(qualityValue);
1202
+ this.api.debug('🎯 Setting quality index: ' + qualityIndex);
1203
+
1204
+ try {
1205
+ const tracks = this.streamPlayer.getTracksFor('video');
1206
+ if (tracks && tracks.length > 0) {
1207
+ const bitrateList = tracks[0].bitrateList;
1208
+ if (bitrateList && bitrateList[qualityIndex]) {
1209
+ const targetBitrate = bitrateList[qualityIndex].bitrate || bitrateList[qualityIndex].bandwidth;
1210
+
1211
+ this.api.debug('🎯 Target bitrate: ' + Math.round(targetBitrate / 1000) + 'k');
1212
+
1213
+ // 1. Salva il tempo corrente
1214
+ const currentTime = this.api.video.currentTime;
1215
+ const wasPlaying = !this.api.video.paused;
1216
+
1217
+ // 2. Pausa il video
1218
+ this.api.video.pause();
1219
+
1220
+ // 3. Configura i limiti di bitrate
1221
+ this.streamPlayer.updateSettings({
1222
+ streaming: {
1223
+ abr: {
1224
+ autoSwitchBitrate: {
1225
+ video: false
1226
+ },
1227
+ maxBitrate: {
1228
+ video: targetBitrate + 1000 // Aggiungi 1k di margine
1229
+ },
1230
+ minBitrate: {
1231
+ video: targetBitrate - 1000 // Sottrai 1k di margine
1232
+ }
1233
+ }
1234
+ }
1235
+ });
1236
+
1237
+ // 4. Forza il reload facendo un seek
1238
+ setTimeout(() => {
1239
+ // Skip forward 0.1 seconds to force segment reload
1240
+ this.api.video.currentTime = currentTime + 0.1;
1241
+
1242
+ // Riprendi la riproduzione
1243
+ if (wasPlaying) {
1244
+ setTimeout(() => {
1245
+ this.api.video.play();
1246
+ }, 100);
1247
+ }
1248
+
1249
+ this.api.debug('✅ Quality changed with seek');
1250
+ }, 100);
1251
+ }
1252
+ }
1253
+
1254
+ } catch (e) {
1255
+ this.api.debug('❌ Error: ' + e.message);
1256
+ }
1257
+ }
1258
+
1259
+ setupManifestEvents(videoElement) {
1260
+ const events = ['loadstart', 'loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough',
1261
+ 'play', 'pause', 'playing', 'ended', 'timeupdate', 'volumechange',
1262
+ 'waiting', 'seeking', 'seeked', 'error', 'progress'];
1263
+
1264
+ events.forEach(eventName => {
1265
+ videoElement.addEventListener(eventName, (e) => {
1266
+ this.api.debug('📺 Event: ' + eventName + ' (readyState: ' + videoElement.readyState + ')');
1267
+ this.api.triggerEvent(eventName, e);
1268
+ });
1269
+ });
1270
+ }
1271
+
1272
+ createIframePlayer() {
1273
+ this.isUsingIframe = true;
1274
+
164
1275
  if (!this.options.videoId && !this.options.videoUrl && !this.options.signedUrl) {
165
- this.api.debug('No video source provided');
1276
+ this.api.debug('No video source for iframe player');
166
1277
  return;
167
1278
  }
168
1279
 
169
- // Hide native player
170
1280
  if (this.options.replaceNativePlayer) {
171
1281
  this.api.video.style.display = 'none';
172
1282
  }
173
1283
 
174
- // Create container
175
1284
  this.streamContainer = document.createElement('div');
176
1285
  this.streamContainer.className = 'cloudflare-stream-container';
177
- this.streamContainer.style.cssText = `
178
- position: absolute;
179
- top: 0;
180
- left: 0;
181
- width: 100%;
182
- height: 100%;
183
- z-index: 100;
184
- `;
185
-
186
- // Build iframe URL
1286
+ this.streamContainer.style.cssText = 'position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 100;';
1287
+
187
1288
  const iframeSrc = this.buildIframeUrl();
188
1289
 
189
- // Create iframe
190
1290
  this.streamIframe = document.createElement('iframe');
191
1291
  this.streamIframe.src = iframeSrc;
192
- this.streamIframe.style.cssText = `
193
- border: none;
194
- width: 100%;
195
- height: 100%;
196
- `;
1292
+ this.streamIframe.style.cssText = 'border: none; width: 100%; height: 100%;';
197
1293
  this.streamIframe.allow = 'accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;';
198
1294
  this.streamIframe.allowFullscreen = true;
199
1295
 
200
1296
  this.streamContainer.appendChild(this.streamIframe);
201
1297
  this.api.container.appendChild(this.streamContainer);
202
1298
 
203
- // Setup Stream object for API access
204
1299
  this.setupStreamAPI();
205
1300
 
206
- this.api.debug('Cloudflare Stream player created');
1301
+ // Iframe player has built-in quality selector
1302
+ this.availableQualities = [{
1303
+ label: 'Auto (Cloudflare)',
1304
+ value: 'auto',
1305
+ active: true
1306
+ }];
1307
+
1308
+ this.api.debug('Cloudflare Stream iframe player created');
207
1309
  this.api.triggerEvent('cloudflare:playerready', {
208
- videoId: this.options.videoId
1310
+ videoId: this.options.videoId,
1311
+ mode: 'iframe'
209
1312
  });
210
1313
  }
211
1314
 
212
- /**
213
- * Build iframe URL with all options
214
- */
215
1315
  buildIframeUrl() {
216
1316
  let baseUrl;
217
1317
 
218
- // Priority 1: Signed URL (for private videos)
219
1318
  if (this.options.signedUrl) {
220
1319
  return this.options.signedUrl;
221
1320
  }
222
1321
 
223
- // Priority 2: Full video URL
224
1322
  if (this.options.videoUrl) {
225
1323
  baseUrl = this.options.videoUrl;
226
- }
227
- // Priority 3: Build from video ID
228
- else if (this.options.videoId) {
1324
+ } else if (this.options.videoId) {
229
1325
  if (this.options.customerCode) {
230
- baseUrl = `https://customer-${this.options.customerCode}.cloudflarestream.com/${this.options.videoId}/iframe`;
1326
+ baseUrl = 'https://customer-' + this.options.customerCode + '.cloudflarestream.com/' + this.options.videoId + '/iframe';
231
1327
  } else {
232
- baseUrl = `https://iframe.videodelivery.net/${this.options.videoId}`;
1328
+ baseUrl = 'https://iframe.videodelivery.net/' + this.options.videoId;
233
1329
  }
234
1330
  }
235
1331
 
236
- // Add query parameters
237
1332
  const params = new URLSearchParams();
238
-
239
1333
  if (this.options.autoplay) params.append('autoplay', 'true');
240
1334
  if (this.options.muted) params.append('muted', 'true');
241
1335
  if (this.options.loop) params.append('loop', 'true');
@@ -248,90 +1342,60 @@
248
1342
  if (this.options.defaultTextTrack) params.append('defaultTextTrack', this.options.defaultTextTrack);
249
1343
 
250
1344
  const queryString = params.toString();
251
- return queryString ? `${baseUrl}?${queryString}` : baseUrl;
1345
+ return queryString ? baseUrl + '?' + queryString : baseUrl;
252
1346
  }
253
1347
 
254
- /**
255
- * Setup Stream API for iframe communication
256
- */
257
1348
  setupStreamAPI() {
258
- // Create Stream object wrapper
259
1349
  this.streamPlayer = {
260
1350
  iframe: this.streamIframe,
261
-
262
- // Playback control
263
1351
  play: () => this.sendCommand('play'),
264
1352
  pause: () => this.sendCommand('pause'),
265
-
266
- // Volume
267
1353
  mute: () => this.sendCommand('mute'),
268
1354
  unmute: () => this.sendCommand('unmute'),
269
-
270
- // Seeking
271
1355
  seek: (time) => this.sendCommand('seek', time),
272
-
273
- // Properties (these require message passing)
274
1356
  getCurrentTime: () => this.getProperty('currentTime'),
275
1357
  getDuration: () => this.getProperty('duration'),
276
1358
  getVolume: () => this.getProperty('volume'),
277
1359
  getPaused: () => this.getProperty('paused'),
278
1360
  getMuted: () => this.getProperty('muted'),
279
-
280
- // Setters
281
1361
  setVolume: (volume) => this.sendCommand('volume', volume),
282
1362
  setPlaybackRate: (rate) => this.sendCommand('playbackRate', rate)
283
1363
  };
284
1364
 
285
- // Listen for messages from iframe
286
1365
  this.setupMessageListener();
287
-
288
1366
  this.isPlayerReady = true;
289
1367
  }
290
1368
 
291
- /**
292
- * Send command to iframe
293
- */
294
1369
  sendCommand(command, value) {
295
1370
  if (!this.streamIframe || !this.streamIframe.contentWindow) {
296
1371
  return Promise.reject('Player not ready');
297
1372
  }
298
1373
 
299
- const message = value !== undefined
300
- ? { event: command, value: value }
301
- : { event: command };
302
-
1374
+ const message = value !== undefined ? { event: command, value: value } : { event: command };
303
1375
  this.streamIframe.contentWindow.postMessage(message, '*');
304
1376
  return Promise.resolve();
305
1377
  }
306
1378
 
307
- /**
308
- * Get property from iframe
309
- */
310
1379
  getProperty(property) {
311
1380
  return new Promise((resolve) => {
312
- // Note: Cloudflare Stream uses standard video events
313
- // Property getters work via event listeners
314
1381
  const handler = (e) => {
315
1382
  if (e.data && e.data.event === property) {
316
1383
  window.removeEventListener('message', handler);
317
1384
  resolve(e.data.value);
318
1385
  }
319
1386
  };
1387
+
320
1388
  window.addEventListener('message', handler);
321
1389
  this.sendCommand('get' + property.charAt(0).toUpperCase() + property.slice(1));
322
1390
  });
323
1391
  }
324
1392
 
325
- /**
326
- * Setup message listener for iframe events
327
- */
328
1393
  setupMessageListener() {
329
1394
  window.addEventListener('message', (event) => {
330
1395
  if (!event.data || !event.data.event) return;
331
1396
 
332
1397
  const data = event.data;
333
1398
 
334
- // Map Cloudflare Stream events to standard events
335
1399
  switch (data.event) {
336
1400
  case 'play':
337
1401
  this.api.triggerEvent('play', {});
@@ -344,209 +1408,201 @@
344
1408
  this.api.triggerEvent('ended', {});
345
1409
  break;
346
1410
  case 'timeupdate':
347
- this.api.triggerEvent('timeupdate', {
348
- currentTime: data.currentTime,
349
- duration: data.duration
350
- });
1411
+ this.api.triggerEvent('timeupdate', { currentTime: data.currentTime, duration: data.duration });
351
1412
  break;
352
1413
  case 'volumechange':
353
- this.api.triggerEvent('volumechange', {
354
- volume: data.volume,
355
- muted: data.muted
356
- });
1414
+ this.api.triggerEvent('volumechange', { volume: data.volume, muted: data.muted });
357
1415
  break;
358
1416
  case 'loadedmetadata':
359
1417
  this.api.triggerEvent('loadedmetadata', data);
360
- this.api.triggerEvent('cloudflare:ready', {});
361
1418
  break;
362
1419
  case 'error':
363
1420
  this.api.triggerEvent('error', data);
364
- this.api.triggerEvent('cloudflare:error', data);
365
1421
  break;
366
1422
  }
367
1423
  });
368
1424
  }
369
1425
 
370
1426
  /**
371
- * Add custom methods to player
1427
+ * Add custom methods to player API
372
1428
  */
373
1429
  addCustomMethods() {
374
- // Load video
375
- this.api.player.loadCloudflareVideo = (videoId, customerCode) => {
376
- return this.loadVideo(videoId, customerCode);
1430
+ this.api.player.loadCloudflareVideo = (videoId, customerCode, useManifest) => {
1431
+ return this.loadVideo(videoId, customerCode, useManifest);
377
1432
  };
378
1433
 
379
- // Get Stream player
380
- this.api.player.getCloudflarePlayer = () => {
381
- return this.streamPlayer;
1434
+ this.api.player.loadCloudflareManifest = (manifestUrl) => {
1435
+ return this.loadManifest(manifestUrl);
382
1436
  };
1437
+
1438
+ this.api.player.getCloudflarePlayer = () => this.streamPlayer;
1439
+ this.api.player.getHLSInstance = () => this.hlsInstance;
1440
+ this.api.player.isCloudflareUsingIframe = () => this.isUsingIframe;
1441
+ this.api.player.isCloudflareUsingManifest = () => this.isUsingManifest;
1442
+ this.api.player.getCloudflareQualities = () => this.availableQualities;
1443
+ this.api.player.setCloudflareQuality = (quality) => this.changeQuality(quality);
383
1444
  }
384
1445
 
385
- /**
386
- * Play
387
- */
1446
+ // Playback control methods
388
1447
  play() {
389
- if (!this.streamPlayer) {
390
- return Promise.reject('Player not initialized');
391
- }
1448
+ if (this.isUsingManifest) return this.api.video.play();
1449
+ if (!this.streamPlayer) return Promise.reject('Player not initialized');
392
1450
  return this.streamPlayer.play();
393
1451
  }
394
1452
 
395
- /**
396
- * Pause
397
- */
398
1453
  pause() {
399
- if (!this.streamPlayer) {
400
- return Promise.reject('Player not initialized');
1454
+ if (this.isUsingManifest) {
1455
+ this.api.video.pause();
1456
+ return Promise.resolve();
401
1457
  }
1458
+ if (!this.streamPlayer) return Promise.reject('Player not initialized');
402
1459
  return this.streamPlayer.pause();
403
1460
  }
404
1461
 
405
- /**
406
- * Seek
407
- */
408
1462
  seek(seconds) {
409
- if (!this.streamPlayer) {
410
- return Promise.reject('Player not initialized');
1463
+ if (this.isUsingManifest) {
1464
+ this.api.video.currentTime = seconds;
1465
+ return Promise.resolve();
411
1466
  }
1467
+ if (!this.streamPlayer) return Promise.reject('Player not initialized');
412
1468
  return this.streamPlayer.seek(seconds);
413
1469
  }
414
1470
 
415
- /**
416
- * Get current time
417
- */
418
1471
  getCurrentTime() {
419
- if (!this.streamPlayer) {
420
- return Promise.reject('Player not initialized');
421
- }
1472
+ if (this.isUsingManifest) return Promise.resolve(this.api.video.currentTime);
1473
+ if (!this.streamPlayer) return Promise.reject('Player not initialized');
422
1474
  return this.streamPlayer.getCurrentTime();
423
1475
  }
424
1476
 
425
- /**
426
- * Get duration
427
- */
428
1477
  getDuration() {
429
- if (!this.streamPlayer) {
430
- return Promise.reject('Player not initialized');
431
- }
1478
+ if (this.isUsingManifest) return Promise.resolve(this.api.video.duration);
1479
+ if (!this.streamPlayer) return Promise.reject('Player not initialized');
432
1480
  return this.streamPlayer.getDuration();
433
1481
  }
434
1482
 
435
- /**
436
- * Set volume
437
- */
438
1483
  setVolume(volume) {
439
- if (!this.streamPlayer) {
440
- return Promise.reject('Player not initialized');
1484
+ if (this.isUsingManifest) {
1485
+ this.api.video.volume = volume;
1486
+ return Promise.resolve();
441
1487
  }
1488
+ if (!this.streamPlayer) return Promise.reject('Player not initialized');
442
1489
  return this.streamPlayer.setVolume(volume);
443
1490
  }
444
1491
 
445
- /**
446
- * Get volume
447
- */
448
1492
  getVolume() {
449
- if (!this.streamPlayer) {
450
- return Promise.reject('Player not initialized');
451
- }
1493
+ if (this.isUsingManifest) return Promise.resolve(this.api.video.volume);
1494
+ if (!this.streamPlayer) return Promise.reject('Player not initialized');
452
1495
  return this.streamPlayer.getVolume();
453
1496
  }
454
1497
 
455
- /**
456
- * Mute
457
- */
458
1498
  mute() {
459
- if (!this.streamPlayer) {
460
- return Promise.reject('Player not initialized');
1499
+ if (this.isUsingManifest) {
1500
+ this.api.video.muted = true;
1501
+ return Promise.resolve();
461
1502
  }
1503
+ if (!this.streamPlayer) return Promise.reject('Player not initialized');
462
1504
  return this.streamPlayer.mute();
463
1505
  }
464
1506
 
465
- /**
466
- * Unmute
467
- */
468
1507
  unmute() {
469
- if (!this.streamPlayer) {
470
- return Promise.reject('Player not initialized');
1508
+ if (this.isUsingManifest) {
1509
+ this.api.video.muted = false;
1510
+ return Promise.resolve();
471
1511
  }
1512
+ if (!this.streamPlayer) return Promise.reject('Player not initialized');
472
1513
  return this.streamPlayer.unmute();
473
1514
  }
474
1515
 
475
- /**
476
- * Get muted state
477
- */
478
1516
  getMuted() {
479
- if (!this.streamPlayer) {
480
- return Promise.reject('Player not initialized');
481
- }
1517
+ if (this.isUsingManifest) return Promise.resolve(this.api.video.muted);
1518
+ if (!this.streamPlayer) return Promise.reject('Player not initialized');
482
1519
  return this.streamPlayer.getMuted();
483
1520
  }
484
1521
 
485
- /**
486
- * Get paused state
487
- */
488
1522
  getPaused() {
489
- if (!this.streamPlayer) {
490
- return Promise.reject('Player not initialized');
491
- }
1523
+ if (this.isUsingManifest) return Promise.resolve(this.api.video.paused);
1524
+ if (!this.streamPlayer) return Promise.reject('Player not initialized');
492
1525
  return this.streamPlayer.getPaused();
493
1526
  }
494
1527
 
495
- /**
496
- * Set playback rate
497
- */
498
1528
  setPlaybackRate(rate) {
499
- if (!this.streamPlayer) {
500
- return Promise.reject('Player not initialized');
1529
+ if (this.isUsingManifest) {
1530
+ this.api.video.playbackRate = rate;
1531
+ return Promise.resolve();
501
1532
  }
1533
+ if (!this.streamPlayer) return Promise.reject('Player not initialized');
502
1534
  return this.streamPlayer.setPlaybackRate(rate);
503
1535
  }
504
1536
 
505
- /**
506
- * Load new video
507
- */
508
- loadVideo(videoId, customerCode) {
1537
+ loadVideo(videoId, customerCode, useManifest) {
509
1538
  this.options.videoId = videoId;
510
- if (customerCode) {
511
- this.options.customerCode = customerCode;
512
- }
1539
+ if (customerCode) this.options.customerCode = customerCode;
1540
+ if (useManifest !== undefined) this.options.useNativePlayer = useManifest;
513
1541
 
514
- // Remove existing player
515
- if (this.streamContainer) {
516
- this.streamContainer.remove();
517
- }
518
-
519
- // Create new player
1542
+ this.disposePlayer();
520
1543
  this.createStreamPlayer();
521
-
522
1544
  this.api.triggerEvent('cloudflare:videoloaded', { videoId, customerCode });
523
1545
  return Promise.resolve(videoId);
524
1546
  }
525
1547
 
526
- /**
527
- * Dispose plugin
528
- */
529
- dispose() {
530
- this.api.debug('Disposing plugin');
1548
+ loadManifest(manifestUrl) {
1549
+ this.options.manifestUrl = manifestUrl;
1550
+ this.options.useNativePlayer = true;
1551
+ this.extractFromUrl(manifestUrl);
1552
+
1553
+ this.disposePlayer();
1554
+ this.createManifestPlayer();
1555
+ this.api.triggerEvent('cloudflare:manifestloaded', { manifestUrl });
1556
+ return Promise.resolve(manifestUrl);
1557
+ }
1558
+
1559
+ disposePlayer() {
1560
+ if (this.loadingCheckInterval) {
1561
+ clearInterval(this.loadingCheckInterval);
1562
+ this.loadingCheckInterval = null;
1563
+ }
1564
+
1565
+ if (this.hlsInstance) {
1566
+ this.hlsInstance.destroy();
1567
+ this.hlsInstance = null;
1568
+ }
531
1569
 
532
1570
  if (this.streamContainer) {
533
1571
  this.streamContainer.remove();
534
1572
  this.streamContainer = null;
535
1573
  }
536
1574
 
1575
+ if (this.streamPlayer && this.streamPlayer.destroy) {
1576
+ this.streamPlayer.destroy();
1577
+ }
1578
+
537
1579
  this.streamPlayer = null;
538
1580
  this.streamIframe = null;
1581
+ this.isUsingIframe = false;
1582
+ this.isUsingManifest = false;
1583
+ this.availableQualities = [];
1584
+ this.currentQuality = null;
1585
+ }
539
1586
 
540
- // Restore native player
541
- if (this.api.video && this.options.replaceNativePlayer) {
542
- this.api.video.style.display = '';
1587
+ dispose() {
1588
+ this.api.debug('Disposing plugin');
1589
+
1590
+ this.stopQualityMonitoring(); // AGGIUNGI QUESTA RIGA
1591
+
1592
+ if (this.hlsInstance) {
1593
+ this.hlsInstance.destroy();
1594
+ this.hlsInstance = null;
543
1595
  }
544
1596
 
545
- this.api.debug('Plugin disposed');
1597
+ if (this.streamPlayer) {
1598
+ this.streamPlayer.reset();
1599
+ this.streamPlayer = null;
1600
+ }
546
1601
  }
1602
+
547
1603
  }
548
1604
 
549
- // Register plugin globally
1605
+ // Register plugin
550
1606
  if (typeof window.registerMYETVPlugin === 'function') {
551
1607
  window.registerMYETVPlugin('cloudflare', CloudflareStreamPlugin);
552
1608
  } else {