shaka-player 4.2.3 → 4.2.5

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/demo/index.html +1 -1
  5. package/dist/demo.compiled.debug.js +64 -64
  6. package/dist/demo.compiled.debug.map +2 -2
  7. package/dist/demo.compiled.js +64 -64
  8. package/dist/demo.compiled.map +2 -2
  9. package/dist/deps.js +5 -4
  10. package/dist/locales.js +32 -32
  11. package/dist/receiver.compiled.debug.js +12 -12
  12. package/dist/receiver.compiled.debug.map +2 -2
  13. package/dist/receiver.compiled.js +12 -12
  14. package/dist/receiver.compiled.map +2 -2
  15. package/dist/shaka-player.compiled.d.ts +15 -1
  16. package/dist/shaka-player.compiled.debug.d.ts +15 -1
  17. package/dist/shaka-player.compiled.debug.externs.js +95 -80
  18. package/dist/shaka-player.compiled.debug.js +583 -573
  19. package/dist/shaka-player.compiled.debug.map +4 -4
  20. package/dist/shaka-player.compiled.externs.js +95 -80
  21. package/dist/shaka-player.compiled.js +823 -820
  22. package/dist/shaka-player.compiled.map +4 -4
  23. package/dist/shaka-player.ui.d.ts +15 -1
  24. package/dist/shaka-player.ui.debug.d.ts +15 -1
  25. package/dist/shaka-player.ui.debug.externs.js +95 -80
  26. package/dist/shaka-player.ui.debug.js +612 -602
  27. package/dist/shaka-player.ui.debug.map +4 -4
  28. package/dist/shaka-player.ui.externs.js +95 -80
  29. package/dist/shaka-player.ui.js +963 -960
  30. package/dist/shaka-player.ui.map +4 -4
  31. package/docs/tutorials/ui.md +7 -7
  32. package/docs/tutorials/welcome.md +2 -2
  33. package/externs/mux.js +3 -0
  34. package/externs/shaka/player.js +2 -1
  35. package/lib/cast/cast_proxy.js +2 -1
  36. package/lib/cast/cast_receiver.js +19 -5
  37. package/lib/cast/cast_sender.js +2 -1
  38. package/lib/cast/cast_utils.js +16 -6
  39. package/lib/cea/cea708_window.js +2 -2
  40. package/lib/dash/segment_template.js +1 -1
  41. package/lib/hls/hls_parser.js +21 -4
  42. package/lib/media/content_workarounds.js +12 -0
  43. package/lib/media/media_source_engine.js +6 -0
  44. package/lib/media/playhead.js +23 -1
  45. package/lib/media/streaming_engine.js +3 -12
  46. package/lib/media/transmuxer.js +6 -0
  47. package/lib/player.js +24 -11
  48. package/lib/polyfill/abort_controller.js +143 -0
  49. package/lib/polyfill/media_capabilities.js +59 -9
  50. package/lib/text/ttml_text_parser.js +32 -7
  51. package/lib/text/ui_text_displayer.js +22 -5
  52. package/lib/util/manifest_parser_utils.js +4 -0
  53. package/lib/util/platform.js +9 -1
  54. package/lib/util/player_configuration.js +10 -3
  55. package/lib/util/stream_utils.js +30 -10
  56. package/package.json +1 -1
  57. package/roadmap.md +24 -6
  58. package/shaka-player.uncompiled.js +1 -0
  59. package/test/cast/cast_receiver_integration.js +97 -16
  60. package/test/cast/cast_receiver_unit.js +17 -12
  61. package/test/cast/cast_utils_unit.js +1 -0
  62. package/test/dash/dash_parser_manifest_unit.js +1 -0
  63. package/test/hls/hls_parser_unit.js +1 -0
  64. package/test/media/drm_engine_unit.js +11 -1
  65. package/test/media/media_source_engine_integration.js +108 -1
  66. package/test/media/playhead_unit.js +41 -0
  67. package/test/media/streaming_engine_integration.js +13 -0
  68. package/test/media/streaming_engine_unit.js +36 -23
  69. package/test/player_unit.js +87 -0
  70. package/test/polyfill/media_capabilities_unit.js +261 -0
  71. package/test/test/util/simple_fakes.js +3 -0
  72. package/test/test/util/stream_generator.js +212 -8
  73. package/test/test/util/test_scheme.js +3 -2
  74. package/test/test/util/util.js +3 -2
  75. package/test/text/mp4_ttml_parser_unit.js +6 -4
  76. package/test/text/ttml_text_parser_unit.js +119 -86
  77. package/test/text/ui_text_displayer_unit.js +76 -0
  78. package/test/ui/text_displayer_layout_unit.js +14 -0
  79. package/test/util/stream_utils_unit.js +18 -0
  80. package/test/util/xml_utils_unit.js +6 -1
  81. package/ui/controls.js +49 -37
  82. package/ui/settings_menu.js +11 -2
