Skip to content

Commit

Permalink
feat: Caching and other efficiency improvements for mcap polyfill (sh…
Browse files Browse the repository at this point in the history
…aka-project#4708)

This PR caches the result of `requestMediaKeySystemAccess` saving time
on subsequent calls.

It also makes the calls to `decodingInfo` synchronous. The reason for
this, is the result of testing on multiple devices that the behaviour of
`requestMediaKeySystemAccess` appears to be synchronous, making this
synchronous can save over a second on older TVs.

Closes shaka-project#4574
  • Loading branch information
dave-nicholas committed Nov 18, 2022
1 parent 877e954 commit 884c4ca
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 6 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -27,6 +27,7 @@ Charter Communications Inc <*@charter.com>
Code It <*@code-it.fr>
Damien Deis <developer.deis@gmail.com>
Dany L'Hébreux <danylhebreux@gmail.com>
Dave Nicholas <davenicholasuk@gmail.com>
Dl Dador <dldador@gmail.com>
Edgeware AB <*@edgeware.tv>
Enson Choy <enson.choy@harmonicinc.com>
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS
Expand Up @@ -43,6 +43,7 @@ Chris Fillmore <fillmore.chris@gmail.com>
Costel Madalin Grecu <madalin.grecu@adswizz.com>
Damien Deis <developer.deis@gmail.com>
Dany L'Hébreux <danylhebreux@gmail.com>
Dave Nicholas <davenicholasuk@gmail.com>
Dl Dador <dldador@gmail.com>
Donato Borrello <donato@jwplayer.com>
Duc Pham <duc.pham@edgeware.tv>
Expand Down
44 changes: 41 additions & 3 deletions lib/polyfill/media_capabilities.js
Expand Up @@ -185,10 +185,25 @@ shaka.polyfill.MediaCapabilities = class {
mediaKeySystemConfig.videoCapabilities = videoCapabilities;
}

const cacheKey = shaka.polyfill.MediaCapabilities
.generateKeySystemCacheKey_(
mediaDecodingConfig.video.contentType,
mediaDecodingConfig.audio.contentType,
mediaDecodingConfig.keySystemConfiguration.keySystem);

let keySystemAccess;
try {
keySystemAccess = await navigator.requestMediaKeySystemAccess(
mediaCapkeySystemConfig.keySystem, [mediaKeySystemConfig]);
if (cacheKey in shaka.polyfill.MediaCapabilities
.memoizedMediaKeySystemAccessRequests_) {
keySystemAccess = shaka.polyfill.MediaCapabilities
.memoizedMediaKeySystemAccessRequests_[cacheKey];
} else {
keySystemAccess = await navigator.requestMediaKeySystemAccess(
mediaCapkeySystemConfig.keySystem, [mediaKeySystemConfig]);
shaka.polyfill.MediaCapabilities
.memoizedMediaKeySystemAccessRequests_[cacheKey] =
keySystemAccess;
}
} catch (e) {
shaka.log.info('navigator.requestMediaKeySystemAccess failed.');
}
Expand All @@ -201,18 +216,41 @@ shaka.polyfill.MediaCapabilities = class {

return res;
}

/**
* A method for generating a key for the MediaKeySystemAccessRequests cache.
*
* @param {!string} videoCodec
* @param {!string} audioCodec
* @param {!string} keySystem
* @return {!string}
* @private
*/
static generateKeySystemCacheKey_(videoCodec, audioCodec, keySystem) {
return `${videoCodec}#${audioCodec}#${keySystem}`;
}
};

/**
* A copy of the MediaCapabilities instance, to prevent Safari from
* garbage-collecting the polyfilled method on it. We make it public and export
* garbage-collecting the polyfilled method on it. We make it public and export
* it to ensure that it is not stripped out by the compiler.
*
* @type {MediaCapabilities}
* @export
*/
shaka.polyfill.MediaCapabilities.originalMcap = null;

/**
* A cache that stores the MediaKeySystemAccess result of calling
* `navigator.requestMediaKeySystemAccess` by a key combination of
* video/audio codec and key system string.
*
* @type {(Object<(!string), (!MediaKeySystemAccess)>)}
* @export
*/
shaka.polyfill.MediaCapabilities.memoizedMediaKeySystemAccessRequests_ = {};

