1<html> 2<head> 3<style> 4body { 5 font-family: Verdana, Arial; 6 font-size: 12px; 7} 8 9/* Give the same font styling to form elements. */ 10input, select, textarea, button { 11 font-family: inherit; 12 font-size: inherit; 13} 14 15.content { 16 display: flex; 17 flex-direction: column; 18 width: 100%; 19 height: 100%; 20} 21 22.description { 23 padding-bottom: 5px; 24} 25 26.description .title { 27 font-size: 120%; 28 font-weight: bold; 29} 30 31.route_controls { 32 flex: 0; 33 align-self: center; 34 text-align: right; 35 padding-bottom: 5px; 36} 37 38.route_controls .label { 39 display: inline-block; 40 vertical-align: top; 41 font-weight: bold; 42} 43 44.route_controls .control { 45 width: 500px; 46} 47 48.messages { 49 flex: 1; 50 min-height: 100px; 51 border: 1px solid gray; 52 overflow: auto; 53} 54 55.messages .message { 56 padding: 3px; 57 border-bottom: 1px solid #cccbca; 58} 59 60.messages .message .timestamp { 61 font-size: 90%; 62 font-style: italic; 63} 64 65.messages .status { 66 background-color: #d6d6d6; /* light gray */ 67} 68 69.messages .sent { 70 background-color: #c5e8fc; /* light blue */ 71} 72 73.messages .recv { 74 background-color: #fcf4e3; /* light yellow */ 75} 76 77.message_controls { 78 flex: 0; 79 text-align: right; 80 padding-top: 5px; 81} 82 83.message_controls textarea { 84 width: 100%; 85 height: 10em; 86} 87</style> 88<script language="JavaScript"> 89// Application state. 90var demoMode = false; 91var currentSubscriptionId = null; 92var currentRouteId = null; 93 94// List of currently supported source protocols. 95var allowedSourceProtocols = ['cast', 'dial']; 96 97// Values from cef_media_route_connection_state_t. 98var CEF_MRCS_UNKNOWN = 0; 99var CEF_MRCS_CONNECTING = 1; 100var CEF_MRCS_CONNECTED = 2; 101var CEF_MRCS_CLOSED = 3; 102var CEF_MRCS_TERMINATED = 4; 103 104function getStateLabel(state) { 105 switch (state) { 106 case CEF_MRCS_CONNECTING: return "CONNECTING"; 107 case CEF_MRCS_CONNECTED: return "CONNECTED"; 108 case CEF_MRCS_CLOSED: return "CLOSED"; 109 case CEF_MRCS_TERMINATED: return "TERMINATED"; 110 default: break; 111 } 112 return "UNKNOWN"; 113} 114 115// Values from cef_media_sink_icon_type_t. 116var CEF_MSIT_CAST = 0; 117var CEF_MSIT_CAST_AUDIO_GROUP = 1; 118var CEF_MSIT_CAST_AUDIO = 2; 119var CEF_MSIT_MEETING = 3; 120var CEF_MSIT_HANGOUT = 4; 121var CEF_MSIT_EDUCATION = 5; 122var CEF_MSIT_WIRED_DISPLAY = 6; 123var CEF_MSIT_GENERIC = 7; 124 125function getIconTypeLabel(type) { 126 switch (type) { 127 case CEF_MSIT_CAST: return "CAST"; 128 case CEF_MSIT_CAST_AUDIO_GROUP: return "CAST_AUDIO_GROUP"; 129 case CEF_MSIT_CAST_AUDIO: return "CAST_AUDIO"; 130 case CEF_MSIT_MEETING: return "MEETING"; 131 case CEF_MSIT_HANGOUT: return "HANGOUT"; 132 case CEF_MSIT_EDUCATION: return "EDUCATION"; 133 case CEF_MSIT_WIRED_DISPLAY: return "WIRED_DISPLAY"; 134 case CEF_MSIT_GENERIC: return "GENERIC"; 135 default: break; 136 } 137 return "UNKNOWN"; 138} 139 140 141/// 142// Manage show/hide of default text for form elements. 143/// 144 145// Default messages that are shown until the user focuses on the input field. 146var defaultSourceText = 'Enter URN here and click "Create Route"'; 147var defaultMessageText = 'Enter message contents here and click "Send Message"'; 148 149function getDefaultText(control) { 150 if (control === 'source') 151 return defaultSourceText; 152 if (control === 'message') 153 return defaultMessageText; 154 return null; 155} 156 157function hideDefaultText(control) { 158 var element = document.getElementById(control); 159 var defaultText = getDefaultText(control); 160 if (element.value === defaultText) 161 element.value = ''; 162} 163 164function showDefaultText(control) { 165 var element = document.getElementById(control); 166 var defaultText = getDefaultText(control); 167 if (element.value === '') 168 element.value = defaultText; 169} 170 171function initDefaultText() { 172 showDefaultText('source'); 173 showDefaultText('message'); 174} 175 176 177/// 178// Retrieve current form values. Return null if validation fails. 179/// 180 181function getCurrentSource() { 182 var sourceInput = document.getElementById('source'); 183 var value = sourceInput.value; 184 if (value === defaultSourceText || value.length === 0 || value.indexOf(':') < 0) { 185 return null; 186 } 187 188 // Validate the URN value. 189 try { 190 var url = new URL(value); 191 if ((url.hostname.length === 0 && url.pathname.length === 0) || 192 !allowedSourceProtocols.includes(url.protocol.slice(0, -1))) { 193 return null; 194 } 195 } catch (e) { 196 return null; 197 } 198 199 return value; 200} 201 202function getCurrentSink() { 203 var sinksSelect = document.getElementById('sink'); 204 if (sinksSelect.options.length === 0) 205 return null; 206 return sinksSelect.value; 207} 208 209function getCurrentMessage() { 210 var messageInput = document.getElementById('message'); 211 if (messageInput.value === defaultMessageText || messageInput.value.length === 0) 212 return null; 213 return messageInput.value; 214} 215 216 217/// 218// Set disabled state of form elements. 219/// 220 221function updateControls() { 222 document.getElementById('source').disabled = hasRoute(); 223 document.getElementById('sink').disabled = hasRoute(); 224 document.getElementById('create_route').disabled = 225 hasRoute() || getCurrentSource() === null || getCurrentSink() === null; 226 document.getElementById('terminate_route').disabled = !hasRoute(); 227 document.getElementById('message').disabled = !hasRoute(); 228 document.getElementById('send_message').disabled = !hasRoute() || getCurrentMessage() === null; 229} 230 231 232/// 233// Manage the media sinks list. 234/// 235 236/* 237Expected format for |sinks| is: 238 [ 239 { 240 name: string, 241 type: string ('cast' or 'dial'), 242 id: string, 243 desc: string, 244 icon: int 245 }, ... 246 ] 247*/ 248function updateSinks(sinks) { 249 var sinksSelect = document.getElementById('sink'); 250 251 // Currently selected value. 252 var selectedValue = sinksSelect.options.length === 0 ? null : sinksSelect.value; 253 254 // Build a list of old (existing) values. 255 var oldValues = []; 256 for (var i = 0; i < sinksSelect.options.length; ++i) { 257 oldValues.push(sinksSelect.options[i].value); 258 } 259 260 // Build a list of new (possibly new or existing) values. 261 var newValues = []; 262 for(var i = 0; i < sinks.length; i++) { 263 newValues.push(sinks[i].id); 264 } 265 266 // Remove old values that no longer exist. 267 for (var i = sinksSelect.options.length - 1; i >= 0; --i) { 268 if (!newValues.includes(sinksSelect.options[i].value)) { 269 sinksSelect.remove(i); 270 } 271 } 272 273 // Add new values that don't already exist. 274 for(var i = 0; i < sinks.length; i++) { 275 var sink = sinks[i]; 276 if (oldValues.includes(sink.id)) 277 continue; 278 var opt = document.createElement('option'); 279 opt.innerHTML = sink.name + ' (' + sink.model_name + ', ' + sink.type + ', ' + 280 getIconTypeLabel(sink.icon) + ', ' + sink.ip_address + ':' + sink.port + ')'; 281 opt.value = sink.id; 282 sinksSelect.appendChild(opt); 283 } 284 285 if (sinksSelect.options.length === 0) { 286 selectedValue = null; 287 } else if (!newValues.includes(selectedValue)) { 288 // The previously selected value no longer exists. 289 // Select the first value in the new list. 290 selectedValue = sinksSelect.options[0].value; 291 sinksSelect.value = selectedValue; 292 } 293 294 updateControls(); 295 296 return selectedValue; 297} 298 299 300/// 301// Manage the current media route. 302/// 303 304function hasRoute() { 305 return currentRouteId !== null; 306} 307 308function createRoute() { 309 console.assert(!hasRoute()); 310 var source = getCurrentSource(); 311 console.assert(source !== null); 312 var sink = getCurrentSink(); 313 console.assert(sink !== null); 314 315 if (demoMode) { 316 onRouteCreated('demo-route-id'); 317 return; 318 } 319 320 sendCefQuery( 321 {name: 'createRoute', source_urn: source, sink_id: sink}, 322 (message) => onRouteCreated(JSON.parse(message).route_id) 323 ); 324} 325 326function onRouteCreated(route_id) { 327 currentRouteId = route_id; 328 showStatusMessage('Route ' + route_id + '\ncreated'); 329 updateControls(); 330} 331 332function terminateRoute() { 333 console.assert(hasRoute()); 334 var source = getCurrentSource(); 335 console.assert(source !== null); 336 var sink = getCurrentSink(); 337 console.assert(sink !== null); 338 339 if (demoMode) { 340 onRouteTerminated(); 341 return; 342 } 343 344 sendCefQuery( 345 {name: 'terminateRoute', route_id: currentRouteId}, 346 (unused) => {} 347 ); 348} 349 350function onRouteTerminated() { 351 showStatusMessage('Route ' + currentRouteId + '\nterminated'); 352 currentRouteId = null; 353 updateControls(); 354} 355 356 357/// 358// Manage messages. 359/// 360 361function sendMessage() { 362 console.assert(hasRoute()); 363 var message = getCurrentMessage(); 364 console.assert(message !== null); 365 366 if (demoMode) { 367 showSentMessage(message); 368 setTimeout(function(){ if (hasRoute()) { recvMessage('Demo ACK for: ' + message); } }, 1000); 369 return; 370 } 371 372 sendCefQuery( 373 {name: 'sendMessage', route_id: currentRouteId, message: message}, 374 (unused) => showSentMessage(message) 375 ); 376} 377 378function recvMessage(message) { 379 console.assert(hasRoute()); 380 console.assert(message !== undefined && message !== null && message.length > 0); 381 showRecvMessage(message); 382} 383 384function showStatusMessage(message) { 385 showMessage('status', message); 386} 387 388function showSentMessage(message) { 389 showMessage('sent', message); 390} 391 392function showRecvMessage(message) { 393 showMessage('recv', message); 394} 395 396function showMessage(type, message) { 397 if (!['status', 'sent', 'recv'].includes(type)) { 398 console.warn('Invalid message type: ' + type); 399 return; 400 } 401 402 if (message[0] === '{') { 403 try { 404 // Pretty print JSON strings. 405 message = JSON.stringify(JSON.parse(message), null, 2); 406 } catch(e) {} 407 } 408 409 var messagesDiv = document.getElementById('messages'); 410 411 var newDiv = document.createElement("div"); 412 newDiv.innerHTML = 413 '<span class="timestamp">' + (new Date().toLocaleString()) + 414 ' (' + type.toUpperCase() + ')</span><br/>'; 415 // Escape any HTML tags or entities in |message|. 416 var pre = document.createElement('pre'); 417 pre.appendChild(document.createTextNode(message)); 418 newDiv.appendChild(pre); 419 newDiv.className = 'message ' + type; 420 421 messagesDiv.appendChild(newDiv); 422 423 // Always scroll to bottom. 424 messagesDiv.scrollTop = messagesDiv.scrollHeight; 425} 426 427 428/// 429// Manage communication with native code in media_router_test.cc. 430/// 431 432function onCefError(code, message) { 433 showStatusMessage('ERROR: ' + message + ' (' + code + ')'); 434} 435 436function sendCefQuery(payload, onSuccess, onFailure=onCefError, persistent=false) { 437 // Results in a call to the OnQuery method in media_router_test.cc 438 return window.cefQuery({ 439 request: JSON.stringify(payload), 440 onSuccess: onSuccess, 441 onFailure: onFailure, 442 persistent: persistent 443 }); 444} 445 446/* 447Expected format for |message| is: 448 { 449 name: string, 450 payload: dictionary 451 } 452*/ 453function onCefSubscriptionMessage(message) { 454 if (message.name === 'onSinks') { 455 // List of sinks. 456 updateSinks(message.payload.sinks_list); 457 } else if (message.name === 'onRouteStateChanged') { 458 // Route status changed. 459 if (message.payload.route_id === currentRouteId) { 460 var connection_state = message.payload.connection_state; 461 showStatusMessage('Route ' + currentRouteId + 462 '\nconnection state ' + getStateLabel(connection_state) + 463 ' (' + connection_state + ')'); 464 if ([CEF_MRCS_CLOSED, CEF_MRCS_TERMINATED].includes(connection_state)) { 465 onRouteTerminated(); 466 } 467 } 468 } else if (message.name === 'onRouteMessageReceived') { 469 // Route message received. 470 if (message.payload.route_id === currentRouteId) { 471 recvMessage(message.payload.message); 472 } 473 } 474} 475 476// Subscribe to ongoing message notifications from the native code. 477function startCefSubscription() { 478 currentSubscriptionId = sendCefQuery( 479 {name: 'subscribe'}, 480 (message) => onCefSubscriptionMessage(JSON.parse(message)), 481 (code, message) => { 482 onCefError(code, message); 483 currentSubscriptionId = null; 484 }, 485 true 486 ); 487} 488 489function stopCefSubscription() { 490 if (currentSubscriptionId !== null) { 491 // Results in a call to the OnQueryCanceled method in media_router_test.cc 492 window.cefQueryCancel(currentSubscriptionId); 493 } 494} 495 496 497/// 498// Example app load/unload. 499/// 500 501function initDemoMode() { 502 demoMode = true; 503 504 var sinks = [ 505 { 506 name: 'Sink 1', 507 type: 'cast', 508 id: 'sink1', 509 desc: 'My cast device', 510 icon: CEF_MSIT_CAST 511 }, 512 { 513 name: 'Sink 2', 514 type: 'dial', 515 id: 'sink2', 516 desc: 'My dial device', 517 icon: CEF_MSIT_GENERIC 518 } 519 ]; 520 updateSinks(sinks); 521 522 showStatusMessage('Running in Demo mode.'); 523 showSentMessage('Demo sent message.'); 524 showRecvMessage('Demo recv message.'); 525} 526 527function onLoad() { 528 initDefaultText(); 529 530 if (window.cefQuery === undefined) { 531 // Initialize demo mode when running outside of CEF. 532 // This supports development and testing of the HTML/JS behavior outside 533 // of a cefclient build. 534 initDemoMode(); 535 return; 536 } 537 538 startCefSubscription() 539} 540 541function onUnload() { 542 if (demoMode) 543 return; 544 545 if (hasRoute()) 546 terminateRoute(); 547 stopCefSubscription(); 548} 549</script> 550<title>Media Router Example</title> 551</head> 552<body bgcolor="white" onLoad="onLoad()" onUnload="onUnload()"> 553<div class="content"> 554 <div class="description"> 555 <span class="title">Media Router Example</span> 556 <p> 557 <b>Overview:</b> 558 Chromium supports communication with devices on the local network via the 559 <a href="https://blog.oakbits.com/google-cast-protocol-overview.html" target="_blank">Cast</a> and 560 <a href="http://www.dial-multiscreen.org/" target="_blank">DIAL</a> protocols. 561 CEF exposes this functionality via the CefMediaRouter interface which is demonstrated by this test. 562 Test code is implemented in resources/media_router.html and browser/media_router_test.cc. 563 </p> 564 <p> 565 <b>Usage:</b> 566 Devices available on your local network will be discovered automatically and populated in the "Sink" list. 567 Enter a URN for "Source", select an available device from the "Sink" list, and click the "Create Route" button. 568 Cast URNs take the form "cast:<i><appId></i>?clientId=<i><clientId></i>" and DIAL URNs take the form "dial:<i><appId></i>", 569 where <i><appId></i> is the <a href="https://developers.google.com/cast/docs/registration" target="_blank">registered application ID</a> 570 and <i><clientId></i> is an arbitrary numeric identifier. 571 Status information and messages will be displayed in the center of the screen. 572 After creating a route you can send messages to the receiver app using the textarea at the bottom of the screen. 573 Messages are usually in JSON format with a example of Cast communication to be found 574 <a href="https://bitbucket.org/chromiumembedded/cef/issues/2900/add-mediarouter-support-for-cast-receiver#comment-56680326" target="_blank">here</a>. 575 </p> 576 </div> 577 <div class="route_controls"> 578 <span class="label">Source:</span> 579 <input type="text" id="source" class="control" onInput="updateControls()" onFocus="hideDefaultText('source')" onBlur="showDefaultText('source')"/> 580 <br/> 581 <span class="label">Sink:</span> 582 <select id="sink" size="3" class="control"></select> 583 <br/> 584 <input type="button" id="create_route" onclick="createRoute()" value="Create Route" disabled/> 585 <input type="button" id="terminate_route" onclick="terminateRoute()" value="Terminate Route" disabled/> 586 </div> 587 <div id="messages" class="messages"> 588 </div> 589 <div class="message_controls"> 590 <textarea id="message" onInput="updateControls()" onFocus="hideDefaultText('message')" onBlur="showDefaultText('message')" disabled></textarea> 591 <br/><input type="button" id="send_message" onclick="sendMessage()" value="Send Message" disabled/> 592 </div> 593</div> 594</body> 595</html> 596