@@ -34,6 +34,8 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
34
34
 
35
35
  /** @type {shaka.util.PublicPromise} */
36
36
  let messageWaitPromise;
37
+ /** @type {Array.<string>} */
38
+ let pendingMessages = null;
37
39
 
38
40
  /** @type {!Array.<function()>} */
39
41
  let toRestore;
@@ -91,6 +93,9 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
91
93
  toRestore = [];
92
94
  pendingWaitWrapperCalls = 0;
93
95
 
96
+ messageWaitPromise = null;
97
+ pendingMessages = null;
98
+
94
99
  fakeInitState = {
95
100
  player: {
96
101
  configure: {},
@@ -118,6 +123,12 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
118
123
  player = null;
119
124
  video = null;
120
125
  receiver = null;
126
+
127
+ if (messageWaitPromise) {
128
+ messageWaitPromise.resolve([]);
129
+ }
130
+ messageWaitPromise = null;
131
+ pendingMessages = null;
121
132
  });
122
133
 
123
134
  afterAll(() => {
@@ -128,6 +139,66 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
128
139
  }
129
140
  });
130
141
 
142
+ describe('without drm', () => {
143
+ it('sends reasonably-sized updates', async () => {
144
+ // Use an unencrypted asset.
145
+ fakeInitState.manifest = 'test:sintel';
146
+
147
+ const p = waitForLoadedData();
148
+
149
+ // Start the process of loading by sending a fake init message.
150
+ fakeConnectedSenders(1);
151
+ fakeIncomingMessage({
152
+ type: 'init',
153
+ initState: fakeInitState,
154
+ appData: {},
155
+ }, mockShakaMessageBus);
156
+
157
+ await p;
158
+ // Wait for an update message.
159
+ const messages = await waitForUpdateMessages();
160
+ for (const message of messages) {
161
+ // Check that the update message is of a reasonable size. From previous
162
+ // testing we found that the socket would silently reject data that got
163
+ // too big. 6KB is safely below the limit.
164
+ expect(message.length).toBeLessThan(6000);
165
+ }
166
+ });
167
+
168
+ it('has reasonable average message size', async () => {
169
+ // Use an unencrypted asset.
170
+ fakeInitState.manifest = 'test:sintel';
171
+
172
+ const p = waitForLoadedData();
173
+
174
+ // Start the process of loading by sending a fake init message.
175
+ fakeConnectedSenders(1);
176
+ fakeIncomingMessage({
177
+ type: 'init',
178
+ initState: fakeInitState,
179
+ appData: {},
180
+ }, mockShakaMessageBus);
181
+
182
+ await p;
183
+
184
+ // Collect messages over 50 update cycles, and average their length.
185
+ // Not all properties are passed along on every update message, so
186
+ // the average length is expected to be lower than the length of the first
187
+ // update message.
188
+ let totalLength = 0;
189
+ let totalMessages = 0;
190
+ for (let i = 0; i < 50; i++) {
191
+ // eslint-disable-next-line no-await-in-loop
192
+ const messages = await waitForUpdateMessages();
193
+ for (const message of messages) {
194
+ totalLength += message.length;
195
+ totalMessages += 1;
196
+ }
197
+ }
198
+ expect(totalLength / totalMessages).toBeLessThan(2000);
199
+ });
200
+ });
201
+
131
202
  filterDescribe('with drm', () => support['com.widevine.alpha'], () => {
132
203
  drmIt('sends reasonably-sized updates', async () => {
133
204
  // Use an encrypted asset, to make sure DRM info doesn't balloon the size.
@@ -150,11 +221,13 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
150
221
 
151
222
  await p;
152
223
  // Wait for an update message.
153
- const message = await waitForUpdateMessage();
154
- // Check that the update message is of a reasonable size. From previous
155
- // testing we found that the socket would silently reject data that got
156
- // too big. 6KB is safely below the limit.
157
- expect(message.length).toBeLessThan(7 * 1024);
224
+ const messages = await waitForUpdateMessages();
225
+ for (const message of messages) {
226
+ // Check that the update message is of a reasonable size. From previous
227
+ // testing we found that the socket would silently reject data that got
228
+ // too big. 6KB is safely below the limit.
229
+ expect(message.length).toBeLessThan(6000);
230
+ }
158
231
  });
159
232
 
160
233
  drmIt('has reasonable average message size', async () => {
@@ -177,17 +250,22 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
177
250
  }, mockShakaMessageBus);
178
251
 
179
252
  await p;
180
- // Collect 50 update messages, and average their length.
253
+
254
+ // Collect messages over 50 update cycles, and average their length.
181
255
  // Not all properties are passed along on every update message, so
182
256
  // the average length is expected to be lower than the length of the first
183
257
  // update message.
184
258
  let totalLength = 0;
259
+ let totalMessages = 0;
185
260
  for (let i = 0; i < 50; i++) {
186
261
  // eslint-disable-next-line no-await-in-loop
187
- const message = await waitForUpdateMessage();
188
- totalLength += message.length;
262
+ const messages = await waitForUpdateMessages();
263
+ for (const message of messages) {
264
+ totalLength += message.length;
265
+ totalMessages += 1;
266
+ }
189
267
  }
190
- expect(totalLength / 50).toBeLessThan(3000);
268
+ expect(totalLength / totalMessages).toBeLessThan(2000);
191
269
  });
