myetv-player 1.0.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.
Files changed (50) hide show
  1. package/.github/workflows/npm-publish.yml +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +866 -0
  4. package/build.js +189 -0
  5. package/css/README.md +1 -0
  6. package/css/myetv-player.css +13702 -0
  7. package/css/myetv-player.min.css +1 -0
  8. package/dist/README.md +1 -0
  9. package/dist/myetv-player.js +6408 -0
  10. package/dist/myetv-player.min.js +6183 -0
  11. package/package.json +27 -0
  12. package/plugins/README.md +1 -0
  13. package/plugins/google-analytics/README.md +1 -0
  14. package/plugins/google-analytics/myetv-player-g-analytics-plugin.js +548 -0
  15. package/plugins/youtube/README.md +1 -0
  16. package/plugins/youtube/myetv-player-youtube-plugin.js +418 -0
  17. package/scss/README.md +1 -0
  18. package/scss/_audio-player.scss +21 -0
  19. package/scss/_base.scss +131 -0
  20. package/scss/_controls.scss +30 -0
  21. package/scss/_loading.scss +111 -0
  22. package/scss/_menus.scss +4070 -0
  23. package/scss/_mixins.scss +112 -0
  24. package/scss/_poster.scss +8 -0
  25. package/scss/_progress-bar.scss +2203 -0
  26. package/scss/_resolution.scss +68 -0
  27. package/scss/_responsive.scss +1532 -0
  28. package/scss/_themes.scss +30 -0
  29. package/scss/_title-overlay.scss +2262 -0
  30. package/scss/_tooltips.scss +7 -0
  31. package/scss/_variables.scss +49 -0
  32. package/scss/_video.scss +2401 -0
  33. package/scss/_volume.scss +1981 -0
  34. package/scss/_watermark.scss +8 -0
  35. package/scss/myetv-player.scss +51 -0
  36. package/scss/package.json +16 -0
  37. package/src/README.md +1 -0
  38. package/src/chapters.js +521 -0
  39. package/src/controls.js +1005 -0
  40. package/src/core.js +1650 -0
  41. package/src/events.js +330 -0
  42. package/src/fullscreen.js +82 -0
  43. package/src/i18n.js +348 -0
  44. package/src/playlist.js +177 -0
  45. package/src/plugins.js +384 -0
  46. package/src/quality.js +921 -0
  47. package/src/streaming.js +346 -0
  48. package/src/subtitles.js +426 -0
  49. package/src/utils.js +51 -0
  50. package/src/watermark.js +195 -0
