Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(chromecast)!: Add v2 receiver app with a redirect mode #96

Merged
merged 1 commit into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>