192
270
  });
193
271
 
@@ -227,12 +305,12 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
227
305
  expect(pendingWaitWrapperCalls).toBe(0);
228
306
 
229
307
  // Wait for a final update message before proceeding.
230
- await waitForUpdateMessage();
308
+ await waitForUpdateMessages();
231
309
  });
232
310
 
233
311
  /**
234
312
  * Creates a wrapper around a method on a given prototype, which makes it
235
- * wait on waitForUpdateMessage before returning, and registers that wrapper
313
+ * wait on waitForUpdateMessages before returning, and registers that wrapper
236
314
  * to be uninstalled afterwards.
237
315
  * The replaced method is expected to be a method that returns a promise.
238
316
  * @param {!Object} prototype
@@ -249,7 +327,7 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
249
327
  'Waiting for update message before calling ' +
250
328
  name + '.' + methodName + '...');
251
329
  const originalArguments = Array.from(arguments);
252
- await waitForUpdateMessage();
330
+ await waitForUpdateMessages();
253
331
  // eslint-disable-next-line no-restricted-syntax
254
332
  return original.apply(this, originalArguments);
255
333
  };
@@ -265,7 +343,8 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
265
343
  });
266
344
  }
267
345
 
268
- function waitForUpdateMessage() {
346
+ function waitForUpdateMessages() {
347
+ pendingMessages = [];
269
348
  messageWaitPromise = new shaka.util.PublicPromise();
270
349
  return messageWaitPromise;
271
350
  }
@@ -316,10 +395,12 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
316
395
  bus.messages.push(CastUtils.deserialize(message));
317
396
  // Check to see if it's an update message.
318
397
  const parsed = CastUtils.deserialize(message);
319
- if (parsed.type == 'update' && messageWaitPromise) {
398
+ if (parsed.type == 'update' && pendingMessages) {
320
399
  shaka.log.debug('Received update message. Proceeding...');
321
- messageWaitPromise.resolve(message);
322
- messageWaitPromise = null;
400
+ // The code waiting on this Promise will get an array of all of the
401
+ // messages processed within this tick.
402
+ pendingMessages.push(message);
403
+ messageWaitPromise.resolve(pendingMessages);
323
404
  }
324
405
  });
325
406
  const channel = {
@@ -245,18 +245,20 @@ filterDescribe('CastReceiver', castReceiverSupport, () => {
245
245
  const fakeEvent = {type: 'timeupdate'};
246
246
  mockVideo.on['timeupdate'](fakeEvent);
247
247
 
248
- // There are now "update" and "event" messages, in that order.
249
- expect(mockShakaMessageBus.messages).toEqual([
250
- {
251
- type: 'update',
252
- update: jasmine.any(Object),
253
- },
254
- {
255
- type: 'event',
256
- targetName: 'video',
257
- event: jasmine.objectContaining(fakeEvent),
258
- },
259
- ]);
248
+ // There are now some number of "update" and "event" messages, in that
249
+ // order.
250
+ expect(mockShakaMessageBus.messages).toContain({
251
+ type: 'update',
252
+ update: jasmine.any(Object),
253
+ });
254
+ expect(mockShakaMessageBus.messages).toContain({
255
+ type: 'event',
256
+ targetName: 'video',
257
+ event: jasmine.objectContaining(fakeEvent),
258
+ });
259
+ const eventIndex = mockShakaMessageBus.messages.findIndex(
260
+ (message) => message.type == 'event');
261
+ expect(eventIndex).toBe(mockShakaMessageBus.messages.length - 1);
260
262
  });
261
263
  });
262
264
 
@@ -1105,6 +1107,9 @@ filterDescribe('CastReceiver', castReceiverSupport, () => {
1105
1107
  for (const name in CastUtils.PlayerGetterMethods) {
1106
1108
  player[name] = jasmine.createSpy(name);
1107
1109
  }
1110
+ for (const name in CastUtils.LargePlayerGetterMethods) {
1111
+ player[name] = jasmine.createSpy(name);
1112
+ }
1108
1113
  for (const name in CastUtils.PlayerGetterMethodsThatRequireLive) {
1109
1114
  player[name] = jasmine.createSpy(name);
1110
1115
  }
@@ -34,6 +34,7 @@ describe('CastUtils', () => {
34
34
  const castMembers = CastUtils.PlayerVoidMethods
35
35
  .concat(CastUtils.PlayerPromiseMethods)
36
36
  .concat(Object.keys(CastUtils.PlayerGetterMethods))
37
+ .concat(Object.keys(CastUtils.LargePlayerGetterMethods))
37
38
  .concat(Object.keys(CastUtils.PlayerGetterMethodsThatRequireLive));
38
39
  // eslint-disable-next-line no-restricted-syntax
39
40
  const allPlayerMembers = Object.getOwnPropertyNames(shaka.Player.prototype);
@@ -2106,6 +2106,7 @@ describe('DashParser Manifest', () => {
2106
2106
  /** @type {shaka.extern.Manifest} */
