@oat-sa/tao-core-ui 1.6.2 → 1.6.3

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.
@@ -1,4 +1,4 @@
1
- define(['jquery', 'util/urlParser', 'core/eventifier', 'ui/mediaplayer/support', 'handlebars', 'i18n', 'lodash', 'lib/dompurify/purify'], function ($$1, UrlParser, eventifier, support, Handlebars, __, _, DOMPurify) { 'use strict';
1
+ define(['jquery', 'util/urlParser', 'core/eventifier', 'ui/mediaplayer/support', 'handlebars', 'i18n', 'lodash', 'lib/dompurify/purify', 'ui/mediaplayer/utils/reminder', 'ui/mediaplayer/utils/timeObserver'], function ($$1, UrlParser, eventifier, support, Handlebars, __, _, DOMPurify, reminderManagerFactory, timeObserverFactory) { 'use strict';
2
2
 
3
3
  $$1 = $$1 && $$1.hasOwnProperty('default') ? $$1['default'] : $$1;
4
4
  UrlParser = UrlParser && UrlParser.hasOwnProperty('default') ? UrlParser['default'] : UrlParser;
@@ -8,6 +8,8 @@ define(['jquery', 'util/urlParser', 'core/eventifier', 'ui/mediaplayer/support',
8
8
  __ = __ && __.hasOwnProperty('default') ? __['default'] : __;
9
9
  _ = _ && _.hasOwnProperty('default') ? _['default'] : _;
10
10
  DOMPurify = DOMPurify && DOMPurify.hasOwnProperty('default') ? DOMPurify['default'] : DOMPurify;
11
+ reminderManagerFactory = reminderManagerFactory && reminderManagerFactory.hasOwnProperty('default') ? reminderManagerFactory['default'] : reminderManagerFactory;
12
+ timeObserverFactory = timeObserverFactory && timeObserverFactory.hasOwnProperty('default') ? timeObserverFactory['default'] : timeObserverFactory;
11
13
 
12
14
  function _typeof(obj) {
13
15
  "@babel/helpers - typeof";
@@ -261,47 +263,56 @@ define(['jquery', 'util/urlParser', 'core/eventifier', 'ui/mediaplayer/support',
261
263
  */
262
264
  /**
263
265
  * CSS namespace
264
- * @type {String}
266
+ * @type {string}
265
267
  */
266
268
 
267
269
  var ns = '.mediaplayer';
268
270
  /**
269
271
  * Range value of the volume
270
- * @type {Number}
272
+ * @type {number}
271
273
  */
272
274
 
273
275
  var volumeRange = 100;
276
+ /**
277
+ * Delay before considering a media stalled
278
+ * @type {number}
279
+ */
280
+
281
+ var stalledDetectionDelay = 2000;
274
282
  /**
275
283
  * List of media events that can be listened to for debugging
276
- * @type {String[]}
284
+ * @type {string[]}
277
285
  */
278
286
 
279
287
  var mediaEvents = ['abort', 'canplay', 'canplaythrough', 'canshowcurrentframe', 'dataunavailable', 'durationchange', 'emptied', 'empty', 'ended', 'error', 'loadeddata', 'loadedfirstframe', 'loadedmetadata', 'loadstart', 'pause', 'play', 'playing', 'progress', 'ratechange', 'seeked', 'seeking', 'stalled', 'suspend', 'timeupdate', 'volumechange', 'waiting'];
280
288
  /**
281
289
  * List of player events that can be listened to for debugging
282
- * @type {String[]}
290
+ * @type {string[]}
283
291
  */
284
292
 
285
- var playerEvents = ['end', 'error', 'pause', 'play', 'playing', 'ready', 'recovererror', 'resize', 'stalled', 'timeupdate'];
293
+ var playerEvents = ['end', 'error', 'pause', 'play', 'playing', 'ready', 'resize', 'stalled', 'timeupdate'];
286
294
  /**
287
295
  * Defines a player object dedicated to the native HTML5 player
288
296
  * @param {jQuery} $container - Where to render the player
289
- * @param {Object} config - The list of config entries
297
+ * @param {object} config - The list of config entries
290
298
  * @param {Array} config.sources - The list of media sources
291
- * @param {String} [config.type] - The type of player (video or audio) (default: video)
292
- * @param {Boolean} [config.preview] - Enables the media preview (load media metadata)
293
- * @param {Boolean} [config.debug] - Enables the debug mode
294
- * @returns {Object} player
299
+ * @param {string} [config.type] - The type of player (video or audio) (default: video)
300
+ * @param {boolean} [config.preview] - Enables the media preview (load media metadata)
301
+ * @param {boolean} [config.debug] - Enables the debug mode
302
+ * @param {number} [config.config.stalledDetectionDelay] - The delay before considering a media is stalled
303
+ * @returns {object} player
295
304
  */
296
305
 
297
306
  function html5PlayerFactory($container) {
298
307
  var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
299
308
  var type = config.type || 'video';
300
309
  var sources = config.sources || [];
310
+ var updateObserver = reminderManagerFactory();
311
+ var timeObserver = timeObserverFactory();
312
+ config.stalledDetectionDelay = config.stalledDetectionDelay || stalledDetectionDelay;
301
313
  var $media;
302
314
  var media;
303
- var playback = false;
304
- var stalled = false;
315
+ var state = {};
305
316
 
306
317
  var getDebugContext = function getDebugContext(action) {
307
318
  var networkState = media && media.networkState;
@@ -317,10 +328,10 @@ define(['jquery', 'util/urlParser', 'core/eventifier', 'ui/mediaplayer/support',
317
328
  args[_key - 1] = arguments[_key];
318
329
  }
319
330
 
320
- return config.debug && (_window$console = window.console).log.apply(_window$console, [getDebugContext(action)].concat(args));
331
+ return (config.debug === true || config.debug === action) && (_window$console = window.console).log.apply(_window$console, [getDebugContext(action)].concat(args));
321
332
  };
322
333
 
323
- var player = {
334
+ return eventifier({
324
335
  init: function init() {
325
336
  var _this = this;
326
337
 
@@ -331,6 +342,7 @@ define(['jquery', 'util/urlParser', 'core/eventifier', 'ui/mediaplayer/support',
331
342
  var poster = '';
332
343
  var link = '';
333
344
  var result = false;
345
+ state = {};
334
346
  sources.forEach(function (source) {
335
347
  if (!page.sameDomain(source.src)) {
336
348
  cors = true;
@@ -351,54 +363,105 @@ define(['jquery', 'util/urlParser', 'core/eventifier', 'ui/mediaplayer/support',
351
363
  link: link
352
364
  }));
353
365
  $container.append($media);
354
- playback = false;
355
- stalled = false;
356
366
  media = $media.get(0);
357
- result = !!(media && support.checkSupport(media)); // remove the browser native controls if we can use the API instead
367
+ result = !!(media && support.checkSupport(media)); // Remove the browser native controls if we can use the API instead
358
368
 
359
369
  if (support.canControl()) {
360
370
  $media.removeAttr('controls');
361
- }
371
+ } // Detect stalled video when the timer suddenly jump to the end
372
+
362
373
 
374
+ timeObserver.removeAllListeners().on('irregularity', function (position) {
375
+ if (state.playback && state.stallDetection) {
376
+ _this.stalled(position);
377
+ }
378
+ });
363
379
  $media.on("play".concat(ns), function () {
364
- playback = true;
380
+ state.playback = true;
381
+ state.playedViaApi = false;
382
+ timeObserver.init(media.currentTime, media.duration);
365
383
 
366
384
  _this.trigger('play');
367
385
  }).on("pause".concat(ns), function () {
386
+ if (state.stallDetection && !state.pausedViaApi && updateObserver.running && updateObserver.elapsed < 100) {
387
+ // The pause event may be triggered after a connectivity issue as the player is out of data
388
+ _this.stalled();
389
+ }
390
+
391
+ state.pausedViaApi = false;
392
+ state.playing = false;
393
+ updateObserver.stop();
394
+
368
395
  _this.trigger('pause');
396
+ }).on("seeked".concat(ns), function () {
397
+ // When the user try changing the current playing position while the network is down,
398
+ // the player will end the playback by moving straight to the end.
399
+ if (state.seekedViaApi && Math.floor(state.seekAt) !== Math.floor(media.currentTime)) {
400
+ state.stallDetection = true;
401
+ }
402
+
403
+ state.seekedViaApi = false;
369
404
  }).on("ended".concat(ns), function () {
370
- playback = false;
405
+ updateObserver.forget().stop();
406
+ timeObserver.update(media.currentTime);
407
+ state.playback = false;
408
+ state.playing = false;
371
409
 
372
410
  _this.trigger('end');
373
411
  }).on("timeupdate".concat(ns), function () {
412
+ state.playing = true;
413
+ updateObserver.start();
414
+ timeObserver.update(media.currentTime);
415
+
374
416
  _this.trigger('timeupdate');
375
417
  }).on('loadstart', function () {
376
- if (stalled) {
377
- return;
378
- }
379
-
380
418
  if (media.networkState === HTMLMediaElement.NETWORK_NO_SOURCE) {
381
419
  _this.trigger('error');
382
420
  }
383
421
 
384
422
  if (!config.preview && media.networkState === HTMLMediaElement.NETWORK_IDLE) {
385
423
  _this.trigger('ready');
386
- }
424
+ } // The media may be unreachable straight from the beginning
425
+
426
+
427
+ _this.detectStalledNetwork();
428
+ }).on("waiting".concat(ns), function () {
429
+ // The "waiting" event means the player is pending data,
430
+ // it may be the symptom of a connectivity issue
431
+ _this.detectStalledNetwork();
387
432
  }).on("error".concat(ns), function () {
388
- if (media.networkState === HTMLMediaElement.NETWORK_NO_SOURCE) {
433
+ if (media.networkState === HTMLMediaElement.NETWORK_NO_SOURCE || media.error instanceof MediaError && media.error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED) {
434
+ // No source means the player does not support the supplied media.
435
+ // Or it can be more explicit with the not supported error.
436
+ // There is nothing that we can do from this stage.
389
437
  _this.trigger('error');
390
438
  } else {
391
- _this.trigger('recovererror', media.networkState === HTMLMediaElement.NETWORK_LOADING);
439
+ // Other errors need special attention as they can be recoverable
440
+ _this.handleError(media.error);
392
441
  }
393
- }).on("canplay".concat(ns), function () {
442
+ }).on('loadedmetadata', function () {
443
+ timeObserver.init(media.currentTime, media.duration);
394
444
 
395
- _this.trigger('ready');
445
+ _this.ready();
446
+ }).on("canplay".concat(ns), function () {
447
+ if (!state.stalled) {
448
+ _this.ready();
449
+ }
396
450
  }).on("stalled".concat(ns), function () {
397
- stalled = true;
398
-
399
- _this.trigger('stalled');
451
+ // The "stalled" event may be triggered once the player is halted after initialisation,
452
+ // but this does not mean the playback is actually stalled, hence we only take care of the playing state
453
+ if (state.playing && !media.paused) {
454
+ _this.handleError(media.error);
455
+ }
400
456
  }).on("playing".concat(ns), function () {
401
- stalled = false;
457
+ if (state.stallDetection) {
458
+ // The "playing" event may occur after a connectivity issue.
459
+ // For the sake of the stall detection, we need to discard this event
460
+ return;
461
+ }
462
+
463
+ updateObserver.forget().start();
464
+ state.playing = true;
402
465
 
403
466
  _this.trigger('playing');
404
467
  }); // install debug logger
@@ -421,18 +484,120 @@ define(['jquery', 'util/urlParser', 'core/eventifier', 'ui/mediaplayer/support',
421
484
  });
422
485
  }
423
486
 
424
- sources.forEach(function (source) {
425
- var src = source.src,
426
- type = source.type;
427
-
428
- _this.addMedia(src, type);
429
- });
487
+ result = result && sources.reduce(function (supported, source) {
488
+ return _this.addMedia(source.src, source.type) || supported;
489
+ }, false);
430
490
  return result;
431
491
  },
492
+ handleError: function handleError(error) {
493
+ var _this2 = this;
494
+
495
+ // Discard legitimate and non-blocking errors
496
+ switch (error && error.name) {
497
+ case 'NotAllowedError':
498
+ debug('api call', 'handleError', 'the autoplay is not allowed without a user interaction', error);
499
+ return;
500
+
501
+ case 'AbortError':
502
+ debug('api call', 'handleError', 'the action has been aborted for some reason', error);
503
+ return;
504
+ }
505
+
506
+ debug('api call', 'handleError', error); // Detect if the playback can continue a bit
507
+
508
+ var canContinueTemporarily = media && (media.readyState === HTMLMediaElement.HAVE_ENOUGH_DATA || media.readyState === HTMLMediaElement.HAVE_FUTURE_DATA || media.readyState === HTMLMediaElement.HAVE_CURRENT_DATA); // If a connectivity error occurs we may need to enter in stalled mode unless we can wait a bit
509
+
510
+ if (error instanceof MediaError && (error.code === MediaError.MEDIA_ERR_NETWORK || error.code === MediaError.MEDIA_ERR_DECODE) && !canContinueTemporarily) {
511
+ this.stalled();
512
+ return;
513
+ } // To this point, there is a big chance the media is stalled.
514
+ // We start an observer to remind as soon as an irregularity occurs on the time update
515
+
516
+
517
+ state.stallDetection = true;
518
+ updateObserver.remind(function () {
519
+ // The last time update is a bit old, the media is most probably stalled now
520
+ if (updateObserver.elapsed >= config.stalledDetectionDelay) {
521
+ _this2.stalled();
522
+ }
523
+ }, config.stalledDetectionDelay);
524
+ updateObserver.start();
525
+ },
526
+ ready: function ready() {
527
+ if (!state.ready) {
528
+ state.ready = true;
529
+ this.trigger('ready');
530
+ }
531
+ },
532
+ detectStalledNetwork: function detectStalledNetwork() {
533
+ var _this3 = this;
534
+
535
+ // Install an observer that will watch the network state after a small delay.
536
+ // It is needed since the network state may need time to settle.
537
+ setTimeout(function () {
538
+ if (media && media.networkState === HTMLMediaElement.NETWORK_NO_SOURCE && media.readyState === HTMLMediaElement.HAVE_NOTHING) {
539
+ if (!state.ready) {
540
+ _this3.trigger('ready');
541
+ }
542
+
543
+ _this3.stalled();
544
+ }
545
+ }, config.stalledDetectionDelay);
546
+ },
547
+ stalled: function stalled(position) {
548
+ debug('api call', 'stalled');
549
+
550
+ if (media) {
551
+ if ('undefined' !== typeof position) {
552
+ state.stalledAt = position;
553
+ } else {
554
+ state.stalledAt = timeObserver.position;
555
+ }
556
+ }
557
+
558
+ state.stalled = true;
559
+ state.stallDetection = false;
560
+ updateObserver.forget().stop();
561
+ this.pause();
562
+ this.trigger('stalled');
563
+ },
564
+ recover: function recover() {
565
+ debug('api call', 'recover');
566
+ state.stalled = false;
567
+ state.stallDetection = false;
568
+
569
+ if (media) {
570
+ // Special processing of video player to prevent visual glitch while reloading
571
+ if (media.tagName === 'VIDEO') {
572
+ // Temporarily set the size of the media to prevent a shrink while reloading it
573
+ $media.width($media.width());
574
+ $media.height($media.height());
575
+ $media.on('loadedmetadata.recover', function () {
576
+ $media.off('loadedmetadata.recover');
577
+ $media.css({
578
+ width: '',
579
+ height: ''
580
+ });
581
+ });
582
+ }
583
+
584
+ media.load();
585
+
586
+ if (state.stalledAt) {
587
+ this.seek(state.stalledAt);
588
+ }
589
+
590
+ if (state.playback && !state.playing || state.playedViaApi) {
591
+ this.play();
592
+ }
593
+ }
594
+ },
432
595
  destroy: function destroy() {
433
596
  debug('api call', 'destroy');
434
597
  this.stop();
435
598
  this.removeAllListeners();
599
+ updateObserver.forget();
600
+ timeObserver.removeAllListeners();
436
601
 
437
602
  if ($media) {
438
603
  $media.off(ns).remove();
@@ -440,8 +605,7 @@ define(['jquery', 'util/urlParser', 'core/eventifier', 'ui/mediaplayer/support',
440
605
 
441
606
  $media = void 0;
442
607
  media = void 0;
443
- playback = false;
444
- stalled = false;
608
+ state = {};
445
609
  },
446
610
  getMedia: function getMedia() {
447
611
  debug('api call', 'getMedia', media);
@@ -506,40 +670,54 @@ define(['jquery', 'util/urlParser', 'core/eventifier', 'ui/mediaplayer/support',
506
670
 
507
671
  if (media) {
508
672
  media.currentTime = parseFloat(time);
673
+ state.seekedViaApi = true;
674
+ state.seekAt = media.currentTime;
675
+ timeObserver.seek(media.currentTime);
509
676
 
510
- if (!playback) {
677
+ if (!state.playback) {
511
678
  this.play();
512
679
  }
513
680
  }
514
681
  },
515
682
  play: function play() {
683
+ var _this4 = this;
684
+
516
685
  debug('api call', 'play');
517
686
 
518
687
  if (media) {
519
- media.play().catch(function (err) {
520
- return debug('playback error', err);
521
- });
688
+ state.playedViaApi = true;
689
+ var startPlayPromise = media.play();
690
+
691
+ if ('undefined' !== typeof startPlayPromise) {
692
+ startPlayPromise.catch(function (error) {
693
+ return _this4.handleError(error);
694
+ });
695
+ }
522
696
  }
523
697
  },
524
698
  pause: function pause() {
525
699
  debug('api call', 'pause');
526
700
 
527
701
  if (media) {
702
+ if (!media.paused) {
703
+ state.pausedViaApi = true;
704
+ }
705
+
528
706
  media.pause();
529
707
  }
530
708
  },
531
709
  stop: function stop() {
532
710
  debug('api call', 'stop');
533
711
 
534
- if (media && playback) {
712
+ if (media && media.duration && state.playback && !state.stalled) {
535
713
  media.currentTime = media.duration;
536
714
  }
537
715
  },
538
- mute: function mute(state) {
539
- debug('api call', 'mute', state);
716
+ mute: function mute(muted) {
717
+ debug('api call', 'mute', muted);
540
718
 
541
719
  if (media) {
542
- media.muted = !!state;
720
+ media.muted = !!muted;
543
721
  }
544
722
  },
545
723
  isMuted: function isMuted() {
@@ -552,11 +730,11 @@ define(['jquery', 'util/urlParser', 'core/eventifier', 'ui/mediaplayer/support',
552
730
  debug('api call', 'isMuted', mute);
553
731
  return mute;
554
732
  },
555
- addMedia: function addMedia(src, type) {
556
- debug('api call', 'addMedia', src, type);
733
+ addMedia: function addMedia(src, srcType) {
734
+ debug('api call', 'addMedia', src, srcType);
557
735
 
558
736
  if (media) {
559
- if (!support.checkSupport(media, type)) {
737
+ if (!support.checkSupport(media, srcType)) {
560
738
  return false;
561
739
  }
562
740
  }
@@ -564,25 +742,24 @@ define(['jquery', 'util/urlParser', 'core/eventifier', 'ui/mediaplayer/support',
564
742
  if (src && $media) {
565
743
  $media.append(sourceTpl({
566
744
  src: src,
567
- type: type
745
+ type: srcType
568
746
  }));
569
747
  return true;
570
748
  }
571
749
 
572
750
  return false;
573
751
  },
574
- setMedia: function setMedia(src, type) {
575
- debug('api call', 'setMedia', src, type);
752
+ setMedia: function setMedia(src, srcType) {
753
+ debug('api call', 'setMedia', src, srcType);
576
754
 
577
755
  if ($media) {
578
756
  $media.empty();
579
- return this.addMedia(src, type);
757
+ return this.addMedia(src, srcType);
580
758
  }
581
759
 
582
760
  return false;
583
761
  }
584
- };
585
- return eventifier(player);
762
+ });
586
763
  }
587
764
 
588
765
  return html5PlayerFactory;
@@ -21,11 +21,13 @@ define(function () { 'use strict';
21
21
  /**
22
22
  * A Regex to detect Apple mobile browsers
23
23
  * @type {RegExp}
24
+ * @private
24
25
  */
25
26
  var reAppleMobiles = /ip(hone|od)/i;
26
27
  /**
27
28
  * A list of MIME types with codec declaration
28
29
  * @type {Object}
30
+ * @private
29
31
  */
30
32
 
31
33
  var supportedMimeTypes = {
@@ -39,11 +41,23 @@ define(function () { 'use strict';
39
41
  'audio/ogg': 'audio/ogg; codecs="vorbis"',
40
42
  'audio/wav': 'audio/wav; codecs="1"'
41
43
  };
44
+ /**
45
+ * Checks support for a MIME type.
46
+ * @param {HTMLMediaElement} media The media element on which check support
47
+ * @param {String} mimeType A MIME type to check the support for
48
+ * @returns {string}
49
+ * @private
50
+ */
51
+
52
+ var findSupport = function findSupport(media, mimeType) {
53
+ return media.canPlayType(mimeType).replace(/no/, '');
54
+ };
42
55
  /**
43
56
  * Support detection
44
57
  * @type {Object}
45
58
  */
46
59
 
60
+
47
61
  var support = {
48
62
  /**
49
63
  * Checks if the browser can play media
@@ -53,13 +67,13 @@ define(function () { 'use strict';
53
67
  * @private
54
68
  */
55
69
  checkSupport: function checkSupport(media, mimeType) {
56
- var support = !!media.canPlayType;
70
+ var support = media.canPlayType;
57
71
 
58
72
  if (support && mimeType) {
59
- return !!media.canPlayType(supportedMimeTypes[mimeType] || mimeType).replace(/no/, '');
73
+ return !!(supportedMimeTypes[mimeType] && findSupport(media, supportedMimeTypes[mimeType]) || findSupport(media, mimeType));
60
74
  }
61
75
 
62
- return support;
76
+ return !!support;
63
77
  },
64
78
 
65
79
  /**