Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Timeout unfulfilled request to decodingInfo and requestMediaKeySystemAccess #7682

Merged
merged 7 commits into from
Nov 29, 2024
29 changes: 23 additions & 6 deletions lib/media/drm_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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.
};
Expand Down Expand Up @@ -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),
tykus160 marked this conversation as resolved.
Show resolved Hide resolved
);

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.
Expand Down
27 changes: 27 additions & 0 deletions lib/util/functional.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

goog.provide('shaka.util.Functional');

goog.require('shaka.util.Timer');

/**
* @summary A set of functional utility functions.
*/
Expand Down Expand Up @@ -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.<T>} asyncProcess
* @return {!Promise.<T>}
* @template T
*/
static promiseWithTimeout(seconds, asyncProcess) {
return Promise.race([
tykus160 marked this conversation as resolved.
Show resolved Hide resolved
asyncProcess,
new Promise(((_, reject) => {
new shaka.util.Timer(reject).tickAfter(seconds);
})),
]);
}
};
9 changes: 8 additions & 1 deletion lib/util/stream_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}));
Expand Down
34 changes: 34 additions & 0 deletions test/util/functional_unit.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
Loading