diff --git a/externs/shaka/offline.js b/externs/shaka/offline.js index 513ab54668..ee97eb0e5d 100644 --- a/externs/shaka/offline.js +++ b/externs/shaka/offline.js @@ -162,7 +162,7 @@ shakaExtern.PeriodDB; * encrypted: boolean, * keyId: ?string, * segments: !Array., - * variantIds: ?Array. + * variantIds: !Array. * }} * * @property {number} id @@ -197,7 +197,7 @@ shakaExtern.PeriodDB; * The key ID this stream is encrypted with. * @property {!Array.} segments * An array of segments that make up the stream - * @property {?Array.} variantIds + * @property {!Array.} variantIds * An array of ids of variants the stream is a part of. */ shakaExtern.StreamDB; diff --git a/lib/offline/db_engine.js b/lib/offline/db_engine.js index 1e4f8004f2..f348dbeea5 100644 --- a/lib/offline/db_engine.js +++ b/lib/offline/db_engine.js @@ -188,17 +188,23 @@ shaka.offline.DBEngine.prototype.destroy = function() { /** @override */ shaka.offline.DBEngine.prototype.getManifest = function(key) { - return this.get_( - shaka.offline.DBEngine.Store_.MANIFEST, - key); + /** @const */ + var Store = shaka.offline.DBEngine.Store_; + + return this.get_(Store.MANIFEST, key).then(function(manifest) { + return manifest ? + shaka.offline.DBEngine.sanitizeManifest_(manifest) : + null; + }); }; /** @override */ shaka.offline.DBEngine.prototype.forEachManifest = function(each) { - return this.forEach_( - shaka.offline.DBEngine.Store_.MANIFEST, - each); + /** @const */ + var Store = shaka.offline.DBEngine.Store_; + + return this.forEach_(Store.MANIFEST, each); }; @@ -565,3 +571,93 @@ shaka.offline.DBEngine.getLastId_ = function(store, onId) { } }; }; + + +/** + * @param {!Object} manifest + * @return {!shakaExtern.ManifestDB} + * @private + */ +shaka.offline.DBEngine.sanitizeManifest_ = function(manifest) { + // Purposely do not use types here as it will allow us to "schrodinger's cat" + // the type until the end where we can conform the type to the final + // ManifestDB type. + + /** @const */ + var ContentType = shaka.util.ManifestParserUtils.ContentType; + + // There are three cases: + // 1. All streams' variant ids are null + // 2. All streams' variant ids are non-null + // 3. Some streams' variant ids are null and other are non-null + // Case 3 is invalid and should never happen in production. + + var allStreams = []; + manifest.periods.forEach(function(period) { + allStreams.push.apply(allStreams, period.streams); + }); + + var audioStreams = allStreams.filter(function(stream) { + return stream.contentType == ContentType.AUDIO; + }); + + var videoStreams = allStreams.filter(function(stream) { + return stream.contentType == ContentType.VIDEO; + }); + + var audioVideoStreams = []; + audioVideoStreams.push.apply(audioVideoStreams, audioStreams); + audioVideoStreams.push.apply(audioVideoStreams, videoStreams); + + var allVariantIdsNull = allStreams.every(function(stream) { + var ids = stream.variantIds; + return ids == null; + }); + + var allVariantIdsNonNull = allStreams.every(function(stream) { + var ids = stream.variantIds; + return ids != null && ids != undefined; + }); + + // Case 3 + goog.asserts.assert( + allVariantIdsNull || allVariantIdsNonNull, + 'All variant ids should be null or non-null.'); + + // Convert Case 1 to Case 2 + // TODO (vaage) : Move the conversion of case 1 to case 2 to database + // upgrade. + if (allVariantIdsNull) { + // Since all the variant ids are null, we need to first make them into + // valid arrays. + allStreams.forEach(function(stream) { + stream.variantIds = []; + }); + + /** @type {number} */ + var currentVariantId = 0; + + // It is not possible in the pre-variant world of shaka to have audio-only + // and video-only content mixed in with audio-video content. So we can + // assume that there is only audio-only or video-only if one group is empty. + if (audioStreams.length == 0 || videoStreams.length == 0) { + // Create all audio only and all video only variants. + audioVideoStreams.forEach(function(stream) { + stream.variantIds.push(currentVariantId); + currentVariantId++; + }); + } else { + // Create all audio and video variants. + audioStreams.forEach(function(audio) { + videoStreams.forEach(function(video) { + audio.variantIds.push(currentVariantId); + video.variantIds.push(currentVariantId); + + currentVariantId++; + }); + }); + } + } + + return /** @type {shakaExtern.ManifestDB} */ (manifest); +}; diff --git a/lib/offline/offline_utils.js b/lib/offline/offline_utils.js index 33bd0687e9..f0cdd21579 100644 --- a/lib/offline/offline_utils.js +++ b/lib/offline/offline_utils.js @@ -125,66 +125,14 @@ shaka.offline.OfflineUtils.recreateVariants = function( var MapUtils = shaka.util.MapUtils; var OfflineUtils = shaka.offline.OfflineUtils; - // There are three cases: - // 1. All streams' variant ids are null - // 2. All streams' variant ids are non-null - // 3. Some streams' variant ids are null and other are non-null - // Case 3 is invalid and should never happen in production. + // Create a variant for each variant id. + /** @type {!Object.} */ + var variantMap = {}; - /** @type {!Array.} */ var allStreams = []; allStreams.push.apply(allStreams, audios); allStreams.push.apply(allStreams, videos); - var allVariantIdsNull = - allStreams.every(function(stream) { return stream.variantIds == null; }); - - var allVariantIdsNonNull = - allStreams.every(function(stream) { return stream.variantIds != null; }); - - // Case 3 - goog.asserts.assert( - allVariantIdsNull || allVariantIdsNonNull, - 'All variant ids should be null or non-null.'); - - // Convert Case 1 to Case 2 - // TODO (vaage) : Move the conversion of case 1 to case 2 to a storage upgrade - // section of code. - if (allVariantIdsNull) { - // Since all the variant ids are null, we need to first make them into - // valid arrays. - allStreams.forEach(function(stream) { - stream.variantIds = []; - }); - - /** @type {number} */ - var currentVariantId = 0; - - // It is not possible in the pre-variant world of shaka to have audio-only - // and video-only content mixed in with audio-video content. So we can - // assume that there is only audio-only or video-only if one group is empty. - if (audios.length == 0 || videos.length == 0) { - // Create all audio only and all video only variants. - allStreams.forEach(function(stream) { - stream.variantIds.push(currentVariantId); - currentVariantId++; - }); - } else { - // Create all audio and video variants. - audios.forEach(function(audio) { - videos.forEach(function(video) { - audio.variantIds.push(currentVariantId); - video.variantIds.push(currentVariantId); - - currentVariantId++; - }); - }); - } - } - - // Create a variant for each variant id. - /** @type {!Object.} */ - var variantMap = {}; allStreams.forEach(function(stream) { stream.variantIds.forEach(function(id) { if (!variantMap[id]) { diff --git a/test/offline/db_engine_unit.js b/test/offline/db_engine_unit.js index 5186808d5b..169a39848d 100644 --- a/test/offline/db_engine_unit.js +++ b/test/offline/db_engine_unit.js @@ -16,6 +16,9 @@ */ describe('DBEngine', /** @suppress {accessControls} */ function() { + /** @const */ + var ContentType = shaka.util.ManifestParserUtils.ContentType; + /** @const {string} */ var dbName = 'shaka-player-test-db'; @@ -208,6 +211,64 @@ describe('DBEngine', /** @suppress {accessControls} */ function() { .then(done).catch(fail); })); + // TODO : Remove this test once we drop support for DB Engine Version 1 + it('fills missing variant ids for old manifests', checkAndRun(function(done) { + /** @type {number} */ + var id = db.reserveManifestId(); + + // Create a manifest with two streams. One video and one audio. When db + // engine recreates the variant ids, it should pair them together into + // a variant. + + /** @type {shakaExtern.ManifestDB} */ + var originalManifest = createManifest(id); + originalManifest.periods.push({ + startTime: 0, + streams: [ + createStream(0, ContentType.AUDIO), + createStream(1, ContentType.AUDIO), + createStream(2, ContentType.VIDEO), + createStream(3, ContentType.VIDEO) + ] + }); + + // Remove the variant ids field from all streams. + originalManifest.periods[0].streams.forEach(function(stream) { + delete stream['variantIds']; + expect(stream.variantIds).toBe(undefined); + }); + + Promise.resolve() + .then(function() { + return db.insertManifest(originalManifest); + }) + .then(function() { + return db.getManifest(id); + }) + .then(function(manifest) { + expect(manifest.periods.length).toBe(1); + + var streams = manifest.periods[0].streams; + expect(streams.length).toBe(4); + + // Create a mapping of variants to stream ids. + var variants = {}; + streams.forEach(function(stream) { + stream.variantIds.forEach(function(id) { + variants[id] = variants[id] || []; + variants[id].push(stream.id); + }); + }); + + shaka.log.info(variants); + + expect(variants[0]).toEqual([0, 2]); + expect(variants[1]).toEqual([0, 3]); + expect(variants[2]).toEqual([1, 2]); + expect(variants[3]).toEqual([1, 3]); + }).then(done).catch(fail); + })); + /** * Before running the test, check if DBEngine is supported on this platform. @@ -244,6 +305,34 @@ describe('DBEngine', /** @suppress {accessControls} */ function() { } + /** + * @param {number} id + * @param {string} type + * @return {shakaExtern.StreamDB} + */ + function createStream(id, type) { + return { + id: id, + primary: false, + presentationTimeOffset: 0, + contentType: type, + mimeType: '', + codecs: '', + frameRate: undefined, + kind: undefined, + language: '', + label: null, + width: null, + height: null, + initSegmentUri: null, + encrypted: false, + keyId: null, + segments: [], + variantIds: [] + }; + } + + /** * @param {number} id * @return {shakaExtern.SegmentDataDB} diff --git a/test/offline/offline_utils_unit.js b/test/offline/offline_utils_unit.js index 57f73f3ce2..6073a5758c 100644 --- a/test/offline/offline_utils_unit.js +++ b/test/offline/offline_utils_unit.js @@ -73,26 +73,6 @@ describe('OfflineUtils', function() { expect(variants[1].video.id).toBe(3); }); - it('will create variants with no variant ids', function() { - /** @type {!Array.} */ - var audios = [ - createStreamDB(0, audioType, null), - createStreamDB(1, audioType, null) - ]; - /** @type {!Array.} */ - var videos = [ - createStreamDB(2, videoType, null), - createStreamDB(3, videoType, null) - ]; - /** @type {!Array.} */ - var drm = []; - - /** @type {!Array.} */ - var variants = OfflineUtils.recreateVariants(audios, videos, drm); - - expect(variants.length).toBe(4); - }); - it('will create variants when there is only audio', function() { /** @type {!Array.} */ var audios = [ @@ -110,23 +90,6 @@ describe('OfflineUtils', function() { expect(variants.length).toBe(2); }); - it('will create variants when there is only audio with no ids', function() { - /** @type {!Array.} */ - var audios = [ - createStreamDB(0, audioType, null), - createStreamDB(1, audioType, null) - ]; - /** @type {!Array.} */ - var videos = []; - /** @type {!Array.} */ - var drm = []; - - /** @type {!Array.} */ - var variants = OfflineUtils.recreateVariants(audios, videos, drm); - - expect(variants.length).toBe(2); - }); - it('will create variants when there is only video', function() { /** @type {!Array.} */ var audios = []; @@ -144,27 +107,10 @@ describe('OfflineUtils', function() { expect(variants.length).toBe(2); }); - it('will create variants when there is only video with no ids', function() { - /** @type {!Array.} */ - var audios = []; - /** @type {!Array.} */ - var videos = [ - createStreamDB(2, videoType, null), - createStreamDB(3, videoType, null) - ]; - /** @type {!Array.} */ - var drm = []; - - /** @type {!Array.} */ - var variants = OfflineUtils.recreateVariants(audios, videos, drm); - - expect(variants.length).toBe(2); - }); - /** * @param {number} id * @param {string} type - * @param {?Array.} variants + * @param {!Array.} variants * @return {shakaExtern.StreamDB} */ function createStreamDB(id, type, variants) { diff --git a/test/test/util/manifest_db_builder.js b/test/test/util/manifest_db_builder.js index 06dd24d80b..409ad597e9 100644 --- a/test/test/util/manifest_db_builder.js +++ b/test/test/util/manifest_db_builder.js @@ -139,7 +139,7 @@ shaka.test.ManifestDBBuilder.prototype.stream = function() { encrypted: false, keyId: null, segments: [], - variantIds: null + variantIds: [] }; this.currentStream_ = stream;