From d9b9bae785525487bd5e0f4b07b3ad49b77dcfc5 Mon Sep 17 00:00:00 2001 From: Daniel Marino <1237997+CHaNGeTe@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:37:16 +0100 Subject: [PATCH 1/6] fix: Timeout unfulfilled request to decodingInfo and requestMediaKeySystemAccess 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. --- lib/media/drm_engine.js | 50 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 5ccd43d57d..601c1f1b46 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -1870,8 +1870,27 @@ shaka.media.DrmEngine = class { offlineConfig.sessionTypes = ['persistent-license']; const configs = [offlineConfig, basicConfig]; - - const access = await navigator.requestMediaKeySystemAccess( + // 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 checkAccessOrTimeout = (keySystem, configs) => { + return new Promise((resolve, reject) => { + let resolved = false; + navigator.requestMediaKeySystemAccess(keySystem, configs) + .then((access) => { + resolved = true; + resolve(access); + }); + new shaka.util.Timer(() => { + if (!resolved) { + reject(new Error('Timeout requestMediaKeySystemAccess')); + } + }).tickAfter(TIMEOUT_FOR_CHECKACCESS_IN_SECONDS); + }); + }; + const access = await checkAccessOrTimeout( keySystem, configs); await processMediaKeySystemAccess(keySystem, access); } catch (error) {} // Ignore errors. @@ -1907,13 +1926,34 @@ 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 decodeOrTimeout = (decodingConfig) => { + return new Promise((resolve, reject) => { + let resolved = false; + navigator.mediaCapabilities.decodingInfo(decodingConfig) + .then((result) => { + resolved = true; + resolve(result); + }); + new shaka.util.Timer(() => { + if (!resolved) { + reject(new Error('Timeout decodingInfo')); + } + }).tickAfter(TIMEOUT_FOR_DECODING_INFO_IN_SECONDS); + }); + }; const decodingInfo = - await navigator.mediaCapabilities.decodingInfo(decodingConfig); + await decodeOrTimeout(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. From 190c14796265a93cdd959a8ef99e7ab4bd305b45 Mon Sep 17 00:00:00 2001 From: Daniel Marino <1237997+CHaNGeTe@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:18:12 +0100 Subject: [PATCH 2/6] chore: utility function promiseWithTimeout --- lib/media/drm_engine.js | 43 ++++++++++------------------------------- lib/util/functional.js | 27 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 601c1f1b46..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'); @@ -1875,23 +1876,11 @@ shaka.media.DrmEngine = class { // 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 checkAccessOrTimeout = (keySystem, configs) => { - return new Promise((resolve, reject) => { - let resolved = false; - navigator.requestMediaKeySystemAccess(keySystem, configs) - .then((access) => { - resolved = true; - resolve(access); - }); - new shaka.util.Timer(() => { - if (!resolved) { - reject(new Error('Timeout requestMediaKeySystemAccess')); - } - }).tickAfter(TIMEOUT_FOR_CHECKACCESS_IN_SECONDS); - }); - }; - const access = await checkAccessOrTimeout( - keySystem, configs); + const access = + await shaka.util.Functional.promiseWithTimeout( + TIMEOUT_FOR_CHECKACCESS_IN_SECONDS, + navigator.requestMediaKeySystemAccess(keySystem, configs), + ); await processMediaKeySystemAccess(keySystem, access); } catch (error) {} // Ignore errors. }; @@ -1930,23 +1919,11 @@ shaka.media.DrmEngine = class { // 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 decodeOrTimeout = (decodingConfig) => { - return new Promise((resolve, reject) => { - let resolved = false; - navigator.mediaCapabilities.decodingInfo(decodingConfig) - .then((result) => { - resolved = true; - resolve(result); - }); - new shaka.util.Timer(() => { - if (!resolved) { - reject(new Error('Timeout decodingInfo')); - } - }).tickAfter(TIMEOUT_FOR_DECODING_INFO_IN_SECONDS); - }); - }; const decodingInfo = - await decodeOrTimeout(decodingConfig); + await shaka.util.Functional.promiseWithTimeout( + TIMEOUT_FOR_DECODING_INFO_IN_SECONDS, + navigator.mediaCapabilities.decodingInfo(decodingConfig), + ); const access = decodingInfo.keySystemAccess; await processMediaKeySystemAccess(keySystem, access); 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); + })), + ]); + } }; From c8f33ba9c9d199f9a1821c8cc4330201554e45c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mari=C3=B1o?= Date: Wed, 27 Nov 2024 15:18:12 +0100 Subject: [PATCH 3/6] chore: utility function promiseWithTimeout --- lib/media/drm_engine.js | 43 ++++++++++------------------------------- lib/util/functional.js | 27 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index 601c1f1b46..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'); @@ -1875,23 +1876,11 @@ shaka.media.DrmEngine = class { // 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 checkAccessOrTimeout = (keySystem, configs) => { - return new Promise((resolve, reject) => { - let resolved = false; - navigator.requestMediaKeySystemAccess(keySystem, configs) - .then((access) => { - resolved = true; - resolve(access); - }); - new shaka.util.Timer(() => { - if (!resolved) { - reject(new Error('Timeout requestMediaKeySystemAccess')); - } - }).tickAfter(TIMEOUT_FOR_CHECKACCESS_IN_SECONDS); - }); - }; - const access = await checkAccessOrTimeout( - keySystem, configs); + const access = + await shaka.util.Functional.promiseWithTimeout( + TIMEOUT_FOR_CHECKACCESS_IN_SECONDS, + navigator.requestMediaKeySystemAccess(keySystem, configs), + ); await processMediaKeySystemAccess(keySystem, access); } catch (error) {} // Ignore errors. }; @@ -1930,23 +1919,11 @@ shaka.media.DrmEngine = class { // 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 decodeOrTimeout = (decodingConfig) => { - return new Promise((resolve, reject) => { - let resolved = false; - navigator.mediaCapabilities.decodingInfo(decodingConfig) - .then((result) => { - resolved = true; - resolve(result); - }); - new shaka.util.Timer(() => { - if (!resolved) { - reject(new Error('Timeout decodingInfo')); - } - }).tickAfter(TIMEOUT_FOR_DECODING_INFO_IN_SECONDS); - }); - }; const decodingInfo = - await decodeOrTimeout(decodingConfig); + await shaka.util.Functional.promiseWithTimeout( + TIMEOUT_FOR_DECODING_INFO_IN_SECONDS, + navigator.mediaCapabilities.decodingInfo(decodingConfig), + ); const access = decodingInfo.keySystemAccess; await processMediaKeySystemAccess(keySystem, access); 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); + })), + ]); + } }; From 359a2016773ca4f6f0ac6eec605fdc556cc19e28 Mon Sep 17 00:00:00 2001 From: Daniel Marino Date: Thu, 28 Nov 2024 14:43:08 +0100 Subject: [PATCH 4/6] fix: add promiseWithTimeout to streamUtils --- lib/util/stream_utils.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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); })); From a58e7501a5b8763d7fcc06fed15d0ddec8ea88d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mari=C3=B1o?= <1237997+CHaNGeTe@users.noreply.github.com> Date: Thu, 28 Nov 2024 14:44:39 +0100 Subject: [PATCH 5/6] test(Functional): Add tests for Functional.promiseWithTimeout --- test/util/functional_unit.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 test/util/functional_unit.js diff --git a/test/util/functional_unit.js b/test/util/functional_unit.js new file mode 100644 index 0000000000..c99e4cb43a --- /dev/null +++ b/test/util/functional_unit.js @@ -0,0 +1,36 @@ +/*! @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)); + try { + await Functional.promiseWithTimeout(1, asyncProcess); + fail('Expected promise to be rejected'); + } catch (error) { + expect(error).toBe('error'); + } + }); + + it('rejects if asyncProcess takes longer than the timeout', async () => { + const asyncProcess = new Promise((resolve) => setTimeout(() => resolve('success'), 2000)); + try { + await Functional.promiseWithTimeout(1, asyncProcess); + fail('Expected promise to be rejected'); + } catch (error) { + expect(error).toBeUndefined(); + } + }); + }); +}); From 85aefa1850bba80408dfe44591a27798ad5772e0 Mon Sep 17 00:00:00 2001 From: Daniel Marino <1237997+CHaNGeTe@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:23:30 +0100 Subject: [PATCH 6/6] fix: Functional test lint + refactor to expectAsync --- test/util/functional_unit.js | 48 +++++++++++++++++------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/test/util/functional_unit.js b/test/util/functional_unit.js index c99e4cb43a..46d09e93ef 100644 --- a/test/util/functional_unit.js +++ b/test/util/functional_unit.js @@ -5,32 +5,30 @@ */ 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'); - }); + 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)); - try { - await Functional.promiseWithTimeout(1, asyncProcess); - fail('Expected promise to be rejected'); - } catch (error) { - expect(error).toBe('error'); - } - }); + 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)); - try { - await Functional.promiseWithTimeout(1, asyncProcess); - fail('Expected promise to be rejected'); - } catch (error) { - expect(error).toBeUndefined(); - } - }); + 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(); }); + }); });