shaka-player 4.0.2 → 4.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +25 -18
  2. package/CONTRIBUTORS +1 -0
  3. package/demo/common/message_ids.js +1 -0
  4. package/demo/config.js +3 -1
  5. package/demo/locales/en.json +1 -0
  6. package/demo/locales/source.json +4 -0
  7. package/dist/demo.compiled.debug.js +4 -4
  8. package/dist/demo.compiled.debug.map +2 -2
  9. package/dist/demo.compiled.js +4 -4
  10. package/dist/demo.compiled.map +2 -2
  11. package/dist/deps.js +2 -2
  12. package/dist/locales.js +31 -31
  13. package/dist/receiver.compiled.debug.js +2 -2
  14. package/dist/receiver.compiled.debug.map +2 -2
  15. package/dist/receiver.compiled.js +2 -2
  16. package/dist/receiver.compiled.map +2 -2
  17. package/dist/shaka-player.compiled.d.ts +14 -8
  18. package/dist/shaka-player.compiled.debug.d.ts +14 -8
  19. package/dist/shaka-player.compiled.debug.externs.js +10 -2
  20. package/dist/shaka-player.compiled.debug.js +165 -157
  21. package/dist/shaka-player.compiled.debug.map +3 -3
  22. package/dist/shaka-player.compiled.externs.js +10 -2
  23. package/dist/shaka-player.compiled.js +758 -754
  24. package/dist/shaka-player.compiled.map +3 -3
  25. package/dist/shaka-player.ui.d.ts +15 -9
  26. package/dist/shaka-player.ui.debug.d.ts +15 -9
  27. package/dist/shaka-player.ui.debug.externs.js +10 -2
  28. package/dist/shaka-player.ui.debug.js +198 -186
  29. package/dist/shaka-player.ui.debug.map +3 -3
  30. package/dist/shaka-player.ui.externs.js +10 -2
  31. package/dist/shaka-player.ui.js +931 -928
  32. package/dist/shaka-player.ui.map +3 -3
  33. package/docs/design/chromecast.md +3 -1
  34. package/docs/tutorials/drm-config.md +16 -0
  35. package/docs/tutorials/ui.md +26 -0
  36. package/externs/chromecast.js +9 -2
  37. package/externs/shaka/manifest.js +7 -0
  38. package/externs/shaka/player.js +21 -2
  39. package/externs/shaka/text.js +7 -0
  40. package/lib/cast/cast_proxy.js +19 -8
  41. package/lib/cast/cast_receiver.js +3 -3
  42. package/lib/cast/cast_sender.js +12 -2
  43. package/lib/hls/hls_parser.js +12 -1
  44. package/lib/media/drm_engine.js +17 -8
  45. package/lib/media/gap_jumping_controller.js +23 -1
  46. package/lib/media/playhead.js +47 -4
  47. package/lib/media/stall_detector.js +19 -2
  48. package/lib/media/streaming_engine.js +8 -1
  49. package/lib/offline/manifest_converter.js +1 -0
  50. package/lib/player.js +175 -69
  51. package/lib/text/cue.js +6 -0
  52. package/lib/text/ui_text_displayer.js +1 -0
  53. package/lib/text/vtt_text_parser.js +4 -0
  54. package/lib/util/error.js +6 -0
  55. package/lib/util/fake_event.js +39 -0
  56. package/lib/util/player_configuration.js +2 -0
  57. package/lib/util/stats.js +28 -1
  58. package/lib/util/stream_utils.js +7 -0
  59. package/package.json +1 -1
  60. package/test/cast/cast_proxy_unit.js +3 -1
  61. package/test/cast/cast_sender_unit.js +21 -6
  62. package/test/demo/demo_unit.js +2 -1
  63. package/test/media/adaptation_set_unit.js +1 -0
  64. package/test/media/playhead_unit.js +114 -25
  65. package/test/media/stall_detector_unit.js +20 -2
  66. package/test/media/streaming_engine_integration.js +2 -1
  67. package/test/media/streaming_engine_unit.js +1 -0
  68. package/test/player_integration.js +3 -0
  69. package/test/player_unit.js +142 -1
  70. package/test/test/util/manifest_generator.js +2 -0
  71. package/test/test/util/simple_fakes.js +14 -0
  72. package/test/test/util/streaming_engine_util.js +1 -0
  73. package/test/text/vtt_text_parser_unit.js +4 -0
  74. package/test/ui/ui_unit.js +18 -5
  75. package/test/util/stream_utils_unit.js +108 -0
  76. package/ui/controls.js +12 -8
  77. package/ui/externs/ui.js +10 -1
  78. package/ui/statistics_button.js +10 -0
  79. package/ui/ui.js +12 -1
