• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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>&lt;appId&gt;</i>?clientId=<i>&lt;clientId&gt;</i>" and DIAL URNs take the form "dial:<i>&lt;appId&gt;</i>",
569      where <i>&lt;appId&gt;</i> is the <a href="https://developers.google.com/cast/docs/registration" target="_blank">registered application ID</a>
570      and <i>&lt;clientId&gt;</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