Skip to content

Commit db8ad31

Browse files
committed
feat(offline): Load init segments first for keys
It is unlikely that we will be able to load DRM sessions inside the service worker for BG fetch. However, sometimes we have to get the DRM keys from the init segments. This changes Storage.downloadSegments_ to download the init segments first if it looks like they will contain needed init data to create license requests. This also fixes a typo that was preventing us from getting init data from segments, and adds a test that would catch that issue. Issue #879 Change-Id: Ide859ed0eb2d9208150787f14d915135df681d96
1 parent 5215f53 commit db8ad31

File tree

3 files changed

+187
-64
lines changed

3 files changed

+187
-64
lines changed

lib/offline/storage.js

Lines changed: 74 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -439,13 +439,7 @@ shaka.offline.Storage = class {
439439
}
440440

441441
await this.downloadSegments_(toDownload, manifestId, manifestDB,
442-
downloader, config, activeHandle.cell);
443-
this.ensureNotDestroyed_();
444-
445-
// Now that we have the keys loaded into the DrmEngine, we can attach
446-
// those fields to the manifest.
447-
this.setManifestDrmFields_(manifest, manifestDB, drmEngine, config);
448-
await activeHandle.cell.updateManifest(manifestId, manifestDB);
442+
downloader, config, activeHandle.cell, manifest, drmEngine);
449443
this.ensureNotDestroyed_();
450444

