@mux/mux-data-theoplayer 4.8.5

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/src/index.js ADDED
@@ -0,0 +1,551 @@
1
+ import window from 'global/window'; // Remove if you do not need to access the global `window`
2
+ import mux from 'mux-embed';
3
+
4
+ const log = mux.log;
5
+ const assign = mux.utils.assign;
6
+ const ManifestParser = mux.utils.manifestParser;
7
+ const {secondsToMs, safeCall, extractHostname} = mux.utils;
8
+ const players = [];
9
+
10
+ const findPlayerObject = function (player) {
11
+ for (var i = 0; i < players.length; i++) {
12
+ if (players[i]['player'] === player) {
13
+ return players[i];
14
+ }
15
+ }
16
+
17
+ return false;
18
+ };
19
+
20
+ const getAdInformation = function ({ ads }) {
21
+ // Bail out if we don't have an ad module, or if we don't have any current ads
22
+ if (!ads) { return {}; }
23
+
24
+ const { currentAds } = ads;
25
+
26
+ // Also if there is not a current ad
27
+ if (!currentAds || currentAds.length === 0) { return {}; }
28
+
29
+ // I haven't seen anything where it's more than one, so we'll just take the first
30
+ const currentAd = currentAds[0];
31
+ let adData = {};
32
+
33
+ if (currentAd.integration === 'google-ima') {
34
+ adData.ad_creative_id = currentAd.creativeId;
35
+ adData.ad_id = currentAd.id;
36
+ adData.ad_asset_url = currentAd.mediaUrl;
37
+ } else if (currentAd.integration === 'theo') {
38
+ // For THEO's, we need to try to get what we can
39
+ adData.ad_id = currentAd.id;
40
+ adData.ad_creative_id = currentAd.id;
41
+ adData.ad_asset_url = (currentAd.mediaFiles[0] || {}).resourceURI;
42
+ }
43
+
44
+ return adData;
45
+ };
46
+
47
+ const initTHEOplayerMux = function (player, options, theoplayer = window.THEOplayer || window.theoplayer) {
48
+ const defaults = {
49
+ // Allow customers to be in full control of the "errors" that are fatal
50
+ automaticErrorTracking: true
51
+ };
52
+
53
+ // Make sure we got a player - Check properties to ensure that a player was passed
54
+ if (typeof player !== 'object' || !theoplayer || typeof player.seeking === 'undefined') {
55
+ log.warn('[theoplayer-mux] You must provide a valid THEOplayer to initTHEOplayerMux.');
56
+ return;
57
+ }
58
+
59
+ // Make sure this isn't the second time
60
+ let playerObject = findPlayerObject(player);
61
+
62
+ if (playerObject) {
63
+ log.warn('[theoplayer-mux] You have already initialized Mux on this player.');
64
+ return;
65
+ }
66
+
67
+ // Remember this for later
68
+ playerObject = {
69
+ 'player': player,
70
+ 'id': mux.utils.generateShortID()
71
+ };
72
+ players.push(playerObject);
73
+
74
+ // Prepare the data passed in
75
+ options = assign(defaults, options);
76
+
77
+ options.data = assign({
78
+ player_software_name: 'THEOplayer',
79
+ player_software_version: theoplayer.version, // Replace with method to retrieve the version of the player as necessary
80
+ player_mux_plugin_name: 'theoplayer-mux',
81
+ player_mux_plugin_version: '[AIV]{version}[/AIV]'
82
+ }, options.data);
83
+
84
+ // Retrieve the ID and the player element
85
+ const playerID = playerObject.id;
86
+
87
+ let destroyed = false;
88
+
89
+ // Enable customers to emit events through the player instance
90
+ // player.mux = {};
91
+ // player.mux.emit = function (eventType, data) {
92
+ // mux.emit(playerID, eventType, data);
93
+ // };
94
+ const emit = function (eventType, data) {
95
+ mux.emit(playerID, eventType, data);
96
+ };
97
+
98
+ // Allow mux to retrieve the current time - used to track buffering from the mux side
99
+ // Return current playhead time in milliseconds
100
+ options.getPlayheadTime = () => {
101
+ if (destroyed || !player) { return; }
102
+ return secondsToMs(player.currentTime);
103
+ };
104
+
105
+ // Allow mux to automatically retrieve state information about the player on each event sent
106
+ // If these properties are not accessible through getters at runtime, you may need to set them
107
+ // on certain events and store them in a local variable, and return them in the method e.g.
108
+ // let playerWidth, playerHeight;
109
+ // player.on('resize', (width, height) => {
110
+ // playerWidth = width;
111
+ // playerHeight = height;
112
+ // });
113
+ // options.getStateData = () => {
114
+ // return {
115
+ // ...
116
+ // player_width: playerWidth,
117
+ // player_height: playerHeight
118
+ // };
119
+ // };
120
+ let currentManifest;
121
+ let sessionData = {};
122
+
123
+ options.getStateData = () => {
124
+ if (destroyed || !player) { return {}; }
125
+
126
+ let width, height, fullscreen, language;
127
+ let latencyMetrics = {partHoldBack: undefined, holdBack: undefined, targetDuration: undefined, partTargetDuration: undefined, newestProgramTime: undefined};
128
+ let parseManifest = () => {
129
+ try {
130
+ if (currentManifest) {
131
+ latencyMetrics.targetDuration = currentManifest.targetDuration;
132
+ latencyMetrics.partTargetDuration = currentManifest.partTargetDuration;
133
+ const {segments} = currentManifest;
134
+
135
+ if (segments.length > 0) {
136
+ const latestFrag = segments[segments.length - 1];
137
+
138
+ if (latestFrag.dateTimeObject) {
139
+ latencyMetrics.newestProgramTime = latestFrag && latestFrag.dateTimeObject.getTime() + secondsToMs(latestFrag.duration);
140
+ }
141
+ }
142
+
143
+ if (currentManifest.serverControl) {
144
+ latencyMetrics.partHoldBack = currentManifest.serverControl.partHoldBack;
145
+ latencyMetrics.holdBack = currentManifest.serverControl.holdBack;
146
+ }
147
+ }
148
+ } catch (e) {
149
+ }
150
+ };
151
+
152
+ parseManifest();
153
+
154
+ // 2.x THEOplayer with the UI "feature"
155
+ if (player.ui) {
156
+ if (player.ui.fluid()) {
157
+ width = player.element && player.element.offsetWidth;
158
+ height = player.element && player.element.offsetHeight;
159
+ } else {
160
+ width = player.ui.width();
161
+ height = player.ui.height();
162
+ }
163
+
164
+ fullscreen = player.ui.isFullscreen();
165
+ language = player.ui.language();
166
+ } else { // 1.x, or 2.x where they don't have the UI "feature"
167
+ width = player.width || (player.element && player.element.offsetWidth);
168
+ height = player.height || (player.element && player.element.offsetHeight);
169
+ fullscreen = player.fullscreenEnabled;
170
+ }
171
+
172
+ let data = {
173
+ // Required properties - these must be provided every time this is called
174
+ // You _should_ only provide these values if they are defined (i.e. not 'undefined')
175
+ player_is_paused: player.paused, // Return whether the player is paused, stopped, or complete (i.e. in any state that is not actively trying to play back the video)
176
+ player_width: width, // Return the width, in pixels, of the player on screen
177
+ player_height: height, // Return the height, in pixels, of the player on screen
178
+ video_source_height: player.videoHeight, // Return the height, in pixels, of the current rendition playing in the player
179
+ video_source_width: player.videoWidth, // Return the height, in pixels, of the current rendition playing in the player
180
+
181
+ // Preferred properties - these should be provided in this callback if possible
182
+ // If any are missing, that is okay, but this will be a lack of data for the customer at a later time
183
+ player_is_fullscreen: fullscreen, // Return true if the player is fullscreen
184
+ player_autoplay_on: player.autoplay, // Return true if the player is autoplay
185
+ player_preload_on: !!(player.preload && (player.preload !== 'none')), // Return true if the player is preloading data (metadata, on, auto are all "true")
186
+ video_source_url: player.src, // Return the playback URL (i.e. URL to master manifest or MP4 file)
187
+ // video_source_mime_type: 'dash', // Return the mime type (if possible), otherwise the source type (hls, dash, mp4, flv, etc). This won't be static n the near future!
188
+ video_source_duration: isNaN(player.duration) ? undefined : secondsToMs(player.duration), // Return the duration of the source as reported by the player (could be different than is reported by the customer)
189
+ video_source_is_live: player.duration === Infinity,
190
+ // Optional properties - if you have them, send them, but if not, no big deal
191
+ video_poster_url: player.poster, // Return the URL of the poster image used
192
+ player_language_code: language, // Return the language code (e.g. `en`, `en-us`)
193
+ // Latency metrics
194
+ player_program_time: safeCall(player.currentProgramDateTime, 'getTime'),
195
+ video_part_holdback: latencyMetrics.partHoldBack && secondsToMs(latencyMetrics.partHoldBack),
196
+ video_holdback: latencyMetrics.holdBack && secondsToMs(latencyMetrics.holdBack),
197
+ video_target_duration: latencyMetrics.targetDuration && secondsToMs(latencyMetrics.targetDuration),
198
+ video_part_target_duration: latencyMetrics.partTargetDuration && secondsToMs(latencyMetrics.partTargetDuration),
199
+ player_manifest_newest_program_time: isNaN(latencyMetrics.newestProgramTime) ? undefined : latencyMetrics.newestProgramTime
200
+ };
201
+
202
+ assign(data, sessionData);
203
+ sessionData = {};
204
+ return data;
205
+ };
206
+
207
+ var isAdBreak = false;
208
+ var adPlayEventSeen = false;
209
+ var currentSourceUrl = null;
210
+ var firstPlay = true;
211
+ var sendPlay = false;
212
+ let emitPlayBugFlag = false;
213
+
214
+ // The following are linking events that the Mux core SDK requires with events from the player.
215
+ // There may be some cases where the player will send the same Mux event on multiple different
216
+ // events at the player level (e.g. mux.emit('play') may be as a result of multiple player events)
217
+ // OR multiple mux events will be sent as the result of a single player event (e.g. if there is
218
+ // a single event for breaking to a midroll ad, and mux requires a `pause` and an `adbreakstart` event both)
219
+
220
+ // Start initializing early, so playerready event is correct. // Lastly, initialize the tracking.
221
+ mux.init(playerID, options);
222
+
223
+ // Emit the `playerready` event when the player has finished initialization and is ready to begin
224
+ // playback.
225
+ emit('playerready');
226
+
227
+ // Emit the `pause` event when the player is instructed to pause playback. Examples are:
228
+ // 1) User clicks pause to halt playback
229
+ // 2) Playback of content is paused in order to break to an ad (may require simulating the `pause` event when the ad break starts if player is not explicitly paused)
230
+ player.addEventListener('pause', () => {
231
+ if (player.ads && isAdBreak && adPlayEventSeen) { // tiny workaround
232
+ adPlayEventSeen = false;
233
+ emit('adpause', getAdInformation(player));
234
+ } else {
235
+ if (firstPlay === false) {
236
+ emitPlayBugFlag = true;
237
+ }
238
+ emit('pause');
239
+ }
240
+ });
241
+
242
+ // Emit the `play` event when the player is instructed to start playback of the content. Examples are:
243
+ // 1) Initial playback of the content via an autoplay mechanism
244
+ // 2) The user clicking play on the player
245
+ // 3) The user resuming playback of the video (by clicking play) after the player has been paused
246
+ // 4) Content playback is resuming after having been paused for an ad to be played inline (may require additional event tracking than the one below)
247
+ player.addEventListener('play', () => {
248
+ if (player.ads && isAdBreak) {
249
+ adPlayEventSeen = true; // upcoming pause events are adpause events
250
+ emit('adplay', getAdInformation(player));
251
+ } else {
252
+ // We have to do this to catch when player.play() is called right after player.sources is updated,
253
+ // But they didn't wait for the sourcechange player event before attempting to play
254
+ if (currentSourceUrl && !firstPlay && player.src !== currentSourceUrl) {
255
+ // The source has changed!
256
+ // Annoyingly, we can't send the metadata here because it's not included in the event
257
+ // So flag that we need to emit a play event later when sourcechange comes in and we have the new metadata
258
+ sendPlay = true;
259
+ } else {
260
+ // hacky fix for THEOplayer bug where player.ads.playing is updated
261
+ // to 'true' even though an ad isn't playing which then causes 'play'
262
+ // event to be emitted twice in a row.
263
+ if (emitPlayBugFlag === false && firstPlay === false) {
264
+ return;
265
+ }
266
+ currentSourceUrl = player.src;
267
+ emit('play');
268
+ }
269
+ };
270
+ firstPlay = false;
271
+ });
272
+
273
+ // Emit the `playing` event when the player begins actual playback of the content after the most recent
274
+ // `play` event. This should refer to when the first frame is displayed to the user (and when the next
275
+ // frame is presented for resuming from a paused state)
276
+ player.addEventListener('playing', () => {
277
+ if (player.ads && isAdBreak) {
278
+ emit('adplaying', getAdInformation(player));
279
+ } else {
280
+ emit('playing');
281
+ }
282
+ });
283
+
284
+ // Emit the `seeking` event when the player begins seeking to a new position in playback
285
+ player.addEventListener('seeking', () => {
286
+ emit('seeking');
287
+ });
288
+
289
+ // Emit the `seeked` event when the player completes the seeking event (the new playhead position
290
+ // is available, and the player is beginnig to play back at the new location)
291
+ player.addEventListener('seeked', () => {
292
+ emit('seeked');
293
+ });
294
+
295
+ // Emit the `timeupdate` event when the current playhead position has progressed in playback
296
+ // This event should happen at least every 250 milliseconds
297
+ player.addEventListener('timeupdate', () => {
298
+ emit('timeupdate', {
299
+ player_playhead_time: player.currentTime // If you have the time passed in as a param to your event, use that
300
+ });
301
+ });
302
+ let prevBytesLoaded = 0;
303
+
304
+ const getTotalBytesLoaded = () => {
305
+ const totalBytesLoaded = player.metrics.totalBytesLoaded;
306
+
307
+ if (totalBytesLoaded) {
308
+ const diffBytes = totalBytesLoaded - prevBytesLoaded;
309
+
310
+ prevBytesLoaded = totalBytesLoaded;
311
+
312
+ if (diffBytes === 0) {
313
+ return undefined;
314
+ }
315
+ if (diffBytes < 0) {
316
+ return totalBytesLoaded;
317
+ }
318
+ return diffBytes;
319
+ }
320
+ };
321
+ let networkRequestMap = new Map();
322
+
323
+ const getRequestProperties = (req) => {
324
+ let request;
325
+
326
+ if (req.type === 'manifest' || req.type === 'segment') {
327
+ const { url, headers, body, useCredentials, type, subType, mediaType, responseType } = req;
328
+
329
+ request = { url, headers, body, useCredentials, type, subType, mediaType, responseType };
330
+ } else {
331
+ request = req;
332
+ };
333
+ return JSON.stringify(request);
334
+ };
335
+
336
+ const requestInterceptor = (req) => {
337
+ let request;
338
+
339
+ if (req.type === 'manifest' || req.type === 'segment') {
340
+ request = getRequestProperties(req);
341
+ } else {
342
+ request = req;
343
+ }
344
+ networkRequestMap.set(request, Date.now());
345
+ };
346
+
347
+ const responseInterceptor = (res) => {
348
+ const manifest = res.body;
349
+ const bytesLoaded = getTotalBytesLoaded();
350
+ const request = getRequestProperties(res.request);
351
+
352
+ if (res.status >= 400 && res.status <= 599) {
353
+ emit('requestfailed', {
354
+ request_error_code: res.status,
355
+ request_error_text: res.statusText,
356
+ request_hostname: res.request.url,
357
+ request_type: res.request.type,
358
+ request_start: networkRequestMap.get(request)
359
+ });
360
+ };
361
+ if (res.request.type === 'segment') {
362
+ emit('requestcompleted', {
363
+ request_type: 'media',
364
+ request_hostname: extractHostname(res.url),
365
+ request_response_headers: res.headers,
366
+ request_start: networkRequestMap.get(request),
367
+ request_response_start: undefined,
368
+ request_total_bytes_loaded: bytesLoaded, // only supports DASH videos
369
+ request_response_end: Date.now()
370
+ });
371
+ };
372
+ if (res.request.type === 'manifest' && typeof (manifest) === 'string') {
373
+ currentManifest = new ManifestParser(manifest);
374
+
375
+ // Only assign if data is coming. The reset happens in getStateData
376
+ if (!isJSONEmpty(currentManifest.sessionData)) {
377
+ sessionData = currentManifest.sessionData;
378
+ }
379
+ }
380
+ networkRequestMap.delete(request);
381
+ };
382
+
383
+ player.network.addRequestInterceptor(requestInterceptor);
384
+ player.network.addResponseInterceptor(responseInterceptor);
385
+
386
+ // Emit the `error` event when the current playback has encountered a fatal
387
+ // error. Ensure to pass the error code and error message to Mux in this
388
+ // event. You _must_ include at least one of error code and error message
389
+ // (but both is better)
390
+ if (options.automaticErrorTracking) {
391
+ player.addEventListener('error', (event) => {
392
+ // media aborted error code 5004
393
+ if (event.errorObject.code === 5004) {
394
+ emit('requestcanceled', {
395
+ request_hostname: undefined,
396
+ request_url: undefined,
397
+ request_type: 'media'
398
+ });
399
+ }
400
+ emit('error', {
401
+ player_error_code: player.error.code, // The code of the error
402
+ player_error_message: event.error // The message of the error
403
+ });
404
+ });
405
+ }
406
+
407
+ // Emit the `ended` event when the current asset has played to completion,
408
+ // without error.
409
+ player.addEventListener('ended', () => {
410
+ emit('ended');
411
+ });
412
+
413
+ // THEOplayer has a nice sourcechange event that can have metadata, which does
414
+ // exactly what we want on videochange, unless immediately after the player.sources is updated
415
+ // a player.play() is called before the sourcechange has first taken affect
416
+ // which causes the view to be incorrectly split
417
+ player.addEventListener('sourcechange', (event) => {
418
+ // Only do this if they provde `mux` under metadata and the source has changed
419
+ if (event.source && event.source.metadata && event.source.metadata.mux) {
420
+ if (player.src !== currentSourceUrl) {
421
+ currentSourceUrl = player.src;
422
+ emit('videochange', event.source.metadata.mux);
423
+ if (sendPlay) {
424
+ emit('play');
425
+ sendPlay = false;
426
+ }
427
+ } else {
428
+ // we need to do this to inject the metadata even if the source url hasn't changed
429
+ // because there is a corner case where setting an update as the first play won't have any metadata
430
+ emit('hb', event.source.metadata.mux);
431
+ }
432
+ }
433
+ });
434
+
435
+ // And clean up on destroy
436
+ player.addEventListener('destroy', () => {
437
+ destroyed = true;
438
+ emit('destroy');
439
+ });
440
+
441
+ /* AD EVENTS */
442
+ // Depending on your player, you may have separate ad events to track, or
443
+ // the standard playback events may double as ad events. If the latter is the
444
+ // case, you should track the state of the player (ad vs content) and then
445
+ // just prepend the Mux events above with 'ad' when those events fire and
446
+ // the player is in ad mode.
447
+
448
+ // THEOplayer 1.x does not have this module, so we can't track the ads
449
+ if (player.ads) {
450
+ // Emit the `adbreakstart` event when the player breaks to an ad slot. This
451
+ // may be directly at the beginning (before a play event) for pre-rolls, or
452
+ // (for both pre-rolls and mid/post-rolls) may be when the content is paused
453
+ // in order to break to ad.
454
+ player.ads.addEventListener('adbreakbegin', () => {
455
+ emitPlayBugFlag = true;
456
+ isAdBreak = true;
457
+ emit('adbreakstart', getAdInformation(player));
458
+ });
459
+
460
+ // Emit the `adbreakend` event when the ad break is over and content is about
461
+ // to be resumed.
462
+ player.ads.addEventListener('adbreakend', () => {
463
+ isAdBreak = false;
464
+ emit('adbreakend', getAdInformation(player));
465
+ });
466
+
467
+ // Emit the `adplay` event when an individual ad within an ad break is instructed
468
+ // to play. This should match the `play` event, but specific to ads (e.g. should
469
+ // fire on initial play as well as plays after a pause)
470
+ // player.ads.addEventListener('adbegin', () => {
471
+ // emit('adplay');
472
+ // });
473
+
474
+ // Emit the `adended` event when an individual ad within an ad break is played to
475
+ // completion. This should match the `ended` event, but specific to ads
476
+ player.ads.addEventListener('adend', () => {
477
+ emit('adended', getAdInformation(player));
478
+ });
479
+
480
+ // Emit the `aderror` event when an individual ad within an ad break encounters
481
+ // an error. This should match the `error` event, but specific to ads
482
+ player.ads.addEventListener('aderror', () => {
483
+ isAdBreak = false;
484
+ emit('aderror', getAdInformation(player));
485
+ });
486
+ }
487
+
488
+ const currentBitrate = {
489
+ video: undefined,
490
+ audio: undefined,
491
+ total: undefined
492
+ };
493
+
494
+ const dispatchBandWidthEvent = () => {
495
+ const videoBitrate = currentBitrate.video || 0;
496
+ const audioBitrate = currentBitrate.audio || 0;
497
+
498
+ // For HLS streams, video and audio report the total bitrate (the same value), so
499
+ // only add if the two values aren't exactly the same.
500
+ const total = videoBitrate === audioBitrate ? videoBitrate : videoBitrate + audioBitrate;
501
+
502
+ if (total > 0 && total !== currentBitrate.total) {
503
+ currentBitrate.total = total;
504
+ emit('renditionchange', {
505
+ video_source_bitrate: currentBitrate.total
506
+ });
507
+ }
508
+ };
509
+
510
+ player.videoTracks.addEventListener('addtrack', (trackEvent) => {
511
+ trackEvent.track.addEventListener('activequalitychanged', function (qualityEvent) {
512
+ currentBitrate.video = qualityEvent.quality.bandwidth;
513
+ dispatchBandWidthEvent();
514
+ });
515
+ });
516
+
517
+ player.audioTracks.addEventListener('addtrack', (trackEvent) => {
518
+ trackEvent.track.addEventListener('activequalitychanged', function (qualityEvent) {
519
+ currentBitrate.audio = qualityEvent.quality.bandwidth;
520
+ dispatchBandWidthEvent();
521
+ });
522
+ });
523
+
524
+ return {
525
+ emit
526
+ };
527
+ };
528
+
529
+ function isJSONEmpty (jsonObj) {
530
+ return jsonObj &&
531
+ Object.keys(jsonObj).length === 0 &&
532
+ Object.getPrototypeOf(jsonObj) === Object.prototype;
533
+ }
534
+
535
+ // Because THEOplayer's player object is not extensible, we have to expose the
536
+ // videochange capability directly on the window
537
+ window.changeMuxVideo = function (player, data) {
538
+ log.info('[theoplayer-mux] `changeMuxVideo` has been deprecated in favor of ' +
539
+ 'setting Mux metadata when directly changing source, or simply emitting a ' +
540
+ '`videochange` event. See https://docs.mux.com/docs/web-integration-guide?muxLang=THEOplayer#section-6-changing-the-video');
541
+
542
+ const playerObject = findPlayerObject(player);
543
+
544
+ if (playerObject) {
545
+ mux.emit(playerObject.id, 'videochange', data);
546
+ } else {
547
+ log.warn('[theoplayer-mux] The provided player has not been tracked by Mux before.');
548
+ }
549
+ };
550
+
551
+ export default initTHEOplayerMux;