From 54b8f6eaee8153c80ee2cea8d13651adb79ac943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Velad=20Galv=C3=A1n?= Date: Mon, 1 Feb 2021 20:45:19 +0100 Subject: [PATCH] feat: Add downloadSizeCallback before storing offline (#3049) When downloading content for storage, it's useful to know how much storage space will be needed before storing. This is extremely important for mobile devices that have limited space. See google/shaka-player-embedded#167. Use the info in the manifest and make HEAD requests to determine the size of the content we're downloading. Add a downloadSizeCallback that will be called with the size once we know it. The callback determine if the content can be downloaded due to its estimated size. Closes #3049 . --- externs/shaka/player.js | 5 +++++ lib/offline/storage.js | 23 +++++++++++++++++++++++ lib/util/error.js | 11 +++++++++++ lib/util/player_configuration.js | 10 ++++++++++ 4 files changed, 49 insertions(+) diff --git a/externs/shaka/player.js b/externs/shaka/player.js index b58ee48a20..1cc3446048 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -893,6 +893,7 @@ shaka.extern.AbrConfiguration; * @typedef {{ * trackSelectionCallback: * function(shaka.extern.TrackList):!Promise, + * downloadSizeCallback: function(number):!Promise, * progressCallback: function(shaka.extern.StoredContent,number), * usePersistentLicense: boolean * }} @@ -902,6 +903,10 @@ shaka.extern.AbrConfiguration; * Called inside store() to determine which tracks to save from a * manifest. It is passed an array of Tracks from the manifest and it should * return an array of the tracks to store. + * @property {function(number):!Promise} downloadSizeCallback + * Called inside store() to determine if the content can be + * downloaded due to its estimated size. The estimated size of the download is + * passed and it must return if the download is allowed or not. * @property {function(shaka.extern.StoredContent,number)} progressCallback * Called inside store() to give progress info back to the app. * It is given the current manifest being stored and the progress of it being diff --git a/lib/offline/storage.js b/lib/offline/storage.js index 2a853e704d..81c59cc55c 100644 --- a/lib/offline/storage.js +++ b/lib/offline/storage.js @@ -537,6 +537,29 @@ shaka.offline.Storage = class { // Let the application choose which tracks to store. const chosenTracks = await config.offline.trackSelectionCallback(allTracks); + const duration = manifest.presentationTimeline.getDuration(); + let sizeEstimate = 0; + for (const track of chosenTracks) { + const trackSize = track.bandwidth * duration / 8; + sizeEstimate += trackSize; + } + try { + const allowedDownload = + await config.offline.downloadSizeCallback(sizeEstimate); + if (!allowedDownload) { + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.STORAGE, + shaka.util.Error.Code.STORAGE_LIMIT_REACHED); + } + } catch (e) { + shaka.log.warning( + 'downloadSizeCallback has produced an unexpected error', e); + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.STORAGE, + shaka.util.Error.Code.DOWNLOAD_SIZE_CALLBACK_ERROR); + } /** @type {!Set.} */ const variantIds = new Set(); diff --git a/lib/util/error.js b/lib/util/error.js index b4686e6676..37d4b2b5e0 100644 --- a/lib/util/error.js +++ b/lib/util/error.js @@ -976,6 +976,17 @@ shaka.util.Error.Code = { */ 'MISSING_STORAGE_CELL': 9013, + /** + * The storage limit defined in downloadSizeCallback has been + * reached. + */ + 'STORAGE_LIMIT_REACHED': 9014, + + /** + * downloadSizeCallback has produced an unexpected error. + */ + 'DOWNLOAD_SIZE_CALLBACK_ERROR': 9015, + /** * CS IMA SDK, required for ad insertion, has not been included on the page. */ diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index eff0d0ebed..ed93189abc 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -159,6 +159,16 @@ shaka.util.PlayerConfiguration = class { // eslint-disable-next-line require-await trackSelectionCallback: async (tracks) => tracks, + downloadSizeCallback: async (sizeEstimate) => { + if (navigator.storage && navigator.storage.estimate) { + const estimate = await navigator.storage.estimate(); + // Limit to 95% of quota. + return estimate.usage + sizeEstimate < estimate.quota * 0.95; + } else { + return true; + } + }, + // Need some operation in the callback or else closure may remove calls // to the function as it would be a no-op. The operation can't just be a // log message, because those are stripped in the compiled build.