@gumlet/insights-js-core 1.0.3 → 1.1.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/main.yml +87 -0
- package/.gitlab-ci.yml +54 -0
- package/LICENSE +21 -0
- package/README.md +30 -0
- package/bitbucket-pipelines.yml +35 -0
- package/docs/payload-documentation.md +72 -0
- package/html/bitmovin.html +82 -0
- package/html/dashjs.html +55 -0
- package/html/hlsjs.html +72 -0
- package/html/html5.html +59 -0
- package/html/shaka.html +102 -0
- package/html/videojs.html +67 -0
- package/index.html +73 -0
- package/jest.config.js +187 -0
- package/js/adapters/Bitmovin7Adapter.js +352 -0
- package/js/adapters/BitmovinAdapter.js +198 -0
- package/js/adapters/DashjsAdapter.js +140 -0
- package/js/adapters/HTML5Adapter.js +774 -0
- package/js/adapters/HlsjsAdapter.js +152 -0
- package/js/adapters/ShakaAdapter.js +81 -0
- package/js/adapters/VideoJsAdapter.js +455 -0
- package/js/analyticsStateMachines/Bitmovin7AnalyticsStateMachine.js +471 -0
- package/js/analyticsStateMachines/BitmovinAnalyticsStateMachine.js +299 -0
- package/js/analyticsStateMachines/HTML5AnalyticsStateMachine.js +443 -0
- package/js/analyticsStateMachines/VideoJsAnalyticsStateMachine.js +503 -0
- package/js/cast/CastClient.js +50 -0
- package/js/cast/CastReceiver.js +37 -0
- package/js/core/AdapterFactory.js +41 -0
- package/js/core/Analytics.js +1357 -0
- package/js/core/AnalyticsStateMachineFactory.js +36 -0
- package/js/core/GumletInsightsExport.js +75 -0
- package/js/enums/CDNProviders.js +11 -0
- package/js/enums/Events.js +32 -0
- package/js/enums/GumletEnum.js +19 -0
- package/js/enums/MIMETypes.js +30 -0
- package/js/enums/Players.js +11 -0
- package/js/enums/StreamTypes.js +15 -0
- package/js/utils/EventsCall.js +22 -0
- package/js/utils/HttpCall.js +57 -0
- package/js/utils/LicenseCall.js +18 -0
- package/js/utils/Logger.js +40 -0
- package/js/utils/PlayerDetector.js +75 -0
- package/js/utils/PlayerInitCall.js +22 -0
- package/js/utils/SessionCreationCall.js +22 -0
- package/js/utils/Settings.js +3 -0
- package/js/utils/Utils.js +195 -0
- package/package.json +62 -1
- package/precommit.bash +8 -0
- package/tests/stage1.test.js +50 -0
- package/webpack.config.debug.js +34 -0
- package/webpack.config.js +40 -0
- package/webpack.config.release.js +62 -0
- package/gumlet-insights.min.js +0 -2
- package/gumlet-insights.min.js.LICENSE.txt +0 -10
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
import Events from '../enums/Events';
|
|
2
|
+
import {getMIMETypeFromFileExtension} from '../enums/MIMETypes';
|
|
3
|
+
import {getStreamTypeFromMIMEType} from '../enums/StreamTypes';
|
|
4
|
+
import {Players} from '../enums/Players';
|
|
5
|
+
|
|
6
|
+
const BUFFERING_TIMECHANGED_TIMEOUT = 1000;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef QualityLevelInfo
|
|
10
|
+
* @type {Object}
|
|
11
|
+
* @prop {bitrate} number
|
|
12
|
+
* @prop {width} number
|
|
13
|
+
* @prop {height} number
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Base-class for all HTML5 media based playback engines
|
|
18
|
+
* @class
|
|
19
|
+
* @constructor
|
|
20
|
+
*/
|
|
21
|
+
export class HTML5Adapter {
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @constructs
|
|
25
|
+
* @param {HTMLMediaElement} mediaElement
|
|
26
|
+
* @param {function} eventCallback
|
|
27
|
+
* @param {AnalyticsStateMachine} stateMachine
|
|
28
|
+
*/
|
|
29
|
+
constructor(mediaElement, eventCallback, stateMachine, playerName = null) {
|
|
30
|
+
/**
|
|
31
|
+
* @public
|
|
32
|
+
* @member {AnalyticsEventCallback}
|
|
33
|
+
*/
|
|
34
|
+
this.eventCallback = eventCallback;
|
|
35
|
+
|
|
36
|
+
if (playerName) {
|
|
37
|
+
this.playerSoftwareName = playerName;
|
|
38
|
+
}else {
|
|
39
|
+
this.playerSoftwareName = Players.HTML;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @member {AnalyticsStateMachine}
|
|
44
|
+
*/
|
|
45
|
+
this.stateMachine = stateMachine;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @public
|
|
49
|
+
* @member {HTMLMediaElement}
|
|
50
|
+
*/
|
|
51
|
+
this.mediaEl = mediaElement;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @public
|
|
55
|
+
* @member {function[]}
|
|
56
|
+
*/
|
|
57
|
+
this.mediaElEventHandlers = [];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @private
|
|
61
|
+
* @member {number}
|
|
62
|
+
*/
|
|
63
|
+
this.analyticsBitrate_ = -1;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @private
|
|
67
|
+
* @member {string}
|
|
68
|
+
*/
|
|
69
|
+
this.audioCodec_ = "";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @private
|
|
73
|
+
* @member {string}
|
|
74
|
+
*/
|
|
75
|
+
this.videoCodec_ = "";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @private
|
|
79
|
+
* @member {number}
|
|
80
|
+
*/
|
|
81
|
+
this.bufferingTimeout_ = null;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @private
|
|
85
|
+
* @member {boolean}
|
|
86
|
+
*/
|
|
87
|
+
this.isBuffering_ = false;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @private
|
|
91
|
+
* @member {boolean}
|
|
92
|
+
*/
|
|
93
|
+
this.isLive_ = false;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @private
|
|
97
|
+
* @member {boolean}
|
|
98
|
+
*/
|
|
99
|
+
this.isPaused_ = false;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @private
|
|
103
|
+
* @member {number}
|
|
104
|
+
*/
|
|
105
|
+
this.previousMediaTime_ = null;
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @private
|
|
110
|
+
* @member {boolean}
|
|
111
|
+
*/
|
|
112
|
+
this.needsReadyEvent_ = true;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @private
|
|
116
|
+
* @member {boolean}
|
|
117
|
+
*/
|
|
118
|
+
this.needsFirstPlayIntent_ = true;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @private
|
|
122
|
+
* @member {boolean}
|
|
123
|
+
*/
|
|
124
|
+
this.mediaElementSet_ = false;
|
|
125
|
+
this.playbackReadySet_ = false;
|
|
126
|
+
|
|
127
|
+
if (mediaElement) {
|
|
128
|
+
this.setMediaElement();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Used to setup against the media element.
|
|
136
|
+
* We need this method to desynchronize construction of this class
|
|
137
|
+
* and the actual initialization against the media element.
|
|
138
|
+
* That is because at construction some media engine
|
|
139
|
+
* may not already have the media element attached, for example
|
|
140
|
+
* when passing in the DOM element is happening at once with passing the source URL
|
|
141
|
+
* and can not be decoupled.
|
|
142
|
+
* We are then awaiting an event from the engine and calling this with the media element
|
|
143
|
+
* as argument from our sub-class.
|
|
144
|
+
*
|
|
145
|
+
* This method can also be called without arguments and then it will perform
|
|
146
|
+
* initialization against the existing media element (should only be called once, will throw an error otherwise)
|
|
147
|
+
*
|
|
148
|
+
* It can also be used to replace the element.
|
|
149
|
+
*
|
|
150
|
+
*
|
|
151
|
+
* @param {HTMLMediaElement} mediaElement
|
|
152
|
+
*/
|
|
153
|
+
setMediaElement(mediaElement = null) {
|
|
154
|
+
// replace previously existing, if calld with args
|
|
155
|
+
if (mediaElement && this.mediaEl) {
|
|
156
|
+
this.unregisterMediaElement();
|
|
157
|
+
this.mediaElementSet_ = false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// if called without args we assume it's already there
|
|
161
|
+
// we can also be called with args but without any being there before
|
|
162
|
+
if (mediaElement) {
|
|
163
|
+
this.mediaEl = mediaElement;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!this.mediaEl) {
|
|
167
|
+
throw new Error('No media element owned');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (this.mediaElementSet_) {
|
|
171
|
+
throw new Error('Media element already set (only call this once)');
|
|
172
|
+
}
|
|
173
|
+
this.mediaElementSet_ = true;
|
|
174
|
+
|
|
175
|
+
this.registerMediaElement();
|
|
176
|
+
this.onMaybeReady();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Implemented by sub-class to deliver current quality-level info
|
|
181
|
+
* specific to media-engine.
|
|
182
|
+
* @returns {QualityLevelInfo}
|
|
183
|
+
* @abstract
|
|
184
|
+
*/
|
|
185
|
+
getCurrentQualityLevelInfo() {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @abstract
|
|
191
|
+
* @returns {boolean}
|
|
192
|
+
*/
|
|
193
|
+
isLive() {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Can be overriden by sub-classes
|
|
199
|
+
* @returns {string}
|
|
200
|
+
*/
|
|
201
|
+
getMIMEType() {
|
|
202
|
+
const mediaEl = this.mediaEl;
|
|
203
|
+
let streamURL = "";
|
|
204
|
+
if (!mediaEl) {
|
|
205
|
+
streamURL = null;
|
|
206
|
+
}
|
|
207
|
+
if (mediaEl && mediaEl.src) {
|
|
208
|
+
streamURL = mediaEl.src
|
|
209
|
+
}else if(mediaEl && mediaEl.querySelector('source') && mediaEl.querySelector('source').src){
|
|
210
|
+
streamURL = mediaEl.querySelector('source').src;
|
|
211
|
+
}else{
|
|
212
|
+
streamURL = null;
|
|
213
|
+
}
|
|
214
|
+
return getMIMETypeFromFileExtension(streamURL);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Can be overriden by sub-classes
|
|
219
|
+
* @returns {string}
|
|
220
|
+
*/
|
|
221
|
+
getStreamType() {
|
|
222
|
+
return getStreamTypeFromMIMEType(this.getMIMEType());
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @abstract
|
|
227
|
+
* @returns {string}
|
|
228
|
+
*/
|
|
229
|
+
getPlayerVersion() {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Can be overriden by subclasses.
|
|
235
|
+
* @returns {string}
|
|
236
|
+
*/
|
|
237
|
+
getStreamURL() {
|
|
238
|
+
const mediaEl = this.mediaEl;
|
|
239
|
+
|
|
240
|
+
if (!mediaEl) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
if (mediaEl && mediaEl.src) {
|
|
244
|
+
return mediaEl.src
|
|
245
|
+
}else if(mediaEl && mediaEl.querySelector('source') && mediaEl.querySelector('source').src){
|
|
246
|
+
return mediaEl.querySelector('source').src;
|
|
247
|
+
}else{
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
resetMedia() {
|
|
253
|
+
this.mediaEl = null;
|
|
254
|
+
this.mediaElEventHandlers = [];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
registerMediaElement() {
|
|
258
|
+
|
|
259
|
+
const mediaEl = this.mediaEl;
|
|
260
|
+
if (!mediaEl) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
this.listenToMediaElementEvent('loadedmetadata', () => {
|
|
265
|
+
|
|
266
|
+
// See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState
|
|
267
|
+
// HAVE_NOTHING 0 No information is available about the media resource.
|
|
268
|
+
// HAVE_METADATA 1 Enough of the media resource has been retrieved that
|
|
269
|
+
// the metadata attributes are initialized. Seeking will no longer raise an exception.
|
|
270
|
+
// HAVE_CURRENT_DATA 2 Data is available for the current playback position,
|
|
271
|
+
// but not enough to actually play more than one frame.
|
|
272
|
+
// HAVE_FUTURE_DATA 3 Data for the current playback position as well as for
|
|
273
|
+
// at least a little bit of time into the future is available
|
|
274
|
+
// (in other words, at least two frames of video, for example).
|
|
275
|
+
// HAVE_ENOUGH_DATA 4 Enough data is available—and the download rate is high
|
|
276
|
+
// enough—that the media can be played through to the end without interruption.
|
|
277
|
+
if (mediaEl.readyState !== 1) {
|
|
278
|
+
// we can't really gather any more information at this point
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// silent
|
|
283
|
+
this.checkQualityLevelAttributes(true);
|
|
284
|
+
|
|
285
|
+
const {
|
|
286
|
+
duration,
|
|
287
|
+
autoplay,
|
|
288
|
+
videoWidth,
|
|
289
|
+
videoHeight,
|
|
290
|
+
muted
|
|
291
|
+
} = mediaEl;
|
|
292
|
+
|
|
293
|
+
const width = mediaEl.clientWidth;
|
|
294
|
+
const height = mediaEl.clientHeight;
|
|
295
|
+
|
|
296
|
+
// This is redundant with what we give to updateMetadata method.
|
|
297
|
+
// Not sure if there are good reasons to keep that so or if we should better centralize.
|
|
298
|
+
const info = {
|
|
299
|
+
type : 'html5',
|
|
300
|
+
isLive : this.isLive(),
|
|
301
|
+
version : this.getPlayerVersion() || 'html5',
|
|
302
|
+
streamType : this.getStreamType(),
|
|
303
|
+
streamUrl : this.getStreamURL(),
|
|
304
|
+
duration,
|
|
305
|
+
autoplay,
|
|
306
|
+
// HTMLVideoElement.width and HTMLVideoElement.height
|
|
307
|
+
// is a DOMString that reflects the height HTML attribute,
|
|
308
|
+
// which specifies the height of the display area, in CSS pixels.
|
|
309
|
+
// See https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement
|
|
310
|
+
currentVideoData:{
|
|
311
|
+
height : videoHeight,
|
|
312
|
+
width : videoWidth,
|
|
313
|
+
bitrate : this.analyticsBitrate_,
|
|
314
|
+
audioCodec : this.audioCodec_,
|
|
315
|
+
videoCodec : this.videoCodec_,
|
|
316
|
+
},
|
|
317
|
+
// Returns an unsigned long containing the intrinsic
|
|
318
|
+
// height of the resource in CSS pixels,
|
|
319
|
+
// taking into account the dimensions, aspect ratio,
|
|
320
|
+
// clean aperture, resolution, and so forth,
|
|
321
|
+
// as defined for the format used by the resource.
|
|
322
|
+
// If the element's ready state is HAVE_NOTHING, the value is 0.
|
|
323
|
+
// See https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement
|
|
324
|
+
videoWindowWidth : parseInt(width),
|
|
325
|
+
videoWindowHeight: parseInt(height),
|
|
326
|
+
muted
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// silence events if we have not yet intended play
|
|
330
|
+
if (this.needsFirstPlayIntent_) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
this.eventCallback(Events.METADATA_LOADED, info);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// For DashJS this is written in DashjsAdapter.js as it overrides
|
|
338
|
+
this.listenToMediaElementEvent('canplay', () => {
|
|
339
|
+
|
|
340
|
+
if (this.playbackReadySet_) {
|
|
341
|
+
return;
|
|
342
|
+
}else {
|
|
343
|
+
this.playbackReadySet_ = true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const {
|
|
347
|
+
duration,
|
|
348
|
+
autoplay,
|
|
349
|
+
videoWidth,
|
|
350
|
+
videoHeight,
|
|
351
|
+
muted
|
|
352
|
+
} = mediaEl;
|
|
353
|
+
|
|
354
|
+
const width = mediaEl.clientWidth;
|
|
355
|
+
const height = mediaEl.clientHeight;
|
|
356
|
+
|
|
357
|
+
const info = {
|
|
358
|
+
type : 'html5',
|
|
359
|
+
isLive : this.isLive(),
|
|
360
|
+
version : this.getPlayerVersion() || 'html5',
|
|
361
|
+
streamType : this.getStreamType(),
|
|
362
|
+
streamUrl : this.getStreamURL(),
|
|
363
|
+
duration : duration,
|
|
364
|
+
autoplay : autoplay,
|
|
365
|
+
playerSoftware : this.playerSoftwareName,
|
|
366
|
+
videoWindowWidth : parseInt(width),
|
|
367
|
+
videoWindowHeight: parseInt(height),
|
|
368
|
+
currentVideoData:{
|
|
369
|
+
height : videoHeight,
|
|
370
|
+
width : videoWidth,
|
|
371
|
+
bitrate : this.analyticsBitrate_,
|
|
372
|
+
audioCodec : this.audioCodec_,
|
|
373
|
+
videoCodec : this.videoCodec_,
|
|
374
|
+
},
|
|
375
|
+
muted
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
this.eventCallback(Events.SOURCE_LOADED, info);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// We need the PLAY event to indicate the intent to play
|
|
382
|
+
// NOTE: use TIMECHANGED event on 'playing' and trigger PLAY as intended in states.dot graph
|
|
383
|
+
|
|
384
|
+
this.listenToMediaElementEvent('play', () => {
|
|
385
|
+
const { currentTime, videoWidth, videoHeight,} = mediaEl;
|
|
386
|
+
|
|
387
|
+
this.needsFirstPlayIntent_ = false;
|
|
388
|
+
|
|
389
|
+
this.eventCallback(Events.PLAY, {
|
|
390
|
+
currentVideoData:{
|
|
391
|
+
height : videoHeight,
|
|
392
|
+
width : videoWidth,
|
|
393
|
+
bitrate : this.analyticsBitrate_,
|
|
394
|
+
audioCodec : this.audioCodec_,
|
|
395
|
+
videoCodec : this.videoCodec_,
|
|
396
|
+
},
|
|
397
|
+
currentTime
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
this.listenToMediaElementEvent('pause', () => {
|
|
402
|
+
this.onPaused();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
this.listenToMediaElementEvent('playing', () => {
|
|
406
|
+
const {currentTime, videoWidth, videoHeight} = mediaEl;
|
|
407
|
+
|
|
408
|
+
this.isPaused_ = false;
|
|
409
|
+
|
|
410
|
+
// silence events if we have not yet intended play
|
|
411
|
+
if (this.needsFirstPlayIntent_) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
this.eventCallback(Events.TIMECHANGED, {
|
|
416
|
+
currentVideoData:{
|
|
417
|
+
height : videoHeight,
|
|
418
|
+
width : videoWidth,
|
|
419
|
+
bitrate : this.analyticsBitrate_,
|
|
420
|
+
audioCodec : this.audioCodec_,
|
|
421
|
+
videoCodec : this.videoCodec_,
|
|
422
|
+
},
|
|
423
|
+
currentTime
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
this.listenToMediaElementEvent('error', () => {
|
|
428
|
+
const {currentTime, error} = mediaEl;
|
|
429
|
+
|
|
430
|
+
this.eventCallback(Events.ERROR, {
|
|
431
|
+
currentTime,
|
|
432
|
+
// See https://developer.mozilla.org/en-US/docs/Web/API/MediaError
|
|
433
|
+
code : error.code,
|
|
434
|
+
message : error.message
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
this.listenToMediaElementEvent('volumechange', () => {
|
|
439
|
+
const {muted, currentTime} = mediaEl;
|
|
440
|
+
|
|
441
|
+
if (muted) {
|
|
442
|
+
this.eventCallback(Events.MUTE, {
|
|
443
|
+
currentTime
|
|
444
|
+
});
|
|
445
|
+
} else {
|
|
446
|
+
this.eventCallback(Events.UN_MUTE, {
|
|
447
|
+
currentTime
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
this.listenToMediaElementEvent('seeking', () => {
|
|
453
|
+
const {currentTime, videoWidth, videoHeight} = mediaEl;
|
|
454
|
+
|
|
455
|
+
this.eventCallback(Events.SEEK, {
|
|
456
|
+
currentVideoData:{
|
|
457
|
+
height : videoHeight,
|
|
458
|
+
width : videoWidth,
|
|
459
|
+
bitrate : this.analyticsBitrate_,
|
|
460
|
+
audioCodec : this.audioCodec_,
|
|
461
|
+
videoCodec : this.videoCodec_,
|
|
462
|
+
},
|
|
463
|
+
currentTime,
|
|
464
|
+
droppedFrames: 0
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
this.listenToMediaElementEvent('seeked', () => {
|
|
469
|
+
const {currentTime, videoWidth, videoHeight} = mediaEl;
|
|
470
|
+
|
|
471
|
+
if (this.bufferingTimeout_) {
|
|
472
|
+
clearTimeout(this.bufferingTimeout_);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
this.eventCallback(Events.SEEKED, {
|
|
476
|
+
currentVideoData:{
|
|
477
|
+
height : videoHeight,
|
|
478
|
+
width : videoWidth,
|
|
479
|
+
bitrate : this.analyticsBitrate_,
|
|
480
|
+
audioCodec : this.audioCodec_,
|
|
481
|
+
videoCodec : this.videoCodec_,
|
|
482
|
+
},
|
|
483
|
+
currentTime,
|
|
484
|
+
droppedFrames: 0
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
this.listenToMediaElementEvent('ended', () => {
|
|
489
|
+
const {currentTime, videoWidth, videoHeight} = mediaEl;
|
|
490
|
+
|
|
491
|
+
this.eventCallback(Events.END, {
|
|
492
|
+
currentVideoData:{
|
|
493
|
+
height : videoHeight,
|
|
494
|
+
width : videoWidth,
|
|
495
|
+
bitrate : this.analyticsBitrate_,
|
|
496
|
+
audioCodec : this.audioCodec_,
|
|
497
|
+
videoCodec : this.videoCodec_,
|
|
498
|
+
},
|
|
499
|
+
currentTime
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
this.listenToMediaElementEvent('timeupdate', () => {
|
|
504
|
+
|
|
505
|
+
const {currentTime, videoHeight, videoWidth} = mediaEl;
|
|
506
|
+
|
|
507
|
+
this.isBuffering_ = false;
|
|
508
|
+
|
|
509
|
+
// silence events if we have not yet intended play
|
|
510
|
+
if (this.needsFirstPlayIntent_) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (!this.isPaused_) {
|
|
515
|
+
this.eventCallback(Events.TIMECHANGED, {
|
|
516
|
+
currentVideoData:{
|
|
517
|
+
height : videoHeight,
|
|
518
|
+
width : videoWidth,
|
|
519
|
+
bitrate : this.analyticsBitrate_,
|
|
520
|
+
audioCodec : this.audioCodec_,
|
|
521
|
+
videoCodec : this.videoCodec_,
|
|
522
|
+
},
|
|
523
|
+
currentTime
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
this.checkQualityLevelAttributes();
|
|
528
|
+
|
|
529
|
+
// We are doing this in case we can not rely
|
|
530
|
+
// on the "stalled" or "waiting" events in a specific browser
|
|
531
|
+
// and to detect intrinsinc paused states (when we do not get a paused event)
|
|
532
|
+
// but the player is paused already before attach or is paused from initialization on.
|
|
533
|
+
this.checkPlayheadProgress();
|
|
534
|
+
|
|
535
|
+
this.previousMediaTime_ = currentTime;
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// The stalled event is fired when the user agent is trying to fetch media data,
|
|
539
|
+
// but data is unexpectedly not forthcoming.
|
|
540
|
+
// https://developer.mozilla.org/en-US/docs/Web/Events/stalled
|
|
541
|
+
this.listenToMediaElementEvent('stalled', () => {
|
|
542
|
+
|
|
543
|
+
// this event doesn't indicate buffering by definition (interupted playback),
|
|
544
|
+
// only that data throughput to playout buffers is not as high as expected
|
|
545
|
+
// It happens on Chrome every once in a while as SourceBuffer's are not fed
|
|
546
|
+
// as fast as the underlying native player may prefer (but it does not lead to
|
|
547
|
+
// interuption).
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// The waiting event is fired when playback has stopped because of a temporary lack of data.
|
|
551
|
+
// See https://developer.mozilla.org/en-US/docs/Web/Events/waiting
|
|
552
|
+
this.listenToMediaElementEvent('waiting', () => {
|
|
553
|
+
|
|
554
|
+
this.onBuffering();
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Should only be calld when a mediaEl is attached
|
|
561
|
+
*/
|
|
562
|
+
listenToMediaElementEvent(event, handler) {
|
|
563
|
+
if (!this.mediaEl) {
|
|
564
|
+
throw new Error('No media attached');
|
|
565
|
+
}
|
|
566
|
+
const boundHandler = handler.bind(this);
|
|
567
|
+
|
|
568
|
+
this.mediaElEventHandlers.push(boundHandler);
|
|
569
|
+
this.mediaEl.addEventListener(event, boundHandler, false);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
onMaybeReady() {
|
|
573
|
+
|
|
574
|
+
if (!this.needsReadyEvent_ || !this.mediaEl) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
this.needsReadyEvent_ = false;
|
|
579
|
+
|
|
580
|
+
let playerPreload = false;
|
|
581
|
+
const {
|
|
582
|
+
duration,
|
|
583
|
+
autoplay,
|
|
584
|
+
videoWidth,
|
|
585
|
+
videoHeight,
|
|
586
|
+
muted,
|
|
587
|
+
preload
|
|
588
|
+
} = this.mediaEl;
|
|
589
|
+
|
|
590
|
+
let streamURL = "";
|
|
591
|
+
|
|
592
|
+
if (this.mediaEl && this.mediaEl.src) {
|
|
593
|
+
streamURL = this.mediaEl.src
|
|
594
|
+
}else if(this.mediaEl && this.mediaEl.querySelector('source') && this.mediaEl.querySelector('source').src){
|
|
595
|
+
streamURL = this.mediaEl.querySelector('source').src;
|
|
596
|
+
}else{
|
|
597
|
+
streamURL = null;
|
|
598
|
+
}
|
|
599
|
+
const width = this.mediaEl.clientWidth;
|
|
600
|
+
const height = this.mediaEl.clientHeight;
|
|
601
|
+
|
|
602
|
+
if (preload !== 'none') {
|
|
603
|
+
playerPreload = true;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const info = {
|
|
607
|
+
type : 'html5',
|
|
608
|
+
isLive : this.isLive(),
|
|
609
|
+
version : this.getPlayerVersion() || 'html5',
|
|
610
|
+
streamType : this.getStreamType(),
|
|
611
|
+
streamUrl : this.getStreamURL(),
|
|
612
|
+
duration : duration,
|
|
613
|
+
autoplay : autoplay,
|
|
614
|
+
preload : playerPreload,
|
|
615
|
+
playerSoftware : this.playerSoftwareName,
|
|
616
|
+
currentVideoData:{
|
|
617
|
+
height : videoHeight,
|
|
618
|
+
width : videoWidth,
|
|
619
|
+
bitrate : this.analyticsBitrate_,
|
|
620
|
+
audioCodec : this.audioCodec_,
|
|
621
|
+
videoCodec : this.videoCodec_,
|
|
622
|
+
},
|
|
623
|
+
// HTMLVideoElement.width and HTMLVideoElement.height
|
|
624
|
+
// is a DOMString that reflects the height HTML attribute,
|
|
625
|
+
// which specifies the height of the display area, in CSS pixels.
|
|
626
|
+
// See https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement
|
|
627
|
+
// width : parseInt(width),
|
|
628
|
+
// height : parseInt(height),
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
// Returns an unsigned long containing the intrinsic
|
|
632
|
+
// height of the resource in CSS pixels,
|
|
633
|
+
// taking into account the dimensions, aspect ratio,
|
|
634
|
+
// clean aperture, resolution, and so forth,
|
|
635
|
+
// as defined for the format used by the resource.
|
|
636
|
+
// If the element's ready state is HAVE_NOTHING, the value is 0.
|
|
637
|
+
// See https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement
|
|
638
|
+
videoWindowWidth : parseInt(width),
|
|
639
|
+
videoWindowHeight: parseInt(height),
|
|
640
|
+
muted
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
this.stateMachine.updateMetadata(info);
|
|
644
|
+
this.eventCallback(Events.READY, info);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Should only be calld when a mediaEl is attached
|
|
649
|
+
*/
|
|
650
|
+
unregisterMediaElement() {
|
|
651
|
+
if (!this.mediaEl) {
|
|
652
|
+
throw new Error('No media attached');
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
this.mediaElEventHandlers.forEach((handler) => {
|
|
656
|
+
this.mediaEl.removeEventListener(handler);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
this.resetMedia();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
onBuffering() {
|
|
663
|
+
const {currentTime} = this.mediaEl;
|
|
664
|
+
|
|
665
|
+
// this handler may be called multiple times
|
|
666
|
+
// for one actual buffering-event occuring so lets guard from
|
|
667
|
+
// triggering this event redundantly.
|
|
668
|
+
if (this.isBuffering_ || this.isPaused_) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
this.eventCallback(Events.START_BUFFERING, {
|
|
673
|
+
currentTime
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
this.isBuffering_ = true;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
onPaused() {
|
|
680
|
+
if (this.isPaused_) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const {currentTime, videoHeight, videoWidth} = this.mediaEl;
|
|
685
|
+
|
|
686
|
+
this.eventCallback(Events.PAUSE, {
|
|
687
|
+
currentVideoData:{
|
|
688
|
+
height : videoHeight,
|
|
689
|
+
width : videoWidth,
|
|
690
|
+
bitrate : this.analyticsBitrate_,
|
|
691
|
+
audioCodec : this.audioCodec_,
|
|
692
|
+
videoCodec : this.videoCodec_,
|
|
693
|
+
},
|
|
694
|
+
currentTime
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
this.isPaused_ = true;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
checkPlayheadProgress() {
|
|
701
|
+
const mediaEl = this.mediaEl;
|
|
702
|
+
|
|
703
|
+
if (mediaEl.paused) {
|
|
704
|
+
this.onPaused();
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (this.bufferingTimeout_) {
|
|
708
|
+
clearTimeout(this.bufferingTimeout_);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
this.bufferingTimeout_ = setTimeout(() => {
|
|
712
|
+
|
|
713
|
+
if (mediaEl.paused || mediaEl.ended && !this.isBuffering_) {
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const timeDelta = mediaEl.currentTime - this.previousMediaTime_;
|
|
718
|
+
|
|
719
|
+
if (timeDelta < BUFFERING_TIMECHANGED_TIMEOUT) {
|
|
720
|
+
this.onBuffering();
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
}, BUFFERING_TIMECHANGED_TIMEOUT);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* @param {boolean} silent
|
|
728
|
+
*/
|
|
729
|
+
checkQualityLevelAttributes(silent = false) {
|
|
730
|
+
|
|
731
|
+
const mediaEl = this.mediaEl;
|
|
732
|
+
|
|
733
|
+
const qualityLevelInfo = this.getCurrentQualityLevelInfo();
|
|
734
|
+
if (!qualityLevelInfo) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const {bitrate, width, height, videoCodec, audioCodec} = qualityLevelInfo;
|
|
739
|
+
|
|
740
|
+
const isLive = this.isLive();
|
|
741
|
+
|
|
742
|
+
if (isLive !== this.isLive_) {
|
|
743
|
+
this.isLive_ = isLive;
|
|
744
|
+
|
|
745
|
+
if (!silent) {
|
|
746
|
+
this.stateMachine.updateMetadata({
|
|
747
|
+
isLive
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
this.audioCodec_ = audioCodec;
|
|
754
|
+
this.videoCodec_ = videoCodec;
|
|
755
|
+
|
|
756
|
+
if (this.analyticsBitrate_ !== bitrate) {
|
|
757
|
+
const eventData = {
|
|
758
|
+
width,
|
|
759
|
+
height,
|
|
760
|
+
bitrate,
|
|
761
|
+
videoCodec,
|
|
762
|
+
audioCodec,
|
|
763
|
+
currentTime: mediaEl.currentTime
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
if (!silent) {
|
|
767
|
+
this.eventCallback(Events.VIDEO_CHANGE, eventData);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
this.analyticsBitrate_ = bitrate;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
}
|
|
774
|
+
}
|