shaka-player 3.2.2 → 3.3.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 (188) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +1 -1
  3. package/.github/ISSUE_TEMPLATE/question.md +1 -1
  4. package/.github/workflows/build_and_test.yaml +73 -0
  5. package/.github/workflows/release.yaml +53 -0
  6. package/AUTHORS +6 -5
  7. package/CHANGELOG.md +314 -1
  8. package/CONTRIBUTORS +4 -2
  9. package/build/checkversion.py +1 -1
  10. package/build/conformance.textproto +183 -169
  11. package/build/test.py +6 -0
  12. package/build/types/core +3 -0
  13. package/build/types/offline +1 -0
  14. package/build/types/polyfill +1 -0
  15. package/build/types/ui +2 -0
  16. package/demo/common/assets.js +18 -0
  17. package/demo/common/message_ids.js +13 -1
  18. package/demo/config.js +31 -1
  19. package/demo/index.html +1 -1
  20. package/demo/locales/en.json +12 -0
  21. package/demo/locales/source.json +48 -0
  22. package/dist/controls.css +26 -2
  23. package/dist/controls.css.map +1 -1
  24. package/dist/demo.compiled.debug.js +19 -16
  25. package/dist/demo.compiled.debug.map +3 -3
  26. package/dist/demo.compiled.js +19 -16
  27. package/dist/demo.compiled.map +3 -3
  28. package/dist/deps.js +21 -14
  29. package/dist/locales.js +31 -29
  30. package/dist/receiver.compiled.debug.js +11 -9
  31. package/dist/receiver.compiled.debug.map +3 -3
  32. package/dist/receiver.compiled.js +11 -9
  33. package/dist/receiver.compiled.map +3 -3
  34. package/dist/shaka-player.compiled.d.ts +232 -18
  35. package/dist/shaka-player.compiled.debug.d.ts +232 -18
  36. package/dist/shaka-player.compiled.debug.externs.js +293 -17
  37. package/dist/shaka-player.compiled.debug.js +481 -422
  38. package/dist/shaka-player.compiled.debug.map +4 -4
  39. package/dist/shaka-player.compiled.externs.js +293 -17
  40. package/dist/shaka-player.compiled.js +846 -817
  41. package/dist/shaka-player.compiled.map +4 -4
  42. package/dist/shaka-player.ui.d.ts +250 -30
  43. package/dist/shaka-player.ui.debug.d.ts +250 -30
  44. package/dist/shaka-player.ui.debug.externs.js +322 -28
  45. package/dist/shaka-player.ui.debug.js +630 -553
  46. package/dist/shaka-player.ui.debug.map +4 -4
  47. package/dist/shaka-player.ui.externs.js +322 -28
  48. package/dist/shaka-player.ui.js +1048 -1006
  49. package/dist/shaka-player.ui.map +4 -4
  50. package/docs/design/bg-fetch-after.gv +29 -0
  51. package/docs/design/bg-fetch-after.gv.png +0 -0
  52. package/docs/design/bg-fetch-before.gv +18 -0
  53. package/docs/design/bg-fetch-before.gv.png +0 -0
  54. package/docs/design/bg-fetch.md +134 -0
  55. package/docs/tutorials/blob-url.md +15 -0
  56. package/docs/tutorials/index.json +1 -0
  57. package/docs/tutorials/ui-customization.md +66 -0
  58. package/docs/tutorials/ui.md +5 -5
  59. package/externs/cmcd.js +186 -0
  60. package/externs/ima.js +3 -0
  61. package/externs/shaka/ads.js +5 -0
  62. package/externs/shaka/manifest.js +12 -1
  63. package/externs/shaka/manifest_parser.js +10 -1
  64. package/externs/shaka/net.js +20 -4
  65. package/externs/shaka/offline.js +31 -2
  66. package/externs/shaka/player.js +138 -5
  67. package/externs/shaka/text.js +24 -0
  68. package/externs/webcrypto.js +18 -0
  69. package/karma.conf.js +19 -15
  70. package/lib/abr/ewma.js +14 -0
  71. package/lib/abr/ewma_bandwidth_estimator.js +16 -2
  72. package/lib/abr/simple_abr_manager.js +6 -0
  73. package/lib/ads/ad_manager.js +1 -0
  74. package/lib/ads/client_side_ad.js +8 -0
  75. package/lib/ads/client_side_ad_manager.js +8 -4
  76. package/lib/ads/server_side_ad.js +7 -0
  77. package/lib/cast/cast_proxy.js +3 -0
  78. package/lib/cast/cast_receiver.js +3 -0
  79. package/lib/cast/cast_utils.js +1 -0
  80. package/lib/dash/dash_parser.js +79 -33
  81. package/lib/dash/segment_base.js +25 -1
  82. package/lib/dash/segment_list.js +18 -9
  83. package/lib/dash/segment_template.js +23 -10
  84. package/lib/hls/hls_parser.js +116 -23
  85. package/lib/hls/manifest_text_parser.js +1 -0
  86. package/lib/media/quality_observer.js +274 -0
  87. package/lib/media/region_observer.js +39 -33
  88. package/lib/media/region_timeline.js +17 -19
  89. package/lib/media/segment_index.js +8 -1
  90. package/lib/media/segment_reference.js +37 -4
  91. package/lib/media/streaming_engine.js +47 -6
  92. package/lib/net/http_fetch_plugin.js +31 -11
  93. package/lib/net/http_plugin_utils.js +1 -0
  94. package/lib/net/http_xhr_plugin.js +40 -14
  95. package/lib/net/networking_engine.js +85 -18
  96. package/lib/offline/download_info.js +68 -0
  97. package/lib/offline/download_manager.js +13 -5
  98. package/lib/offline/indexeddb/base_storage_cell.js +35 -0
  99. package/lib/offline/indexeddb/storage_mechanism.js +1 -1
  100. package/lib/offline/indexeddb/v5_storage_cell.js +5 -0
  101. package/lib/offline/storage.js +401 -293
  102. package/lib/offline/stored_content_utils.js +2 -0
  103. package/lib/player.js +254 -27
  104. package/lib/polyfill/media_capabilities.js +30 -15
  105. package/lib/polyfill/random_uuid.js +52 -0
  106. package/lib/text/cue.js +18 -0
  107. package/lib/text/ttml_text_parser.js +109 -60
  108. package/lib/text/ui_text_displayer.js +19 -13
  109. package/lib/util/cmcd_manager.js +672 -0
  110. package/lib/util/error.js +6 -0
  111. package/lib/util/fake_event_target.js +21 -1
  112. package/lib/util/language_utils.js +12 -0
  113. package/lib/util/mutex.js +52 -0
  114. package/lib/util/periods.js +98 -64
  115. package/lib/util/player_configuration.js +18 -0
  116. package/lib/util/stream_utils.js +86 -10
  117. package/lib/util/xml_utils.js +19 -0
  118. package/package.json +3 -6
  119. package/shaka-player.uncompiled.js +3 -0
  120. package/test/dash/dash_parser_content_protection_unit.js +2 -0
  121. package/test/dash/dash_parser_live_unit.js +144 -1
  122. package/test/dash/dash_parser_manifest_unit.js +37 -2
  123. package/test/dash/dash_parser_segment_base_unit.js +2 -0
  124. package/test/dash/dash_parser_segment_list_unit.js +2 -1
  125. package/test/dash/dash_parser_segment_template_unit.js +2 -1
  126. package/test/hls/hls_live_unit.js +2 -0
  127. package/test/hls/hls_parser_unit.js +76 -0
  128. package/test/media/drm_engine_unit.js +3 -3
  129. package/test/media/quality_observer_unit.js +142 -0
  130. package/test/media/region_observer_unit.js +40 -5
  131. package/test/media/region_timeline_unit.js +40 -22
  132. package/test/media/segment_reference_unit.js +16 -1
  133. package/test/media/streaming_engine_integration.js +4 -2
  134. package/test/media/streaming_engine_unit.js +147 -0
  135. package/test/net/http_plugin_unit.js +20 -15
  136. package/test/offline/storage_integration.js +108 -4
  137. package/test/player_integration.js +140 -8
  138. package/test/player_src_equals_integration.js +41 -8
  139. package/test/player_unit.js +19 -9
  140. package/test/test/assets/chapters.vtt +2 -2
  141. package/test/test/assets/chapters2.vtt +10 -0
  142. package/test/test/util/dash_parser_util.js +4 -0
  143. package/test/test/util/fake_ad.js +7 -0
  144. package/test/test/util/fake_drm_engine.js +9 -2
  145. package/test/test/util/manifest_generator.js +3 -2
  146. package/test/test/util/test_scheme.js +14 -1
  147. package/test/test/util/ttml_utils.js +81 -0
  148. package/test/test/util/util.js +10 -7
  149. package/test/text/mp4_ttml_parser_unit.js +11 -18
  150. package/test/text/ttml_text_parser_unit.js +544 -228
  151. package/test/ui/text_displayer_layout_unit.js +8 -7
  152. package/test/ui/ui_unit.js +250 -0
  153. package/test/util/cmcd_manager_unit.js +291 -0
  154. package/test/util/stream_utils_unit.js +36 -30
  155. package/third_party/closure-uri/uri.js +21 -0
  156. package/ui/airplay_button.js +1 -0
  157. package/ui/audio_language_selection.js +7 -0
  158. package/ui/cast_button.js +1 -0
  159. package/ui/context_menu.js +109 -0
  160. package/ui/controls.js +44 -7
  161. package/ui/controls.less +1 -0
  162. package/ui/enums.js +4 -0
  163. package/ui/externs/ui.js +28 -1
  164. package/ui/fast_forward_button.js +14 -5
  165. package/ui/fullscreen_button.js +1 -0
  166. package/ui/less/buttons.less +0 -4
  167. package/ui/less/containers.less +74 -0
  168. package/ui/less/overflow_menu.less +0 -10
  169. package/ui/less/tooltip.less +97 -0
  170. package/ui/locales/en.json +1 -0
  171. package/ui/locales/source.json +4 -0
  172. package/ui/localization.js +12 -36
  173. package/ui/loop_button.js +5 -0
  174. package/ui/mute_button.js +2 -1
  175. package/ui/overflow_menu.js +5 -25
  176. package/ui/pip_button.js +5 -0
  177. package/ui/play_button.js +2 -2
  178. package/ui/playback_rate_selection.js +8 -9
  179. package/ui/resolution_selection.js +7 -0
  180. package/ui/rewind_button.js +15 -6
  181. package/ui/seek_bar.js +4 -2
  182. package/ui/settings_menu.js +36 -4
  183. package/ui/small_play_button.js +1 -0
  184. package/ui/statistics_button.js +253 -0
  185. package/ui/text_selection.js +12 -4
  186. package/ui/ui.js +30 -0
  187. package/ui/volume_bar.js +1 -1
  188. package/externs/media_capabilities.js +0 -16
