shaka-player 3.3.5 → 4.0.2

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 (184) hide show
  1. package/.github/config.yml +1 -0
  2. package/.github/workflows/demo-version-index.yaml +8 -0
  3. package/.github/workflows/release-please.yaml +18 -2
  4. package/.github/workflows/sync-labels.yaml +33 -0
  5. package/.github/workflows/update-issues.yaml +29 -0
  6. package/.github/workflows/validate-pr-title.yaml +35 -0
  7. package/AUTHORS +1 -0
  8. package/CHANGELOG.md +215 -41
  9. package/CONTRIBUTORS +1 -0
  10. package/README.md +14 -11
  11. package/app-engine/demo-version-index/README.md +3 -3
  12. package/app-engine/demo-version-index/app.yaml +27 -1
  13. package/app-engine/demo-version-index/generate.py +164 -0
  14. package/app-engine/demo-version-index/main.py +4 -155
  15. package/app-engine/demo-version-index/requirements.txt +0 -1
  16. package/build/checkversion.py +1 -1
  17. package/build/compiler.py +1 -1
  18. package/build/misspellings.txt +1 -0
  19. package/build/shaka-lab.yaml +0 -2
  20. package/build/types/polyfill +0 -3
  21. package/demo/common/asset.js +17 -1
  22. package/demo/common/assets.js +29 -0
  23. package/demo/common/message_ids.js +6 -3
  24. package/demo/config.js +10 -11
  25. package/demo/custom.js +39 -0
  26. package/demo/demo.less +0 -1
  27. package/demo/demo_utils.js +0 -1
  28. package/demo/locales/en.json +6 -3
  29. package/demo/locales/source.json +22 -10
  30. package/demo/main.js +9 -6
  31. package/demo/search.js +2 -1
  32. package/dist/controls.css +3 -3
  33. package/dist/controls.css.map +1 -1
  34. package/dist/demo.compiled.debug.js +71 -64
  35. package/dist/demo.compiled.debug.map +3 -3
  36. package/dist/demo.compiled.js +71 -64
  37. package/dist/demo.compiled.map +3 -3
  38. package/dist/demo.css +1 -1
  39. package/dist/demo.css.map +1 -1
  40. package/dist/deps.js +17 -20
  41. package/dist/locales.js +33 -31
  42. package/dist/receiver.compiled.debug.js +32 -28
  43. package/dist/receiver.compiled.debug.map +3 -3
  44. package/dist/receiver.compiled.js +32 -28
  45. package/dist/receiver.compiled.map +3 -3
  46. package/dist/shaka-player.compiled.d.ts +311 -314
  47. package/dist/shaka-player.compiled.debug.d.ts +311 -314
  48. package/dist/shaka-player.compiled.debug.externs.js +126 -126
  49. package/dist/shaka-player.compiled.debug.js +430 -484
  50. package/dist/shaka-player.compiled.debug.map +4 -4
  51. package/dist/shaka-player.compiled.externs.js +126 -126
  52. package/dist/shaka-player.compiled.js +829 -845
  53. package/dist/shaka-player.compiled.map +4 -4
  54. package/dist/shaka-player.ui.d.ts +357 -358
  55. package/dist/shaka-player.ui.debug.d.ts +357 -358
  56. package/dist/shaka-player.ui.debug.externs.js +134 -126
  57. package/dist/shaka-player.ui.debug.js +489 -541
  58. package/dist/shaka-player.ui.debug.map +4 -4
  59. package/dist/shaka-player.ui.externs.js +134 -126
  60. package/dist/shaka-player.ui.js +988 -1000
  61. package/dist/shaka-player.ui.map +4 -4
  62. package/docs/tutorials/config.md +0 -2
  63. package/docs/tutorials/drm-config.md +5 -0
  64. package/docs/tutorials/fairplay.md +7 -22
  65. package/docs/tutorials/faq.md +0 -11
  66. package/docs/tutorials/index.json +2 -8
  67. package/docs/tutorials/plugins.md +1 -3
  68. package/docs/tutorials/upgrade.md +85 -11
  69. package/externs/polyfill.js +15 -0
  70. package/externs/shaka/manifest.js +5 -1
  71. package/externs/shaka/net.js +12 -0
  72. package/externs/shaka/offline.js +5 -1
  73. package/externs/shaka/player.js +39 -21
  74. package/externs/shaka/text.js +12 -12
  75. package/externs/sourcebuffer.js +15 -0
  76. package/lib/abr/simple_abr_manager.js +2 -1
  77. package/lib/ads/client_side_ad.js +13 -2
  78. package/lib/ads/client_side_ad_manager.js +22 -4
  79. package/lib/cast/cast_utils.js +0 -1
  80. package/lib/dash/content_protection.js +85 -5
  81. package/lib/dash/dash_parser.js +1 -0
  82. package/lib/hls/hls_parser.js +390 -747
  83. package/lib/hls/manifest_text_parser.js +2 -3
  84. package/lib/media/drm_engine.js +105 -37
  85. package/lib/media/gap_jumping_controller.js +10 -45
  86. package/lib/media/media_source_engine.js +150 -30
  87. package/lib/media/playhead.js +2 -5
  88. package/lib/media/presentation_timeline.js +1 -2
  89. package/lib/media/segment_index.js +13 -46
  90. package/lib/media/segment_reference.js +47 -1
  91. package/lib/media/streaming_engine.js +28 -11
  92. package/lib/media/time_ranges_utils.js +4 -9
  93. package/lib/net/networking_engine.js +3 -0
  94. package/lib/offline/indexeddb/v1_storage_cell.js +1 -0
  95. package/lib/offline/indexeddb/v2_storage_cell.js +1 -0
  96. package/lib/offline/manifest_converter.js +1 -0
  97. package/lib/offline/storage.js +8 -41
  98. package/lib/player.js +17 -201
  99. package/lib/polyfill/media_capabilities.js +12 -6
  100. package/lib/polyfill/mediasource.js +0 -28
  101. package/lib/polyfill/patchedmediakeys_nop.js +2 -0
  102. package/lib/polyfill/patchedmediakeys_webkit.js +2 -0
  103. package/lib/text/cue.js +0 -6
  104. package/lib/text/lrc_text_parser.js +8 -0
  105. package/lib/text/mp4_ttml_parser.js +8 -0
  106. package/lib/text/mp4_vtt_parser.js +8 -0
  107. package/lib/text/sbv_text_parser.js +8 -0
  108. package/lib/text/simple_text_displayer.js +1 -7
  109. package/lib/text/srt_text_parser.js +8 -0
  110. package/lib/text/ssa_text_parser.js +8 -0
  111. package/lib/text/text_engine.js +19 -3
  112. package/lib/text/ttml_text_parser.js +8 -0
  113. package/lib/text/ui_text_displayer.js +1 -7
  114. package/lib/text/vtt_text_parser.js +27 -5
  115. package/lib/text/web_vtt_generator.js +1 -7
  116. package/lib/util/error.js +9 -13
  117. package/lib/util/fairplay_utils.js +109 -0
  118. package/lib/util/functional.js +0 -42
  119. package/lib/util/mime_utils.js +16 -5
  120. package/lib/util/platform.js +45 -0
  121. package/lib/util/player_configuration.js +17 -6
  122. package/lib/util/pssh.js +23 -4
  123. package/lib/util/stream_utils.js +92 -22
  124. package/lib/util/uint8array_utils.js +0 -16
  125. package/lib/util/xml_utils.js +1 -1
  126. package/package.json +5 -2
  127. package/roadmap.md +19 -19
  128. package/shaka-player.uncompiled.js +0 -3
  129. package/test/dash/dash_parser_content_protection_unit.js +44 -0
  130. package/test/dash/dash_parser_segment_template_unit.js +3 -4
  131. package/test/demo/demo_unit.js +2 -1
  132. package/test/hls/hls_live_unit.js +155 -230
  133. package/test/hls/hls_parser_unit.js +515 -432
  134. package/test/media/drm_engine_unit.js +86 -120
  135. package/test/media/media_source_engine_integration.js +25 -3
  136. package/test/media/media_source_engine_unit.js +3 -2
  137. package/test/media/playhead_unit.js +27 -191
  138. package/test/media/segment_index_unit.js +15 -25
  139. package/test/media/streaming_engine_integration.js +2 -39
  140. package/test/media/streaming_engine_unit.js +3 -2
  141. package/test/media/time_ranges_utils_unit.js +0 -5
  142. package/test/offline/manifest_convert_unit.js +34 -4
  143. package/test/offline/offline_manifest_parser_unit.js +1 -0
  144. package/test/offline/storage_integration.js +5 -4
  145. package/test/player_external.js +0 -6
  146. package/test/player_integration.js +16 -0
  147. package/test/player_unit.js +26 -0
  148. package/test/test/util/fake_media_source_engine.js +11 -4
  149. package/test/test/util/manifest_generator.js +3 -1
  150. package/test/test/util/manifest_parser_util.js +4 -1
  151. package/test/test/util/offline_utils.js +1 -0
  152. package/test/test/util/streaming_engine_util.js +1 -0
  153. package/test/test/util/test_scheme.js +24 -1
  154. package/test/test/util/util.js +4 -4
  155. package/test/text/cue_integration.js +2 -2
  156. package/test/text/lrc_text_parser_unit.js +6 -7
  157. package/test/text/mp4_ttml_parser_unit.js +6 -4
  158. package/test/text/mp4_vtt_parser_unit.js +6 -5
  159. package/test/text/sbv_text_parser_unit.js +5 -6
  160. package/test/text/srt_text_parser_unit.js +5 -6
  161. package/test/text/ssa_text_parser_unit.js +17 -29
  162. package/test/text/text_engine_unit.js +45 -5
  163. package/test/text/ttml_text_parser_unit.js +77 -77
  164. package/test/text/vtt_text_parser_unit.js +116 -59
  165. package/test/ui/ui_integration.js +0 -16
  166. package/test/ui/ui_unit.js +27 -0
  167. package/test/util/cmcd_manager_unit.js +3 -0
  168. package/test/util/stream_utils_unit.js +23 -7
  169. package/ui/controls.js +65 -40
  170. package/ui/fullscreen_button.js +26 -6
  171. package/ui/less/containers.less +4 -4
  172. package/ui/less/general.less +1 -1
  173. package/ui/less/range_elements.less +1 -0
  174. package/ui/locales/en.json +1 -0
  175. package/ui/locales/source.json +5 -0
  176. package/ui/resolution_selection.js +31 -33
  177. package/ui/ui.js +0 -17
  178. package/.github/pull_request_template.md +0 -10
  179. package/docs/tutorials/upgrade-v2-5.md +0 -441
  180. package/docs/tutorials/upgrade-v3.md +0 -38
  181. package/lib/polyfill/mathround.js +0 -49
  182. package/lib/polyfill/patchedmediakeys_apple.js +0 -744
  183. package/lib/polyfill/patchedmediakeys_ms.js +0 -698
  184. package/test/util/functional_unit.js +0 -82