451445
const offlineUri = shaka.offline.OfflineUri.manifest(
@@ -498,69 +492,87 @@ shaka.offline.Storage = class {
498492
* @param {!shaka.offline.DownloadManager} downloader
499493
* @param {shaka.extern.PlayerConfiguration} config
500494
* @param {shaka.extern.StorageCell} storage
495+
* @param {shaka.extern.Manifest} manifest
496+
* @param {!shaka.media.DrmEngine} drmEngine
501497
* @return {!Promise}
502498
* @private
503499
*/
504500
async downloadSegments_(
505-
toDownload, manifestId, manifestDB, downloader, config, storage) {
506-
for (const download of toDownload) {
507-
/** @param {?BufferSource} data */
508-
let data;
509-
const request = download.makeSegmentRequest(config);
510-
const estimateId = download.estimateId;
511-
const isInitSegment = download.isInitSegment;
512-
const onDownloaded = (d) => {
513-
data = d;
514-
return Promise.resolve();
515-
};
516-
downloader.queue(download.groupId,
517-
request, estimateId, isInitSegment, onDownloaded);
518-
downloader.queueWork(download.groupId, async () => {
519-
goog.asserts.assert(data, 'We should have loaded data by now');
520-
521-
const ref = /** @type {!shaka.media.SegmentReference} */ (
522-
download.ref);
523-
const idForRef = shaka.offline.DownloadInfo.idForSegmentRef(ref);
524-
525-
// Store the segment.
526-
const ids = await storage.addSegments([{data: data}]);
527-
this.ensureNotDestroyed_();
528-
this.segmentsFromStore_.push(ids[0]);
529-
530-
// Attach the segment to the manifest.
531-
let complete = true;
532-
for (const stream of manifestDB.streams) {
533-
for (const segment of stream.segments) {
534-
if (segment.pendingSegmentRefId == idForRef) {
535-
segment.dataKey = ids[0];
536-
// Now that the segment has been associated with the
537-
// appropriate dataKey, the pendingSegmentRefId is no longer
538-
// necessary.
539-
segment.pendingSegmentRefId = undefined;
540-
}
541-
if (segment.pendingInitSegmentRefId == idForRef) {
542-
segment.initSegmentKey = ids[0];
543-
// Now that the init segment has been associated with the
544-
// appropriate initSegmentKey, the pendingInitSegmentRefId is
545-
// no longer necessary.
546-
segment.pendingInitSegmentRefId = undefined;
547-
}
548-
if (segment.pendingSegmentRefId) {
549-
complete = false;
550-
}
551-
if (segment.pendingInitSegmentRefId) {
552-
complete = false;
501+
toDownload, manifestId, manifestDB, downloader, config, storage,
502+
manifest, drmEngine) {
503+
/** @param {!Array.<!shaka.offline.DownloadInfo>} toDownload */
504+
const download = (toDownload) => {
505+
for (const download of toDownload) {
506+
/** @param {?BufferSource} data */
507+
let data;
508+
const request = download.makeSegmentRequest(config);
509+
const estimateId = download.estimateId;
510+
const isInitSegment = download.isInitSegment;
511+
const onDownloaded = (d) => {
512+
data = d;
513+
return Promise.resolve();
514+
};
515+
downloader.queue(download.groupId,
516+
request, estimateId, isInitSegment, onDownloaded);
517+
downloader.queueWork(download.groupId, async () => {
518+
goog.asserts.assert(data, 'We should have loaded data by now');
519+
520+
const ref = /** @type {!shaka.media.SegmentReference} */ (
521+
download.ref);
522+
const idForRef = shaka.offline.DownloadInfo.idForSegmentRef(ref);
523+
524+
// Store the segment.
525+
const ids = await storage.addSegments([{data: data}]);
526+
this.ensureNotDestroyed_();
527+
this.segmentsFromStore_.push(ids[0]);
528+
529+
// Attach the segment to the manifest.
530+
let complete = true;
531+
for (const stream of manifestDB.streams) {
532+
for (const segment of stream.segments) {
533+
if (segment.pendingSegmentRefId == idForRef) {
534+
segment.dataKey = ids[0];
535+
// Now that the segment has been associated with the
536+
// appropriate dataKey, the pendingSegmentRefId is no longer
537+
// necessary.
538+
segment.pendingSegmentRefId = undefined;
539+
}
540+
if (segment.pendingInitSegmentRefId == idForRef) {
541+
segment.initSegmentKey = ids[0];
542+
// Now that the init segment has been associated with the
543+
// appropriate initSegmentKey, the pendingInitSegmentRefId is
544+
// no longer necessary.
545+
segment.pendingInitSegmentRefId = undefined;
546+
}
547+
if (segment.pendingSegmentRefId) {
548+
complete = false;
549+
}
550+
if (segment.pendingInitSegmentRefId) {
551+
complete = false;
552+
}
553553
}
554554
}
555-
}
556-
if (complete) {
557-
manifestDB.isIncomplete = false;
558-
}
559-
});
555+
if (complete) {
556+
manifestDB.isIncomplete = false;
557+
}
558+
});
559+
}
560+
};
561+
562+
if (this.getManifestIsEncrypted_(manifest) &&
563+
!this.getManifestIncludesInitData_(manifest)) {
564+
// Background fetch can't make DRM sessions, so if we have to get the
565+
// init data from the init segments, download those first before anything
566+
// else.
567+
download(toDownload.filter((info) => info.isInitSegment));
568+
toDownload = toDownload.filter((info) => !info.isInitSegment);
560569
}
561570

562-
// Re-store the manifest.
571+
download(toDownload);
572+
573+
// Re-store the manifest, to update the size and attach session IDs.
563574
manifestDB.size = await downloader.waitToFinish();
575+
this.setManifestDrmFields_(manifest, manifestDB, drmEngine, config);
564576
goog.asserts.assert(
565577
!manifestDB.isIncomplete, 'The manifest should be complete by now');
566578
await storage.updateManifest(manifestId, manifestDB);
@@ -723,7 +735,7 @@ shaka.offline.Storage = class {
723735
downloader.setCallbacks(onProgress, onInitData);
724736

725737
const needsInitData = this.getManifestIsEncrypted_(manifest) &&
726-
this.getManifestIncludesInitData_(manifest);
738+
!this.getManifestIncludesInitData_(manifest);
727739

728740
let currentSystemId = null;
729741
if (needsInitData) {

test/offline/storage_integration.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ goog.require('shaka.test.ManifestGenerator');
2222
goog.require('shaka.test.TestScheme');
2323
goog.require('shaka.test.Util');
2424
goog.require('shaka.util.AbortableOperation');
25+
goog.require('shaka.util.BufferUtils');
2526
goog.require('shaka.util.Error');
2627
goog.require('shaka.util.EventManager');
2728
goog.require('shaka.util.PlayerConfiguration');
2829
goog.require('shaka.util.PublicPromise');
30+
goog.require('shaka.util.Uint8ArrayUtils');
2931
goog.requireType('shaka.media.SegmentReference');
3032

3133
/** @return {boolean} */
@@ -58,12 +60,16 @@ filterDescribe('Storage', storageSupport, () => {
5860
const manifestWithNonZeroStartUri = 'fake:manifest-with-non-zero-start';
5961
const manifestWithLiveTimelineUri = 'fake:manifest-with-live-timeline';
6062
const manifestWithAlternateSegmentsUri = 'fake:manifest-with-alt-segments';
63+
const manifestWithVideoInitSegmentsUri =
64+
'fake:manifest-with-video-init-segments';
6165

66+
const initSegmentUri = 'fake:init-segment';
6267
const segment1Uri = 'fake:segment-1';
6368
const segment2Uri = 'fake:segment-2';
6469
const segment3Uri = 'fake:segment-3';
6570
const segment4Uri = 'fake:segment-4';
6671

72+
const alternateInitSegmentUri = 'fake:alt-init-segment';
6773
const alternateSegment1Uri = 'fake:alt-segment-1';
6874
const alternateSegment2Uri = 'fake:alt-segment-2';
6975
const alternateSegment3Uri = 'fake:alt-segment-3';
@@ -945,6 +951,46 @@ filterDescribe('Storage', storageSupport, () => {
945951
}
946952
});
947953

954+
it('can extract DRM info from segments', async () => {
955+
const pssh1 =
956+
'00000028' + // atom size
957+
'70737368' + // atom type='pssh'
958+
'00000000' + // v0, flags=0
959+
'edef8ba979d64acea3c827dcd51d21ed' + // system id (Widevine)
960+
'00000008' + // data size
961+
'0102030405060708'; // data
962+
const psshData1 = shaka.util.Uint8ArrayUtils.fromHex(pssh1);
963+
const pssh2 =
964+
'00000028' + // atom size
965+
'70737368' + // atom type='pssh'
966+
'00000000' + // v0, flags=0
967+
'edef8ba979d64acea3c827dcd51d21ed' + // system id (Widevine)
968+
'00000008' + // data size
969+
'1337420123456789'; // data
970+
const psshData2 = shaka.util.Uint8ArrayUtils.fromHex(pssh2);
971+
netEngine.setResponseValue(initSegmentUri,
972+
shaka.util.BufferUtils.toArrayBuffer(psshData1));
973+
netEngine.setResponseValue(alternateInitSegmentUri,
974+
shaka.util.BufferUtils.toArrayBuffer(psshData2));
975+
976+
const drm = new shaka.test.FakeDrmEngine();
977+
const drmInfo = makeDrmInfo();
978+
drmInfo.keySystem = 'com.widevine.alpha';
979+
drm.setDrmInfo(drmInfo);
980+
overrideDrmAndManifest(
981+
storage,
982+
drm,
983+
makeManifestWithVideoInitSegments());
984+
985+
const stored = await storage.store(
986+
manifestWithVideoInitSegmentsUri, noMetadata, fakeMimeType).promise;
987+
goog.asserts.assert(stored.offlineUri != null, 'URI should not be null!');
988+
989+
// The manifest chooses the alternate stream, so expect only the alt init
990+
// segment.
991+
expect(drm.newInitData).toHaveBeenCalledWith('cenc', psshData2);
992+
});
993+
948994
it('can store multiple assets at once', async () => {
949995
// Block the network so that we won't finish the first store command.
950996
/** @type {!shaka.util.PublicPromise} */
@@ -1399,6 +1445,62 @@ filterDescribe('Storage', storageSupport, () => {
13991445
};
14001446
}
14011447

1448+
/** @return {shaka.extern.Manifest} */
1449+
function makeManifestWithVideoInitSegments() {
1450+
const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
1451+
manifest.presentationTimeline.setDuration(20);
1452+
1453+
manifest.addVariant(0, (variant) => {
1454+
variant.bandwidth = kbps(13);
1455+
variant.addVideo(1, (stream) => {
1456+
stream.bandwidth = kbps(13);
1457+
stream.size(100, 200);
1458+
});
1459+
});
1460+
manifest.addVariant(2, (variant) => {
1461+
variant.bandwidth = kbps(20);
1462+
variant.addVideo(3, (stream) => {
1463+
stream.bandwidth = kbps(20);
1464+
stream.size(200, 400);
1465+
});
1466+
});
1467+
});
1468+
1469+
const stream = manifest.variants[0].video;
1470+
goog.asserts.assert(stream, 'The first stream should exist');
1471+
stream.encrypted = true;
1472+
const init = new shaka.media.InitSegmentReference(
1473+
() => [initSegmentUri], 0, null);
1474+
const refs = [
1475+
makeReference(segment1Uri, 0, 1),
1476+
makeReference(segment2Uri, 1, 2),
1477+
makeReference(segment3Uri, 2, 3),
1478+
makeReference(segment4Uri, 3, 4),
1479+
];
1480+
for (const ref of refs) {
1481+
ref.initSegmentReference = init;
1482+
}
1483+
overrideSegmentIndex(stream, refs);
1484+
1485+
const streamAlt = manifest.variants[1].video;
1486+
goog.asserts.assert(streamAlt, 'The second stream should exist');
1487+
streamAlt.encrypted = true;
1488+
const initAlt = new shaka.media.InitSegmentReference(
1489+
() => [alternateInitSegmentUri], 0, null);
1490+
const refsAlt = [
1491+
makeReference(alternateSegment1Uri, 0, 1),
1492+
makeReference(alternateSegment2Uri, 1, 2),
1493+
makeReference(alternateSegment3Uri, 2, 3),
1494+
makeReference(alternateSegment4Uri, 3, 4),
1495+
];
1496+
for (const ref of refsAlt) {
1497+
ref.initSegmentReference = initAlt;
1498+
}
1499+
overrideSegmentIndex(streamAlt, refsAlt);
1500+
1501+
return manifest;
1502+
}
1503+
14021504
/** @return {shaka.extern.Manifest} */
14031505
function makeManifestWithPerStreamBandwidth() {
14041506
const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
@@ -1612,6 +1714,8 @@ filterDescribe('Storage', storageSupport, () => {
16121714
makeManifestWithLiveTimeline();
16131715
this.map_[manifestWithAlternateSegmentsUri] =
16141716
makeManifestWithAlternateSegments();
1717+
this.map_[manifestWithVideoInitSegmentsUri] =
1718+
makeManifestWithVideoInitSegments();
16151719
}
16161720

16171721
/** @override */

test/test/util/fake_drm_engine.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ goog.require('shaka.test.Util');
1717
*/
1818
shaka.test.FakeDrmEngine = class {
1919
constructor() {
20-
/** @private {!Array.<number>} */
20+
/** @private {!Array.<string>} */
2121
this.offlineSessions_ = [];
2222
/** @private {?shaka.extern.DrmInfo} */
2323
this.drmInfo_ = null;
@@ -45,6 +45,13 @@ shaka.test.FakeDrmEngine = class {
4545
// be returned.
4646
this.getDrmInfo.and.callFake(() => this.drmInfo_);
4747

48+
/** @type {!jasmine.Spy} */
49+
this.newInitData = jasmine.createSpy('newInitData');
50+
this.newInitData.and.callFake((initDataType, initData) => {
51+
const num = 1 + this.offlineSessions_.length;
52+
this.offlineSessions_.push('session-' + num);
53+
});
54+
4855
/** @type {!jasmine.Spy} */
4956
this.getExpiration = jasmine.createSpy('getExpiration');
5057
this.getExpiration.and.returnValue(Infinity);
@@ -90,7 +97,7 @@ shaka.test.FakeDrmEngine = class {
9097
}
9198

9299
/**
93-
* @param {!Array.<number>} sessions
100+
* @param {!Array.<string>} sessions
94101
*/
95102
setSessionIds(sessions) {
96103
// Copy the values to break the reference to the input value.

0 commit comments

Comments
 (0)