Skip to content

Commit

Permalink
feat(offline): Load init segments first for keys
Browse files Browse the repository at this point in the history
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 shaka-project#879

Change-Id: Ide859ed0eb2d9208150787f14d915135df681d96
  • Loading branch information
theodab committed Aug 13, 2021
1 parent 5215f53 commit db8ad31
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 64 deletions.
136 changes: 74 additions & 62 deletions lib/offline/storage.js
Expand Up @@ -439,13 +439,7 @@ shaka.offline.Storage = class {
}

await this.downloadSegments_(toDownload, manifestId, manifestDB,
downloader, config, activeHandle.cell);
this.ensureNotDestroyed_();

// Now that we have the keys loaded into the DrmEngine, we can attach
// those fields to the manifest.
this.setManifestDrmFields_(manifest, manifestDB, drmEngine, config);
await activeHandle.cell.updateManifest(manifestId, manifestDB);
downloader, config, activeHandle.cell, manifest, drmEngine);
this.ensureNotDestroyed_();

const offlineUri = shaka.offline.OfflineUri.manifest(
Expand Down Expand Up @@ -498,69 +492,87 @@ shaka.offline.Storage = class {
* @param {!shaka.offline.DownloadManager} downloader
* @param {shaka.extern.PlayerConfiguration} config
* @param {shaka.extern.StorageCell} storage
* @param {shaka.extern.Manifest} manifest
* @param {!shaka.media.DrmEngine} drmEngine
* @return {!Promise}
* @private
*/
async downloadSegments_(
toDownload, manifestId, manifestDB, downloader, config, storage) {
for (const download of toDownload) {
/** @param {?BufferSource} data */
let data;
const request = download.makeSegmentRequest(config);
const estimateId = download.estimateId;
const isInitSegment = download.isInitSegment;
const onDownloaded = (d) => {
data = d;
return Promise.resolve();
};
downloader.queue(download.groupId,
request, estimateId, isInitSegment, onDownloaded);
downloader.queueWork(download.groupId, async () => {
goog.asserts.assert(data, 'We should have loaded data by now');

const ref = /** @type {!shaka.media.SegmentReference} */ (
download.ref);
const idForRef = shaka.offline.DownloadInfo.idForSegmentRef(ref);

// Store the segment.
const ids = await storage.addSegments([{data: data}]);
this.ensureNotDestroyed_();
this.segmentsFromStore_.push(ids[0]);

// Attach the segment to the manifest.
let complete = true;
for (const stream of manifestDB.streams) {
for (const segment of stream.segments) {
if (segment.pendingSegmentRefId == idForRef) {
segment.dataKey = ids[0];
// Now that the segment has been associated with the
// appropriate dataKey, the pendingSegmentRefId is no longer
// necessary.
segment.pendingSegmentRefId = undefined;
}
if (segment.pendingInitSegmentRefId == idForRef) {
segment.initSegmentKey = ids[0];
// Now that the init segment has been associated with the
// appropriate initSegmentKey, the pendingInitSegmentRefId is
// no longer necessary.
segment.pendingInitSegmentRefId = undefined;
}
if (segment.pendingSegmentRefId) {
complete = false;
}
if (segment.pendingInitSegmentRefId) {
complete = false;
toDownload, manifestId, manifestDB, downloader, config, storage,
manifest, drmEngine) {
/** @param {!Array.<!shaka.offline.DownloadInfo>} toDownload */
const download = (toDownload) => {
for (const download of toDownload) {
/** @param {?BufferSource} data */
let data;
const request = download.makeSegmentRequest(config);
const estimateId = download.estimateId;
const isInitSegment = download.isInitSegment;
const onDownloaded = (d) => {
data = d;
return Promise.resolve();
};
downloader.queue(download.groupId,
request, estimateId, isInitSegment, onDownloaded);
downloader.queueWork(download.groupId, async () => {
goog.asserts.assert(data, 'We should have loaded data by now');

const ref = /** @type {!shaka.media.SegmentReference} */ (
download.ref);
const idForRef = shaka.offline.DownloadInfo.idForSegmentRef(ref);

// Store the segment.
const ids = await storage.addSegments([{data: data}]);
this.ensureNotDestroyed_();
this.segmentsFromStore_.push(ids[0]);

// Attach the segment to the manifest.
let complete = true;
for (const stream of manifestDB.streams) {
for (const segment of stream.segments) {
if (segment.pendingSegmentRefId == idForRef) {
segment.dataKey = ids[0];
// Now that the segment has been associated with the
// appropriate dataKey, the pendingSegmentRefId is no longer
// necessary.
segment.pendingSegmentRefId = undefined;
}
if (segment.pendingInitSegmentRefId == idForRef) {
segment.initSegmentKey = ids[0];
// Now that the init segment has been associated with the
// appropriate initSegmentKey, the pendingInitSegmentRefId is
// no longer necessary.
segment.pendingInitSegmentRefId = undefined;
}
if (segment.pendingSegmentRefId) {
complete = false;
}
if (segment.pendingInitSegmentRefId) {
complete = false;
}
}
}
}
if (complete) {
manifestDB.isIncomplete = false;
}
});
if (complete) {
manifestDB.isIncomplete = false;
}
});
}
};

if (this.getManifestIsEncrypted_(manifest) &&
!this.getManifestIncludesInitData_(manifest)) {
// Background fetch can't make DRM sessions, so if we have to get the
// init data from the init segments, download those first before anything
// else.
download(toDownload.filter((info) => info.isInitSegment));
toDownload = toDownload.filter((info) => !info.isInitSegment);
}

// Re-store the manifest.
download(toDownload);

// Re-store the manifest, to update the size and attach session IDs.
manifestDB.size = await downloader.waitToFinish();
this.setManifestDrmFields_(manifest, manifestDB, drmEngine, config);
goog.asserts.assert(
!manifestDB.isIncomplete, 'The manifest should be complete by now');
await storage.updateManifest(manifestId, manifestDB);
Expand Down Expand Up @@ -723,7 +735,7 @@ shaka.offline.Storage = class {
downloader.setCallbacks(onProgress, onInitData);

const needsInitData = this.getManifestIsEncrypted_(manifest) &&
this.getManifestIncludesInitData_(manifest);
!this.getManifestIncludesInitData_(manifest);

let currentSystemId = null;
if (needsInitData) {
Expand Down
104 changes: 104 additions & 0 deletions test/offline/storage_integration.js
Expand Up @@ -22,10 +22,12 @@ goog.require('shaka.test.ManifestGenerator');
goog.require('shaka.test.TestScheme');
goog.require('shaka.test.Util');
goog.require('shaka.util.AbortableOperation');
goog.require('shaka.util.BufferUtils');
goog.require('shaka.util.Error');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.PlayerConfiguration');
goog.require('shaka.util.PublicPromise');
goog.require('shaka.util.Uint8ArrayUtils');
goog.requireType('shaka.media.SegmentReference');

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

const initSegmentUri = 'fake:init-segment';
const segment1Uri = 'fake:segment-1';
const segment2Uri = 'fake:segment-2';
const segment3Uri = 'fake:segment-3';
const segment4Uri = 'fake:segment-4';

const alternateInitSegmentUri = 'fake:alt-init-segment';
const alternateSegment1Uri = 'fake:alt-segment-1';
const alternateSegment2Uri = 'fake:alt-segment-2';
const alternateSegment3Uri = 'fake:alt-segment-3';
Expand Down Expand Up @@ -945,6 +951,46 @@ filterDescribe('Storage', storageSupport, () => {
}
});

it('can extract DRM info from segments', async () => {
const pssh1 =
'00000028' + // atom size
'70737368' + // atom type='pssh'
'00000000' + // v0, flags=0
'edef8ba979d64acea3c827dcd51d21ed' + // system id (Widevine)
'00000008' + // data size
'0102030405060708'; // data
const psshData1 = shaka.util.Uint8ArrayUtils.fromHex(pssh1);
const pssh2 =
'00000028' + // atom size
'70737368' + // atom type='pssh'
'00000000' + // v0, flags=0
'edef8ba979d64acea3c827dcd51d21ed' + // system id (Widevine)
'00000008' + // data size
'1337420123456789'; // data
const psshData2 = shaka.util.Uint8ArrayUtils.fromHex(pssh2);
netEngine.setResponseValue(initSegmentUri,
shaka.util.BufferUtils.toArrayBuffer(psshData1));
netEngine.setResponseValue(alternateInitSegmentUri,
shaka.util.BufferUtils.toArrayBuffer(psshData2));

const drm = new shaka.test.FakeDrmEngine();
const drmInfo = makeDrmInfo();
drmInfo.keySystem = 'com.widevine.alpha';
drm.setDrmInfo(drmInfo);
overrideDrmAndManifest(
storage,
drm,
makeManifestWithVideoInitSegments());

const stored = await storage.store(
manifestWithVideoInitSegmentsUri, noMetadata, fakeMimeType).promise;
goog.asserts.assert(stored.offlineUri != null, 'URI should not be null!');

// The manifest chooses the alternate stream, so expect only the alt init
// segment.
expect(drm.newInitData).toHaveBeenCalledWith('cenc', psshData2);
});

it('can store multiple assets at once', async () => {
// Block the network so that we won't finish the first store command.
/** @type {!shaka.util.PublicPromise} */
Expand Down Expand Up @@ -1399,6 +1445,62 @@ filterDescribe('Storage', storageSupport, () => {
};
}

/** @return {shaka.extern.Manifest} */
function makeManifestWithVideoInitSegments() {
const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
manifest.presentationTimeline.setDuration(20);

manifest.addVariant(0, (variant) => {
variant.bandwidth = kbps(13);
variant.addVideo(1, (stream) => {
stream.bandwidth = kbps(13);
stream.size(100, 200);
});
});
manifest.addVariant(2, (variant) => {
variant.bandwidth = kbps(20);
variant.addVideo(3, (stream) => {
stream.bandwidth = kbps(20);
stream.size(200, 400);
});
});
});

const stream = manifest.variants[0].video;
goog.asserts.assert(stream, 'The first stream should exist');
stream.encrypted = true;
const init = new shaka.media.InitSegmentReference(
() => [initSegmentUri], 0, null);
const refs = [
makeReference(segment1Uri, 0, 1),
makeReference(segment2Uri, 1, 2),
makeReference(segment3Uri, 2, 3),
makeReference(segment4Uri, 3, 4),
];
for (const ref of refs) {
ref.initSegmentReference = init;
}
overrideSegmentIndex(stream, refs);

const streamAlt = manifest.variants[1].video;
goog.asserts.assert(streamAlt, 'The second stream should exist');
streamAlt.encrypted = true;
const initAlt = new shaka.media.InitSegmentReference(
() => [alternateInitSegmentUri], 0, null);
const refsAlt = [
makeReference(alternateSegment1Uri, 0, 1),
makeReference(alternateSegment2Uri, 1, 2),
makeReference(alternateSegment3Uri, 2, 3),
makeReference(alternateSegment4Uri, 3, 4),
];
for (const ref of refsAlt) {
ref.initSegmentReference = initAlt;
}
overrideSegmentIndex(streamAlt, refsAlt);

return manifest;
}

/** @return {shaka.extern.Manifest} */
function makeManifestWithPerStreamBandwidth() {
const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
Expand Down Expand Up @@ -1612,6 +1714,8 @@ filterDescribe('Storage', storageSupport, () => {
makeManifestWithLiveTimeline();
this.map_[manifestWithAlternateSegmentsUri] =
makeManifestWithAlternateSegments();
this.map_[manifestWithVideoInitSegmentsUri] =
makeManifestWithVideoInitSegments();
}

/** @override */
Expand Down
11 changes: 9 additions & 2 deletions test/test/util/fake_drm_engine.js
Expand Up @@ -17,7 +17,7 @@ goog.require('shaka.test.Util');
*/
shaka.test.FakeDrmEngine = class {
constructor() {
/** @private {!Array.<number>} */
/** @private {!Array.<string>} */
this.offlineSessions_ = [];
/** @private {?shaka.extern.DrmInfo} */
this.drmInfo_ = null;
Expand Down Expand Up @@ -45,6 +45,13 @@ shaka.test.FakeDrmEngine = class {
// be returned.
this.getDrmInfo.and.callFake(() => this.drmInfo_);

/** @type {!jasmine.Spy} */
this.newInitData = jasmine.createSpy('newInitData');
this.newInitData.and.callFake((initDataType, initData) => {
const num = 1 + this.offlineSessions_.length;
this.offlineSessions_.push('session-' + num);
});

/** @type {!jasmine.Spy} */
this.getExpiration = jasmine.createSpy('getExpiration');
this.getExpiration.and.returnValue(Infinity);
Expand Down Expand Up @@ -90,7 +97,7 @@ shaka.test.FakeDrmEngine = class {
}

/**
* @param {!Array.<number>} sessions
* @param {!Array.<string>} sessions
*/
setSessionIds(sessions) {
// Copy the values to break the reference to the input value.
Expand Down

0 comments on commit db8ad31

Please sign in to comment.