@@ -179,9 +179,8 @@ shaka.hls.ManifestTextParser = class {
179
179
  segmentTags.push(currentMapTag);
180
180
  }
181
181
  // The URI appears after all of the tags describing the segment.
182
- const segment =
183
- new shaka.hls.Segment(absoluteSegmentUri, segmentTags,
184
- partialSegmentTags);
182
+ const segment = new shaka.hls.Segment(absoluteSegmentUri, segmentTags,
183
+ partialSegmentTags);
185
184
  segments.push(segment);
186
185
  segmentTags = [];
187
186
  partialSegmentTags = [];
@@ -14,7 +14,6 @@ goog.require('shaka.util.BufferUtils');
14
14
  goog.require('shaka.util.Destroyer');
15
15
  goog.require('shaka.util.Error');
16
16
  goog.require('shaka.util.EventManager');
17
- goog.require('shaka.util.FairPlayUtils');
18
17
  goog.require('shaka.util.FakeEvent');
19
18
  goog.require('shaka.util.IDestroyable');
20
19
  goog.require('shaka.util.Iterables');
@@ -128,6 +127,9 @@ shaka.media.DrmEngine = class {
128
127
 
129
128
  /** @private {boolean} */
130
129
  this.srcEquals_ = false;
130
+
131
+ /** @private {Promise} */
132
+ this.mediaKeysAttached_ = null;
131
133
  }
