diff --git a/lib/offline/storage.js b/lib/offline/storage.js index a4eedbd443..5364bf30fb 100644 --- a/lib/offline/storage.js +++ b/lib/offline/storage.js @@ -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( @@ -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.} 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); @@ -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) { diff --git a/test/offline/storage_integration.js b/test/offline/storage_integration.js index d0a0ec893e..e926aa76f1 100644 --- a/test/offline/storage_integration.js +++ b/test/offline/storage_integration.js @@ -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} */ @@ -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'; @@ -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} */ @@ -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) => { @@ -1612,6 +1714,8 @@ filterDescribe('Storage', storageSupport, () => { makeManifestWithLiveTimeline(); this.map_[manifestWithAlternateSegmentsUri] = makeManifestWithAlternateSegments(); + this.map_[manifestWithVideoInitSegmentsUri] = + makeManifestWithVideoInitSegments(); } /** @override */ diff --git a/test/test/util/fake_drm_engine.js b/test/test/util/fake_drm_engine.js index 1682a1425f..1720f6b6c0 100644 --- a/test/test/util/fake_drm_engine.js +++ b/test/test/util/fake_drm_engine.js @@ -17,7 +17,7 @@ goog.require('shaka.test.Util'); */ shaka.test.FakeDrmEngine = class { constructor() { - /** @private {!Array.} */ + /** @private {!Array.} */ this.offlineSessions_ = []; /** @private {?shaka.extern.DrmInfo} */ this.drmInfo_ = null; @@ -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); @@ -90,7 +97,7 @@ shaka.test.FakeDrmEngine = class { } /** - * @param {!Array.} sessions + * @param {!Array.} sessions */ setSessionIds(sessions) { // Copy the values to break the reference to the input value.