shaka-player 3.3.11 → 3.3.13

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 (58) hide show
  1. package/.github/workflows/build-and-test.yaml +11 -6
  2. package/.github/workflows/compute-incremental-coverage.py +14 -3
  3. package/.github/workflows/demo-version-index.yaml +9 -1
  4. package/.github/workflows/nightly-demo.yaml +1 -1
  5. package/.github/workflows/release-please.yaml +4 -4
  6. package/.github/workflows/report-incremental-coverage.yaml +1 -1
  7. package/.github/workflows/selenium-lab-tests.yaml +8 -6
  8. package/CHANGELOG.md +26 -0
  9. package/dist/deps.js +5 -5
  10. package/dist/locales.js +31 -31
  11. package/dist/shaka-player.compiled.d.ts +2 -1
  12. package/dist/shaka-player.compiled.debug.d.ts +2 -1
  13. package/dist/shaka-player.compiled.debug.externs.js +3 -1
  14. package/dist/shaka-player.compiled.debug.js +104 -101
  15. package/dist/shaka-player.compiled.debug.map +4 -4
  16. package/dist/shaka-player.compiled.externs.js +3 -1
  17. package/dist/shaka-player.compiled.js +725 -722
  18. package/dist/shaka-player.compiled.map +4 -4
  19. package/dist/shaka-player.ui.d.ts +2 -1
  20. package/dist/shaka-player.ui.debug.d.ts +2 -1
  21. package/dist/shaka-player.ui.debug.externs.js +3 -1
  22. package/dist/shaka-player.ui.debug.js +122 -119
  23. package/dist/shaka-player.ui.debug.map +4 -4
  24. package/dist/shaka-player.ui.externs.js +3 -1
  25. package/dist/shaka-player.ui.js +843 -840
  26. package/dist/shaka-player.ui.map +4 -4
  27. package/docs/tutorials/welcome.md +2 -2
  28. package/lib/ads/client_side_ad_manager.js +5 -2
  29. package/lib/cast/cast_proxy.js +2 -1
  30. package/lib/cast/cast_receiver.js +19 -5
  31. package/lib/cast/cast_sender.js +2 -1
  32. package/lib/cast/cast_utils.js +16 -6
  33. package/lib/hls/hls_parser.js +9 -2
  34. package/lib/media/drm_engine.js +4 -2
  35. package/lib/media/playhead.js +23 -1
  36. package/lib/media/streaming_engine.js +3 -12
  37. package/lib/offline/indexeddb/storage_mechanism.js +24 -0
  38. package/lib/player.js +24 -11
  39. package/lib/polyfill/media_capabilities.js +1 -0
  40. package/lib/text/ttml_text_parser.js +69 -93
  41. package/lib/util/error.js +7 -0
  42. package/lib/util/manifest_parser_utils.js +4 -0
  43. package/lib/util/periods.js +9 -9
  44. package/lib/util/xml_utils.js +45 -6
  45. package/package.json +1 -1
  46. package/test/cast/cast_receiver_integration.js +97 -16
  47. package/test/cast/cast_receiver_unit.js +17 -12
  48. package/test/cast/cast_utils_unit.js +1 -0
  49. package/test/media/playhead_unit.js +41 -0
  50. package/test/media/streaming_engine_integration.js +13 -0
  51. package/test/media/streaming_engine_unit.js +23 -8
  52. package/test/offline/storage_integration.js +30 -0
  53. package/test/player_unit.js +87 -0
  54. package/test/test/util/simple_fakes.js +3 -0
  55. package/test/util/periods_unit.js +149 -11
  56. package/test/util/xml_utils_unit.js +64 -0
  57. package/ui/controls.js +4 -1
  58. package/ui/settings_menu.js +11 -2
@@ -1034,6 +1034,36 @@ filterDescribe('Storage', storageSupport, () => {
1034
1034
  }
1035
1035
  });
1036
1036
 