132
134
 
133
135
  /** @override */
@@ -184,6 +186,7 @@ shaka.media.DrmEngine = class {
184
186
  this.onError_ = () => {};
185
187
  this.playerInterface_ = null;
186
188
  this.srcEquals_ = false;
189
+ this.mediaKeysAttached_ = null;
187
190
  }
188
191
 
189
192
  /**
@@ -387,7 +390,59 @@ shaka.media.DrmEngine = class {
387
390
  }
388
391
 
389
392
  /**
390
- * Attach MediaKeys to the video element and start processing events.
393
+ * Attach MediaKeys to the video element
394
+ * @return {Promise}
395
+ * @private
396
+ */
397
+ async attachMediaKeys_() {
398
+ if (this.video_.mediaKeys) {
399
+ return;
400
+ }
401
+
402
+ // An attach process has already started, let's wait it out
403
+ if (this.mediaKeysAttached_) {
404
+ await this.mediaKeysAttached_;
405
+
406
+ return;
407
+ }
408
+
409
+ try {
410
+ this.mediaKeysAttached_ = this.video_.setMediaKeys(this.mediaKeys_);
411
+
412
+ await this.mediaKeysAttached_;
413
+ } catch (exception) {
414
+ goog.asserts.assert(exception instanceof Error, 'Wrong error type!');
415
+
416
+ this.onError_(new shaka.util.Error(
417
+ shaka.util.Error.Severity.CRITICAL,
418
+ shaka.util.Error.Category.DRM,
419
+ shaka.util.Error.Code.FAILED_TO_ATTACH_TO_VIDEO,
420
+ exception.message));
421
+ }
422
+
423
+ this.destroyer_.ensureNotDestroyed();
424
+ }
425
+
426
+ /**
427
+ * Processes encrypted event and start licence challenging
428
+ * @return {!Promise}
429
+ * @private
430
+ */
431
+ async onEncryptedEvent_(event) {
432
+ /**
433
+ * MediaKeys should be added when receiving an encrypted event. Setting
434
+ * mediaKeys before could result into encrypted event not being fired on
435
+ * some browsers
436
+ */
437
+ await this.attachMediaKeys_();
438
+
439
+ this.newInitData(
440
+ event.initDataType,
441
+ shaka.util.BufferUtils.toUint8(event.initData));
442
+ }
443
+
444
+ /**
445
+ * Start processing events.
391
446
  * @param {HTMLMediaElement} video
392
447
  * @return {!Promise}
393
448
  */
@@ -420,27 +475,30 @@ shaka.media.DrmEngine = class {
420
475
  () => this.closeOpenSessions_());
421
476
  }
422
477
 
423
- let setMediaKeys = this.video_.setMediaKeys(this.mediaKeys_);
424
- setMediaKeys = setMediaKeys.catch((exception) => {
425
- goog.asserts.assert(exception instanceof Error, 'Wrong error type!');
426
- return Promise.reject(new shaka.util.Error(
427
- shaka.util.Error.Severity.CRITICAL,
428
- shaka.util.Error.Category.DRM,
429
- shaka.util.Error.Code.FAILED_TO_ATTACH_TO_VIDEO,
430
- exception.message));
431
- });
478
+ const manifestInitData = this.currentDrmInfo_.initData.find(
479
+ (initDataOverride) => initDataOverride.initData.length > 0);
432
480
 
