blob: 029ca9feaba007edda6acf1828d9ae2df64aee6f [file] [log] [blame]
// Copyright 2018 The Chromium 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 "chrome/browser/ui/media_router/media_router_ui_base.h"
#include <algorithm>
#include <string>
#include <unordered_map>
#include <utility>
#include "base/macros.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/media/router/media_router.h"
#include "chrome/browser/media/router/media_router_factory.h"
#include "chrome/browser/media/router/media_router_feature.h"
#include "chrome/browser/media/router/media_router_metrics.h"
#include "chrome/browser/media/router/media_routes_observer.h"
#include "chrome/browser/media/router/presentation/presentation_service_delegate_impl.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/session_tab_helper.h"
#include "chrome/common/media_router/media_route.h"
#include "chrome/common/media_router/media_sink.h"
#include "chrome/common/media_router/media_source.h"
#include "chrome/common/media_router/media_source_helper.h"
#include "chrome/common/media_router/route_request_result.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/constants.h"
#include "third_party/icu/source/i18n/unicode/coll.h"
#include "url/origin.h"
#if !defined(OS_MACOSX) || BUILDFLAG(MAC_VIEWS_BROWSER)
#include "chrome/browser/media/router/providers/wired_display/wired_display_media_route_provider.h"
#include "ui/display/display.h"
#endif
namespace media_router {
namespace {
std::string TruncateHost(const std::string& host) {
const std::string truncated =
net::registry_controlled_domains::GetDomainAndRegistry(
host, net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES);
// The truncation will be empty in some scenarios (e.g. host is
// simply an IP address). Fail gracefully.
return truncated.empty() ? host : truncated;
}
// Returns the first source in |sources| that can be connected to, or an empty
// source if there is none. This is used by the Media Router to find such a
// matching route if it exists.
MediaSource GetSourceForRouteObserver(const std::vector<MediaSource>& sources) {
auto source_it =
std::find_if(sources.begin(), sources.end(), IsCastPresentationUrl);
return source_it != sources.end() ? *source_it : MediaSource("");
}
} // namespace
MediaRouterUIBase::UIMediaRoutesObserver::UIMediaRoutesObserver(
MediaRouter* router,
const MediaSource::Id& source_id,
const RoutesUpdatedCallback& callback)
: MediaRoutesObserver(router, source_id), callback_(callback) {
DCHECK(!callback_.is_null());
}
MediaRouterUIBase::UIMediaRoutesObserver::~UIMediaRoutesObserver() {}
void MediaRouterUIBase::UIMediaRoutesObserver::OnRoutesUpdated(
const std::vector<MediaRoute>& routes,
const std::vector<MediaRoute::Id>& joinable_route_ids) {
callback_.Run(routes, joinable_route_ids);
}
MediaRouterUIBase::MediaRouterUIBase()
: current_route_request_id_(-1),
route_request_counter_(0),
initiator_(nullptr),
weak_factory_(this) {}
MediaRouterUIBase::~MediaRouterUIBase() {
if (query_result_manager_.get())
query_result_manager_->RemoveObserver(this);
if (presentation_service_delegate_.get())
presentation_service_delegate_->RemoveDefaultPresentationRequestObserver(
this);
// If |start_presentation_context_| still exists, then it means presentation
// route request was never attempted.
if (start_presentation_context_) {
bool presentation_sinks_available = std::any_of(
sinks_.begin(), sinks_.end(), [](const MediaSinkWithCastModes& sink) {
return base::ContainsKey(sink.cast_modes,
MediaCastMode::PRESENTATION);
});
if (presentation_sinks_available) {
start_presentation_context_->InvokeErrorCallback(
blink::mojom::PresentationError(blink::mojom::PresentationErrorType::
PRESENTATION_REQUEST_CANCELLED,
"Dialog closed."));
} else {
start_presentation_context_->InvokeErrorCallback(
blink::mojom::PresentationError(
blink::mojom::PresentationErrorType::NO_AVAILABLE_SCREENS,
"No screens found."));
}
}
}
void MediaRouterUIBase::InitWithDefaultMediaSource(
content::WebContents* initiator,
PresentationServiceDelegateImpl* delegate) {
DCHECK(initiator);
DCHECK(!presentation_service_delegate_);
DCHECK(!query_result_manager_);
InitCommon(initiator);
if (delegate) {
presentation_service_delegate_ = delegate->GetWeakPtr();
presentation_service_delegate_->AddDefaultPresentationRequestObserver(this);
}
if (delegate && delegate->HasDefaultPresentationRequest()) {
OnDefaultPresentationChanged(delegate->GetDefaultPresentationRequest());
} else {
// Register for MediaRoute updates without a media source.
routes_observer_ = std::make_unique<UIMediaRoutesObserver>(
GetMediaRouter(), MediaSource::Id(),
base::BindRepeating(&MediaRouterUIBase::OnRoutesUpdated,
base::Unretained(this)));
}
}
void MediaRouterUIBase::InitWithStartPresentationContext(
content::WebContents* initiator,
PresentationServiceDelegateImpl* delegate,
std::unique_ptr<StartPresentationContext> context) {
DCHECK(initiator);
DCHECK(delegate);
DCHECK(context);
DCHECK(!start_presentation_context_);
DCHECK(!query_result_manager_);
start_presentation_context_ = std::move(context);
presentation_service_delegate_ = delegate->GetWeakPtr();
InitCommon(initiator);
OnDefaultPresentationChanged(
start_presentation_context_->presentation_request());
}
std::vector<MediaSource> MediaRouterUIBase::GetSourcesForCastMode(
MediaCastMode cast_mode) const {
return query_result_manager_->GetSourcesForCastMode(cast_mode);
}
void MediaRouterUIBase::HandleCreateSessionRequestRouteResponse(
const RouteRequestResult&) {}
void MediaRouterUIBase::InitCommon(content::WebContents* initiator) {
DCHECK(initiator);
initiator_ = initiator;
GetMediaRouter()->OnUserGesture();
// Create |collator_| before |query_result_manager_| so that |collator_| is
// already set up when we get a callback from |query_result_manager_|.
UErrorCode error = U_ZERO_ERROR;
const std::string& locale = g_browser_process->GetApplicationLocale();
collator_.reset(
icu::Collator::createInstance(icu::Locale(locale.c_str()), error));
if (U_FAILURE(error)) {
DLOG(ERROR) << "Failed to create collator for locale " << locale;
collator_.reset();
}
query_result_manager_ =
std::make_unique<QueryResultManager>(GetMediaRouter());
query_result_manager_->AddObserver(this);
// Use a placeholder URL as origin for mirroring.
url::Origin origin = url::Origin::Create(GURL());
// Desktop mirror mode is always available.
query_result_manager_->SetSourcesForCastMode(
MediaCastMode::DESKTOP_MIRROR, {MediaSourceForDesktop()}, origin);
// For now, file mirroring is always availible if enabled.
if (CastLocalMediaEnabled()) {
query_result_manager_->SetSourcesForCastMode(
MediaCastMode::LOCAL_FILE, {MediaSourceForTab(0)}, origin);
}
SessionID::id_type tab_id = SessionTabHelper::IdForTab(initiator).id();
if (tab_id != -1) {
MediaSource mirroring_source(MediaSourceForTab(tab_id));
query_result_manager_->SetSourcesForCastMode(MediaCastMode::TAB_MIRROR,
{mirroring_source}, origin);
}
// Get the current list of media routes, so that the WebUI will have routes
// information at initialization.
OnRoutesUpdated(GetMediaRouter()->GetCurrentRoutes(),
std::vector<MediaRoute::Id>());
#if !defined(OS_MACOSX) || BUILDFLAG(MAC_VIEWS_BROWSER)
display_observer_ = WebContentsDisplayObserver::Create(
initiator_, base::BindRepeating(&MediaRouterUIBase::UpdateSinks,
base::Unretained(this)));
#endif
}
void MediaRouterUIBase::OnDefaultPresentationChanged(
const content::PresentationRequest& presentation_request) {
std::vector<MediaSource> sources =
MediaSourcesForPresentationUrls(presentation_request.presentation_urls);
presentation_request_ = presentation_request;
query_result_manager_->SetSourcesForCastMode(
MediaCastMode::PRESENTATION, sources,
presentation_request_->frame_origin);
// Register for MediaRoute updates. NOTE(mfoltz): If there are multiple
// sources that can be connected to via the dialog, this will break. We will
// need to observe multiple sources (keyed by sinks) in that case. As this is
// Cast-specific for the forseeable future, it may be simpler to plumb a new
// observer API for this case.
const MediaSource source_for_route_observer =
GetSourceForRouteObserver(sources);
routes_observer_ = std::make_unique<UIMediaRoutesObserver>(
GetMediaRouter(), source_for_route_observer.id(),
base::BindRepeating(&MediaRouterUIBase::OnRoutesUpdated,
base::Unretained(this)));
}
void MediaRouterUIBase::OnDefaultPresentationRemoved() {
presentation_request_.reset();
query_result_manager_->RemoveSourcesForCastMode(MediaCastMode::PRESENTATION);
// Register for MediaRoute updates without a media source.
routes_observer_ = std::make_unique<UIMediaRoutesObserver>(
GetMediaRouter(), MediaSource::Id(),
base::BindRepeating(&MediaRouterUIBase::OnRoutesUpdated,
base::Unretained(this)));
}
base::Optional<RouteParameters> MediaRouterUIBase::GetRouteParameters(
const MediaSink::Id& sink_id,
MediaCastMode cast_mode) {
DCHECK(query_result_manager_);
DCHECK(initiator_);
RouteParameters params;
// Note that there is a rarely-encountered bug, where the MediaCastMode to
// MediaSource mapping could have been updated, between when the user clicked
// on the UI to start a create route request, and when this function is
// called. However, since the user does not have visibility into the
// MediaSource, and that it occurs very rarely in practice, we leave it as-is
// for now.
std::unique_ptr<MediaSource> source =
query_result_manager_->GetSourceForCastModeAndSink(cast_mode, sink_id);
if (!source) {
LOG(ERROR) << "No corresponding MediaSource for cast mode "
<< static_cast<int>(cast_mode) << " and sink " << sink_id;
return base::nullopt;
}
params.source_id = source->id();
bool for_presentation_source = cast_mode == MediaCastMode::PRESENTATION;
if (for_presentation_source && !presentation_request_) {
DLOG(ERROR) << "Requested to create a route for presentation, but "
<< "presentation request is missing.";
return base::nullopt;
}
current_route_request_id_ = ++route_request_counter_;
params.origin = for_presentation_source ? presentation_request_->frame_origin
: url::Origin::Create(GURL());
DVLOG(1) << "DoCreateRoute: origin: " << params.origin;
// There are 3 cases. In cases (1) and (3) the MediaRouterUIBase will need to
// be notified. In case (2) the dialog will be closed.
// (1) Non-presentation route request (e.g., mirroring). No additional
// notification necessary.
// (2) Presentation route request for a PresentationRequest.start() call.
// The StartPresentationContext will need to be answered with the route
// response.
// (3) Browser-initiated presentation route request. If successful,
// PresentationServiceDelegateImpl will have to be notified. Note that we
// treat subsequent route requests from a Presentation API-initiated
// dialogs as browser-initiated.
if (!for_presentation_source || !start_presentation_context_) {
params.route_response_callbacks.push_back(base::BindOnce(
&MediaRouterUIBase::OnRouteResponseReceived, weak_factory_.GetWeakPtr(),
current_route_request_id_, sink_id, cast_mode,
base::UTF8ToUTF16(GetTruncatedPresentationRequestSourceName())));
}
if (for_presentation_source) {
if (start_presentation_context_) {
// |start_presentation_context_| will be nullptr after this call, as the
// object will be transferred to the callback.
params.route_response_callbacks.push_back(
base::BindOnce(&StartPresentationContext::HandleRouteResponse,
std::move(start_presentation_context_)));
params.route_response_callbacks.push_back(base::BindOnce(
&MediaRouterUIBase::HandleCreateSessionRequestRouteResponse,
weak_factory_.GetWeakPtr()));
} else if (presentation_service_delegate_) {
params.route_response_callbacks.push_back(base::BindOnce(
&PresentationServiceDelegateImpl::OnRouteResponse,
presentation_service_delegate_, *presentation_request_));
}
}
params.route_response_callbacks.push_back(
base::BindOnce(&MediaRouterUIBase::MaybeReportCastingSource,
weak_factory_.GetWeakPtr(), cast_mode));
params.timeout = GetRouteRequestTimeout(cast_mode);
CHECK(initiator());
params.incognito = initiator()->GetBrowserContext()->IsOffTheRecord();
return base::make_optional(std::move(params));
}
bool MediaRouterUIBase::CreateRoute(const MediaSink::Id& sink_id,
MediaCastMode cast_mode) {
base::Optional<RouteParameters> params =
GetRouteParameters(sink_id, cast_mode);
if (!params)
return false;
GetMediaRouter()->CreateRoute(params->source_id, sink_id, params->origin,
initiator_,
std::move(params->route_response_callbacks),
params->timeout, params->incognito);
return true;
}
void MediaRouterUIBase::CloseRoute(const MediaRoute::Id& route_id) {
GetMediaRouter()->TerminateRoute(route_id);
}
void MediaRouterUIBase::OnResultsUpdated(
const std::vector<MediaSinkWithCastModes>& sinks) {
sinks_ = sinks;
const icu::Collator* collator_ptr = collator_.get();
std::sort(sinks_.begin(), sinks_.end(),
[collator_ptr](const MediaSinkWithCastModes& sink1,
const MediaSinkWithCastModes& sink2) {
return sink1.sink.CompareUsingCollator(sink2.sink, collator_ptr);
});
UpdateSinks();
}
void MediaRouterUIBase::OnRoutesUpdated(
const std::vector<MediaRoute>& routes,
const std::vector<MediaRoute::Id>& joinable_route_ids) {
routes_.clear();
for (const MediaRoute& route : routes) {
if (route.for_display()) {
#ifndef NDEBUG
for (const MediaRoute& existing_route : routes_) {
if (existing_route.media_sink_id() == route.media_sink_id()) {
DVLOG(2) << "Received another route for display with the same sink"
<< " id as an existing route. " << route.media_route_id()
<< " has the same sink id as "
<< existing_route.media_sink_id() << ".";
}
}
#endif
routes_.push_back(route);
}
}
}
void MediaRouterUIBase::OnRouteResponseReceived(
int route_request_id,
const MediaSink::Id& sink_id,
MediaCastMode cast_mode,
const base::string16& presentation_request_source_name,
const RouteRequestResult& result) {
DVLOG(1) << "OnRouteResponseReceived";
// If we receive a new route that we aren't expecting, do nothing.
if (route_request_id != current_route_request_id_)
return;
const MediaRoute* route = result.route();
if (!route) {
// The provider will handle sending an issue for a failed route request.
DVLOG(1) << "MediaRouteResponse returned error: " << result.error();
}
current_route_request_id_ = -1;
}
void MediaRouterUIBase::MaybeReportCastingSource(
MediaCastMode cast_mode,
const RouteRequestResult& result) {
if (result.result_code() == RouteRequestResult::OK)
MediaRouterMetrics::RecordMediaRouterCastingSource(cast_mode);
}
GURL MediaRouterUIBase::GetFrameURL() const {
return presentation_request_ ? presentation_request_->frame_origin.GetURL()
: GURL();
}
std::vector<MediaSinkWithCastModes> MediaRouterUIBase::GetEnabledSinks() const {
#if !defined(OS_MACOSX) || BUILDFLAG(MAC_VIEWS_BROWSER)
if (!display_observer_)
return sinks_;
// Filter out the wired display sink for the display that the dialog is on.
// This is not the best place to do this because MRUI should not perform a
// provider-specific behavior, but we currently do not have a way to
// communicate dialog-specific information to/from the
// WiredDisplayMediaRouteProvider.
std::vector<MediaSinkWithCastModes> enabled_sinks;
const std::string display_sink_id =
WiredDisplayMediaRouteProvider::GetSinkIdForDisplay(
display_observer_->GetCurrentDisplay());
for (const MediaSinkWithCastModes& sink : sinks_) {
if (sink.sink.id() != display_sink_id)
enabled_sinks.push_back(sink);
}
return enabled_sinks;
#else
return sinks_;
#endif
}
std::string MediaRouterUIBase::GetTruncatedPresentationRequestSourceName()
const {
GURL gurl = GetFrameURL();
CHECK(initiator());
return gurl.SchemeIs(extensions::kExtensionScheme)
? GetExtensionName(gurl, extensions::ExtensionRegistry::Get(
initiator()->GetBrowserContext()))
: TruncateHost(GetHostFromURL(gurl));
}
MediaRouter* MediaRouterUIBase::GetMediaRouter() const {
CHECK(initiator());
return MediaRouterFactory::GetApiForBrowserContext(
initiator()->GetBrowserContext());
}
} // namespace media_router