| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/media/router/providers/cast/cast_activity_manager.h" |
| |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/containers/contains.h" |
| #include "base/containers/cxx20_erase.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/json/json_reader.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/media/router/data_decoder_util.h" |
| #include "chrome/browser/media/router/providers/cast/app_activity.h" |
| #include "chrome/browser/media/router/providers/cast/cast_media_route_provider_metrics.h" |
| #include "chrome/browser/media/router/providers/cast/cast_session_client.h" |
| #include "chrome/browser/media/router/providers/cast/mirroring_activity.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/media_router/browser/logger_impl.h" |
| #include "components/media_router/browser/media_router_metrics.h" |
| #include "components/media_router/common/media_source.h" |
| #include "components/media_router/common/mojom/media_router.mojom.h" |
| #include "components/media_router/common/providers/cast/channel/cast_message_util.h" |
| #include "components/media_router/common/providers/cast/channel/enum_table.h" |
| #include "components/media_router/common/route_request_result.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "url/origin.h" |
| |
| using blink::mojom::PresentationConnectionCloseReason; |
| using blink::mojom::PresentationConnectionState; |
| |
| namespace media_router { |
| |
| namespace { |
| |
| constexpr char kLoggerComponent[] = "CastActivityManager"; |
| |
| } // namespace |
| |
| CastActivityManager::CastActivityManager( |
| MediaSinkServiceBase* media_sink_service, |
| CastSessionTracker* session_tracker, |
| cast_channel::CastMessageHandler* message_handler, |
| mojom::MediaRouter* media_router, |
| mojom::Logger* logger, |
| const std::string& hash_token) |
| : media_sink_service_(media_sink_service), |
| session_tracker_(session_tracker), |
| message_handler_(message_handler), |
| media_router_(media_router), |
| logger_(logger), |
| hash_token_(hash_token) { |
| DCHECK(media_sink_service_); |
| DCHECK(session_tracker_); |
| DCHECK(message_handler_); |
| DCHECK(media_router_); |
| DCHECK(logger_); |
| message_handler_->AddObserver(this); |
| for (const auto& sink_id_session : session_tracker_->GetSessions()) { |
| const MediaSinkInternal* sink = |
| media_sink_service_->GetSinkById(sink_id_session.first); |
| if (!sink) |
| break; |
| AddNonLocalActivity(*sink, *sink_id_session.second); |
| } |
| session_tracker_->AddObserver(this); |
| } |
| |
| CastActivityManager::~CastActivityManager() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // This call is needed to ensure mirroring activies are terminated when the |
| // browser shuts down. This works when the browser is closed through its UI, |
| // or when it is given an opportunity to shut down gracefully, e.g. with |
| // SIGINT on Linux, but not SIGTERM. |
| TerminateAllLocalMirroringActivities(); |
| |
| message_handler_->RemoveObserver(this); |
| session_tracker_->RemoveObserver(this); |
| } |
| |
| void CastActivityManager::LaunchSession( |
| const CastMediaSource& cast_source, |
| const MediaSinkInternal& sink, |
| const std::string& presentation_id, |
| const url::Origin& origin, |
| int frame_tree_node_id, |
| bool off_the_record, |
| mojom::MediaRouteProvider::CreateRouteCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (cast_source.app_params().empty()) { |
| LaunchSessionParsed(cast_source, sink, presentation_id, origin, |
| frame_tree_node_id, off_the_record, std::move(callback), |
| data_decoder::DataDecoder::ValueOrError()); |
| } else { |
| GetDataDecoder().ParseJson( |
| cast_source.app_params(), |
| base::BindOnce(&CastActivityManager::LaunchSessionParsed, |
| weak_ptr_factory_.GetWeakPtr(), cast_source, sink, |
| presentation_id, origin, frame_tree_node_id, |
| off_the_record, std::move(callback))); |
| } |
| } |
| |
| void CastActivityManager::LaunchSessionParsed( |
| const CastMediaSource& cast_source, |
| const MediaSinkInternal& sink, |
| const std::string& presentation_id, |
| const url::Origin& origin, |
| int frame_tree_node_id, |
| bool off_the_record, |
| mojom::MediaRouteProvider::CreateRouteCallback callback, |
| data_decoder::DataDecoder::ValueOrError result) { |
| if (!cast_source.app_params().empty() && !result.has_value()) { |
| logger_->LogError(mojom::LogCategory::kRoute, kLoggerComponent, |
| base::StrCat({"Error parsing JSON data in appParams: ", |
| result.error()}), |
| sink.id(), cast_source.source_id(), presentation_id); |
| std::move(callback).Run( |
| absl::nullopt, nullptr, std::string("Invalid JSON Format of appParams"), |
| mojom::RouteRequestResultCode::NO_SUPPORTED_PROVIDER); |
| return; |
| } |
| |
| // If the sink is already associated with a route, then it will be removed |
| // when the receiver sends an updated RECEIVER_STATUS message. |
| MediaSource source(cast_source.source_id()); |
| const MediaSink::Id& sink_id = sink.sink().id(); |
| MediaRoute::Id route_id = |
| MediaRoute::GetMediaRouteId(presentation_id, sink_id, source); |
| MediaRoute route(route_id, source, sink_id, /* description */ std::string(), |
| /* is_local */ true); |
| route.set_presentation_id(presentation_id); |
| route.set_local_presentation(true); |
| route.set_off_the_record(off_the_record); |
| if (cast_source.ContainsStreamingApp()) { |
| route.set_controller_type(RouteControllerType::kMirroring); |
| } else { |
| route.set_controller_type(RouteControllerType::kGeneric); |
| } |
| route.set_media_sink_name(sink.sink().name()); |
| route.set_is_connecting(true); |
| |
| // We either have a value, or an error, however `LaunchSession` calls this |
| // function is a default constructed `result`, which is supposed to be |
| // ignored. |
| absl::optional<base::Value> opt_result = absl::nullopt; |
| if (result.has_value() && !result->is_none()) |
| opt_result = std::move(*result); |
| |
| DoLaunchSessionParams params(route, cast_source, sink, origin, |
| frame_tree_node_id, std::move(opt_result), |
| std::move(callback)); |
| |
| // If there is currently a session on the sink, it must be terminated before |
| // the new session can be launched. |
| auto activity_it = |
| base::ranges::find(activities_, sink_id, [](const auto& activity) { |
| return activity.second->route().media_sink_id(); |
| }); |
| |
| if (activity_it == activities_.end()) { |
| DoLaunchSession(std::move(params)); |
| } else { |
| const MediaRoute::Id& existing_route_id = |
| activity_it->second->route().media_route_id(); |
| // We cannot launch the new session in the TerminateSession() callback |
| // because if we create a session there, then it may get deleted when |
| // OnSessionRemoved() is called to notify that the previous session |
| // was removed on the receiver. |
| TerminateSession(existing_route_id, base::DoNothing()); |
| // The new session will be launched when OnSessionRemoved() is called for |
| // the old session. |
| SetPendingLaunch(std::move(params)); |
| } |
| } |
| |
| void CastActivityManager::DoLaunchSession(DoLaunchSessionParams params) { |
| const MediaRoute& route = params.route; |
| const MediaRoute::Id& route_id = route.media_route_id(); |
| const CastMediaSource& cast_source = params.cast_source; |
| const MediaSinkInternal& sink = params.sink; |
| const int frame_tree_node_id = params.frame_tree_node_id; |
| std::string app_id = ChooseAppId(cast_source, params.sink); |
| auto app_params = std::move(params.app_params); |
| |
| if (IsSiteInitiatedMirroringSource(cast_source.source_id())) { |
| base::UmaHistogramBoolean(kHistogramAudioSender, |
| cast_source.site_requested_audio_capture()); |
| } |
| RecordLaunchSessionRequestSupportedAppTypes( |
| cast_source.supported_app_types()); |
| |
| cast_source.ContainsStreamingApp() |
| ? AddMirroringActivity(route, app_id, frame_tree_node_id, |
| sink.cast_data()) |
| : AddAppActivity(route, app_id); |
| |
| if (frame_tree_node_id != -1) { |
| // If there is a route from this frame already, stop it. |
| auto route_it = routes_by_frame_.find(frame_tree_node_id); |
| if (route_it != routes_by_frame_.end()) { |
| TerminateSession(route_it->second, base::DoNothing()); |
| } |
| |
| routes_by_frame_[frame_tree_node_id] = route_id; |
| } |
| |
| NotifyAllOnRoutesUpdated(); |
| base::TimeDelta launch_timeout = cast_source.launch_timeout(); |
| std::vector<std::string> type_str; |
| for (ReceiverAppType type : cast_source.supported_app_types()) { |
| type_str.push_back(cast_util::EnumToString(type).value().data()); |
| } |
| message_handler_->LaunchSession( |
| sink.cast_data().cast_channel_id, app_id, launch_timeout, type_str, |
| app_params, |
| base::BindOnce(&CastActivityManager::HandleLaunchSessionResponse, |
| weak_ptr_factory_.GetWeakPtr(), std::move(params))); |
| logger_->LogInfo(mojom::LogCategory::kRoute, kLoggerComponent, |
| "Sent a Launch Session request.", sink.id(), |
| cast_source.source_id(), |
| MediaRoute::GetPresentationIdFromMediaRouteId(route_id)); |
| } |
| |
| void CastActivityManager::SetPendingLaunch(DoLaunchSessionParams params) { |
| if (pending_launch_ && pending_launch_->callback) { |
| std::move(pending_launch_->callback) |
| .Run(absl::nullopt, nullptr, "Pending launch session params destroyed", |
| mojom::RouteRequestResultCode::CANCELLED); |
| } |
| pending_launch_ = std::move(params); |
| } |
| |
| AppActivity* CastActivityManager::FindActivityForSessionJoin( |
| const CastMediaSource& cast_source, |
| const std::string& presentation_id) { |
| // We only allow joining by session ID. The Cast SDK uses |
| // "cast-session_<Session ID>" as the presentation ID in the reconnect |
| // request. |
| if (!base::StartsWith(presentation_id, kCastPresentationIdPrefix, |
| base::CompareCase::SENSITIVE)) { |
| // TODO(crbug.com/1291725): Find session by presentation_id. |
| return nullptr; |
| } |
| |
| // Find the session ID. |
| std::string session_id{ |
| presentation_id.substr(strlen(kCastPresentationIdPrefix))}; |
| |
| // Find activity by session ID. Search should fail if the session ID is not |
| // valid. |
| auto it = base::ranges::find( |
| app_activities_, session_id, |
| [](const auto& entry) { return entry.second->session_id(); }); |
| return it == app_activities_.end() ? nullptr : it->second; |
| } |
| |
| AppActivity* CastActivityManager::FindActivityForAutoJoin( |
| const CastMediaSource& cast_source, |
| const url::Origin& origin, |
| int frame_tree_node_id) { |
| switch (cast_source.auto_join_policy()) { |
| case AutoJoinPolicy::kTabAndOriginScoped: |
| case AutoJoinPolicy::kOriginScoped: |
| break; |
| case AutoJoinPolicy::kPageScoped: |
| return nullptr; |
| } |
| |
| auto it = base::ranges::find_if( |
| app_activities_, |
| [&cast_source, &origin, frame_tree_node_id](const auto& pair) { |
| AutoJoinPolicy policy = cast_source.auto_join_policy(); |
| const AppActivity* activity = pair.second; |
| if (!activity->route().is_local()) |
| return false; |
| if (!cast_source.ContainsApp(activity->app_id())) |
| return false; |
| return activity->HasJoinableClient(policy, origin, frame_tree_node_id); |
| }); |
| return it == app_activities_.end() ? nullptr : it->second; |
| } |
| |
| void CastActivityManager::JoinSession( |
| const CastMediaSource& cast_source, |
| const std::string& presentation_id, |
| const url::Origin& origin, |
| int frame_tree_node_id, |
| bool off_the_record, |
| mojom::MediaRouteProvider::JoinRouteCallback callback) { |
| AppActivity* activity = nullptr; |
| if (presentation_id == kAutoJoinPresentationId) { |
| activity = FindActivityForAutoJoin(cast_source, origin, frame_tree_node_id); |
| if (!activity && cast_source.default_action_policy() != |
| DefaultActionPolicy::kCastThisTab) { |
| auto sink = GetSinkForMirroringActivity(frame_tree_node_id); |
| if (sink) { |
| LaunchSession(cast_source, *sink, presentation_id, origin, |
| frame_tree_node_id, off_the_record, std::move(callback)); |
| return; |
| } |
| } |
| } else { |
| activity = FindActivityForSessionJoin(cast_source, presentation_id); |
| } |
| |
| if (!activity || !activity->CanJoinSession(cast_source, off_the_record)) { |
| std::move(callback).Run(absl::nullopt, nullptr, |
| std::string("No matching route"), |
| mojom::RouteRequestResultCode::ROUTE_NOT_FOUND); |
| return; |
| } |
| |
| const MediaSinkInternal* sink = |
| media_sink_service_->GetSinkById(activity->route().media_sink_id()); |
| if (!sink) { |
| logger_->LogError(mojom::LogCategory::kRoute, kLoggerComponent, |
| "Cannot find the sink to join with sink_id.", |
| activity->route().media_sink_id(), |
| cast_source.source_id(), presentation_id); |
| std::move(callback).Run(absl::nullopt, nullptr, |
| std::string("Sink not found"), |
| mojom::RouteRequestResultCode::SINK_NOT_FOUND); |
| return; |
| } |
| |
| mojom::RoutePresentationConnectionPtr presentation_connection = |
| activity->AddClient(cast_source, origin, frame_tree_node_id); |
| |
| if (!activity->session_id()) { |
| // This should never happen, but it looks like maybe it does. See |
| // crbug.com/1114067. |
| NOTREACHED(); |
| static const char kErrorMessage[] = "Internal error: missing session ID"; |
| // Checking for |logger_| here is pure paranoia, but this code only exists |
| // to fix a crash we can't reproduce, so creating even a tiny possibility of |
| // a different crash seems like a bad idea. |
| if (logger_) { |
| // The empty string parameters could have real values, but they're omitted |
| // out of an abundance of caution, and they're not especially relevant to |
| // this error anyway. |
| logger_->LogError(mojom::LogCategory::kRoute, kLoggerComponent, |
| kErrorMessage, "", "", ""); |
| } |
| std::move(callback).Run(absl::nullopt, nullptr, kErrorMessage, |
| mojom::RouteRequestResultCode::UNKNOWN_ERROR); |
| return; |
| } |
| |
| const CastSession* session = |
| session_tracker_->GetSessionById(*activity->session_id()); |
| const std::string& client_id = cast_source.client_id(); |
| activity->SendMessageToClient( |
| client_id, |
| CreateNewSessionMessage(*session, client_id, *sink, hash_token_)); |
| message_handler_->EnsureConnection(sink->cast_data().cast_channel_id, |
| client_id, session->destination_id(), |
| cast_source.connection_type()); |
| |
| // Route is now local; update route queries. |
| NotifyAllOnRoutesUpdated(); |
| std::move(callback).Run(activity->route(), std::move(presentation_connection), |
| absl::nullopt, mojom::RouteRequestResultCode::OK); |
| logger_->LogInfo(mojom::LogCategory::kRoute, kLoggerComponent, |
| "Successfully joined session", sink->id(), |
| cast_source.source_id(), presentation_id); |
| } |
| |
| void CastActivityManager::OnActivityStopped(const std::string& route_id) { |
| TerminateSession(route_id, base::DoNothing()); |
| } |
| |
| void CastActivityManager::RemoveActivity( |
| ActivityMap::iterator activity_it, |
| PresentationConnectionState state, |
| PresentationConnectionCloseReason close_reason) { |
| // Keep a copy of route id so it does not get deleted. |
| std::string route_id(activity_it->first); |
| RemoveActivityWithoutNotification(activity_it, state, close_reason); |
| if (state == PresentationConnectionState::CLOSED) { |
| media_router_->OnPresentationConnectionClosed( |
| route_id, close_reason, |
| /* message */ "Activity removed from CastActivityManager."); |
| } else { |
| media_router_->OnPresentationConnectionStateChanged(route_id, state); |
| } |
| NotifyAllOnRoutesUpdated(); |
| } |
| |
| void CastActivityManager::RemoveActivityWithoutNotification( |
| ActivityMap::iterator activity_it, |
| PresentationConnectionState state, |
| PresentationConnectionCloseReason close_reason) { |
| switch (state) { |
| case PresentationConnectionState::CLOSED: |
| activity_it->second->ClosePresentationConnections(close_reason); |
| break; |
| case PresentationConnectionState::TERMINATED: |
| activity_it->second->TerminatePresentationConnections(); |
| break; |
| default: |
| DLOG(ERROR) << "Invalid state: " << state; |
| } |
| |
| base::EraseIf(routes_by_frame_, [activity_it](const auto& pair) { |
| return pair.second == activity_it->first; |
| }); |
| app_activities_.erase(activity_it->first); |
| activities_.erase(activity_it); |
| } |
| |
| void CastActivityManager::TerminateSession( |
| const MediaRoute::Id& route_id, |
| mojom::MediaRouteProvider::TerminateRouteCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| const std::string source_id = |
| MediaRoute::GetMediaSourceIdFromMediaRouteId(route_id); |
| const std::string presentation_id = |
| MediaRoute::GetPresentationIdFromMediaRouteId(route_id); |
| logger_->LogInfo(mojom::LogCategory::kRoute, kLoggerComponent, |
| "Terminating a session.", "", source_id, presentation_id); |
| auto activity_it = activities_.find(route_id); |
| if (activity_it == activities_.end()) { |
| logger_->LogWarning(mojom::LogCategory::kRoute, kLoggerComponent, |
| "Cannot find the activity to terminate with route id.", |
| "", source_id, presentation_id); |
| std::move(callback).Run("Activity not found", |
| mojom::RouteRequestResultCode::ROUTE_NOT_FOUND); |
| return; |
| } |
| |
| const auto& activity = activity_it->second; |
| const auto& session_id = activity->session_id(); |
| const MediaRoute& route = activity->route(); |
| |
| // There is no session associated with the route, e.g. the launch request is |
| // still pending. |
| if (!session_id) { |
| // |route_id| might be a reference to the item in |routes_by_frame_|. |
| // RemoveActivity() deletes this item in |routes_by_frame_| and invalidates |
| // |route_id|. |
| RemoveActivity(activity_it, PresentationConnectionState::TERMINATED, |
| PresentationConnectionCloseReason::CLOSED); |
| logger_->LogInfo(mojom::LogCategory::kRoute, kLoggerComponent, |
| "Terminated session has no session ID.", "", source_id, |
| presentation_id); |
| std::move(callback).Run(absl::nullopt, mojom::RouteRequestResultCode::OK); |
| return; |
| } |
| |
| const MediaSinkInternal* sink = media_sink_service_->GetSinkByRoute(route); |
| CHECK(sink); |
| |
| // TODO(crbug.com/1291748): Get the real client ID. |
| absl::optional<std::string> client_id = absl::nullopt; |
| |
| activity->SendStopSessionMessageToClients(hash_token_); |
| message_handler_->StopSession( |
| sink->cast_channel_id(), *session_id, client_id, |
| MakeResultCallbackForRoute(route_id, std::move(callback))); |
| } |
| |
| bool CastActivityManager::CreateMediaController( |
| const std::string& route_id, |
| mojo::PendingReceiver<mojom::MediaController> media_controller, |
| mojo::PendingRemote<mojom::MediaStatusObserver> observer) { |
| auto activity_it = activities_.find(route_id); |
| if (activity_it == activities_.end()) |
| return false; |
| activity_it->second->CreateMediaController(std::move(media_controller), |
| std::move(observer)); |
| return true; |
| } |
| |
| CastActivityManager::ActivityMap::iterator |
| CastActivityManager::FindActivityByChannelId(int channel_id) { |
| return base::ranges::find_if(activities_, [channel_id, this](auto& entry) { |
| const MediaRoute& route = entry.second->route(); |
| const MediaSinkInternal* sink = media_sink_service_->GetSinkByRoute(route); |
| return sink && sink->cast_data().cast_channel_id == channel_id; |
| }); |
| } |
| |
| CastActivityManager::ActivityMap::iterator |
| CastActivityManager::FindActivityBySink(const MediaSinkInternal& sink) { |
| const MediaSink::Id& sink_id = sink.sink().id(); |
| return base::ranges::find(activities_, sink_id, [](const auto& activity) { |
| return activity.second->route().media_sink_id(); |
| }); |
| } |
| |
| AppActivity* CastActivityManager::AddAppActivity(const MediaRoute& route, |
| const std::string& app_id) { |
| std::unique_ptr<AppActivity> activity( |
| cast_activity_factory_for_test_ |
| ? cast_activity_factory_for_test_->MakeAppActivity(route, app_id) |
| : std::make_unique<AppActivity>(route, app_id, message_handler_, |
| session_tracker_)); |
| auto* const activity_ptr = activity.get(); |
| activities_.emplace(route.media_route_id(), std::move(activity)); |
| app_activities_[route.media_route_id()] = activity_ptr; |
| return activity_ptr; |
| } |
| |
| CastActivity* CastActivityManager::AddMirroringActivity( |
| const MediaRoute& route, |
| const std::string& app_id, |
| const int frame_tree_node_id, |
| const CastSinkExtraData& cast_data) { |
| // We could theoretically use base::Unretained() below instead of |
| // GetWeakPtr(), but that seems like an unnecessary optimization here. |
| auto on_stop = |
| base::BindOnce(&CastActivityManager::OnActivityStopped, |
| weak_ptr_factory_.GetWeakPtr(), route.media_route_id()); |
| auto activity = cast_activity_factory_for_test_ |
| ? cast_activity_factory_for_test_->MakeMirroringActivity( |
| route, app_id, std::move(on_stop)) |
| : std::make_unique<MirroringActivity>( |
| route, app_id, message_handler_, session_tracker_, |
| frame_tree_node_id, cast_data, std::move(on_stop)); |
| activity->CreateMojoBindings(media_router_); |
| auto* const activity_ptr = activity.get(); |
| activities_.emplace(route.media_route_id(), std::move(activity)); |
| return activity_ptr; |
| } |
| |
| void CastActivityManager::OnAppMessage( |
| int channel_id, |
| const cast::channel::CastMessage& message) { |
| // Note: app messages are received only after session is created. |
| DVLOG(2) << "Received app message on cast channel " << channel_id; |
| auto it = FindActivityByChannelId(channel_id); |
| if (it == activities_.end()) { |
| DVLOG(2) << "No activity associated with channel!"; |
| return; |
| } |
| it->second->OnAppMessage(message); |
| } |
| |
| void CastActivityManager::OnInternalMessage( |
| int channel_id, |
| const cast_channel::InternalMessage& message) { |
| DVLOG(2) << "Received internal message on cast channel " << channel_id; |
| auto it = FindActivityByChannelId(channel_id); |
| if (it == activities_.end()) { |
| DVLOG(2) << "No activity associated with channel!"; |
| return; |
| } |
| it->second->OnInternalMessage(message); |
| } |
| |
| void CastActivityManager::OnSessionAddedOrUpdated(const MediaSinkInternal& sink, |
| const CastSession& session) { |
| auto activity_it = FindActivityByChannelId(sink.cast_data().cast_channel_id); |
| |
| // If |activity| is null, we have discovered a non-local activity. |
| if (activity_it == activities_.end()) { |
| // TODO(crbug.com/954797): Test this case. |
| AddNonLocalActivity(sink, session); |
| NotifyAllOnRoutesUpdated(); |
| return; |
| } |
| |
| CastActivity* activity = activity_it->second.get(); |
| DCHECK(activity->route().media_sink_id() == sink.sink().id()); |
| |
| const auto& existing_session_id = activity->session_id(); |
| |
| // This condition seems to always be true in practice, but if it's not, we |
| // still try to handle them gracefully below. |
| // |
| // TODO(crbug.com/1291721): Replace VLOG_IF with an UMA metric. |
| VLOG_IF(1, !existing_session_id) << "No existing_session_id."; |
| |
| // If |existing_session_id| is empty, then most likely it's due to a pending |
| // launch. Check the app ID to see if the existing activity should be |
| // updated or replaced. Otherwise, check the session ID to see if the |
| // existing activity should be updated or replaced. |
| if (existing_session_id ? existing_session_id == session.session_id() |
| : activity->app_id() == session.app_id()) { |
| activity->SetOrUpdateSession(session, sink, hash_token_); |
| } else { |
| // NOTE(jrw): This happens if a receiver switches to a new session (or |
| // app), causing the activity associated with the old session to be |
| // considered remote. This scenario is tested in the unit tests, but it's |
| // unclear whether it even happens in practice; I haven't been able to |
| // trigger it. |
| // |
| // TODO(crbug.com/1291721): Try to come up with a test to exercise this |
| // code. Figure out why this code was originally written to explicitly |
| // avoid calling NotifyAllOnRoutesUpdated(). |
| RemoveActivityWithoutNotification( |
| activity_it, PresentationConnectionState::TERMINATED, |
| PresentationConnectionCloseReason::CLOSED); |
| AddNonLocalActivity(sink, session); |
| } |
| NotifyAllOnRoutesUpdated(); |
| } |
| |
| void CastActivityManager::OnSessionRemoved(const MediaSinkInternal& sink) { |
| auto activity_it = FindActivityBySink(sink); |
| if (activity_it != activities_.end()) { |
| logger_->LogInfo( |
| mojom::LogCategory::kRoute, kLoggerComponent, |
| "Session removed by the receiver.", sink.sink().id(), |
| MediaRoute::GetMediaSourceIdFromMediaRouteId(activity_it->first), |
| MediaRoute::GetPresentationIdFromMediaRouteId(activity_it->first)); |
| RemoveActivity(activity_it, PresentationConnectionState::TERMINATED, |
| PresentationConnectionCloseReason::CLOSED); |
| } |
| if (pending_launch_ && pending_launch_->sink.id() == sink.id()) { |
| DoLaunchSession(std::move(*pending_launch_)); |
| pending_launch_.reset(); |
| } |
| } |
| |
| void CastActivityManager::OnMediaStatusUpdated( |
| const MediaSinkInternal& sink, |
| const base::Value::Dict& media_status, |
| absl::optional<int> request_id) { |
| auto it = FindActivityBySink(sink); |
| if (it != activities_.end()) { |
| it->second->SendMediaStatusToClients(media_status, request_id); |
| } |
| } |
| |
| void CastActivityManager::OnSourceChanged(const std::string& media_route_id, |
| int old_frame_tree_node_id, |
| int frame_tree_node_id) { |
| auto current_it = routes_by_frame_.find(old_frame_tree_node_id); |
| if (current_it == routes_by_frame_.end() || |
| current_it->second != media_route_id) { |
| return; |
| } |
| |
| auto route_it = routes_by_frame_.find(frame_tree_node_id); |
| if (route_it != routes_by_frame_.end()) { |
| // Session is terminated as to not allow 2 cast sessions to have the same |
| // source tab. |
| TerminateSession(route_it->second, base::DoNothing()); |
| } |
| |
| routes_by_frame_.erase(old_frame_tree_node_id); |
| routes_by_frame_[frame_tree_node_id] = media_route_id; |
| } |
| |
| // This method is only called in one place, so it should probably be inlined. |
| cast_channel::ResultCallback CastActivityManager::MakeResultCallbackForRoute( |
| const std::string& route_id, |
| mojom::MediaRouteProvider::TerminateRouteCallback callback) { |
| return base::BindOnce(&CastActivityManager::HandleStopSessionResponse, |
| weak_ptr_factory_.GetWeakPtr(), route_id, |
| std::move(callback)); |
| } |
| |
| void CastActivityManager::SendRouteMessage(const std::string& media_route_id, |
| const std::string& message) { |
| GetDataDecoder().ParseJson( |
| message, |
| base::BindOnce(&CastActivityManager::SendRouteJsonMessage, |
| weak_ptr_factory_.GetWeakPtr(), media_route_id, message)); |
| } |
| |
| void CastActivityManager::SendRouteJsonMessage( |
| const std::string& media_route_id, |
| const std::string& message, |
| data_decoder::DataDecoder::ValueOrError result) { |
| if (!result.has_value()) { |
| logger_->LogError( |
| mojom::LogCategory::kRoute, kLoggerComponent, |
| "Error parsing JSON data when sending route JSON message: " + |
| result.error(), |
| "", MediaRoute::GetMediaSourceIdFromMediaRouteId(media_route_id), |
| MediaRoute::GetPresentationIdFromMediaRouteId(media_route_id)); |
| return; |
| } |
| |
| const std::string* client_id = result->FindStringKey("clientId"); |
| if (!client_id) { |
| logger_->LogError( |
| mojom::LogCategory::kRoute, kLoggerComponent, |
| "Cannot send route JSON message without client id.", "", |
| MediaRoute::GetMediaSourceIdFromMediaRouteId(media_route_id), |
| MediaRoute::GetPresentationIdFromMediaRouteId(media_route_id)); |
| return; |
| } |
| |
| const auto it = activities_.find(media_route_id); |
| if (it == activities_.end()) { |
| logger_->LogError( |
| mojom::LogCategory::kRoute, kLoggerComponent, |
| "No activity found with the given route_id to send route JSON message.", |
| "", MediaRoute::GetMediaSourceIdFromMediaRouteId(media_route_id), |
| MediaRoute::GetPresentationIdFromMediaRouteId(media_route_id)); |
| return; |
| } |
| CastActivity& activity = *it->second; |
| |
| auto message_ptr = |
| blink::mojom::PresentationConnectionMessage::NewMessage(message); |
| activity.SendMessageToClient(*client_id, std::move(message_ptr)); |
| } |
| |
| void CastActivityManager::AddNonLocalActivity(const MediaSinkInternal& sink, |
| const CastSession& session) { |
| const MediaSink::Id& sink_id = sink.sink().id(); |
| |
| // We derive the MediaSource from a session using the app ID. |
| const std::string& app_id = session.app_id(); |
| std::unique_ptr<CastMediaSource> cast_source = |
| CastMediaSource::FromAppId(app_id); |
| MediaSource source(cast_source->source_id()); |
| |
| // The session ID is used instead of presentation ID in determining the |
| // route ID. |
| MediaRoute::Id route_id = |
| MediaRoute::GetMediaRouteId(session.session_id(), sink_id, source); |
| logger_->LogInfo(mojom::LogCategory::kRoute, kLoggerComponent, |
| "Adding non-local route.", sink_id, cast_source->source_id(), |
| MediaRoute::GetPresentationIdFromMediaRouteId(route_id)); |
| // Route description is set in SetOrUpdateSession(). |
| MediaRoute route(route_id, source, sink_id, /* description */ std::string(), |
| /* is_local */ false); |
| route.set_media_sink_name(sink.sink().name()); |
| |
| CastActivity* activity_ptr = nullptr; |
| if (cast_source->ContainsStreamingApp()) { |
| route.set_controller_type(RouteControllerType::kMirroring); |
| activity_ptr = AddMirroringActivity(route, app_id, -1, sink.cast_data()); |
| } else { |
| route.set_controller_type(RouteControllerType::kGeneric); |
| activity_ptr = AddAppActivity(route, app_id); |
| } |
| activity_ptr->SetOrUpdateSession(session, sink, hash_token_); |
| } |
| |
| const MediaRoute* CastActivityManager::GetRoute( |
| const MediaRoute::Id& route_id) const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| auto it = activities_.find(route_id); |
| return it != activities_.end() ? &(it->second->route()) : nullptr; |
| } |
| |
| std::vector<MediaRoute> CastActivityManager::GetRoutes() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| std::vector<MediaRoute> routes; |
| for (const auto& activity : activities_) |
| routes.push_back(activity.second->route()); |
| |
| return routes; |
| } |
| |
| void CastActivityManager::NotifyAllOnRoutesUpdated() { |
| std::vector<MediaRoute> routes = GetRoutes(); |
| media_router_->OnRoutesUpdated(mojom::MediaRouteProviderId::CAST, routes); |
| } |
| |
| void CastActivityManager::HandleLaunchSessionResponse( |
| DoLaunchSessionParams params, |
| cast_channel::LaunchSessionResponse response, |
| cast_channel::LaunchSessionCallbackWrapper* out_callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| const MediaRoute& route = params.route; |
| const MediaRoute::Id& route_id = route.media_route_id(); |
| const MediaSinkInternal& sink = params.sink; |
| const CastMediaSource& cast_source = params.cast_source; |
| |
| // Make copies so that they outlive params, which gets moved before they are |
| // used. |
| const std::string sink_name = sink.sink().name(); |
| const MediaSink::Id sink_id = sink.sink().id(); |
| const base::Time request_creation_time = params.creation_time; |
| |
| auto activity_it = activities_.find(route_id); |
| if (activity_it == activities_.end()) { |
| const std::string error_message = |
| "LaunchSession Response of the route that no longer exists."; |
| logger_->LogError(mojom::LogCategory::kRoute, kLoggerComponent, |
| error_message, sink.id(), cast_source.source_id(), |
| MediaRoute::GetPresentationIdFromMediaRouteId(route_id)); |
| std::move(params.callback) |
| .Run(absl::nullopt, nullptr, error_message, |
| mojom::RouteRequestResultCode::ROUTE_NOT_FOUND); |
| return; |
| } |
| |
| if (response.result != cast_channel::LaunchSessionResponse::Result::kOk) { |
| switch (response.result) { |
| case cast_channel::LaunchSessionResponse::Result::kPendingUserAuth: |
| HandleLaunchSessionResponseMiddleStages( |
| std::move(params), |
| "Pending user authentication for the cast request", out_callback); |
| SendPendingUserAuthNotification(sink_name, sink_id); |
| MediaRouterMetrics::RecordMediaRouterPendingUserAuthLatency( |
| base::Time::Now() - request_creation_time); |
| MediaRouterMetrics::RecordMediaRouterUserPromptWhenLaunchingCast( |
| MediaRouterUserPromptWhenLaunchingCast::kPendingUserAuth); |
| break; |
| case cast_channel::LaunchSessionResponse::Result::kUserAllowed: |
| HandleLaunchSessionResponseMiddleStages( |
| std::move(params), "The user accepted the cast request", |
| out_callback); |
| media_router_->ClearTopIssueForSink(sink_id); |
| break; |
| case cast_channel::LaunchSessionResponse::Result::kUserNotAllowed: |
| HandleLaunchSessionResponseFailures( |
| activity_it, std::move(params), |
| "Failed to launch session as the user declined the cast request.", |
| mojom::RouteRequestResultCode::USER_NOT_ALLOWED); |
| MediaRouterMetrics::RecordMediaRouterUserPromptWhenLaunchingCast( |
| MediaRouterUserPromptWhenLaunchingCast::kUserNotAllowed); |
| media_router_->ClearTopIssueForSink(sink_id); |
| break; |
| case cast_channel::LaunchSessionResponse::Result::kNotificationDisabled: |
| HandleLaunchSessionResponseFailures( |
| activity_it, std::move(params), |
| "Failed to launch session as the notifications are disabled on the " |
| "receiver device.", |
| mojom::RouteRequestResultCode::NOTIFICATION_DISABLED); |
| break; |
| case cast_channel::LaunchSessionResponse::Result::kTimedOut: |
| HandleLaunchSessionResponseFailures( |
| activity_it, std::move(params), |
| "Failed to launch session due to timeout.", |
| mojom::RouteRequestResultCode::TIMED_OUT); |
| break; |
| default: |
| HandleLaunchSessionResponseFailures( |
| activity_it, std::move(params), |
| base::StrCat({"Failed to launch session. ", response.error_msg}), |
| mojom::RouteRequestResultCode::UNKNOWN_ERROR); |
| break; |
| } |
| return; |
| } |
| |
| auto session = CastSession::From(sink, *response.receiver_status); |
| if (!session) { |
| HandleLaunchSessionResponseFailures( |
| activity_it, std::move(params), |
| "Unable to get session from launch response. Cast session is not " |
| "launched.", |
| mojom::RouteRequestResultCode::ROUTE_NOT_FOUND); |
| return; |
| } |
| RecordLaunchSessionResponseAppType(session->value().Find("appType")); |
| |
| mojom::RoutePresentationConnectionPtr presentation_connection; |
| const std::string& client_id = cast_source.client_id(); |
| std::string app_id = ChooseAppId(cast_source, params.sink); |
| const auto channel_id = sink.cast_data().cast_channel_id; |
| const auto destination_id = session->destination_id(); |
| |
| if (MediaSource(cast_source.source_id()).IsCastPresentationUrl()) { |
| presentation_connection = activity_it->second->AddClient( |
| cast_source, params.origin, params.frame_tree_node_id); |
| if (!client_id.empty()) { |
| activity_it->second->SendMessageToClient( |
| client_id, |
| CreateReceiverActionCastMessage(client_id, sink, hash_token_)); |
| } |
| } |
| |
| if (client_id.empty()) { |
| if (!cast_source.ContainsStreamingApp()) { |
| logger_->LogError( |
| mojom::LogCategory::kRoute, kLoggerComponent, |
| "The client ID was unexpectedly empty for a non-mirroring app.", |
| sink.id(), cast_source.source_id(), |
| MediaRoute::GetPresentationIdFromMediaRouteId(route_id)); |
| } |
| } else { |
| activity_it->second->SendMessageToClient( |
| client_id, |
| CreateNewSessionMessage(*session, client_id, sink, hash_token_)); |
| } |
| EnsureConnection(client_id, channel_id, destination_id, cast_source); |
| |
| activity_it->second->SetOrUpdateSession(*session, sink, hash_token_); |
| |
| if (!client_id.empty() && base::Contains(session->message_namespaces(), |
| cast_channel::kMediaNamespace)) { |
| // Request media status from the receiver. |
| base::Value::Dict request; |
| request.Set("type", cast_util::EnumToString< |
| cast_channel::V2MessageType, |
| cast_channel::V2MessageType::kMediaGetStatus>()); |
| message_handler_->SendMediaRequest(channel_id, request, client_id, |
| destination_id); |
| } |
| |
| activity_it->second->SetRouteIsConnecting(false); |
| NotifyAllOnRoutesUpdated(); |
| logger_->LogInfo(mojom::LogCategory::kRoute, kLoggerComponent, |
| "Successfully Launched the session.", sink.id(), |
| cast_source.source_id(), |
| MediaRoute::GetPresentationIdFromMediaRouteId(route_id)); |
| |
| std::move(params.callback) |
| .Run(route, std::move(presentation_connection), |
| /* error_text */ absl::nullopt, mojom::RouteRequestResultCode::OK); |
| } |
| |
| void CastActivityManager::HandleStopSessionResponse( |
| const MediaRoute::Id& route_id, |
| mojom::MediaRouteProvider::TerminateRouteCallback callback, |
| cast_channel::Result result) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| auto activity_it = activities_.find(route_id); |
| if (activity_it == activities_.end()) { |
| // The activity could've been removed via RECEIVER_STATUS message. |
| std::move(callback).Run(absl::nullopt, mojom::RouteRequestResultCode::OK); |
| return; |
| } |
| |
| const std::string source_id = |
| MediaRoute::GetMediaSourceIdFromMediaRouteId(route_id); |
| const std::string presentation_id = |
| MediaRoute::GetPresentationIdFromMediaRouteId(route_id); |
| |
| if (result == cast_channel::Result::kOk) { |
| // |route_id| might be a reference to the item in |routes_by_frame_|. |
| // RemoveActivity() deletes this item in |routes_by_frame_| and invalidates |
| // |route_id|. |
| RemoveActivity(activity_it, PresentationConnectionState::TERMINATED, |
| PresentationConnectionCloseReason::CLOSED); |
| std::move(callback).Run(absl::nullopt, mojom::RouteRequestResultCode::OK); |
| |
| logger_->LogInfo(mojom::LogCategory::kRoute, kLoggerComponent, |
| "Terminated a route successfully after receiving " |
| "StopSession response OK.", |
| "", source_id, presentation_id); |
| } else { |
| std::string error_msg = |
| "StopSession response is not OK. Failed to terminate route."; |
| std::move(callback).Run(error_msg, |
| mojom::RouteRequestResultCode::UNKNOWN_ERROR); |
| logger_->LogError(mojom::LogCategory::kRoute, kLoggerComponent, error_msg, |
| "", source_id, presentation_id); |
| } |
| } |
| |
| void CastActivityManager::HandleLaunchSessionResponseFailures( |
| ActivityMap::iterator activity_it, |
| DoLaunchSessionParams params, |
| const std::string& message, |
| mojom::RouteRequestResultCode result_code) { |
| logger_->LogError(mojom::LogCategory::kRoute, kLoggerComponent, message, |
| params.sink.id(), params.cast_source.source_id(), |
| MediaRoute::GetPresentationIdFromMediaRouteId( |
| params.route.media_route_id())); |
| std::move(params.callback).Run(absl::nullopt, nullptr, message, result_code); |
| RemoveActivity(activity_it, PresentationConnectionState::CLOSED, |
| PresentationConnectionCloseReason::CONNECTION_ERROR); |
| |
| if (result_code != mojom::RouteRequestResultCode::USER_NOT_ALLOWED && |
| result_code != mojom::RouteRequestResultCode::NOTIFICATION_DISABLED) |
| SendFailedToCastIssue(params.sink.id(), params.route.media_route_id()); |
| } |
| |
| void CastActivityManager::HandleLaunchSessionResponseMiddleStages( |
| DoLaunchSessionParams params, |
| const std::string& message, |
| cast_channel::LaunchSessionCallbackWrapper* out_callback) { |
| DCHECK(out_callback); |
| logger_->LogInfo(mojom::LogCategory::kRoute, kLoggerComponent, message, |
| params.sink.id(), params.cast_source.source_id(), |
| MediaRoute::GetPresentationIdFromMediaRouteId( |
| params.route.media_route_id())); |
| out_callback->callback = |
| base::BindOnce(&CastActivityManager::HandleLaunchSessionResponse, |
| weak_ptr_factory_.GetWeakPtr(), std::move(params)); |
| } |
| |
| void CastActivityManager::EnsureConnection(const std::string& client_id, |
| int channel_id, |
| const std::string& destination_id, |
| const CastMediaSource& cast_source) { |
| // Cast SDK sessions have a |client_id|, and we ensure a virtual connection |
| // for them. For mirroring sessions, we ensure a strong virtual connection for |
| // |message_handler_|. Mirroring initiated via the Cast SDK will have |
| // EnsureConnection() called for both. |
| if (!client_id.empty()) { |
| message_handler_->EnsureConnection(channel_id, client_id, destination_id, |
| cast_source.connection_type()); |
| } |
| if (cast_source.ContainsStreamingApp()) { |
| message_handler_->EnsureConnection( |
| channel_id, message_handler_->source_id(), destination_id, |
| cast_channel::VirtualConnectionType::kStrong); |
| } |
| } |
| |
| void CastActivityManager::SendFailedToCastIssue( |
| const MediaSink::Id& sink_id, |
| const MediaRoute::Id& route_id) { |
| std::string issue_title = |
| l10n_util::GetStringUTF8(IDS_MEDIA_ROUTER_ISSUE_FAILED_TO_CAST); |
| IssueInfo info(issue_title, IssueInfo::Action::DISMISS, |
| IssueInfo::Severity::WARNING); |
| |
| info.sink_id = sink_id; |
| info.route_id = route_id; |
| media_router_->OnIssue(info); |
| } |
| |
| void CastActivityManager::SendPendingUserAuthNotification( |
| const std::string& sink_name, |
| const MediaSink::Id& sink_id) { |
| std::string issue_title = l10n_util::GetStringFUTF8( |
| IDS_MEDIA_ROUTER_ISSUE_CREATE_ROUTE_USER_PENDING_AUTHORIZATION, |
| base::UTF8ToUTF16(sink_name)); |
| |
| IssueInfo info(issue_title, IssueInfo::Action::DISMISS, |
| IssueInfo::Severity::NOTIFICATION); |
| info.sink_id = sink_id; |
| media_router_->OnIssue(info); |
| } |
| |
| absl::optional<MediaSinkInternal> |
| CastActivityManager::GetSinkForMirroringActivity(int frame_tree_node_id) const { |
| auto route_it = routes_by_frame_.find(frame_tree_node_id); |
| if (route_it == routes_by_frame_.end()) { |
| return absl::nullopt; |
| } |
| |
| const MediaRoute::Id& route_id = route_it->second; |
| if (activities_.find(route_id) != activities_.end() && |
| app_activities_.find(route_id) == app_activities_.end()) { |
| return activities_.find(route_id)->second->sink(); |
| } |
| return absl::nullopt; |
| } |
| |
| std::string CastActivityManager::ChooseAppId( |
| const CastMediaSource& source, |
| const MediaSinkInternal& sink) const { |
| const auto sink_capabilities = |
| BitwiseOr<cast_channel::CastDeviceCapability>::FromBits( |
| sink.cast_data().capabilities); |
| for (const auto& info : source.app_infos()) { |
| if (sink_capabilities.HasAll(info.required_capabilities)) |
| return info.app_id; |
| } |
| NOTREACHED() << "Can't determine app ID from capabilities."; |
| return source.app_infos()[0].app_id; |
| } |
| |
| void CastActivityManager::TerminateAllLocalMirroringActivities() { |
| // Save all route IDs so we aren't iterating over |activities_| when it's |
| // modified. |
| std::vector<MediaRoute::Id> route_ids; |
| for (const auto& pair : activities_) { |
| if (pair.second->route().is_local() && |
| // Anything that isn't an app activity is a mirroring activity. |
| app_activities_.find(pair.first) == app_activities_.end()) { |
| route_ids.push_back(pair.first); |
| } |
| } |
| |
| // Terminate the activities. |
| for (const auto& id : route_ids) { |
| TerminateSession(id, base::DoNothing()); |
| } |
| } |
| |
| CastActivityManager::DoLaunchSessionParams::DoLaunchSessionParams( |
| const MediaRoute& route, |
| const CastMediaSource& cast_source, |
| const MediaSinkInternal& sink, |
| const url::Origin& origin, |
| int frame_tree_node_id, |
| const absl::optional<base::Value> app_params, |
| mojom::MediaRouteProvider::CreateRouteCallback callback) |
| : route(route), |
| cast_source(cast_source), |
| sink(sink), |
| origin(origin), |
| frame_tree_node_id(frame_tree_node_id), |
| creation_time(base::Time::Now()), |
| callback(std::move(callback)) { |
| if (app_params) |
| this->app_params = app_params->Clone(); |
| } |
| |
| CastActivityManager::DoLaunchSessionParams::DoLaunchSessionParams( |
| DoLaunchSessionParams&& other) = default; |
| |
| CastActivityManager::DoLaunchSessionParams::~DoLaunchSessionParams() = default; |
| |
| // static |
| CastActivityFactoryForTest* |
| CastActivityManager::cast_activity_factory_for_test_ = nullptr; |
| |
| } // namespace media_router |