Add support for media device discovery and messaging (fixes issue #2900)

Chromium supports communication with media devices on the local network via
the Cast and DIAL protocols. This takes two primary forms:

1. Messaging, where strings representing state information are passed between
   the client and a dedicated receiver app on the media device. The receiver
   app communicates directly with an app-specific backend service to retrieve
   and possibly control media playback.
2. Tab/desktop mirroring, where the media contents are streamed directly from
   the browser to a generic streaming app on the media device and playback is
   controlled by the browser.

This change adds support for device discovery and messaging (but not
mirroring) with functionality exposed via the new CefMediaRouter interface.

To test: Navigate to http://tests/media_router in cefclient and follow the
on-screen instructions.
This commit is contained in:
Marshall Greenblatt
2020-03-19 11:34:15 -04:00
parent fecd582035
commit 03fd5b15da
69 changed files with 5402 additions and 33 deletions

View File

@ -0,0 +1,491 @@
// Copyright (c) 2020 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.
#include "tests/cefclient/browser/media_router_test.h"
#include <string>
#include <vector>
#include "include/base/cef_logging.h"
#include "include/cef_media_router.h"
#include "include/cef_parser.h"
#include "tests/cefclient/browser/test_runner.h"
namespace client {
namespace media_router_test {
namespace {
const char kTestUrlPath[] = "/media_router";
// Application-specific error codes.
const int kMessageFormatError = 1;
const int kRequestFailedError = 2;
// Message strings.
const char kNameKey[] = "name";
const char kNameValueSubscribe[] = "subscribe";
const char kNameValueCreateRoute[] = "createRoute";
const char kNameValueTerminateRoute[] = "terminateRoute";
const char kNameValueSendMessage[] = "sendMessage";
const char kSourceKey[] = "source_urn";
const char kSinkKey[] = "sink_id";
const char kRouteKey[] = "route_id";
const char kMessageKey[] = "message";
const char kSuccessKey[] = "success";
const char kPayloadKey[] = "payload";
// Convert a dictionary value to a JSON string.
CefString GetJSON(CefRefPtr<CefDictionaryValue> dictionary) {
CefRefPtr<CefValue> value = CefValue::Create();
value->SetDictionary(dictionary);
return CefWriteJSON(value, JSON_WRITER_DEFAULT);
}
typedef CefMessageRouterBrowserSide::Callback CallbackType;
void SendSuccess(CefRefPtr<CallbackType> callback,
CefRefPtr<CefDictionaryValue> result) {
callback->Success(GetJSON(result));
}
void SendFailure(CefRefPtr<CallbackType> callback,
int error_code,
const std::string& error_message) {
callback->Failure(error_code, error_message);
}
// Callback for CefMediaRouter::CreateRoute.
class MediaRouteCreateCallback : public CefMediaRouteCreateCallback {
public:
explicit MediaRouteCreateCallback(CefRefPtr<CallbackType> create_callback)
: create_callback_(create_callback) {}
// CefMediaRouteCreateCallback method:
void OnMediaRouteCreateFinished(RouteCreateResult result,
const CefString& error,
CefRefPtr<CefMediaRoute> route) OVERRIDE {
CEF_REQUIRE_UI_THREAD();
if (result == CEF_MRCR_OK) {
CefRefPtr<CefDictionaryValue> result = CefDictionaryValue::Create();
result->SetString(kRouteKey, route->GetId());
SendSuccess(create_callback_, result);
} else {
SendFailure(create_callback_, kRequestFailedError + result, error);
}
create_callback_ = NULL;
}
private:
CefRefPtr<CallbackType> create_callback_;
IMPLEMENT_REFCOUNTING(MediaRouteCreateCallback);
DISALLOW_COPY_AND_ASSIGN(MediaRouteCreateCallback);
};
// Observes MediaRouter events. Only accessed on the UI thread.
class MediaObserver : public CefMediaObserver {
public:
typedef std::vector<CefRefPtr<CefMediaRoute>> MediaRouteVector;
typedef std::vector<CefRefPtr<CefMediaSink>> MediaSinkVector;
MediaObserver(CefRefPtr<CefMediaRouter> media_router,
CefRefPtr<CallbackType> subscription_callback)
: media_router_(media_router),
subscription_callback_(subscription_callback) {}
bool CreateRoute(const std::string& source_urn,
const std::string& sink_id,
CefRefPtr<CallbackType> callback,
std::string& error) {
CefRefPtr<CefMediaSource> source = GetSource(source_urn);
if (!source) {
error = "Invalid source: " + source_urn;
return false;
}
CefRefPtr<CefMediaSink> sink = GetSink(sink_id);
if (!sink) {
error = "Invalid sink: " + sink_id;
return false;
}
media_router_->CreateRoute(source, sink,
new MediaRouteCreateCallback(callback));
return true;
}
bool TerminateRoute(const std::string& route_id, std::string& error) {
CefRefPtr<CefMediaRoute> route = GetRoute(route_id);
if (!route) {
error = "Invalid route: " + route_id;
return false;
}
route->Terminate();
return true;
}
bool SendRouteMessage(const std::string& route_id,
const std::string& message,
std::string& error) {
CefRefPtr<CefMediaRoute> route = GetRoute(route_id);
if (!route) {
error = "Invalid route: " + route_id;
return false;
}
route->SendRouteMessage(message.c_str(), message.size());
return true;
}
protected:
// CefMediaObserver methods:
void OnSinks(const MediaSinkVector& sinks) OVERRIDE {
CEF_REQUIRE_UI_THREAD();
sink_map_.clear();
CefRefPtr<CefDictionaryValue> payload = CefDictionaryValue::Create();
CefRefPtr<CefListValue> sinks_list = CefListValue::Create();
sinks_list->SetSize(sinks.size());
MediaSinkVector::const_iterator it = sinks.begin();
for (size_t idx = 0; it != sinks.end(); ++it, ++idx) {
CefRefPtr<CefMediaSink> sink = *it;
const std::string& sink_id = sink->GetId();
sink_map_.insert(std::make_pair(sink_id, sink));
CefRefPtr<CefDictionaryValue> sink_dict = CefDictionaryValue::Create();
sink_dict->SetString("id", sink_id);
sink_dict->SetString("name", sink->GetName());
sink_dict->SetString("desc", sink->GetDescription());
sink_dict->SetString(
"type", sink->IsCastSink() ? "cast"
: sink->IsDialSink() ? "dial" : "unknown");
sinks_list->SetDictionary(idx, sink_dict);
}
payload->SetList("sinks_list", sinks_list);
SendResponse("onSinks", payload);
}
void OnRoutes(const MediaRouteVector& routes) OVERRIDE {
CEF_REQUIRE_UI_THREAD();
route_map_.clear();
CefRefPtr<CefDictionaryValue> payload = CefDictionaryValue::Create();
CefRefPtr<CefListValue> routes_list = CefListValue::Create();
routes_list->SetSize(routes.size());
MediaRouteVector::const_iterator it = routes.begin();
for (size_t idx = 0; it != routes.end(); ++it, ++idx) {
CefRefPtr<CefMediaRoute> route = *it;
const std::string& route_id = route->GetId();
route_map_.insert(std::make_pair(route_id, route));
CefRefPtr<CefDictionaryValue> route_dict = CefDictionaryValue::Create();
route_dict->SetString("id", route_id);
route_dict->SetString(kSourceKey, route->GetSource()->GetId());
route_dict->SetString(kSinkKey, route->GetSink()->GetId());
routes_list->SetDictionary(idx, route_dict);
}
payload->SetList("routes_list", routes_list);
SendResponse("onRoutes", payload);
}
void OnRouteStateChanged(CefRefPtr<CefMediaRoute> route,
ConnectionState state) OVERRIDE {
CEF_REQUIRE_UI_THREAD();
CefRefPtr<CefDictionaryValue> payload = CefDictionaryValue::Create();
payload->SetString(kRouteKey, route->GetId());
payload->SetInt("connection_state", state);
SendResponse("onRouteStateChanged", payload);
}
void OnRouteMessageReceived(CefRefPtr<CefMediaRoute> route,
const void* message,
size_t message_size) OVERRIDE {
CEF_REQUIRE_UI_THREAD();
std::string message_str(static_cast<const char*>(message), message_size);
CefRefPtr<CefDictionaryValue> payload = CefDictionaryValue::Create();
payload->SetString(kRouteKey, route->GetId());
payload->SetString(kMessageKey, message_str);
SendResponse("onRouteMessageReceived", payload);
}
private:
CefRefPtr<CefMediaSource> GetSource(const std::string& source_urn) {
CefRefPtr<CefMediaSource> source = media_router_->GetSource(source_urn);
if (!source || !source->IsValid())
return NULL;
return source;
}
CefRefPtr<CefMediaSink> GetSink(const std::string& sink_id) {
SinkMap::const_iterator it = sink_map_.find(sink_id);
if (it != sink_map_.end())
return it->second;
return NULL;
}
CefRefPtr<CefMediaRoute> GetRoute(const std::string& route_id) {
RouteMap::const_iterator it = route_map_.find(route_id);
if (it != route_map_.end())
return it->second;
return NULL;
}
void SendResponse(const std::string& name,
CefRefPtr<CefDictionaryValue> payload) {
CefRefPtr<CefDictionaryValue> result = CefDictionaryValue::Create();
result->SetString(kNameKey, name);
result->SetDictionary(kPayloadKey, payload);
SendSuccess(subscription_callback_, result);
}
CefRefPtr<CefMediaRouter> media_router_;
CefRefPtr<CallbackType> subscription_callback_;
typedef std::map<std::string, CefRefPtr<CefMediaSink>> SinkMap;
SinkMap sink_map_;
typedef std::map<std::string, CefRefPtr<CefMediaRoute>> RouteMap;
RouteMap route_map_;
IMPLEMENT_REFCOUNTING(MediaObserver);
DISALLOW_COPY_AND_ASSIGN(MediaObserver);
};
// Handle messages in the browser process. Only accessed on the UI thread.
class Handler : public CefMessageRouterBrowserSide::Handler {
public:
typedef std::vector<std::string> NameVector;
Handler() { CEF_REQUIRE_UI_THREAD(); }
virtual ~Handler() {
SubscriptionStateMap::iterator it = subscription_state_map_.begin();
for (; it != subscription_state_map_.end(); ++it) {
delete it->second;
}
}
// Called due to cefQuery execution in media_router.html.
bool OnQuery(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int64 query_id,
const CefString& request,
bool persistent,
CefRefPtr<Callback> callback) OVERRIDE {
CEF_REQUIRE_UI_THREAD();
// Only handle messages from the test URL.
const std::string& url = frame->GetURL();
if (!test_runner::IsTestURL(url, kTestUrlPath))
return false;
// Parse |request| as a JSON dictionary.
CefRefPtr<CefDictionaryValue> request_dict = ParseJSON(request);
if (!request_dict) {
SendFailure(callback, kMessageFormatError, "Incorrect message format");
return true;
}
// Verify the "name" key.
if (!VerifyKey(request_dict, kNameKey, VTYPE_STRING, callback))
return true;
const std::string& message_name = request_dict->GetString(kNameKey);
if (message_name == kNameValueSubscribe) {
// Subscribe to notifications from the media router.
if (!persistent) {
SendFailure(callback, kMessageFormatError,
"Subscriptions must be persistent");
return true;
}
if (!CreateSubscription(browser, query_id, callback)) {
SendFailure(callback, kRequestFailedError,
"Browser is already subscribed");
}
return true;
}
// All other messages require a current subscription.
CefRefPtr<MediaObserver> media_observer =
GetMediaObserver(browser->GetIdentifier());
if (!media_observer) {
SendFailure(callback, kRequestFailedError,
"Browser is not currently subscribed");
}
if (message_name == kNameValueCreateRoute) {
// Create a new route.
// Verify the "source_urn" key.
if (!VerifyKey(request_dict, kSourceKey, VTYPE_STRING, callback))
return true;
// Verify the "sink_id" key.
if (!VerifyKey(request_dict, kSinkKey, VTYPE_STRING, callback))
return true;
const std::string& source_urn = request_dict->GetString(kSourceKey);
const std::string& sink_id = request_dict->GetString(kSinkKey);
// |callback| will be executed once the route is created.
std::string error;
if (!media_observer->CreateRoute(source_urn, sink_id, callback, error)) {
SendFailure(callback, kRequestFailedError, error);
}
return true;
} else if (message_name == kNameValueTerminateRoute) {
// Terminate an existing route.
// Verify the "route" key.
if (!VerifyKey(request_dict, kRouteKey, VTYPE_STRING, callback))
return true;
const std::string& route_id = request_dict->GetString(kRouteKey);
std::string error;
if (!media_observer->TerminateRoute(route_id, error)) {
SendFailure(callback, kRequestFailedError, error);
} else {
SendSuccessACK(callback);
}
return true;
} else if (message_name == kNameValueSendMessage) {
// Send a route message.
// Verify the "route_id" key.
if (!VerifyKey(request_dict, kRouteKey, VTYPE_STRING, callback))
return true;
// Verify the "message" key.
if (!VerifyKey(request_dict, kMessageKey, VTYPE_STRING, callback))
return true;
const std::string& route_id = request_dict->GetString(kRouteKey);
const std::string& message = request_dict->GetString(kMessageKey);
std::string error;
if (!media_observer->SendRouteMessage(route_id, message, error)) {
SendFailure(callback, kRequestFailedError, error);
} else {
SendSuccessACK(callback);
}
return true;
}
return false;
}
void OnQueryCanceled(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int64 query_id) OVERRIDE {
CEF_REQUIRE_UI_THREAD();
RemoveSubscription(browser->GetIdentifier(), query_id);
}
private:
static void SendSuccessACK(CefRefPtr<Callback> callback) {
CefRefPtr<CefDictionaryValue> result = CefDictionaryValue::Create();
result->SetBool(kSuccessKey, true);
SendSuccess(callback, result);
}
// Convert a JSON string to a dictionary value.
static CefRefPtr<CefDictionaryValue> ParseJSON(const CefString& string) {
CefRefPtr<CefValue> value = CefParseJSON(string, JSON_PARSER_RFC);
if (value.get() && value->GetType() == VTYPE_DICTIONARY)
return value->GetDictionary();
return nullptr;
}
// Verify that |key| exists in |dictionary| and has type |value_type|. Fails
// |callback| and returns false on failure.
static bool VerifyKey(CefRefPtr<CefDictionaryValue> dictionary,
const char* key,
cef_value_type_t value_type,
CefRefPtr<Callback> callback) {
if (!dictionary->HasKey(key) || dictionary->GetType(key) != value_type) {
SendFailure(
callback, kMessageFormatError,
"Missing or incorrectly formatted message key: " + std::string(key));
return false;
}
return true;
}
// Subscription state associated with a single browser.
struct SubscriptionState {
int64 query_id;
CefRefPtr<MediaObserver> observer;
CefRefPtr<CefRegistration> registration;
};
bool CreateSubscription(CefRefPtr<CefBrowser> browser,
int64 query_id,
CefRefPtr<Callback> callback) {
const int browser_id = browser->GetIdentifier();
if (subscription_state_map_.find(browser_id) !=
subscription_state_map_.end()) {
// An subscription already exists for this browser.
return false;
}
CefRefPtr<CefMediaRouter> media_router =
browser->GetHost()->GetRequestContext()->GetMediaRouter();
SubscriptionState* state = new SubscriptionState();
state->query_id = query_id;
state->observer = new MediaObserver(media_router, callback);
state->registration = media_router->AddObserver(state->observer);
subscription_state_map_.insert(std::make_pair(browser_id, state));
// Trigger sink and route callbacks.
media_router->NotifyCurrentSinks();
media_router->NotifyCurrentRoutes();
return true;
}
void RemoveSubscription(int browser_id, int64 query_id) {
SubscriptionStateMap::iterator it =
subscription_state_map_.find(browser_id);
if (it != subscription_state_map_.end() &&
it->second->query_id == query_id) {
delete it->second;
subscription_state_map_.erase(it);
}
}
CefRefPtr<MediaObserver> GetMediaObserver(int browser_id) {
SubscriptionStateMap::const_iterator it =
subscription_state_map_.find(browser_id);
if (it != subscription_state_map_.end()) {
return it->second->observer;
}
return NULL;
}
// Map of browser ID to SubscriptionState object.
typedef std::map<int, SubscriptionState*> SubscriptionStateMap;
SubscriptionStateMap subscription_state_map_;
DISALLOW_COPY_AND_ASSIGN(Handler);
};
} // namespace
void CreateMessageHandlers(test_runner::MessageHandlerSet& handlers) {
handlers.insert(new Handler());
}
} // namespace media_router_test
} // namespace client

View File

@ -0,0 +1,20 @@
// Copyright (c) 2020 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.
#ifndef CEF_TESTS_CEFCLIENT_BROWSER_MEDIA_ROUTER_TEST_H_
#define CEF_TESTS_CEFCLIENT_BROWSER_MEDIA_ROUTER_TEST_H_
#pragma once
#include "tests/cefclient/browser/test_runner.h"
namespace client {
namespace media_router_test {
// Create message handlers. Called from test_runner.cc.
void CreateMessageHandlers(test_runner::MessageHandlerSet& handlers);
} // namespace media_router_test
} // namespace client
#endif // CEF_TESTS_CEFCLIENT_BROWSER_MEDIA_ROUTER_TEST_H_

View File

@ -50,24 +50,25 @@
#define IDS_DRM_HTML 1003
#define IDS_LOCALSTORAGE_HTML 1004
#define IDS_LOGO_PNG 1005
#define IDS_MENU_ICON_1X_PNG 1006
#define IDS_MENU_ICON_2X_PNG 1007
#define IDS_OSRTEST_HTML 1008
#define IDS_OTHER_TESTS_HTML 1009
#define IDS_PDF_HTML 1010
#define IDS_PDF_PDF 1011
#define IDS_PERFORMANCE_HTML 1012
#define IDS_PERFORMANCE2_HTML 1013
#define IDS_PREFERENCES_HTML 1014
#define IDS_RESPONSE_FILTER_HTML 1015
#define IDS_SERVER_HTML 1016
#define IDS_TRANSPARENCY_HTML 1017
#define IDS_URLREQUEST_HTML 1018
#define IDS_WEBSOCKET_HTML 1019
#define IDS_WINDOW_HTML 1020
#define IDS_WINDOW_ICON_1X_PNG 1021
#define IDS_WINDOW_ICON_2X_PNG 1022
#define IDS_XMLHTTPREQUEST_HTML 1023
#define IDS_MEDIA_ROUTER_HTML 1006
#define IDS_MENU_ICON_1X_PNG 1007
#define IDS_MENU_ICON_2X_PNG 1008
#define IDS_OSRTEST_HTML 1009
#define IDS_OTHER_TESTS_HTML 1010
#define IDS_PDF_HTML 1011
#define IDS_PDF_PDF 1012
#define IDS_PERFORMANCE_HTML 1013
#define IDS_PERFORMANCE2_HTML 1014
#define IDS_PREFERENCES_HTML 1015
#define IDS_RESPONSE_FILTER_HTML 1016
#define IDS_SERVER_HTML 1017
#define IDS_TRANSPARENCY_HTML 1018
#define IDS_URLREQUEST_HTML 1019
#define IDS_WEBSOCKET_HTML 1020
#define IDS_WINDOW_HTML 1021
#define IDS_WINDOW_ICON_1X_PNG 1022
#define IDS_WINDOW_ICON_2X_PNG 1023
#define IDS_XMLHTTPREQUEST_HTML 1024
#define IDS_EXTENSIONS_SET_PAGE_COLOR_ICON_PNG 1030
#define IDS_EXTENSIONS_SET_PAGE_COLOR_MANIFEST_JSON 1031

View File

@ -26,8 +26,9 @@ int GetResourceId(const char* resource_name) {
IDS_EXTENSIONS_SET_PAGE_COLOR_POPUP_HTML},
{"extensions/set_page_color/popup.js",
IDS_EXTENSIONS_SET_PAGE_COLOR_POPUP_JS},
{"logo.png", IDS_LOGO_PNG},
{"localstorage.html", IDS_LOCALSTORAGE_HTML},
{"logo.png", IDS_LOGO_PNG},
{"media_router.html", IDS_MEDIA_ROUTER_HTML},
{"menu_icon.1x.png", IDS_MENU_ICON_1X_PNG},
{"menu_icon.2x.png", IDS_MENU_ICON_2X_PNG},
{"osr_test.html", IDS_OSRTEST_HTML},

View File

@ -20,6 +20,7 @@
#include "tests/cefclient/browser/dialog_test.h"
#include "tests/cefclient/browser/drm_test.h"
#include "tests/cefclient/browser/main_context.h"
#include "tests/cefclient/browser/media_router_test.h"
#include "tests/cefclient/browser/preferences_test.h"
#include "tests/cefclient/browser/resource.h"
#include "tests/cefclient/browser/response_filter_test.h"
@ -882,6 +883,9 @@ void CreateMessageHandlers(MessageHandlerSet& handlers) {
// Create the drm test handlers.
drm_test::CreateMessageHandlers(handlers);
// Create the media router test handlers.
media_router_test::CreateMessageHandlers(handlers);
// Create the preferences test handlers.
preferences_test::CreateMessageHandlers(handlers);

View File

@ -0,0 +1,562 @@
<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: 400px;
}
.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";
}
///
// 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
}, ...
]
*/
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.type + ')';
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'
},
{
name: 'Sink 2',
type: 'dial',
id: 'sink2'
}
];
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://bitbucket.org/chromiumembedded/cef/issues/2900/add-mediarouter-support-for-cast-receiver#comment-56680326" 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>

View File

@ -23,6 +23,7 @@
<li><a href="performance2">JavaScript Performance (2) Tests</a></li>
<li><a href="window">JavaScript Window Manipulation</a></li>
<li><a href="localstorage">Local Storage</a></li>
<li><a href="media_router">Media Router (Cast/DIAL)</a></li>
<li><a href="pdf.pdf">PDF Viewer direct</a></li>
<li><a href="pdf">PDF Viewer iframe</a></li>
<li><a href="preferences">Preferences</a></li>

View File

@ -35,6 +35,7 @@ IDS_DRAGGABLE_HTML BINARY "..\\draggable.html"
IDS_DRM_HTML BINARY "..\\drm.html"
IDS_LOCALSTORAGE_HTML BINARY "..\\localstorage.html"
IDS_LOGO_PNG BINARY "..\\logo.png"
IDS_MEDIA_ROUTER_HTML BINARY "..\\media_router.html"
IDS_MENU_ICON_1X_PNG BINARY "..\\menu_icon.1x.png"
IDS_MENU_ICON_2X_PNG BINARY "..\\menu_icon.2x.png"
IDS_OSRTEST_HTML BINARY "..\\..\\..\\shared\\resources\\osr_test.html"