Skip to content

Commit

Permalink
feat(chromecast)!: Add v2 receiver app with a redirect mode (#96)
Browse files Browse the repository at this point in the history
This introduces an optional redirect mode, which changes the top URL instead of using an iframe to host content.  This requires the Cast SDK to be loaded at the destination URL.  This can be used for Shaka Player testing, but not for every URL you might want to see on a Chromecast.

This requires deployment to a new receiver app ID.
  • Loading branch information
joeyparrish authored May 18, 2024
1 parent a6a7998 commit 5930d76
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 74 deletions.
2 changes: 2 additions & 0 deletions backends/chromecast/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ This backend supports the following parameters:
- `receiver-app-id`: The receiver app ID to load, in case you want to host
your own copy. (See also
[receiver-deployment.md](https://github.com/shaka-project/generic-webdriver-server/blob/main/backends/chromecast/receiver-deployment.md))
- `redirect`: Use a redirect strategy instead of an iframe; requires the Cast
SDK to be loaded at the destination URL. Use this for Shaka Player testing.
- `idle-timeout-seconds`: The timeout for idle sessions, after which they will
be closed.
- `connection-timeout-seconds`: The connection timeout for the Chromecast,
Expand Down
14 changes: 12 additions & 2 deletions backends/chromecast/cast-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,10 @@ function cast(flags, log, mode, url) {
request.appId = flags.receiverAppId;
// This is substituted in place of ${POST_DATA} in the registered
// receiver URL.
request.commandParameters = url;
request.commandParameters = JSON.stringify({
url,
redirect: flags.redirect,
});
break;

case Mode.SERIAL_NUMBER:
Expand Down Expand Up @@ -181,7 +184,14 @@ function addChromecastArgs(yargs) {
.option('receiver-app-id', {
description: 'The Chromecast receiver app ID',
type: 'string',
default: 'B602D163',
default: '29993EC8',
})
.option('redirect', {
description:
'Use a redirect strategy instead of an iframe;' +
' requires the Cast SDK to be loaded at the destination URL',
type: 'boolean',
default: false,
})
.option('connection-timeout-seconds', {
description: 'A timeout for the Chromecast connection',
Expand Down
15 changes: 11 additions & 4 deletions backends/chromecast/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ Receiver apps are really just web pages, and everything on the screen is
implemented in HTML, CSS, and JavaScript.

The Chromecast WebDriver server's receiver app hosts an iframe which can be
redirected to any URL at the client's request. This is how we load the
redirected to any URL at the client's request. If the URL is known to load
the Cast SDK, then the receiver app can also redirect to that URL instead,
providing a frameless environment. These are our two methods of loading an
arbitrary URL requested by a test runner like [Karma][] without changing the
receiver app's registered URL.

Expand Down Expand Up @@ -49,14 +51,19 @@ Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Feature_Policy

## Access Limitations

We show an arbitrary URL on the device by embedding it into an iframe in our
Chromecast receiver app. However, sites can prevent iframe-embedding with the
[`X-Frame-Options` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options).
We can show an arbitrary URL on the device by embedding it into an iframe in
our Chromecast receiver app. However, sites can prevent iframe-embedding with
the [`X-Frame-Options` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options).

Though this should not be an issue for a test runner, this may affect other
URLs. Unfortunately, there is no way for the receiver app to detect when this
has happened. See: https://github.com/shaka-project/generic-webdriver-server/issues/8

When possible, such as in Chromecast testing, you should use the `--redirect`
flag and load the Cast SDK in your test environment. This allows you to avoid
the iframe and its limitations, and provides your tests with a flat environment
more representative of your app's production environment.


## Chromecast receiver deployment

Expand Down
2 changes: 1 addition & 1 deletion backends/chromecast/receiver-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ property:

```sh
java \
-Dgenericwebdriver.backend.params.receiver-app-id=B602D163 \
-Dgenericwebdriver.backend.params.receiver-app-id=29993EC8 \
# ...
```

Expand Down
120 changes: 53 additions & 67 deletions backends/chromecast/receiver.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@

<html>
<head>
<title>Chromecast WebDriver Receiver</title>
<script src="https://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"></script>
<title>Chromecast WebDriver Receiver v2</title>
<style>

html, body, iframe {
Expand All @@ -37,75 +36,62 @@
</style>
<script>

// Expose cast.__platform__ asynchronously through postMessage.
// Cannot be used to proxy synchronous calls, but could be used for debugging
// or with an async shim and `await` on all calls from the client.
window.addEventListener('message', (event) => {
const data = event.data;
console.log('Top window received message:', data);

if (data.type == 'cast.__platform__') {
const platform = cast.__platform__;
const command = platform[data.command];

const args = data.args;
try {
const result = command.apply(platform, args);

const message = {
id: data.id,
type: data.type + ':result',
result: result,
};

console.log('Top window sending result:', message);
event.source.postMessage(message, '*');
} catch (error) {
console.log('Failed:', error);

const message = {
id: data.id,
type: data.type + ':error',
error: error.message,
};

console.log('Top window sending error:', message);
event.source.postMessage(message, '*');
}
}
});

window.addEventListener('DOMContentLoaded', () => {
// Ignore the leading '?'. The rest is the URL.
const frameUrl = (location.search + location.hash).substr(1);

const statusText = 'URL: ' + frameUrl;

const context = cast.framework.CastReceiverContext.getInstance();
context.start({
statusText,
disableIdleTimeout: true,
});

// Some features must be explicitly allowed for an iframe.
// These are needed for media-related testing.
// TODO: Make this list configurable.
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
const allowedFeatures = [
'autoplay',
'encrypted-media',
'fullscreen',
'picture-in-picture',
'sync-xhr',
];
// Arbitrary parameters encoded in JSON in the URL.
let parameters;
try {
// Ignore the leading '?'. The rest is JSON data.
parameters = JSON.parse(decodeURI(location.search.substr(1)));
} catch (error) {
document.body.style.textAlign = 'center';
document.body.style.fontSize = '5vw';
document.body.style.marginTop = '2em';
document.body.innerText = 'FAILED TO DECODE JSON PARAMETERS';
return;
}

window.frame.allow = allowedFeatures.join('; ');
window.frame.src = frameUrl;
if (parameters.redirect) {
// The preferred method is to redirect, but this requires that the
// destination URL runs CAF. If it doesn't, this receiver app will time
// out and fail. This won't work for every URL, but will work for Shaka
// Player testing (v4.9+). This gives a flat environment for testing, with
// direct access to things like EME and cast.__platform__.
location.href = parameters.url;
} else {
// For any other URL, we host the destination URL in an iframe and load CAF
// in this frame.

const script = document.createElement('script');
script.src = 'https://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js';
script.onload = () => {
const statusText = 'URL: ' + parameters.url;
const context = cast.framework.CastReceiverContext.getInstance();
context.start({
statusText,
disableIdleTimeout: true,
});
};
document.head.appendChild(script);

// Some features must be explicitly allowed for an iframe.
// These are needed for media-related testing.
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
const allowedFeatures = [
'autoplay',
'encrypted-media',
'fullscreen',
'picture-in-picture',
'sync-xhr',
];

const iframe = document.createElement('iframe');
iframe.allow = allowedFeatures.join('; ');
iframe.src = parameters.url;
document.body.appendChild(iframe);
}
});

</script>
</head>
<body>
<iframe id="frame"></iframe>
</body>
<body></body>
</html>

0 comments on commit 5930d76

Please sign in to comment.