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/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
|
+
|