shaka-player 3.0.14 → 3.0.15

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 (82) hide show
  1. package/AUTHORS +2 -0
  2. package/CHANGELOG.md +39 -0
  3. package/CONTRIBUTORS +2 -0
  4. package/build/generateExterns.js +1 -1
  5. package/demo/common/assets.js +1 -1
  6. package/dist/controls.css +1 -1
  7. package/dist/demo.compiled.debug.js +1 -1
  8. package/dist/demo.compiled.js +1 -1
  9. package/dist/deps.js +1 -1
  10. package/dist/locales.js +34 -34
  11. package/dist/receiver.compiled.debug.js +1 -1
  12. package/dist/receiver.compiled.js +1 -1
  13. package/dist/shaka-player.compiled.d.ts +1 -1
  14. package/dist/shaka-player.compiled.debug.d.ts +1 -1
  15. package/dist/shaka-player.compiled.debug.externs.js +1 -1
  16. package/dist/shaka-player.compiled.debug.js +181 -178
  17. package/dist/shaka-player.compiled.debug.map +3 -3
  18. package/dist/shaka-player.compiled.externs.js +1 -1
  19. package/dist/shaka-player.compiled.js +688 -686
  20. package/dist/shaka-player.compiled.map +3 -3
  21. package/dist/shaka-player.ui.d.ts +1 -1
  22. package/dist/shaka-player.ui.debug.d.ts +1 -1
  23. package/dist/shaka-player.ui.debug.externs.js +3 -3
  24. package/dist/shaka-player.ui.debug.js +169 -166
  25. package/dist/shaka-player.ui.debug.map +4 -4
  26. package/dist/shaka-player.ui.externs.js +3 -3
  27. package/dist/shaka-player.ui.js +815 -813
  28. package/dist/shaka-player.ui.map +4 -4
  29. package/docs/tutorials/fairplay.md +1 -1
  30. package/karma.conf.js +1 -0
  31. package/lib/ads/client_side_ad_manager.js +46 -86
  32. package/lib/ads/server_side_ad_manager.js +7 -10
  33. package/lib/cast/cast_proxy.js +3 -2
  34. package/lib/cast/cast_receiver.js +6 -2
  35. package/lib/cast/cast_sender.js +2 -2
  36. package/lib/dash/dash_parser.js +52 -0
  37. package/lib/dash/segment_list.js +1 -1
  38. package/lib/dash/segment_template.js +1 -1
  39. package/lib/hls/hls_parser.js +25 -10
  40. package/lib/media/drm_engine.js +7 -0
  41. package/lib/media/gap_jumping_controller.js +4 -2
  42. package/lib/media/streaming_engine.js +2 -1
  43. package/lib/net/networking_engine.js +2 -1
  44. package/lib/player.js +18 -17
  45. package/lib/polyfill/patchedmediakeys_apple.js +4 -4
  46. package/lib/polyfill/patchedmediakeys_ms.js +4 -4
  47. package/lib/polyfill/patchedmediakeys_webkit.js +4 -4
  48. package/lib/text/mp4_ttml_parser.js +3 -0
  49. package/lib/text/ttml_text_parser.js +5 -0
  50. package/lib/text/ui_text_displayer.js +6 -1
  51. package/lib/util/fake_event.js +53 -7
  52. package/lib/util/periods.js +4 -1
  53. package/package.json +1 -1
  54. package/test/cast/cast_proxy_unit.js +12 -6
  55. package/test/cast/cast_utils_unit.js +1 -1
  56. package/test/dash/dash_parser_manifest_unit.js +73 -0
  57. package/test/hls/hls_parser_unit.js +5 -1
  58. package/test/media/drm_engine_unit.js +43 -0
  59. package/test/player_integration.js +38 -0
  60. package/test/test/assets/3675/chunk-stream0-00001.m4s +0 -0
  61. package/test/test/assets/3675/chunk-stream0-00002.m4s +0 -0
  62. package/test/test/assets/3675/chunk-stream0-00003.m4s +0 -0
  63. package/test/test/assets/3675/chunk-stream0-00004.m4s +0 -0
  64. package/test/test/assets/3675/chunk-stream0-00005.m4s +0 -0
  65. package/test/test/assets/3675/chunk-stream0-00006.m4s +0 -0
  66. package/test/test/assets/3675/chunk-stream0-00007.m4s +0 -0
  67. package/test/test/assets/3675/dash_0.mpd +30 -0
  68. package/test/test/assets/3675/dash_1.mpd +30 -0
  69. package/test/test/assets/3675/dash_2.mpd +30 -0
  70. package/test/test/assets/3675/dash_3.mpd +30 -0
  71. package/test/test/assets/3675/dash_4.mpd +30 -0
  72. package/test/test/assets/3675/dash_5.mpd +26 -0
  73. package/test/test/assets/3675/init-stream0.m4s +0 -0
  74. package/test/test/util/fake_ad_manager.js +1 -1
  75. package/test/text/ttml_text_parser_unit.js +9 -0
  76. package/test/text/ui_text_displayer_unit.js +18 -0
  77. package/test/ui/localization_unit.js +15 -14
  78. package/ui/cast_button.js +2 -3
  79. package/ui/controls.js +4 -6
  80. package/ui/locales/ru.json +6 -6
  81. package/ui/localization.js +28 -51
  82. package/ui/pip_button.js +2 -3
