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.
- package/.github/workflows/npm-publish.yml +30 -0
- package/LICENSE +21 -0
- package/README.md +866 -0
- package/build.js +189 -0
- package/css/README.md +1 -0
- package/css/myetv-player.css +13702 -0
- package/css/myetv-player.min.css +1 -0
- package/dist/README.md +1 -0
- package/dist/myetv-player.js +6408 -0
- package/dist/myetv-player.min.js +6183 -0
- package/package.json +27 -0
- package/plugins/README.md +1 -0
- package/plugins/google-analytics/README.md +1 -0
- package/plugins/google-analytics/myetv-player-g-analytics-plugin.js +548 -0
- package/plugins/youtube/README.md +1 -0
- package/plugins/youtube/myetv-player-youtube-plugin.js +418 -0
- package/scss/README.md +1 -0
- package/scss/_audio-player.scss +21 -0
- package/scss/_base.scss +131 -0
- package/scss/_controls.scss +30 -0
- package/scss/_loading.scss +111 -0
- package/scss/_menus.scss +4070 -0
- package/scss/_mixins.scss +112 -0
- package/scss/_poster.scss +8 -0
- package/scss/_progress-bar.scss +2203 -0
- package/scss/_resolution.scss +68 -0
- package/scss/_responsive.scss +1532 -0
- package/scss/_themes.scss +30 -0
- package/scss/_title-overlay.scss +2262 -0
- package/scss/_tooltips.scss +7 -0
- package/scss/_variables.scss +49 -0
- package/scss/_video.scss +2401 -0
- package/scss/_volume.scss +1981 -0
- package/scss/_watermark.scss +8 -0
- package/scss/myetv-player.scss +51 -0
- package/scss/package.json +16 -0
- package/src/README.md +1 -0
- package/src/chapters.js +521 -0
- package/src/controls.js +1005 -0
- package/src/core.js +1650 -0
- package/src/events.js +330 -0
- package/src/fullscreen.js +82 -0
- package/src/i18n.js +348 -0
- package/src/playlist.js +177 -0
- package/src/plugins.js +384 -0
- package/src/quality.js +921 -0
- package/src/streaming.js +346 -0
- package/src/subtitles.js +426 -0
- package/src/utils.js +51 -0
- package/src/watermark.js +195 -0
package/src/subtitles.js
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/* Subtitles Module for MYETV Video Player
|
|
2
|
+
* Conservative modularization - original code preserved exactly
|
|
3
|
+
* Created by https://www.myetv.tv https://oskarcosimo.com
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
initializeSubtitles() {
|
|
7
|
+
this.detectTextTracks();
|
|
8
|
+
this.updateSubtitlesUI();
|
|
9
|
+
this.bindSubtitleEvents();
|
|
10
|
+
this.initializeCustomSubtitles();
|
|
11
|
+
|
|
12
|
+
if (this.options.debug) console.log('📝 Detected ' + this.textTracks.length + ' subtitles traces');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
initializeCustomSubtitles() {
|
|
16
|
+
// Initialize player variables
|
|
17
|
+
this.customSubtitles = [];
|
|
18
|
+
this.currentCustomSubtitles = [];
|
|
19
|
+
this.customSubtitlesEnabled = false;
|
|
20
|
+
this.customOverlayElement = null;
|
|
21
|
+
this.customUpdateInterval = null;
|
|
22
|
+
this.currentCustomTrackIndex = -1;
|
|
23
|
+
|
|
24
|
+
this.createCustomSubtitleOverlay();
|
|
25
|
+
this.loadCustomSubtitleTracks();
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
createCustomSubtitleOverlay() {
|
|
30
|
+
var existing = document.querySelector('.custom-subtitle-overlay');
|
|
31
|
+
if (existing && existing.parentNode) {
|
|
32
|
+
existing.parentNode.removeChild(existing);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.customOverlayElement = document.createElement('div');
|
|
36
|
+
this.customOverlayElement.className = 'custom-subtitle-overlay';
|
|
37
|
+
|
|
38
|
+
// ENHANCED styles with responsive defaults
|
|
39
|
+
this.customOverlayElement.style.cssText =
|
|
40
|
+
'position: absolute;' +
|
|
41
|
+
'bottom: 80px;' +
|
|
42
|
+
'left: 50%;' +
|
|
43
|
+
'transform: translateX(-50%);' +
|
|
44
|
+
'z-index: 5;' +
|
|
45
|
+
'color: white;' +
|
|
46
|
+
'font-family: Arial, sans-serif;' +
|
|
47
|
+
'font-size: clamp(12px, 4vw, 18px);' + // RESPONSIVE font-size
|
|
48
|
+
'font-weight: bold;' +
|
|
49
|
+
'text-align: center;' +
|
|
50
|
+
'text-shadow: 2px 2px 4px rgba(0, 0, 0, 1);' +
|
|
51
|
+
'background-color: rgba(0, 0, 0, 0.6);' +
|
|
52
|
+
'padding: 8px 16px;' +
|
|
53
|
+
'border-radius: 6px;' +
|
|
54
|
+
'max-width: 80%;' +
|
|
55
|
+
'line-height: 1.3;' +
|
|
56
|
+
'white-space: pre-line;' +
|
|
57
|
+
'display: none;' +
|
|
58
|
+
'pointer-events: none;' +
|
|
59
|
+
'box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);';
|
|
60
|
+
|
|
61
|
+
var playerContainer = this.video.parentElement;
|
|
62
|
+
if (playerContainer) {
|
|
63
|
+
playerContainer.style.position = 'relative';
|
|
64
|
+
// ENSURE proper layer stacking
|
|
65
|
+
if (!playerContainer.style.zIndex) {
|
|
66
|
+
playerContainer.style.zIndex = '1';
|
|
67
|
+
}
|
|
68
|
+
playerContainer.appendChild(this.customOverlayElement);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (this.options.debug) console.log('✅ Custom subtitle overlay created with responsive settings');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
loadCustomSubtitleTracks() {
|
|
75
|
+
var self = this;
|
|
76
|
+
var trackElements = document.querySelectorAll('track[kind="subtitles"], track[kind="captions"]');
|
|
77
|
+
var loadPromises = [];
|
|
78
|
+
|
|
79
|
+
if (this.options.debug) console.log('📥 Loading ' + trackElements.length + ' subtitle files...');
|
|
80
|
+
|
|
81
|
+
for (var i = 0; i < trackElements.length; i++) {
|
|
82
|
+
var track = trackElements[i];
|
|
83
|
+
|
|
84
|
+
(function (trackElement, index) {
|
|
85
|
+
var promise = fetch(trackElement.src)
|
|
86
|
+
.then(function (response) {
|
|
87
|
+
return response.text();
|
|
88
|
+
})
|
|
89
|
+
.then(function (srtText) {
|
|
90
|
+
var subtitles = self.parseCustomSRT(srtText);
|
|
91
|
+
self.customSubtitles.push({
|
|
92
|
+
label: trackElement.label || 'Track ' + (index + 1),
|
|
93
|
+
language: trackElement.srclang || 'unknown',
|
|
94
|
+
subtitles: subtitles
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (self.options.debug) {
|
|
98
|
+
console.log('✅ Loaded: ' + trackElement.label + ' (' + subtitles.length + ' subtitles)');
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
.catch(function (error) {
|
|
102
|
+
if (self.options.debug) {
|
|
103
|
+
console.error('❌ Error loading ' + trackElement.src + ':', error);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
loadPromises.push(promise);
|
|
108
|
+
})(track, i);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
Promise.all(loadPromises).then(function () {
|
|
112
|
+
if (self.options.debug && self.customSubtitles.length > 0) {
|
|
113
|
+
console.log('✅ All custom subtitle tracks loaded');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (self.options.debug) {
|
|
117
|
+
console.log('📝 Subtitles loaded but NOT auto-enabled - user must activate manually');
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
parseCustomSRT(srtText) {
|
|
123
|
+
var subtitles = [];
|
|
124
|
+
var normalizedText = srtText.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
125
|
+
var blocks = normalizedText.trim().split('\n\n');
|
|
126
|
+
|
|
127
|
+
for (var i = 0; i < blocks.length; i++) {
|
|
128
|
+
var block = blocks[i];
|
|
129
|
+
var lines = block.trim().split('\n');
|
|
130
|
+
|
|
131
|
+
if (lines.length >= 3) {
|
|
132
|
+
var timeLine = lines[1].trim();
|
|
133
|
+
var timeMatch = timeLine.match(/(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})/);
|
|
134
|
+
|
|
135
|
+
if (timeMatch) {
|
|
136
|
+
var startTime = this.customTimeToSeconds(timeMatch[1]);
|
|
137
|
+
var endTime = this.customTimeToSeconds(timeMatch[2]);
|
|
138
|
+
var text = lines.slice(2).join('\n').trim().replace(/<[^>]*>/g, '');
|
|
139
|
+
|
|
140
|
+
if (text.length > 0 && startTime < endTime) {
|
|
141
|
+
subtitles.push({
|
|
142
|
+
start: startTime,
|
|
143
|
+
end: endTime,
|
|
144
|
+
text: text
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return subtitles;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
customTimeToSeconds(timeString) {
|
|
155
|
+
var parts = timeString.split(',');
|
|
156
|
+
var time = parts[0];
|
|
157
|
+
var millis = parts[1];
|
|
158
|
+
var timeParts = time.split(':');
|
|
159
|
+
var hours = parseInt(timeParts[0], 10);
|
|
160
|
+
var minutes = parseInt(timeParts[1], 10);
|
|
161
|
+
var seconds = parseInt(timeParts[2], 10);
|
|
162
|
+
var milliseconds = parseInt(millis, 10);
|
|
163
|
+
|
|
164
|
+
return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
enableCustomSubtitleTrack(trackIndex) {
|
|
168
|
+
if (trackIndex < 0 || trackIndex >= this.customSubtitles.length) return false;
|
|
169
|
+
|
|
170
|
+
this.disableCustomSubtitles();
|
|
171
|
+
|
|
172
|
+
this.customSubtitlesEnabled = true;
|
|
173
|
+
this.currentCustomTrackIndex = trackIndex;
|
|
174
|
+
this.currentCustomSubtitles = this.customSubtitles[trackIndex].subtitles;
|
|
175
|
+
|
|
176
|
+
var self = this;
|
|
177
|
+
this.customUpdateInterval = setInterval(function () {
|
|
178
|
+
if (self.customSubtitlesEnabled && self.currentCustomSubtitles.length > 0) {
|
|
179
|
+
self.updateCustomSubtitleDisplay();
|
|
180
|
+
}
|
|
181
|
+
}, 100);
|
|
182
|
+
|
|
183
|
+
if (this.options.debug) {
|
|
184
|
+
console.log('✅ Custom subtitles enabled: ' + this.customSubtitles[trackIndex].label);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
updateCustomSubtitleDisplay() {
|
|
191
|
+
if (!this.customSubtitlesEnabled || this.currentCustomSubtitles.length === 0) return;
|
|
192
|
+
|
|
193
|
+
var currentTime = this.video.currentTime;
|
|
194
|
+
var currentSubtitle = null;
|
|
195
|
+
|
|
196
|
+
for (var i = 0; i < this.currentCustomSubtitles.length; i++) {
|
|
197
|
+
var sub = this.currentCustomSubtitles[i];
|
|
198
|
+
if (currentTime >= sub.start && currentTime <= sub.end) {
|
|
199
|
+
currentSubtitle = sub;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (currentSubtitle) {
|
|
205
|
+
this.customOverlayElement.textContent = currentSubtitle.text;
|
|
206
|
+
this.customOverlayElement.style.display = 'block';
|
|
207
|
+
} else {
|
|
208
|
+
this.customOverlayElement.style.display = 'none';
|
|
209
|
+
this.customOverlayElement.textContent = '';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
disableCustomSubtitles() {
|
|
214
|
+
this.customSubtitlesEnabled = false;
|
|
215
|
+
this.currentCustomTrackIndex = -1;
|
|
216
|
+
|
|
217
|
+
if (this.customOverlayElement) {
|
|
218
|
+
this.customOverlayElement.style.display = 'none';
|
|
219
|
+
this.customOverlayElement.textContent = '';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (this.customUpdateInterval) {
|
|
223
|
+
clearInterval(this.customUpdateInterval);
|
|
224
|
+
this.customUpdateInterval = null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (this.options.debug) console.log('❌ Custom subtitles disabled');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
detectTextTracks() {
|
|
231
|
+
this.textTracks = [];
|
|
232
|
+
|
|
233
|
+
if (this.video.textTracks) {
|
|
234
|
+
if (this.options.debug) console.log('🔍 Detecting text tracks... Found: ' + this.video.textTracks.length);
|
|
235
|
+
|
|
236
|
+
for (var i = 0; i < this.video.textTracks.length; i++) {
|
|
237
|
+
var track = this.video.textTracks[i];
|
|
238
|
+
|
|
239
|
+
if (track.kind === 'subtitles' || track.kind === 'captions') {
|
|
240
|
+
this.textTracks.push({
|
|
241
|
+
track: track,
|
|
242
|
+
label: track.label || 'Track ' + (i + 1),
|
|
243
|
+
language: track.language || 'unknown',
|
|
244
|
+
kind: track.kind,
|
|
245
|
+
index: i
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (this.options.debug) console.log('📊 Total subtitle tracks detected: ' + this.textTracks.length);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
enableSubtitleTrack(trackIndex) {
|
|
255
|
+
if (trackIndex < 0 || trackIndex >= this.textTracks.length) return;
|
|
256
|
+
|
|
257
|
+
this.disableAllTracks();
|
|
258
|
+
|
|
259
|
+
var success = this.enableCustomSubtitleTrack(trackIndex);
|
|
260
|
+
|
|
261
|
+
if (success) {
|
|
262
|
+
this.currentSubtitleTrack = this.textTracks[trackIndex].track;
|
|
263
|
+
this.subtitlesEnabled = true;
|
|
264
|
+
|
|
265
|
+
this.updateSubtitlesButton();
|
|
266
|
+
this.populateSubtitlesMenu();
|
|
267
|
+
|
|
268
|
+
if (this.options.debug) {
|
|
269
|
+
console.log('✅ Subtitles enabled: ' + this.textTracks[trackIndex].label);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Trigger evento
|
|
273
|
+
this.triggerEvent('subtitlechange', {
|
|
274
|
+
enabled: true,
|
|
275
|
+
trackIndex: trackIndex,
|
|
276
|
+
trackLabel: this.textTracks[trackIndex].label,
|
|
277
|
+
trackLanguage: this.textTracks[trackIndex].language
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
disableSubtitles() {
|
|
283
|
+
this.disableCustomSubtitles();
|
|
284
|
+
this.disableAllTracks();
|
|
285
|
+
|
|
286
|
+
this.currentSubtitleTrack = null;
|
|
287
|
+
this.subtitlesEnabled = false;
|
|
288
|
+
|
|
289
|
+
this.updateSubtitlesButton();
|
|
290
|
+
this.populateSubtitlesMenu();
|
|
291
|
+
|
|
292
|
+
if (this.options.debug) console.log('📝 Subtitles disabled');
|
|
293
|
+
|
|
294
|
+
this.triggerEvent('subtitlechange', {
|
|
295
|
+
enabled: false,
|
|
296
|
+
trackIndex: -1
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
disableAllTracks() {
|
|
301
|
+
if (this.video.textTracks) {
|
|
302
|
+
for (var i = 0; i < this.video.textTracks.length; i++) {
|
|
303
|
+
this.video.textTracks[i].mode = 'hidden';
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
getAvailableSubtitles() {
|
|
309
|
+
return this.textTracks.map(function (t) {
|
|
310
|
+
return {
|
|
311
|
+
label: t.label,
|
|
312
|
+
language: t.language,
|
|
313
|
+
kind: t.kind
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
setSubtitleTrack(trackIndex) {
|
|
319
|
+
if (trackIndex === -1) {
|
|
320
|
+
this.disableSubtitles();
|
|
321
|
+
} else {
|
|
322
|
+
this.enableSubtitleTrack(trackIndex);
|
|
323
|
+
}
|
|
324
|
+
return this;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
getCurrentSubtitleTrack() {
|
|
328
|
+
if (!this.subtitlesEnabled || !this.currentSubtitleTrack) return -1;
|
|
329
|
+
|
|
330
|
+
for (var i = 0; i < this.textTracks.length; i++) {
|
|
331
|
+
if (this.textTracks[i].track === this.currentSubtitleTrack) {
|
|
332
|
+
return i;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return -1;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
isSubtitlesEnabled() {
|
|
339
|
+
return this.subtitlesEnabled;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
updateSubtitlesButton() {
|
|
343
|
+
var subtitlesBtn = this.controls && this.controls.querySelector('.subtitles-btn');
|
|
344
|
+
if (!subtitlesBtn) return;
|
|
345
|
+
|
|
346
|
+
if (this.subtitlesEnabled) {
|
|
347
|
+
subtitlesBtn.classList.add('active');
|
|
348
|
+
subtitlesBtn.title = this.t('subtitlesdisable');
|
|
349
|
+
} else {
|
|
350
|
+
subtitlesBtn.classList.remove('active');
|
|
351
|
+
subtitlesBtn.title = this.t('subtitlesenable');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
populateSubtitlesMenu() {
|
|
356
|
+
var subtitlesMenu = this.controls && this.controls.querySelector('.subtitles-menu');
|
|
357
|
+
if (!subtitlesMenu) return;
|
|
358
|
+
|
|
359
|
+
var menuHTML = '<div class="subtitles-option ' + (!this.subtitlesEnabled ? 'active' : '') + '" data-track="off">Off</div>';
|
|
360
|
+
|
|
361
|
+
for (var i = 0; i < this.textTracks.length; i++) {
|
|
362
|
+
var trackData = this.textTracks[i];
|
|
363
|
+
var isActive = this.currentSubtitleTrack === trackData.track;
|
|
364
|
+
menuHTML += '<div class="subtitles-option ' + (isActive ? 'active' : '') + '" data-track="' + i + '">' + trackData.label + '</div>';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
subtitlesMenu.innerHTML = menuHTML;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
updateSubtitlesUI() {
|
|
371
|
+
var subtitlesControl = this.controls && this.controls.querySelector('.subtitles-control');
|
|
372
|
+
|
|
373
|
+
if (this.textTracks.length > 0 && this.options.showSubtitles) {
|
|
374
|
+
if (subtitlesControl) subtitlesControl.style.display = 'block';
|
|
375
|
+
this.populateSubtitlesMenu();
|
|
376
|
+
} else {
|
|
377
|
+
if (subtitlesControl) subtitlesControl.style.display = 'none';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
this.updateSubtitlesButton();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
bindSubtitleEvents() {
|
|
384
|
+
var self = this;
|
|
385
|
+
|
|
386
|
+
var subtitlesBtn = this.controls && this.controls.querySelector('.subtitles-btn');
|
|
387
|
+
if (subtitlesBtn) {
|
|
388
|
+
subtitlesBtn.addEventListener('click', function (e) {
|
|
389
|
+
e.stopPropagation();
|
|
390
|
+
self.toggleSubtitles();
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
var subtitlesMenu = this.controls && this.controls.querySelector('.subtitles-menu');
|
|
395
|
+
if (subtitlesMenu) {
|
|
396
|
+
subtitlesMenu.addEventListener('click', function (e) {
|
|
397
|
+
self.handleSubtitlesMenuClick(e);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
handleSubtitlesMenuClick(e) {
|
|
403
|
+
if (!e.target.classList.contains('subtitles-option')) return;
|
|
404
|
+
|
|
405
|
+
var trackData = e.target.getAttribute('data-track');
|
|
406
|
+
|
|
407
|
+
if (trackData === 'off') {
|
|
408
|
+
this.disableSubtitles();
|
|
409
|
+
} else {
|
|
410
|
+
var trackIndex = parseInt(trackData, 10);
|
|
411
|
+
this.enableSubtitleTrack(trackIndex);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
this.updateSubtitlesButton();
|
|
415
|
+
this.populateSubtitlesMenu();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
toggleSubtitles() {
|
|
419
|
+
if (this.textTracks.length === 0) return;
|
|
420
|
+
|
|
421
|
+
if (this.subtitlesEnabled) {
|
|
422
|
+
this.disableSubtitles();
|
|
423
|
+
} else {
|
|
424
|
+
this.enableSubtitleTrack(0);
|
|
425
|
+
}
|
|
426
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Utils Module for MYETV Video Player
|
|
2
|
+
// Conservative modularization - original code preserved exactly
|
|
3
|
+
// Created by https://www.myetv.tv https://oskarcosimo.com
|
|
4
|
+
|
|
5
|
+
getBufferedTime() {
|
|
6
|
+
if (!this.video || !this.video.buffered || this.video.buffered.length === 0) return 0;
|
|
7
|
+
try {
|
|
8
|
+
return this.video.buffered.end(this.video.buffered.length - 1);
|
|
9
|
+
} catch (error) {
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
clearTitleTimeout() {
|
|
15
|
+
if (this.titleTimeout) {
|
|
16
|
+
clearTimeout(this.titleTimeout);
|
|
17
|
+
this.titleTimeout = null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
skipTime(seconds) {
|
|
22
|
+
if (!this.video || !this.video.duration || this.isChangingQuality) return;
|
|
23
|
+
|
|
24
|
+
this.video.currentTime = Math.max(0, Math.min(this.video.duration, this.video.currentTime + seconds));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
updateTimeDisplay() {
|
|
28
|
+
if (this.currentTimeEl && this.video) {
|
|
29
|
+
this.currentTimeEl.textContent = this.formatTime(this.video.currentTime || 0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (this.durationEl && this.video && this.video.duration && !isNaN(this.video.duration)) {
|
|
33
|
+
this.durationEl.textContent = this.formatTime(this.video.duration);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
formatTime(seconds) {
|
|
38
|
+
if (isNaN(seconds) || seconds < 0) return '0:00';
|
|
39
|
+
|
|
40
|
+
const hours = Math.floor(seconds / 3600);
|
|
41
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
42
|
+
const secs = Math.floor(seconds % 60);
|
|
43
|
+
|
|
44
|
+
if (hours > 0) {
|
|
45
|
+
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
46
|
+
}
|
|
47
|
+
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Utils methods for main class
|
|
51
|
+
// All original functionality preserved exactly
|
package/src/watermark.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// Watermark Module for MYETV Video Player
|
|
2
|
+
// Displays a logo overlay on the video with customizable position and link
|
|
3
|
+
// Created by https://www.myetv.tv https://oskarcosimo.com
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Initialize watermark overlay
|
|
7
|
+
* Creates a watermark element overlaid on the video player
|
|
8
|
+
*/
|
|
9
|
+
initializeWatermark() {
|
|
10
|
+
if (!this.options.watermarkUrl) {
|
|
11
|
+
if (this.options.debug) console.log('🏷️ Watermark disabled - no URL provided');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (this.options.debug) console.log('🏷️ Initializing watermark overlay');
|
|
16
|
+
|
|
17
|
+
// Create watermark container
|
|
18
|
+
const watermark = document.createElement('div');
|
|
19
|
+
watermark.className = 'video-watermark';
|
|
20
|
+
|
|
21
|
+
// Set position class - FIX: use template literal correctly
|
|
22
|
+
const position = this.options.watermarkPosition || 'bottomright';
|
|
23
|
+
watermark.classList.add(`watermark-${position}`); // ← FIX QUI
|
|
24
|
+
|
|
25
|
+
// Add hide-on-autohide class if option is enabled
|
|
26
|
+
if (this.options.hideWatermark) {
|
|
27
|
+
watermark.classList.add('hide-on-autohide');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Create watermark image
|
|
31
|
+
const watermarkImg = document.createElement('img');
|
|
32
|
+
watermarkImg.src = this.options.watermarkUrl;
|
|
33
|
+
watermarkImg.alt = 'Watermark';
|
|
34
|
+
|
|
35
|
+
// Add title/tooltip if provided
|
|
36
|
+
if (this.options.watermarkTitle) {
|
|
37
|
+
watermarkImg.title = this.options.watermarkTitle;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Handle image loading error
|
|
41
|
+
watermarkImg.onerror = () => {
|
|
42
|
+
if (this.options.debug) console.warn('🏷️ Watermark image failed to load:', this.options.watermarkUrl);
|
|
43
|
+
watermark.style.display = 'none';
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
watermarkImg.onload = () => {
|
|
47
|
+
if (this.options.debug) console.log('🏷️ Watermark image loaded successfully');
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Add click handler if link URL is provided
|
|
51
|
+
if (this.options.watermarkLink) {
|
|
52
|
+
watermark.style.cursor = 'pointer';
|
|
53
|
+
watermark.addEventListener('click', (e) => {
|
|
54
|
+
e.stopPropagation(); // Prevent video controls interference
|
|
55
|
+
window.open(this.options.watermarkLink, '_blank', 'noopener,noreferrer');
|
|
56
|
+
if (this.options.debug) console.log('🏷️ Watermark clicked, opening:', this.options.watermarkLink);
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
watermark.style.cursor = 'default';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Append image to watermark container
|
|
63
|
+
watermark.appendChild(watermarkImg);
|
|
64
|
+
|
|
65
|
+
// Insert watermark before controls (above video, below controls)
|
|
66
|
+
if (this.controls) {
|
|
67
|
+
this.container.insertBefore(watermark, this.controls);
|
|
68
|
+
} else {
|
|
69
|
+
this.container.appendChild(watermark);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Store reference to watermark element
|
|
73
|
+
this.watermarkElement = watermark;
|
|
74
|
+
|
|
75
|
+
if (this.options.debug) {
|
|
76
|
+
console.log('🏷️ Watermark created:', {
|
|
77
|
+
url: this.options.watermarkUrl,
|
|
78
|
+
link: this.options.watermarkLink || 'none',
|
|
79
|
+
position: position,
|
|
80
|
+
title: this.options.watermarkTitle || 'none',
|
|
81
|
+
hideWithControls: this.options.hideWatermark
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Set or update watermark configuration
|
|
88
|
+
* @param {string} url - URL of the watermark image
|
|
89
|
+
* @param {string} link - Optional URL to open when watermark is clicked
|
|
90
|
+
* @param {string} position - Position of watermark (topleft, topright, bottomleft, bottomright)
|
|
91
|
+
* @param {string} title - Optional tooltip title for the watermark
|
|
92
|
+
*/
|
|
93
|
+
setWatermark(url, link = '', position = 'bottomright', title = '') {
|
|
94
|
+
// Update options
|
|
95
|
+
this.options.watermarkUrl = url;
|
|
96
|
+
this.options.watermarkLink = link;
|
|
97
|
+
this.options.watermarkPosition = position;
|
|
98
|
+
this.options.watermarkTitle = title;
|
|
99
|
+
|
|
100
|
+
// Remove existing watermark if present
|
|
101
|
+
if (this.watermarkElement) {
|
|
102
|
+
this.watermarkElement.remove();
|
|
103
|
+
this.watermarkElement = null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Recreate watermark if URL is provided
|
|
107
|
+
if (url) {
|
|
108
|
+
this.initializeWatermark();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Remove watermark from player
|
|
116
|
+
*/
|
|
117
|
+
removeWatermark() {
|
|
118
|
+
if (this.watermarkElement) {
|
|
119
|
+
this.watermarkElement.remove();
|
|
120
|
+
this.watermarkElement = null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.options.watermarkUrl = '';
|
|
124
|
+
this.options.watermarkLink = '';
|
|
125
|
+
this.options.watermarkPosition = 'bottomright';
|
|
126
|
+
this.options.watermarkTitle = '';
|
|
127
|
+
|
|
128
|
+
if (this.options.debug) console.log('🏷️ Watermark removed');
|
|
129
|
+
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Update watermark position
|
|
135
|
+
* @param {string} position - New position (topleft, topright, bottomleft, bottomright)
|
|
136
|
+
*/
|
|
137
|
+
setWatermarkPosition(position) {
|
|
138
|
+
if (!['topleft', 'topright', 'bottomleft', 'bottomright'].includes(position)) {
|
|
139
|
+
if (this.options.debug) console.warn('🏷️ Invalid watermark position:', position);
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.options.watermarkPosition = position;
|
|
144
|
+
|
|
145
|
+
if (this.watermarkElement) {
|
|
146
|
+
// Remove all position classes
|
|
147
|
+
this.watermarkElement.classList.remove(
|
|
148
|
+
'watermark-topleft',
|
|
149
|
+
'watermark-topright',
|
|
150
|
+
'watermark-bottomleft',
|
|
151
|
+
'watermark-bottomright'
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Add new position class - FIX: use template literal correctly
|
|
155
|
+
this.watermarkElement.classList.add(`watermark-${position}`); // ← FIX QUI
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (this.options.debug) console.log('🏷️ Watermark position updated to:', position);
|
|
159
|
+
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Set whether watermark should hide with controls
|
|
165
|
+
* @param {boolean} hide - True to hide watermark with controls, false to keep always visible
|
|
166
|
+
*/
|
|
167
|
+
setWatermarkAutoHide(hide) {
|
|
168
|
+
this.options.hideWatermark = hide;
|
|
169
|
+
|
|
170
|
+
if (this.watermarkElement) {
|
|
171
|
+
if (hide) {
|
|
172
|
+
this.watermarkElement.classList.add('hide-on-autohide');
|
|
173
|
+
} else {
|
|
174
|
+
this.watermarkElement.classList.remove('hide-on-autohide');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (this.options.debug) console.log('🏷️ Watermark auto-hide set to:', hide);
|
|
179
|
+
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get current watermark settings
|
|
185
|
+
* @returns {object} Current watermark configuration
|
|
186
|
+
*/
|
|
187
|
+
getWatermarkSettings() {
|
|
188
|
+
return {
|
|
189
|
+
url: this.options.watermarkUrl || '',
|
|
190
|
+
link: this.options.watermarkLink || '',
|
|
191
|
+
position: this.options.watermarkPosition || 'bottomright',
|
|
192
|
+
title: this.options.watermarkTitle || '',
|
|
193
|
+
hideWithControls: this.options.hideWatermark
|
|
194
|
+
};
|
|
195
|
+
}
|