Skip to content

Commit

Permalink
feat: Expose the maximum hardware resolution through probeSupport() (#…
Browse files Browse the repository at this point in the history
…6569)

This makes it easier to debug hardware resolution issues through the
support page, which can now show hardware resolution. To show the
support page on Chromecast devices, use chromecast-webdriver-cli.
  • Loading branch information
joeyparrish committed May 9, 2024
1 parent 975235b commit 5da5de2
Show file tree
Hide file tree
Showing 12 changed files with 188 additions and 141 deletions.
1 change: 1 addition & 0 deletions build/check.py
Expand Up @@ -283,6 +283,7 @@ def check_tests(args):
[closure_base_js]))
files.add(os.path.join(base, 'demo', 'common', 'asset.js'))
files.add(os.path.join(base, 'demo', 'common', 'assets.js'))
files.add(os.path.join(base, 'proxy-cast-platform.js'))

localizations = compiler.GenerateLocalizations(None)
localizations.generate(args.force)
Expand Down
2 changes: 2 additions & 0 deletions build/conformance.textproto
Expand Up @@ -73,6 +73,7 @@ requirement {
whitelist_regexp: "demo/"
whitelist_regexp: "test/"
whitelist_regexp: "node_modules/"
whitelist_regexp: "proxy-cast-platform.js"
# This global variable is generated by Google-internal tooling, and should be
# allowed. It will not end up in the compiled code, only at an intermediate
Expand Down Expand Up @@ -286,6 +287,7 @@ requirement: {
"shaka.util.Timer instead."
whitelist_regexp: "demo/"
whitelist_regexp: "test/"
whitelist_regexp: "proxy-cast-platform.js"
}

# Disallow eval, except when testing for modern JS syntax in demo
Expand Down
21 changes: 20 additions & 1 deletion externs/shaka/player.js
Expand Up @@ -455,7 +455,8 @@ shaka.extern.DrmSupportType;
* @typedef {{
* manifest: !Object.<string, boolean>,
* media: !Object.<string, boolean>,
* drm: !Object.<string, ?shaka.extern.DrmSupportType>
* drm: !Object.<string, ?shaka.extern.DrmSupportType>,
* hardwareResolution: shaka.extern.Resolution
* }}
*
* @description
Expand All @@ -471,6 +472,10 @@ shaka.extern.DrmSupportType;
* A map of supported key systems.
* The keys are the key system names. The value is <code>null</code> if it is
* not supported. Key systems not probed will not be in this dictionary.
* @property {shaka.extern.Resolution} hardwareResolution
* The maximum detected hardware resolution, which may have
* height==width==Infinity for devices without a maximum resolution or
* without a way to detect the maximum.
*
* @exportDoc
*/
Expand Down Expand Up @@ -1992,3 +1997,17 @@ shaka.extern.Thumbnail;
* @exportDoc
*/
shaka.extern.Chapter;

/**
* @typedef {{
* width: number,
* height: number
* }}
*
* @property {number} width
* Width in pixels.
* @property {number} height
* Height in pixels.
* @exportDoc
*/
shaka.extern.Resolution;
2 changes: 1 addition & 1 deletion externs/tizen.js
Expand Up @@ -19,7 +19,7 @@ webapis.systeminfo = {};


/**
* @return {{width: number, height: number}}
* @return {shaka.extern.Resolution}
*/
webapis.systeminfo.getMaxVideoResolution = function() {};

Expand Down
3 changes: 3 additions & 0 deletions karma.conf.js
Expand Up @@ -218,6 +218,9 @@ module.exports = (config) => {
// test utilities next, which fill in that namespace
'test/test/util/*.js',

// Proxy cast.__platform__ methods across frames, necessary in testing
'proxy-cast-platform.js',

// bootstrapping for the test suite last; this will load the actual tests
'test/test/boot.js',

Expand Down
4 changes: 2 additions & 2 deletions lib/media/manifest_filterer.js
Expand Up @@ -19,7 +19,7 @@ goog.require('shaka.util.Error');
shaka.media.ManifestFilterer = class {
/**
* @param {?shaka.extern.PlayerConfiguration} config
* @param {{width: number, height: number}} maxHwRes
* @param {shaka.extern.Resolution} maxHwRes
* @param {?shaka.media.DrmEngine} drmEngine
*/
constructor(config, maxHwRes, drmEngine) {
Expand All @@ -28,7 +28,7 @@ shaka.media.ManifestFilterer = class {
/** @private {!shaka.extern.PlayerConfiguration} */
this.config_ = config;

/** @private {{width: number, height: number}} */
/** @private {shaka.extern.Resolution} */
this.maxHwRes_ = maxHwRes;

/** @private {?shaka.media.DrmEngine} drmEngine */
Expand Down
13 changes: 9 additions & 4 deletions lib/player.js
Expand Up @@ -708,7 +708,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
*/
this.lastTextFactory_;

/** @private {{width: number, height: number}} */
/** @private {shaka.extern.Resolution} */
this.maxHwRes_ = {width: Infinity, height: Infinity};

/** @private {!shaka.media.ManifestFilterer} */
Expand Down Expand Up @@ -1015,10 +1015,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
}
const manifest = shaka.media.ManifestParser.probeSupport();
const media = shaka.media.MediaSourceEngine.probeSupport();
const hardwareResolution =
await shaka.util.Platform.detectMaxHardwareResolution();

/** @type {shaka.extern.SupportType} */
const ret = {
manifest: manifest,
media: media,
drm: drm,
manifest,
media,
drm,
hardwareResolution,
};

const plugins = shaka.Player.supportPlugins_;
Expand Down
4 changes: 3 additions & 1 deletion lib/util/platform.js
Expand Up @@ -600,10 +600,12 @@ shaka.util.Platform = class {
/**
* Detect the maximum resolution that the platform's hardware can handle.
*
* @return {!Promise.<{width: number, height: number}>}
* @return {!Promise.<shaka.extern.Resolution>}
*/
static async detectMaxHardwareResolution() {
const Platform = shaka.util.Platform;

/** @type {shaka.extern.Resolution} */
const maxResolution = {
width: Infinity,
height: Infinity,
Expand Down
6 changes: 3 additions & 3 deletions lib/util/stream_utils.js
Expand Up @@ -232,7 +232,7 @@ shaka.util.StreamUtils = class {
*
* @param {!shaka.extern.Manifest} manifest
* @param {shaka.extern.Restrictions} restrictions
* @param {{width: number, height:number}} maxHwResolution
* @param {shaka.extern.Resolution} maxHwResolution
*/
static filterByRestrictions(manifest, restrictions, maxHwResolution) {
manifest.variants = manifest.variants.filter((variant) => {
Expand All @@ -245,7 +245,7 @@ shaka.util.StreamUtils = class {
* @param {shaka.extern.Variant} variant
* @param {shaka.extern.Restrictions} restrictions
* Configured restrictions from the user.
* @param {{width: number, height: number}} maxHwRes
* @param {shaka.extern.Resolution} maxHwRes
* The maximum resolution the hardware can handle.
* This is applied separately from user restrictions because the setting
* should not be easily replaced by the user's configuration.
Expand Down Expand Up @@ -312,7 +312,7 @@ shaka.util.StreamUtils = class {
/**
* @param {!Array.<shaka.extern.Variant>} variants
* @param {shaka.extern.Restrictions} restrictions
* @param {{width: number, height: number}} maxHwRes
* @param {shaka.extern.Resolution} maxHwRes
* @return {boolean} Whether the tracks changed.
*/
static applyRestrictions(variants, restrictions, maxHwRes) {
Expand Down
138 changes: 138 additions & 0 deletions proxy-cast-platform.js
@@ -0,0 +1,138 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* @fileoverview Proxy cast platform methods across frames. Shared between the
* test environment and support.html.
*/

/**
* Patch Cast's cast.__platform__.canDisplayType to allow it to operate across
* frames and origins in our testing environment.
*
* The Cast runtime only exposes cast.__platform__ on the top window, not
* iframes embedded within it. However, both Chromecast WebDriver Server in
* our lab and the test runner Karma use iframes, and there are three different
* origins involved: WebDriver Server's receiver at github.io, Karma's
* top-level, and an inner frame of Karma that is raw HTML as text with no
* origin.
*
* With all of these complexities, the only way to access cast.__platform__ is
* asynchronously via postMessage. This means all callers of
* cast.__platform__.canDisplayType must use `await`, even though the
* underlying method is synchronous. If the caller is running in the top frame
* (as in a real receiver), `await` will do no harm. If the caller is running
* inside our tests in Karma, the `await` is critical to access __platform__
* via this shim.
*/
function proxyCastCanDisplayType() {
if (!navigator.userAgent.includes('CrKey')) {
// Not Chromecast, do nothing.
return;
}

// Create the namespaces if needed.
if (!window.cast) {
window['cast'] = {};
}
if (!cast.__platform__) {
cast['__platform__'] = {};
}

if (cast.__platform__.canDisplayType) {
// Already exists, do nothing.
return;
}

// Create an async shim. Calls to canDisplayType will be translated into
// async messages to the top frame, which will then execute the method and
// post a message back with results (or an error). The resolve/reject
// functions for the shim's returned Promise will be stored temporarily in
// these maps and matched up by request ID.
/** @type {!Map<number, function(?)>} */
const resolveMap = new Map();
/** @type {!Map<number, function(?)>} */
const rejectMap = new Map();
/** @type {number} */
let nextId = 0;

/**
* @typedef {{
* id: number,
* type: string,
* result: *,
* }}
*/
let CastShimMessage;

// Listen for message events for results/errors from the top frame.
window.addEventListener('message', (event) => {
const data = /** @type {CastShimMessage} */(event['data']);
console.log('Received cross-frame message', data);

if (data.type == 'cast.__platform__:result') {
// Find the matching resolve function and resolve the promise for this
// request.
const resolve = resolveMap.get(data.id);
if (resolve) {
resolve(data.result);

// Clear both resolve and reject from the maps for this ID.
resolveMap.delete(data.id);
rejectMap.delete(data.id);
}
} else if (data.type == 'cast.__platform__:error') {
// Find the matching reject function and reject the promise for this
// request.
const reject = rejectMap.get(data.id);
if (reject) {
reject(data.result);

// Clear both resolve and reject from the maps for this ID.
resolveMap.delete(data.id);
rejectMap.delete(data.id);
}
}
});

// Shim canDisplayType to proxy the request up to the top frame.
cast.__platform__.canDisplayType = /** @type {?} */(castCanDisplayTypeShim);

/**
* @param {string} type
* @return {!Promise<boolean>}
*/
function castCanDisplayTypeShim(type) {
return new Promise((resolve, reject) => {
// Craft a message for the top frame to execute this method for us.
const message = {
id: nextId++,
type: 'cast.__platform__',
command: 'canDisplayType',
args: Array.from(arguments),
};

// Store the resolve and reject functions so we can act on results/errors
// later.
resolveMap.set(message.id, resolve);
rejectMap.set(message.id, reject);

// Reject after a 5s timeout. This can happen if we're running under an
// incompatible version of Chromecast WebDriver Server's receiver app.
setTimeout(() => {
reject(new Error('canDisplayType timeout!'));

// Clear both resolve and reject from the maps for this ID.
resolveMap.delete(message.id);
rejectMap.delete(message.id);
}, 5000);

// Send the message to the top frame.
console.log('Sending cross-frame message', message);
window.top.postMessage(message, '*');
});
}
}
3 changes: 3 additions & 0 deletions support.html
Expand Up @@ -39,6 +39,7 @@

</style>
<script src="dist/shaka-player.compiled.js"></script>
<script src="proxy-cast-platform.js"></script>
<script>
function whenLoaded(fn) {
// IE 9 fires DOMContentLoaded, and enters the "interactive"
Expand Down Expand Up @@ -73,7 +74,9 @@
}

function doTest() {
proxyCastCanDisplayType();
shaka.polyfill.installAll();

if (shaka.Player.isBrowserSupported()) {
shaka.Player.probeSupport().then(function(support) {
printSupport(JSON.stringify(support, null, ' '));
Expand Down

0 comments on commit 5da5de2

Please sign in to comment.