@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/.babelrc +3 -0
- package/.dockerignore +1 -0
- package/.eslintrc +9 -0
- package/CHANGELOG.md +169 -0
- package/Dockerfile +11 -0
- package/README.md +21 -0
- package/README_INTERNAL.md +36 -0
- package/THEOplayer.js +57 -0
- package/ads.html +51 -0
- package/dist/theoplayer-mux.js +8 -0
- package/hls-low-latency.html +151 -0
- package/index.html +82 -0
- package/package.json +45 -0
- package/scripts/banner.ejs +6 -0
- package/scripts/deploy.js +48 -0
- package/scripts/server.js +22 -0
- package/src/entry.js +7 -0
- package/src/index.js +551 -0
- package/ui.css +1 -0
- package/webpack.config.js +36 -0
- package/webpack.dev.js +18 -0
- package/webpack.prod.js +9 -0
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;
|