shaka-player
Version:
DASH/EME video player library
345 lines (297 loc) • 12 kB
JavaScript
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// All of the database dumps referenced below were originally made from the
// "Heliocentrism" content in our demo app.
// https://storage.googleapis.com/shaka-demo-assets/heliocentrism/heliocentrism.mpd
// The dumps were made with test/test/util/canned_idb.js
const compatibilityTestsMetadata = [
{
// This was our original (v1) storage format for Shaka Player v2.0,
// deprecated in v2.3. This is not the same as the storage format from
// Shaka Player v1, which is no longer supported.
name: 'v1',
dbImagePath: '/base/test/test/assets/db-dump-v1.json',
manifestKey: 0,
readOnly: true,
makeCell: (connection) => new shaka.offline.indexeddb.V1StorageCell(
connection,
/* segmentStore= */ 'segment',
/* manifestStore= */ 'manifest'),
},
{
// Two variants of v2 exist in the field. This is the initial version of
// v2, as upgraded from v1 databases. It was broken in a way that prevented
// new records from being added. This format was introduced in Shaka Player
// v2.3 and deprecated in v2.3.2.
name: 'v2-broken',
dbImagePath: '/base/test/test/assets/db-dump-v2-broken.json',
manifestKey: 0,
readOnly: true,
makeCell: (connection) => new shaka.offline.indexeddb.V2StorageCell(
connection,
/* segmentStore= */ 'segment-v2',
/* manifestStore= */ 'manifest-v2'),
},
{
// This is the "clean" version of the v2 database format, as created from
// scratch, to which new records could be added. This format was introduced
// in Shaka Player v2.3 and deprecated in v2.3.2.
name: 'v2-clean',
dbImagePath: '/base/test/test/assets/db-dump-v2-clean.json',
manifestKey: 1,
readOnly: true,
makeCell: (connection) => new shaka.offline.indexeddb.V2StorageCell(
connection,
/* segmentStore= */ 'segment-v2',
/* manifestStore= */ 'manifest-v2'),
},
{
// This is the v3 version of the database, which is actually identical to
// the "clean" version of the v2 database. The version number was
// incremented to overcome the "broken" v2 databases. This format was
// introduced in v2.3.2 and deprecated in v3.0.
name: 'v3',
dbImagePath: '/base/test/test/assets/db-dump-v3.json',
manifestKey: 1,
readOnly: true,
makeCell: (connection) => new shaka.offline.indexeddb.V2StorageCell(
connection,
/* segmentStore= */ 'segment-v3',
/* manifestStore= */ 'manifest-v3'),
},
{
// This is the v4 version of the database as written by v2.5.0 - v2.5.9. A
// bug in v2.5 caused the stream metadata from all periods to be written to
// each period. This was corrected in v2.5.10.
// See https://github.com/shaka-project/shaka-player/issues/2389
name: 'v4-broken',
dbImagePath: '/base/test/test/assets/db-dump-v4-broken.json',
manifestKey: 1,
readOnly: true,
makeCell: (connection) => new shaka.offline.indexeddb.V2StorageCell(
connection,
// V4 of the database still used the V3 store names and structures.
/* segmentStore= */ 'segment-v3',
/* manifestStore= */ 'manifest-v3'),
},
{
// This is the v5 version of the database, introduced in v3.0.
name: 'v5',
dbImagePath: '/base/test/test/assets/db-dump-v5.json',
manifestKey: 1,
readOnly: false,
makeCell: (connection) => new shaka.offline.indexeddb.V5StorageCell(
connection,
/* segmentStore= */ 'segment-v5',
/* manifestStore= */ 'manifest-v5'),
},
];
filterDescribe('Storage Compatibility', offlineSupported, () => {
for (const metadata of compatibilityTestsMetadata) {
describe(metadata.name, () => {
makeTests(metadata);
});
}
function makeTests(metadata) {
const CannedIDB = shaka.test.CannedIDB;
const ContentType = shaka.util.ManifestParserUtils.ContentType;
const Util = shaka.test.Util;
/** @type {?shaka.extern.StorageCell} */
let cell = null;
/** @type {?IDBDatabase} */
let connection = null;
/** @type {string} */
let dbImageAsString;
beforeAll(async () => {
const data = await shaka.test.Util.fetch(metadata.dbImagePath);
dbImageAsString = shaka.util.StringUtils.fromUTF8(data);
});
beforeEach(async () => {
const dbName = 'shaka-storage-cell-test';
// Load the canned database image.
await CannedIDB.restoreJSON(
dbName, dbImageAsString, /* wipeDatabase= */ true);
// Track the connection so that we can close it when the test is over.
connection = await shaka.test.IndexedDBUtils.open(dbName);
// Create a storage cell.
cell = metadata.makeCell(connection);
});
afterEach(async () => {
// Destroy the cell before killing the connection.
if (cell) {
await cell.destroy();
}
cell = null;
if (connection) {
connection.close();
}
connection = null;
});
if (metadata.readOnly) {
it('cannot add new manifests', async () => {
const expected = Util.jasmineError(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.NEW_KEY_OPERATION_NOT_SUPPORTED,
jasmine.any(String)));
// There should be one manifest.
const manifests = await cell.getAllManifests();
const manifest = manifests.get(metadata.manifestKey);
expect(manifest).toBeTruthy();
// Make sure that the request fails.
await expectAsync(
cell.addManifests([manifest])).toBeRejectedWith(expected);
});
it('cannot add new segment', async () => {
const expected = Util.jasmineError(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.NEW_KEY_OPERATION_NOT_SUPPORTED,
jasmine.any(String)));
// Update the key to what should be a free key.
const segment = {data: new ArrayBuffer(16)};
// Make sure that the request fails.
await expectAsync(
cell.addSegments([segment])).toBeRejectedWith(expected);
});
} // if (metadata.readOnly)
it('can get all manifests', async () => {
// There should be one manifest.
const map = await cell.getAllManifests();
expect(map).toBeTruthy();
expect(map.size).toBe(1);
expect(map.get(metadata.manifestKey)).toBeTruthy();
});
it('can get manifest and all segments', async () => {
// There should be one manifest.
const manifests = await cell.getManifests([metadata.manifestKey]);
const manifest = manifests[0];
expect(manifest).toBeTruthy();
// Collect all the keys for each segment.
const dataKeys = getAllSegmentKeys(manifest);
// Check that each segment was successfully retrieved.
const segmentData = await cell.getSegments(dataKeys);
expect(segmentData.length).not.toBe(0);
for (const segment of segmentData) {
expect(segment).toBeTruthy();
}
});
it('can update expiration', async () => {
const oldExpiration = Infinity;
const newExpiration = 1000;
const original = await cell.getManifests([metadata.manifestKey]);
expect(original).toBeTruthy();
expect(original[0]).toBeTruthy();
expect(original[0].expiration).toBe(oldExpiration);
await cell.updateManifestExpiration(metadata.manifestKey, newExpiration);
const updated = await cell.getManifests([metadata.manifestKey]);
expect(updated).toBeTruthy();
expect(updated[0]).toBeTruthy();
expect(updated[0].expiration).toBe(newExpiration);
});
it('can remove manifests and segments', async () => {
/** @type {!Array.<number>} */
const manifestKeys = [];
/** @type {!Array.<number>} */
const segmentKeys = [];
const manifests = await cell.getAllManifests();
manifests.forEach((manifest, manifestKey) => {
manifestKeys.push(manifestKey);
for (const key of getAllSegmentKeys(manifest)) {
segmentKeys.push(key);
}
});
expect(manifestKeys.length).toBe(1);
expect(segmentKeys.length).not.toBe(0);
// Remove all the segments.
const noop = () => {};
await cell.removeManifests(manifestKeys, noop);
await cell.removeSegments(segmentKeys, noop);
const expected = Util.jasmineError(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.KEY_NOT_FOUND,
jasmine.any(String)));
const checkMissingSegment = async (key) => {
await expectAsync(cell.getSegments([key])).toBeRejectedWith(expected);
};
const checkMissingManifest = async (key) => {
await expectAsync(cell.getManifests([key])).toBeRejectedWith(expected);
};
// Need to check each key on its own to ensure that each key is missing
// and not just one of the keys is missing.
const checkMissingSegments = (keys) => {
return Promise.all(keys.map((key) => checkMissingSegment(key)));
};
const checkMissingManifests = (keys) => {
return Promise.all(keys.map((key) => checkMissingManifest(key)));
};
await checkMissingSegments(segmentKeys);
await checkMissingManifests(manifestKeys);
});
it('correctly converts to the current manifest format', async () => {
// There should be one manifest.
const manifestDb = (await cell.getManifests([metadata.manifestKey]))[0];
const converter = new shaka.offline.ManifestConverter(
'mechanism', 'cell');
const actual = converter.fromManifestDB(manifestDb);
const expected = shaka.test.ManifestGenerator.generate((manifest) => {
manifest.anyTimeline();
manifest.minBufferTime = 2;
manifest.addPartialVariant((variant) => {
variant.addPartialStream(ContentType.VIDEO, (stream) => {
stream.frameRate = 29.97;
stream.mime('video/webm', 'vp9');
stream.size(640, 480);
});
});
});
expect(actual).toEqual(expected);
const segmentIndex = actual.variants[0].video.segmentIndex;
goog.asserts.assert(segmentIndex != null, 'Null segmentIndex!');
const [segment0, segment1, segment2] = Array.from(segmentIndex);
expect(segment0).toEqual(jasmine.objectContaining({
startTime: 0,
endTime: Util.closeTo(2.06874),
timestampOffset: 0,
appendWindowStart: 0,
appendWindowEnd: Util.closeTo(2.06874),
}));
expect(segment1).toEqual(jasmine.objectContaining({
startTime: Util.closeTo(2.06874),
endTime: Util.closeTo(4.20413),
timestampOffset: Util.closeTo(2.06874),
appendWindowStart: Util.closeTo(2.06874),
appendWindowEnd: Util.closeTo(4.20413),
}));
expect(segment2).toEqual(jasmine.objectContaining({
startTime: Util.closeTo(4.20413),
endTime: Util.closeTo(4.904831),
timestampOffset: Util.closeTo(4.20413),
appendWindowStart: Util.closeTo(4.20413),
appendWindowEnd: Util.closeTo(4.904831),
}));
});
/**
* Get the keys for each segment. This will include the init segments.
*
* @param {shaka.extern.ManifestDB} manifest
* @return {!Array.<number>}
*/
function getAllSegmentKeys(manifest) {
const keys = new Set();
for (const stream of manifest.streams) {
for (const segment of stream.segments) {
if (segment.initSegmentKey != null) {
keys.add(segment.initSegmentKey);
}
keys.add(segment.dataKey);
}
}
return Array.from(keys);
}
} // makeTests
});