@@ -13,6 +13,7 @@ goog.require('shaka.log');
13
13
  goog.require('shaka.media.DrmEngine');
14
14
  goog.require('shaka.media.ManifestParser');
15
15
  goog.require('shaka.net.NetworkingEngine');
16
+ goog.require('shaka.offline.DownloadInfo');
16
17
  goog.require('shaka.offline.DownloadManager');
17
18
  goog.require('shaka.offline.OfflineUri');
18
19
  goog.require('shaka.offline.SessionDeleter');
@@ -28,11 +29,10 @@ goog.require('shaka.util.Functional');
28
29
  goog.require('shaka.util.IDestroyable');
29
30
  goog.require('shaka.util.Iterables');
30
31
  goog.require('shaka.util.MimeUtils');
31
- goog.require('shaka.util.Networking');
32
+ goog.require('shaka.util.Mutex');
32
33
  goog.require('shaka.util.Platform');
33
34
  goog.require('shaka.util.PlayerConfiguration');
34
35
  goog.require('shaka.util.StreamUtils');
35
- goog.requireType('shaka.media.InitSegmentReference');
36
36
  goog.requireType('shaka.media.SegmentReference');
37
37
  goog.requireType('shaka.offline.StorageCellHandle');
38
38
 
@@ -95,14 +95,6 @@ shaka.offline.Storage = class {
95
95
  this.networkingEngine_ = new shaka.net.NetworkingEngine();
96
96
  }
97
97
 
98
- /**
99
- * A list of segment ids for all the segments that were added during the
100
- * current store. If the store fails or is aborted, these need to be
101
- * removed from storage.
102
- * @private {!Array.<number>}
103
- */
104
- this.segmentsFromStore_ = [];
105
-
106
98
  /**
107
99
  * A list of open operations that are being performed by this instance of
108
100
  * |shaka.offline.Storage|.
@@ -285,7 +277,7 @@ shaka.offline.Storage = class {
285
277
  store(uri, appMetadata, mimeType) {
286
278
  goog.asserts.assert(
287
279
  this.networkingEngine_,
288
- 'Cannot call |downloadManifest_| after calling |destroy|.');
280
+ 'Cannot call |store| after calling |destroy|.');
289
281
 
290
282
  // Get a copy of the current config.
291
283
  const config = this.getConfiguration();
@@ -371,6 +363,8 @@ shaka.offline.Storage = class {
371
363
  const muxer = new shaka.offline.StorageMuxer();
372
364
  /** @type {?shaka.offline.StorageCellHandle} */