433
- await setMediaKeys;
434
- this.destroyer_.ensureNotDestroyed();
481
+ /**
482
+ * We can attach media keys before the playback actually begins when:
483
+ * - Using legacy implementations requires MediaKeys to be set before
484
+ * having webkitneedkey / msneedkey event, which will be translated as
485
+ * an encrypted event by the polyfills
486
+ * - Some initData already has been generated (through the manifest)
487
+ * - In case of an offline session
488
+ */
489
+ if (manifestInitData ||
490
+ shaka.util.Platform.isMediaKeysPolyfilled() ||
491
+ this.offlineSessionIds_.length) {
492
+ await this.attachMediaKeys_();
493
+ }
435
494
 
436
495
  this.createOrLoad();
437
- if (!this.currentDrmInfo_.initData.length &&
438
- !this.offlineSessionIds_.length) {
439
- // Explicit init data for any one stream or an offline session is
440
- // sufficient to suppress 'encrypted' events for all streams.
441
- const cb = (e) => this.newInitData(
442
- e.initDataType, shaka.util.BufferUtils.toUint8(e.initData));
443
- this.eventManager_.listen(this.video_, 'encrypted', cb);
496
+
497
+ // Explicit init data for any one stream or an offline session is
498
+ // sufficient to suppress 'encrypted' events for all streams.
499
+ if (!manifestInitData && !this.offlineSessionIds_.length) {
500
+ this.eventManager_.listen(
501
+ this.video_, 'encrypted', (e) => this.onEncryptedEvent_(e));
444
502
  }
445
503
  }
446
504
 