package/lib/player.js CHANGED
@@ -570,7 +570,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
570
570
  enterNode: (node, has, wants) => {
571
571
  this.dispatchEvent(this.makeEvent_(
572
572
  /* name= */ shaka.Player.EventName.OnStateChange,
573
- /* data= */ {'state': node.name}));
573
+ /* data= */ (new Map()).set('state', node.name)));
574
574
 
575
575
  const action = actions.get(node);
576
576
  return action(has, wants);
@@ -601,7 +601,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
601
601
  onIdle: (node) => {
602
602
  this.dispatchEvent(this.makeEvent_(
603
603
  /* name= */ shaka.Player.EventName.OnStateIdle,
604
- /* data= */ {'state': node.name}));
604
+ /* data= */ (new Map()).set('state', node.name)));
605
605
  },
606
606
  };
607
607
 
@@ -621,7 +621,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
621
621
 
622
622
  /**
623
623
  * @param {!shaka.Player.EventName} name
624
- * @param {Object=} data
624
+ * @param {Map.<string, Object>=} data
625
625
  * @return {!shaka.util.FakeEvent}
626
626
  * @private
627
627
  */
@@ -1111,6 +1111,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1111
1111
  'mpg': 'video/mpeg',
1112
1112
  'mpeg': 'video/mpeg',
1113
1113
  'm3u8': 'application/x-mpegurl',
1114
+ 'mpd': 'application/dash+xml',
1114
1115
  'mp3': 'audio/mpeg',
1115
1116
  'aac': 'audio/aac',
1116
1117
  'flac': 'audio/flac',
@@ -2342,12 +2343,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2342
2343
  goog.asserts.assert(!endTime || startTime <= endTime,
2343
2344
  'Metadata start time should be less or equal to the end time!');
2344
2345
  const eventName = shaka.Player.EventName.Metadata;
2345
- const data = {
2346
- startTime: startTime,
2347
- endTime: endTime,
2348
- metadataType: metadataType,
2349
- payload: payload,
2350
- };
2346
+ const data = new Map()
2347
+ .set('startTime', startTime)
2348
+ .set('endTime', endTime)
2349
+ .set('metadataType', metadataType)
2350
+ .set('payload', payload);
2351
2351
  this.dispatchEvent(this.makeEvent_(eventName, data));
2352
2352
  }
2353
2353
 
@@ -4268,7 +4268,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4268
4268
  // Surface the buffering event so that the app knows if/when we are
4269
4269
  // buffering.
4270
4270
  const eventName = shaka.Player.EventName.Buffering;
4271
- this.dispatchEvent(this.makeEvent_(eventName, {'buffering': isBuffering}));
4271
+ const data = (new Map()).set('buffering', isBuffering);
4272
+ this.dispatchEvent(this.makeEvent_(eventName, data));
4272
4273
  }
4273
4274
 
4274
4275
  /**
@@ -4623,10 +4624,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4623
4624
 
4624
4625
  /** @private */
4625
4626
  onAbrStatusChanged_() {
4626
- const event = this.makeEvent_(shaka.Player.EventName.AbrStatusChanged, {
4627
- newStatus: this.config_.abr.enabled,
4628
- });
4629
- this.delayDispatchEvent_(event);
4627
+ const data = (new Map()).set('newStatus', this.config_.abr.enabled);
4628
+ this.delayDispatchEvent_(this.makeEvent_(
4629
+ shaka.Player.EventName.AbrStatusChanged, data));
4630
4630
  }
4631
4631
 
