diff --git a/externs/shaka/manifest.js b/externs/shaka/manifest.js
index 4c3758ce5e..cac0a24bae 100644
--- a/externs/shaka/manifest.js
+++ b/externs/shaka/manifest.js
@@ -452,6 +452,7 @@ shaka.extern.SegmentIndex = class {
* @property {boolean} encrypted
* Defaults to false.
* True if the stream is encrypted.
+ * Note: DRM encryption only, so AES encryption is not taken into account.
* @property {!Array.} drmInfos
* Defaults to [] (i.e., no DRM).
* An array of DrmInfo objects which describe DRM schemes are compatible with
diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js
index 476992a817..90dac24902 100644
--- a/lib/hls/hls_parser.js
+++ b/lib/hls/hls_parser.js
@@ -2971,7 +2971,7 @@ shaka.hls.HlsParser = class {
const stream = this.makeStreamObject_(streamId, codecs, type,
languageValue, primary, name, channelsCount, closedCaptions,
characteristics, forced, sampleRate, spatialAudio);
- stream.encrypted = encrypted;
+ stream.encrypted = encrypted && !aesEncrypted;
stream.drmInfos = drmInfos;
stream.keyIds = keyIds;
stream.mimeType = mimeType;
@@ -3302,7 +3302,11 @@ shaka.hls.HlsParser = class {
}
}
- const aesKeyInfoKey = `${drmTag.toString()}-${firstMediaSequenceNumber}`;
+ const keyUris = shaka.hls.Utils.constructSegmentUris(
+ getUris(), drmTag.getRequiredAttrValue('URI'), variables);
+ const keyMapKey = keyUris.sort().join('');
+ const aesKeyInfoKey =
+ `${drmTag.toString()}-${firstMediaSequenceNumber}-${keyMapKey}`;
if (!this.aesKeyInfoMap_.has(aesKeyInfoKey)) {
// Default AES-128
const keyInfo = {
@@ -3326,10 +3330,6 @@ shaka.hls.HlsParser = class {
// Don't download the key object until the segment is parsed, to avoid a
// startup delay for long manifests with lots of keys.
keyInfo.fetchKey = async () => {
- const keyUris = shaka.hls.Utils.constructSegmentUris(
- getUris(), drmTag.getRequiredAttrValue('URI'), variables);
-
- const keyMapKey = keyUris.sort().join('');
if (!this.aesKeyMap_.has(keyMapKey)) {
const requestType = shaka.net.NetworkingEngine.RequestType.KEY;
const request = shaka.net.NetworkingEngine.makeRequest(
diff --git a/lib/offline/download_info.js b/lib/offline/download_info.js
index 62831f5329..1b18618628 100644
--- a/lib/offline/download_info.js
+++ b/lib/offline/download_info.js
@@ -22,8 +22,9 @@ shaka.offline.DownloadInfo = class {
* @param {number} estimateId
* @param {number} groupId
* @param {boolean} isInitSegment
+ * @param {number} refPosition
*/
- constructor(ref, estimateId, groupId, isInitSegment) {
+ constructor(ref, estimateId, groupId, isInitSegment, refPosition) {
/** @type {shaka.media.SegmentReference|shaka.media.InitSegmentReference} */
this.ref = ref;
@@ -35,6 +36,9 @@ shaka.offline.DownloadInfo = class {
/** @type {boolean} */
this.isInitSegment = isInitSegment;
+
+ /** @type {number} */
+ this.refPosition = refPosition;
}
/**
diff --git a/lib/offline/storage.js b/lib/offline/storage.js
index e393477a5d..76f0ba7d6b 100644
--- a/lib/offline/storage.js
+++ b/lib/offline/storage.js
@@ -13,6 +13,7 @@ goog.require('shaka.log');
goog.require('shaka.media.ManifestParser');
goog.require('shaka.media.SegmentIndex');
goog.require('shaka.media.SegmentReference');
+goog.require('shaka.media.SegmentUtils');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.net.NetworkingUtils');
goog.require('shaka.offline.DownloadInfo');
@@ -400,14 +401,14 @@ shaka.offline.Storage = class {
const clearKeyDataLicenseServerUri = manifest.variants.some((v) => {
if (v.audio) {
for (const drmInfo of v.audio.drmInfos) {
- if (!drmInfo.licenseServerUri.startsWith('data:')) {
+ if (drmInfo.licenseServerUri.startsWith('data:')) {
return true;
}
}
}
if (v.video) {
for (const drmInfo of v.video.drmInfos) {
- if (!drmInfo.licenseServerUri.startsWith('data:')) {
+ if (drmInfo.licenseServerUri.startsWith('data:')) {
return true;
}
}
@@ -531,14 +532,18 @@ shaka.offline.Storage = class {
const isInitSegment = download.isInitSegment;
const onDownloaded = async (data) => {
+ const ref = /** @type {!shaka.media.SegmentReference} */ (
+ download.ref);
+ const id = shaka.offline.DownloadInfo.idForSegmentRef(ref);
+ if (ref.aesKey) {
+ data = await shaka.media.SegmentUtils.aesDecrypt(
+ data, ref.aesKey, download.refPosition);
+ }
// Store the data.
const dataKeys = await storage.addSegments([{data}]);
this.ensureNotDestroyed_();
// Store the necessary update to the manifest, to be processed later.
- const ref = /** @type {!shaka.media.SegmentReference} */ (
- download.ref);
- const id = shaka.offline.DownloadInfo.idForSegmentRef(ref);
pendingManifestUpdates[id] = dataKeys[0];
pendingDataSize += data.byteLength;
};
@@ -1660,7 +1665,7 @@ shaka.offline.Storage = class {
const numberOfParallelDownloads = config.offline.numberOfParallelDownloads;
let groupId = numberOfParallelDownloads === 0 ? stream.id : 0;
- shaka.offline.Storage.forEachSegment_(stream, startTime, (segment) => {
+ shaka.offline.Storage.forEachSegment_(stream, startTime, (segment, pos) => {
const pendingSegmentRefId =
shaka.offline.DownloadInfo.idForSegmentRef(segment);
let pendingInitSegmentRefId = undefined;
@@ -1674,7 +1679,8 @@ shaka.offline.Storage = class {
segment,
estimateId,
groupId,
- /* isInitSegment= */ false);
+ /* isInitSegment= */ false,
+ pos);
toDownload.set(pendingSegmentRefId, segmentDownload);
}
@@ -1689,7 +1695,8 @@ shaka.offline.Storage = class {
segment.initSegmentReference,
estimateId,
groupId,
- /* isInitSegment= */ true);
+ /* isInitSegment= */ true,
+ pos);
toDownload.set(pendingInitSegmentRefId, initDownload);
}
}
@@ -1722,7 +1729,7 @@ shaka.offline.Storage = class {
/**
* @param {shaka.extern.Stream} stream
* @param {number} startTime
- * @param {function(!shaka.media.SegmentReference)} callback
+ * @param {function(!shaka.media.SegmentReference, number)} callback
* @private
*/
static forEachSegment_(stream, startTime, callback) {
@@ -1736,7 +1743,7 @@ shaka.offline.Storage = class {
/** @type {?shaka.media.SegmentReference} */
let ref = stream.segmentIndex.get(i);
while (ref) {
- callback(ref);
+ callback(ref, i);
ref = stream.segmentIndex.get(++i);
}
}
diff --git a/test/offline/storage_playback_integration.js b/test/offline/storage_playback_integration.js
new file mode 100644
index 0000000000..79570111df
--- /dev/null
+++ b/test/offline/storage_playback_integration.js
@@ -0,0 +1,167 @@
+/*! @license
+ * Shaka Player
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/** @return {boolean} */
+function checkStorageSupport() {
+ return shaka.offline.Storage.support();
+}
+
+filterDescribe('Storage', checkStorageSupport, () => {
+ const Util = shaka.test.Util;
+
+ /** @type {!jasmine.Spy} */
+ let onErrorSpy;
+
+ /** @type {!HTMLVideoElement} */
+ let video;
+ /** @type {shaka.Player} */
+ let player;
+ /** @type {shaka.offline.Storage} */
+ let storage;
+ /** @type {!shaka.util.EventManager} */
+ let eventManager;
+
+ let compiledShaka;
+
+ /** @type {!shaka.test.Waiter} */
+ let waiter;
+
+ function checkClearKeySupport() {
+ const clearKeySupport = shakaSupport.drm['org.w3.clearkey'];
+ if (!clearKeySupport) {
+ return false;
+ }
+ return clearKeySupport.encryptionSchemes.includes('cenc');
+ }
+
+ async function eraseStorage() {
+ /** @type {!shaka.offline.StorageMuxer} */
+ const muxer = new shaka.offline.StorageMuxer();
+
+ try {
+ await muxer.erase();
+ } finally {
+ await muxer.destroy();
+ }
+ }
+
+ beforeAll(async () => {
+ video = shaka.test.UiUtils.createVideoElement();
+ document.body.appendChild(video);
+ compiledShaka =
+ await shaka.test.Loader.loadShaka(getClientArg('uncompiled'));
+ });
+
+ beforeEach(async () => {
+ // Make sure we start with a clean slate between each run.
+ await eraseStorage();
+
+ await shaka.test.TestScheme.createManifests(compiledShaka, '_compiled');
+ player = new compiledShaka.Player();
+ storage = new compiledShaka.offline.Storage(player);
+ await player.attach(video);
+
+ // Disable stall detection, which can interfere with playback tests.
+ player.configure('streaming.stallEnabled', false);
+
+ // Grab event manager from the uncompiled library:
+ eventManager = new shaka.util.EventManager();
+ waiter = new shaka.test.Waiter(eventManager);
+ waiter.setPlayer(player);
+
+ onErrorSpy = jasmine.createSpy('onError');
+ onErrorSpy.and.callFake((event) => fail(event.detail));
+ eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy));
+ });
+
+ afterEach(async () => {
+ eventManager.release();
+ await storage.destroy();
+ await player.destroy();
+
+ // Make sure we don't leave anything behind.
+ await eraseStorage();
+ });
+
+ afterAll(() => {
+ document.body.removeChild(video);
+ });
+
+ it('supports DASH AES-128 download and playback', async () => {
+ const url = '/base/test/test/assets/dash-aes-128/dash.mpd';
+ const metadata = {
+ 'title': 'DASH AES-128',
+ 'downloaded': new Date(),
+ };
+
+ storage.store(url, metadata);
+
+ await player.load(url);
+ await video.play();
+ expect(player.isLive()).toBe(false);
+
+ // Wait for the video to start playback. If it takes longer than 10
+ // seconds, fail the test.
+ await waiter.waitForMovementOrFailOnTimeout(video, 10);
+
+ // Play for 2 seconds, but stop early if the video ends. If it takes
+ // longer than 10 seconds, fail the test.
+ await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 2, 10);
+
+ await player.unload();
+ });
+
+ it('supports HLS AES-256 download and playback', async () => {
+ const url = '/base/test/test/assets/hls-aes-256/media.m3u8';
+ const metadata = {
+ 'title': 'HLS AES-256',
+ 'downloaded': new Date(),
+ };
+
+ storage.store(url, metadata);
+
+ await player.load(url);
+ await video.play();
+ expect(player.isLive()).toBe(false);
+
+ // Wait for the video to start playback. If it takes longer than 10
+ // seconds, fail the test.
+ await waiter.waitForMovementOrFailOnTimeout(video, 10);
+
+ // Play for 2 seconds, but stop early if the video ends. If it takes
+ // longer than 10 seconds, fail the test.
+ await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 2, 10);
+
+ await player.unload();
+ });
+
+ drmIt('supports HLS SAMPLE-AES download and playback', async () => {
+ if (!checkClearKeySupport()) {
+ pending('ClearKey is not supported');
+ }
+ const url = '/base/test/test/assets/hls-sample-aes/index.m3u8';
+ const metadata = {
+ 'title': 'HLS SAMPLE-AES',
+ 'downloaded': new Date(),
+ };
+
+ const result = await storage.store(url, metadata).promise;
+
+ await player.load(result.offlineUri);
+ await video.play();
+ expect(player.isLive()).toBe(false);
+
+ // Wait for the video to start playback. If it takes longer than 10
+ // seconds, fail the test.
+ await waiter.waitForMovementOrFailOnTimeout(video, 10);
+
+ // Play for 2 seconds, but stop early if the video ends. If it takes
+ // longer than 10 seconds, fail the test.
+ await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 2, 10);
+
+ await player.unload();
+ });
+});