@@ -589,6 +647,10 @@ shaka.media.DrmEngine = class {
589
647
  * @param {!Uint8Array} initData
590
648
  */
591
649
  newInitData(initDataType, initData) {
650
+ if (!initData.length) {
651
+ return;
652
+ }
653
+
592
654
  // Suppress duplicate init data.
593
655
  // Note that some init data are extremely large and can't portably be used
594
656
  // as keys in a dictionary.
@@ -1106,6 +1168,7 @@ shaka.media.DrmEngine = class {
1106
1168
 
1107
1169
  const metadata = {
1108
1170
  initData: null,
1171
+ initDataType: null,
1109
1172
  loaded: false,
1110
1173
  oldExpiration: Infinity,
1111
1174
  updatePromise: null,
@@ -1182,6 +1245,7 @@ shaka.media.DrmEngine = class {
1182
1245
 
1183
1246
  const metadata = {
1184
1247
  initData: initData,
1248
+ initDataType: initDataType,
1185
1249
  loaded: false,
1186
1250
  oldExpiration: Infinity,
1187
1251
  updatePromise: null,
@@ -1239,23 +1303,6 @@ shaka.media.DrmEngine = class {
1239
1303
  });
1240
1304
  }
1241
1305
 
1242
- /**
1243
- * @param {!Uint8Array} initData
1244
- * @param {string} initDataType
1245
- * @param {?shaka.extern.DrmInfo} drmInfo
1246
- * @return {!Uint8Array}
1247
- */
1248
- static defaultInitDataTransform(initData, initDataType, drmInfo) {
1249
- if (initDataType == 'skd') {
1250
- const cert = drmInfo.serverCertificate;
1251
- const contentId =
1252
- shaka.util.FairPlayUtils.defaultGetContentId(initData);
1253
- initData = shaka.util.FairPlayUtils.initDataTransform(
1254
- initData, contentId, cert);
1255
- }
1256
- return initData;
1257
- }
1258
-
1259
1306
  /**
1260
1307
  * @param {!MediaKeyMessageEvent} event
1261
1308
  * @private
@@ -1316,6 +1363,11 @@ shaka.media.DrmEngine = class {
1316
1363
  request.method = 'POST';
1317
1364
  request.licenseRequestType = event.messageType;
1318
1365
  request.sessionId = session.sessionId;
1366
+ request.drmInfo = this.currentDrmInfo_;
1367
+ if (metadata) {
1368
+ request.initData = metadata.initData;
1369
+ request.initDataType = metadata.initDataType;
1370
+ }
1319
1371
  // NOTE: allowCrossSiteCredentials can be set in a request filter.
1320
1372
 
1321
1373
  if (shaka.media.DrmEngine.isPlayReadyKeySystem(
@@ -1372,6 +1424,9 @@ shaka.media.DrmEngine = class {
1372
1424
  }
1373
1425
  return;
1374
1426
  }
1427
+ if (this.destroyer_.destroyed()) {
1428
+ return;
1429
+ }
1375
1430
 
1376
1431
  const updateEvent = new shaka.util.FakeEvent('drmsessionupdate');
1377
1432
  this.playerInterface_.onEvent(updateEvent);
@@ -1642,6 +1697,16 @@ shaka.media.DrmEngine = class {
1642
1697
 
1643
1698
  const testSystem = async (keySystem) => {
1644
1699
  try {
1700
+ // Our Polyfill will reject anything apart com.apple.fps key systems.
1701
+ // It seems the Safari modern EME API will allow to request a
1702
+ // MediaKeySystemAccess for the ClearKey CDM, create and update a key
1703
+ // session but playback will never start
1704
+ // Safari bug: https://bugs.webkit.org/show_bug.cgi?id=231006
1705
+ if (keySystem === 'org.w3.clearkey' &&
1706
+ shaka.util.Platform.isSafari()) {
1707
+ throw new Error('Unsupported keySystem');
1708
+ }
1709
+
1645
1710
  const access = await navigator.requestMediaKeySystemAccess(
1646
1711
  keySystem, configs);
1647
1712
 
@@ -2262,6 +2327,7 @@ shaka.media.DrmEngine = class {
2262
2327
  * @typedef {{
2263
2328
  * loaded: boolean,
2264
2329
  * initData: Uint8Array,
2330
+ * initDataType: ?string,
2265
2331
  * oldExpiration: number,
2266
2332
  * type: string,
2267
2333
  * updatePromise: shaka.util.PublicPromise
@@ -2273,6 +2339,8 @@ shaka.media.DrmEngine = class {
2273
2339
  * does not mean the session is 'usable'.
2274
2340
  * @property {Uint8Array} initData
2275
2341
  * The init data used to create the session.
2342
+ * @property {?string} initDataType
2343
+ * The init data type used to create the session.
2276
2344
  * @property {!MediaKeySession} session
2277
2345
  * The session object.
2278
2346
  * @property {number} oldExpiration
@@ -11,7 +11,6 @@ goog.require('shaka.media.PresentationTimeline');
11
11
  goog.require('shaka.media.StallDetector');
12
12
  goog.require('shaka.media.TimeRangesUtils');
13
13
  goog.require('shaka.util.EventManager');
14
- goog.require('shaka.util.FakeEvent');
15
14
  goog.require('shaka.util.IReleasable');
16
15
  goog.require('shaka.util.Timer');
17
16
 
@@ -33,10 +32,8 @@ shaka.media.GapJumpingController = class {
33
32
  * playable region. The gap jumping controller takes ownership over the
34
33
  * stall detector.
35
34
  * If no stall detection logic is desired, |null| may be provided.
36
- * @param {function(!Event)} onEvent Called when an event is raised to be sent
37
- * to the application.
38
35
  */
39
- constructor(video, timeline, config, stallDetector, onEvent) {
36
+ constructor(video, timeline, config, stallDetector) {
40
37
  /** @private {HTMLMediaElement} */
41
38
  this.video_ = video;
42
39
 
@@ -46,9 +43,6 @@ shaka.media.GapJumpingController = class {
46
43
  /** @private {?shaka.extern.StreamingConfiguration} */
47
44
  this.config_ = config;
48
45
 
49
- /** @private {?function(!Event)} */
50
- this.onEvent_ = onEvent;
51
-
52
46
  /** @private {shaka.util.EventManager} */
53
47
  this.eventManager_ = new shaka.util.EventManager();
54
48
 
@@ -58,9 +52,6 @@ shaka.media.GapJumpingController = class {
58
52
  /** @private {number} */
59
53
  this.prevReadyState_ = video.readyState;
60
54
 
61
- /** @private {boolean} */
62
- this.didFireLargeGap_ = false;
63
-
64
55
  /**
65
56
  * The stall detector tries to keep the playhead moving forward. It is
66
57
  * managed by the gap-jumping controller to avoid conflicts. On some
@@ -107,7 +98,6 @@ shaka.media.GapJumpingController = class {
107
98
  this.stallDetector_ = null;
108
99
  }
109
100
 
110
- this.onEvent_ = null;
111
101
  this.timeline_ = null;
112
102
  this.video_ = null;
113
103
  }
@@ -128,7 +118,6 @@ shaka.media.GapJumpingController = class {
128
118
  onSeeking() {
129
119
  this.seekingEventReceived_ = true;
130
120
  this.hadSegmentAppended_ = false;
131
- this.didFireLargeGap_ = false;
132
121
  }
133
122
 
134
123
 
@@ -165,7 +154,6 @@ shaka.media.GapJumpingController = class {
165
154
  // When the ready state changes, we have moved on, so we should fire the
166
155
  // large gap event if we see one.
167
156
  if (this.video_.readyState != this.prevReadyState_) {
168
- this.didFireLargeGap_ = false;
169
157
  this.prevReadyState_ = this.video_.readyState;
170
158
  }
171
159
 
@@ -175,7 +163,6 @@ shaka.media.GapJumpingController = class {
175
163
  }
176
164
 
177
165
 
178
- const smallGapLimit = this.config_.smallGapLimit;
179
166
  const currentTime = this.video_.currentTime;
180
167
  const buffered = this.video_.buffered;
181
168
  const gapDetectionThreshold = this.config_.gapDetectionThreshold;
@@ -203,8 +190,6 @@ shaka.media.GapJumpingController = class {
203
190
  }
204
191
 
205
192
  const jumpSize = jumpTo - currentTime;
206
- const isGapSmall = jumpSize <= smallGapLimit;
207
- let jumpLargeGap = false;
208
193
 
209
194
  // If we jump to exactly the gap start, we may detect a small gap due to
210
195
  // rounding errors or browser bugs. We can ignore these extremely small
@@ -213,37 +198,17 @@ shaka.media.GapJumpingController = class {
213
198
  return;
214
199
  }
215
200
 
216
- if (!isGapSmall && !this.didFireLargeGap_) {
217
- this.didFireLargeGap_ = true;
218
-
219
- // Event firing is synchronous.
220
- const data = new Map()
221
- .set('currentTime', currentTime)
222
- .set('gapSize', jumpSize);
223
- const event = new shaka.util.FakeEvent('largegap', data);
224
- event.cancelable = true;
225
- this.onEvent_(event);
226
-
227
- if (this.config_.jumpLargeGaps && !event.defaultPrevented) {
228
- jumpLargeGap = true;
229
- } else {
230
- shaka.log.info('Ignoring large gap at', currentTime, 'size', jumpSize);
231
- }
201
+ if (gapIndex == 0) {
202
+ shaka.log.info(
203
+ 'Jumping forward', jumpSize,
204
+ 'seconds because of gap before start time of', jumpTo);
205
+ } else {
206
+ shaka.log.info(
207
+ 'Jumping forward', jumpSize, 'seconds because of gap starting at',
208
+ buffered.end(gapIndex - 1), 'and ending at', jumpTo);
232
209
  }
233
210
 
234
- if (isGapSmall || jumpLargeGap) {
235
- if (gapIndex == 0) {
236
- shaka.log.info(
237
- 'Jumping forward', jumpSize,
238
- 'seconds because of gap before start time of', jumpTo);
239
- } else {
240
- shaka.log.info(
241
- 'Jumping forward', jumpSize, 'seconds because of gap starting at',
242
- buffered.end(gapIndex - 1), 'and ending at', jumpTo);
243
- }
244
-
245
- this.video_.currentTime = jumpTo;
246
- }
211
+ this.video_.currentTime = jumpTo;
247
212
  }
248
213
  };
249
214
 
@@ -71,6 +71,9 @@ shaka.media.MediaSourceEngine = class {
71
71
  /** @private {shaka.text.TextEngine} */
72
72
  this.textEngine_ = null;
73
73
 
74
+ /** @private {boolean} */
75
+ this.segmentRelativeVttTiming_ = false;
76
+
74
77
  const onMetadataNoOp = (metadata, timestampOffset, segmentEnd) => {};
75
78
 
76
79
  /** @private {!function(!Array.<shaka.extern.ID3Metadata>,
@@ -103,6 +106,12 @@ shaka.media.MediaSourceEngine = class {
103
106
 
104
107
  /** @private {string} */
105
108
  this.url_ = '';
109
+
110
+ /** @private {boolean} */
111
+ this.sequenceMode_ = false;
112
+
113
+ /** @private {!shaka.util.PublicPromise.<number>} */
114
+ this.textSequenceModeOffset_ = new shaka.util.PublicPromise();
106
115
  }
107
116
 
108
117
  /**
@@ -201,6 +210,8 @@ shaka.media.MediaSourceEngine = class {
201
210
  // TTML types
202
211
  'application/ttml+xml',
203
212
  'application/mp4; codecs="stpp"',
213
+ // Containerless types
214
+ ...shaka.media.MediaSourceEngine.RAW_FORMATS,
204
215
  ];
205
216
 
206
217
  const support = {};
@@ -315,14 +326,19 @@ shaka.media.MediaSourceEngine = class {
315
326
  * according to MediaSourceEngine.isStreamSupported.
316
327
  * @param {boolean} forceTransmuxTS
317
328
  * If true, this will transmux TS content even if it is natively supported.
329
+ * @param {boolean=} sequenceMode
330
+ * If true, the media segments are appended to the SourceBuffer in strict
331
+ * sequence.
318
332
  *
319
333
  * @return {!Promise}
320
334
  */
321
- async init(streamsByType, forceTransmuxTS) {
335
+ async init(streamsByType, forceTransmuxTS, sequenceMode=false) {
322
336
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
323
337
 
324
338
  await this.mediaSourceOpen_;
325
339
 
340
+ this.sequenceMode_ = sequenceMode;
341
+
326
342
  for (const contentType of streamsByType.keys()) {
327
343
  const stream = streamsByType.get(contentType);
328
344
  goog.asserts.assert(
@@ -332,7 +348,7 @@ shaka.media.MediaSourceEngine = class {
332
348
  let mimeType = shaka.util.MimeUtils.getFullType(
333
349
  stream.mimeType, stream.codecs);
334
350
  if (contentType == ContentType.TEXT) {
335
- this.reinitText(mimeType);
351
+ this.reinitText(mimeType, sequenceMode);
336
352
  } else {
337
353
  if ((forceTransmuxTS || !MediaSource.isTypeSupported(mimeType)) &&
338
354
  shaka.media.Transmuxer.isSupported(mimeType, contentType)) {
@@ -340,7 +356,9 @@ shaka.media.MediaSourceEngine = class {
340
356
  mimeType =
341
357
  shaka.media.Transmuxer.convertTsCodecs(contentType, mimeType);
342
358
  }
359
+
343
360
  const sourceBuffer = this.mediaSource_.addSourceBuffer(mimeType);
361
+
344
362
  this.eventManager_.listen(
345
363
  sourceBuffer, 'error',
346
364
  () => this.onError_(contentType));
@@ -358,12 +376,14 @@ shaka.media.MediaSourceEngine = class {
358
376
  /**
359
377
  * Reinitialize the TextEngine for a new text type.
360
378
  * @param {string} mimeType
379
+ * @param {boolean} sequenceMode
361
380
  */
362
- reinitText(mimeType) {
381
+ reinitText(mimeType, sequenceMode) {
363
382
  if (!this.textEngine_) {
364
383
  this.textEngine_ = new shaka.text.TextEngine(this.textDisplayer_);
365
384
  }
366
- this.textEngine_.initParser(mimeType);
385
+ this.textEngine_.initParser(mimeType, sequenceMode,
386
+ this.segmentRelativeVttTiming_);
367
387
  }
368
388
 
369
389
  /**
@@ -410,17 +430,15 @@ shaka.media.MediaSourceEngine = class {
410
430
  *
411
431
  * @param {shaka.util.ManifestParserUtils.ContentType} contentType
412
432
  * @param {number} time Playhead time
413
- * @param {number=} smallGapLimit
414
433
  * @return {boolean}
415
434
  */
416
- isBuffered(contentType, time, smallGapLimit) {
435
+ isBuffered(contentType, time) {
417
436
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
418
437
  if (contentType == ContentType.TEXT) {
419
438
  return this.textEngine_.isBuffered(time);
420
439
  } else {
421
440
  const buffered = this.getBuffered_(contentType);
422
- return shaka.media.TimeRangesUtils.isBuffered(
423
- buffered, time, smallGapLimit);
441
+ return shaka.media.TimeRangesUtils.isBuffered(buffered, time);
424
442
  }
425
443
  }
426
444
 
@@ -502,20 +520,30 @@ shaka.media.MediaSourceEngine = class {
502
520
  * @param {?number} endTime relative to the start of the presentation
503
521
  * @param {?boolean} hasClosedCaptions True if the buffer contains CEA closed
504
522
  * captions
523
+ * @param {boolean=} seeked True if we just seeked
505
524
  * @return {!Promise}
506
525
  */
507
- async appendBuffer(contentType, data, startTime, endTime, hasClosedCaptions) {
526
+ async appendBuffer(
527
+ contentType, data, startTime, endTime, hasClosedCaptions, seeked) {
508
528
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
509
529
 
510
530
  if (contentType == ContentType.TEXT) {
531
+ if (this.sequenceMode_) {
532
+ // This won't be known until the first video segment is appended.
533
+ const offset = await this.textSequenceModeOffset_;
534
+ this.textEngine_.setTimestampOffset(offset);
535
+ }
511
536
  await this.textEngine_.appendBuffer(data, startTime, endTime);
512
- } else if (this.transmuxers_[contentType]) {
537
+ return;
538
+ }
539
+
540
+ if (this.transmuxers_[contentType]) {
513
541
  const transmuxedData =
514
542
  await this.transmuxers_[contentType].transmux(data);
515
543
  // For HLS CEA-608/708 CLOSED-CAPTIONS, text data is embedded in
516
544
  // the video stream, so textEngine may not have been initialized.
517
545
  if (!this.textEngine_) {
518
- this.reinitText('text/vtt');
546
+ this.reinitText('text/vtt', this.sequenceMode_);
519
547
  }
520
548
 
521
549
  if (transmuxedData.metadata) {
@@ -534,15 +562,10 @@ shaka.media.MediaSourceEngine = class {
534
562
  closedCaptions, startTime, endTime, videoOffset);
535
563
  }
536
564
 
537
- let transmuxedSegment = transmuxedData.data;
538
- transmuxedSegment = this.workAroundBrokenPlatforms_(
539
- transmuxedSegment, startTime, contentType);
540
-
541
- await this.enqueueOperation_(
542
- contentType, () => this.append_(contentType, transmuxedSegment));
565
+ data = transmuxedData.data;
543
566
  } else if (hasClosedCaptions) {
544
567
  if (!this.textEngine_) {
545
- this.reinitText('text/vtt');
568
+ this.reinitText('text/vtt', this.sequenceMode_);
546
569
  }
547
570
  // If it is the init segment for closed captions, initialize the closed
548
571
  // caption parser.
@@ -557,19 +580,78 @@ shaka.media.MediaSourceEngine = class {
557
580
  closedCaptions, startTime, endTime, videoOffset);
558
581
  }
559
582
  }
583
+ }
560
584
 
561
- data = this.workAroundBrokenPlatforms_(data, startTime, contentType);
585
+ data = this.workAroundBrokenPlatforms_(data, startTime, contentType);
586
+
587
+ const sourceBuffer = this.sourceBuffers_[contentType];
588
+ const SEQUENCE = shaka.media.MediaSourceEngine.SourceBufferMode_.SEQUENCE;
589
+
590
+ if (this.sequenceMode_ && sourceBuffer.mode != SEQUENCE &&
591
+ startTime != null) {
592
+ // This is the first media segment to be appended to a SourceBuffer in
593
+ // sequence mode. We set the mode late so that we can trick MediaSource
594
+ // into extracting a timestamp for us to align text segments in sequence
595
+ // mode.
596
+
597
+ // Timestamps can only be reliably extracted from video, not audio.
598
+ // Packed audio formats do not have internal timestamps at all.
599
+ // Prefer video for this when available.
600
+ const isBestSourceBufferForTimestamps =
601
+ contentType == ContentType.VIDEO ||
602
+ !(ContentType.VIDEO in this.sourceBuffers_);
603
+ if (isBestSourceBufferForTimestamps) {
604
+ // Append the segment in segments mode first, with offset of 0 and an
605
+ // open append window.
606
+ const originalRange =
607
+ [sourceBuffer.appendWindowStart, sourceBuffer.appendWindowEnd];
608
+ sourceBuffer.appendWindowStart = 0;
609
+ sourceBuffer.appendWindowEnd = Infinity;
610
+
611
+ const originalOffset = sourceBuffer.timestampOffset;
612
+ sourceBuffer.timestampOffset = 0;
613
+
614
+ await this.enqueueOperation_(
615
+ contentType, () => this.append_(contentType, data));
616
+
617
+ // Reset the offset and append window.
618
+ sourceBuffer.timestampOffset = originalOffset;
619
+ sourceBuffer.appendWindowStart = originalRange[0];
620
+ sourceBuffer.appendWindowEnd = originalRange[1];
621
+
622
+ // Now get the timestamp of the segment and compute the offset for text
623
+ // segments.
624
+ const mediaStartTime = shaka.media.TimeRangesUtils.bufferStart(
625
+ this.getBuffered_(contentType));
626
+ const textOffset = (startTime || 0) - (mediaStartTime || 0);
627
+ this.textSequenceModeOffset_.resolve(textOffset);
628
+
629
+ // Finally, clear the buffer.
630
+ await this.enqueueOperation_(
631
+ contentType,
632
+ () => this.remove_(contentType, 0, this.mediaSource_.duration));
633
+ }
562
634
 
563
- await this.enqueueOperation_(
564
- contentType,
565
- () => this.append_(contentType, data));
566
- } else {
567
- data = this.workAroundBrokenPlatforms_(data, startTime, contentType);
635
+ // Now switch to sequence mode and fall through to our normal operations.
636
+ sourceBuffer.mode = SEQUENCE;
637
+ }
568
638
 
569
- await this.enqueueOperation_(
570
- contentType,
571
- () => this.append_(contentType, data));
639
+ if (startTime != null && this.sequenceMode_ &&
640
+ contentType != ContentType.TEXT) {
641
+ // In sequence mode, for non-text streams, if we just cleared the buffer
642
+ // and are performing an unbuffered seek, we need to set a new
643
+ // timestampOffset on the sourceBuffer.
644
+ if (seeked) {
645
+ const timestampOffset = /** @type {number} */ (startTime);
646
+ this.enqueueOperation_(
647
+ contentType,
648
+ () => this.setTimestampOffset_(contentType, timestampOffset));
649
+ }
572
650
  }
651
+
652
+ await this.enqueueOperation_(
653
+ contentType,
654
+ () => this.append_(contentType, data));
573
655
  }
574
656
 
575
657
  /**
@@ -670,13 +752,18 @@ shaka.media.MediaSourceEngine = class {
670
752
  * @param {number} appendWindowEnd The timestamp to set the append window end
671
753
  * to. For future appends, frames/samples with timestamps greater than this
672
754
  * value will be dropped.
755
+ * @param {boolean} sequenceMode If true, the timestampOffset will not be
756
+ * applied in this step.
673
757
  * @return {!Promise}
674
758
  */
675
759
  async setStreamProperties(
676
- contentType, timestampOffset, appendWindowStart, appendWindowEnd) {
760
+ contentType, timestampOffset, appendWindowStart, appendWindowEnd,
761
+ sequenceMode) {
677
762
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
678
763
  if (contentType == ContentType.TEXT) {
679
- this.textEngine_.setTimestampOffset(timestampOffset);
764
+ if (!sequenceMode) {
765
+ this.textEngine_.setTimestampOffset(timestampOffset);
766
+ }
680
767
  this.textEngine_.setAppendWindow(appendWindowStart, appendWindowEnd);
681
768
  return;
682
769
  }
@@ -693,7 +780,10 @@ shaka.media.MediaSourceEngine = class {
693
780
  this.enqueueOperation_(
694
781
  contentType,
695
782
  () => this.abort_(contentType)),
696
- this.enqueueOperation_(
783
+ // Don't set the timestampOffset here when in sequenceMode, since we
784
+ // use timestampOffset for a different purpose in that mode (e.g. to
785
+ // indicate where the current segment is).
786
+ sequenceMode ? Promise.resolve() : this.enqueueOperation_(
697
787
  contentType,
698
788
  () => this.setTimestampOffset_(contentType, timestampOffset)),
699
789
  this.enqueueOperation_(
@@ -1085,6 +1175,13 @@ shaka.media.MediaSourceEngine = class {
1085
1175
  }
1086
1176
  }
1087
1177
 
1178
+ /**
1179
+ * @param {boolean} segmentRelativeVttTiming
1180
+ */
1181
+ setSegmentRelativeVttTiming(segmentRelativeVttTiming) {
1182
+ this.segmentRelativeVttTiming_ = segmentRelativeVttTiming;
1183
+ }
1184
+
1088
1185
  /**
1089
1186
  * Apply platform-specific transformations to this segment to work around
1090
1187
  * issues in the platform.
@@ -1145,3 +1242,26 @@ shaka.media.MediaSourceEngine.createObjectURL = window.URL.createObjectURL;
1145
1242
  * The PublicPromise which is associated with this operation.
1146
1243
  */
1147
1244
  shaka.media.MediaSourceEngine.Operation;
1245
+
1246
+
1247
+ /**
1248
+ * @enum {string}
1249
+ * @private
1250
+ */
1251
+ shaka.media.MediaSourceEngine.SourceBufferMode_ = {
1252
+ SEQUENCE: 'sequence',
1253
+ SEGMENTS: 'segments',
1254
+ };
1255
+
1256
+
1257
+ /**
1258
+ * MIME types of raw formats.
1259
+ *
1260
+ * @const {!Array.<string>}
1261
+ */
1262
+ shaka.media.MediaSourceEngine.RAW_FORMATS = [
1263
+ 'audio/aac',
1264
+ 'audio/ac3',
1265
+ 'audio/ec3',
1266
+ 'audio/mpeg',
1267
+ ];