2107
2107
  const manifest = await parser.start('dummy://foo', playerInterface);
2108
2108
  expect(manifest.imageStreams.length).toBe(1);
2109
+ expect(manifest.presentationTimeline.getMaxSegmentDuration()).toBe(1);
2109
2110
  const imageStream = manifest.imageStreams[0];
2110
2111
  expect(imageStream.width).toBe(1024);
2111
2112
  expect(imageStream.height).toBe(1152);
@@ -38,6 +38,7 @@ describe('HlsParser', () => {
38
38
 
39
39
  afterEach(() => {
40
40
  shaka.log.alwaysWarn = originalAlwaysWarn;
41
+ parser.stop();
41
42
  });
42
43
 
43
44
  beforeEach(() => {
@@ -120,11 +120,17 @@ describe('DrmEngine', () => {
120
120
  };
121
121
 
122
122
  drmEngine = new shaka.media.DrmEngine(playerInterface);
123
+
123
124
  config = shaka.util.PlayerConfiguration.createDefault().drm;
124
125
  config.servers = {
125
126
  'drm.abc': 'http://abc.drm/license',
126
127
  'drm.def': 'http://def.drm/license',
127
128
  };
129
+ // Some platforms, such as Xbox, default to parseInbandPssh: true, which
130
+ // ignores encrypted events. So set it explicitly to false, and let
131
+ // individual tests set it to true where relevant.
132
+ config.parseInbandPsshEnabled = false;
133
+
128
134
  drmEngine.configure(config);
129
135
  });
130
136
 
@@ -2548,7 +2554,11 @@ describe('DrmEngine', () => {
2548
2554
  */
2549
2555
  async function sendEncryptedEvent(
2550
2556
  initDataType = 'cenc', initData = new Uint8Array(1), keyId = null) {
2551
- mockVideo.on['encrypted']({initDataType, initData, keyId});
2557
+ // For some platforms, such as Xbox, where parseInbandPssh defaults to
2558
+ // true, this listener may never be set.
2559
+ if (mockVideo.on['encrypted']) {
2560
+ mockVideo.on['encrypted']({initDataType, initData, keyId});
2561
+ }
2552
2562
 
2553
2563
  await Util.shortDelay();
2554
2564
  }
@@ -6,6 +6,8 @@
6
6
 
7
7
  describe('MediaSourceEngine', () => {
8
8
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
9
+ const Cue = shaka.text.Cue;
10
+ const Util = shaka.test.Util;
9
11
  const presentationDuration = 840;
10
12
 
11
13
  /** @type {!HTMLVideoElement} */
@@ -18,6 +20,64 @@ describe('MediaSourceEngine', () => {
18
20
  let metadata;
19
21
  // TODO: add text streams to MSE integration tests
20
22
 
23
+ const mp4CeaCue0 = jasmine.objectContaining({
24
+ startTime: Util.closeTo(0.067, 0.001),
25
+ endTime: Util.closeTo(1, 0.001),
26
+ textAlign: Cue.textAlign.CENTER,
27
+ nestedCues: [
28
+ jasmine.objectContaining({
29
+ startTime: Util.closeTo(0.067, 0.001),
30
+ endTime: Util.closeTo(1, 0.001),
31
+ payload: 'eng:⠀00:00:00:00',
32
+ textAlign: Cue.textAlign.CENTER,
33
+ }),
34
+ ],
35
+ });
36
+
37
+ const tsCeaCue0 = jasmine.objectContaining({
38
+ startTime: Util.closeTo(0.767, 0.001),
39
+ endTime: Util.closeTo(4.972, 0.001),
40
+ textAlign: Cue.textAlign.CENTER,
41
+ payload: 'These are 608 captions\n(top left)',
42
+ });
43
+
44
+ const tsCeaCue1 = jasmine.objectContaining({
45
+ startTime: Util.closeTo(5.305, 0.001),
46
+ endTime: Util.closeTo(11.979, 0.001),
47
+ textAlign: Cue.textAlign.CENTER,
48
+ payload: 'These are 608 captions\n(middle)',
49
+ });
50
+
51
+ const tsCeaCue2 = jasmine.objectContaining({
52
+ startTime: Util.closeTo(12.312, 0.001),
53
+ endTime: Util.closeTo(19.319, 0.001),
54
+ textAlign: Cue.textAlign.CENTER,
55
+ payload: 'These are 608 captions\n(bottom left)',
56
+ });
57
+
58
+ // The same segments as above, but offset by 40 seconds (yes, 40), which is
59
+ // also 2 segments.
60
+ const tsCeaCue3 = jasmine.objectContaining({
61
+ startTime: Util.closeTo(40.767, 0.001),
62
+ endTime: Util.closeTo(44.972, 0.001),
63
+ textAlign: Cue.textAlign.CENTER,
64
+ payload: 'These are 608 captions\n(top left)',
65
+ });
66
+
67
+ const tsCeaCue4 = jasmine.objectContaining({
68
+ startTime: Util.closeTo(45.305, 0.001),
69
+ endTime: Util.closeTo(51.979, 0.001),
70
+ textAlign: Cue.textAlign.CENTER,
71
+ payload: 'These are 608 captions\n(middle)',
72
+ });
73
+
74
+ const tsCeaCue5 = jasmine.objectContaining({
75
+ startTime: Util.closeTo(52.312, 0.001),
76
+ endTime: Util.closeTo(59.319, 0.001),
77
+ textAlign: Cue.textAlign.CENTER,
78
+ payload: 'These are 608 captions\n(bottom left)',
79
+ });
80
+
21
81
  /**
22
82
  * We use a fake text displayer so that we can check if CEA text is being
23
83
  * passed through the system correctly.
@@ -70,6 +130,18 @@ describe('MediaSourceEngine', () => {
70
130
  type, segment, reference, /* hasClosedCaptions= */ false);
71
131
  }
72
132
 
133
+ function appendWithSeek(type, segmentNumber) {
134
+ const segment = generators[type]
135
+ .getSegment(segmentNumber, Date.now() / 1000);
136
+ const reference = dummyReference(type, segmentNumber);
137
+ return mediaSourceEngine.appendBuffer(
138
+ type,
139
+ segment,
140
+ reference,
141
+ /* hasClosedCaptions= */ false,
142
+ /* seeked= */ true);
143
+ }
144
+
73
145
  function appendInitWithClosedCaptions(type) {
74
146
  const segment = generators[type].getInitSegment(Date.now() / 1000);
75
147
  const reference = null;
@@ -375,9 +447,43 @@ describe('MediaSourceEngine', () => {
375
447
  // platforms with native TS support.
376
448
  await mediaSourceEngine.init(initObject, /* forceTransmuxTS= */ true);
377
449
  mediaSourceEngine.setSelectedClosedCaptionId('CC1');
450
+
378
451
  await append(ContentType.VIDEO, 0);
379
452
 
380
453
  expect(textDisplayer.appendSpy).toHaveBeenCalledTimes(3);
454
+
455
+ expect(textDisplayer.appendSpy).toHaveBeenCalledWith([tsCeaCue0]);
456
+ expect(textDisplayer.appendSpy).toHaveBeenCalledWith([tsCeaCue1]);
457
+ expect(textDisplayer.appendSpy).toHaveBeenCalledWith([tsCeaCue2]);
458
+ });
459
+
460
+ it('extracts CEA-708 captions from previous segment from hls', async () => {
461
+ // Load TS file with CEA-708 captions.
462
+ metadata = shaka.test.TestScheme.DATA['cea-708_ts'];
463
+ generators = shaka.test.TestScheme.GENERATORS['cea-708_ts'];
464
+
465
+ const initObject = new Map();
466
+ initObject.set(ContentType.VIDEO, getFakeStream(metadata.video));
467
+ initObject.set(ContentType.TEXT, getFakeStream(metadata.text));
468
+ // Call with forceTransmux = true, so that it will transmux even on
469
+ // platforms with native TS support.
470
+ await mediaSourceEngine.init(initObject, /* forceTransmux= */ true);
471
+ mediaSourceEngine.setSelectedClosedCaptionId('CC1');
472
+
473
+ await append(ContentType.VIDEO, 2);
474
+
475
+ expect(textDisplayer.appendSpy).toHaveBeenCalledTimes(3);
476
+ expect(textDisplayer.appendSpy).toHaveBeenCalledWith([tsCeaCue3]);
477
+ expect(textDisplayer.appendSpy).toHaveBeenCalledWith([tsCeaCue4]);
478
+ expect(textDisplayer.appendSpy).toHaveBeenCalledWith([tsCeaCue5]);
479
+
480
+ textDisplayer.appendSpy.calls.reset();
481
+ await appendWithSeek(ContentType.VIDEO, 0);
482
+
483
+ expect(textDisplayer.appendSpy).toHaveBeenCalledTimes(3);
484
+ expect(textDisplayer.appendSpy).toHaveBeenCalledWith([tsCeaCue0]);
485
+ expect(textDisplayer.appendSpy).toHaveBeenCalledWith([tsCeaCue1]);
486
+ expect(textDisplayer.appendSpy).toHaveBeenCalledWith([tsCeaCue2]);
381
487
  });
382
488
 
383
489
  it('buffers partial TS video segments in sequence mode', async () => {
@@ -430,6 +536,7 @@ describe('MediaSourceEngine', () => {
430
536
  mediaSourceEngine.setSelectedClosedCaptionId('CC1');
431
537
  await appendWithClosedCaptions(ContentType.VIDEO, 0);
432
538
 
433
- expect(textDisplayer.appendSpy).toHaveBeenCalled();
539
+ expect(textDisplayer.appendSpy).toHaveBeenCalledTimes(1);
540
+ expect(textDisplayer.appendSpy).toHaveBeenCalledWith([mp4CeaCue0]);
434
541
  });
435
542
  });
@@ -161,6 +161,8 @@ describe('Playhead', () => {
161
161
  Util.spyFunc(onSeek),
162
162
  Util.spyFunc(onEvent));
163
163
 
164
+ playhead.ready();
165
+
164
166
  expect(video.currentTime).toBe(5);
165
167
  expect(playhead.getTime()).toBe(5);
166
168
 
@@ -182,6 +184,8 @@ describe('Playhead', () => {
182
184
  Util.spyFunc(onSeek),
183
185
  Util.spyFunc(onEvent));
184
186
 
187
+ playhead.ready();
188
+
185
189
  expect(video.addEventListener).toHaveBeenCalledWith(
186
190
  'loadedmetadata', jasmine.any(Function), jasmine.anything());
187
191
  expect(video.addEventListener).not.toHaveBeenCalledWith(
@@ -223,6 +227,8 @@ describe('Playhead', () => {
223
227
  Util.spyFunc(onSeek),
224
228
  Util.spyFunc(onEvent));
225
229
 
230
+ playhead.ready();
231
+
226
232
  video.on['seeking']();
227
233
  expect(playhead.getTime()).toBe(5);
228
234
  expect(video.currentTime).toBe(5);
@@ -264,6 +270,8 @@ describe('Playhead', () => {
264
270
  Util.spyFunc(onSeek),
265
271
  Util.spyFunc(onEvent));
266
272
 
273
+ playhead.ready();
274
+
267
275
  expect(playhead.getTime()).toBe(59); // duration - durationBackoff
268
276
  expect(video.currentTime).toBe(59); // duration - durationBackoff
269
277
  });
@@ -302,6 +310,8 @@ describe('Playhead', () => {
302
310
  Util.spyFunc(onSeek),
303
311
  Util.spyFunc(onEvent));
304
312
 
313
+ playhead.ready();
314
+
305
315
  expect(playhead.getTime()).toBe(30);
306
316
  });
307
317
 
@@ -314,6 +324,8 @@ describe('Playhead', () => {
314
324
  Util.spyFunc(onSeek),
315
325
  Util.spyFunc(onEvent));
316
326
 
327
+ playhead.ready();
328
+
317
329
  expect(video.addEventListener).toHaveBeenCalledWith(
318
330
  'loadedmetadata', jasmine.any(Function), jasmine.anything());
319
331
 
@@ -347,6 +359,8 @@ describe('Playhead', () => {
347
359
  Util.spyFunc(onSeek),
348
360
  Util.spyFunc(onEvent));
349
361
 
362
+ playhead.ready();
363
+
350
364
  expect(video.addEventListener).toHaveBeenCalledWith(
351
365
  'loadedmetadata', jasmine.any(Function), jasmine.anything());
352
366
 
@@ -380,6 +394,8 @@ describe('Playhead', () => {
380
394
  Util.spyFunc(onSeek),
381
395
  Util.spyFunc(onEvent));
382
396
 
397
+ playhead.ready();
398
+
383
399
  // This has to periodically increment the mock date to allow the onSeeking_
384
400
  // handler to seek, if appropriate.
385
401
 
@@ -652,6 +668,8 @@ describe('Playhead', () => {
652
668
  expect(currentTime).toBe(1000);
653
669
  seekCount = 0;
654
670
 
671
+ playhead.ready();
672
+
655
673
  // The availability window slips ahead.
656
674
  timeline.getSeekRangeStart.and.returnValue(1030);
657
675
  timeline.getSeekRangeEnd.and.returnValue(1030);
@@ -696,6 +714,8 @@ describe('Playhead', () => {
696
714
  Util.spyFunc(onSeek),
697
715
  Util.spyFunc(onEvent));
698
716
 
717
+ playhead.ready();
718
+
699
719
  video.on['seeking']();
700
720
  expect(video.currentTime).toBe(5);
701
721
  expect(playhead.getTime()).toBe(5);
@@ -728,6 +748,8 @@ describe('Playhead', () => {
728
748
  Util.spyFunc(onSeek),
729
749
  Util.spyFunc(onEvent));
730
750
 
751
+ playhead.ready();
752
+
731
753
  video.on['seeking']();
732
754
  expect(video.currentTime).toBe(5);
733
755
  expect(playhead.getTime()).toBe(5);
@@ -765,6 +787,9 @@ describe('Playhead', () => {
765
787
 
766
788
  video.currentTime = 0;
767
789
  video.seeking = true;
790
+
791
+ playhead.ready();
792
+
768
793
  // "video.seeking" stays true until the buffered range intersects with
769
794
  // "video.currentTime". Playhead should correct anyway.
770
795
  video.on['seeking']();
@@ -809,6 +834,8 @@ describe('Playhead', () => {
809
834
  Util.spyFunc(onSeek),
810
835
  Util.spyFunc(onEvent));
811
836
 
837
+ playhead.ready();
838
+
812
839
  /**
813
840
  * Prevent retries on the initial start time seek. This will ensure that
814
841
  * only one call is made for the initial seek, and that any additional calls
@@ -933,6 +960,8 @@ describe('Playhead', () => {
933
960
  Util.spyFunc(onSeek),
934
961
  Util.spyFunc(onEvent));
935
962
 
963
+ playhead.ready();
964
+
936
965
  jasmine.clock().tick(500);
937
966
  for (let time = data.start; time < data.waitingAt; time++) {
938
967
  // We don't want to run tick() for 1 second because it will trigger
@@ -1142,6 +1171,8 @@ describe('Playhead', () => {
1142
1171
  Util.spyFunc(onSeek),
1143
1172
  Util.spyFunc(onEvent));
1144
1173
 
1174
+ playhead.ready();
1175
+
1145
1176
  jasmine.clock().tick(500);
1146
1177
  expect(onEvent).not.toHaveBeenCalled();
1147
1178
 
@@ -1190,6 +1221,8 @@ describe('Playhead', () => {
1190
1221
  Util.spyFunc(onSeek),
1191
1222
  Util.spyFunc(onEvent));
1192
1223
 
1224
+ playhead.ready();
1225
+
1193
1226
  playhead.notifyOfBufferingChange();
1194
1227
  jasmine.clock().tick(500);
1195
1228
 
@@ -1212,6 +1245,8 @@ describe('Playhead', () => {
1212
1245
  Util.spyFunc(onSeek),
1213
1246
  Util.spyFunc(onEvent));
1214
1247
 
1248
+ playhead.ready();
1249
+
1215
1250
  playhead.notifyOfBufferingChange();
1216
1251
  jasmine.clock().tick(500);
1217
1252
 
@@ -1236,6 +1271,8 @@ describe('Playhead', () => {
1236
1271
  Util.spyFunc(onSeek),
1237
1272
  Util.spyFunc(onEvent));
1238
1273
 
1274
+ playhead.ready();
1275
+
1239
1276
  playhead.notifyOfBufferingChange();
1240
1277
  jasmine.clock().tick(500);
1241
1278
 
@@ -1260,6 +1297,8 @@ describe('Playhead', () => {
1260
1297
  Util.spyFunc(onSeek),
1261
1298
  Util.spyFunc(onEvent));
1262
1299
 
1300
+ playhead.ready();
1301
+
1263
1302
  playhead.notifyOfBufferingChange();
1264
1303
  jasmine.clock().tick(500);
1265
1304
 
@@ -1287,6 +1326,8 @@ describe('Playhead', () => {
1287
1326
  Util.spyFunc(onSeek),
1288
1327
  Util.spyFunc(onEvent));
1289
1328
 
1329
+ playhead.ready();
1330
+
1290
1331
  jasmine.clock().tick(500);
1291
1332
  expect(onEvent).not.toHaveBeenCalled();
1292
1333
 
@@ -154,6 +154,19 @@ describe('StreamingEngine', () => {
154
154
  /* presentationDuration= */ Infinity);
155
155
  setupPlayhead();
156
156
 
157
+ // Retry on failure for live streams.
158
+ config.failureCallback = () => streamingEngine.retry(0.1);
159
+
160
+ // Ignore 404 errors in live stream tests.
161
+ onError.and.callFake((error) => {
162
+ if (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS &&
163
+ error.data[1] == 404) {
164
+ // 404 error
165
+ } else {
166
+ fail(error);
167
+ }
168
+ });
169
+
157
170
  createStreamingEngine();
158
171
  }
159
172