From 07894ecf14a19ff2052d6029217c6eb201ece402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mari=C3=B1o?= <1237997+CHaNGeTe@users.noreply.github.com> Date: Fri, 29 Nov 2024 12:06:32 +0100 Subject: [PATCH] fix: Timeout unfulfilled request to decodingInfo and requestMediaKeySystemAccess (#7682) On some (Android) WebView environments, decodingInfo and requestMediaKeySystemAccess will not resolve or reject, at least if RESOURCE_PROTECTED_MEDIA_ID is not set. This is a workaround for that issue. Closes #7680 --- lib/media/drm_engine.js | 29 +++++++++++++++++++++++------ lib/util/functional.js | 27 +++++++++++++++++++++++++++ lib/util/stream_utils.js | 9 ++++++++- test/util/functional_unit.js | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 test/util/functional_unit.js diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 5ccd43d57d..606853ee02 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -15,6 +15,7 @@ goog.require('shaka.util.DrmUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.EventManager'); goog.require('shaka.util.FakeEvent'); +goog.require('shaka.util.Functional'); goog.require('shaka.util.IDestroyable'); goog.require('shaka.util.Iterables'); goog.require('shaka.util.ManifestParserUtils'); @@ -1870,9 +1871,16 @@ shaka.media.DrmEngine = class { offlineConfig.sessionTypes = ['persistent-license']; const configs = [offlineConfig, basicConfig]; - - const access = await navigator.requestMediaKeySystemAccess( - keySystem, configs); + // On some (Android) WebView environments, + // requestMediaKeySystemAccess will + // not resolve or reject, at least if RESOURCE_PROTECTED_MEDIA_ID + // is not set. This is a workaround for that issue. + const TIMEOUT_FOR_CHECKACCESS_IN_SECONDS = 1; + const access = + await shaka.util.Functional.promiseWithTimeout( + TIMEOUT_FOR_CHECKACCESS_IN_SECONDS, + navigator.requestMediaKeySystemAccess(keySystem, configs), + ); await processMediaKeySystemAccess(keySystem, access); } catch (error) {} // Ignore errors. }; @@ -1907,13 +1915,22 @@ shaka.media.DrmEngine = class { }, }, }; - + // On some (Android) WebView environments, decodingInfo will + // not resolve or reject, at least if RESOURCE_PROTECTED_MEDIA_ID + // is not set. This is a workaround for that issue. + const TIMEOUT_FOR_DECODING_INFO_IN_SECONDS = 1; const decodingInfo = - await navigator.mediaCapabilities.decodingInfo(decodingConfig); + await shaka.util.Functional.promiseWithTimeout( + TIMEOUT_FOR_DECODING_INFO_IN_SECONDS, + navigator.mediaCapabilities.decodingInfo(decodingConfig), + ); const access = decodingInfo.keySystemAccess; await processMediaKeySystemAccess(keySystem, access); - } catch (error) {} // Ignore errors. + } catch (error) { + // Ignore errors. + shaka.log.v2('Failed to probe support for', keySystem, error); + } }; // Initialize the support structure for each key system. diff --git a/lib/util/functional.js b/lib/util/functional.js index 694d99d49c..06722067b4 100644 --- a/lib/util/functional.js +++ b/lib/util/functional.js @@ -6,6 +6,8 @@ goog.provide('shaka.util.Functional'); +goog.require('shaka.util.Timer'); + /** * @summary A set of functional utility functions. */ @@ -67,4 +69,29 @@ shaka.util.Functional = class { static isNotNull(value) { return value != null; } + + /** + * Returns a Promise which is resolved only if |asyncProcess| is resolved, and + * only if it is resolved in less than |seconds| seconds. + * + * If the returned Promise is resolved, it returns the same value as + * |asyncProcess|. + * + * If |asyncProcess| fails, the returned Promise is rejected. + * If |asyncProcess| takes too long, the returned Promise is rejected, but + * |asyncProcess| is still allowed to complete. + * + * @param {number} seconds + * @param {!Promise.} asyncProcess + * @return {!Promise.} + * @template T + */ + static promiseWithTimeout(seconds, asyncProcess) { + return Promise.race([ + asyncProcess, + new Promise(((_, reject) => { + new shaka.util.Timer(reject).tickAfter(seconds); + })), + ]); + } }; diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js index 6dca1ff98d..8ec728e6b6 100644 --- a/lib/util/stream_utils.js +++ b/lib/util/stream_utils.js @@ -727,7 +727,14 @@ shaka.util.StreamUtils = class { mimeType, audioCodec); } promises.push(new Promise((resolve, reject) => { - navigator.mediaCapabilities.decodingInfo(copy).then((res) => { + // On some (Android) WebView environments, decodingInfo will + // not resolve or reject, at least if RESOURCE_PROTECTED_MEDIA_ID + // is not set. This is a workaround for that issue. + const TIMEOUT_FOR_DECODING_INFO_IN_SECONDS = 1; + shaka.util.Functional.promiseWithTimeout( + TIMEOUT_FOR_DECODING_INFO_IN_SECONDS, + navigator.mediaCapabilities.decodingInfo(copy), + ).then((res) => { resolve(res); }).catch(reject); })); diff --git a/test/util/functional_unit.js b/test/util/functional_unit.js new file mode 100644 index 0000000000..46d09e93ef --- /dev/null +++ b/test/util/functional_unit.js @@ -0,0 +1,34 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +describe('Functional', () => { + const Functional = shaka.util.Functional; + describe('promiseWithTimeout', () => { + it('resolves if asyncProcess resolves within the timeout', async () => { + const asyncProcess = new Promise((resolve) => + setTimeout(() => resolve('success'), 100), + ); + const result = await Functional.promiseWithTimeout(1, asyncProcess); + expect(result).toBe('success'); + }); + + it('rejects if asyncProcess rejects', async () => { + const asyncProcess = new Promise((_, reject) => + setTimeout(() => reject('error'), 100), + ); + const promise = Functional.promiseWithTimeout(1, asyncProcess); + await expectAsync(promise).toBeRejectedWith('error'); + }); + + it('rejects if asyncProcess takes longer than the timeout', async () => { + const asyncProcess = new Promise((resolve) => + setTimeout(() => resolve('success'), 2000), + ); + const promise = Functional.promiseWithTimeout(1, asyncProcess); + await expectAsync(promise).toBeRejected(); + }); + }); +});