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/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "myetv-player",
3
+ "version": "1.0.0",
4
+ "description": "MYETV Video Player - Modular JavaScript and SCSS Build System",
5
+ "main": "dist/myetv-player.js",
6
+ "scripts": {
7
+ "build": "node build.js",
8
+ "build:js": "node build.js",
9
+ "build:scss": "npm run build:scss:expanded && npm run build:scss:min",
10
+ "build:scss:expanded": "sass --style=expanded --no-source-map scss/myetv-player.scss css/myetv-player.css",
11
+ "build:scss:min": "sass --style=compressed --no-source-map scss/myetv-player.scss css/myetv-player.min.css",
12
+ "watch:scss": "sass --watch --style=expanded scss/myetv-player.scss:css/myetv-player.css",
13
+ "watch:scss:dev": "sass --watch --source-map scss/myetv-player.scss:css/myetv-player.css",
14
+ "dev": "npm run watch:scss:dev",
15
+ "test": "echo \"No tests specified\" && exit 0",
16
+ "prepare": "npm run build"
17
+ },
18
+ "devDependencies": {
19
+ "sass": "^1.69.0"
20
+ },
21
+ "author": "MYETV - Oskar Cosimo",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/OskarCosimo/myetv-video-player-opensource"
26
+ }
27
+ }
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,548 @@
1
+ /**
2
+ * MYETV Player - Analytics Plugin
3
+ * File: myetv-player-analytics-plugin.js
4
+ * Tracks video player events with Google Analytics 4 and Matomo
5
+ *
6
+ * Supported Events:
7
+ * - video_start: When video starts playing
8
+ * - video_play: When video is played (after pause)
9
+ * - video_pause: When video is paused
10
+ * - video_progress: At 10%, 25%, 50%, 75%, 90% completion
11
+ * - video_complete: When video finishes (95%+)
12
+ * - video_seek: When user seeks to different position
13
+ * - video_quality_change: When quality is changed
14
+ * - video_volume_change: When volume is changed
15
+ * - video_fullscreen: When fullscreen is toggled
16
+ * - video_error: When video error occurs
17
+ *
18
+ * Created by https://www.myetv.tv https://oskarcosimo.com
19
+ */
20
+
21
+ (function () {
22
+ 'use strict';
23
+
24
+ class AnalyticsPlugin {
25
+ constructor(player, options = {}) {
26
+ this.player = player;
27
+ this.options = {
28
+ // Analytics platform: 'ga4', 'matomo', 'both', or 'custom'
29
+ platform: options.platform || 'ga4',
30
+
31
+ // Google Analytics 4 options
32
+ ga4: {
33
+ enabled: options.ga4?.enabled !== undefined ? options.ga4.enabled : true,
34
+ trackingId: options.ga4?.trackingId || null,
35
+ eventPrefix: options.ga4?.eventPrefix || 'video_',
36
+ customDimensions: options.ga4?.customDimensions || {}
37
+ },
38
+
39
+ // Matomo options
40
+ matomo: {
41
+ enabled: options.matomo?.enabled !== undefined ? options.matomo.enabled : false,
42
+ siteId: options.matomo?.siteId || null,
43
+ trackerUrl: options.matomo?.trackerUrl || null
44
+ },
45
+
46
+ // Custom tracking function
47
+ customTracker: options.customTracker || null,
48
+
49
+ // What to track
50
+ trackEvents: {
51
+ start: options.trackEvents?.start !== undefined ? options.trackEvents.start : true,
52
+ play: options.trackEvents?.play !== undefined ? options.trackEvents.play : true,
53
+ pause: options.trackEvents?.pause !== undefined ? options.trackEvents.pause : true,
54
+ progress: options.trackEvents?.progress !== undefined ? options.trackEvents.progress : true,
55
+ complete: options.trackEvents?.complete !== undefined ? options.trackEvents.complete : true,
56
+ seek: options.trackEvents?.seek !== undefined ? options.trackEvents.seek : true,
57
+ quality: options.trackEvents?.quality !== undefined ? options.trackEvents.quality : true,
58
+ volume: options.trackEvents?.volume !== undefined ? options.trackEvents.volume : false,
59
+ fullscreen: options.trackEvents?.fullscreen !== undefined ? options.trackEvents.fullscreen : true,
60
+ error: options.trackEvents?.error !== undefined ? options.trackEvents.error : true
61
+ },
62
+
63
+ // Progress milestones (percentages)
64
+ progressMilestones: options.progressMilestones || [10, 25, 50, 75, 90],
65
+
66
+ // Video metadata
67
+ videoTitle: options.videoTitle || '',
68
+ videoCategory: options.videoCategory || '',
69
+ videoId: options.videoId || '',
70
+
71
+ // Debug mode
72
+ debug: options.debug || false
73
+ };
74
+
75
+ // Get plugin API
76
+ this.api = player.getPluginAPI();
77
+
78
+ // Tracking state
79
+ this.state = {
80
+ hasStarted: false,
81
+ hasCompleted: false,
82
+ progressMilestones: {},
83
+ lastPosition: 0,
84
+ startTime: null,
85
+ totalWatchTime: 0,
86
+ pauseTime: null,
87
+ seekCount: 0,
88
+ qualityChanges: 0,
89
+ volumeChanges: 0,
90
+ fullscreenToggles: 0
91
+ };
92
+
93
+ // Initialize progress milestones tracking
94
+ this.options.progressMilestones.forEach(milestone => {
95
+ this.state.progressMilestones[milestone] = false;
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Setup plugin
101
+ */
102
+ setup() {
103
+ this.log('Analytics plugin setup started');
104
+
105
+ // Detect video metadata if not provided
106
+ this.detectVideoMetadata();
107
+
108
+ // Setup event listeners
109
+ this.setupEventListeners();
110
+
111
+ // Initialize analytics platforms
112
+ this.initializeAnalytics();
113
+
114
+ this.log('Analytics plugin setup completed');
115
+ }
116
+
117
+ /**
118
+ * Detect video metadata from player
119
+ */
120
+ detectVideoMetadata() {
121
+ if (!this.options.videoTitle) {
122
+ this.options.videoTitle = this.api.video.title ||
123
+ this.api.options.videoTitle ||
124
+ this.api.video.src ||
125
+ 'Untitled Video';
126
+ }
127
+
128
+ if (!this.options.videoId) {
129
+ this.options.videoId = this.api.video.id ||
130
+ this.generateVideoId();
131
+ }
132
+
133
+ this.log('Video metadata:', {
134
+ title: this.options.videoTitle,
135
+ id: this.options.videoId,
136
+ category: this.options.videoCategory
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Generate video ID from source
142
+ */
143
+ generateVideoId() {
144
+ const src = this.api.video.src || this.api.video.currentSrc;
145
+ if (!src) return 'unknown';
146
+
147
+ // Extract filename from URL
148
+ const filename = src.split('/').pop().split('?')[0];
149
+ return filename.replace(/\.[^/.]+$/, ''); // Remove extension
150
+ }
151
+
152
+ /**
153
+ * Initialize analytics platforms
154
+ */
155
+ initializeAnalytics() {
156
+ // Check for GA4
157
+ if (this.options.platform === 'ga4' || this.options.platform === 'both') {
158
+ if (typeof gtag !== 'undefined') {
159
+ this.log('GA4 detected and ready');
160
+ } else if (typeof ga !== 'undefined') {
161
+ this.log('Universal Analytics detected');
162
+ } else {
163
+ this.log('Warning: GA4 not found. Make sure gtag.js is loaded.');
164
+ }
165
+ }
166
+
167
+ // Check for Matomo
168
+ if (this.options.platform === 'matomo' || this.options.platform === 'both') {
169
+ if (typeof _paq !== 'undefined') {
170
+ this.log('Matomo detected and ready');
171
+ } else {
172
+ this.log('Warning: Matomo not found. Make sure Matomo tracking code is loaded.');
173
+ }
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Setup event listeners
179
+ */
180
+ setupEventListeners() {
181
+ // Video play
182
+ if (this.options.trackEvents.play) {
183
+ this.api.addEventListener('play', () => this.onPlay());
184
+ }
185
+
186
+ // Video pause
187
+ if (this.options.trackEvents.pause) {
188
+ this.api.addEventListener('pause', () => this.onPause());
189
+ }
190
+
191
+ // Video ended
192
+ if (this.options.trackEvents.complete) {
193
+ this.api.addEventListener('ended', () => this.onComplete());
194
+ }
195
+
196
+ // Time update for progress tracking
197
+ if (this.options.trackEvents.progress) {
198
+ this.api.addEventListener('timeupdate', () => this.onTimeUpdate());
199
+ }
200
+
201
+ // Seeking
202
+ if (this.options.trackEvents.seek) {
203
+ this.api.addEventListener('seeking', () => this.onSeeking());
204
+ this.api.addEventListener('seeked', () => this.onSeeked());
205
+ }
206
+
207
+ // Quality change
208
+ if (this.options.trackEvents.quality) {
209
+ this.api.addEventListener('qualitychanged', (e) => this.onQualityChange(e));
210
+ }
211
+
212
+ // Volume change
213
+ if (this.options.trackEvents.volume) {
214
+ this.api.addEventListener('volumechange', () => this.onVolumeChange());
215
+ }
216
+
217
+ // Fullscreen
218
+ if (this.options.trackEvents.fullscreen) {
219
+ this.api.addEventListener('fullscreenchange', () => this.onFullscreenChange());
220
+ }
221
+
222
+ // Error
223
+ if (this.options.trackEvents.error) {
224
+ this.api.addEventListener('error', (e) => this.onError(e));
225
+ }
226
+
227
+ this.log('Event listeners registered');
228
+ }
229
+
230
+ /**
231
+ * Handle play event
232
+ */
233
+ onPlay() {
234
+ const currentTime = this.api.getCurrentTime();
235
+
236
+ // Track start only once
237
+ if (!this.state.hasStarted && currentTime < 3) {
238
+ this.state.hasStarted = true;
239
+ this.state.startTime = Date.now();
240
+
241
+ if (this.options.trackEvents.start) {
242
+ this.trackEvent('start', {
243
+ video_current_time: 0,
244
+ video_duration: this.api.getDuration()
245
+ });
246
+ }
247
+ }
248
+
249
+ // Track play (resume)
250
+ this.trackEvent('play', {
251
+ video_current_time: currentTime,
252
+ video_duration: this.api.getDuration(),
253
+ video_percent: this.getWatchedPercent()
254
+ });
255
+
256
+ // Calculate pause duration
257
+ if (this.state.pauseTime) {
258
+ const pauseDuration = Math.round((Date.now() - this.state.pauseTime) / 1000);
259
+ this.log(`Resumed after ${pauseDuration}s pause`);
260
+ this.state.pauseTime = null;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Handle pause event
266
+ */
267
+ onPause() {
268
+ const currentTime = this.api.getCurrentTime();
269
+ const duration = this.api.getDuration();
270
+
271
+ // Don't track pause at the end
272
+ if (currentTime >= duration - 0.5) {
273
+ return;
274
+ }
275
+
276
+ this.state.pauseTime = Date.now();
277
+
278
+ this.trackEvent('pause', {
279
+ video_current_time: currentTime,
280
+ video_duration: duration,
281
+ video_percent: this.getWatchedPercent()
282
+ });
283
+ }
284
+
285
+ /**
286
+ * Handle time update (for progress tracking)
287
+ */
288
+ onTimeUpdate() {
289
+ const percent = this.getWatchedPercent();
290
+
291
+ // Check each milestone
292
+ this.options.progressMilestones.forEach(milestone => {
293
+ if (!this.state.progressMilestones[milestone] && percent >= milestone) {
294
+ this.state.progressMilestones[milestone] = true;
295
+
296
+ this.trackEvent('progress', {
297
+ video_current_time: this.api.getCurrentTime(),
298
+ video_duration: this.api.getDuration(),
299
+ video_percent: milestone,
300
+ milestone: `${milestone}%`
301
+ });
302
+ }
303
+ });
304
+ }
305
+
306
+ /**
307
+ * Handle seeking start
308
+ */
309
+ onSeeking() {
310
+ this.state.lastSeekFrom = this.api.getCurrentTime();
311
+ }
312
+
313
+ /**
314
+ * Handle seeking end
315
+ */
316
+ onSeeked() {
317
+ const seekTo = this.api.getCurrentTime();
318
+ const seekFrom = this.state.lastSeekFrom || 0;
319
+ this.state.seekCount++;
320
+
321
+ this.trackEvent('seek', {
322
+ video_current_time: seekTo,
323
+ video_duration: this.api.getDuration(),
324
+ video_percent: this.getWatchedPercent(),
325
+ seek_from: seekFrom,
326
+ seek_to: seekTo,
327
+ seek_distance: Math.abs(seekTo - seekFrom),
328
+ total_seeks: this.state.seekCount
329
+ });
330
+ }
331
+
332
+ /**
333
+ * Handle quality change
334
+ */
335
+ onQualityChange(event) {
336
+ this.state.qualityChanges++;
337
+
338
+ this.trackEvent('quality_change', {
339
+ video_current_time: this.api.getCurrentTime(),
340
+ video_duration: this.api.getDuration(),
341
+ old_quality: event.detail?.oldQuality || 'unknown',
342
+ new_quality: event.detail?.newQuality || 'unknown',
343
+ total_quality_changes: this.state.qualityChanges
344
+ });
345
+ }
346
+
347
+ /**
348
+ * Handle volume change
349
+ */
350
+ onVolumeChange() {
351
+ this.state.volumeChanges++;
352
+
353
+ this.trackEvent('volume_change', {
354
+ video_current_time: this.api.getCurrentTime(),
355
+ video_volume: this.api.getVolume(),
356
+ video_muted: this.api.video.muted,
357
+ total_volume_changes: this.state.volumeChanges
358
+ });
359
+ }
360
+
361
+ /**
362
+ * Handle fullscreen change
363
+ */
364
+ onFullscreenChange() {
365
+ this.state.fullscreenToggles++;
366
+ const isFullscreen = document.fullscreenElement !== null;
367
+
368
+ this.trackEvent('fullscreen', {
369
+ video_current_time: this.api.getCurrentTime(),
370
+ fullscreen_enabled: isFullscreen,
371
+ total_fullscreen_toggles: this.state.fullscreenToggles
372
+ });
373
+ }
374
+
375
+ /**
376
+ * Handle complete
377
+ */
378
+ onComplete() {
379
+ if (this.state.hasCompleted) return;
380
+
381
+ this.state.hasCompleted = true;
382
+
383
+ const watchDuration = this.state.startTime
384
+ ? Math.round((Date.now() - this.state.startTime) / 1000)
385
+ : 0;
386
+
387
+ this.trackEvent('complete', {
388
+ video_current_time: this.api.getDuration(),
389
+ video_duration: this.api.getDuration(),
390
+ video_percent: 100,
391
+ watch_duration: watchDuration,
392
+ seek_count: this.state.seekCount,
393
+ quality_changes: this.state.qualityChanges
394
+ });
395
+ }
396
+
397
+ /**
398
+ * Handle error
399
+ */
400
+ onError(event) {
401
+ const error = event.detail || this.api.video.error;
402
+
403
+ this.trackEvent('error', {
404
+ video_current_time: this.api.getCurrentTime(),
405
+ error_code: error?.code || 'unknown',
406
+ error_message: error?.message || 'Unknown error',
407
+ video_src: this.api.video.currentSrc
408
+ });
409
+ }
410
+
411
+ /**
412
+ * Track event to all enabled platforms
413
+ */
414
+ trackEvent(action, data = {}) {
415
+ const eventData = {
416
+ video_title: this.options.videoTitle,
417
+ video_id: this.options.videoId,
418
+ video_category: this.options.videoCategory,
419
+ video_url: this.api.video.currentSrc,
420
+ video_provider: 'myetv-player',
421
+ ...data
422
+ };
423
+
424
+ this.log(`Tracking event: ${action}`, eventData);
425
+
426
+ // Track to GA4
427
+ if (this.options.platform === 'ga4' || this.options.platform === 'both') {
428
+ this.trackToGA4(action, eventData);
429
+ }
430
+
431
+ // Track to Matomo
432
+ if (this.options.platform === 'matomo' || this.options.platform === 'both') {
433
+ this.trackToMatomo(action, eventData);
434
+ }
435
+
436
+ // Track to custom tracker
437
+ if (typeof this.options.customTracker === 'function') {
438
+ this.options.customTracker(action, eventData);
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Track to Google Analytics 4
444
+ */
445
+ trackToGA4(action, data) {
446
+ if (typeof gtag === 'undefined') {
447
+ this.log('GA4 not available, skipping');
448
+ return;
449
+ }
450
+
451
+ const eventName = this.options.ga4.eventPrefix + action;
452
+
453
+ gtag('event', eventName, {
454
+ ...data,
455
+ ...this.options.ga4.customDimensions
456
+ });
457
+
458
+ this.log(`GA4 event sent: ${eventName}`);
459
+ }
460
+
461
+ /**
462
+ * Track to Matomo
463
+ */
464
+ trackToMatomo(action, data) {
465
+ if (typeof _paq === 'undefined') {
466
+ this.log('Matomo not available, skipping');
467
+ return;
468
+ }
469
+
470
+ // Matomo custom event format: trackEvent(category, action, name, value)
471
+ _paq.push([
472
+ 'trackEvent',
473
+ 'Video',
474
+ action,
475
+ this.options.videoTitle,
476
+ data.video_current_time || 0
477
+ ]);
478
+
479
+ this.log(`Matomo event sent: ${action}`);
480
+ }
481
+
482
+ /**
483
+ * Get watched percent
484
+ */
485
+ getWatchedPercent() {
486
+ const currentTime = this.api.getCurrentTime();
487
+ const duration = this.api.getDuration();
488
+
489
+ if (!duration || duration === 0) return 0;
490
+
491
+ return Math.round((currentTime / duration) * 100);
492
+ }
493
+
494
+ /**
495
+ * Debug log
496
+ */
497
+ log(...args) {
498
+ if (this.options.debug) {
499
+ console.log('📊 Analytics Plugin:', ...args);
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Get analytics summary
505
+ */
506
+ getSummary() {
507
+ return {
508
+ videoTitle: this.options.videoTitle,
509
+ videoId: this.options.videoId,
510
+ hasStarted: this.state.hasStarted,
511
+ hasCompleted: this.state.hasCompleted,
512
+ currentProgress: this.getWatchedPercent(),
513
+ milestonesReached: Object.keys(this.state.progressMilestones)
514
+ .filter(k => this.state.progressMilestones[k]),
515
+ seekCount: this.state.seekCount,
516
+ qualityChanges: this.state.qualityChanges,
517
+ volumeChanges: this.state.volumeChanges,
518
+ fullscreenToggles: this.state.fullscreenToggles,
519
+ watchDuration: this.state.startTime
520
+ ? Math.round((Date.now() - this.state.startTime) / 1000)
521
+ : 0
522
+ };
523
+ }
524
+
525
+ /**
526
+ * Dispose plugin
527
+ */
528
+ dispose() {
529
+ this.log('Analytics plugin disposed');
530
+
531
+ // Track final state if video was started but not completed
532
+ if (this.state.hasStarted && !this.state.hasCompleted) {
533
+ this.trackEvent('abandoned', {
534
+ video_current_time: this.api.getCurrentTime(),
535
+ video_duration: this.api.getDuration(),
536
+ video_percent: this.getWatchedPercent(),
537
+ watch_duration: this.state.startTime
538
+ ? Math.round((Date.now() - this.state.startTime) / 1000)
539
+ : 0
540
+ });
541
+ }
542
+ }
543
+ }
544
+
545
+ // Register plugin globally
546
+ window.registerMYETVPlugin('analytics', AnalyticsPlugin);
547
+
548
+ })();
@@ -0,0 +1 @@
1
+