shaka-player 4.0.0 → 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 (114) hide show
  1. package/.github/config.yml +1 -0
  2. package/.github/workflows/release-please.yaml +6 -11
  3. package/CHANGELOG.md +33 -0
  4. package/CONTRIBUTORS +1 -0
  5. package/README.md +4 -4
  6. package/build/misspellings.txt +1 -0
  7. package/demo/common/message_ids.js +1 -0
  8. package/demo/config.js +3 -1
  9. package/demo/demo.less +0 -1
  10. package/demo/locales/en.json +1 -0
  11. package/demo/locales/source.json +4 -0
  12. package/dist/controls.css +3 -3
  13. package/dist/controls.css.map +1 -1
  14. package/dist/demo.compiled.debug.js +4 -4
  15. package/dist/demo.compiled.debug.map +2 -2
  16. package/dist/demo.compiled.js +4 -4
  17. package/dist/demo.compiled.map +2 -2
  18. package/dist/demo.css +1 -1
  19. package/dist/demo.css.map +1 -1
  20. package/dist/deps.js +3 -3
  21. package/dist/locales.js +32 -32
  22. package/dist/receiver.compiled.debug.js +2 -2
  23. package/dist/receiver.compiled.debug.map +2 -2
  24. package/dist/receiver.compiled.js +2 -2
  25. package/dist/receiver.compiled.map +2 -2
  26. package/dist/shaka-player.compiled.d.ts +15 -8
  27. package/dist/shaka-player.compiled.debug.d.ts +15 -8
  28. package/dist/shaka-player.compiled.debug.externs.js +11 -2
  29. package/dist/shaka-player.compiled.debug.js +266 -253
  30. package/dist/shaka-player.compiled.debug.map +3 -3
  31. package/dist/shaka-player.compiled.externs.js +11 -2
  32. package/dist/shaka-player.compiled.js +792 -784
  33. package/dist/shaka-player.compiled.map +3 -3
  34. package/dist/shaka-player.ui.d.ts +16 -9
  35. package/dist/shaka-player.ui.debug.d.ts +16 -9
  36. package/dist/shaka-player.ui.debug.externs.js +11 -2
  37. package/dist/shaka-player.ui.debug.js +299 -282
  38. package/dist/shaka-player.ui.debug.map +3 -3
  39. package/dist/shaka-player.ui.externs.js +11 -2
  40. package/dist/shaka-player.ui.js +948 -941
  41. package/dist/shaka-player.ui.map +3 -3
  42. package/docs/design/chromecast.md +3 -1
  43. package/docs/tutorials/drm-config.md +16 -0
  44. package/docs/tutorials/ui.md +26 -0
  45. package/docs/tutorials/upgrade.md +4 -2
  46. package/externs/chromecast.js +9 -2
  47. package/externs/shaka/manifest.js +7 -0
  48. package/externs/shaka/player.js +32 -2
  49. package/externs/shaka/text.js +7 -0
  50. package/karma.conf.js +4 -0
  51. package/lib/abr/simple_abr_manager.js +2 -1
  52. package/lib/cast/cast_proxy.js +19 -8
  53. package/lib/cast/cast_receiver.js +3 -3
  54. package/lib/cast/cast_sender.js +12 -2
  55. package/lib/dash/dash_parser.js +2 -2
  56. package/lib/hls/hls_parser.js +46 -15
  57. package/lib/media/drm_engine.js +36 -8
  58. package/lib/media/gap_jumping_controller.js +23 -1
  59. package/lib/media/media_source_engine.js +91 -37
  60. package/lib/media/playhead.js +47 -4
  61. package/lib/media/stall_detector.js +19 -2
  62. package/lib/media/streaming_engine.js +9 -3
  63. package/lib/offline/indexeddb/storage_mechanism.js +4 -2
  64. package/lib/offline/manifest_converter.js +1 -0
  65. package/lib/player.js +339 -175
  66. package/lib/text/cue.js +6 -0
  67. package/lib/text/ui_text_displayer.js +1 -0
  68. package/lib/text/vtt_text_parser.js +11 -7
  69. package/lib/util/config_utils.js +25 -0
  70. package/lib/util/error.js +13 -0
  71. package/lib/util/fake_event.js +39 -0
  72. package/lib/util/manifest_parser_utils.js +1 -1
  73. package/lib/util/mime_utils.js +62 -6
  74. package/lib/util/periods.js +5 -4
  75. package/lib/util/player_configuration.js +16 -14
  76. package/lib/util/stats.js +28 -1
  77. package/lib/util/stream_utils.js +11 -2
  78. package/package.json +5 -2
  79. package/test/cast/cast_proxy_unit.js +3 -1
  80. package/test/cast/cast_sender_unit.js +21 -6
  81. package/test/demo/demo_unit.js +2 -1
  82. package/test/hls/hls_live_unit.js +79 -7
  83. package/test/hls/manifest_text_parser_unit.js +19 -0
  84. package/test/media/adaptation_set_unit.js +1 -0
  85. package/test/media/playhead_unit.js +114 -25
  86. package/test/media/stall_detector_unit.js +20 -2
  87. package/test/media/streaming_engine_integration.js +2 -1
  88. package/test/media/streaming_engine_unit.js +1 -0
  89. package/test/offline/indexeddb_storage_unit.js +1 -1
  90. package/test/offline/storage_compatibility_unit.js +1 -1
  91. package/test/player_external.js +7 -1
  92. package/test/player_integration.js +9 -10
  93. package/test/player_src_equals_integration.js +81 -83
  94. package/test/player_unit.js +240 -2
  95. package/test/test/boot.js +9 -0
  96. package/test/test/externs/filters.js +4 -0
  97. package/test/test/util/manifest_generator.js +2 -0
  98. package/test/test/util/simple_fakes.js +45 -2
  99. package/test/test/util/streaming_engine_util.js +1 -0
  100. package/test/text/vtt_text_parser_unit.js +68 -5
  101. package/test/ui/ui_unit.js +18 -5
  102. package/test/util/mime_utils_unit.js +54 -0
  103. package/test/util/periods_unit.js +79 -0
  104. package/test/util/stream_utils_unit.js +108 -0
  105. package/ui/controls.js +12 -8
  106. package/ui/externs/ui.js +10 -1
  107. package/ui/less/containers.less +1 -1
  108. package/ui/less/general.less +1 -1
  109. package/ui/less/range_elements.less +1 -0
  110. package/ui/resolution_selection.js +1 -1
  111. package/ui/statistics_button.js +10 -0
  112. package/ui/ui.js +12 -1
  113. package/.github/pull_request_template.md +0 -10
  114. package/docs/tutorials/gsoc-ideas-list.md +0 -3