@@ -6,6 +6,7 @@
6
6
 
7
7
  describe('Player', () => {
8
8
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
9
+ const StreamUtils = shaka.util.StreamUtils;
9
10
  const Util = shaka.test.Util;
10
11
 
11
12
  const originalLogError = shaka.log.error;
@@ -29,6 +30,8 @@ describe('Player', () => {
29
30
  let player;
30
31
  /** @type {!shaka.test.FakeAbrManager} */
31
32
  let abrManager;
33
+ /** @type {function(!shaka.util.Error)} */
34
+ let onErrorCallback;
32
35
 
33
36
  /** @type {!shaka.test.FakeNetworkingEngine} */
34
37
  let networkingEngine;
@@ -40,6 +43,8 @@ describe('Player', () => {
40
43
  let playhead;
41
44
  /** @type {!shaka.test.FakeTextDisplayer} */
42
45
  let textDisplayer;
46
+ /** @type {shaka.extern.BufferedInfo} */
47
+ let bufferedInfo;
43
48
 
44
49
  let mediaSourceEngine;
45
50
 
@@ -101,6 +106,13 @@ describe('Player', () => {
101
106
  abrManager = new shaka.test.FakeAbrManager();
102
107
  textDisplayer = createTextDisplayer();
103
108
 
109
+ bufferedInfo = {
110
+ total: [],
111
+ audio: [],
112
+ video: [{start: 12, end: 26}],
113
+ text: [],
114
+ };
115
+
104
116
  function dependencyInjector(player) {
105
117
  // Create a networking engine that always returns an empty buffer.
106
118
  networkingEngine = new shaka.test.FakeNetworkingEngine();
@@ -119,10 +131,14 @@ describe('Player', () => {
119
131
  setSegmentRelativeVttTiming:
120
132
  jasmine.createSpy('setSegmentRelativeVttTiming'),
121
133
  getTextDisplayer: () => textDisplayer,
134
+ getBufferedInfo: () => bufferedInfo,
122
135
  ended: jasmine.createSpy('ended').and.returnValue(false),
123
136
  };
124
137
 
125
- player.createDrmEngine = () => drmEngine;
138
+ player.createDrmEngine = ({onError}) => {
139
+ onErrorCallback = onError;
140
+ return drmEngine;
141
+ };
126
142
  player.createNetworkingEngine = () => networkingEngine;
127
143
  player.createPlayhead = (startTime) => {
128
144
  const callableSetStartTime =
@@ -321,6 +337,131 @@ describe('Player', () => {
321
337
  });
322
338
  });
323
339
 
340
+ describe('onError and tryToRecoverFromError', () => {
341
+ const httpError = new shaka.util.Error(
342
+ shaka.util.Error.Severity.RECOVERABLE,
343
+ shaka.util.Error.Category.NETWORK,
344
+ shaka.util.Error.Code.HTTP_ERROR);
345
+ const nonHttpError = new shaka.util.Error(
346
+ shaka.util.Error.Severity.RECOVERABLE,
347
+ shaka.util.Error.Category.NETWORK,
348
+ shaka.util.Error.Code.TIMEOUT);
349
+ /** @type {?jasmine.Spy} */
350
+ let dispatchEventSpy;
351
+
352
+ beforeEach(async () => {
353
+ manifest = shaka.test.ManifestGenerator.generate((manifest) => {
354
+ manifest.addVariant(11, (variant) => {
355
+ variant.addAudio(2);
356
+ variant.addVideo(3);
357
+ });
358
+ manifest.addVariant(12, (variant) => {
359
+ variant.addAudio(4);
360
+ variant.addVideo(5);
361
+ });
362
+ });
363
+ await player.load(fakeManifestUri, 0, fakeMimeType);
364
+ dispatchEventSpy = spyOn(player, 'dispatchEvent').and.returnValue(true);
365
+ });
366
+
367
+ afterEach(() => {
368
+ dispatchEventSpy.calls.reset();
369
+ });
370
+
371
+ it('does not handle non NETWORK HTTP_ERROR', () => {
372
+ onErrorCallback(nonHttpError);
373
+ expect(nonHttpError.handled).toBeFalsy();
374
+ expect(player.dispatchEvent).toHaveBeenCalled();
375
+ });
376
+
377
+ describe('when config.streaming.maxDisabledTime is 0', () => {
378
+ it('does not handle NETWORK HTTP_ERROR', () => {
379
+ player.configure({streaming: {maxDisabledTime: 0}});
380
+ httpError.handled = false;
381
+ onErrorCallback(httpError);
382
+ expect(httpError.handled).toBeFalsy();
383
+ expect(player.dispatchEvent).toHaveBeenCalled();
384
+ });
385
+ });
386
+
387
+ describe('when config.streaming.maxDisabledTime is 30', () => {
388
+ /** @type {number} */
389
+ const maxDisabledTime = 30;
390
+ /** @type {number} */
391
+ const currentTime = 123;
392
+ const chosenVariant = {
393
+ id: 1,
394
+ language: 'es',
395
+ disabledUntilTime: 0,
396
+ primary: false,
397
+ bandwidth: 2100,
398
+ allowedByApplication: true,
399
+ allowedByKeySystem: true,
400
+ decodingInfos: [],
401
+ };
402
+ /** @type {?jasmine.Spy} */
403
+ let applyRestrictionsSpy;
404
+ /** @type {?jasmine.Spy} */
405
+ let chooseVariantSpy;
406
+ /** @type {?jasmine.Spy} */
407
+ let getBufferedInfoSpy;
408
+ /** @type {?jasmine.Spy} */
409
+ let switchVariantSpy;
410
+
411
+ describe('and there is new variant', () => {
412
+ const oldDateNow = Date.now;
413
+ beforeEach(() => {
414
+ Date.now = () => currentTime * 1000;
415
+ chooseVariantSpy = spyOn(player, 'chooseVariant_')
416
+ .and.returnValue(chosenVariant);
417
+ getBufferedInfoSpy = spyOn(player, 'getBufferedInfo')
418
+ .and.returnValue(bufferedInfo);
419
+ switchVariantSpy = spyOn(player, 'switchVariant_');
420
+ applyRestrictionsSpy = spyOn(StreamUtils, 'applyRestrictions');
421
+ httpError.handled = false;
422
+ dispatchEventSpy.calls.reset();
423
+ player.configure({streaming: {maxDisabledTime}});
424
+ player.setMaxHardwareResolution(123, 456);
425
+ onErrorCallback(httpError);
426
+ });
427
+
428
+ afterEach(() => {
429
+ Date.now = oldDateNow;
430
+ chooseVariantSpy.calls.reset();
431
+ getBufferedInfoSpy.calls.reset();
432
+ switchVariantSpy.calls.reset();
433
+ applyRestrictionsSpy.calls.reset();
434
+ });
435
+
436
+ it('handles HTTP_ERROR', () => {
437
+ expect(httpError.handled).toBeTruthy();
438
+ });
439
+
440
+ it('does not dispatch any error', () => {
441
+ expect(dispatchEventSpy).not.toHaveBeenCalled();
442
+ });
443
+
444
+ it('disables the current variant and applies restrictions', () => {
445
+ const foundDisabledVariant =
446
+ manifest.variants.some(({disabledUntilTime}) =>
447
+ disabledUntilTime == currentTime + maxDisabledTime);
448
+ expect(foundDisabledVariant).toBeTruthy();
449
+ expect(applyRestrictionsSpy).toHaveBeenCalledWith(
450
+ manifest.variants,
451
+ player.getConfiguration().restrictions,
452
+ {width: 123, height: 456});
453
+ });
454
+
455
+ it('switches the variant', () => {
456
+ expect(chooseVariantSpy).toHaveBeenCalled();
457
+ expect(getBufferedInfoSpy).toHaveBeenCalled();
458
+ expect(switchVariantSpy)
459
+ .toHaveBeenCalledWith(chosenVariant, false, true, 14);
460
+ });
461
+ });
462
+ });
463
+ });
464
+
324
465
  describe('setTextTrackVisibility', () => {
325
466
  beforeEach(() => {
326
467
  manifest = shaka.test.ManifestGenerator.generate((manifest) => {
@@ -265,6 +265,8 @@ shaka.test.ManifestGenerator.Variant = class {
265
265
  this.language = 'und';
266
266
  /** @type {number} */
267
267
  this.bandwidth = 0;
268
+ /** @type {number} */
269
+ this.disabledUntilTime = 0;
268
270
  /** @type {boolean} */
269
271
  this.primary = false;
270
272
  /** @type {boolean} */
@@ -333,6 +333,12 @@ shaka.test.FakePlayhead = class {
333
333
  /** @private {number} */
334
334
  this.startTime_ = 0;
335
335
 
336
+ /** @private {number} */
337
+ this.gapsJumped_ = 0;
338
+
339
+ /** @private {number} */
340
+ this.stallsDetected_ = 0;
341
+
336
342
  /** @type {!jasmine.Spy} */
337
343
  this.setStartTime = jasmine.createSpy('setStartTime')
338
344
  .and.callFake((value) => {
@@ -343,6 +349,14 @@ shaka.test.FakePlayhead = class {
343
349
  this.getTime = jasmine.createSpy('getTime')
344
350
  .and.callFake(() => this.startTime_);
345
351
 
352
+ /** @type {!jasmine.Spy} */
353
+ this.getGapsJumped = jasmine.createSpy('getGapsJumped')
354
+ .and.callFake(() => this.gapsJumped_);
355
+
356
+ /** @type {!jasmine.Spy} */
357
+ this.getStallsDetected = jasmine.createSpy('getTime')
358
+ .and.callFake(() => this.stallsDetected_);
359
+
346
360
  /** @type {!jasmine.Spy} */
347
361
  this.setBuffering = jasmine.createSpy('setBuffering');
348
362
 
@@ -287,6 +287,7 @@ shaka.test.StreamingEngineUtil = class {
287
287
  bandwidth: 0,
288
288
  id: 0,
289
289
  language: 'und',
290
+ disabledUntilTime: 0,
290
291
  primary: false,
291
292
  decodingInfos: [],
292
293
  };
@@ -624,6 +624,7 @@ describe('VttTextParser', () => {
624
624
  });
625
625
 
626
626
  it('supports global style blocks', () => {
627
+ const textShadow = '-1px 0 black, 0 1px black, 1px 0 black, 0 -1px black';
627
628
  verifyHelper(
628
629
  [
629
630
  {
@@ -632,6 +633,7 @@ describe('VttTextParser', () => {
632
633
  payload: 'Test',
633
634
  color: 'cyan',
634
635
  fontSize: '10px',
636
+ textShadow: textShadow,
635
637
  },
636
638
  {
637
639
  startTime: 40,
@@ -639,6 +641,7 @@ describe('VttTextParser', () => {
639
641
  payload: 'Test2',
640
642
  color: 'cyan',
641
643
  fontSize: '10px',
644
+ textShadow: textShadow,
642
645
  },
643
646
  ],
644
647
  'WEBVTT\n\n' +
@@ -646,6 +649,7 @@ describe('VttTextParser', () => {
646
649
  '::cue {\n' +
647
650
  'color: cyan;\n'+
648
651
  'font-size: 10px;\n'+
652
+ `text-shadow: ${textShadow};\n`+
649
653
  '}\n\n' +
650
654
  '00:00:20.000 --> 00:00:40.000\n' +
651
655
  'Test\n\n' +
@@ -809,12 +809,25 @@ describe('UI', () => {
809
809
  }
810
810
  });
811
811
  it('is updated periodically', async () => {
812
- function getStatsFromContainer() {
812
+ // There is no guaranteed ordering, so fetch by the stat name.
813
+ function getStatsElementByName(name) {
813
814
  const nodes = statisticsContainer.childNodes;
814
- width = nodes[0].childNodes[1].textContent.replace(' (px)', '');
815
- height = nodes[1].childNodes[1].textContent.replace(' (px)', '');
816
- bufferingTime =
817
- nodes[13].childNodes[1].textContent.replace(' (s)', '');
815
+ for (const node of nodes) {
816
+ if (node.hasChildNodes() &&
817
+ node.childNodes[0].textContent.includes(name)) {
818
+ return node;
819
+ }
820
+ }
821
+ return null;
822
+ }
823
+
824
+ function getStatsFromContainer() {
825
+ width = getStatsElementByName(
826
+ 'width').childNodes[1].textContent.replace(' (px)', '');
827
+ height = getStatsElementByName(
828
+ 'height').childNodes[1].textContent.replace(' (px)', '');
829
+ bufferingTime = getStatsElementByName(
830
+ 'bufferingTime').childNodes[1].textContent.replace(' (s)', '');
818
831
  }
819
832
 
820
833
  /** @type {!string} */
@@ -902,4 +902,112 @@ describe('StreamUtils', () => {
902
902
  expect(manifest.variants[0].video.id).toBe(2);
903
903
  });
904
904
  });
905
+
906
+ describe('meetsRestrictions', () => {
907
+ const oldDateNow = Date.now;
908
+ /* @param {shaka.extern.Restrictions} */
909
+ const restrictions = {
910
+ minWidth: 10,
911
+ maxWidth: 20,
912
+ minHeight: 10,
913
+ maxHeight: 20,
914
+ minPixels: 10,
915
+ maxPixels: 20,
916
+ minFrameRate: 21,
917
+ maxFrameRate: 25,
918
+ minBandwidth: 1000,
919
+ maxBandwidth: 3000,
920
+ };
921
+ /* @param {{width: number, height: number}} */
922
+ const maxHwRes = {width: 123, height: 456};
923
+ /* @param {shaka.extern.Variant} */
924
+ let variant;
925
+ /* @param {boolean} */
926
+ let meetsRestrictionsResult;
927
+
928
+ beforeEach(() => {
929
+ Date.now = () => 123 * 1000;
930
+ });
931
+
932
+ afterEach(() => {
933
+ Date.now = oldDateNow;
934
+ });
935
+
936
+ /*
937
+ * @param {number} disabledUntilTime
938
+ */
939
+ const checkRestrictions = ({disabledUntilTime = 0}) => {
940
+ /* @param {shaka.extern.Variant} */
941
+ variant = {
942
+ id: 1,
943
+ language: 'es',
944
+ disabledUntilTime,
945
+ video: null,
946
+ audio: null,
947
+ primary: false,
948
+ bandwidth: 2000,
949
+ allowedByApplication: true,
950
+ allowedByKeySystem: true,
951
+ decodingInfos: [],
952
+ };
953
+ meetsRestrictionsResult = StreamUtils.meetsRestrictions(variant,
954
+ restrictions, maxHwRes);
955
+ };
956
+
957
+ describe('when disabledUntilTime > now', () => {
958
+ beforeEach(() => {
959
+ checkRestrictions({disabledUntilTime: 124});
960
+ });
961
+
962
+ it('does not meet the restrictions', () => {
963
+ expect(meetsRestrictionsResult).toBeFalsy();
964
+ });
965
+
966
+ it('does not reset disabledUntilTime', () => {
967
+ expect(variant.disabledUntilTime).toBe(124);
968
+ });
969
+ });
970
+
971
+ describe('when disabledUntilTime == now', () => {
972
+ beforeEach(() => {
973
+ checkRestrictions({disabledUntilTime: 123});
974
+ });
975
+
976
+ it('meets the restrictions', () => {
977
+ expect(meetsRestrictionsResult).toBeTruthy();
978
+ });
979
+
980
+ it('resets disabledUntilTime', () => {
981
+ expect(variant.disabledUntilTime).toBe(0);
982
+ });
983
+ });
984
+
985
+ describe('when disabledUntilTime == now', () => {
986
+ beforeEach(() => {
987
+ checkRestrictions({disabledUntilTime: 122});
988
+ });
989
+
990
+ it('meets the restrictions', () => {
991
+ expect(meetsRestrictionsResult).toBeTruthy();
992
+ });
993
+
994
+ it('resets disabledUntilTime', () => {
995
+ expect(variant.disabledUntilTime).toBe(0);
996
+ });
997
+ });
998
+
999
+ describe('when disabledUntilTime == 0', () => {
1000
+ beforeEach(() => {
1001
+ checkRestrictions({disabledUntilTime: 0});
1002
+ });
1003
+
1004
+ it('meets the restrictions', () => {
1005
+ expect(meetsRestrictionsResult).toBeTruthy();
1006
+ });
1007
+
1008
+ it('leaves disabledUntilTime = 0', () => {
1009
+ expect(variant.disabledUntilTime).toBe(0);
1010
+ });
1011
+ });
1012
+ });
905
1013
  });
package/ui/controls.js CHANGED
@@ -53,7 +53,8 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
53
53
 
54
54
  /** @private {shaka.cast.CastProxy} */
55
55
  this.castProxy_ = new shaka.cast.CastProxy(
56
- video, player, this.config_.castReceiverAppId);
56
+ video, player, this.config_.castReceiverAppId,
57
+ this.config_.castAndroidReceiverCompatible);
57
58
 
58
59
  /** @private {boolean} */
59
60
  this.castAllowed_ = true;
@@ -315,7 +316,8 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
315
316
  configure(config) {
316
317
  this.config_ = config;
317
318
 
318
- this.castProxy_.changeReceiverId(config.castReceiverAppId);
319
+ this.castProxy_.changeReceiverId(config.castReceiverAppId,
320
+ config.castAndroidReceiverCompatible);
319
321
 
320
322
  // Deconstruct the old layout if applicable
321
323
  if (this.seekBar_) {
@@ -1258,21 +1260,23 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
1258
1260
  return;
1259
1261
  }
1260
1262
 
1263
+ const keyboardSeekDistance = this.config_.keyboardSeekDistance;
1264
+
1261
1265
  switch (event.key) {
1262
1266
  case 'ArrowLeft':
1263
1267
  // If it's not focused on the volume bar, move the seek time backward
1264
- // for 5 sec. Otherwise, the volume will be adjusted automatically.
1265
- if (this.seekBar_ && !isVolumeBar) {
1268
+ // for a few sec. Otherwise, the volume will be adjusted automatically.
1269
+ if (this.seekBar_ && !isVolumeBar && keyboardSeekDistance > 0) {
1266
1270
  event.preventDefault();
1267
- this.seek_(this.seekBar_.getValue() - 5);
1271
+ this.seek_(this.seekBar_.getValue() - keyboardSeekDistance);
1268
1272
  }
1269
1273
  break;
1270
1274
  case 'ArrowRight':
1271
1275
  // If it's not focused on the volume bar, move the seek time forward
1272
- // for 5 sec. Otherwise, the volume will be adjusted automatically.
1273
- if (this.seekBar_ && !isVolumeBar) {
1276
+ // for a few sec. Otherwise, the volume will be adjusted automatically.
1277
+ if (this.seekBar_ && !isVolumeBar && keyboardSeekDistance > 0) {
1274
1278
  event.preventDefault();
1275
- this.seek_(this.seekBar_.getValue() + 5);
1279
+ this.seek_(this.seekBar_.getValue() + keyboardSeekDistance);
1276
1280
  }
1277
1281
  break;
1278
1282
  // Jump to the beginning of the video's seek range.
package/ui/externs/ui.js CHANGED
@@ -72,6 +72,7 @@ shaka.extern.UIVolumeBarColors;
72
72
  * addBigPlayButton: boolean,
73
73
  * customContextMenu: boolean,
74
74
  * castReceiverAppId: string,
75
+ * castAndroidReceiverCompatible: boolean,
75
76
  * clearBufferOnQualityChange: boolean,
76
77
  * showUnbufferedStart: boolean,
77
78
  * seekBarColors: shaka.extern.UISeekBarColors,
@@ -83,7 +84,8 @@ shaka.extern.UIVolumeBarColors;
83
84
  * enableKeyboardPlaybackControls: boolean,
84
85
  * enableFullscreenOnRotation: boolean,
85
86
  * forceLandscapeOnFullscreen: boolean,
86
- * enableTooltips: boolean
87
+ * enableTooltips: boolean,
88
+ * keyboardSeekDistance: number
87
89
  * }}
88
90
  *
89
91
  * @property {!Array.<string>} controlPanelElements
@@ -109,6 +111,8 @@ shaka.extern.UIVolumeBarColors;
109
111
  * Whether or not a custom context menu replaces the default.
110
112
  * @property {string} castReceiverAppId
111
113
  * Receiver app id to use for the Chromecast support.
114
+ * @property {boolean} castAndroidReceiverCompatible
115
+ * Indicates if the app is compatible with an Android Cast Receiver.
112
116
  * @property {boolean} clearBufferOnQualityChange
113
117
  * Only applicable if the resolution selection is part of the UI.
114
118
  * Whether buffer should be cleared when changing resolution
@@ -174,6 +178,11 @@ shaka.extern.UIVolumeBarColors;
174
178
  * Whether or not buttons in the control panel display tooltips that contain
175
179
  * information about their function.
176
180
  * Defaults to false.
181
+ * @property {number} keyboardSeekDistance
182
+ * The time interval, in seconds, to seek when the user presses the left or
183
+ * right keyboard keys when the video is selected. If less than or equal to 0,
184
+ * no seeking will occur.
185
+ * Defaults to 5 seconds.
177
186
  * @exportDoc
178
187
  */
179
188
  shaka.extern.UIConfiguration;
@@ -109,6 +109,14 @@ shaka.ui.StatisticsButton = class extends shaka.ui.Element {
109
109
  this.currentStats_[name], false) + ' (m)';
110
110
  };
111
111
 
112
+ const parseGaps = (name) => {
113
+ return this.currentStats_[name] + ' (gaps)';
114
+ };
115
+
116
+ const parseStalls = (name) => {
117
+ return this.currentStats_[name] + ' (stalls)';
118
+ };
119
+
112
120
  /** @private {!Object.<string, function(string):string>} */
113
121
  this.parseFrom_ = {
114
122
  'width': parsePx,
@@ -128,6 +136,8 @@ shaka.ui.StatisticsButton = class extends shaka.ui.Element {
128
136
  'corruptedFrames': parseFrames,
129
137
  'decodedFrames': parseFrames,
130
138
  'droppedFrames': parseFrames,
139
+ 'stallsDetected': parseStalls,
140
+ 'gapsJumped': parseGaps,
131
141
  };
132
142
 
133
143
  /** @private {shaka.util.Timer} */
package/ui/ui.js CHANGED
@@ -218,6 +218,7 @@ shaka.ui.Overlay = class {
218
218
  addBigPlayButton: false,
219
219
  customContextMenu: false,
220
220
  castReceiverAppId: '',
221
+ castAndroidReceiverCompatible: false,
221
222
  clearBufferOnQualityChange: true,
222
223
  showUnbufferedStart: false,
223
224
  seekBarColors: {
@@ -238,6 +239,7 @@ shaka.ui.Overlay = class {
238
239
  enableFullscreenOnRotation: true,
239
240
  forceLandscapeOnFullscreen: true,
240
241
  enableTooltips: false,
242
+ keyboardSeekDistance: 5,
241
243
  };
242
244
 
243
245
  // Check AirPlay support
@@ -388,19 +390,28 @@ shaka.ui.Overlay = class {
388
390
  // Get and configure cast app id.
389
391
  let castAppId = '';
390
392
 
393
+ // Get and configure cast Android Receiver Compatibility
394
+ let castAndroidReceiverCompatible = false;
395
+
391
396
  // Cast receiver id can be specified on either container or video.
392
397
  // It should not be provided on both. If it was, we will use the last
393
398
  // one we saw.
394
399
  if (container['dataset'] &&
395
400
  container['dataset']['shakaPlayerCastReceiverId']) {
396
401
  castAppId = container['dataset']['shakaPlayerCastReceiverId'];
402
+ castAndroidReceiverCompatible =
403
+ container['dataset']['shakaPlayerCastAndroidReceiverCompatible'] ===
404
+ 'true';
397
405
  } else if (video['dataset'] &&
398
406
  video['dataset']['shakaPlayerCastReceiverId']) {
399
407
  castAppId = video['dataset']['shakaPlayerCastReceiverId'];
408
+ castAndroidReceiverCompatible =
409
+ video['dataset']['shakaPlayerCastAndroidReceiverCompatible'] === 'true';
400
410
  }
401
411
 
402
412
  if (castAppId.length) {
403
- ui.configure({castReceiverAppId: castAppId});
413
+ ui.configure({castReceiverAppId: castAppId,
414
+ castAndroidReceiverCompatible: castAndroidReceiverCompatible});
404
415
  }
405
416
 
406
417
  if (shaka.util.Dom.asHTMLMediaElement(video).controls) {