// Install at a lower priority than MediaSource polyfill, so that we have
// MediaSource available first.
shaka.polyfill.register(shaka.polyfill.MediaCapabilities.install, -1);
8 changes: 5 additions & 3 deletions lib/util/stream_utils.js
Expand Up @@ -515,7 +515,6 @@ shaka.util.StreamUtils = class {

const mediaCapabilities = navigator.mediaCapabilities;

const operations = [];
const getVariantDecodingInfos = (async (variant, decodingConfig) => {
try {
const result = await mediaCapabilities.decodingInfo(decodingConfig);
Expand All @@ -531,11 +530,14 @@ shaka.util.StreamUtils = class {
const decodingConfigs = shaka.util.StreamUtils.getDecodingConfigs_(
variant, usePersistentLicenses, srcEquals);

// The reason we are performing this await in a loop rather than
// batching into a `promise.all` is performance related.
// https://github.com/shaka-project/shaka-player/pull/4708#discussion_r1022581178
for (const config of decodingConfigs) {
operations.push(getVariantDecodingInfos(variant, config));
// eslint-disable-next-line no-await-in-loop
await getVariantDecodingInfos(variant, config);
}
}
await Promise.all(operations);
}


Expand Down
176 changes: 176 additions & 0 deletions test/polyfill/media_capabilities_unit.js
@@ -0,0 +1,176 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

describe('MediaCapabilities', () => {
const originalVendor = navigator.vendor;
const originalUserAgent = navigator.userAgent;
const originalRequestMediaKeySystemAccess =
navigator.requestMediaKeySystemAccess;
const originalMediaCapabilities = navigator.mediaCapabilities;
/** @type {MediaDecodingConfiguration} */
let mockDecodingConfig;

beforeAll(() => {
Object.defineProperty(window['navigator'],
'userAgent', {
value: 'unknown', configurable: true,
writable: true,
});
Object.defineProperty(window['navigator'],
'vendor', {
value: 'unknown', configurable: true,
writable: true,
});
Object.defineProperty(window['navigator'],
'requestMediaKeySystemAccess', {
value: 'unknown', configurable: true,
writable: true,
});
Object.defineProperty(window['navigator'],
'mediaCapabilities', {
value: undefined, configurable: true,
writable: true,
});
});

beforeEach(() => {
mockDecodingConfig = {
audio: {
bitrate: 100891,
channels: 2,
contentType: 'audio/mp4; codecs="mp4a.40.2"',
samplerate: 48000,
spatialRendering: false,
},
keySystemConfiguration: {
audio: {robustness: 'SW_SECURE_CRYPTO'},
distinctiveIdentifier: 'optional',
initDataType: 'cenc',
keySystem: 'com.widevine.alpha',
persistentState: 'optional',
sessionTypes: ['temporary'],
video: {robustness: 'SW_SECURE_CRYPTO'},
},
type: 'media-source',
video: {
bitrate: 349265,
contentType: 'video/mp4; codecs="avc1.4D4015"',
framerate: 23.976023976023978,
height: 288,
width: 512,
},
};
shaka.polyfill.MediaCapabilities.memoizedMediaKeySystemAccessRequests_ = {};
});

afterAll(() => {
Object.defineProperty(window['navigator'],
'userAgent', {value: originalUserAgent});
Object.defineProperty(window['navigator'],
'vendor', {value: originalVendor});
Object.defineProperty(window['navigator'],
'requestMediaKeySystemAccess',
{value: originalRequestMediaKeySystemAccess});
Object.defineProperty(window['navigator'],
'mediaCapabilities', {value: originalMediaCapabilities});
});

describe('install', () => {
it('should define decoding info method', () => {
shaka.polyfill.MediaCapabilities.install();

expect(navigator.mediaCapabilities.decodingInfo).toBeDefined();
});
});

describe('decodingInfo', () => {
it('should check codec support when MediaDecodingConfiguration.type ' +
'is "media-source"', () => {
const isTypeSupportedSpy =
spyOn(window['MediaSource'], 'isTypeSupported').and.returnValue(true);
shaka.polyfill.MediaCapabilities.install();
navigator.mediaCapabilities.decodingInfo(mockDecodingConfig);

expect(isTypeSupportedSpy).toHaveBeenCalledTimes(2);
expect(isTypeSupportedSpy).toHaveBeenCalledWith(
mockDecodingConfig.video.contentType,
);
expect(isTypeSupportedSpy).toHaveBeenCalledWith(
mockDecodingConfig.audio.contentType,
);
});

it('should check codec support when MediaDecodingConfiguration.type ' +
'is "file"', () => {
const supportsMediaTypeSpy =
spyOn(shaka['util']['Platform'],
'supportsMediaType').and.returnValue(true);
mockDecodingConfig.type = 'file';
shaka.polyfill.MediaCapabilities.install();
navigator.mediaCapabilities.decodingInfo(mockDecodingConfig);

expect(supportsMediaTypeSpy).toHaveBeenCalledTimes(2);
expect(supportsMediaTypeSpy).toHaveBeenCalledWith(
mockDecodingConfig.video.contentType,
);
expect(supportsMediaTypeSpy).toHaveBeenCalledWith(
mockDecodingConfig.audio.contentType,
);
});

it('should check MediaKeySystem when keySystemConfiguration is present',
async () => {
const mockResult = {mockKeySystemAccess: 'mockKeySystemAccess'};
spyOn(window['MediaSource'], 'isTypeSupported').and.returnValue(true);
const requestKeySystemAccessSpy =
spyOn(window['navigator'],
'requestMediaKeySystemAccess').and.returnValue(mockResult);

shaka.polyfill.MediaCapabilities.install();
const result = await navigator.mediaCapabilities
.decodingInfo(mockDecodingConfig);

expect(requestKeySystemAccessSpy).toHaveBeenCalledWith(
'com.widevine.alpha',
[{
audioCapabilities: [
{
robustness: 'SW_SECURE_CRYPTO',
contentType: 'audio/mp4; codecs="mp4a.40.2"',
},
],
distinctiveIdentifier: 'optional',
initDataTypes: ['cenc'],
persistentState: 'optional',
sessionTypes: ['temporary'],
videoCapabilities: [{
robustness: 'SW_SECURE_CRYPTO',
contentType: 'video/mp4; codecs="avc1.4D4015"',
}],
}],
);
expect(result.keySystemAccess).toEqual(mockResult);
});

it('should read previously requested codec/key system'+
'combinations from cache', async () => {
const mockResult = {mockKeySystemAccess: 'mockKeySystemAccess'};
spyOn(window['MediaSource'], 'isTypeSupported').and.returnValue(true);
const requestKeySystemAccessSpy =
spyOn(window['navigator'],
'requestMediaKeySystemAccess').and.returnValue(mockResult);

shaka.polyfill.MediaCapabilities.install();
await navigator.mediaCapabilities
.decodingInfo(mockDecodingConfig);
await navigator.mediaCapabilities
.decodingInfo(mockDecodingConfig);

expect(requestKeySystemAccessSpy)
.toHaveBeenCalledTimes(1);
});
});
});

0 comments on commit 884c4ca

Please sign in to comment.