1037
+ /**
1038
+ * In some situations, indexedDB.open() can just hang, and call neither the
1039
+ * 'success' nor the 'error' callbacks.
1040
+ * I'm not sure what causes it, but it seems to happen consistently between
1041
+ * reloads when it does so it might be a browser-based issue.
1042
+ * In that case, we should time out with an error, instead of also hanging.
1043
+ */
1044
+ it('throws an error if indexedDB open times out', async () => {
1045
+ const oldOpen = window.indexedDB.open;
1046
+ window.indexedDB.open = () => {
1047
+ // Just return a dummy object.
1048
+ return /** @type {!IDBOpenDBRequest} */ ({
1049
+ onsuccess: (event) => {},
1050
+ onerror: (error) => {},
1051
+ });
1052
+ };
1053
+
1054
+ /** @type {!shaka.offline.StorageMuxer} */
1055
+ const muxer = new shaka.offline.StorageMuxer();
1056
+ const expectedError = shaka.test.Util.jasmineError(new shaka.util.Error(
1057
+ shaka.util.Error.Severity.CRITICAL,
1058
+ shaka.util.Error.Category.STORAGE,
1059
+ shaka.util.Error.Code.INDEXED_DB_INIT_TIMED_OUT));
1060
+
1061
+ await expectAsync(muxer.init())
1062
+ .toBeRejectedWith(expectedError);
1063
+
1064
+ window.indexedDB.open = oldOpen;
1065
+ });
1066
+
1037
1067
  it('throws an error if the content is a live stream', async () => {
1038
1068
  const expected = Util.jasmineError(new shaka.util.Error(
1039
1069
  shaka.util.Error.Severity.CRITICAL,
@@ -3735,6 +3735,93 @@ describe('Player', () => {
3735
3735
  });
3736
3736
  });
3737
3737
 
3738
+ describe('config streaming.failureCallback default', () => {
3739
+ /** @type {jasmine.Spy} */
3740
+ let retryStreaming;
3741
+ /** @type {jasmine.Spy} */
3742
+ let isLive;
3743
+
3744
+ /**
3745
+ * @suppress {accessControls}
3746
+ * @param {!shaka.util.Error} error
3747
+ */
3748
+ function defaultStreamingFailureCallback(error) {
3749
+ player.defaultStreamingFailureCallback_(error);
3750
+ }
3751
+
3752
+ beforeEach(() => {
3753
+ retryStreaming = jasmine.createSpy('retryStreaming');
3754
+ isLive = jasmine.createSpy('isLive');
3755
+
3756
+ player.retryStreaming = Util.spyFunc(retryStreaming);
3757
+ player.isLive = Util.spyFunc(isLive);
3758
+ });
3759
+
3760
+ it('ignores VOD failures', () => {
3761
+ isLive.and.returnValue(false);
3762
+
3763
+ const error = new shaka.util.Error(
3764
+ shaka.util.Error.Severity.CRITICAL,
3765
+ shaka.util.Error.Category.NETWORK,
3766
+ shaka.util.Error.Code.BAD_HTTP_STATUS,
3767
+ /* url= */ '',
3768
+ /* http_status= */ 404);
3769
+
3770
+ defaultStreamingFailureCallback(error);
3771
+ expect(retryStreaming).not.toHaveBeenCalled();
3772
+ });
3773
+
3774
+ it('retries live on HTTP 404', () => {
3775
+ isLive.and.returnValue(true);
3776
+
3777
+ const error = new shaka.util.Error(
3778
+ shaka.util.Error.Severity.CRITICAL,
3779
+ shaka.util.Error.Category.NETWORK,
3780
+ shaka.util.Error.Code.BAD_HTTP_STATUS,
3781
+ /* url= */ '',
3782
+ /* http_status= */ 404);
3783
+
3784
+ defaultStreamingFailureCallback(error);
3785
+ expect(retryStreaming).toHaveBeenCalled();
3786
+ });
3787
+
3788
+ it('retries live on generic HTTP error', () => {
3789
+ isLive.and.returnValue(true);
3790
+
3791
+ const error = new shaka.util.Error(
3792
+ shaka.util.Error.Severity.CRITICAL,
3793
+ shaka.util.Error.Category.NETWORK,
3794
+ shaka.util.Error.Code.HTTP_ERROR);
3795
+
3796
+ defaultStreamingFailureCallback(error);
3797
+ expect(retryStreaming).toHaveBeenCalled();
3798
+ });
3799
+
3800
+ it('retries live on HTTP timeout', () => {
3801
+ isLive.and.returnValue(true);
3802
+
3803
+ const error = new shaka.util.Error(
3804
+ shaka.util.Error.Severity.CRITICAL,
3805
+ shaka.util.Error.Category.NETWORK,
3806
+ shaka.util.Error.Code.TIMEOUT);
3807
+
3808
+ defaultStreamingFailureCallback(error);
3809
+ expect(retryStreaming).toHaveBeenCalled();
3810
+ });
3811
+
3812
+ it('ignores other live failures', () => {
3813
+ isLive.and.returnValue(true);
3814
+
3815
+ const error = new shaka.util.Error(
3816
+ shaka.util.Error.Severity.CRITICAL,
3817
+ shaka.util.Error.Category.MEDIA,
3818
+ shaka.util.Error.Code.VIDEO_ERROR);
3819
+
3820
+ defaultStreamingFailureCallback(error);
3821
+ expect(retryStreaming).not.toHaveBeenCalled();
3822
+ });
3823
+ });
3824
+
3738
3825
  /**
3739
3826
  * Gets the currently active variant track.
3740
3827
  * @return {shaka.extern.Track}
@@ -333,6 +333,9 @@ shaka.test.FakePlayhead = class {
333
333
  /** @private {number} */
334
334
  this.startTime_ = 0;
335
335
 
336
+ /** @type {!jasmine.Spy} */
337
+ this.ready = jasmine.createSpy('ready');
338
+
336
339
  /** @type {!jasmine.Spy} */
337
340
  this.setStartTime = jasmine.createSpy('setStartTime')
338
341
  .and.callFake((value) => {
@@ -12,6 +12,14 @@ describe('PeriodCombiner', () => {
12
12
  /** @type {shaka.util.PeriodCombiner} */
13
13
  let combiner;
14
14
 
15
+ const makeAudioStreamWithRoles = (id, roles, primary = true) => {
16
+ const stream = makeAudioStream('en');
17
+ stream.originalId = id;
18
+ stream.roles = roles;
19
+ stream.primary = primary;
20
+ return stream;
21
+ };
22
+
15
23
  beforeEach(() => {
16
24
  combiner = new shaka.util.PeriodCombiner();
17
25
  });
@@ -1127,20 +1135,149 @@ describe('PeriodCombiner', () => {
1127
1135
  const variants = combiner.getVariants();
1128
1136
 
1129
1137
  expect(variants).toEqual(jasmine.arrayWithExactContents([
1130
- makeAVVariant(1080, 'en'),
1131
- makeAVVariant(1080, 'en'),
1138
+ makeAVVariant(1080, 'en', 2, ['role1', 'role2']),
1139
+ makeAVVariant(1080, 'en', 2, ['role1']),
1132
1140
  ]));
1133
1141
 
1134
1142
  // We can use the originalId field to see what each track is composed of.
1135
- const audio1 = variants[0].audio;
1136
- expect(audio1.roles).toEqual(['role1', 'role2']);
1137
- expect(audio1.originalId).toBe('stream1,stream3');
1143
+ expect(variants[0].audio.originalId).toBe('stream1,stream3');
1144
+ expect(variants[1].audio.originalId).toBe('stream2,stream4');
1145
+ });
1138
1146
 
1139
- const audio2 = variants[1].audio;
1140
- expect(audio2.roles).toEqual(['role1']);
1141
- expect(audio2.originalId).toBe('stream2,stream4');
1147
+ it('Matches streams with roles in common', async () => {
1148
+ /** @type {!Array.<shaka.util.PeriodCombiner.Period>} */
1149
+ const periods = [
1150
+ {
1151
+ id: '1',
1152
+ videoStreams: [
1153
+ makeVideoStream(720),
1154
+ makeVideoStream(1080),
1155
+ ],
1156
+ audioStreams: [
1157
+ makeAudioStreamWithRoles('stream1', ['main']),
1158
+ makeAudioStreamWithRoles('stream2', ['description'], false),
1159
+ ],
1160
+ textStreams: [],
1161
+ imageStreams: [],
1162
+ },
1163
+ {
1164
+ id: '2',
1165
+ videoStreams: [
1166
+ makeVideoStream(720),
1167
+ makeVideoStream(1080),
1168
+ ],
1169
+ audioStreams: [
1170
+ makeAudioStreamWithRoles('stream1', ['main']),
1171
+ makeAudioStreamWithRoles('stream2', ['description'], false),
1172
+ ],
1173
+ textStreams: [],
1174
+ imageStreams: [],
1175
+ },
1176
+ ];
1177
+
1178
+ await combiner.combinePeriods(periods, /* isDynamic= */ false);
1179
+ const variants = combiner.getVariants();
1180
+
1181
+ console.log(variants);
1182
+
1183
+ expect(variants.length).toBe(4);
1184
+
1185
+ expect(variants).toEqual(jasmine.arrayWithExactContents([
1186
+ makeAVVariant(720, 'en', 2, ['main']),
1187
+ makeAVVariant(1080, 'en', 2, ['main']),
1188
+ makeAVVariant(720, 'en', 2, ['description']),
1189
+ makeAVVariant(1080, 'en', 2, ['description']),
1190
+ ]));
1191
+
1192
+ // We can use the originalId field to see what each track is composed of.
1193
+ expect(variants[0].audio.originalId).toBe('stream1,stream1');
1194
+ expect(variants[1].audio.originalId).toBe('stream1,stream1');
1195
+ expect(variants[2].audio.originalId).toBe('stream2,stream2');
1196
+ expect(variants[3].audio.originalId).toBe('stream2,stream2');
1142
1197
  });
1143
1198
 
1199
+ it('Matches streams with mismatched roles', async () => {
1200
+ /** @type {!Array.<shaka.util.PeriodCombiner.Period>} */
1201
+ const periods = [
1202
+ {
1203
+ id: '0',
1204
+ videoStreams: [
1205
+ makeVideoStream(720),
1206
+ makeVideoStream(1080),
1207
+ ],
1208
+ audioStreams: [
1209
+ makeAudioStreamWithRoles('stream1', ['main']),
1210
+ ],
1211
+ textStreams: [],
1212
+ imageStreams: [],
1213
+ },
1214
+ {
1215
+ id: '1',
1216
+ videoStreams: [
1217
+ makeVideoStream(720),
1218
+ makeVideoStream(1080),
1219
+ ],
1220
+ audioStreams: [
1221
+ makeAudioStreamWithRoles('stream1', ['main']),
1222
+ makeAudioStreamWithRoles('stream2', ['description'], false),
1223
+ ],
1224
+ textStreams: [],
1225
+ imageStreams: [],
1226
+ },
1227
+ {
1228
+ id: '2',
1229
+ videoStreams: [
1230
+ makeVideoStream(720),
1231
+ makeVideoStream(1080),
1232
+ ],
1233
+ audioStreams: [
1234
+ makeAudioStreamWithRoles('stream1', ['main']),
1235
+ ],
1236
+ textStreams: [],
1237
+ imageStreams: [],
1238
+ },
1239
+ {
1240
+ id: '3',
1241
+ videoStreams: [
1242
+ makeVideoStream(720),
1243
+ makeVideoStream(1080),
1244
+ ],
1245
+ audioStreams: [
1246
+ makeAudioStreamWithRoles('stream1', ['main']),
1247
+ makeAudioStreamWithRoles('stream2', ['description'], false),
1248
+ ],
1249
+ textStreams: [],
1250
+ imageStreams: [],
1251
+ },
1252
+ ];
1253
+
1254
+ await combiner.combinePeriods(periods, /* isDynamic= */ false);
1255
+ const variants = combiner.getVariants();
1256
+
1257
+ console.log(variants);
1258
+
1259
+ expect(variants.length).toBe(4);
1260
+
1261
+ expect(variants).toEqual(jasmine.arrayWithExactContents([
1262
+ makeAVVariant(720, 'en', 2, ['main']),
1263
+ makeAVVariant(1080, 'en', 2, ['main']),
1264
+ makeAVVariant(720, 'en', 2, ['description', 'main']),
1265
+ makeAVVariant(1080, 'en', 2, ['description', 'main']),
1266
+ ]));
1267
+
1268
+ // We can use the originalId field to see what each track is composed of.
1269
+ expect(variants[0].audio.originalId)
1270
+ .toBe('stream1,stream1,stream1,stream1');
1271
+
1272
+ expect(variants[1].audio.originalId)
1273
+ .toBe('stream1,stream1,stream1,stream1');
1274
+
1275
+ expect(variants[2].audio.originalId)
1276
+ .toBe('stream1,stream2,stream1,stream2');
1277
+
1278
+ expect(variants[3].audio.originalId)
1279
+ .toBe('stream1,stream2,stream1,stream2');
1280
+ });
1144
1281
 
1145
1282
  it('The number of variants stays stable after many periods ' +
1146
1283
  'when going between similar content and varying ads', async () => {
@@ -1148,7 +1285,7 @@ describe('PeriodCombiner', () => {
1148
1285
  // https://github.com/shaka-project/shaka-player/issues/2716
1149
1286
  // that used to cause our period flattening logic to keep
1150
1287
  // creating new variants for every new period added.
1151
- // It's ok to create a few additional varinats/streams,
1288
+ // It's ok to create a few additional variants/streams,
1152
1289
  // but we should stabilize eventually and keep the number
1153
1290
  // of variants from growing indefinitely.
1154
1291
 
@@ -1334,7 +1471,7 @@ describe('PeriodCombiner', () => {
1334
1471
  expect(isCandidateBetter).toBe(WORSE);
1335
1472
 
1336
1473
  // Make sure it works correctly whether it's the candidate or the best
1337
- // value that is equel to the output.
1474
+ // value that is equal to the output.
1338
1475
  isCandidateBetter = PeriodCombiner.compareClosestPreferLower(
1339
1476
  /* output= */ 5, /* bestValue= */ 3, /* candidateValue= */ 5);
1340
1477
  expect(isCandidateBetter).toBe(BETTER);
@@ -1461,11 +1598,12 @@ describe('PeriodCombiner', () => {
1461
1598
  * @param {number=} channels
1462
1599
  * @return {shaka.extern.Variant}
1463
1600
  */
1464
- function makeAVVariant(height, language, channels = 2) {
1601
+ function makeAVVariant(height, language, channels = 2, roles = []) {
1465
1602
  const variant = jasmine.objectContaining({
1466
1603
  language,
1467
1604
  audio: jasmine.objectContaining({
1468
1605
  language,
1606
+ roles,
1469
1607
  channelsCount: channels,
1470
1608
  }),
1471
1609
  video: jasmine.objectContaining({
@@ -371,5 +371,69 @@ describe('XmlUtils', () => {
371
371
  expect(XmlUtils.parseFloat(HUGE_NUMBER_STRING)).toBe(Infinity);
372
372
  expect(XmlUtils.parseFloat('-' + HUGE_NUMBER_STRING)).toBe(-Infinity);
373
373
  });
374
+
375
+ describe('parseXmlString', () => {
376
+ it('parses a simple XML document', () => {
377
+ const xmlString = [
378
+ '<?xml version="1.0"?>',
379
+ '<Root>',
380
+ ' <Child></Child>',
381
+ '</Root>',
382
+ ].join('\n');
383
+ const doc = XmlUtils.parseXmlString(xmlString, 'Root');
384
+ expect(doc).not.toBeNull();
385
+ expect(doc.tagName).toBe('Root');
386
+ });
387
+
388
+ it('returns null on an empty XML document', () => {
389
+ const xmlString = '';
390
+ const doc = XmlUtils.parseXmlString(xmlString, 'Root');
391
+ expect(doc).toBeNull();
392
+ });
393
+
394
+ it('returns null on malformed XML', () => {
395
+ const xmlString = [
396
+ '<?xml version="1.0"?>',
397
+ '<Root>',
398
+ ' <Child</Child>',
399
+ '</Root>',
400
+ ].join('\n');
401
+ const doc = XmlUtils.parseXmlString(xmlString, 'Root');
402
+ expect(doc).toBeNull();
403
+ });
404
+
405
+ it('returns null on root element mismatch', () => {
406
+ const xmlString = [
407
+ '<?xml version="1.0"?>',
408
+ '<Root>',
409
+ ' <Child></Child>',
410
+ '</Root>',
411
+ ].join('\n');
412
+ const doc = XmlUtils.parseXmlString(xmlString, 'Document');
413
+ expect(doc).toBeNull();
414
+ });
415
+
416
+ it('returns null on XML that embeds HTML', () => {
417
+ const xmlString = [
418
+ '<?xml version="1.0"?>',
419
+ '<Root>',
420
+ ' <Child xmlns="http://www.w3.org/1999/xhtml"></Child>',
421
+ '</Root>',
422
+ ].join('\n');
423
+ const doc = XmlUtils.parseXmlString(xmlString, 'Root');
424
+ expect(doc).toBeNull();
425
+ });
426
+
427
+ it('returns null on XML that embeds SVG', () => {
428
+ const xmlString = [
429
+ '<?xml version="1.0"?>',
430
+ '<Root>',
431
+ ' <Child xmlns="http://www.w3.org/2000/svg"></Child>',
432
+ '</Root>',
433
+ ].join('\n');
434
+ const doc = XmlUtils.parseXmlString(xmlString, 'Root');
435
+ expect(doc).toBeNull();
436
+ });
437
+ });
374
438
  });
375
439
 
package/ui/controls.js CHANGED
@@ -1004,7 +1004,10 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
1004
1004
  if (!this.video_ ||
1005
1005
  this.video_.readyState == 0 ||
1006
1006
  this.castProxy_.isCasting() ||
1007
- !this.config_.enableFullscreenOnRotation) { return; }
1007
+ !this.config_.enableFullscreenOnRotation ||
1008
+ !document.fullscreenEnabled) {
1009
+ return;
1010
+ }
1008
1011
 
1009
1012
  if (screen.orientation.type.includes('landscape') &&
1010
1013
  !document.fullscreenElement) {
@@ -12,6 +12,7 @@ goog.require('shaka.ui.Enums');
12
12
  goog.require('shaka.ui.Utils');
13
13
  goog.require('shaka.util.Dom');
14
14
  goog.require('shaka.util.FakeEvent');
15
+ goog.require('shaka.util.Iterables');
15
16
  goog.requireType('shaka.ui.Controls');
16
17
 
17
18
 
@@ -116,8 +117,16 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element {
116
117
  this.eventManager.listen(this.backButton, 'click', () => {
117
118
  shaka.ui.Utils.setDisplay(this.parent, true);
118
119
 
119
- /** @type {!HTMLElement} */
120
- (this.parent.childNodes[0]).focus();
120
+ const isDisplayed =
121
+ (element) => element.classList.contains('shaka-hidden') == false;
122
+
123
+ const Iterables = shaka.util.Iterables;
124
+ if (Iterables.some(this.parent.childNodes, isDisplayed)) {
125
+ // Focus on the first visible child of the overflow menu
126
+ const visibleElements =
127
+ Iterables.filter(this.parent.childNodes, isDisplayed);
128
+ /** @type {!HTMLElement} */ (visibleElements[0]).focus();
129
+ }
121
130
 
122
131
  // Make sure controls are displayed
123
132
  this.controls.computeOpacity();