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.
- package/README.md +76 -2
- package/css/myetv-player.css +321 -208
- package/css/myetv-player.min.css +1 -1
- package/dist/myetv-player.js +219 -37
- package/dist/myetv-player.min.js +204 -26
- package/package.json +3 -1
- package/plugins/cloudflare/README.md +26 -4
- package/plugins/cloudflare/myetv-player-cloudflare-stream-plugin.js +1273 -217
- package/plugins/facebook/myetv-player-facebook-plugin.js +1340 -164
- package/plugins/twitch/myetv-player-twitch-plugin.js +428 -167
- package/plugins/vimeo/README.md +1 -1
- package/plugins/vimeo/myetv-player-vimeo.js +560 -247
- package/plugins/youtube/README.md +18 -7
- package/plugins/youtube/myetv-player-youtube-plugin.js +1485 -190
- package/scss/_base.scss +0 -15
- package/scss/_controls.scss +182 -2
- package/scss/_menus.scss +51 -0
- package/scss/_responsive.scss +187 -321
- package/scss/_title-overlay.scss +27 -0
- package/scss/_video.scss +0 -75
- package/scss/_watermark.scss +120 -0
- package/scss/myetv-player.scss +7 -7
- package/src/controls.js +72 -21
- package/src/core.js +43 -5
- package/src/events.js +33 -5
- package/src/utils.js +20 -6
- package/src/watermark.js +51 -0
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 || '
|
|
89
|
+
preload: options.preload || 'auto',
|
|
28
90
|
controls: options.controls !== false,
|
|
29
91
|
defaultTextTrack: options.defaultTextTrack || null,
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
*
|
|
451
|
+
* Extract qualities from HLS levels
|
|
162
452
|
*/
|
|
163
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
1326
|
+
baseUrl = 'https://customer-' + this.options.customerCode + '.cloudflarestream.com/' + this.options.videoId + '/iframe';
|
|
231
1327
|
} else {
|
|
232
|
-
baseUrl =
|
|
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 ?
|
|
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
|
-
|
|
375
|
-
|
|
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
|
-
|
|
380
|
-
|
|
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 (
|
|
390
|
-
|
|
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 (
|
|
400
|
-
|
|
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 (
|
|
410
|
-
|
|
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 (
|
|
420
|
-
|
|
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 (
|
|
430
|
-
|
|
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 (
|
|
440
|
-
|
|
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 (
|
|
450
|
-
|
|
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 (
|
|
460
|
-
|
|
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 (
|
|
470
|
-
|
|
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 (
|
|
480
|
-
|
|
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 (
|
|
490
|
-
|
|
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 (
|
|
500
|
-
|
|
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
|
-
|
|
512
|
-
}
|
|
1539
|
+
if (customerCode) this.options.customerCode = customerCode;
|
|
1540
|
+
if (useManifest !== undefined) this.options.useNativePlayer = useManifest;
|
|
513
1541
|
|
|
514
|
-
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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.
|
|
1597
|
+
if (this.streamPlayer) {
|
|
1598
|
+
this.streamPlayer.reset();
|
|
1599
|
+
this.streamPlayer = null;
|
|
1600
|
+
}
|
|
546
1601
|
}
|
|
1602
|
+
|
|
547
1603
|
}
|
|
548
1604
|
|
|
549
|
-
// Register plugin
|
|
1605
|
+
// Register plugin
|
|
550
1606
|
if (typeof window.registerMYETVPlugin === 'function') {
|
|
551
1607
|
window.registerMYETVPlugin('cloudflare', CloudflareStreamPlugin);
|
|
552
1608
|
} else {
|