package/src/events.js ADDED
@@ -0,0 +1,330 @@
1
+ // Events Module for MYETV Video Player
2
+ // Conservative modularization - original code preserved exactly
3
+ // Created by https://www.myetv.tv https://oskarcosimo.com
4
+
5
+ addEventListener(eventType, callback) {
6
+ if (typeof callback !== 'function') {
7
+ if (this.options.debug) console.warn(`Callback for event '${eventType}' is not a function`);
8
+ return this;
9
+ }
10
+
11
+ if (!this.eventCallbacks[eventType]) {
12
+ this.eventCallbacks[eventType] = [];
13
+ }
14
+
15
+ this.eventCallbacks[eventType].push(callback);
16
+ if (this.options.debug) console.log(`Event '${eventType}' registered`);
17
+ return this;
18
+ }
19
+
20
+ removeEventListener(eventType, callback) {
21
+ if (!this.eventCallbacks[eventType]) return this;
22
+
23
+ const index = this.eventCallbacks[eventType].indexOf(callback);
24
+ if (index > -1) {
25
+ this.eventCallbacks[eventType].splice(index, 1);
26
+ if (this.options.debug) console.log(`Event '${eventType}' removed`);
27
+ }
28
+ return this;
29
+ }
30
+
31
+ triggerEvent(eventType, data = {}) {
32
+ if (!this.eventCallbacks[eventType]) return;
33
+
34
+ this.eventCallbacks[eventType].forEach(callback => {
35
+ try {
36
+ callback({
37
+ type: eventType,
38
+ timestamp: Date.now(),
39
+ player: this,
40
+ ...data
41
+ });
42
+ } catch (error) {
43
+ if (this.options.debug) console.error(`Error in event '${eventType}':`, error);
44
+ }
45
+ });
46
+ }
47
+
48
+ getEventData() {
49
+ const state = this.getPlayerState();
50
+ return {
51
+ played: state.isPlaying,
52
+ paused: state.isPaused,
53
+ subtitleEnabled: state.subtitlesEnabled,
54
+ pipMode: state.isPictureInPicture,
55
+ fullscreenMode: state.isFullscreen,
56
+ speed: state.playbackRate,
57
+ controlBarLength: state.currentTime,
58
+ volumeIsMuted: state.isMuted,
59
+ // Additional useful data
60
+ duration: state.duration,
61
+ volume: state.volume,
62
+ quality: state.currentQuality,
63
+ buffered: this.getBufferedTime()
64
+ };
65
+ }
66
+
67
+ bindSubtitleEvents() {
68
+ if (this.video.textTracks) {
69
+ for (let i = 0; i < this.video.textTracks.length; i++) {
70
+ const track = this.video.textTracks[i];
71
+
72
+ track.addEventListener('cuechange', () => {
73
+ if (this.options.debug) console.log('Cue change detected:', track.activeCues);
74
+ });
75
+ }
76
+ }
77
+ }
78
+
79
+ setupVolumeTooltipEvents() {
80
+ const volumeSlider = this.controls?.querySelector('.volume-slider');
81
+ if (!volumeSlider) return;
82
+
83
+ let isMouseOverVolume = false;
84
+ let isDragging = false;
85
+
86
+ volumeSlider.addEventListener('mouseenter', () => {
87
+ isMouseOverVolume = true;
88
+ // IMPORTANT: Set position FIRST, then show tooltip
89
+ this.updateVolumeTooltipPosition(this.video.volume);
90
+ this.updateVolumeTooltip();
91
+ if (this.volumeTooltip) {
92
+ this.volumeTooltip.classList.add('visible');
93
+ }
94
+ });
95
+
96
+ volumeSlider.addEventListener('mouseleave', () => {
97
+ isMouseOverVolume = false;
98
+ if (!isDragging) {
99
+ setTimeout(() => {
100
+ if (!isMouseOverVolume && !isDragging && this.volumeTooltip) {
101
+ this.volumeTooltip.classList.remove('visible');
102
+ }
103
+ }, 150);
104
+ }
105
+ });
106
+
107
+ volumeSlider.addEventListener('mousemove', (e) => {
108
+ if (isMouseOverVolume && this.volumeTooltip && !isDragging) {
109
+ const rect = volumeSlider.getBoundingClientRect();
110
+ const offsetX = e.clientX - rect.left;
111
+ const sliderWidth = rect.width;
112
+
113
+ const thumbSize = 14;
114
+ const availableWidth = sliderWidth - thumbSize;
115
+ let volumeAtPosition = (offsetX - thumbSize / 2) / availableWidth;
116
+ volumeAtPosition = Math.max(0, Math.min(1, volumeAtPosition));
117
+
118
+ const hoverVolume = Math.round(volumeAtPosition * 100);
119
+
120
+ // Set position first, then update text to avoid flicker
121
+ this.updateVolumeTooltipPosition(volumeAtPosition);
122
+ this.volumeTooltip.textContent = hoverVolume + '%';
123
+ }
124
+ });
125
+
126
+ // During dragging - set position immediately
127
+ volumeSlider.addEventListener('mousedown', () => {
128
+ isDragging = true;
129
+ if (this.volumeTooltip) {
130
+ // Ensure position is set before showing
131
+ this.updateVolumeTooltipPosition(this.video.volume);
132
+ this.volumeTooltip.classList.add('visible');
133
+ }
134
+ });
135
+
136
+ document.addEventListener('mouseup', () => {
137
+ if (isDragging) {
138
+ isDragging = false;
139
+ setTimeout(() => {
140
+ if (!isMouseOverVolume && this.volumeTooltip) {
141
+ this.volumeTooltip.classList.remove('visible');
142
+ }
143
+ }, 500);
144
+ }
145
+ });
146
+
147
+ volumeSlider.addEventListener('input', (e) => {
148
+ const volumeValue = parseFloat(e.target.value);
149
+ const volume = Math.round(volumeValue * 100);
150
+ if (this.volumeTooltip) {
151
+ // Update position first, then text
152
+ this.updateVolumeTooltipPosition(volumeValue);
153
+ this.volumeTooltip.textContent = volume + '%';
154
+ }
155
+ isDragging = true;
156
+ });
157
+
158
+ volumeSlider.addEventListener('change', () => {
159
+ // Ensure final position is correct
160
+ this.updateVolumeTooltip();
161
+ setTimeout(() => {
162
+ isDragging = false;
163
+ }, 100);
164
+ });
165
+ }
166
+
167
+ bindEvents() {
168
+ if (this.video) {
169
+ this.video.addEventListener('loadedmetadata', () => {
170
+ this.updateDuration();
171
+ setTimeout(() => {
172
+ this.initializeSubtitles();
173
+ }, 100);
174
+ });
175
+ this.video.addEventListener('timeupdate', () => this.updateProgress());
176
+ this.video.addEventListener('progress', () => this.updateBuffer());
177
+ this.video.addEventListener('waiting', () => {
178
+ if (!this.isChangingQuality) {
179
+ this.showLoading();
180
+ }
181
+ });
182
+ this.video.addEventListener('canplay', () => {
183
+ if (!this.isChangingQuality) {
184
+ this.hideLoading();
185
+ }
186
+ });
187
+ this.video.addEventListener('ended', () => this.onVideoEnded());
188
+ this.video.addEventListener('loadstart', () => {
189
+ if (!this.isChangingQuality) {
190
+ this.showLoading();
191
+ }
192
+ });
193
+ this.video.addEventListener('loadeddata', () => {
194
+ if (!this.isChangingQuality) {
195
+ this.hideLoading();
196
+ }
197
+ });
198
+ // Complete video click logic with doubleTapPause support (DESKTOP)
199
+ this.video.addEventListener('click', () => {
200
+ if (!this.options.pauseClick) return;
201
+
202
+ if (this.options.doubleTapPause) {
203
+ // DOUBLE TAP MODE: primo click mostra controlli, secondo pausa
204
+ const controlsVisible = this.controls && this.controls.classList.contains('show');
205
+
206
+ if (controlsVisible) {
207
+ // Controlbar VISIBILE - pausa video
208
+ this.togglePlayPause();
209
+ } else {
210
+ // Controlbar NASCOSTA - solo mostra controlli
211
+ this.showControlsNow();
212
+ this.resetAutoHideTimer();
213
+ }
214
+ } else {
215
+ // NORMAL MODE: sempre pausa (comportamento originale)
216
+ this.togglePlayPause();
217
+ }
218
+ });
219
+ this.video.addEventListener('volumechange', () => this.updateVolumeSliderVisual());
220
+
221
+ // Complete touch logic with doubleTapPause support (MOBILE)
222
+ this.video.addEventListener('touchend', (e) => {
223
+ // Prevent click event from firing after touchend
224
+ e.preventDefault();
225
+
226
+ if (!this.options.pauseClick) return;
227
+
228
+ if (this.options.doubleTapPause) {
229
+ // DOUBLE TAP MODE: primo tap mostra controlli, secondo pausa (SAME as desktop)
230
+ const controlsVisible = this.controls && this.controls.classList.contains('show');
231
+
232
+ if (controlsVisible) {
233
+ // Controlbar VISIBILE - pausa video
234
+ this.togglePlayPause();
235
+ } else {
236
+ // Controlbar NASCOSTA - solo mostra controlli
237
+ this.showControlsNow();
238
+ this.resetAutoHideTimer();
239
+ }
240
+ } else {
241
+ // NORMAL MODE: sempre pausa (comportamento originale, SAME as desktop)
242
+ this.togglePlayPause();
243
+ }
244
+ });
245
+
246
+ // CRITICAL: Start auto-hide when video starts playing
247
+ this.video.addEventListener('play', () => {
248
+ if (this.options.autoHide && this.autoHideInitialized) {
249
+ this.showControlsNow();
250
+ this.resetAutoHideTimer();
251
+ }
252
+ });
253
+
254
+ // Picture-in-Picture Events
255
+ this.video.addEventListener('enterpictureinpicture', () => {
256
+ this.onEnterPiP();
257
+ this.triggerEvent('pipchange', {
258
+ active: true,
259
+ mode: 'enter'
260
+ });
261
+ });
262
+
263
+ this.video.addEventListener('leavepictureinpicture', () => {
264
+ this.onLeavePiP();
265
+ this.triggerEvent('pipchange', {
266
+ active: false,
267
+ mode: 'exit'
268
+ });
269
+ });
270
+ }
271
+
272
+ if (this.playPauseBtn) {
273
+ this.playPauseBtn.addEventListener('click', () => this.togglePlayPause());
274
+ }
275
+
276
+ if (this.muteBtn) {
277
+ this.muteBtn.addEventListener('click', () => this.toggleMute());
278
+ }
279
+
280
+ if (this.fullscreenBtn) {
281
+ this.fullscreenBtn.addEventListener('click', () => this.toggleFullscreen());
282
+ }
283
+
284
+ if (this.pipBtn) {
285
+ this.pipBtn.addEventListener('click', () => this.togglePictureInPicture());
286
+ }
287
+
288
+ if (this.subtitlesBtn) {
289
+ this.subtitlesBtn.addEventListener('click', () => this.toggleSubtitles());
290
+ }
291
+
292
+ if (this.volumeSlider) {
293
+ this.volumeSlider.addEventListener('input', (e) => {
294
+ this.updateVolume(e.target.value);
295
+ this.updateVolumeSliderVisual();
296
+ this.initVolumeTooltip();
297
+ });
298
+ }
299
+
300
+ if (this.progressContainer) {
301
+ this.progressContainer.addEventListener('click', (e) => this.seek(e));
302
+ this.progressContainer.addEventListener('mousedown', (e) => this.startSeeking(e));
303
+ }
304
+
305
+ this.setupSeekTooltip();
306
+
307
+ // NOTE: Auto-hide events are handled in initAutoHide() after everything is ready
308
+
309
+ if (this.speedMenu) {
310
+ this.speedMenu.addEventListener('click', (e) => this.changeSpeed(e));
311
+ }
312
+
313
+ if (this.qualityMenu) {
314
+ this.qualityMenu.addEventListener('click', (e) => this.changeQuality(e));
315
+ }
316
+
317
+ if (this.subtitlesMenu) {
318
+ this.subtitlesMenu.addEventListener('click', (e) => this.handleSubtitlesMenuClick(e));
319
+ }
320
+
321
+ document.addEventListener('fullscreenchange', () => this.updateFullscreenButton());
322
+ document.addEventListener('webkitfullscreenchange', () => this.updateFullscreenButton());
323
+ document.addEventListener('mozfullscreenchange', () => this.updateFullscreenButton());
324
+
325
+ document.addEventListener('mousemove', (e) => this.continueSeeking(e));
326
+ document.addEventListener('mouseup', () => this.endSeeking());
327
+ }
328
+
329
+ // Events methods for main class
330
+ // All original functionality preserved exactly
@@ -0,0 +1,82 @@
1
+ // Fullscreen Module for MYETV Video Player
2
+ // Conservative modularization - original code preserved exactly
3
+ // Created by https://www.myetv.tv https://oskarcosimo.com
4
+
5
+ isFullscreenActive() {
6
+ return !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement);
7
+ }
8
+
9
+ checkPiPSupport() {
10
+ return 'pictureInPictureEnabled' in document;
11
+ }
12
+
13
+ enterFullscreen() {
14
+ const element = this.container.parentElement || this.container;
15
+
16
+ if (element.requestFullscreen) {
17
+ element.requestFullscreen();
18
+ } else if (element.webkitRequestFullscreen) {
19
+ element.webkitRequestFullscreen();
20
+ } else if (element.mozRequestFullScreen) {
21
+ element.mozRequestFullScreen();
22
+ }
23
+ }
24
+
25
+ exitFullscreen() {
26
+ if (document.exitFullscreen) {
27
+ document.exitFullscreen();
28
+ } else if (document.webkitExitFullscreen) {
29
+ document.webkitExitFullscreen();
30
+ } else if (document.mozCancelFullScreen) {
31
+ document.mozCancelFullScreen();
32
+ }
33
+ }
34
+
35
+ async enterPictureInPicture() {
36
+ if (!this.isPiPSupported || !this.video) return;
37
+
38
+ try {
39
+ await this.video.requestPictureInPicture();
40
+ } catch (error) {
41
+ if (this.options.debug) console.error('Errore avvio Picture-in-Picture:', error);
42
+ }
43
+ }
44
+
45
+ async exitPictureInPicture() {
46
+ if (!this.isPiPSupported) return;
47
+
48
+ try {
49
+ await document.exitPictureInPicture();
50
+ } catch (error) {
51
+ if (this.options.debug) console.error('Errore uscita Picture-in-Picture:', error);
52
+ }
53
+ }
54
+
55
+ onEnterPiP() {
56
+ if (this.pipIcon) this.pipIcon.classList.add('hidden');
57
+ if (this.pipExitIcon) this.pipExitIcon.classList.remove('hidden');
58
+
59
+ if (this.controls) {
60
+ this.controls.style.opacity = '0';
61
+ }
62
+
63
+ if (this.titleOverlay) {
64
+ this.titleOverlay.style.opacity = '0';
65
+ }
66
+ }
67
+
68
+ onLeavePiP() {
69
+ if (this.pipIcon) this.pipIcon.classList.remove('hidden');
70
+ if (this.pipExitIcon) this.pipExitIcon.classList.add('hidden');
71
+
72
+ if (this.controls) {
73
+ this.controls.style.opacity = '';
74
+ }
75
+
76
+ if (this.titleOverlay) {
77
+ this.titleOverlay.style.opacity = '';
78
+ }
79
+ }
80
+
81
+ // Fullscreen methods for main class
82
+ // All original functionality preserved exactly