4632
4632
  /**
@@ -4643,7 +4643,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4643
4643
  }
4644
4644
 
4645
4645
  const eventName = shaka.Player.EventName.Error;
4646
- const event = this.makeEvent_(eventName, {'detail': error});
4646
+ const event = this.makeEvent_(eventName, (new Map()).set('detail', error));
4647
4647
  this.dispatchEvent(event);
4648
4648
  if (event.defaultPrevented) {
4649
4649
  error.handled = true;
@@ -4672,7 +4672,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4672
4672
  eventElement: region.eventElement,
4673
4673
  };
4674
4674
 
4675
- this.dispatchEvent(this.makeEvent_(eventName, {detail: clone}));
4675
+ const data = (new Map()).set('detail', clone);
4676
+ this.dispatchEvent(this.makeEvent_(eventName, data));
4676
4677
  }
4677
4678
 
4678
4679
  /**
@@ -5472,7 +5473,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
5472
5473
  * @define {string} A version number taken from git at compile time.
5473
5474
  * @export
5474
5475
  */
5475
- shaka.Player.version = 'v3.0.14-uncompiled';
5476
+ shaka.Player.version = 'v3.0.15-uncompiled';
5476
5477
 
5477
5478
  // Initialize the deprecation system using the version string we just set
5478
5479
  // on the player.
@@ -567,10 +567,10 @@ class extends shaka.util.FakeEventTarget {
567
567
 
568
568
  const isNew = this.keyStatuses.getStatus() == undefined;
569
569
 
570
- const event2 = new shaka.util.FakeEvent('message', {
571
- messageType: isNew ? 'license-request' : 'license-renewal',
572
- message: shaka.util.BufferUtils.toArrayBuffer(event.message),
573
- });
570
+ const data = new Map()
571
+ .set('messageType', isNew ? 'license-request' : 'license-renewal')
572
+ .set('message', shaka.util.BufferUtils.toArrayBuffer(event.message));
573
+ const event2 = new shaka.util.FakeEvent('message', data);
574
574
 
575
575
  this.dispatchEvent(event2);
576
576
  }
@@ -519,10 +519,10 @@ class extends shaka.util.FakeEventTarget {
519
519
 
520
520
  const isNew = this.keyStatuses.getStatus() == undefined;
521
521
 
522
- const event2 = new shaka.util.FakeEvent('message', {
523
- messageType: isNew ? 'license-request' : 'license-renewal',
524
- message: shaka.util.BufferUtils.toArrayBuffer(event.message),
525
- });
522
+ const data = new Map()
523
+ .set('messageType', isNew ? 'license-request' : 'license-renewal')
524
+ .set('message', shaka.util.BufferUtils.toArrayBuffer(event.message));
525
+ const event2 = new shaka.util.FakeEvent('message', data);
526
526
 
527
527
  this.dispatchEvent(event2);
528
528
  }
