myetv-player 1.0.0 → 1.0.6

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.
Files changed (37) hide show
  1. package/.github/workflows/codeql.yml +100 -0
  2. package/README.md +36 -58
  3. package/SECURITY.md +50 -0
  4. package/css/myetv-player.css +301 -218
  5. package/css/myetv-player.min.css +1 -1
  6. package/dist/myetv-player.js +1713 -1503
  7. package/dist/myetv-player.min.js +1670 -1471
  8. package/package.json +6 -1
  9. package/plugins/README.md +1016 -0
  10. package/plugins/cloudflare/README.md +1068 -0
  11. package/plugins/cloudflare/myetv-player-cloudflare-stream-plugin.js +556 -0
  12. package/plugins/facebook/README.md +1024 -0
  13. package/plugins/facebook/myetv-player-facebook-plugin.js +437 -0
  14. package/plugins/gamepad-remote-controller/README.md +816 -0
  15. package/plugins/gamepad-remote-controller/myetv-player-gamepad-remote-plugin.js +678 -0
  16. package/plugins/google-adsense-ads/README.md +1 -0
  17. package/plugins/google-adsense-ads/g-adsense-ads-plugin.js +158 -0
  18. package/plugins/google-ima-ads/README.md +1 -0
  19. package/plugins/google-ima-ads/g-ima-ads-plugin.js +355 -0
  20. package/plugins/twitch/README.md +1185 -0
  21. package/plugins/twitch/myetv-player-twitch-plugin.js +569 -0
  22. package/plugins/vast-vpaid-ads/README.md +1 -0
  23. package/plugins/vast-vpaid-ads/vast-vpaid-ads-plugin.js +346 -0
  24. package/plugins/vimeo/README.md +1416 -0
  25. package/plugins/vimeo/myetv-player-vimeo.js +640 -0
  26. package/plugins/youtube/README.md +851 -0
  27. package/plugins/youtube/myetv-player-youtube-plugin.js +1714 -210
  28. package/scss/README.md +160 -0
  29. package/scss/_menus.scss +840 -672
  30. package/scss/_responsive.scss +67 -105
  31. package/scss/_volume.scss +67 -105
  32. package/src/README.md +559 -0
  33. package/src/controls.js +16 -4
  34. package/src/core.js +1192 -1062
  35. package/src/i18n.js +27 -1
  36. package/src/quality.js +478 -436
  37. package/src/subtitles.js +2 -2
