cef/tests/cefclient/resources/media_router.html
2023-03-13 13:50:48 -04:00

593 lines
15 KiB
HTML

<html>
<head>
<style>
body {
font-family: Verdana, Arial;
font-size: 12px;
}
/* Give the same font styling to form elements. */
input, select, textarea, button {
font-family: inherit;
font-size: inherit;
}
.content {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.description {
padding-bottom: 5px;
}
.description .title {
font-size: 120%;
font-weight: bold;
}
.route_controls {
flex: 0;
align-self: center;
text-align: right;
padding-bottom: 5px;
}
.route_controls .label {
display: inline-block;
vertical-align: top;
font-weight: bold;
}
.route_controls .control {
width: 500px;
}
.messages {
flex: 1;
min-height: 100px;
border: 1px solid gray;
overflow: auto;
}
.messages .message {
padding: 3px;
border-bottom: 1px solid #cccbca;
}
.messages .message .timestamp {
font-size: 90%;
font-style: italic;
}
.messages .status {
background-color: #d6d6d6; /* light gray */
}
.messages .sent {
background-color: #c5e8fc; /* light blue */
}
.messages .recv {
background-color: #fcf4e3; /* light yellow */
}
.message_controls {
flex: 0;
text-align: right;
padding-top: 5px;
}
.message_controls textarea {
width: 100%;
height: 10em;
}
</style>
<script language="JavaScript">
// Application state.
var demoMode = false;
var currentSubscriptionId = null;
var currentRouteId = null;
// List of currently supported source protocols.
var allowedSourceProtocols = ['cast', 'dial'];
// Values from cef_media_route_connection_state_t.
var CEF_MRCS_UNKNOWN = 0;
var CEF_MRCS_CONNECTING = 1;
var CEF_MRCS_CONNECTED = 2;
var CEF_MRCS_CLOSED = 3;
var CEF_MRCS_TERMINATED = 4;
function getStateLabel(state) {
switch (state) {
case CEF_MRCS_CONNECTING: return "CONNECTING";
case CEF_MRCS_CONNECTED: return "CONNECTED";
case CEF_MRCS_CLOSED: return "CLOSED";
case CEF_MRCS_TERMINATED: return "TERMINATED";
default: break;
}
return "UNKNOWN";
}
// Values from cef_media_sink_icon_type_t.
var CEF_MSIT_CAST = 0;
var CEF_MSIT_CAST_AUDIO_GROUP = 1;
var CEF_MSIT_CAST_AUDIO = 2;
var CEF_MSIT_MEETING = 3;
var CEF_MSIT_HANGOUT = 4;
var CEF_MSIT_EDUCATION = 5;
var CEF_MSIT_WIRED_DISPLAY = 6;
var CEF_MSIT_GENERIC = 7;
function getIconTypeLabel(type) {
switch (type) {
case CEF_MSIT_CAST: return "CAST";
case CEF_MSIT_CAST_AUDIO_GROUP: return "CAST_AUDIO_GROUP";
case CEF_MSIT_CAST_AUDIO: return "CAST_AUDIO";
case CEF_MSIT_MEETING: return "MEETING";
case CEF_MSIT_HANGOUT: return "HANGOUT";
case CEF_MSIT_EDUCATION: return "EDUCATION";
case CEF_MSIT_WIRED_DISPLAY: return "WIRED_DISPLAY";
case CEF_MSIT_GENERIC: return "GENERIC";
default: break;
}
return "UNKNOWN";
}
///
// Manage show/hide of default text for form elements.
///
// Default messages that are shown until the user focuses on the input field.
var defaultSourceText = 'Enter URN here and click "Create Route"';
var defaultMessageText = 'Enter message contents here and click "Send Message"';
function getDefaultText(control) {
if (control === 'source')
return defaultSourceText;
if (control === 'message')
return defaultMessageText;
return null;
}
function hideDefaultText(control) {
var element = document.getElementById(control);
var defaultText = getDefaultText(control);
if (element.value === defaultText)
element.value = '';
}
function showDefaultText(control) {
var element = document.getElementById(control);
var defaultText = getDefaultText(control);
if (element.value === '')
element.value = defaultText;
}
function initDefaultText() {
showDefaultText('source');
showDefaultText('message');
}
///
// Retrieve current form values. Return null if validation fails.
///
function getCurrentSource() {
var sourceInput = document.getElementById('source');
var value = sourceInput.value;
if (value === defaultSourceText || value.length === 0 || value.indexOf(':') < 0) {
return null;
}
// Validate the URN value.
try {
var url = new URL(value);
if ((url.hostname.length === 0 && url.pathname.length === 0) ||
!allowedSourceProtocols.includes(url.protocol.slice(0, -1))) {
return null;
}
} catch (e) {
return null;
}
return value;
}
function getCurrentSink() {
var sinksSelect = document.getElementById('sink');
if (sinksSelect.options.length === 0)
return null;
return sinksSelect.value;
}
function getCurrentMessage() {
var messageInput = document.getElementById('message');
if (messageInput.value === defaultMessageText || messageInput.value.length === 0)
return null;
return messageInput.value;
}
///
// Set disabled state of form elements.
///
function updateControls() {
document.getElementById('source').disabled = hasRoute();
document.getElementById('sink').disabled = hasRoute();
document.getElementById('create_route').disabled =
hasRoute() || getCurrentSource() === null || getCurrentSink() === null;
document.getElementById('terminate_route').disabled = !hasRoute();
document.getElementById('message').disabled = !hasRoute();
document.getElementById('send_message').disabled = !hasRoute() || getCurrentMessage() === null;
}
///
// Manage the media sinks list.
///
/*
Expected format for |sinks| is:
[
{
name: string,
type: string ('cast' or 'dial'),
id: string,
icon: int
}, ...
]
*/
function updateSinks(sinks) {
var sinksSelect = document.getElementById('sink');
// Currently selected value.
var selectedValue = sinksSelect.options.length === 0 ? null : sinksSelect.value;
// Build a list of old (existing) values.
var oldValues = [];
for (var i = 0; i < sinksSelect.options.length; ++i) {
oldValues.push(sinksSelect.options[i].value);
}
// Build a list of new (possibly new or existing) values.
var newValues = [];
for(var i = 0; i < sinks.length; i++) {
newValues.push(sinks[i].id);
}
// Remove old values that no longer exist.
for (var i = sinksSelect.options.length - 1; i >= 0; --i) {
if (!newValues.includes(sinksSelect.options[i].value)) {
sinksSelect.remove(i);
}
}
// Add new values that don't already exist.
for(var i = 0; i < sinks.length; i++) {
var sink = sinks[i];
if (oldValues.includes(sink.id))
continue;
var opt = document.createElement('option');
opt.innerHTML = sink.name + ' (' + sink.model_name + ', ' + sink.type + ', ' +
getIconTypeLabel(sink.icon) + ', ' + sink.ip_address + ':' + sink.port + ')';
opt.value = sink.id;
sinksSelect.appendChild(opt);
}
if (sinksSelect.options.length === 0) {
selectedValue = null;
} else if (!newValues.includes(selectedValue)) {
// The previously selected value no longer exists.
// Select the first value in the new list.
selectedValue = sinksSelect.options[0].value;
sinksSelect.value = selectedValue;
}
updateControls();
return selectedValue;
}
///
// Manage the current media route.
///
function hasRoute() {
return currentRouteId !== null;
}
function createRoute() {
console.assert(!hasRoute());
var source = getCurrentSource();
console.assert(source !== null);
var sink = getCurrentSink();
console.assert(sink !== null);
if (demoMode) {
onRouteCreated('demo-route-id');
return;
}
sendCefQuery(
{name: 'createRoute', source_urn: source, sink_id: sink},
(message) => onRouteCreated(JSON.parse(message).route_id)
);
}
function onRouteCreated(route_id) {
currentRouteId = route_id;
showStatusMessage('Route ' + route_id + '\ncreated');
updateControls();
}
function terminateRoute() {
console.assert(hasRoute());
var source = getCurrentSource();
console.assert(source !== null);
var sink = getCurrentSink();
console.assert(sink !== null);
if (demoMode) {
onRouteTerminated();
return;
}
sendCefQuery(
{name: 'terminateRoute', route_id: currentRouteId},
(unused) => {}
);
}
function onRouteTerminated() {
showStatusMessage('Route ' + currentRouteId + '\nterminated');
currentRouteId = null;
updateControls();
}
///
// Manage messages.
///
function sendMessage() {
console.assert(hasRoute());
var message = getCurrentMessage();
console.assert(message !== null);
if (demoMode) {
showSentMessage(message);
setTimeout(function(){ if (hasRoute()) { recvMessage('Demo ACK for: ' + message); } }, 1000);
return;
}
sendCefQuery(
{name: 'sendMessage', route_id: currentRouteId, message: message},
(unused) => showSentMessage(message)
);
}
function recvMessage(message) {
console.assert(hasRoute());
console.assert(message !== undefined && message !== null && message.length > 0);
showRecvMessage(message);
}
function showStatusMessage(message) {
showMessage('status', message);
}
function showSentMessage(message) {
showMessage('sent', message);
}
function showRecvMessage(message) {
showMessage('recv', message);
}
function showMessage(type, message) {
if (!['status', 'sent', 'recv'].includes(type)) {
console.warn('Invalid message type: ' + type);
return;
}
if (message[0] === '{') {
try {
// Pretty print JSON strings.
message = JSON.stringify(JSON.parse(message), null, 2);
} catch(e) {}
}
var messagesDiv = document.getElementById('messages');
var newDiv = document.createElement("div");
newDiv.innerHTML =
'<span class="timestamp">' + (new Date().toLocaleString()) +
' (' + type.toUpperCase() + ')</span><br/>';
// Escape any HTML tags or entities in |message|.
var pre = document.createElement('pre');
pre.appendChild(document.createTextNode(message));
newDiv.appendChild(pre);
newDiv.className = 'message ' + type;
messagesDiv.appendChild(newDiv);
// Always scroll to bottom.
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
///
// Manage communication with native code in media_router_test.cc.
///
function onCefError(code, message) {
showStatusMessage('ERROR: ' + message + ' (' + code + ')');
}
function sendCefQuery(payload, onSuccess, onFailure=onCefError, persistent=false) {
// Results in a call to the OnQuery method in media_router_test.cc
return window.cefQuery({
request: JSON.stringify(payload),
onSuccess: onSuccess,
onFailure: onFailure,
persistent: persistent
});
}
/*
Expected format for |message| is:
{
name: string,
payload: dictionary
}
*/
function onCefSubscriptionMessage(message) {
if (message.name === 'onSinks') {
// List of sinks.
updateSinks(message.payload.sinks_list);
} else if (message.name === 'onRouteStateChanged') {
// Route status changed.
if (message.payload.route_id === currentRouteId) {
var connection_state = message.payload.connection_state;
showStatusMessage('Route ' + currentRouteId +
'\nconnection state ' + getStateLabel(connection_state) +
' (' + connection_state + ')');
if ([CEF_MRCS_CLOSED, CEF_MRCS_TERMINATED].includes(connection_state)) {
onRouteTerminated();
}
}
} else if (message.name === 'onRouteMessageReceived') {
// Route message received.
if (message.payload.route_id === currentRouteId) {
recvMessage(message.payload.message);
}
}
}
// Subscribe to ongoing message notifications from the native code.
function startCefSubscription() {
currentSubscriptionId = sendCefQuery(
{name: 'subscribe'},
(message) => onCefSubscriptionMessage(JSON.parse(message)),
(code, message) => {
onCefError(code, message);
currentSubscriptionId = null;
},
true
);
}
function stopCefSubscription() {
if (currentSubscriptionId !== null) {
// Results in a call to the OnQueryCanceled method in media_router_test.cc
window.cefQueryCancel(currentSubscriptionId);
}
}
///
// Example app load/unload.
///
function initDemoMode() {
demoMode = true;
var sinks = [
{
name: 'Sink 1',
type: 'cast',
id: 'sink1',
icon: CEF_MSIT_CAST
},
{
name: 'Sink 2',
type: 'dial',
id: 'sink2',
icon: CEF_MSIT_GENERIC
}
];
updateSinks(sinks);
showStatusMessage('Running in Demo mode.');
showSentMessage('Demo sent message.');
showRecvMessage('Demo recv message.');
}
function onLoad() {
initDefaultText();
if (window.cefQuery === undefined) {
// Initialize demo mode when running outside of CEF.
// This supports development and testing of the HTML/JS behavior outside
// of a cefclient build.
initDemoMode();
return;
}
startCefSubscription()
}
function onUnload() {
if (demoMode)
return;
if (hasRoute())
terminateRoute();
stopCefSubscription();
}
</script>
<title>Media Router Example</title>
</head>
<body bgcolor="white" onLoad="onLoad()" onUnload="onUnload()">
<div class="content">
<div class="description">
<span class="title">Media Router Example</span>
<p>
<b>Overview:</b>
Chromium supports communication with devices on the local network via the
<a href="https://blog.oakbits.com/google-cast-protocol-overview.html" target="_blank">Cast</a> and
<a href="http://www.dial-multiscreen.org/" target="_blank">DIAL</a> protocols.
CEF exposes this functionality via the CefMediaRouter interface which is demonstrated by this test.
Test code is implemented in resources/media_router.html and browser/media_router_test.cc.
</p>
<p>
<b>Usage:</b>
Devices available on your local network will be discovered automatically and populated in the "Sink" list.
Enter a URN for "Source", select an available device from the "Sink" list, and click the "Create Route" button.
Cast URNs take the form "cast:<i>&lt;appId&gt;</i>?clientId=<i>&lt;clientId&gt;</i>" and DIAL URNs take the form "dial:<i>&lt;appId&gt;</i>",
where <i>&lt;appId&gt;</i> is the <a href="https://developers.google.com/cast/docs/registration" target="_blank">registered application ID</a>
and <i>&lt;clientId&gt;</i> is an arbitrary numeric identifier.
Status information and messages will be displayed in the center of the screen.
After creating a route you can send messages to the receiver app using the textarea at the bottom of the screen.
Messages are usually in JSON format with a example of Cast communication to be found
<a href="https://github.com/chromiumembedded/cef/issues/2900#issuecomment-1465022620" target="_blank">here</a>.
</p>
</div>
<div class="route_controls">
<span class="label">Source:</span>
<input type="text" id="source" class="control" onInput="updateControls()" onFocus="hideDefaultText('source')" onBlur="showDefaultText('source')"/>
<br/>
<span class="label">Sink:</span>
<select id="sink" size="3" class="control"></select>
<br/>
<input type="button" id="create_route" onclick="createRoute()" value="Create Route" disabled/>
<input type="button" id="terminate_route" onclick="terminateRoute()" value="Terminate Route" disabled/>
</div>
<div id="messages" class="messages">
</div>
<div class="message_controls">
<textarea id="message" onInput="updateControls()" onFocus="hideDefaultText('message')" onBlur="showDefaultText('message')" disabled></textarea>
<br/><input type="button" id="send_message" onclick="sendMessage()" value="Send Message" disabled/>
</div>
</div>
</body>
</html>