@@ -454,10 +454,10 @@ shaka.polyfill.PatchedMediaKeysWebkit.MediaKeys = class {
454
454
 
455
455
  const isNew = session.keyStatuses.getStatus() == undefined;
456
456
 
457
- const event2 = new shaka.util.FakeEvent('message', {
458
- messageType: isNew ? 'licenserequest' : 'licenserenewal',
459
- message: event.message,
460
- });
457
+ const data = new Map()
458
+ .set('messageType', isNew ? 'licenserequest' : 'licenserenewal')
459
+ .set('message', event.message);
460
+ const event2 = new shaka.util.FakeEvent('message', data);
461
461
 
462
462
  session.generated();
463
463
  session.dispatchEvent(event2);
@@ -88,6 +88,9 @@ shaka.text.Mp4TtmlParser = class {
88
88
 
89
89
  shaka.text.TextEngine.registerParser(
90
90
  'application/mp4; codecs="stpp"', () => new shaka.text.Mp4TtmlParser());
91
+ shaka.text.TextEngine.registerParser(
92
+ 'application/mp4; codecs="stpp.ttml"',
93
+ () => new shaka.text.Mp4TtmlParser());
91
94
  shaka.text.TextEngine.registerParser(
92
95
  'application/mp4; codecs="stpp.ttml.im1t"',
93
96
  () => new shaka.text.Mp4TtmlParser());
@@ -277,6 +277,11 @@ shaka.text.TtmlTextParser = class {
277
277
  !cueElement.hasAttribute('begin') &&
278
278
  !cueElement.hasAttribute('end');
279
279
 
280
+ if (cueElement.nodeType == Node.COMMENT_NODE) {
281
+ // The comments do not contain information that interests us here.
282
+ return null;
283
+ }
284
+
280
285
  if (
281
286
  cueElement.nodeType != Node.ELEMENT_NODE ||
282
287
  /* Disregards empty elements without time attributes nor content
@@ -156,9 +156,14 @@ shaka.text.UITextDisplayer = class {
156
156
  }
157
157
 
158
158
  // Remove the cues out of the time range.
159
+ const oldNumCues = this.cues_.length;
159
160
  this.cues_ = this.cues_.filter(
160
161
  (cue) => cue.startTime < start || cue.endTime >= end);
161
- this.updateCaptions_();
162
+ // If anything was actually removed in this process, force the captions to
163
+ // update. This makes sure that the currently-displayed cues will stop
164
+ // displaying if removed (say, due to the user changing languages).
165
+ const forceUpdate = oldNumCues > this.cues_.length;
166
+ this.updateCaptions_(forceUpdate);
162
167
 
163
168
  return true;
164
169
  }
@@ -6,6 +6,8 @@
6
6
 
7
7
  goog.provide('shaka.util.FakeEvent');
8
8
 
9
+ goog.require('goog.asserts');
10
+
9
11
 
10
12
  /**
11
13
  * @summary Create an Event work-alike object based on the provided dictionary.
@@ -16,18 +18,62 @@ goog.provide('shaka.util.FakeEvent');
16
18
  */
17
19
  shaka.util.FakeEvent = class {
18
20
  /**
19
- * @param {string} type
20
- * @param {Object=} dict
21
+ * @param {!Event} event
22
+ * @return {!shaka.util.FakeEvent}
21
23
  */
22
- constructor(type, dict = {}) {
23
- // Take properties from dict if present.
24
- for (const key in dict) {
25
- Object.defineProperty(this, key, {
26
- value: dict[key],
24
+ static fromRealEvent(event) {
25
+ const fakeEvent = new shaka.util.FakeEvent(event.type);
26
+ for (const key in event) {
27
+ Object.defineProperty(fakeEvent, key, {
28
+ value: event[key],
27
29
  writable: true,
28
30
  enumerable: true,
29
31
  });
30
32
  }
33
+ return fakeEvent;
34
+ }
35
+
36
+ /**
37
+ * Allows us to tell the compiler that the dictionary "map" is actually a
38
+ * generic object, for backwards compatibility.
39
+ * @param {!Map.<string, Object>} dict
40
+ * @return {!Object}
41
+ * @suppress {invalidCasts}
42
+ * @private
43
+ */
44
+ static recastDictAsObject_(dict) {
45
+ goog.asserts.assert(!(dict instanceof Map), 'dict should not be a map');
46
+ return /** @type {!Object} */ (dict);
47
+ }
48
+
49
+ /**
50
+ * @param {string} type
51
+ * @param {Map.<string, Object>=} dict
52
+ */
53
+ constructor(type, dict) {
54
+ if (dict) {
55
+ if (dict instanceof Map) {
56
+ // Take properties from dict if present.
57
+ for (const key of dict.keys()) {
58
+ Object.defineProperty(this, key, {
59
+ value: dict.get(key),
60
+ writable: true,
61
+ enumerable: true,
62
+ });
63
+ }
64
+ } else {
65
+ // For backwards compatibility with external apps that may make use of
66
+ // this public constructor, this should still accept generic objects.
67
+ const obj = shaka.util.FakeEvent.recastDictAsObject_(dict);
68
+ for (const key in obj) {
69
+ Object.defineProperty(this, key, {
70
+ value: obj[key],
71
+ writable: true,
72
+ enumerable: true,
73
+ });
74
+ }
75
+ }
76
+ }
31
77
 
32
78
  // The properties below cannot be set by the dict. They are all provided
33
79
  // for compatibility with native events.
@@ -75,7 +75,10 @@ shaka.util.PeriodCombiner = class {
75
75
 
76
76
  /** @return {!Array.<shaka.extern.Stream>} */
77
77
  getTextStreams() {
78
- return this.textStreams_;
78
+ // Return a copy of the array because makeTextStreamsForClosedCaptions
79
+ // may make changes to the contents of the array. Those changes should not
80
+ // propagate back to the PeriodCombiner.
81
+ return this.textStreams_.slice();
79
82
  }
80
83
 
81
84
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shaka-player",
3
3
  "description": "DASH/EME video player library",
4
- "version": "3.0.14",
4
+ "version": "3.0.15",
5
5
  "homepage": "https://github.com/google/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
@@ -297,7 +297,8 @@ describe('CastProxy', () => {
297
297
  'timeupdate', Util.spyFunc(proxyListener));
298
298
 
299
299
  expect(proxyListener).not.toHaveBeenCalled();
300
- const fakeEvent = new FakeEvent('timeupdate', {detail: 8675309});
300
+ const fakeEvent = new FakeEvent(
301
+ 'timeupdate', (new Map()).set('detail', 8675309));
301
302
  mockVideo.on['timeupdate'](fakeEvent);
302
303
  expect(proxyListener).toHaveBeenCalledWith(jasmine.objectContaining({
303
304
  type: 'timeupdate',
@@ -315,7 +316,8 @@ describe('CastProxy', () => {
315
316
  mockSender.hasRemoteProperties.and.returnValue(true);
316
317
 
317
318
  expect(proxyListener).not.toHaveBeenCalled();
318
- const fakeEvent = new FakeEvent('timeupdate', {detail: 8675309});
319
+ const fakeEvent = new FakeEvent(
320
+ 'timeupdate', (new Map()).set('detail', 8675309));
319
321
  mockVideo.on['timeupdate'](fakeEvent);
320
322
  expect(proxyListener).not.toHaveBeenCalled();
321
323
  });
@@ -332,7 +334,8 @@ describe('CastProxy', () => {
332
334
  mockSender.hasRemoteProperties.and.returnValue(true);
333
335
 
334
336
  expect(proxyListener).not.toHaveBeenCalled();
335
- const fakeEvent = new FakeEvent('timeupdate', {detail: 8675309});
337
+ const fakeEvent = new FakeEvent(
338
+ 'timeupdate', (new Map()).set('detail', 8675309));
336
339
  mockSender.onRemoteEvent('video', fakeEvent);
337
340
  expect(proxyListener).toHaveBeenCalledWith(jasmine.objectContaining({
338
341
  type: 'timeupdate',
@@ -446,7 +449,8 @@ describe('CastProxy', () => {
446
449
  'buffering', Util.spyFunc(proxyListener));
447
450
 
448
451
  expect(proxyListener).not.toHaveBeenCalled();
449
- const fakeEvent = new FakeEvent('buffering', {detail: 8675309});
452
+ const fakeEvent = new FakeEvent(
453
+ 'buffering', (new Map()).set('detail', 8675309));
450
454
  mockPlayer.listeners['buffering'](fakeEvent);
451
455
  expect(proxyListener).toHaveBeenCalledWith(jasmine.objectContaining({
452
456
  type: 'buffering',
@@ -464,7 +468,8 @@ describe('CastProxy', () => {
464
468
  mockSender.hasRemoteProperties.and.returnValue(true);
465
469
 
466
470
  expect(proxyListener).not.toHaveBeenCalled();
467
- const fakeEvent = new FakeEvent('buffering', {detail: 8675309});
471
+ const fakeEvent = new FakeEvent(
472
+ 'buffering', (new Map()).set('detail', 8675309));
468
473
  mockPlayer.listeners['buffering'](fakeEvent);
469
474
  expect(proxyListener).not.toHaveBeenCalled();
470
475
  });
@@ -481,7 +486,8 @@ describe('CastProxy', () => {
481
486
  mockSender.hasRemoteProperties.and.returnValue(true);
482
487
 
483
488
  expect(proxyListener).not.toHaveBeenCalled();
484
- const fakeEvent = new FakeEvent('buffering', {detail: 8675309});
489
+ const fakeEvent = new FakeEvent(
490
+ 'buffering', (new Map()).set('detail', 8675309));
485
491
  mockSender.onRemoteEvent('player', fakeEvent);
486
492
  expect(proxyListener).toHaveBeenCalledWith(jasmine.objectContaining({
487
493
  type: 'buffering',
@@ -129,7 +129,7 @@ describe('CastUtils', () => {
129
129
  expect(typeof deserialized).toBe('object');
130
130
 
131
131
  // The object can be used to construct a FakeEvent.
132
- const fakeEvent = new FakeEvent(deserialized['type'], deserialized);
132
+ const fakeEvent = FakeEvent.fromRealEvent(deserialized);
133
133
 
134
134
  // The fake event has the same type and properties as the original.
135
135
  const asObj = /** @type {!Object} */ (fakeEvent);
@@ -1796,4 +1796,77 @@ describe('DashParser Manifest', () => {
1796
1796
  expect(uri).not.toContain('/p2/');
1797
1797
  }
1798
1798
  });
1799
+
1800
+ /**
1801
+ * @param {!Array.<number>} periods Start time of multiple periods
1802
+ * @return {string}
1803
+ */
1804
+ function buildManifestWithPeriodStartTime(periods) {
1805
+ const mpdTemplate = [
1806
+ `<MPD type="dynamic"`,
1807
+ 'availabilityStartTime="1970-01-01T00:00:00Z"',
1808
+ 'timeShiftBufferDepth="PT10H">',
1809
+ ' %(periods)s',
1810
+ '</MPD>',
1811
+ ].join('\n');
1812
+ const periodTemplate = (id, period, duration) => {
1813
+ return [
1814
+ ` <Period id="${id}" start="PT${period}S">`,
1815
+ ' <AdaptationSet mimeType="video/mp4" lang="en" group="1">',
1816
+ ' <SegmentTemplate startNumber="1" media="l-$Number$.mp4">',
1817
+ ' <SegmentTimeline>',
1818
+ ` <S t="0" d="${duration}" />`,
1819
+ ' </SegmentTimeline>',
1820
+ ' </SegmentTemplate>',
1821
+ ' <Representation id="1"/>',
1822
+ ' </AdaptationSet>',
1823
+ ' </Period>',
1824
+ ].join('\n');
1825
+ };
1826
+ const periodXmls = periods.map((period, i) => {
1827
+ const duration = i+1 === periods.length ? 10 : periods[i+1] - period;
1828
+ // Period start time as ID here. If we use index then there will be
1829
+ // periods with same period ID and different start time which are invalid.
1830
+ return periodTemplate(period, period, duration);
1831
+ });
1832
+ return sprintf(mpdTemplate, {
1833
+ periods: periodXmls.join('\n'),
1834
+ });
1835
+ }
1836
+
1837
+ // Bug description: Inconsistent period start time in the manifests due
1838
+ // to failover triggered in backend servers
1839
+
1840
+ // When one of the servers is down, the manifest will be served by other
1841
+ // redundant servers. The period start time might become out of sync
1842
+ // during the switch-over/recovery.
1843
+
1844
+ it('skip periods that are earlier than max period start time', async () => {
1845
+ const sources = [
1846
+ buildManifestWithPeriodStartTime([5, 15]),
1847
+ buildManifestWithPeriodStartTime([6, 15]), // simulate out-of-sync of +1s
1848
+ buildManifestWithPeriodStartTime([4, 15]), // simulate out-of-sync of -1s
1849
+ ];
1850
+ const segments = [];
1851
+
1852
+ for (const source of sources) {
1853
+ fakeNetEngine.setResponseText('dummy://foo', source);
1854
+ /** @type {shaka.extern.Manifest} */
1855
+ // eslint-disable-next-line no-await-in-loop
1856
+ const manifest = await parser.start('dummy://foo', playerInterface);
1857
+ const video = manifest.variants[0].video;
1858
+ // eslint-disable-next-line no-await-in-loop
1859
+ await video.createSegmentIndex();
1860
+ goog.asserts.assert(video.segmentIndex, 'Null segmentIndex!');
1861
+ segments.push(Array.from(video.segmentIndex));
1862
+ }
1863
+
1864
+ // Expect identical segments
1865
+ expect(segments[0].length).toBe(2);
1866
+ expect(segments[1].length).toBe(2);
1867
+ expect(segments[0][0].startTime).toBe(5);
1868
+ expect(segments[1][0].startTime).toBe(5);
1869
+ expect(segments[0][1].startTime).toBe(15);
1870
+ expect(segments[1][1].startTime).toBe(15);
1871
+ });
1799
1872
  });
@@ -776,7 +776,7 @@ describe('HlsParser', () => {
776
776
  'RESOLUTION=960x540,FRAME-RATE=120,AUDIO="aud2"\n',
777
777
  'video2\n',
778
778
  '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
779
- 'URI="audio"\n',
779
+ 'DEFAULT=YES,URI="audio"\n',
780
780
  '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud2",LANGUAGE="fr",',
781
781
  'URI="audio2"\n',
782
782
  ].join('');
@@ -794,6 +794,7 @@ describe('HlsParser', () => {
794
794
  manifest.anyTimeline();
795
795
  manifest.addPartialVariant((variant) => {
796
796
  variant.bandwidth = 200;
797
+ variant.primary = true;
797
798
  variant.addPartialStream(ContentType.VIDEO, (stream) => {
798
799
  stream.size(960, 540);
799
800
  });
@@ -803,6 +804,7 @@ describe('HlsParser', () => {
803
804
  });
804
805
  manifest.addPartialVariant((variant) => {
805
806
  variant.bandwidth = 300;
807
+ variant.primary = false;
806
808
  variant.addPartialStream(ContentType.VIDEO, (stream) => {
807
809
  stream.size(960, 540);
808
810
  });
@@ -1211,6 +1213,8 @@ describe('HlsParser', () => {
1211
1213
  // stream.
1212
1214
  const timeline = actual.presentationTimeline;
1213
1215
  expect(timeline.getDuration()).toBe(10);
1216
+ expect(timeline.getSeekRangeStart()).toBe(0);
1217
+ expect(timeline.getSeekRangeEnd()).toBe(10);
1214
1218
 
1215
1219
  expect(actual.textStreams.length).toBe(1);
1216
1220
  expect(actual.variants.length).toBe(1);
@@ -1954,6 +1954,49 @@ describe('DrmEngine', () => {
1954
1954
  [drmInfoAudio]);
1955
1955
  expect(returned).toEqual([drmInfoDesired]);
1956
1956
  });
1957
+
1958
+ it('dedupes the merged init data based on keyId matching', () => {
1959
+ const serverCert = new Uint8Array(0);
1960
+ const drmInfoVideo = {
1961
+ keySystem: 'drm.abc',
1962
+ licenseServerUri: 'http://abc.drm/license',
1963
+ distinctiveIdentifierRequired: false,
1964
+ persistentStateRequired: true,
1965
+ videoRobustness: 'really_really_ridiculously_good',
1966
+ serverCertificate: serverCert,
1967
+ serverCertificateUri: '',
1968
+ initData: [{keyId: 'v-init'}],
1969
+ keyIds: new Set(['deadbeefdeadbeefdeadbeefdeadbeef']),
1970
+ };
1971
+ const drmInfoAudio = {
1972
+ keySystem: 'drm.abc',
1973
+ licenseServerUri: undefined,
1974
+ distinctiveIdentifierRequired: true,
1975
+ persistentStateRequired: false,
1976
+ audioRobustness: 'good',
1977
+ serverCertificate: undefined,
1978
+ serverCertificateUri: '',
1979
+ initData: [{keyId: 'v-init'}, {keyId: 'a-init'}],
1980
+ keyIds: new Set(['eadbeefdeadbeefdeadbeefdeadbeefd']),
1981
+ };
1982
+ const drmInfoDesired = {
1983
+ keySystem: 'drm.abc',
1984
+ licenseServerUri: 'http://abc.drm/license',
1985
+ distinctiveIdentifierRequired: true,
1986
+ persistentStateRequired: true,
1987
+ audioRobustness: 'good',
1988
+ videoRobustness: 'really_really_ridiculously_good',
1989
+ serverCertificate: serverCert,
1990
+ initData: [{keyId: 'v-init'}, {keyId: 'a-init'}],
1991
+ keyIds: new Set([
1992
+ 'deadbeefdeadbeefdeadbeefdeadbeef',
1993
+ 'eadbeefdeadbeefdeadbeefdeadbeefd',
1994
+ ]),
1995
+ };
1996
+ const returned = shaka.media.DrmEngine.getCommonDrmInfos([drmInfoVideo],
1997
+ [drmInfoAudio]);
1998
+ expect(returned).toEqual([drmInfoDesired]);
1999
+ });
1957
2000
  }); // describe('getCommonDrmInfos')
1958
2001
 
1959
2002
  describe('configure', () => {
@@ -649,6 +649,44 @@ describe('Player', () => {
649
649
  });
650
650
  });
651
651
 
652
+ describe('seek range', () => {
653
+ // Regression test for Issue #3675.
654
+ it('has a reasonable seek range after going from live to VOD', async () => {
655
+ const netEngine = player.getNetworkingEngine();
656
+ const startTime = Date.now();
657
+ netEngine.registerRequestFilter((type, request) => {
658
+ if (type != shaka.net.NetworkingEngine.RequestType.MANIFEST) {
659
+ return;
660
+ }
661
+ // Simulate a live stream by providing different manifests over time.
662
+ const time = (Date.now() - startTime) / 1000;
663
+ const manifestNumber = Math.min(5, Math.floor(0.5 + time / 2));
664
+ request.uris = [
665
+ '/base/test/test/assets/3675/dash_' + manifestNumber + '.mpd',
666
+ ];
667
+ console.log('getting manifest', request.uris);
668
+ });
669
+
670
+ // Play the stream.
671
+ await player.load('/base/test/test/assets/3675/dash_0.mpd');
672
+ video.play();
673
+
674
+ // Wait for the stream to be over.
675
+ eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy));
676
+ /** @type {shaka.test.Waiter} */
677
+ const waiter = new shaka.test.Waiter(eventManager)
678
+ .timeoutAfter(20).failOnTimeout(true);
679
+ await waiter.waitForEnd(video);
680
+
681
+ // The stream should have transitioned to VOD by now.
682
+ expect(player.isLive()).toBe(false);
683
+
684
+ // Check that the final seek range is as expected.
685
+ const seekRange = player.seekRange();
686
+ expect(seekRange.end).toBe(14);
687
+ });
688
+ });
689
+
652
690
  describe('buffering', () => {
653
691
  const startBuffering = jasmine.objectContaining({buffering: true});
654
692
  const endBuffering = jasmine.objectContaining({buffering: false});
@@ -0,0 +1,30 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3
+ xmlns="urn:mpeg:dash:schema:mpd:2011"
4
+ xmlns:xlink="http://www.w3.org/1999/xlink"
5
+ xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
6
+ profiles="urn:mpeg:dash:profile:isoff-live:2011"
7
+ type="dynamic"
8
+ minimumUpdatePeriod="PT2S"
9
+ suggestedPresentationDelay="PT2S"
10
+ availabilityStartTime="2021-10-28T07:36:27.755Z"
11
+ publishTime="2021-10-28T07:36:29.744Z"
12
+ timeShiftBufferDepth="PT10.0S"
13
+ maxSegmentDuration="PT2.0S"
14
+ minBufferTime="PT4.0S">
15
+ <ProgramInformation>
16
+ </ProgramInformation>
17
+ <ServiceDescription id="0">
18
+ </ServiceDescription>
19
+ <Period id="0" start="PT0.0S">
20
+ <AdaptationSet id="0" contentType="video" startWithSAP="1" segmentAlignment="true" bitstreamSwitching="true" frameRate="30/1" maxWidth="1280" maxHeight="720" par="16:9">
21
+ <Representation id="0" mimeType="video/mp4" codecs="avc1.64001f" bandwidth="122012" width="1280" height="720" sar="1:1">
22
+ <SegmentTemplate timescale="15360" initialization="init-stream$RepresentationID$.m4s" media="chunk-stream$RepresentationID$-$Number%05d$.m4s" startNumber="1">
23
+ <SegmentTimeline>
24
+ <S t="0" d="30720" />
25
+ </SegmentTimeline>
26
+ </SegmentTemplate>
27
+ </Representation>
28
+ </AdaptationSet>
29
+ </Period>
30
+ </MPD>
@@ -0,0 +1,30 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3
+ xmlns="urn:mpeg:dash:schema:mpd:2011"
4
+ xmlns:xlink="http://www.w3.org/1999/xlink"
5
+ xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
6
+ profiles="urn:mpeg:dash:profile:isoff-live:2011"
7
+ type="dynamic"
8
+ minimumUpdatePeriod="PT2S"
9
+ suggestedPresentationDelay="PT2S"
10
+ availabilityStartTime="2021-10-28T07:36:27.755Z"
11
+ publishTime="2021-10-28T07:36:31.750Z"
12
+ timeShiftBufferDepth="PT10.0S"
13
+ maxSegmentDuration="PT2.0S"
14
+ minBufferTime="PT4.0S">
15
+ <ProgramInformation>
16
+ </ProgramInformation>
17
+ <ServiceDescription id="0">
18
+ </ServiceDescription>
19
+ <Period id="0" start="PT0.0S">
20
+ <AdaptationSet id="0" contentType="video" startWithSAP="1" segmentAlignment="true" bitstreamSwitching="true" frameRate="30/1" maxWidth="1280" maxHeight="720" par="16:9">
21
+ <Representation id="0" mimeType="video/mp4" codecs="avc1.64001f" bandwidth="122012" width="1280" height="720" sar="1:1">
22
+ <SegmentTemplate timescale="15360" initialization="init-stream$RepresentationID$.m4s" media="chunk-stream$RepresentationID$-$Number%05d$.m4s" startNumber="1">
23
+ <SegmentTimeline>
24
+ <S t="0" d="30720" r="1" />
25
+ </SegmentTimeline>
26
+ </SegmentTemplate>
27
+ </Representation>
28
+ </AdaptationSet>
29
+ </Period>
30
+ </MPD>