@@ -0,0 +1,556 @@
1
+ /**
2
+ * MYETV Player - Cloudflare Stream Plugin
3
+ * File: myetv-player-cloudflare-stream-plugin.js
4
+ * Integrates Cloudflare Stream videos with full API control
5
+ * Created by https://www.myetv.tv https://oskarcosimo.com
6
+ */
7
+
8
+ (function () {
9
+ 'use strict';
10
+
11
+ class CloudflareStreamPlugin {
12
+ constructor(player, options = {}) {
13
+ this.player = player;
14
+ 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
24
+ autoplay: options.autoplay || false,
25
+ muted: options.muted || false,
26
+ loop: options.loop || false,
27
+ preload: options.preload || 'metadata', // 'none', 'metadata', 'auto'
28
+ controls: options.controls !== false,
29
+ defaultTextTrack: options.defaultTextTrack || null,
30
+
31
+ // Player customization
32
+ poster: options.poster || null, // Custom poster image
33
+ primaryColor: options.primaryColor || null, // Custom player color
34
+ 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
41
+ debug: options.debug || false,
42
+ replaceNativePlayer: options.replaceNativePlayer !== false,
43
+ autoLoadFromData: options.autoLoadFromData !== false,
44
+ responsive: options.responsive !== false,
45
+
46
+ ...options
47
+ };
48
+
49
+ this.streamPlayer = null;
50
+ this.streamIframe = null;
51
+ this.streamContainer = null;
52
+ this.isPlayerReady = false;
53
+
54
+ // Get plugin API
55
+ this.api = player.getPluginAPI ? player.getPluginAPI() : {
56
+ player: player,
57
+ video: player.video,
58
+ container: player.container,
59
+ controls: player.controls,
60
+ debug: (msg) => {
61
+ if (this.options.debug) console.log('☁️ Cloudflare Stream:', msg);
62
+ },
63
+ triggerEvent: (event, data) => player.triggerEvent(event, data)
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Setup plugin
69
+ */
70
+ setup() {
71
+ this.api.debug('Setup started');
72
+
73
+ // Auto-detect from data attributes
74
+ if (this.options.autoLoadFromData) {
75
+ this.autoDetectSource();
76
+ }
77
+
78
+ // Create player if we have a source
79
+ if (this.options.videoId || this.options.videoUrl || this.options.signedUrl) {
80
+ this.createStreamPlayer();
81
+ }
82
+
83
+ // Add custom methods
84
+ this.addCustomMethods();
85
+
86
+ this.api.debug('Setup completed');
87
+ }
88
+
89
+ /**
90
+ * Auto-detect source from data attributes
91
+ */
92
+ autoDetectSource() {
93
+ // Check data attributes
94
+ const dataVideoId = this.api.video.getAttribute('data-cloudflare-video-id');
95
+ const dataCustomerCode = this.api.video.getAttribute('data-cloudflare-customer');
96
+ const dataVideoType = this.api.video.getAttribute('data-video-type');
97
+
98
+ if (dataVideoId && dataVideoType === 'cloudflare') {
99
+ this.options.videoId = dataVideoId;
100
+ if (dataCustomerCode) {
101
+ this.options.customerCode = dataCustomerCode;
102
+ }
103
+ this.api.debug('Video ID detected: ' + dataVideoId);
104
+ return;
105
+ }
106
+
107
+ // Check video src
108
+ const src = this.api.video.src || this.api.video.currentSrc;
109
+ if (src && this.isCloudflareUrl(src)) {
110
+ this.extractFromUrl(src);
111
+ return;
112
+ }
113
+
114
+ // Check source elements
115
+ const sources = this.api.video.querySelectorAll('source');
116
+ for (const source of sources) {
117
+ const sourceSrc = source.getAttribute('src');
118
+ const sourceType = source.getAttribute('type');
119
+
120
+ if ((sourceType === 'video/cloudflare' || this.isCloudflareUrl(sourceSrc)) && sourceSrc) {
121
+ this.extractFromUrl(sourceSrc);
122
+ return;
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Check if URL is a Cloudflare Stream URL
129
+ */
130
+ isCloudflareUrl(url) {
131
+ if (!url) return false;
132
+ return /cloudflarestream\.com|videodelivery\.net/.test(url);
133
+ }
134
+
135
+ /**
136
+ * Extract video ID and customer code from URL
137
+ */
138
+ 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
142
+
143
+ const match1 = url.match(/cloudflarestream\.com\/([a-f0-9]+)/);
144
+ const match2 = url.match(/videodelivery\.net\/([a-f0-9]+)/);
145
+ const match3 = url.match(/([a-z0-9-]+)\.cloudflarestream\.com/);
146
+
147
+ if (match1) {
148
+ this.options.videoId = match1[1];
149
+ } else if (match2) {
150
+ this.options.videoId = match2[1];
151
+ }
152
+
153
+ if (match3) {
154
+ this.options.customerCode = match3[1];
155
+ }
156
+
157
+ this.api.debug('Extracted - Video ID: ' + this.options.videoId + ', Customer: ' + this.options.customerCode);
158
+ }
159
+
160
+ /**
161
+ * Create Cloudflare Stream player
162
+ */
163
+ createStreamPlayer() {
164
+ if (!this.options.videoId && !this.options.videoUrl && !this.options.signedUrl) {
165
+ this.api.debug('No video source provided');
166
+ return;
167
+ }
168
+
169
+ // Hide native player
170
+ if (this.options.replaceNativePlayer) {
171
+ this.api.video.style.display = 'none';
172
+ }
173
+
174
+ // Create container
175
+ this.streamContainer = document.createElement('div');
176
+ 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
187
+ const iframeSrc = this.buildIframeUrl();
188
+
189
+ // Create iframe
190
+ this.streamIframe = document.createElement('iframe');
191
+ this.streamIframe.src = iframeSrc;
192
+ this.streamIframe.style.cssText = `
193
+ border: none;
194
+ width: 100%;
195
+ height: 100%;
196
+ `;
197
+ this.streamIframe.allow = 'accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;';
198
+ this.streamIframe.allowFullscreen = true;
199
+
200
+ this.streamContainer.appendChild(this.streamIframe);
201
+ this.api.container.appendChild(this.streamContainer);
202
+
203
+ // Setup Stream object for API access
204
+ this.setupStreamAPI();
205
+
206
+ this.api.debug('Cloudflare Stream player created');
207
+ this.api.triggerEvent('cloudflare:playerready', {
208
+ videoId: this.options.videoId
209
+ });
210
+ }
211
+
212
+ /**
213
+ * Build iframe URL with all options
214
+ */
215
+ buildIframeUrl() {
216
+ let baseUrl;
217
+
218
+ // Priority 1: Signed URL (for private videos)
219
+ if (this.options.signedUrl) {
220
+ return this.options.signedUrl;
221
+ }
222
+
223
+ // Priority 2: Full video URL
224
+ if (this.options.videoUrl) {
225
+ baseUrl = this.options.videoUrl;
226
+ }
227
+ // Priority 3: Build from video ID
228
+ else if (this.options.videoId) {
229
+ if (this.options.customerCode) {
230
+ baseUrl = `https://customer-${this.options.customerCode}.cloudflarestream.com/${this.options.videoId}/iframe`;
231
+ } else {
232
+ baseUrl = `https://iframe.videodelivery.net/${this.options.videoId}`;
233
+ }
234
+ }
235
+
236
+ // Add query parameters
237
+ const params = new URLSearchParams();
238
+
239
+ if (this.options.autoplay) params.append('autoplay', 'true');
240
+ if (this.options.muted) params.append('muted', 'true');
241
+ if (this.options.loop) params.append('loop', 'true');
242
+ if (this.options.controls === false) params.append('controls', 'false');
243
+ if (this.options.preload) params.append('preload', this.options.preload);
244
+ if (this.options.poster) params.append('poster', encodeURIComponent(this.options.poster));
245
+ if (this.options.primaryColor) params.append('primaryColor', this.options.primaryColor.replace('#', ''));
246
+ if (this.options.startTime) params.append('startTime', this.options.startTime);
247
+ if (this.options.adUrl) params.append('ad-url', encodeURIComponent(this.options.adUrl));
248
+ if (this.options.defaultTextTrack) params.append('defaultTextTrack', this.options.defaultTextTrack);
249
+
250
+ const queryString = params.toString();
251
+ return queryString ? `${baseUrl}?${queryString}` : baseUrl;
252
+ }
253
+
254
+ /**
255
+ * Setup Stream API for iframe communication
256
+ */
257
+ setupStreamAPI() {
258
+ // Create Stream object wrapper
259
+ this.streamPlayer = {
260
+ iframe: this.streamIframe,
261
+
262
+ // Playback control
263
+ play: () => this.sendCommand('play'),
264
+ pause: () => this.sendCommand('pause'),
265
+
266
+ // Volume
267
+ mute: () => this.sendCommand('mute'),
268
+ unmute: () => this.sendCommand('unmute'),
269
+
270
+ // Seeking
271
+ seek: (time) => this.sendCommand('seek', time),
272
+
273
+ // Properties (these require message passing)
274
+ getCurrentTime: () => this.getProperty('currentTime'),
275
+ getDuration: () => this.getProperty('duration'),
276
+ getVolume: () => this.getProperty('volume'),
277
+ getPaused: () => this.getProperty('paused'),
278
+ getMuted: () => this.getProperty('muted'),
279
+
280
+ // Setters
281
+ setVolume: (volume) => this.sendCommand('volume', volume),
282
+ setPlaybackRate: (rate) => this.sendCommand('playbackRate', rate)
283
+ };
284
+
285
+ // Listen for messages from iframe
286
+ this.setupMessageListener();
287
+
288
+ this.isPlayerReady = true;
289
+ }
290
+
291
+ /**
292
+ * Send command to iframe
293
+ */
294
+ sendCommand(command, value) {
295
+ if (!this.streamIframe || !this.streamIframe.contentWindow) {
296
+ return Promise.reject('Player not ready');
297
+ }
298
+
299
+ const message = value !== undefined
300
+ ? { event: command, value: value }
301
+ : { event: command };
302
+
303
+ this.streamIframe.contentWindow.postMessage(message, '*');
304
+ return Promise.resolve();
305
+ }
306
+
307
+ /**
308
+ * Get property from iframe
309
+ */
310
+ getProperty(property) {
311
+ return new Promise((resolve) => {
312
+ // Note: Cloudflare Stream uses standard video events
313
+ // Property getters work via event listeners
314
+ const handler = (e) => {
315
+ if (e.data && e.data.event === property) {
316
+ window.removeEventListener('message', handler);
317
+ resolve(e.data.value);
318
+ }
319
+ };
320
+ window.addEventListener('message', handler);
321
+ this.sendCommand('get' + property.charAt(0).toUpperCase() + property.slice(1));
322
+ });
323
+ }
324
+
325
+ /**
326
+ * Setup message listener for iframe events
327
+ */
328
+ setupMessageListener() {
329
+ window.addEventListener('message', (event) => {
330
+ if (!event.data || !event.data.event) return;
331
+
332
+ const data = event.data;
333
+
334
+ // Map Cloudflare Stream events to standard events
335
+ switch (data.event) {
336
+ case 'play':
337
+ this.api.triggerEvent('play', {});
338
+ this.api.triggerEvent('playing', {});
339
+ break;
340
+ case 'pause':
341
+ this.api.triggerEvent('pause', {});
342
+ break;
343
+ case 'ended':
344
+ this.api.triggerEvent('ended', {});
345
+ break;
346
+ case 'timeupdate':
347
+ this.api.triggerEvent('timeupdate', {
348
+ currentTime: data.currentTime,
349
+ duration: data.duration
350
+ });
351
+ break;
352
+ case 'volumechange':
353
+ this.api.triggerEvent('volumechange', {
354
+ volume: data.volume,
355
+ muted: data.muted
356
+ });
357
+ break;
358
+ case 'loadedmetadata':
359
+ this.api.triggerEvent('loadedmetadata', data);
360
+ this.api.triggerEvent('cloudflare:ready', {});
361
+ break;
362
+ case 'error':
363
+ this.api.triggerEvent('error', data);
364
+ this.api.triggerEvent('cloudflare:error', data);
365
+ break;
366
+ }
367
+ });
368
+ }
369
+
370
+ /**
371
+ * Add custom methods to player
372
+ */
373
+ addCustomMethods() {
374
+ // Load video
375
+ this.api.player.loadCloudflareVideo = (videoId, customerCode) => {
376
+ return this.loadVideo(videoId, customerCode);
377
+ };
378
+
379
+ // Get Stream player
380
+ this.api.player.getCloudflarePlayer = () => {
381
+ return this.streamPlayer;
382
+ };
383
+ }
384
+
385
+ /**
386
+ * Play
387
+ */
388
+ play() {
389
+ if (!this.streamPlayer) {
390
+ return Promise.reject('Player not initialized');
391
+ }
392
+ return this.streamPlayer.play();
393
+ }
394
+
395
+ /**
396
+ * Pause
397
+ */
398
+ pause() {
399
+ if (!this.streamPlayer) {
400
+ return Promise.reject('Player not initialized');
401
+ }
402
+ return this.streamPlayer.pause();
403
+ }
404
+
405
+ /**
406
+ * Seek
407
+ */
408
+ seek(seconds) {
409
+ if (!this.streamPlayer) {
410
+ return Promise.reject('Player not initialized');
411
+ }
412
+ return this.streamPlayer.seek(seconds);
413
+ }
414
+
415
+ /**
416
+ * Get current time
417
+ */
418
+ getCurrentTime() {
419
+ if (!this.streamPlayer) {
420
+ return Promise.reject('Player not initialized');
421
+ }
422
+ return this.streamPlayer.getCurrentTime();
423
+ }
424
+
425
+ /**
426
+ * Get duration
427
+ */
428
+ getDuration() {
429
+ if (!this.streamPlayer) {
430
+ return Promise.reject('Player not initialized');
431
+ }
432
+ return this.streamPlayer.getDuration();
433
+ }
434
+
435
+ /**
436
+ * Set volume
437
+ */
438
+ setVolume(volume) {
439
+ if (!this.streamPlayer) {
440
+ return Promise.reject('Player not initialized');
441
+ }
442
+ return this.streamPlayer.setVolume(volume);
443
+ }
444
+
445
+ /**
446
+ * Get volume
447
+ */
448
+ getVolume() {
449
+ if (!this.streamPlayer) {
450
+ return Promise.reject('Player not initialized');
451
+ }
452
+ return this.streamPlayer.getVolume();
453
+ }
454
+
455
+ /**
456
+ * Mute
457
+ */
458
+ mute() {
459
+ if (!this.streamPlayer) {
460
+ return Promise.reject('Player not initialized');
461
+ }
462
+ return this.streamPlayer.mute();
463
+ }
464
+
465
+ /**
466
+ * Unmute
467
+ */
468
+ unmute() {
469
+ if (!this.streamPlayer) {
470
+ return Promise.reject('Player not initialized');
471
+ }
472
+ return this.streamPlayer.unmute();
473
+ }
474
+
475
+ /**
476
+ * Get muted state
477
+ */
478
+ getMuted() {
479
+ if (!this.streamPlayer) {
480
+ return Promise.reject('Player not initialized');
481
+ }
482
+ return this.streamPlayer.getMuted();
483
+ }
484
+
485
+ /**
486
+ * Get paused state
487
+ */
488
+ getPaused() {
489
+ if (!this.streamPlayer) {
490
+ return Promise.reject('Player not initialized');
491
+ }
492
+ return this.streamPlayer.getPaused();
493
+ }
494
+
495
+ /**
496
+ * Set playback rate
497
+ */
498
+ setPlaybackRate(rate) {
499
+ if (!this.streamPlayer) {
500
+ return Promise.reject('Player not initialized');
501
+ }
502
+ return this.streamPlayer.setPlaybackRate(rate);
503
+ }
504
+
505
+ /**
506
+ * Load new video
507
+ */
508
+ loadVideo(videoId, customerCode) {
509
+ this.options.videoId = videoId;
510
+ if (customerCode) {
511
+ this.options.customerCode = customerCode;
512
+ }
513
+
514
+ // Remove existing player
515
+ if (this.streamContainer) {
516
+ this.streamContainer.remove();
517
+ }
518
+
519
+ // Create new player
520
+ this.createStreamPlayer();
521
+
522
+ this.api.triggerEvent('cloudflare:videoloaded', { videoId, customerCode });
523
+ return Promise.resolve(videoId);
524
+ }
525
+
526
+ /**
527
+ * Dispose plugin
528
+ */
529
+ dispose() {
530
+ this.api.debug('Disposing plugin');
531
+
532
+ if (this.streamContainer) {
533
+ this.streamContainer.remove();
534
+ this.streamContainer = null;
535
+ }
536
+
537
+ this.streamPlayer = null;
538
+ this.streamIframe = null;
539
+
540
+ // Restore native player
541
+ if (this.api.video && this.options.replaceNativePlayer) {
542
+ this.api.video.style.display = '';
543
+ }
544
+
545
+ this.api.debug('Plugin disposed');
546
+ }
547
+ }
548
+
549
+ // Register plugin globally
550
+ if (typeof window.registerMYETVPlugin === 'function') {
551
+ window.registerMYETVPlugin('cloudflare', CloudflareStreamPlugin);
552
+ } else {
553
+ console.error('☁️ MYETV Player plugin system not found');
554
+ }
555
+
556
+ })();