373
365
  let activeHandle = null;
366
+ /** @type {?number} */
367
+ let manifestId = null;
374
368
 
375
369
  // This will be used to store any errors from drm engine. Whenever drm
376
370
  // engine is passed to another function to do work, we should check if this
@@ -398,6 +392,7 @@ shaka.offline.Storage = class {
398
392
  uri);
399
393
  }
400
394
 
395
+ // Create the DRM engine, and load the keys in the manifest.
401
396
  drmEngine = await this.createDrmEngine(
402
397
  manifest,
403
398
  (e) => { drmError = drmError || e; },
@@ -422,38 +417,38 @@ shaka.offline.Storage = class {
422
417
  this.ensureNotDestroyed_();
423
418
 
424
419
  goog.asserts.assert(drmEngine, 'drmEngine should be non-null here.');
420
+ const {manifestDB, toDownload} = this.makeManifestDB_(
421
+ drmEngine, manifest, uri, appMetadata, config, downloader);
425
422
 
426
- const manifestDB = await this.downloadManifest_(
427
- activeHandle.cell, drmEngine, manifest, uri, appMetadata, config,
428
- downloader);
423
+ // Store the empty manifest, before downloading the segments.
424
+ const ids = await activeHandle.cell.addManifests([manifestDB]);
425
+ this.ensureNotDestroyed_();
426
+ manifestId = ids[0];
427
+
428
+ goog.asserts.assert(drmEngine, 'drmEngine should be non-null here.');
429
429
  this.ensureNotDestroyed_();
430
430
  if (drmError) {
431
431
  throw drmError;
432
432
  }
433
433
 
434
- const ids = await activeHandle.cell.addManifests([manifestDB]);
434
+ await this.downloadSegments_(toDownload, manifestId, manifestDB,
435
+ downloader, config, activeHandle.cell, manifest, drmEngine);
435
436
  this.ensureNotDestroyed_();
436
437
 
437
438
  const offlineUri = shaka.offline.OfflineUri.manifest(
438
- activeHandle.path.mechanism, activeHandle.path.cell, ids[0]);
439
+ activeHandle.path.mechanism, activeHandle.path.cell, manifestId);
439
440
 
440
441
  return shaka.offline.StoredContentUtils.fromManifestDB(
441
442
  offlineUri, manifestDB);
442
443
  } catch (e) {
443
- // If we did start saving some data, we need to remove it all to avoid
444
- // wasting storage. However if the muxer did not manage to initialize,
445
- // then we won't have an active cell to remove the segments from.
446
- if (activeHandle) {
447
- await activeHandle.cell.removeSegments(
448
- this.segmentsFromStore_, () => {});
444
+ if (manifestId != null) {
445
+ await shaka.offline.Storage.cleanStoredManifest(manifestId);
449
446
  }
450
447
 
451
448
  // If we already had an error, ignore this error to avoid hiding
452
449
  // the original error.
453
450
  throw drmError || e;
454
451
  } finally {
455
- this.segmentsFromStore_ = [];
456
-
457
452
  await muxer.destroy();
458
453
 
459
454
  if (parser) {
@@ -466,6 +461,215 @@ shaka.offline.Storage = class {
466
461
  }
467
462
  }
468
463
 
464
+ /**
465
+ * Download and then store the contents of each segment.
466
+ * The promise this returns will wait for local downloads.
467
+ *
468
+ * @param {!Array.<!shaka.offline.DownloadInfo>} toDownload
469
+ * @param {number} manifestId
470
+ * @param {shaka.extern.ManifestDB} manifestDB
471
+ * @param {!shaka.offline.DownloadManager} downloader
472
+ * @param {shaka.extern.PlayerConfiguration} config
473
+ * @param {shaka.extern.StorageCell} storage
474
+ * @param {shaka.extern.Manifest} manifest
475
+ * @param {!shaka.media.DrmEngine} drmEngine
476
+ * @return {!Promise}
477
+ * @private
478
+ */
479
+ async downloadSegments_(
480
+ toDownload, manifestId, manifestDB, downloader, config, storage,
481
+ manifest, drmEngine) {
482
+ /**
483
+ * @param {!Array.<!shaka.offline.DownloadInfo>} toDownload
484
+ * @param {boolean} updateDRM
485
+ */
486
+ const download = async (toDownload, updateDRM) => {
487
+ const throwIfAbortedFn = () => {
488
+ this.ensureNotDestroyed_();
489
+ };
490
+ for (const download of toDownload) {
491
+ /** @param {?BufferSource} data */
492
+ let data;
493
+ const request = download.makeSegmentRequest(config);
494
+ const estimateId = download.estimateId;
495
+ const isInitSegment = download.isInitSegment;
496
+ const onDownloaded = (d) => {
497
+ data = d;
498
+ return Promise.resolve();
499
+ };
500
+ downloader.queue(download.groupId,
501
+ request, estimateId, isInitSegment, onDownloaded);
502
+ downloader.queueWork(download.groupId, async () => {
503
+ goog.asserts.assert(data, 'We should have loaded data by now');
504
+ goog.asserts.assert(data instanceof ArrayBuffer,
505
+ 'The data should be an ArrayBuffer');
506
+
507
+ const ref = /** @type {!shaka.media.SegmentReference} */ (
508
+ download.ref);
509
+ manifestDB = (await shaka.offline.Storage.assignStreamToManifest(
510
+ manifestId, ref, {data}, throwIfAbortedFn)) || manifestDB;
511
+ });
512
+ }
513
+ await downloader.waitToFinish();
514
+
515
+ if (updateDRM) {
516
+ // Re-store the manifest, to attach session IDs.
517
+ // These were (maybe) discovered inside the downloader; we can only add
518
+ // them now, at the end, since the manifestDB is in flux during the
519
+ // process of downloading and storing, and assignStreamToManifest does
520
+ // not know about the DRM engine.
521
+ this.ensureNotDestroyed_();
522
+ this.setManifestDrmFields_(manifest, manifestDB, drmEngine, config);
523
+ await storage.updateManifest(manifestId, manifestDB);
524
+ }
525
+ };
526
+
527
+ const usingBgFetch = false; // TODO: Get.
528
+
529
+ if (this.getManifestIsEncrypted_(manifest) && usingBgFetch &&
530
+ !this.getManifestIncludesInitData_(manifest)) {
531
+ // Background fetch can't make DRM sessions, so if we have to get the
532
+ // init data from the init segments, download those first before anything
533
+ // else.
534
+ await download(toDownload.filter((info) => info.isInitSegment), true);
535
+ this.ensureNotDestroyed_();
536
+ toDownload = toDownload.filter((info) => !info.isInitSegment);
537
+ }
538
+
539
+ if (!usingBgFetch) {
540
+ await download(toDownload, false);
541
+ this.ensureNotDestroyed_();
542
+
543
+ goog.asserts.assert(
544
+ !manifestDB.isIncomplete, 'The manifest should be complete by now');
545
+ } else {
546
+ // TODO: Send the request to the service worker. Don't await the result.
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Removes all of the contents for a given manifest, statelessly.
552
+ *
553
+ * @param {number} manifestId
554
+ * @return {!Promise}
555
+ */
556
+ static async cleanStoredManifest(manifestId) {
557
+ const muxer = new shaka.offline.StorageMuxer();
558
+ await muxer.init();
559
+ const activeHandle = await muxer.getActive();
560
+ const uri = shaka.offline.OfflineUri.manifest(
561
+ activeHandle.path.mechanism,
562
+ activeHandle.path.cell,
563
+ manifestId);
564
+ await muxer.destroy();
565
+ const storage = new shaka.offline.Storage();
566
+ await storage.remove(uri.toString());
567
+ }
568
+
569
+ /**
570
+ * Load the given manifest, modifies it by assigning the given data to the
571
+ * segments corresponding to "ref", then re-stores the manifest.
572
+ * The parts of this function that modify the manifest are protected by a
573
+ * mutex, to prevent race conditions; specifically, it prevents two parallel
574
+ * instances of this method from both loading the manifest into memory at the
575
+ * same time, which would result in the slower/later call overwriting the
576
+ * changes of the other.
577
+ *
578
+ * @param {number} manifestId
579
+ * @param {!shaka.media.SegmentReference} ref
580
+ * @param {shaka.extern.SegmentDataDB} data
581
+ * @param {function()} throwIfAbortedFn A function that should throw if the
582
+ * download has been aborted.
583
+ * @return {!Promise.<?shaka.extern.ManifestDB>}
584
+ */
585
+ static async assignStreamToManifest(manifestId, ref, data, throwIfAbortedFn) {
586
+ /** @type {shaka.offline.StorageMuxer} */
587
+ const muxer = new shaka.offline.StorageMuxer();
588
+
589
+ const idForRef = shaka.offline.DownloadInfo.idForSegmentRef(ref);
590
+ let manifestUpdated = false;
591
+ let dataKey;
592
+ let activeHandle;
593
+ /** @type {!shaka.extern.ManifestDB} */
594
+ let manifestDB;
595
+
596
+ let mutexId = 0;
597
+
598
+ try {
599
+ await muxer.init();
600
+ activeHandle = await muxer.getActive();
601
+
602
+ // Store the data.
603
+ const dataKeys = await activeHandle.cell.addSegments([data]);
604
+ dataKey = dataKeys[0];
605
+ throwIfAbortedFn();
606
+
607
+ // Acquire the mutex before accessing the manifest, since there could be
608
+ // multiple instances of this method running at once.
609
+ mutexId = await shaka.offline.Storage.mutex_.acquire();
610
+ throwIfAbortedFn();
611
+
612
+ // Load the manifest.
613
+ const manifests = await activeHandle.cell.getManifests([manifestId]);
614
+ throwIfAbortedFn();
615
+ manifestDB = manifests[0];
616
+
617
+ // Assign the stored data to the manifest.
618
+ let complete = true;
619
+ for (const stream of manifestDB.streams) {
620
+ for (const segment of stream.segments) {
621
+ if (segment.pendingSegmentRefId == idForRef) {
622
+ segment.dataKey = dataKey;
623
+ // Now that the segment has been associated with the appropriate
624
+ // dataKey, the pendingSegmentRefId is no longer necessary.
625
+ segment.pendingSegmentRefId = undefined;
626
+ }
627
+ if (segment.pendingInitSegmentRefId == idForRef) {
628
+ segment.initSegmentKey = dataKey;
629
+ // Now that the init segment has been associated with the
630
+ // appropriate initSegmentKey, the pendingInitSegmentRefId is no
631
+ // longer necessary.
632
+ segment.pendingInitSegmentRefId = undefined;
633
+ }
634
+ if (segment.pendingSegmentRefId) {
635
+ complete = false;
636
+ }
637
+ if (segment.pendingInitSegmentRefId) {
638
+ complete = false;
639
+ }
640
+ }
641
+ }
642
+
643
+ // Update the size of the manifest.
644
+ manifestDB.size += data.data.byteLength;
645
+
646
+ // Mark the manifest as complete, if all segments are downloaded.
647
+ if (complete) {
648
+ manifestDB.isIncomplete = false;
649
+ }
650
+
651
+ // Re-store the manifest.
652
+ await activeHandle.cell.updateManifest(manifestId, manifestDB);
653
+ manifestUpdated = true;
654
+ throwIfAbortedFn();
655
+ } catch (e) {
656
+ await shaka.offline.Storage.cleanStoredManifest(manifestId);
657
+
658
+ if (activeHandle && !manifestUpdated && dataKey) {
659
+ // The cleanStoredManifest method will not "see" any segments that have
660
+ // been downloaded but not assigned to the manifest yet. So un-store
661
+ // them separately.
662
+ await activeHandle.cell.removeSegments([dataKey], (key) => {});
663
+ }
664
+
665
+ throw e;
666
+ } finally {
667
+ await muxer.destroy();
668
+ shaka.offline.Storage.mutex_.release(mutexId);
669
+ }
670
+ return manifestDB;
671
+ }
672
+
469
673
  /**
470
674
  * Filter |manifest| such that it will only contain the variants and text
471
675
  * streams that we want to store and can actually play.
@@ -477,16 +681,15 @@ shaka.offline.Storage = class {
477
681
  * @private
478
682
  */
479
683
  async filterManifest_(manifest, drmEngine, config) {
480
- const StreamUtils = shaka.util.StreamUtils;
481
-
482
684
  // Filter the manifest based on the restrictions given in the player
483
685
  // configuration.
484
686
  const maxHwRes = {width: Infinity, height: Infinity};
485
- StreamUtils.filterByRestrictions(manifest, config.restrictions, maxHwRes);
687
+ shaka.util.StreamUtils.filterByRestrictions(
688
+ manifest, config.restrictions, maxHwRes);
486
689
 
487
690
  // Filter the manifest based on what we know MediaCapabilities will be able
488
691
  // to play later (no point storing something we can't play).
489
- StreamUtils.filterManifestByMediaCapabilities(
692
+ await shaka.util.StreamUtils.filterManifestByMediaCapabilities(
490
693
  manifest, config.offline.usePersistentLicense);
491
694
 
492
695
  // Gather all tracks.
@@ -498,24 +701,24 @@ shaka.offline.Storage = class {
498
701
  const preferredVideoCodecs = config.preferredVideoCodecs;
499
702
  const preferredAudioCodecs = config.preferredAudioCodecs;
500
703
 
501
- StreamUtils.chooseCodecsAndFilterManifest(
704
+ shaka.util.StreamUtils.chooseCodecsAndFilterManifest(
502
705
  manifest, preferredVideoCodecs, preferredAudioCodecs,
503
706
  preferredAudioChannelCount, preferredDecodingAttributes);
504
707
 
505
708
  for (const variant of manifest.variants) {
506
709
  goog.asserts.assert(
507
- StreamUtils.isPlayable(variant),
710
+ shaka.util.StreamUtils.isPlayable(variant),
508
711
  'We should have already filtered by "is playable"');
509
712
 
510
- allTracks.push(StreamUtils.variantToTrack(variant));
713
+ allTracks.push(shaka.util.StreamUtils.variantToTrack(variant));
511
714
  }
512
715
 
513
716
  for (const text of manifest.textStreams) {
514
- allTracks.push(StreamUtils.textStreamToTrack(text));
717
+ allTracks.push(shaka.util.StreamUtils.textStreamToTrack(text));
515
718
  }
516
719
 
517
720
  for (const image of manifest.imageStreams) {
518
- allTracks.push(StreamUtils.imageStreamToTrack(image));
721
+ allTracks.push(shaka.util.StreamUtils.imageStreamToTrack(image));
519
722
  }
520
723
 
521
724
  // Let the application choose which tracks to store.
@@ -584,19 +787,21 @@ shaka.offline.Storage = class {
584
787
 
585
788
  /**
586
789
  * Create a download manager and download the manifest.
790
+ * This also sets up download infos for each segment to be downloaded.
587
791
  *
588
- * @param {shaka.extern.StorageCell} storage
589
792
  * @param {!shaka.media.DrmEngine} drmEngine
590
793
  * @param {shaka.extern.Manifest} manifest
591
794
  * @param {string} uri
592
795
  * @param {!Object} metadata
593
796
  * @param {shaka.extern.PlayerConfiguration} config
594
797
  * @param {!shaka.offline.DownloadManager} downloader
595
- * @return {!Promise.<shaka.extern.ManifestDB>}
798
+ * @return {{
799
+ * manifestDB: shaka.extern.ManifestDB,
800
+ * toDownload: !Array.<!shaka.offline.DownloadInfo>
801
+ * }}
596
802
  * @private
597
803
  */
598
- async downloadManifest_(
599
- storage, drmEngine, manifest, uri, metadata, config, downloader) {
804
+ makeManifestDB_(drmEngine, manifest, uri, metadata, config, downloader) {
600
805
  const pendingContent = shaka.offline.StoredContentUtils.fromManifest(
601
806
  uri, manifest, /* size= */ 0, metadata);
602
807
  // In https://github.com/google/shaka-player/issues/2652, we found that this
@@ -619,12 +824,73 @@ shaka.offline.Storage = class {
619
824
  };
620
825
  downloader.setCallbacks(onProgress, onInitData);
621
826
 
622
- const isEncrypted = manifest.variants.some((variant) => {
827
+ const needsInitData = this.getManifestIsEncrypted_(manifest) &&
828
+ !this.getManifestIncludesInitData_(manifest);
829
+
830
+ let currentSystemId = null;
831
+ if (needsInitData) {
832
+ const drmInfo = drmEngine.getDrmInfo();
833
+ currentSystemId =
834
+ shaka.offline.Storage.defaultSystemIds_.get(drmInfo.keySystem);
835
+ }
836
+
837
+ // Make the estimator, which is used to make the download registries.
838
+ const estimator = new shaka.offline.StreamBandwidthEstimator();
839
+ for (const stream of manifest.textStreams) {
840
+ estimator.addText(stream);
841
+ }
842
+ for (const stream of manifest.imageStreams) {
843
+ estimator.addImage(stream);
844
+ }
845
+ for (const variant of manifest.variants) {
846
+ estimator.addVariant(variant);
847
+ }
848
+ const {streams, toDownload} = this.createStreams_(
849
+ downloader, estimator, drmEngine, manifest, config);
850
+
851
+ const drmInfo = drmEngine.getDrmInfo();
852
+ const usePersistentLicense = config.offline.usePersistentLicense;
853
+ if (drmInfo && usePersistentLicense) {
854
+ // Don't store init data, since we have stored sessions.
855
+ drmInfo.initData = [];
856
+ }
857
+
858
+ const manifestDB = {
859
+ creationTime: Date.now(),
860
+ originalManifestUri: uri,
861
+ duration: manifest.presentationTimeline.getDuration(),
862
+ size: 0,
863
+ expiration: drmEngine.getExpiration(),
864
+ streams,
865
+ sessionIds: usePersistentLicense ? drmEngine.getSessionIds() : [],
866
+ drmInfo,
867
+ appMetadata: metadata,
868
+ isIncomplete: true,
869
+ };
870
+
871
+ return {manifestDB, toDownload};
872
+ }
873
+
874
+ /**
875
+ * @param {shaka.extern.Manifest} manifest
876
+ * @return {boolean}
877
+ * @private
878
+ */
879
+ getManifestIsEncrypted_(manifest) {
880
+ return manifest.variants.some((variant) => {
623
881
  const videoEncrypted = variant.video && variant.video.encrypted;
624
882
  const audioEncrypted = variant.audio && variant.audio.encrypted;
625
883
  return videoEncrypted || audioEncrypted;
626
884
  });
627
- const includesInitData = manifest.variants.some((variant) => {
885
+ }
886
+
887
+ /**
888
+ * @param {shaka.extern.Manifest} manifest
889
+ * @return {boolean}
890
+ * @private
891
+ */
892
+ getManifestIncludesInitData_(manifest) {
893
+ return manifest.variants.some((variant) => {
628
894
  const videoDrmInfos = variant.video ? variant.video.drmInfos : [];
629
895
  const audioDrmInfos = variant.audio ? variant.audio.drmInfos : [];
630
896
  const drmInfos = videoDrmInfos.concat(audioDrmInfos);
@@ -632,36 +898,28 @@ shaka.offline.Storage = class {
632
898
  return drmInfos.initData && drmInfos.initData.length;
633
899
  });
634
900
  });
635
- const needsInitData = isEncrypted && !includesInitData;
636
-
637
- let currentSystemId = null;
638
- if (needsInitData) {
639
- const drmInfo = drmEngine.getDrmInfo();
640
- currentSystemId =
641
- shaka.offline.Storage.defaultSystemIds_.get(drmInfo.keySystem);
642
- }
643
-
644
- try {
645
- const manifestDB = this.createOfflineManifest_(
646
- downloader, storage, drmEngine, manifest, uri, metadata, config);
901
+ }
647
902
 
648
- manifestDB.size = await downloader.waitToFinish();
649
- manifestDB.expiration = drmEngine.getExpiration();
650
- const sessions = drmEngine.getSessionIds();
651
- manifestDB.sessionIds = config.offline.usePersistentLicense ?
652
- sessions : [];
903
+ /**
904
+ * @param {shaka.extern.Manifest} manifest
905
+ * @param {shaka.extern.ManifestDB} manifestDB
906
+ * @param {!shaka.media.DrmEngine} drmEngine
907
+ * @param {shaka.extern.PlayerConfiguration} config
908
+ * @private
909
+ */
910
+ setManifestDrmFields_(manifest, manifestDB, drmEngine, config) {
911
+ manifestDB.expiration = drmEngine.getExpiration();
653
912
 
654
- if (isEncrypted && config.offline.usePersistentLicense &&
655
- !sessions.length) {
656
- throw new shaka.util.Error(
657
- shaka.util.Error.Severity.CRITICAL,
658
- shaka.util.Error.Category.STORAGE,
659
- shaka.util.Error.Code.NO_INIT_DATA_FOR_OFFLINE);
660
- }
913
+ const sessions = drmEngine.getSessionIds();
914
+ manifestDB.sessionIds = config.offline.usePersistentLicense ?
915
+ sessions : [];
661
916
 
662
- return manifestDB;
663
- } finally {
664
- await downloader.destroy();
917
+ if (this.getManifestIsEncrypted_(manifest) &&
918
+ config.offline.usePersistentLicense && !sessions.length) {
919
+ throw new shaka.util.Error(
920
+ shaka.util.Error.Severity.CRITICAL,
921
+ shaka.util.Error.Category.STORAGE,
922
+ shaka.util.Error.Code.NO_INIT_DATA_FOR_OFFLINE);
665
923
  }
666
924
  }
667
925
 
@@ -764,7 +1022,7 @@ shaka.offline.Storage = class {
764
1022
  * @return {!Promise}
765
1023
  * @private
766
1024
  */
767
- removeFromStorage_( storage, uri, manifest) {
1025
+ removeFromStorage_(storage, uri, manifest) {
768
1026
  /** @type {!Array.<number>} */
769
1027
  const segmentIds = shaka.offline.Storage.getAllSegmentIds_(manifest);
770
1028
 
@@ -921,6 +1179,10 @@ shaka.offline.Storage = class {
921
1179
  const playerInterface = {
922
1180
  networkingEngine: networkingEngine,
923
1181
 
1182
+ // No need to apply CMCD data for offline requests
1183
+ modifyManifestRequest: (request, manifestInfo) => {},
1184
+ modifySegmentRequest: (request, segmentInfo) => {},
1185
+
924
1186
  // Don't bother filtering now. We will do that later when we have all the
925
1187
  // information we need to filter.
926
1188
  filter: () => Promise.resolve(),
@@ -1007,100 +1269,23 @@ shaka.offline.Storage = class {
1007
1269
  }
1008
1270
 
1009
1271
  /**
1010
- * Creates an offline 'manifest' for the real manifest. This does not store
1011
- * the segments yet, only adds them to the download manager through
1012
- * createStreams_.
1013
- *
1014
- * @param {!shaka.offline.DownloadManager} downloader
1015
- * @param {shaka.extern.StorageCell} storage
1016
- * @param {!shaka.media.DrmEngine} drmEngine
1017
- * @param {shaka.extern.Manifest} manifest
1018
- * @param {string} originalManifestUri
1019
- * @param {!Object} metadata
1020
- * @param {shaka.extern.PlayerConfiguration} config
1021
- * @return {shaka.extern.ManifestDB}
1022
- * @private
1023
- */
1024
- createOfflineManifest_(
1025
- downloader, storage, drmEngine, manifest, originalManifestUri, metadata,
1026
- config) {
1027
- const estimator = new shaka.offline.StreamBandwidthEstimator();
1028
-
1029
- const streams = this.createStreams_(
1030
- downloader, storage, estimator, drmEngine, manifest, config);
1031
-
1032
- const usePersistentLicense = config.offline.usePersistentLicense;
1033
- const drmInfo = drmEngine.getDrmInfo();
1034
-
1035
- if (drmInfo && usePersistentLicense) {
1036
- // Don't store init data, since we have stored sessions.
1037
- drmInfo.initData = [];
1038
- }
1039
-
1040
- return {
1041
- creationTime: Date.now(),
1042
- originalManifestUri: originalManifestUri,
1043
- duration: manifest.presentationTimeline.getDuration(),
1044
- size: 0,
1045
- expiration: drmEngine.getExpiration(),
1046
- streams: streams,
1047
- sessionIds: usePersistentLicense ? drmEngine.getSessionIds() : [],
1048
- drmInfo: drmInfo,
1049
- appMetadata: metadata,
1050
- };
1051
- }
1052
-
1053
- /**
1054
- * Converts manifest Streams to database Streams. This will use the current
1055
- * configuration to get the tracks to use, then it will search each segment
1056
- * index and add all the segments to the download manager through
1057
- * createStream_.
1272
+ * Converts manifest Streams to database Streams.
1058
1273
  *
1059
1274
  * @param {!shaka.offline.DownloadManager} downloader
1060
- * @param {shaka.extern.StorageCell} storage
1061
1275
  * @param {shaka.offline.StreamBandwidthEstimator} estimator
1062
1276
  * @param {!shaka.media.DrmEngine} drmEngine
1063
1277
  * @param {shaka.extern.Manifest} manifest
1064
1278
  * @param {shaka.extern.PlayerConfiguration} config
1065
- * @return {!Array.<shaka.extern.StreamDB>}
1279
+ * @return {{
1280
+ * streams: !Array.<shaka.extern.StreamDB>,
1281
+ * toDownload: !Array.<!shaka.offline.DownloadInfo>
1282
+ * }}
1066
1283
  * @private
1067
1284
  */
1068
- createStreams_(downloader, storage, estimator, drmEngine, manifest, config) {
1069
- // Pass all variants and text streams to the estimator so that we can
1070
- // get the best estimate for each stream later.
1071
- for (const variant of manifest.variants) {
1072
- estimator.addVariant(variant);
1073
- }
1074
- for (const text of manifest.textStreams) {
1075
- estimator.addText(text);
1076
- }
1077
- for (const image of manifest.imageStreams) {
1078
- estimator.addImage(image);
1079
- }
1080
-
1081
- // TODO(joeyparrish): Break out stack-based state and method params into a
1082
- // separate class to clean up. See:
1083
- // https://github.com/google/shaka-player/issues/2781#issuecomment-678438039
1084
-
1085
- /**
1086
- * A cache mapping init segment references to Promises to their DB key.
1087
- *
1088
- * @type {!Map.<shaka.media.InitSegmentReference, !Promise.<?number>>}
1089
- */
1090
- const initSegmentDbKeyCache = new Map();
1091
-
1092
- // A null init segment reference always maps to a null DB key.
1093
- initSegmentDbKeyCache.set(
1094
- null, /** @type {!Promise.<?number>} */(Promise.resolve(null)));
1095
-
1096
- /**
1097
- * A cache mapping equivalent segment references to Promises to their DB
1098
- * key. The key in this map is a string of the form
1099
- * "<URI>-<startByte>-<endByte>".
1100
- *
1101
- * @type {!Map.<string, !Promise.<number>>}
1102
- */
1103
- const segmentDbKeyCache = new Map();
1285
+ createStreams_(downloader, estimator, drmEngine, manifest, config) {
1286
+ // Download infos are stored based on their refId, to dedup them.
1287
+ /** @type {!Map.<string, !shaka.offline.DownloadInfo>} */
1288
+ const toDownload = new Map();
1104
1289
 
1105
1290
  // Find the streams we want to download and create a stream db instance
1106
1291
  // for each of them.
@@ -1110,8 +1295,7 @@ shaka.offline.Storage = class {
1110
1295
 
1111
1296
  for (const stream of streamSet) {
1112
1297
  const streamDB = this.createStream_(
1113
- downloader, storage, estimator, manifest, stream, config,
1114
- initSegmentDbKeyCache, segmentDbKeyCache);
1298
+ downloader, estimator, manifest, stream, config, toDownload);
1115
1299
  streamDBs.set(stream.id, streamDB);
1116
1300
  }
1117
1301
 
@@ -1125,27 +1309,26 @@ shaka.offline.Storage = class {
1125
1309
  }
1126
1310
  }
1127
1311
 
1128
- return Array.from(streamDBs.values());
1312
+ return {
1313
+ streams: Array.from(streamDBs.values()),
1314
+ toDownload: Array.from(toDownload.values()),
1315
+ };
1129
1316
  }
1130
1317
 
1131
1318
  /**
1132
1319
  * Converts a manifest stream to a database stream. This will search the
1133
- * segment index and add all the segments to the download manager.
1320
+ * segment index and add all the segments to the download infos.
1134
1321
  *
1135
1322
  * @param {!shaka.offline.DownloadManager} downloader
1136
- * @param {shaka.extern.StorageCell} storage
1137
1323
  * @param {shaka.offline.StreamBandwidthEstimator} estimator
1138
1324
  * @param {shaka.extern.Manifest} manifest
1139
1325
  * @param {shaka.extern.Stream} stream
1140
1326
  * @param {shaka.extern.PlayerConfiguration} config
1141
- * @param {!Map.<shaka.media.InitSegmentReference, !Promise.<?number>>}
1142
- * initSegmentDbKeyCache
1143
- * @param {!Map.<string, !Promise.<number>>} segmentDbKeyCache
1327
+ * @param {!Map.<string, !shaka.offline.DownloadInfo>} toDownload
1144
1328
  * @return {shaka.extern.StreamDB}
1145
1329
  * @private
1146
1330
  */
1147
- createStream_(downloader, storage, estimator, manifest, stream, config,
1148
- initSegmentDbKeyCache, segmentDbKeyCache) {
1331
+ createStream_(downloader, estimator, manifest, stream, config, toDownload) {
1149
1332
  /** @type {shaka.extern.StreamDB} */
1150
1333
  const streamDb = {
1151
1334
  id: stream.id,
@@ -1175,138 +1358,60 @@ shaka.offline.Storage = class {
1175
1358
  tilesLayout: stream.tilesLayout,
1176
1359
  };
1177
1360
 
1178
- // Download each stream in parallel.
1179
- const downloadGroup = stream.id;
1180
-
1181
1361
  const startTime =
1182
1362
  manifest.presentationTimeline.getSegmentAvailabilityStart();
1183
1363
 
1184
1364
  shaka.offline.Storage.forEachSegment_(stream, startTime, (segment) => {
1185
- const initSegmentKeyPromise = this.getInitSegmentDbKey_(
1186
- downloader, downloadGroup, stream.id, storage, estimator,
1187
- segment.initSegmentReference, config, initSegmentDbKeyCache);
1188
-
1189
- const segmentKeyPromise = this.getSegmentDbKey_(
1190
- downloader, downloadGroup, stream.id, storage, estimator, segment,
1191
- config, segmentDbKeyCache);
1192
-
1193
- downloader.queueWork(downloadGroup, async () => {
1194
- const initSegmentKey = await initSegmentKeyPromise;
1195
- const dataKey = await segmentKeyPromise;
1196
-
1197
- streamDb.segments.push({
1198
- initSegmentKey,
1199
- startTime: segment.startTime,
1200
- endTime: segment.endTime,
1201
- appendWindowStart: segment.appendWindowStart,
1202
- appendWindowEnd: segment.appendWindowEnd,
1203
- timestampOffset: segment.timestampOffset,
1204
- tilesLayout: segment.tilesLayout,
1205
- dataKey,
1206
- });
1207
- });
1208
- });
1209
-
1210
- return streamDb;
1211
- }
1212
-
1213
- /**
1214
- * Get a Promise to the DB key for a given init segment reference.
1215
- *
1216
- * The return values will be cached so that multiple calls with the same init
1217
- * segment reference will only trigger one request.
1218
- *
1219
- * @param {!shaka.offline.DownloadManager} downloader
1220
- * @param {number} downloadGroup
1221
- * @param {number} streamId
1222
- * @param {shaka.extern.StorageCell} storage
1223
- * @param {shaka.offline.StreamBandwidthEstimator} estimator
1224
- * @param {shaka.media.InitSegmentReference} initSegmentReference
1225
- * @param {shaka.extern.PlayerConfiguration} config
1226
- * @param {!Map.<shaka.media.InitSegmentReference, !Promise.<?number>>}
1227
- * initSegmentDbKeyCache
1228
- * @return {!Promise.<?number>}
1229
- * @private
1230
- */
1231
- getInitSegmentDbKey_(
1232
- downloader, downloadGroup, streamId, storage, estimator,
1233
- initSegmentReference, config, initSegmentDbKeyCache) {
1234
- if (initSegmentDbKeyCache.has(initSegmentReference)) {
1235
- return initSegmentDbKeyCache.get(initSegmentReference);
1236
- }
1237
-
1238
- const request = shaka.util.Networking.createSegmentRequest(
1239
- initSegmentReference.getUris(),
1240
- initSegmentReference.startByte,
1241
- initSegmentReference.endByte,
1242
- config.streaming.retryParameters);
1243
-
1244
- const promise = downloader.queue(
1245
- downloadGroup,
1246
- request,
1247
- estimator.getInitSegmentEstimate(streamId),
1248
- /* isInitSegment= */ true,
1249
- async (data) => {
1250
- /** @type {!Array.<number>} */
1251
- const ids = await storage.addSegments([{data: data}]);
1252
- this.segmentsFromStore_.push(ids[0]);
1253
- return ids[0];
1254
- });
1255
-
1256
- initSegmentDbKeyCache.set(initSegmentReference, promise);
1257
- return promise;
1258
- }
1365
+ const pendingSegmentRefId =
1366
+ shaka.offline.DownloadInfo.idForSegmentRef(segment);
1367
+ let pendingInitSegmentRefId = undefined;
1368
+
1369
+ // Set up the download for the segment, which will be downloaded later,
1370
+ // perhaps in a service worker.
1371
+ if (!toDownload.has(pendingSegmentRefId)) {
1372
+ const estimateId = downloader.addDownloadEstimate(
1373
+ estimator.getSegmentEstimate(stream.id, segment));
1374
+ const segmentDownload = new shaka.offline.DownloadInfo(
1375
+ segment,
1376
+ estimateId,
1377
+ stream.id,
1378
+ /* isInitSegment= */ false);
1379
+ toDownload.set(pendingSegmentRefId, segmentDownload);
1380
+ }
1259
1381
 
1260
- /**
1261
- * Get a Promise to the DB key for a given segment reference.
1262
- *
1263
- * The return values will be cached so that multiple calls with the same
1264
- * segment reference will only trigger one request.
1265
- *
1266
- * @param {!shaka.offline.DownloadManager} downloader
1267
- * @param {number} downloadGroup
1268
- * @param {number} streamId
1269
- * @param {shaka.extern.StorageCell} storage
1270
- * @param {shaka.offline.StreamBandwidthEstimator} estimator
1271
- * @param {shaka.media.SegmentReference} segmentReference
1272
- * @param {shaka.extern.PlayerConfiguration} config
1273
- * @param {!Map.<string, !Promise.<number>>} segmentDbKeyCache
1274
- * @return {!Promise.<number>}
1275
- * @private
1276
- */
1277
- getSegmentDbKey_(
1278
- downloader, downloadGroup, streamId, storage, estimator,
1279
- segmentReference, config, segmentDbKeyCache) {
1280
- const mapKey = [
1281
- segmentReference.getUris()[0],
1282
- segmentReference.startByte,
1283
- segmentReference.endByte,
1284
- ].join('-');
1285
-
1286
- if (segmentDbKeyCache.has(mapKey)) {
1287
- return segmentDbKeyCache.get(mapKey);
1288
- }
1382
+ // Set up the download for the init segment, similarly, if there is one.
1383
+ if (segment.initSegmentReference) {
1384
+ pendingInitSegmentRefId = shaka.offline.DownloadInfo.idForSegmentRef(
1385
+ segment.initSegmentReference);
1386
+ if (!toDownload.has(pendingInitSegmentRefId)) {
1387
+ const estimateId = downloader.addDownloadEstimate(
1388
+ estimator.getInitSegmentEstimate(stream.id));
1389
+ const initDownload = new shaka.offline.DownloadInfo(
1390
+ segment.initSegmentReference,
1391
+ estimateId,
1392
+ stream.id,
1393
+ /* isInitSegment= */ true);
1394
+ toDownload.set(pendingInitSegmentRefId, initDownload);
1395
+ }
1396
+ }
1289
1397
 
1290
- const request = shaka.util.Networking.createSegmentRequest(
1291
- segmentReference.getUris(),
1292
- segmentReference.startByte,
1293
- segmentReference.endByte,
1294
- config.streaming.retryParameters);
1295
-
1296
- const promise = downloader.queue(
1297
- downloadGroup,
1298
- request,
1299
- estimator.getSegmentEstimate(streamId, segmentReference),
1300
- /* isInitSegment= */ false,
1301
- async (data) => {
1302
- /** @type {!Array.<number>} */
1303
- const ids = await storage.addSegments([{data: data}]);
1304
- this.segmentsFromStore_.push(ids[0]);
1305
- return ids[0];
1306
- });
1398
+ /** @type {!shaka.extern.SegmentDB} */
1399
+ const segmentDB = {
1400
+ pendingInitSegmentRefId,
1401
+ initSegmentKey: pendingInitSegmentRefId ? 0 : null,
1402
+ startTime: segment.startTime,
1403
+ endTime: segment.endTime,
1404
+ appendWindowStart: segment.appendWindowStart,
1405
+ appendWindowEnd: segment.appendWindowEnd,
1406
+ timestampOffset: segment.timestampOffset,
1407
+ tilesLayout: segment.tilesLayout,
1408
+ pendingSegmentRefId,
1409
+ dataKey: 0,
1410
+ };
1411
+ streamDb.segments.push(segmentDB);
1412
+ });
1307
1413
 
1308
- segmentDbKeyCache.set(mapKey, promise);
1309
- return promise;
1414
+ return streamDb;
1310
1415
  }
1311
1416
 
1312
1417
  /**
@@ -1408,21 +1513,21 @@ shaka.offline.Storage = class {
1408
1513
  * @private
1409
1514
  */
1410
1515
  static getAllSegmentIds_(manifest) {
1411
- /** @type {!Array.<number>} */
1412
- const ids = [];
1516
+ /** @type {!Set.<number>} */
1517
+ const ids = new Set();
1413
1518
 
1414
1519
  // Get every segment for every stream in the manifest.
1415
1520
  for (const stream of manifest.streams) {
1416
1521
  for (const segment of stream.segments) {
1417
1522
  if (segment.initSegmentKey != null) {
1418
- ids.push(segment.initSegmentKey);
1523
+ ids.add(segment.initSegmentKey);
1419
1524
  }
1420
1525
 
1421
- ids.push(segment.dataKey);
1526
+ ids.add(segment.dataKey);
1422
1527
  }
1423
1528
  }
1424
1529
 
1425
- return ids;
1530
+ return Array.from(ids);
1426
1531
  }
1427
1532
 
1428
1533
  /**
@@ -1557,6 +1662,9 @@ shaka.offline.Storage = class {
1557
1662
  }
1558
1663
  };
1559
1664
 
1665
+ /** @private {!shaka.util.Mutex} */
1666
+ shaka.offline.Storage.mutex_ = new shaka.util.Mutex();
1667
+
1560
1668
  shaka.offline.Storage.defaultSystemIds_ = new Map()
1561
1669
  .set('org.w3.clearkey', '1077efecc0b24d02ace33c1e52e2fb4b')
1562
1670
  .set('com.widevine.alpha', 'edef8ba979d64acea3c827dcd51d21ed')