Skip to content

Commit

Permalink
fix: Timeout unfulfilled request to decodingInfo and requestMediaKeyS…
Browse files Browse the repository at this point in the history
…ystemAccess (#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
  • Loading branch information
CHaNGeTe authored and avelad committed Dec 4, 2024
1 parent 0121c0c commit cc7c738
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 7 deletions.
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 @@ -1857,9 +1858,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 @@ -1894,13 +1902,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.
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([
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 @@ -708,7 +708,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();
});
});
});

0 comments on commit cc7c738

Please sign in to comment.