mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-25 17:45:03 +03:00
03fc0bca99
Tracking progress when removing an asset was missing from the new storage system. This CL adds that support back. Issue #1248 Change-Id: Iab275cd75af817cfc34ee0888ddeea257b1ead56
1127 lines
36 KiB
JavaScript
1127 lines
36 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2016 Google Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
describe('Storage', function() {
|
|
const englishUS = 'en-us';
|
|
const frenchCanadian= 'fr-ca';
|
|
|
|
const manifestWithPerStreamBandwidthUri =
|
|
'fake:manifest-with-per-stream-bandwidth';
|
|
const manifestWithoutPerStreamBandwidthUri =
|
|
'fake:manifest-without-per-stream-bandwidth';
|
|
const manifestWithNonZeroStartUri = 'fake:manifest-with-non-zero-start';
|
|
const manifestWithLiveTimelineUri = 'fake:manifest-with-live-timeline';
|
|
|
|
const segment1Uri = 'fake:segment-1';
|
|
const segment2Uri = 'fake:segment-2';
|
|
const segment3Uri = 'fake:segment-3';
|
|
const segment4Uri = 'fake:segment-4';
|
|
|
|
const noMetadata = {};
|
|
|
|
const kbps = 1000;
|
|
|
|
describe('persistent license', function() {
|
|
/** @type {!shaka.Player} */
|
|
let player;
|
|
/** @type {!shaka.offline.Storage} */
|
|
let storage;
|
|
|
|
beforeEach(drmCheckAndRun(async function() {
|
|
// Make sure we start with a clean slate between each run.
|
|
await eraseStorage();
|
|
|
|
// Use a real Player since Storage only uses the configuration and
|
|
// networking engine. This allows us to use Player.configure in these
|
|
// tests.
|
|
player = new shaka.Player(new shaka.test.FakeVideo());
|
|
storage = new shaka.offline.Storage(player);
|
|
}));
|
|
|
|
afterEach(drmCheckAndRun(async function() {
|
|
await storage.destroy();
|
|
await player.destroy();
|
|
|
|
// Make sure we don't leave anything behind.
|
|
await eraseStorage();
|
|
}));
|
|
|
|
drmIt('removes persistent license', drmCheckAndRun(async function() {
|
|
const TestManifestParser = shaka.test.TestScheme.ManifestParser;
|
|
const storeOfflineLicense = true;
|
|
|
|
// PART 1 - Download and store content that has a persistent license
|
|
// associated with it.
|
|
let stored = await storage.store(
|
|
'test:sintel-enc', noMetadata, TestManifestParser);
|
|
expect(stored.offlineUri).toBeTruthy();
|
|
|
|
/** @type {shaka.offline.OfflineUri} */
|
|
let uri = shaka.offline.OfflineUri.parse(stored.offlineUri);
|
|
goog.asserts.assert(uri, 'Stored offline uri should be non-null');
|
|
|
|
let manifest = await getStoredManifest(uri);
|
|
expect(manifest.offlineSessionIds).toBeTruthy();
|
|
expect(manifest.offlineSessionIds.length).toBeTruthy();
|
|
|
|
let error = null;
|
|
|
|
/** @type {function():!shaka.media.DrmEngine} */
|
|
let getDrm = () => {
|
|
let drm = new shaka.media.DrmEngine({
|
|
netEngine: player.getNetworkingEngine(),
|
|
onError: (e) => { error = error || e; },
|
|
onKeyStatus: () => {},
|
|
onExpirationUpdated: () => {},
|
|
onEvent: () => {}
|
|
});
|
|
drm.configure(player.getConfiguration().drm);
|
|
|
|
return drm;
|
|
};
|
|
|
|
// PART 2 - Check that the licences are stored.
|
|
let drm = getDrm();
|
|
await shaka.util.IDestroyable.with([drm], async () => {
|
|
await drm.init(manifest, storeOfflineLicense);
|
|
await Promise.all(manifest.offlineSessionIds.map(async (session) => {
|
|
let foundSession = await loadOfflineSession(drm, session);
|
|
expect(foundSession).toBeTruthy();
|
|
}));
|
|
});
|
|
|
|
expect(error).toBeFalsy();
|
|
|
|
// PART 3 - Remove the manifest from storage. This should remove all the
|
|
// sessions.
|
|
await storage.remove(uri.toString());
|
|
|
|
// PART 4 - Check that the licenses were removed.
|
|
drm = getDrm();
|
|
await shaka.util.IDestroyable.with([drm], async () => {
|
|
await drm.init(manifest, storeOfflineLicense);
|
|
await Promise.all(manifest.offlineSessionIds.map(async (session) => {
|
|
let notFoundSession = await loadOfflineSession(drm, session);
|
|
expect(notFoundSession).toBeFalsy();
|
|
}));
|
|
});
|
|
|
|
expect(error).toBeTruthy();
|
|
expect(error.code).toBe(shaka.util.Error.Code.OFFLINE_SESSION_REMOVED);
|
|
}));
|
|
});
|
|
|
|
describe('default track selection callback', function() {
|
|
const select = shaka.offline.Storage.defaultTrackSelect;
|
|
|
|
it('selects the largest SD video with middle quality audio', function() {
|
|
const tracks = [
|
|
variantTrack(0, 360, englishUS, 1 * kbps),
|
|
variantTrack(1, 480, englishUS, 2.0 * kbps),
|
|
variantTrack(2, 480, englishUS, 2.1 * kbps),
|
|
variantTrack(3, 480, englishUS, 2.2 * kbps),
|
|
variantTrack(4, 720, englishUS, 3 * kbps),
|
|
variantTrack(5, 1080, englishUS, 4 * kbps),
|
|
];
|
|
|
|
let selected = select(englishUS, tracks);
|
|
expect(selected).toBeTruthy();
|
|
expect(selected.length).toBe(1);
|
|
expect(selected[0]).toBeTruthy();
|
|
expect(selected[0].language).toBe(englishUS);
|
|
expect(selected[0].height).toBe(480);
|
|
expect(selected[0].bandwidth).toBe(2.1 * kbps);
|
|
});
|
|
|
|
it('selects all text tracks', function() {
|
|
const tracks = [
|
|
textTrack(0, englishUS),
|
|
textTrack(1, frenchCanadian),
|
|
];
|
|
|
|
let selected = select(englishUS, tracks);
|
|
expect(selected).toBeTruthy();
|
|
expect(selected.length).toBe(2);
|
|
tracks.forEach((track) => {
|
|
expect(selected).toContain(track);
|
|
});
|
|
});
|
|
|
|
describe('language matching', function() {
|
|
it('finds exact match', function() {
|
|
const tracks = [
|
|
variantTrack(0, 480, 'eng-us', 1 * kbps),
|
|
variantTrack(1, 480, 'fr-ca', 1 * kbps),
|
|
variantTrack(2, 480, 'eng-ca', 1 * kbps),
|
|
];
|
|
|
|
let selected = select('eng-us', tracks);
|
|
expect(selected).toBeTruthy();
|
|
expect(selected.length).toBe(1);
|
|
expect(selected[0]).toBeTruthy();
|
|
expect(selected[0].language).toBe('eng-us');
|
|
});
|
|
|
|
it('finds exact match with only base', function() {
|
|
const tracks = [
|
|
variantTrack(0, 480, 'eng-us', 1 * kbps),
|
|
variantTrack(1, 480, 'fr-ca', 1 * kbps),
|
|
variantTrack(2, 480, 'eng-ca', 1 * kbps),
|
|
variantTrack(3, 480, 'eng', 1 * kbps),
|
|
];
|
|
|
|
let selected = select('eng', tracks);
|
|
expect(selected).toBeTruthy();
|
|
expect(selected.length).toBe(1);
|
|
expect(selected[0]).toBeTruthy();
|
|
expect(selected[0].language).toBe('eng');
|
|
});
|
|
|
|
it('finds base match when exact match is not found', function() {
|
|
const tracks = [
|
|
variantTrack(0, 480, 'eng-us', 1 * kbps),
|
|
variantTrack(1, 480, 'fr-ca', 1 * kbps),
|
|
variantTrack(2, 480, 'eng-ca', 1 * kbps),
|
|
];
|
|
|
|
let selected = select('fr', tracks);
|
|
expect(selected).toBeTruthy();
|
|
expect(selected.length).toBe(1);
|
|
expect(selected[0]).toBeTruthy();
|
|
expect(selected[0].language).toBe('fr-ca');
|
|
});
|
|
|
|
it('finds common base when exact match is not found', function() {
|
|
const tracks = [
|
|
variantTrack(0, 480, 'eng-us', 1 * kbps),
|
|
variantTrack(1, 480, 'fr-ca', 1 * kbps),
|
|
variantTrack(2, 480, 'eng-ca', 1 * kbps),
|
|
];
|
|
|
|
let selected = select('fr-uk', tracks);
|
|
expect(selected).toBeTruthy();
|
|
expect(selected.length).toBe(1);
|
|
expect(selected[0]).toBeTruthy();
|
|
expect(selected[0].language).toBe('fr-ca');
|
|
});
|
|
|
|
it('finds primary track when no match is found', function() {
|
|
const tracks = [
|
|
variantTrack(0, 480, 'eng-us', 1 * kbps),
|
|
variantTrack(1, 480, 'fr-ca', 1 * kbps),
|
|
variantTrack(2, 480, 'eng-ca', 1 * kbps),
|
|
];
|
|
|
|
tracks[0].primary = true;
|
|
|
|
let selected = select('de', tracks);
|
|
expect(selected).toBeTruthy();
|
|
expect(selected.length).toBe(1);
|
|
expect(selected[0]).toBeTruthy();
|
|
expect(selected[0].language).toBe('eng-us');
|
|
});
|
|
}); // describe('language matching')
|
|
}); // describe('default track selection callback')
|
|
|
|
|
|
describe('no support', function() {
|
|
/** @type {!shaka.Player} */
|
|
let player;
|
|
/** @type {!shaka.offline.Storage} */
|
|
let storage;
|
|
|
|
beforeEach(function() {
|
|
shaka.offline.StorageMuxer.overrideSupport({});
|
|
|
|
player = new shaka.Player(new shaka.test.FakeVideo());
|
|
storage = new shaka.offline.Storage(player);
|
|
});
|
|
|
|
afterEach(async function() {
|
|
await storage.destroy();
|
|
await player.destroy();
|
|
|
|
shaka.offline.StorageMuxer.clearOverride();
|
|
});
|
|
|
|
it('throws error using list', async function() {
|
|
try {
|
|
await storage.list();
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.code).toBe(shaka.util.Error.Code.STORAGE_NOT_SUPPORTED);
|
|
}
|
|
});
|
|
|
|
it('throws error using store', async function() {
|
|
try {
|
|
await storage.store('the-uri-wont-matter');
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.code).toBe(shaka.util.Error.Code.STORAGE_NOT_SUPPORTED);
|
|
}
|
|
});
|
|
|
|
it('throws error using remove', async function() {
|
|
try {
|
|
await storage.remove('the-uri-wont-matter');
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.code).toBe(shaka.util.Error.Code.STORAGE_NOT_SUPPORTED);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('basic function', function() {
|
|
/**
|
|
* Keep a reference to the networking engine so that we can interrupt
|
|
* networking calls.
|
|
*
|
|
* @type {!shaka.test.FakeNetworkingEngine}
|
|
*/
|
|
let netEngine;
|
|
/** @type {!shaka.Player} */
|
|
let player;
|
|
/** @type {!shaka.offline.Storage} */
|
|
let storage;
|
|
|
|
beforeEach(checkAndRun(async function() {
|
|
// Make sure we start with a clean slate between each run.
|
|
await eraseStorage();
|
|
|
|
netEngine = makeNetworkEngine();
|
|
|
|
// Use a real Player since Storage only uses the configuration and
|
|
// networking engine. This allows us to use Player.configure in these
|
|
// tests.
|
|
player = new shaka.Player(new shaka.test.FakeVideo(), function(player) {
|
|
player.createNetworkingEngine = () => netEngine;
|
|
});
|
|
|
|
storage = new shaka.offline.Storage(player);
|
|
}));
|
|
|
|
afterEach(checkAndRun(async function() {
|
|
await storage.destroy();
|
|
await player.destroy();
|
|
|
|
// Make sure we don't leave anything behind.
|
|
await eraseStorage();
|
|
}));
|
|
|
|
describe('reports progress on store', function() {
|
|
it('uses stream bandwidth', checkAndRun(async function() {
|
|
// Change storage to only store one track so that it will be easy
|
|
// for use to ensure that only the one track was stored.
|
|
let selectTrack = (tracks) => {
|
|
let selected = tracks.filter((t) => t.language == frenchCanadian);
|
|
expect(selected.length).toBe(1);
|
|
return selected;
|
|
};
|
|
|
|
/**
|
|
* These numbers are the overall progress based on the segment sizes
|
|
* per stream. We assume a specific download order for the content
|
|
* based on the order of the streams and segments.
|
|
*
|
|
* Since the audio stream has smaller segments, its contribution to
|
|
* the overall progress is much smaller than the video stream segments.
|
|
*
|
|
* @type {!Array.<number>}
|
|
*/
|
|
let progressSteps = [
|
|
0.19, 0.25, 0.44, 0.5, 0.69, 0.75, 0.94, 1.0
|
|
];
|
|
|
|
let progressCallback = (content, progress) => {
|
|
expect(progress).toBeCloseTo(progressSteps.shift());
|
|
};
|
|
|
|
storage.configure({
|
|
trackSelectionCallback: selectTrack,
|
|
progressCallback: progressCallback
|
|
});
|
|
|
|
// Store manifest 1 as it will have per-stream bandwidth.
|
|
await storage.store(
|
|
manifestWithPerStreamBandwidthUri, noMetadata, FakeManifestParser);
|
|
expect(progressSteps.length).toBe(0);
|
|
}));
|
|
|
|
it('uses variant bandwidth when stream bandwidth is unavailable',
|
|
checkAndRun(async function() {
|
|
// Change storage to only store one track so that it will be easy
|
|
// for use to ensure that only the one track was stored.
|
|
let selectTrack = (tracks) => {
|
|
let selected = tracks.filter((t) => t.language == frenchCanadian);
|
|
expect(selected.length).toBe(1);
|
|
return selected;
|
|
};
|
|
|
|
/**
|
|
* These numbers are the overall progress based on the segment sizes
|
|
* per stream. We assume a specific download order for the content
|
|
* based on the order of the streams and segments.
|
|
*
|
|
* Since we do not have per-stream bandwidth, the amount each
|
|
* influences the overall progress is based on the stream type,
|
|
* storage's default bandwidth assumptions, and the variant's
|
|
* bandwidth.
|
|
*
|
|
* In this example we see a larger difference between the audio and
|
|
* video contributions to progress.
|
|
*
|
|
* @type {!Array.<number>}
|
|
*/
|
|
let progressSteps = [
|
|
0.01, 0.25, 0.26, 0.5, 0.51, 0.75, 0.76, 1.0
|
|
];
|
|
|
|
let progressCallback = (content, progress) => {
|
|
expect(progress).toBeCloseTo(progressSteps.shift());
|
|
};
|
|
|
|
storage.configure({
|
|
trackSelectionCallback: selectTrack,
|
|
progressCallback: progressCallback
|
|
});
|
|
|
|
// Store manifest 2 as it won't have per-stream bandwidth.
|
|
await storage.store(
|
|
manifestWithoutPerStreamBandwidthUri,
|
|
noMetadata,
|
|
FakeManifestParser);
|
|
expect(progressSteps.length).toBe(0);
|
|
}));
|
|
});
|
|
|
|
it('stores and lists content', checkAndRun(async function() {
|
|
// Manifest 1 and 2 use zero start times.
|
|
await storage.store(
|
|
manifestWithPerStreamBandwidthUri,
|
|
noMetadata,
|
|
FakeManifestParser);
|
|
await storage.store(
|
|
manifestWithoutPerStreamBandwidthUri,
|
|
noMetadata,
|
|
FakeManifestParser);
|
|
// Make sure to use manifest 3 as it has a non-zero start time.
|
|
await storage.store(
|
|
manifestWithNonZeroStartUri,
|
|
noMetadata,
|
|
FakeManifestParser);
|
|
|
|
let content = await storage.list();
|
|
|
|
expect(content).toBeTruthy();
|
|
expect(content.length).toBe(3);
|
|
|
|
expect(content[0]).toBeTruthy();
|
|
expect(content[0].originalManifestUri)
|
|
.toBe(manifestWithPerStreamBandwidthUri);
|
|
|
|
expect(content[1]).toBeTruthy();
|
|
expect(content[1].originalManifestUri)
|
|
.toBe(manifestWithoutPerStreamBandwidthUri);
|
|
|
|
expect(content[2]).toBeTruthy();
|
|
expect(content[2].originalManifestUri).toBe(manifestWithNonZeroStartUri);
|
|
}));
|
|
|
|
it('only stores chosen tracks', checkAndRun(async function() {
|
|
// Change storage to only store one track so that it will be easy
|
|
// for use to ensure that only the one track was stored.
|
|
let selectTrack = (tracks) => {
|
|
let selected = tracks.filter((t) => t.language == frenchCanadian);
|
|
expect(selected.length).toBe(1);
|
|
return selected;
|
|
};
|
|
storage.configure({
|
|
trackSelectionCallback: selectTrack
|
|
});
|
|
|
|
// Stored content should reflect the tracks in the first period, so we
|
|
// should only find track there.
|
|
let stored = await storage.store(
|
|
manifestWithPerStreamBandwidthUri, noMetadata, FakeManifestParser);
|
|
expect(stored).toBeTruthy();
|
|
expect(stored.tracks).toBeTruthy();
|
|
expect(stored.tracks.length).toBe(1);
|
|
expect(stored.tracks[0].language).toBe(frenchCanadian);
|
|
|
|
// Pull the manifest out of storage so that we can ensure that it only
|
|
// has one variant.
|
|
/** @type {shaka.offline.OfflineUri} */
|
|
let uri = shaka.offline.OfflineUri.parse(stored.offlineUri);
|
|
expect(uri).toBeTruthy();
|
|
|
|
/** @type {!shaka.offline.StorageMuxer} */
|
|
let muxer = new shaka.offline.StorageMuxer();
|
|
await shaka.util.IDestroyable.with([muxer], async () => {
|
|
await muxer.init();
|
|
let cell = await muxer.getCell(uri.mechanism(), uri.cell());
|
|
let manifests = await cell.getManifests([uri.key()]);
|
|
expect(manifests).toBeTruthy();
|
|
expect(manifests.length).toBe(1);
|
|
|
|
let manifest = manifests[0];
|
|
expect(manifest).toBeTruthy();
|
|
expect(manifest.periods).toBeTruthy();
|
|
expect(manifest.periods.length).toBe(1);
|
|
|
|
let period = manifest.periods[0];
|
|
expect(period).toBeTruthy();
|
|
expect(period.streams).toBeTruthy();
|
|
// There should be 2 streams, an audio and a video stream.
|
|
expect(period.streams.length).toBe(2);
|
|
|
|
let audio = period.streams.filter((s) => s.contentType == 'audio')[0];
|
|
expect(audio).toBeTruthy();
|
|
expect(audio.language).toBe(frenchCanadian);
|
|
});
|
|
}));
|
|
|
|
it('stores drm info without license', checkAndRun(async function() {
|
|
const drmInfo = makeDrmInfo();
|
|
const session1 = 'session-1';
|
|
const session2 = 'session-2';
|
|
const expiration = 1000;
|
|
|
|
// TODO(vaage): Is there a way we can set the session ids without needing
|
|
// to overload an internal call in storage.
|
|
let drm = new shaka.test.FakeDrmEngine();
|
|
drm.setDrmInfo(drmInfo);
|
|
drm.setSessionIds([session1, session2]);
|
|
drm.getExpiration.and.returnValue(expiration);
|
|
|
|
overrideDrmAndManifest(
|
|
storage,
|
|
drm,
|
|
makeManifestWithPerStreamBandwidth());
|
|
|
|
let stored = await storage.store(manifestWithPerStreamBandwidthUri);
|
|
|
|
/** @type {shaka.offline.OfflineUri} */
|
|
let uri = shaka.offline.OfflineUri.parse(stored.offlineUri);
|
|
expect(uri).toBeTruthy();
|
|
|
|
/** @type {!shaka.offline.StorageMuxer} */
|
|
let muxer = new shaka.offline.StorageMuxer();
|
|
await shaka.util.IDestroyable.with([muxer], async () => {
|
|
await muxer.init();
|
|
let cell = await muxer.getCell(uri.mechanism(), uri.cell());
|
|
let manifests = await cell.getManifests([uri.key()]);
|
|
let manifest = manifests[0];
|
|
expect(manifest).toBeTruthy();
|
|
|
|
expect(manifest.drmInfo).toEqual(drmInfo);
|
|
|
|
expect(manifest.expiration).toBe(expiration);
|
|
|
|
expect(manifest.sessionIds).toBeTruthy();
|
|
expect(manifest.sessionIds.length).toBe(2);
|
|
expect(manifest.sessionIds).toContain(session1);
|
|
expect(manifest.sessionIds).toContain(session2);
|
|
});
|
|
}));
|
|
|
|
// Make sure that when we configure storage to NOT store persistent
|
|
// licenses that we don't store the sessions.
|
|
it('stores drm info with no license',
|
|
checkAndRun(async function() {
|
|
const drmInfo = makeDrmInfo();
|
|
const session1 = 'session-1';
|
|
const session2 = 'session-2';
|
|
|
|
// TODO(vaage): Is there a way we can set the session ids without
|
|
// needing to overload an internal call in storage.
|
|
let drm = new shaka.test.FakeDrmEngine();
|
|
drm.setDrmInfo(drmInfo);
|
|
drm.setSessionIds([session1, session2]);
|
|
|
|
overrideDrmAndManifest(
|
|
storage,
|
|
drm,
|
|
makeManifestWithPerStreamBandwidth());
|
|
storage.configure({usePersistentLicense: false});
|
|
|
|
let stored = await storage.store(manifestWithPerStreamBandwidthUri);
|
|
|
|
/** @type {shaka.offline.OfflineUri} */
|
|
let uri = shaka.offline.OfflineUri.parse(stored.offlineUri);
|
|
expect(uri).toBeTruthy();
|
|
|
|
/** @type {!shaka.offline.StorageMuxer} */
|
|
let muxer = new shaka.offline.StorageMuxer();
|
|
await shaka.util.IDestroyable.with([muxer], async () => {
|
|
await muxer.init();
|
|
let cell = await muxer.getCell(uri.mechanism(), uri.cell());
|
|
let manifests = await cell.getManifests([uri.key()]);
|
|
let manifest = manifests[0];
|
|
expect(manifest).toBeTruthy();
|
|
|
|
expect(manifest.drmInfo).toEqual(drmInfo);
|
|
|
|
// When there is no expiration, the expiration is set to Infinity.
|
|
expect(manifest.expiration).toBe(Infinity);
|
|
|
|
expect(manifest.sessionIds).toBeTruthy();
|
|
expect(manifest.sessionIds.length).toBe(0);
|
|
});
|
|
}));
|
|
|
|
// TODO(vaage): Remove the need to limit the number of store commands. With
|
|
// all the changes, it should be very easy to do now.
|
|
it('throws an error if another store is in progress',
|
|
checkAndRun(async function() {
|
|
// Block the network so that we won't finish the first store command.
|
|
/** @type {!shaka.util.PublicPromise} */
|
|
let hangingPromise = netEngine.delayNextRequest();
|
|
/** @type {!Promise} */
|
|
let storePromise = storage.store(
|
|
manifestWithPerStreamBandwidthUri,
|
|
noMetadata,
|
|
FakeManifestParser);
|
|
|
|
try {
|
|
await storage.store(
|
|
manifestWithoutPerStreamBandwidthUri,
|
|
noMetadata,
|
|
FakeManifestParser);
|
|
fail();
|
|
} catch (e) {
|
|
const Code = shaka.util.Error.Code;
|
|
expect(e.code).toBe(Code.STORE_ALREADY_IN_PROGRESS);
|
|
}
|
|
|
|
// Unblock the original store and wait for it to complete.
|
|
hangingPromise.resolve();
|
|
await storePromise;
|
|
}));
|
|
|
|
it('throws an error if the content is a live stream',
|
|
checkAndRun(async function() {
|
|
try {
|
|
await storage.store(
|
|
manifestWithLiveTimelineUri,
|
|
noMetadata,
|
|
FakeManifestParser);
|
|
} catch (e) {
|
|
const Code = shaka.util.Error.Code;
|
|
expect(e.code).toBe(Code.CANNOT_STORE_LIVE_OFFLINE);
|
|
}
|
|
}));
|
|
|
|
it('throws an error if DRM sessions are not ready',
|
|
checkAndRun(async function() {
|
|
const drmInfo = makeDrmInfo();
|
|
const noSessions = [];
|
|
|
|
// TODO(vaage): Is there a way we can set the session ids without
|
|
// needing to overload an internal call in storage.
|
|
let drm = new shaka.test.FakeDrmEngine();
|
|
drm.setDrmInfo(drmInfo);
|
|
drm.setSessionIds(noSessions);
|
|
|
|
overrideDrmAndManifest(
|
|
storage,
|
|
drm,
|
|
makeManifestWithPerStreamBandwidth());
|
|
|
|
try {
|
|
await storage.store(manifestWithPerStreamBandwidthUri);
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.code).toBe(shaka.util.Error.Code.NO_INIT_DATA_FOR_OFFLINE);
|
|
}
|
|
}));
|
|
|
|
it('throws an error if destroyed mid-store', checkAndRun(async function() {
|
|
// Block the network so that we won't finish the store command.
|
|
netEngine.delayNextRequest();
|
|
let storePromise = storage.store(
|
|
manifestWithPerStreamBandwidthUri, noMetadata, FakeManifestParser);
|
|
|
|
// Destroy storage. This should cause the store command to reject the
|
|
// promise.
|
|
await storage.destroy();
|
|
|
|
try {
|
|
await storePromise;
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.code).toBe(shaka.util.Error.Code.OPERATION_ABORTED);
|
|
}
|
|
}));
|
|
|
|
it('stops for networking errors', checkAndRun(async function() {
|
|
// Force all network requests to fail.
|
|
netEngine.request.and.callFake(() => {
|
|
return shaka.util.AbortableOperation.failed(new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.NETWORK,
|
|
shaka.util.Error.Code.HTTP_ERROR
|
|
));
|
|
});
|
|
|
|
try {
|
|
await storage.store(
|
|
manifestWithPerStreamBandwidthUri,
|
|
noMetadata,
|
|
FakeManifestParser);
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.code).toBe(shaka.util.Error.Code.HTTP_ERROR);
|
|
}
|
|
}));
|
|
|
|
it('throws an error if removing malformed uri',
|
|
checkAndRun(async function() {
|
|
const badUri = 'this-is-an-invalid-uri';
|
|
try {
|
|
await storage.remove(badUri);
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.code).toBe(shaka.util.Error.Code.MALFORMED_OFFLINE_URI);
|
|
}
|
|
}));
|
|
|
|
it('throws an error if removing missing manifest',
|
|
checkAndRun(async function() {
|
|
// Store a piece of content, but then change the uri slightly so that
|
|
// it won't be found when we try to remove it (with the wrong uri).
|
|
let stored = await storage.store(
|
|
manifestWithPerStreamBandwidthUri,
|
|
noMetadata,
|
|
FakeManifestParser);
|
|
let storedUri = shaka.offline.OfflineUri.parse(stored.offlineUri);
|
|
let missingManifestUri = shaka.offline.OfflineUri.manifest(
|
|
storedUri.mechanism(), storedUri.cell(), storedUri.key() + 1);
|
|
|
|
try {
|
|
await storage.remove(missingManifestUri.toString());
|
|
fail();
|
|
} catch (e) {
|
|
expect(e.code).toBe(shaka.util.Error.Code.KEY_NOT_FOUND);
|
|
}
|
|
}));
|
|
|
|
it('removes manifest', checkAndRun(async function() {
|
|
let stored = await storage.store(
|
|
manifestWithPerStreamBandwidthUri, noMetadata, FakeManifestParser);
|
|
|
|
await storage.remove(stored.offlineUri);
|
|
}));
|
|
|
|
it('removes manifest with missing segments', checkAndRun(async function() {
|
|
let stored = await storage.store(
|
|
manifestWithPerStreamBandwidthUri, noMetadata, FakeManifestParser);
|
|
|
|
/** @type {shaka.offline.OfflineUri} */
|
|
let uri = shaka.offline.OfflineUri.parse(stored.offlineUri);
|
|
expect(uri).toBeTruthy();
|
|
|
|
/** @type {!shaka.offline.StorageMuxer} */
|
|
let muxer = new shaka.offline.StorageMuxer();
|
|
await shaka.util.IDestroyable.with([muxer], async () => {
|
|
await muxer.init();
|
|
let cell = await muxer.getCell(uri.mechanism(), uri.cell());
|
|
let manifests = await cell.getManifests([uri.key()]);
|
|
let manifest = manifests[0];
|
|
|
|
// Get the stream from the manifest. The segment count is based on how
|
|
// we created manifest 1.
|
|
let stream = manifest.periods[0].streams[0];
|
|
expect(stream).toBeTruthy();
|
|
expect(stream.segments.length).toBe(4);
|
|
|
|
// Remove all the segments so that all segments will be missing.
|
|
// There should be way more than one segment.
|
|
let keys = stream.segments.map((segment) => segment.dataKey);
|
|
expect(keys.length).toBeGreaterThan(0);
|
|
|
|
const noop = () => {};
|
|
await cell.removeSegments(keys, noop);
|
|
});
|
|
|
|
await storage.remove(uri.toString());
|
|
}));
|
|
});
|
|
|
|
/**
|
|
* @param {number} id
|
|
* @param {number} height
|
|
* @param {string} language
|
|
* @param {number} bandwidth
|
|
* @return {shaka.extern.Track}
|
|
*/
|
|
function variantTrack(id, height, language, bandwidth) {
|
|
return {
|
|
id: id,
|
|
active: false,
|
|
type: 'variant',
|
|
bandwidth: bandwidth,
|
|
language: language,
|
|
label: null,
|
|
kind: null,
|
|
width: height * (16 / 9),
|
|
height: height,
|
|
frameRate: 30,
|
|
mimeType: 'video/mp4,audio/mp4',
|
|
codecs: 'mp4,mp4',
|
|
audioCodec: 'mp4',
|
|
videoCodec: 'mp4',
|
|
primary: false,
|
|
roles: [],
|
|
videoId: id * 2,
|
|
audioId: id * 2 + 1,
|
|
channelsCount: 2,
|
|
audioBandwidth: bandwidth * 0.33,
|
|
videoBandwidth: bandwidth * 0.67
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {number} id
|
|
* @param {string} language
|
|
* @return {shaka.extern.Track}
|
|
*/
|
|
function textTrack(id, language) {
|
|
return {
|
|
id: id,
|
|
active: false,
|
|
type: 'text',
|
|
bandwidth: 1000,
|
|
language: language,
|
|
label: null,
|
|
kind: null,
|
|
width: null,
|
|
height: null,
|
|
frameRate: null,
|
|
mimeType: 'text/vtt',
|
|
codecs: 'vtt',
|
|
audioCodec: null,
|
|
videoCodec: null,
|
|
primary: false,
|
|
roles: [],
|
|
videoId: null,
|
|
audioId: null,
|
|
channelsCount: null,
|
|
audioBandwidth: null,
|
|
videoBandwidth: null
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {string} uri
|
|
* @return {function():!Array.<string>}
|
|
*/
|
|
function uris(uri) {
|
|
return () => [uri];
|
|
}
|
|
|
|
/**
|
|
* @return {shaka.extern.Manifest}
|
|
*/
|
|
function makeManifestWithPerStreamBandwidth() {
|
|
const SegmentReference = shaka.media.SegmentReference;
|
|
|
|
let manifest = new shaka.test.ManifestGenerator()
|
|
.setPresentationDuration(20)
|
|
.addPeriod(0)
|
|
.addVariant(0).language(englishUS).bandwidth(13 * kbps)
|
|
.addVideo(1).size(100, 200).bandwidth(10 * kbps)
|
|
.addAudio(2).language(englishUS).bandwidth(3 * kbps)
|
|
.addVariant(3).language(frenchCanadian).bandwidth(13 * kbps)
|
|
.addVideo(4).size(100, 200).bandwidth(10 * kbps)
|
|
.addAudio(5).language(frenchCanadian).bandwidth(3 * kbps)
|
|
.build();
|
|
|
|
getAllStreams(manifest).forEach((stream) => {
|
|
// Make a new copy each time as the segment index can modify
|
|
// each reference.
|
|
let refs = [
|
|
new SegmentReference(0, 0, 1, uris(segment1Uri), 0, null),
|
|
new SegmentReference(1, 1, 2, uris(segment2Uri), 0, null),
|
|
new SegmentReference(2, 2, 3, uris(segment3Uri), 0, null),
|
|
new SegmentReference(3, 3, 4, uris(segment4Uri), 0, null)
|
|
];
|
|
|
|
overrideSegmentIndex(stream, refs);
|
|
});
|
|
|
|
return manifest;
|
|
}
|
|
|
|
/**
|
|
* @return {shaka.extern.Manifest}
|
|
*/
|
|
function makeManifestWithoutPerStreamBandwidth() {
|
|
let manifest = makeManifestWithPerStreamBandwidth();
|
|
|
|
// Remove the per stream bandwidth.
|
|
getAllStreams(manifest).forEach((stream) => {
|
|
stream.bandwidth = undefined;
|
|
});
|
|
|
|
return manifest;
|
|
}
|
|
|
|
/**
|
|
* @return {shaka.extern.Manifest}
|
|
*/
|
|
function makeManifestWithNonZeroStart() {
|
|
const SegmentReference = shaka.media.SegmentReference;
|
|
|
|
let manifest = makeManifestWithPerStreamBandwidth();
|
|
|
|
getAllStreams(manifest).forEach((stream) => {
|
|
let refs = [
|
|
new SegmentReference(0, 10, 11, uris(segment1Uri), 0, null),
|
|
new SegmentReference(1, 11, 12, uris(segment2Uri), 0, null),
|
|
new SegmentReference(2, 12, 13, uris(segment3Uri), 0, null),
|
|
new SegmentReference(3, 13, 14, uris(segment4Uri), 0, null)
|
|
];
|
|
|
|
overrideSegmentIndex(stream, refs);
|
|
});
|
|
|
|
return manifest;
|
|
}
|
|
|
|
/**
|
|
* @return {shaka.extern.Manifest}
|
|
*/
|
|
function makeManifestWithLiveTimeline() {
|
|
let manifest = makeManifestWithPerStreamBandwidth();
|
|
manifest.presentationTimeline.setDuration(Infinity);
|
|
manifest.presentationTimeline.setStatic(false);
|
|
return manifest;
|
|
}
|
|
|
|
/**
|
|
* @param {shaka.extern.Manifest} manifest
|
|
* @return {!Array.<shaka.extern.Stream>}
|
|
*/
|
|
function getAllStreams(manifest) {
|
|
let streams = [];
|
|
|
|
manifest.periods.forEach((period) => {
|
|
period.variants.forEach((variant) => {
|
|
if (variant.audio) { streams.push(variant.audio); }
|
|
if (variant.video) { streams.push(variant.video); }
|
|
|
|
});
|
|
period.textStreams.forEach((stream) => {
|
|
streams.push(stream);
|
|
});
|
|
});
|
|
|
|
return streams;
|
|
}
|
|
|
|
/**
|
|
* @param {shaka.extern.Stream} stream
|
|
* @param {!Array.<shaka.media.SegmentReference>} segments
|
|
*/
|
|
function overrideSegmentIndex(stream, segments) {
|
|
let index = new shaka.media.SegmentIndex(segments);
|
|
stream.findSegmentPosition = (time) => index.find(time);
|
|
stream.getSegmentReference = (time) => index.get(time);
|
|
}
|
|
|
|
/** @return {!shaka.test.FakeNetworkingEngine} */
|
|
function makeNetworkEngine() {
|
|
let map = {};
|
|
map[segment1Uri] = new ArrayBuffer(16);
|
|
map[segment2Uri] = new ArrayBuffer(16);
|
|
map[segment3Uri] = new ArrayBuffer(16);
|
|
map[segment4Uri] = new ArrayBuffer(16);
|
|
|
|
let net = new shaka.test.FakeNetworkingEngine();
|
|
net.setResponseMap(map);
|
|
return net;
|
|
}
|
|
|
|
function eraseStorage() {
|
|
let muxer = new shaka.offline.StorageMuxer();
|
|
return shaka.util.IDestroyable.with([muxer], async () => {
|
|
await muxer.init();
|
|
await muxer.erase();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {!shaka.offline.Storage} storage
|
|
* @param {!shaka.media.DrmEngine} drm
|
|
* @param {shaka.extern.Manifest} manifest
|
|
*/
|
|
function overrideDrmAndManifest(storage, drm, manifest) {
|
|
/**
|
|
* @type {{
|
|
* drmEngine: !shaka.media.DrmEngine,
|
|
* manifest: shaka.extern.Manifest
|
|
* }}
|
|
*/
|
|
let ret = {
|
|
drmEngine: drm,
|
|
manifest: manifest
|
|
};
|
|
|
|
storage.loadInternal = () => Promise.resolve(ret);
|
|
}
|
|
|
|
/**
|
|
* @return {shaka.extern.DrmInfo}
|
|
*/
|
|
function makeDrmInfo() {
|
|
let drmInfo = {
|
|
keySystem: 'com.example.abc',
|
|
licenseServerUri: 'http://example.com',
|
|
persistentStateRequired: true,
|
|
distinctiveIdentifierRequired: false,
|
|
initData: null,
|
|
keyIds: null,
|
|
serverCertificate: null,
|
|
audioRobustness: 'HARDY',
|
|
videoRobustness: 'OTHER'
|
|
};
|
|
|
|
return drmInfo;
|
|
}
|
|
|
|
/** @implements {shaka.extern.ManifestParser} */
|
|
let FakeManifestParser = class {
|
|
constructor() {
|
|
this.map_ = {};
|
|
this.map_[manifestWithPerStreamBandwidthUri] =
|
|
makeManifestWithPerStreamBandwidth();
|
|
this.map_[manifestWithoutPerStreamBandwidthUri] =
|
|
makeManifestWithoutPerStreamBandwidth();
|
|
this.map_[manifestWithNonZeroStartUri] =
|
|
makeManifestWithNonZeroStart();
|
|
this.map_[manifestWithLiveTimelineUri] =
|
|
makeManifestWithLiveTimeline();
|
|
}
|
|
|
|
/** @override */
|
|
configure(params) {}
|
|
|
|
/** @override */
|
|
start(uri, player) {
|
|
return Promise.resolve(this.map_[uri]);
|
|
}
|
|
|
|
/** @override */
|
|
stop() {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/** @override */
|
|
update() {}
|
|
|
|
/** @override */
|
|
onExpirationUpdated(session, number) {}
|
|
};
|
|
|
|
/**
|
|
* @param {!shaka.media.DrmEngine} drmEngine
|
|
* @param {string} sessionName
|
|
* @return {!Promise.<MediaKeySession>}
|
|
*
|
|
* @suppress {accessControls}
|
|
*/
|
|
function loadOfflineSession(drmEngine, sessionName) {
|
|
return drmEngine.loadOfflineSession_(sessionName);
|
|
}
|
|
|
|
/**
|
|
* @param {!shaka.offline.OfflineUri} uri
|
|
* @return {!Promise.<shaka.extern.Manifest>}
|
|
*/
|
|
async function getStoredManifest(uri) {
|
|
/** @type {!shaka.offline.StorageMuxer} */
|
|
let muxer = new shaka.offline.StorageMuxer();
|
|
let manifestDB = await shaka.util.IDestroyable.with([muxer], async () => {
|
|
await muxer.init();
|
|
let cell = await muxer.getCell(uri.mechanism(), uri.cell());
|
|
let manifests = await cell.getManifests([uri.key()]);
|
|
let manifest = manifests[0];
|
|
|
|
return manifest;
|
|
});
|
|
|
|
goog.asserts.assert(manifestDB, 'A manifest should have been found');
|
|
|
|
let converter = new shaka.offline.ManifestConverter(
|
|
uri.mechanism(), uri.cell());
|
|
|
|
return converter.fromManifestDB(manifestDB);
|
|
}
|
|
|
|
/**
|
|
* Before running the test, check if storage is supported on this
|
|
* platform.
|
|
*
|
|
* @param {function():!Promise} test
|
|
* @return {function():!Promise}
|
|
*/
|
|
function checkAndRun(test) {
|
|
return async () => {
|
|
let hasSupport = shaka.offline.Storage.support();
|
|
if (hasSupport) {
|
|
await test();
|
|
} else {
|
|
pending('Storage is not supported on this platform.');
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Before running the test, check if licensing and storage is supported on
|
|
* this platform.
|
|
*
|
|
* @param {function():!Promise} test
|
|
* @return {function():!Promise}
|
|
*/
|
|
function drmCheckAndRun(test) {
|
|
return async () => {
|
|
let support = await shaka.Player.probeSupport();
|
|
|
|
let widevineSupport = support.drm['com.widevine.alpha'];
|
|
let storageSupport = shaka.offline.Storage.support();
|
|
|
|
if (!widevineSupport) {
|
|
pending('Widevine is not supported on this platform');
|
|
return;
|
|
}
|
|
|
|
if (!widevineSupport.persistentState) {
|
|
pending('Widevine persistent state is not supported on this platform');
|
|
return;
|
|
}
|
|
|
|
if (!storageSupport) {
|
|
pending('Storage is not supported on this platform.');
|
|
return;
|
|
}
|
|
|
|
return test();
|
|
};
|
|
}
|
|
});
|