Skip to content

Commit 5930d76

Browse files
authored
feat(chromecast)!: Add v2 receiver app with a redirect mode (#96)
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.
1 parent a6a7998 commit 5930d76

File tree

5 files changed

+79
-74
lines changed

5 files changed

+79
-74
lines changed

backends/chromecast/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ This backend supports the following parameters:
4040
- `receiver-app-id`: The receiver app ID to load, in case you want to host
4141
your own copy. (See also
4242
[receiver-deployment.md](https://github.com/shaka-project/generic-webdriver-server/blob/main/backends/chromecast/receiver-deployment.md))
43+
- `redirect`: Use a redirect strategy instead of an iframe; requires the Cast
44+
SDK to be loaded at the destination URL. Use this for Shaka Player testing.
4345
- `idle-timeout-seconds`: The timeout for idle sessions, after which they will
4446
be closed.
4547
- `connection-timeout-seconds`: The connection timeout for the Chromecast,

backends/chromecast/cast-utils.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,10 @@ function cast(flags, log, mode, url) {
9191
request.appId = flags.receiverAppId;
9292
// This is substituted in place of ${POST_DATA} in the registered
9393
// receiver URL.
94-
request.commandParameters = url;
94+
request.commandParameters = JSON.stringify({
95+
url,
96+
redirect: flags.redirect,
97+
});
9598
break;
9699

97100
case Mode.SERIAL_NUMBER:
@@ -181,7 +184,14 @@ function addChromecastArgs(yargs) {
181184
.option('receiver-app-id', {
182185
description: 'The Chromecast receiver app ID',
183186
type: 'string',
184-
default: 'B602D163',
187+
default: '29993EC8',
188+
})
189+
.option('redirect', {
190+
description:
191+
'Use a redirect strategy instead of an iframe;' +
192+
' requires the Cast SDK to be loaded at the destination URL',
193+
type: 'boolean',
194+
default: false,
185195
})
186196
.option('connection-timeout-seconds', {
187197
description: 'A timeout for the Chromecast connection',

backends/chromecast/how-it-works.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ Receiver apps are really just web pages, and everything on the screen is
1616
implemented in HTML, CSS, and JavaScript.
1717

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

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

5052
## Access Limitations
5153

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

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

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

6168
## Chromecast receiver deployment
6269

backends/chromecast/receiver-deployment.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ property:
8282

8383
```sh
8484
java \
85-
-Dgenericwebdriver.backend.params.receiver-app-id=B602D163 \
85+
-Dgenericwebdriver.backend.params.receiver-app-id=29993EC8 \
8686
# ...
8787
```
8888

backends/chromecast/receiver.html

Lines changed: 53 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717

1818
<html>
1919
<head>
20-
<title>Chromecast WebDriver Receiver</title>
21-
<script src="https://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"></script>
20+
<title>Chromecast WebDriver Receiver v2</title>
2221
<style>
2322

2423
html, body, iframe {
@@ -37,75 +36,62 @@
3736
</style>
3837
<script>
3938

40-
// Expose cast.__platform__ asynchronously through postMessage.
41-
// Cannot be used to proxy synchronous calls, but could be used for debugging
42-
// or with an async shim and `await` on all calls from the client.
43-
window.addEventListener('message', (event) => {
44-
const data = event.data;
45-
console.log('Top window received message:', data);
46-
47-
if (data.type == 'cast.__platform__') {
48-
const platform = cast.__platform__;
49-
const command = platform[data.command];
50-
51-
const args = data.args;
52-
try {
53-
const result = command.apply(platform, args);
54-
55-
const message = {
56-
id: data.id,
57-
type: data.type + ':result',
58-
result: result,
59-
};
60-
61-
console.log('Top window sending result:', message);
62-
event.source.postMessage(message, '*');
63-
} catch (error) {
64-
console.log('Failed:', error);
65-
66-
const message = {
67-
id: data.id,
68-
type: data.type + ':error',
69-
error: error.message,
70-
};
71-
72-
console.log('Top window sending error:', message);
73-
event.source.postMessage(message, '*');
74-
}
75-
}
76-
});
77-
7839
window.addEventListener('DOMContentLoaded', () => {
79-
// Ignore the leading '?'. The rest is the URL.
80-
const frameUrl = (location.search + location.hash).substr(1);
81-
82-
const statusText = 'URL: ' + frameUrl;
83-
84-
const context = cast.framework.CastReceiverContext.getInstance();
85-
context.start({
86-
statusText,
87-
disableIdleTimeout: true,
88-
});
89-
90-
// Some features must be explicitly allowed for an iframe.
91-
// These are needed for media-related testing.
92-
// TODO: Make this list configurable.
93-
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
94-
const allowedFeatures = [
95-
'autoplay',
96-
'encrypted-media',
97-
'fullscreen',
98-
'picture-in-picture',
99-
'sync-xhr',
100-
];
40+
// Arbitrary parameters encoded in JSON in the URL.
41+
let parameters;
42+
try {
43+
// Ignore the leading '?'. The rest is JSON data.
44+
parameters = JSON.parse(decodeURI(location.search.substr(1)));
45+
} catch (error) {
46+
document.body.style.textAlign = 'center';
47+
document.body.style.fontSize = '5vw';
48+
document.body.style.marginTop = '2em';
49+
document.body.innerText = 'FAILED TO DECODE JSON PARAMETERS';
50+
return;
51+
}
10152

102-
window.frame.allow = allowedFeatures.join('; ');
103-
window.frame.src = frameUrl;
53+
if (parameters.redirect) {
54+
// The preferred method is to redirect, but this requires that the
55+
// destination URL runs CAF. If it doesn't, this receiver app will time
56+
// out and fail. This won't work for every URL, but will work for Shaka
57+
// Player testing (v4.9+). This gives a flat environment for testing, with
58+
// direct access to things like EME and cast.__platform__.
59+
location.href = parameters.url;
60+
} else {
61+
// For any other URL, we host the destination URL in an iframe and load CAF
62+
// in this frame.
63+
64+
const script = document.createElement('script');
65+
script.src = 'https://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js';
66+
script.onload = () => {
67+
const statusText = 'URL: ' + parameters.url;
68+
const context = cast.framework.CastReceiverContext.getInstance();
69+
context.start({
70+
statusText,
71+
disableIdleTimeout: true,
72+
});
73+
};
74+
document.head.appendChild(script);
75+
76+
// Some features must be explicitly allowed for an iframe.
77+
// These are needed for media-related testing.
78+
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
79+
const allowedFeatures = [
80+
'autoplay',
81+
'encrypted-media',
82+
'fullscreen',
83+
'picture-in-picture',
84+
'sync-xhr',
85+
];
86+
87+
const iframe = document.createElement('iframe');
88+
iframe.allow = allowedFeatures.join('; ');
89+
iframe.src = parameters.url;
90+
document.body.appendChild(iframe);
91+
}
10492
});
10593

10694
</script>
10795
</head>
108-
<body>
109-
<iframe id="frame"></iframe>
110-
</body>
96+
<body></body>
11197
</html>

0 commit comments

Comments
 (0)