@@ -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,12 +131,22 @@ 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
- player.createPlayhead = () => playhead;
143
+ player.createPlayhead = (startTime) => {
144
+ const callableSetStartTime =
145
+ shaka.test.Util.spyFunc(playhead.setStartTime);
146
+ callableSetStartTime(startTime);
147
+ playhead.setStartTime.calls.reset();
148
+ return playhead;
149
+ };
128
150
  player.createMediaSourceEngine = () => mediaSourceEngine;
129
151
  player.createStreamingEngine = () => streamingEngine;
130
152
  }
@@ -156,6 +178,7 @@ describe('Player', () => {
156
178
  window.MediaSource.isTypeSupported = originalIsTypeSupported;
157
179
  shaka.media.ManifestParser.unregisterParserByMime(fakeMimeType);
158
180
  navigator.mediaCapabilities.decodingInfo = originalDecodingInfo;
181
+ onError.calls.reset();
159
182
  }
160
183
  });
161
184
 
@@ -314,6 +337,131 @@ describe('Player', () => {
314
337
  });
315
338
  });
316
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
+
317
465
  describe('setTextTrackVisibility', () => {
318
466
  beforeEach(() => {
319
467
  manifest = shaka.test.ManifestGenerator.generate((manifest) => {
@@ -2658,6 +2806,51 @@ describe('Player', () => {
2658
2806
  expect(activeVariant.id).toBe(1);
2659
2807
  });
2660
2808
 
2809
+ // Regression test for https://github.com/shaka-project/shaka-player/issues/4190
2810
+ it('throws only one RESTRICTIONS_CANNOT_BE_MET error', async () => {
2811
+ manifest = shaka.test.ManifestGenerator.generate((manifest) => {
2812
+ manifest.addVariant(0, (variant) => {
2813
+ variant.addVideo(1, (stream) => {
2814
+ stream.keyIds = new Set(['abc']);
2815
+ });
2816
+ });
2817
+ });
2818
+
2819
+ // Check that error is RESTRICTIONS_CANNOT_BE_MET
2820
+ onError.and.callFake((e) => {
2821
+ const error = e.detail;
2822
+ shaka.test.Util.expectToEqualError(
2823
+ error,
2824
+ new shaka.util.Error(
2825
+ shaka.util.Error.Severity.CRITICAL,
2826
+ shaka.util.Error.Category.MANIFEST,
2827
+ shaka.util.Error.Code.RESTRICTIONS_CANNOT_BE_MET, {
2828
+ hasAppRestrictions: false,
2829
+ missingKeys: ['abc'],
2830
+ restrictedKeyStatuses: [],
2831
+ }));
2832
+ });
2833
+
2834
+ await player.load(fakeManifestUri, 0, fakeMimeType);
2835
+ const activeVariant = getActiveVariantTrack();
2836
+ expect(activeVariant.id).toBe(0);
2837
+
2838
+ abrManager.chooseIndex = 0;
2839
+ abrManager.chooseVariant.calls.reset();
2840
+
2841
+ onKeyStatus({'abc': 'output-restricted'});
2842
+
2843
+ // Ensure that RESTRICTIONS_CANNOT_BE_MET is thrown once
2844
+ expect(onError).toHaveBeenCalledTimes(1);
2845
+
2846
+ // It does not call chooseVariant
2847
+ expect(abrManager.chooseVariant).not.toHaveBeenCalled();
2848
+
2849
+ // The first variant is disallowed.
2850
+ expect(manifest.variants[0].id).toBe(0);
2851
+ expect(manifest.variants[0].allowedByKeySystem).toBe(false);
2852
+ });
2853
+
2661
2854
  it('doesn\'t switch if the active stream isn\'t restricted', async () => {
2662
2855
  manifest = shaka.test.ManifestGenerator.generate((manifest) => {
2663
2856
  manifest.addVariant(0, (variant) => {
@@ -3664,6 +3857,51 @@ describe('Player', () => {
3664
3857
  });
3665
3858
  });
3666
3859
 
3860
+ describe('config streaming.startAtSegmentBoundary', () => {
3861
+ // Test for https://github.com/shaka-project/shaka-player/issues/4188
3862
+ // In that issue, using streaming.startAtSegmentBoundary in v4.0.0 caused
3863
+ // an exception due to the use of a removed method.
3864
+ it('adjusts start time', async () => {
3865
+ player.configure('streaming.startAtSegmentBoundary', true);
3866
+
3867
+ // In order to adjust the start time to a segment boundary, we need
3868
+ // segment descriptions in the segment index. So create a non-default
3869
+ // fake manifest.
3870
+ const timeline = new shaka.media.PresentationTimeline(300, 0);
3871
+ timeline.setStatic(true);
3872
+ // This duration is used by useSegmentTemplate below to decide how many
3873
+ // references to generate.
3874
+ timeline.setDuration(300);
3875
+ manifest = shaka.test.ManifestGenerator.generate((manifest) => {
3876
+ manifest.presentationTimeline = timeline;
3877
+ manifest.addVariant(0, (variant) => {
3878
+ variant.addVideo(1, (stream) => {
3879
+ stream.useSegmentTemplate(
3880
+ '$Number$.mp4', /* segmentDuration= */ 10);
3881
+ });
3882
+ });
3883
+ });
3884
+
3885
+ await player.load(fakeManifestUri, 12, fakeMimeType);
3886
+
3887
+ expect(playhead.setStartTime).toHaveBeenCalledWith(10);
3888
+ });
3889
+
3890
+ it('does not fail with no segments', async () => {
3891
+ player.configure('streaming.startAtSegmentBoundary', true);
3892
+
3893
+ // Without useSegmentTemplate in the fake manifest, the call to
3894
+ // getIteratorForTime() in Player produces a null iterator. Using the
3895
+ // default fake manifest should cover that case. But just to make sure,
3896
+ // verify that we have no segments before calling load().
3897
+ const variant = manifest.variants[0];
3898
+ expect(variant.video.segmentIndex.getIteratorForTime(0)).toBeNull();
3899
+ expect(variant.audio.segmentIndex.getIteratorForTime(0)).toBeNull();
3900
+
3901
+ await player.load(fakeManifestUri, 0, fakeMimeType);
3902
+ });
3903
+ });
3904
+
3667
3905
  /**
3668
3906
  * Gets the currently active variant track.
3669
3907
  * @return {shaka.extern.Track}
package/test/test/boot.js CHANGED
@@ -242,6 +242,15 @@ window.xfilterDescribe = (describeName, cond, describeBody) => {
242
242
  window['describe'] = oldDescribe;
243
243
  };
244
244
 
245
+ /**
246
+ * A very short alias for filtering offline integration tests.
247
+ *
248
+ * @return {boolean}
249
+ */
250
+ window.offlineSupported = () => {
251
+ return shaka.offline.StorageMuxer.support();
252
+ };
253
+
245
254
  /**
246
255
  * Load node modules used in testing and install them into window.
247
256
  * @return {!Promise}
@@ -38,3 +38,7 @@ var filterDescribe = function(name, cond, callback) {};
38
38
  * @param {function()} callback
39
39
  */
40
40
  var xfilterDescribe = function(name, cond, callback) {};
41
+
42
+
43
+ /** @return {boolean} */
44
+ var offlineSupported = function() {};
@@ -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} */
@@ -152,9 +152,27 @@ shaka.test.FakeVideo = class {
152
152
  constructor(currentTime) {
153
153
  /** @const {!Object.<string, !Function>} */
154
154
  this.on = {}; // event listeners
155
+
155
156
  /** @type {!Array.<!TextTrack>} */
156
157
  this.textTracks = [];
157
158
 
159
+ // In a real video element, textTracks is an event target.
160
+ // Since Player listens to events on textTracks, we need to fake that
161
+ // interface.
162
+ this.textTracksEventTarget = new shaka.util.FakeEventTarget();
163
+ this.textTracks.addEventListener =
164
+ // eslint-disable-next-line no-restricted-syntax
165
+ this.textTracksEventTarget.addEventListener.bind(
166
+ this.textTracksEventTarget);
167
+ this.textTracks.removeEventListener =
168
+ // eslint-disable-next-line no-restricted-syntax
169
+ this.textTracksEventTarget.removeEventListener.bind(
170
+ this.textTracksEventTarget);
171
+ this.textTracks.dispatchEvent =
172
+ // eslint-disable-next-line no-restricted-syntax
173
+ this.textTracksEventTarget.dispatchEvent.bind(
174
+ this.textTracksEventTarget);
175
+
158
176
  this.currentTime = currentTime || 0;
159
177
  this.readyState = 0;
160
178
  this.playbackRate = 1;
@@ -173,6 +191,10 @@ shaka.test.FakeVideo = class {
173
191
  jasmine.createSpy('addTextTrack').and.callFake((kind, id) => {
174
192
  const track = new shaka.test.FakeTextTrack();
175
193
  this.textTracks.push(track);
194
+
195
+ const trackEvent = new shaka.util.FakeEvent('addtrack', {track});
196
+ this.textTracksEventTarget.dispatchEvent(trackEvent);
197
+
176
198
  return track;
177
199
  });
178
200
 
@@ -308,11 +330,32 @@ shaka.test.FakePlayhead = class {
308
330
  /** @type {!jasmine.Spy} */
309
331
  this.setRebufferingGoal = jasmine.createSpy('setRebufferingGoal');
310
332
 
333
+ /** @private {number} */
334
+ this.startTime_ = 0;
335
+
336
+ /** @private {number} */
337
+ this.gapsJumped_ = 0;
338
+
339
+ /** @private {number} */
340
+ this.stallsDetected_ = 0;
341
+
342
+ /** @type {!jasmine.Spy} */
343
+ this.setStartTime = jasmine.createSpy('setStartTime')
344
+ .and.callFake((value) => {
345
+ this.startTime_ = value;
346
+ });
347
+
348
+ /** @type {!jasmine.Spy} */
349
+ this.getTime = jasmine.createSpy('getTime')
350
+ .and.callFake(() => this.startTime_);
351
+
311
352
  /** @type {!jasmine.Spy} */
312
- this.setStartTime = jasmine.createSpy('setStartTime');
353
+ this.getGapsJumped = jasmine.createSpy('getGapsJumped')
354
+ .and.callFake(() => this.gapsJumped_);
313
355
 
314
356
  /** @type {!jasmine.Spy} */
315
- this.getTime = jasmine.createSpy('getTime').and.returnValue(0);
357
+ this.getStallsDetected = jasmine.createSpy('getTime')
358
+ .and.callFake(() => this.stallsDetected_);
316
359
 
317
360
  /** @type {!jasmine.Spy} */
318
361
  this.setBuffering = jasmine.createSpy('setBuffering');
@@ -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
  };
@@ -535,7 +535,61 @@ describe('VttTextParser', () => {
535
535
  'Test\n\n' +
536
536
  '00:00:40.000 --> 00:00:50.000 line:-1\n' +
537
537
  'Test2',
538
- {periodStart: 0, segmentStart: 25, segmentEnd: 65, vttOffset: 0});
538
+ {periodStart: 0, segmentStart: 25, segmentEnd: 65, vttOffset: 0},
539
+ /* sequenceMode= */ true);
540
+ });
541
+
542
+ it('ignores X-TIMESTAMP-MAP header if not in sequence mode', () => {
543
+ verifyHelper(
544
+ [
545
+ {startTime: 20, endTime: 40, payload: 'Test'},
546
+ {startTime: 40, endTime: 50, payload: 'Test2'},
547
+ ],
548
+ 'WEBVTT\n' +
549
+ 'X-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:01:00:00.000\n\n' +
550
+ '00:00:20.000 --> 00:00:40.000 line:0\n' +
551
+ 'Test\n\n' +
552
+ '00:00:40.000 --> 00:00:50.000 line:-1\n' +
553
+ 'Test2',
554
+ {periodStart: 0, segmentStart: 25, segmentEnd: 65, vttOffset: 0},
555
+ /* sequenceMode= */ false);
556
+ });
557
+
558
+ it('parses X-TIMESTAMP-MAP header with non-zero local base', () => {
559
+ verifyHelper(
560
+ [
561
+ {startTime: 1800, endTime: 1810, payload: 'Test'},
562
+ {startTime: 1820, endTime: 1830, payload: 'Test2'},
563
+ ],
564
+ // 162000000 = 30 * 60 * 90k = 30 minutes for the TS part of the map.
565
+ // The local (VTT) part of the map is 1 hour.
566
+ // So text times of 1 hour map to media times of 30 minutes.
567
+ 'WEBVTT\n' +
568
+ 'X-TIMESTAMP-MAP=MPEGTS:162000000,LOCAL:01:00:00.000\n\n' +
569
+ '01:00:00.000 --> 01:00:10.000 line:0\n' +
570
+ 'Test\n\n' +
571
+ '01:00:20.000 --> 01:00:30.000 line:-1\n' +
572
+ 'Test2',
573
+ {periodStart: 0, segmentStart: 25, segmentEnd: 65, vttOffset: 0},
574
+ /* sequenceMode= */ true);
575
+ });
576
+
577
+ it('combines X-TIMESTAMP-MAP header with periodStart', () => {
578
+ verifyHelper(
579
+ [
580
+ {startTime: 130, endTime: 150, payload: 'Test'},
581
+ {startTime: 150, endTime: 160, payload: 'Test2'},
582
+ ],
583
+ // 900000 = 10 sec, so expect every timestamp to be 10
584
+ // seconds ahead of what is specified.
585
+ 'WEBVTT\n' +
586
+ 'X-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000\n\n' +
587
+ '00:00:20.000 --> 00:00:40.000 line:0\n' +
588
+ 'Test\n\n' +
589
+ '00:00:40.000 --> 00:00:50.000 line:-1\n' +
590
+ 'Test2',
591
+ {periodStart: 100, segmentStart: 25, segmentEnd: 65, vttOffset: 0},
592
+ /* sequenceMode= */ true);
539
593
  });
540
594
 
541
595
  it('handles timestamp rollover with X-TIMESTAMP-MAP header', () => {
@@ -551,7 +605,8 @@ describe('VttTextParser', () => {
551
605
  'Test',
552
606
  // Non-null segmentStart takes precedence over X-TIMESTAMP-MAP.
553
607
  // This protects us from rollover in the MPEGTS field.
554
- {periodStart: 0, segmentStart: 95440, segmentEnd: 95550, vttOffset: 0});
608
+ {periodStart: 0, segmentStart: 95440, segmentEnd: 95550, vttOffset: 0},
609
+ /* sequenceMode= */ true);
555
610
 
556
611
  verifyHelper(
557
612
  [
@@ -564,10 +619,12 @@ describe('VttTextParser', () => {
564
619
  'X-TIMESTAMP-MAP=MPEGTS:9745408,LOCAL:00:00:00.000\n\n' +
565
620
  '00:00:00.000 --> 00:00:02.000 line:0\n' +
566
621
  'Test2',
567
- {periodStart: 0, segmentStart: 95550, segmentEnd: 95560, vttOffset: 0});
622
+ {periodStart: 0, segmentStart: 95550, segmentEnd: 95560, vttOffset: 0},
623
+ /* sequenceMode= */ true);
568
624
  });
569
625
 
570
626
  it('supports global style blocks', () => {
627
+ const textShadow = '-1px 0 black, 0 1px black, 1px 0 black, 0 -1px black';
571
628
  verifyHelper(
572
629
  [
573
630
  {
@@ -576,6 +633,7 @@ describe('VttTextParser', () => {
576
633
  payload: 'Test',
577
634
  color: 'cyan',
578
635
  fontSize: '10px',
636
+ textShadow: textShadow,
579
637
  },
580
638
  {
581
639
  startTime: 40,
@@ -583,6 +641,7 @@ describe('VttTextParser', () => {
583
641
  payload: 'Test2',
584
642
  color: 'cyan',
585
643
  fontSize: '10px',
644
+ textShadow: textShadow,
586
645
  },
587
646
  ],
588
647
  'WEBVTT\n\n' +
@@ -590,6 +649,7 @@ describe('VttTextParser', () => {
590
649
  '::cue {\n' +
591
650
  'color: cyan;\n'+
592
651
  'font-size: 10px;\n'+
652
+ `text-shadow: ${textShadow};\n`+
593
653
  '}\n\n' +
594
654
  '00:00:20.000 --> 00:00:40.000\n' +
595
655
  'Test\n\n' +
@@ -978,11 +1038,14 @@ describe('VttTextParser', () => {
978
1038
  * @param {!Array} cues
979
1039
  * @param {string} text
980
1040
  * @param {shaka.extern.TextParser.TimeContext} time
1041
+ * @param {boolean=} sequenceMode
981
1042
  */
982
- function verifyHelper(cues, text, time) {
1043
+ function verifyHelper(cues, text, time, sequenceMode = false) {
983
1044
  const data =
984
1045
  shaka.util.BufferUtils.toUint8(shaka.util.StringUtils.toUTF8(text));
985
- const result = new shaka.text.VttTextParser().parseMedia(data, time);
1046
+ const parser = new shaka.text.VttTextParser();
1047
+ parser.setSequenceMode(sequenceMode);
1048
+ const result = parser.parseMedia(data, time);
986
1049
 
987
1050
  const expected = cues.map((cue) => {
988
1051
  if (cue.nestedCues) {
@@ -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} */
@@ -0,0 +1,54 @@
1
+ /*! @license
2
+ * Shaka Player
3
+ * Copyright 2016 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ describe('MimeUtils', () => {
8
+ const getNormalizedCodec = (codecs) =>
9
+ shaka.util.MimeUtils.getNormalizedCodec(codecs);
10
+
11
+ it('normalizes codecs', () => {
12
+ expect(getNormalizedCodec('mp4a.66')).toBe('aac');
13
+ expect(getNormalizedCodec('mp4a.67')).toBe('aac');
14
+ expect(getNormalizedCodec('mp4a.68')).toBe('aac');
15
+
16
+ expect(getNormalizedCodec('mp3')).toBe('mp3');
17
+ expect(getNormalizedCodec('mp4a.69')).toBe('mp3');
18
+ expect(getNormalizedCodec('mp4a.6B')).toBe('mp3');
19
+ expect(getNormalizedCodec('mp4a.6b')).toBe('mp3');
20
+
21
+ expect(getNormalizedCodec('mp4a.40.2')).toBe('aac');
22
+ expect(getNormalizedCodec('mp4a.40.02')).toBe('aac');
23
+ expect(getNormalizedCodec('mp4a.40.5')).toBe('aac');
24
+ expect(getNormalizedCodec('mp4a.40.05')).toBe('aac');
25
+ expect(getNormalizedCodec('mp4a.40.29')).toBe('aac');
26
+ expect(getNormalizedCodec('mp4a.40.42')).toBe('aac');
27
+
28
+ expect(getNormalizedCodec('ac-3')).toBe('ac-3');
29
+ expect(getNormalizedCodec('mp4a.a5')).toBe('ac-3');
30
+ expect(getNormalizedCodec('mp4a.A5')).toBe('ac-3');
31
+
32
+ expect(getNormalizedCodec('ec-3')).toBe('ec-3');
33
+ expect(getNormalizedCodec('mp4a.a6')).toBe('ec-3');
34
+ expect(getNormalizedCodec('mp4a.A6')).toBe('ec-3');
35
+
36
+ expect(getNormalizedCodec('dtsc')).toBe('dtsc');
37
+ expect(getNormalizedCodec('mp4a.a9')).toBe('dtsc');
38
+
39
+ expect(getNormalizedCodec('dtsx')).toBe('dtsx');
40
+ expect(getNormalizedCodec('mp4a.b2')).toBe('dtsx');
41
+
42
+ expect(getNormalizedCodec('vp8')).toBe('vp8');
43
+ expect(getNormalizedCodec('vp8.0')).toBe('vp8');
44
+
45
+ expect(getNormalizedCodec('avc1')).toBe('avc');
46
+ expect(getNormalizedCodec('avc3')).toBe('avc');
47
+
48
+ expect(getNormalizedCodec('hvc1')).toBe('hevc');
49
+ expect(getNormalizedCodec('hev1')).toBe('hevc');
50
+
51
+ expect(getNormalizedCodec('dvh1.05')).toBe('dovi');
52
+ expect(getNormalizedCodec('dvhe.05')).toBe('dovi');
53
+ });
54
+ });