-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(cast): Use cast platform APIs in MediaCapabilties polyfill (#4727)
See #4726 for more context. This allows Cast devices to properly filter stream variants with a resolution surpassing that of the device's capabilities. We place the fix in the `MediaCapabilities` polyfill since it's intended to be the right way to check for anything related to platform support. HDR support checks will require `eotf=smpte2048`, as indicated in #2813 (comment). Specifically, a `{hev|hvc}1.2` profile is only an *indication* of an HDR transfer function, but *may* be a non-HDR 10-bit color stream. In Cast, the platform can distinguish between the two by explicitly providing the transfer function; it uses `smpte2048` (`"PQ"`) because this is the "basis of HDR video formats..." (https://en.wikipedia.org/wiki/Perceptual_quantizer).
- Loading branch information
1 parent
aef7f52
commit fb1f0dc
Showing
2 changed files
with
320 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,261 @@ | ||
/*! @license | ||
* Shaka Player | ||
* Copyright 2016 Google LLC | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
describe('MediaCapabilities', () => { | ||
const Util = shaka.test.Util; | ||
const originalCast = window['cast']; | ||
const originalVendor = navigator.vendor; | ||
const originalUserAgent = navigator.userAgent; | ||
const originalRequestMediaKeySystemAccess = | ||
navigator.requestMediaKeySystemAccess; | ||
const originalMediaCapabilities = navigator.mediaCapabilities; | ||
|
||
/** @type {MediaDecodingConfiguration} */ | ||
let mockDecodingConfig; | ||
/** @type {!jasmine.Spy} */ | ||
let mockCanDisplayType; | ||
|
||
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_ = {}; | ||
|
||
mockCanDisplayType = jasmine.createSpy('canDisplayType'); | ||
mockCanDisplayType.and.returnValue(false); | ||
}); | ||
|
||
afterEach(() => { | ||
window['cast'] = originalCast; | ||
}); | ||
|
||
afterAll(() => { | ||
window['cast'] = originalCast; | ||
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('throws when the cast namespace is not available', async () => { | ||
// Temporarily remove window.cast to trigger error. It's restored after | ||
// every test. | ||
delete window['cast']; | ||
|
||
const isChromecastSpy = | ||
spyOn(shaka['util']['Platform'], | ||
'isChromecast').and.returnValue(true); | ||
const expected = Util.jasmineError(new shaka.util.Error( | ||
shaka.util.Error.Severity.CRITICAL, | ||
shaka.util.Error.Category.CAST, | ||
shaka.util.Error.Code.CAST_API_UNAVAILABLE)); | ||
const isTypeSupportedSpy = | ||
spyOn(window['MediaSource'], 'isTypeSupported').and.returnValue(true); | ||
|
||
shaka.polyfill.MediaCapabilities.install(); | ||
await expectAsync( | ||
navigator.mediaCapabilities.decodingInfo(mockDecodingConfig)) | ||
.toBeRejectedWith(expected); | ||
|
||
expect(isTypeSupportedSpy).not.toHaveBeenCalled(); | ||
// 1 (during install()) + 1 (for video config check). | ||
expect(isChromecastSpy).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
it('falls back to isTypeSupported() when canDisplayType() missing', | ||
async () => { | ||
// We only set the cast namespace, but not the canDisplayType() API. | ||
window['cast'] = {}; | ||
const isChromecastSpy = | ||
spyOn(shaka['util']['Platform'], | ||
'isChromecast').and.returnValue(true); | ||
const isTypeSupportedSpy = | ||
spyOn(window['MediaSource'], 'isTypeSupported') | ||
.and.returnValue(true); | ||
|
||
shaka.polyfill.MediaCapabilities.install(); | ||
await navigator.mediaCapabilities.decodingInfo(mockDecodingConfig); | ||
|
||
expect(mockCanDisplayType).not.toHaveBeenCalled(); | ||
// 1 (during install()) + 1 (for video config check). | ||
expect(isChromecastSpy).toHaveBeenCalledTimes(2); | ||
// 1 (fallback in canCastDisplayType()) + | ||
// 1 (mockDecodingConfig.audio). | ||
expect(isTypeSupportedSpy).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
it('should use cast.__platform__.canDisplayType for "supported" field ' + | ||
'when platform is Cast', async () => { | ||
// We're using quotes to access window.cast because the compiler | ||
// knows about lots of Cast-specific APIs we aren't mocking. We | ||
// don't need this mock strictly type-checked. | ||
window['cast'] = { | ||
__platform__: {canDisplayType: mockCanDisplayType}, | ||
}; | ||
const isChromecastSpy = | ||
spyOn(shaka['util']['Platform'], | ||
'isChromecast').and.returnValue(true); | ||
const isTypeSupportedSpy = | ||
spyOn(window['MediaSource'], 'isTypeSupported').and.returnValue(true); | ||
|
||
// Tests an HDR stream's extended MIME type is correctly provided. | ||
mockDecodingConfig.video.transferFunction = 'pq'; | ||
mockDecodingConfig.video.contentType = | ||
'video/mp4; codecs="hev1.2.4.L153.B0"'; | ||
// Round to a whole number since we can't rely on number => string | ||
// conversion precision on all devices. | ||
mockDecodingConfig.video.framerate = 24; | ||
mockCanDisplayType.and.callFake((type) => { | ||
expect(type).toBe( | ||
'video/mp4; ' + | ||
'codecs="hev1.2.4.L153.B0"; ' + | ||
'width=512; ' + | ||
'height=288; ' + | ||
'framerate=24; ' + | ||
'eotf=smpte2084'); | ||
return true; | ||
}); | ||
|
||
shaka.polyfill.MediaCapabilities.install(); | ||
await navigator.mediaCapabilities.decodingInfo(mockDecodingConfig); | ||
|
||
// 1 (during install()) + 1 (for video config check). | ||
expect(isChromecastSpy).toHaveBeenCalledTimes(2); | ||
// 1 (mockDecodingConfig.audio). | ||
expect(isTypeSupportedSpy).toHaveBeenCalledTimes(1); | ||
// Called once in canCastDisplayType. | ||
expect(mockCanDisplayType).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
}); |