// Copyright 2019 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/mirroring_activity.h"

#include <stdint.h>

#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>

#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_tokenizer.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/media/cast_mirroring_service_host_factory.h"
#include "chrome/browser/media/router/discovery/access_code/access_code_cast_feature.h"
#include "chrome/browser/media/router/media_router_feature.h"
#include "chrome/browser/media/router/providers/cast/cast_activity_manager.h"
#include "chrome/browser/media/router/providers/cast/cast_internal_message_util.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/grit/generated_resources.h"
#include "components/access_code_cast/common/access_code_cast_metrics.h"
#include "components/media_router/browser/mirroring_to_flinging_switcher.h"
#include "components/media_router/common/discovery/media_sink_internal.h"
#include "components/media_router/common/mojom/debugger.mojom.h"
#include "components/media_router/common/mojom/logger.mojom.h"
#include "components/media_router/common/mojom/media_router.mojom.h"
#include "components/media_router/common/providers/cast/channel/cast_device_capability.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 "components/mirroring/mojom/session_parameters.mojom.h"
#include "content/public/browser/browser_thread.h"
#include "media/base/media_switches.h"
#include "media/cast/cast_config.h"
#include "media/cast/constants.h"
#include "media/cast/logging/stats_event_subscriber.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "net/base/ip_address.h"
#include "third_party/openscreen/src/cast/common/channel/proto/cast_channel.pb.h"
#include "ui/base/l10n/l10n_util.h"

using blink::mojom::PresentationConnectionMessagePtr;
using cast_channel::Result;
using media::cast::StatsEventSubscriber;
using media_router::mojom::MediaRouteProvider;
using media_router::mojom::MediaRouter;
using mirroring::MirroringServiceHostFactory;
using mirroring::mojom::SessionError;
using mirroring::mojom::SessionParameters;
using mirroring::mojom::SessionType;

namespace media_router {

namespace {

using CastStat = media::cast::StatsEventSubscriber::CastStat;
using MirroringType = MirroringActivity::MirroringType;

constexpr char kHistogramSessionLaunch[] =
    "MediaRouter.CastStreaming.Session.Launch";
constexpr char kHistogramSessionLength[] =
    "MediaRouter.CastStreaming.Session.Length";
constexpr char kHistogramSessionLengthOffscreenTab[] =
    "MediaRouter.CastStreaming.Session.Length.OffscreenTab";
constexpr char kHistogramSessionLengthScreen[] =
    "MediaRouter.CastStreaming.Session.Length.Screen";
constexpr char kHistogramSessionLengthTab[] =
    "MediaRouter.CastStreaming.Session.Length.Tab";
constexpr char kHistogramStartSuccess[] =
    "MediaRouter.CastStreaming.Start.Success";

const char kHistogramTypeAudio[] = "Audio";
const char kHistogramTypeVideo[] = "Video";

constexpr char kLoggerComponent[] = "MirroringService";

enum class HistogramType { kMemory, kLatency, kPercentage };

struct HistogramStatistic {
  CastStat stat;
  const char* name;
  HistogramType type;

  // Percentage statistics have a secondary statistic (the denominator).
  std::optional<CastStat> secondary_stat = std::nullopt;
};

constexpr std::array kHistograms = std::to_array<HistogramStatistic>({
    {.stat = CastStat::TRANSMISSION_KBPS,
     .name = "TransmissionRate",
     .type = HistogramType::kMemory},
    {.stat = CastStat::AVG_ENCODE_TIME_MS,
     .name = "AverageEncodeTime",
     .type = HistogramType::kLatency},
    {.stat = CastStat::AVG_CAPTURE_LATENCY_MS,
     .name = "AverageCaptureLatency",
     .type = HistogramType::kLatency},
    {.stat = CastStat::AVG_E2E_LATENCY_MS,
     .name = "AverageEndToEndLatency",
     .type = HistogramType::kLatency},
    {.stat = CastStat::AVG_NETWORK_LATENCY_MS,
     .name = "AverageNetworkLatency",
     .type = HistogramType::kLatency},
    {.stat = CastStat::NUM_PACKETS_RETRANSMITTED,
     .name = "RetransmittedPacketsPercentage",
     .type = HistogramType::kPercentage,
     .secondary_stat = CastStat::NUM_PACKETS_SENT},
    {.stat = CastStat::NUM_FRAMES_LATE,
     .name = "LateFramesPercentage",
     .type = HistogramType::kPercentage,
     .secondary_stat = CastStat::NUM_FRAMES_CAPTURED},
});

std::string_view GetMirroringNamespace(const base::DictValue& message) {
  const std::string* type = message.FindString("type");
  if (type &&
      *type == cast_util::EnumToString<cast_channel::CastMessageType,
                                       cast_channel::CastMessageType::kRpc>()) {
    return mirroring::mojom::kRemotingNamespace;
  } else {
    return mirroring::mojom::kWebRtcNamespace;
  }
}

std::optional<MirroringType> GetMirroringType(const MediaRoute& route) {
  if (!route.is_local()) {
    return std::nullopt;
  }

  const auto source = route.media_source();
  if (source.IsTabMirroringSource()) {
    return MirroringType::kTab;
  }
  if (source.IsDesktopMirroringSource()) {
    return MirroringType::kDesktop;
  }

  if (base::FeatureList::IsEnabled(media::kMediaRemotingWithoutFullscreen) &&
      source.IsRemotePlaybackSource()) {
    return MirroringType::kTab;
  }

  if (!source.url().is_valid()) {
    NOTREACHED() << "Invalid source: " << source;
  }

  if (source.IsCastPresentationUrl()) {
    const auto cast_source = CastMediaSource::FromMediaSource(source);
    if (!cast_source || !cast_source->ContainsStreamingApp()) {
      NOTREACHED() << "Non-mirroring Cast app: " << source;
    }

    // The source having a valid Cast presentation URL indicates that it is site
    // initiated mirroring, which we represent as tab mirroring here.
    return MirroringType::kTab;
  }

  if (!source.url().SchemeIsHTTPOrHTTPS()) {
    NOTREACHED() << "Invalid source: " << source;
  }
  return MirroringType::kOffscreenTab;
}

const char* GetHistogramName(MirroringType type) {
  switch (type) {
    case MirroringType::kTab:
      return kHistogramSessionLengthTab;

    case MirroringType::kDesktop:
      return kHistogramSessionLengthScreen;

    case MirroringType::kOffscreenTab:
      return kHistogramSessionLengthOffscreenTab;
  }
  NOTREACHED();
}

// TODO(crbug.com/41363512): Remove support for sender side letterboxing.
bool ShouldForceLetterboxing(std::string_view model_name) {
  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          "disable-cast-letterboxing")) {
    return false;
  }
  return model_name.find("Nest Hub") != std::string_view::npos;
}

struct LatePacketResult {
  int num_late_packets;
  int num_total_packets;
};
std::optional<LatePacketResult> GetNumLatePackets(
    const base::ListValue* network_latency_ms_histo,
    int64_t target_playout_delay) {
  if (!network_latency_ms_histo) {
    return std::nullopt;
  }

  static constexpr char kOverflowBucketPrefix[] = ">=";
  static constexpr char kBucketDelimiter[] = "-";

  int all_count = 0;
  int exceeded_count = 0;
  for (const base::Value& entry : *network_latency_ms_histo) {
    if (!entry.is_dict() || entry.GetDict().empty()) {
      continue;
    }

    const auto& [key_str, value] = *entry.GetDict().cbegin();
    const std::string_view key = key_str;
    const int count = value.GetIfDouble().value_or(0);
    if (count == 0) {
      continue;
    }
    all_count += count;

    std::optional<std::string_view> min_str;
    if (key.starts_with(kOverflowBucketPrefix)) {
      min_str = key.substr(2);
    } else {
      base::StringViewTokenizer tokenizer(key, kBucketDelimiter);
      min_str = tokenizer.GetNextTokenView();
    }

    int min = 0;
    if (min_str && base::StringToInt(*min_str, &min) &&
        min > target_playout_delay) {
      exceeded_count += count;
    }
  }
  if (all_count > 0) {
    return LatePacketResult{exceeded_count, all_count};
  }
  return std::nullopt;
}

std::string GetHistogramName(std::string_view streaming_type,
                             std::string_view suffix) {
  return base::StrCat({"CastStreaming.Sender.", streaming_type, ".", suffix});
}

void MaybeRecordPercentageHistogram(std::string_view name,
                                    std::optional<double> numerator,
                                    std::optional<double> denominator) {
  if (numerator && denominator && *denominator != 0) {
    base::UmaHistogramPercentage(name, *numerator * 100 / *denominator);
  }
}

void MaybeRecordHistogram(const base::DictValue& statistics,
                          std::string_view streaming_type,
                          const HistogramStatistic& histogram) {
  const std::optional<double> value = statistics.FindDouble(
      StatsEventSubscriber::CastStatToString(histogram.stat));
  if (!value) {
    return;
  }

  std::string histogram_name = GetHistogramName(streaming_type, histogram.name);
  switch (histogram.type) {
    case HistogramType::kMemory:
      base::UmaHistogramMemoryKB(histogram_name, *value);
      break;

    case HistogramType::kLatency:
      base::UmaHistogramTimes(histogram_name, base::Milliseconds(*value));
      break;

    case HistogramType::kPercentage: {
      const std::optional<double> denominator = statistics.FindDouble(
          StatsEventSubscriber::CastStatToString(*histogram.secondary_stat));
      MaybeRecordPercentageHistogram(histogram_name, value, denominator);
      break;
    }
  }
}

void RecordCastStreamingSenderUma(const base::DictValue& all_mirroring_stats,
                                  std::string_view stats_dict_key,
                                  int64_t target_playout_delay) {
  const base::DictValue* mirroring_stats =
      all_mirroring_stats.FindDict(stats_dict_key);
  if (!mirroring_stats) {
    return;
  }
  const char* streaming_type =
      stats_dict_key == StatsEventSubscriber::kAudioStatsDictKey
          ? kHistogramTypeAudio
          : kHistogramTypeVideo;

  for (const auto& histogram : kHistograms) {
    MaybeRecordHistogram(*mirroring_stats, streaming_type, histogram);
  }

  // The network latency histogram is a special case due to requiring additional
  // analysis to generate the histogram value.
  const base::ListValue* network_latency_ms_histo =
      mirroring_stats->FindList(StatsEventSubscriber::CastStatToString(
          CastStat::NETWORK_LATENCY_MS_HISTO));
  const std::optional<LatePacketResult> late_packet_result =
      GetNumLatePackets(network_latency_ms_histo, target_playout_delay);
  if (late_packet_result) {
    const std::string histogram_name = GetHistogramName(
        streaming_type, "ExceededPlayoutDelayPacketsPercentage");
    MaybeRecordPercentageHistogram(histogram_name,
                                   late_packet_result->num_late_packets,
                                   late_packet_result->num_total_packets);
  }
}

}  // namespace

MirroringActivity::MirroringActivity(
    const MediaRoute& route,
    const std::string& app_id,
    cast_channel::CastMessageHandler* message_handler,
    CastSessionTracker* session_tracker,
    mojo::Remote<mojom::Logger>& logger,
    mojo::Remote<mojom::Debugger>& debugger,
    content::FrameTreeNodeId frame_tree_node_id,
    const CastSinkExtraData& cast_data,
    OnStopCallback callback,
    OnSourceChangedCallback source_changed_callback)
    : CastActivity(route,
                   app_id,
                   message_handler,
                   session_tracker,
                   logger,
                   debugger),
      media_status_(mojom::MediaStatus::New()),
      mirroring_type_(GetMirroringType(route)),
      frame_tree_node_id_(frame_tree_node_id),
      cast_data_(cast_data),
      on_stop_(std::move(callback)),
      source_changed_callback_(std::move(source_changed_callback)) {
  DETACH_FROM_SEQUENCE(ui_sequence_checker_);
}

MirroringActivity::~MirroringActivity() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  content::GetUIThreadTaskRunner({})->DeleteSoon(FROM_HERE, std::move(host_));

  if (!did_start_mirroring_timestamp_) {
    return;
  }

  // Record mirroring pause metrics.
  if (mirroring_pause_timestamp_) {  // The session is ending while paused.
    AccessCodeCastMetrics::RecordMirroringPauseDuration(
        base::Time::Now() - mirroring_pause_timestamp_.value());
  }
  // We can only get a profile on the UI thread, so we must post a task to check
  // if we should log certain metrics.
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(
          [](int pause_count) {
            // Don't record pause count if the cast session cannot be paused.
            if (pause_count > 0 ||
                media_router::IsAccessCodeCastFreezeUiEnabled(
                    ProfileManager::GetLastUsedProfileIfLoaded())) {
              AccessCodeCastMetrics::RecordMirroringPauseCount(pause_count);
            }
          },
          mirroring_pause_count_));

  const base::TimeDelta cast_duration =
      base::Time::Now() - *did_start_mirroring_timestamp_;
  base::UmaHistogramLongTimes(kHistogramSessionLength, cast_duration);

  const int64_t target_playout_delay_ms =
      target_playout_delay_.value_or(media::cast::kDefaultTargetPlayoutDelay)
          .InMilliseconds();
  for (auto key : {StatsEventSubscriber::kAudioStatsDictKey,
                   StatsEventSubscriber::kVideoStatsDictKey}) {
    RecordCastStreamingSenderUma(most_recent_mirroring_stats_, key,
                                 target_playout_delay_ms);
  }

  if (mirroring_type_.has_value()) {
    base::UmaHistogramLongTimes(GetHistogramName(*mirroring_type_),
                                cast_duration);
  }
}

void MirroringActivity::BindChannelToServiceReceiver() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  CHECK(!channel_to_service_receiver_);
  channel_to_service_receiver_ =
      channel_to_service_.BindNewPipeAndPassReceiver();
}

void MirroringActivity::CreateMirroringServiceHost(
    MirroringServiceHostFactory* host_factory_for_test) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  if (!mirroring_type_) {
    return;
  }

  // base::Unretained use is fine, since it is used with a
  // base::NoDestructor<mirroring::CastMirroringServiceHostFactory> instance.
  auto host_factory = base::Unretained(
      host_factory_for_test
          ? host_factory_for_test
          : &mirroring::CastMirroringServiceHostFactory::GetInstance());

  base::OnceCallback<std::unique_ptr<mirroring::MirroringServiceHost>()>
      host_creation_task;

  // Get a reference to the mirroring service host.
  switch (*mirroring_type_) {
    case MirroringType::kDesktop: {
      auto stream_id = route_.media_source().DesktopStreamId();
      CHECK(stream_id);
      host_creation_task = base::BindOnce(
          &MirroringServiceHostFactory::GetForDesktop, host_factory, stream_id);
      break;
    }
    case MirroringType::kTab:
      host_creation_task =
          base::BindOnce(&MirroringServiceHostFactory::GetForTab, host_factory,
                         frame_tree_node_id_);
      break;
    case MirroringType::kOffscreenTab:
      host_creation_task =
          base::BindOnce(&MirroringServiceHostFactory::GetForOffscreenTab,
                         host_factory, route_.media_source().url(),
                         route_.presentation_id(), frame_tree_node_id_);
      break;
  }

  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, std::move(host_creation_task)
                     .Then(base::BindOnce(&MirroringActivity::set_host,
                                          weak_ptr_factory_.GetWeakPtr())));
}

void MirroringActivity::OnError(SessionError error) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  logger_.get()->LogError(
      media_router::mojom::LogCategory::kMirroring, kLoggerComponent,
      base::StringPrintf(
          "Mirroring will stop. MirroringService.SessionError: %d",
          static_cast<int>(error)),
      route_.media_sink_id(), route_.media_source().id(),
      route_.presentation_id());
  if (will_start_mirroring_timestamp_) {
    will_start_mirroring_timestamp_.reset();
  }
  // Metrics for general errors are captured by the mirroring service in
  // MediaRouter.MirroringService.SessionError.
  StopMirroring();
}

void MirroringActivity::DidStart() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  if (!will_start_mirroring_timestamp_) {
    // DidStart() was called unexpectedly.
    return;
  }
  did_start_mirroring_timestamp_ = base::Time::Now();
  base::UmaHistogramTimes(
      kHistogramSessionLaunch,
      *did_start_mirroring_timestamp_ - *will_start_mirroring_timestamp_);
  CHECK(mirroring_type_);
  base::UmaHistogramEnumeration(kHistogramStartSuccess, *mirroring_type_);

  will_start_mirroring_timestamp_.reset();

  if (should_fetch_stats_on_start_) {
    ScheduleFetchMirroringStats();
  }
}

void MirroringActivity::DidStop() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  StopMirroring();
}

void MirroringActivity::LogInfoMessage(const std::string& message) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  logger_.get()->LogInfo(media_router::mojom::LogCategory::kMirroring,
                         kLoggerComponent, message, route_.media_sink_id(),
                         route_.media_source().id(), route_.presentation_id());
}

void MirroringActivity::LogErrorMessage(const std::string& message) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  logger_.get()->LogError(media_router::mojom::LogCategory::kMirroring,
                          kLoggerComponent, message, route_.media_sink_id(),
                          route_.media_source().id(), route_.presentation_id());
}

void MirroringActivity::OnSourceChanged() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  if (!host_) {
    return;
  }

  std::optional<content::FrameTreeNodeId> frame_tree_node_id =
      host_->GetTabSourceId();
  if (!source_changed_callback_ || !frame_tree_node_id ||
      frame_tree_node_id == frame_tree_node_id_) {
    return;
  }

  source_changed_callback_.Run(frame_tree_node_id_, *frame_tree_node_id);
  frame_tree_node_id_ = *frame_tree_node_id;

  // The source changed, which means that a new capturer was created that is
  // now sending frames. Ensure the state is now PLAYING.
  media_status_->play_state = mojom::MediaStatus::PlayState::PLAYING;
  OnMirroringResumed();
  NotifyMediaStatusObservers();

  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&SwitchToFlingingIfPossible, frame_tree_node_id_));
}

void MirroringActivity::OnRemotingStateChanged(bool is_remoting) {
  media_status_->can_play_pause = !is_remoting;
  // Transitions to/from remoting restart the capturer. Set the state to
  // playing.
  media_status_->play_state = mojom::MediaStatus::PlayState::PLAYING;
  OnMirroringResumed();
  NotifyMediaStatusObservers();
}

void MirroringActivity::OnMessage(mirroring::mojom::CastMessagePtr message) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  CHECK(message);
  HandleParseJsonResult(route().media_route_id(),
                        base::JSONReader::ReadAndReturnValueWithError(
                            message->json_format_data, base::JSON_PARSE_RFC));
}

void MirroringActivity::OnAppMessage(
    const openscreen::cast::proto::CastMessage& message) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  if (!route_.is_local()) {
    return;
  }
  if (message.namespace_() != mirroring::mojom::kWebRtcNamespace &&
      message.namespace_() != mirroring::mojom::kRemotingNamespace) {
    // Ignore message with wrong namespace.
    DVLOG(2) << "Ignoring message with namespace " << message.namespace_();
    return;
  }
  CastSession* session = GetSession();
  if (!session) {
    DVLOG(2) << "No valid session.";
    return;
  }

  if (message.destination_id() != session->destination_id() &&
      message.destination_id() != "*") {
    // Ignore messages sent to someone else.
    DVLOG(2) << "Ignoring message intended for destination_id:\""
             << message.destination_id() << "\" (expected \""
             << session->destination_id() << "\").";
    return;
  }

  if (message.source_id() != message_handler_->source_id()) {
    // Ignore messages sent by a stranger.
    DVLOG(2) << "Ignoring message unexpectedly sent by source_id: \""
             << message.source_id() << "\" (expected \""
             << message_handler_->source_id() << "\")";
    return;
  }

  CHECK(message.has_payload_utf8());
  CHECK_EQ(message.protocol_version(),
           openscreen::cast::proto::CastMessage_ProtocolVersion_CASTV2_1_0);
  // TODO(crbug.com/375654306): Remove this message logging once general logging
  // can be toggled through WebUI.
  if (message.namespace_() == mirroring::mojom::kWebRtcNamespace) {
    logger_.get()->LogInfo(media_router::mojom::LogCategory::kMirroring,
                           kLoggerComponent,
                           base::StrCat({"Relaying app message from receiver:",
                                         message.payload_utf8()}),
                           route().media_sink_id(), route().media_source().id(),
                           route().presentation_id());
  }

  mirroring::mojom::CastMessagePtr ptr = mirroring::mojom::CastMessage::New();
  ptr->message_namespace = message.namespace_();
  ptr->json_format_data = message.payload_utf8();
  channel_to_service_->OnMessage(std::move(ptr));
}

void MirroringActivity::OnInternalMessage(
    const cast_channel::InternalMessage& message) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  if (!route_.is_local()) {
    return;
  }
  mirroring::mojom::CastMessagePtr ptr = mirroring::mojom::CastMessage::New();
  ptr->message_namespace = message.message_namespace;
  if (std::optional<std::string> json_data = base::WriteJson(message.message)) {
    ptr->json_format_data = *std::move(json_data);
  } else {
    // Should never fail to serialize JSON from an internal message.
    NOTREACHED();
  }
  // TODO(crbug.com/375654306): Remove this message logging once general logging
  // can be toggled through WebUI.
  if (message.message_namespace == mirroring::mojom::kWebRtcNamespace) {
    logger_.get()->LogInfo(
        media_router::mojom::LogCategory::kMirroring, kLoggerComponent,
        base::StrCat({"Relaying internal WebRTC message from receiver: ",
                      ptr->json_format_data}),
        route().media_sink_id(), route().media_source().id(),
        route().presentation_id());
  }
  channel_to_service_->OnMessage(std::move(ptr));
}

void MirroringActivity::BindMediaController(
    mojo::PendingReceiver<mojom::MediaController> media_controller,
    mojo::PendingRemote<mojom::MediaStatusObserver> observer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  media_controller_receivers_.Add(this, std::move(media_controller));
  media_status_observers_.Add(std::move(observer));
}

std::string MirroringActivity::GetRouteDescription(
    const CastSession& session) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  if (!mirroring_type_) {
    return CastActivity::GetRouteDescription(session);
  }
  switch (*mirroring_type_) {
    case MirroringType::kTab:
      return l10n_util::GetStringUTF8(IDS_MEDIA_ROUTER_CASTING_TAB);
    case MirroringType::kDesktop:
      return l10n_util::GetStringUTF8(IDS_MEDIA_ROUTER_CASTING_DESKTOP);
    case MirroringType::kOffscreenTab:
      return l10n_util::GetStringFUTF8(
          IDS_MEDIA_ROUTER_PRESENTATION_ROUTE_DESCRIPTION,
          base::UTF8ToUTF16(route().media_source().url().GetHost()));
  }
}

void MirroringActivity::HandleParseJsonResult(
    const std::string& route_id,
    const base::JSONReader::Result& result) {
  CastSession* session = GetSession();
  if (!session) {
    // TODO(crbug.com/41457011): If we're reaching here, determine why.
    logger_.get()->LogError(
        media_router::mojom::LogCategory::kMirroring, kLoggerComponent,
        base::StrCat({"Failed to retrieve the session."}),
        route().media_sink_id(), route().media_source().id(),
        route().presentation_id());
    return;
  }

  if (!result.has_value()) {
    // TODO(crbug.com/41426190): Record UMA metric for parse result.
    logger_.get()->LogError(
        media_router::mojom::LogCategory::kMirroring, kLoggerComponent,
        base::StrCat(
            {"Failed to parse Cast client message:", result.error().message}),
        route().media_sink_id(), route().media_source().id(),
        route().presentation_id());
    return;
  }

  const base::DictValue* dict = result->GetIfDict();
  if (!dict) {
    logger_.get()->LogError(
        media_router::mojom::LogCategory::kMirroring, kLoggerComponent,
        "Failed to parse Cast client message: Not a dictionary",
        route().media_sink_id(), route().media_source().id(),
        route().presentation_id());
    return;
  }

  const std::string_view message_namespace = GetMirroringNamespace(*dict);
  if (message_namespace == mirroring::mojom::kWebRtcNamespace) {
    logger_.get()->LogInfo(media_router::mojom::LogCategory::kMirroring,
                           kLoggerComponent,
                           base::StrCat({"WebRTC message received: ",
                                         GetScrubbedLogMessage(*dict)}),
                           route().media_sink_id(), route().media_source().id(),
                           route().presentation_id());
  }

  openscreen::cast::proto::CastMessage cast_message =
      cast_channel::CreateCastMessage(std::string(message_namespace), *result,
                                      message_handler_->source_id(),
                                      session->destination_id());
  if (message_handler_->SendCastMessage(cast_data_.cast_channel_id,
                                        cast_message) == Result::kFailed) {
    logger_.get()->LogError(
        media_router::mojom::LogCategory::kMirroring, kLoggerComponent,
        base::StringPrintf(
            "Failed to send Cast message to channel_id: %d, in namespace: %s",
            cast_data_.cast_channel_id, message_namespace),
        route().media_sink_id(), route().media_source().id(),
        route().presentation_id());
  }
}

void MirroringActivity::OnSessionSet(const CastSession& session) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  if (!mirroring_type_) {
    return;
  }
  will_start_mirroring_timestamp_ = base::Time::Now();
  // We use unretained here because weak pointers may be passed safely between
  // sequences, but must always be dereferenced and invalidated on the same
  // SequencedTaskRunner otherwise checking the pointer would be racey. See more
  // at base/memory/weak_ptr.h.
  debugger_.get()->ShouldFetchMirroringStats(
      base::BindOnce(&MirroringActivity::StartSession, base::Unretained(this),
                     session.destination_id()));
}
void MirroringActivity::StartSession(const std::string& destination_id,
                                     bool enable_rtcp_reporting) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  const auto cast_source =
      CastMediaSource::FromMediaSource(route_.media_source());
  CHECK(cast_source);

  // Derive session type by intersecting the sink capabilities with what the
  // media source can provide.
  const bool has_audio = cast_data_.capabilities.Has(
                             cast_channel::CastDeviceCapability::kAudioOut) &&
                         cast_source->ProvidesStreamingAudioCapture();
  const bool has_video = cast_data_.capabilities.Has(
      cast_channel::CastDeviceCapability::kVideoOut);
  if (!has_audio && !has_video) {
    return;
  }
  const SessionType session_type = has_audio && has_video
                                       ? SessionType::AUDIO_AND_VIDEO
                                   : has_audio ? SessionType::AUDIO_ONLY
                                               : SessionType::VIDEO_ONLY;

  // Bind Mojo receivers for the interfaces this object implements.
  mojo::PendingRemote<mirroring::mojom::SessionObserver> observer_remote;
  observer_receiver_.Bind(observer_remote.InitWithNewPipeAndPassReceiver());
  mojo::PendingRemote<mirroring::mojom::CastMessageChannel> channel_remote;
  channel_receiver_.Bind(channel_remote.InitWithNewPipeAndPassReceiver());

  // If the target playout delay has not yet been set (from site-initiated
  // mirroring request) then try to set it from a feature or commandline.
  target_playout_delay_ = cast_source->target_playout_delay().has_value()
                              ? cast_source->target_playout_delay()
                              : GetCastMirroringPlayoutDelay();
  should_fetch_stats_on_start_ = enable_rtcp_reporting;
  if (did_start_mirroring_timestamp_ && enable_rtcp_reporting) {
    ScheduleFetchMirroringStats();
  }

  // If this fails, it's probably because CreateMojoBindings() hasn't been
  // called.
  CHECK(channel_to_service_receiver_);
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(
          &MirroringActivity::StartOnUiThread, weak_ptr_factory_.GetWeakPtr(),
          SessionParameters::New(
              session_type, cast_data_.ip_endpoint.address(),
              sink_.sink().name(), destination_id,
              message_handler_->source_id(), target_playout_delay_,
              route().media_source().IsRemotePlaybackSource(),
              ShouldForceLetterboxing(cast_data_.model_name),
              enable_rtcp_reporting),
          std::move(observer_remote), std::move(channel_remote),
          std::move(channel_to_service_receiver_), route_.media_sink_name()));
}

void MirroringActivity::StartOnUiThread(
    mirroring::mojom::SessionParametersPtr session_params,
    mojo::PendingRemote<mirroring::mojom::SessionObserver> observer,
    mojo::PendingRemote<mirroring::mojom::CastMessageChannel> outbound_channel,
    mojo::PendingReceiver<mirroring::mojom::CastMessageChannel> inbound_channel,
    const std::string& sink_name) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(ui_sequence_checker_);
  CHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (!host_) {
    return;
  }

  host_->Start(std::move(session_params), std::move(observer),
               std::move(outbound_channel), std::move(inbound_channel),
               sink_name);
}

void MirroringActivity::StopMirroring() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  // Running the callback will cause this object to be deleted.
  if (on_stop_) {
    std::move(on_stop_).Run();
  }
}

std::string MirroringActivity::GetScrubbedLogMessage(
    const base::DictValue& message) {
  auto scrubbed_message = message.Clone();
  base::ListValue* streams =
      scrubbed_message.FindListByDottedPath("offer.supportedStreams");
  if (!streams) {
    return base::WriteJson(scrubbed_message).value_or("");
  }

  // An entry is "scrubbed" if the sensitive data that it contains should
  // be replaced with a preset string for privacy or security reasons.
  for (base::Value& item : *streams) {
    base::DictValue* dict = item.GetIfDict();
    if (!dict) {
      continue;
    }
    for (std::string_view key : {"aesKey", "aesIvMask"}) {
      if (std::string* match = dict->FindString(key)) {
        *match = "[REDACTED]";
      }
    }
  }
  return base::WriteJson(scrubbed_message).value_or("");
}

void MirroringActivity::ScheduleFetchMirroringStats() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  // When a mirroring route starts, create a mirroring stats fetch loop every
  // kRtcpReportInterval, which is the same interval that the logger will send
  // stats data.
  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&MirroringActivity::FetchMirroringStats,
                     weak_ptr_factory_.GetWeakPtr()),
      media::cast::kRtcpReportInterval);
}

void MirroringActivity::FetchMirroringStats() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  // Only fetch mirroring stats if our feature is still enabled AND if the
  // current mirroring route still exits.
  if (!should_fetch_stats_on_start_ || !host_) {
    return;
  }

  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&mirroring::MirroringServiceHost::GetMirroringStats,
                     host_->GetWeakPtr(),
                     base::BindPostTaskToCurrentDefault(
                         base::BindOnce(&MirroringActivity::OnMirroringStats,
                                        weak_ptr_factory_.GetWeakPtr()))));

  ScheduleFetchMirroringStats();
}

void MirroringActivity::OnMirroringStats(base::Value json_stats) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  debugger_.get()->OnMirroringStats(json_stats.Clone());
  if (json_stats.is_dict()) {
    most_recent_mirroring_stats_ = std::move(json_stats.GetDict());

    // Mirroring statistics are logged as we go, to enable piecing together a
    // timeline using the delta between each statistics instance in the logs.
    // However, only the final set of statistics is submitted to UMA.
    if (VLOG_IS_ON(2)) {
      if (std::optional<std::string> stats_json =
              base::WriteJson(most_recent_mirroring_stats_)) {
        VLOG(2) << __func__ << " most_recent_mirroring_stats_: " << *stats_json;
      }
    }
  }
}

void MirroringActivity::Play() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  if (host_) {
    base::OnceCallback<void()> cb = base::BindOnce(
        &MirroringActivity::SetPlayState, weak_ptr_factory_.GetWeakPtr(),
        mojom::MediaStatus::PlayState::PLAYING);
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(&mirroring::MirroringServiceHost::Resume,
                                  host_->GetWeakPtr(), std::move(cb)));
  }
}

void MirroringActivity::Pause() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(io_sequence_checker_);
  if (host_) {
    base::OnceCallback<void()> cb = base::BindOnce(
        &MirroringActivity::SetPlayState, weak_ptr_factory_.GetWeakPtr(),
        mojom::MediaStatus::PlayState::PAUSED);
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(&mirroring::MirroringServiceHost::Pause,
                                  host_->GetWeakPtr(), std::move(cb)));
  }
}

void MirroringActivity::SetPlayState(mojom::MediaStatus::PlayState play_state) {
  media_status_->play_state = play_state;
  if (play_state == mojom::MediaStatus::PlayState::PLAYING) {
    OnMirroringResumed();
  } else if (play_state == mojom::MediaStatus::PlayState::PAUSED) {
    OnMirroringPaused();
  }
  NotifyMediaStatusObservers();
}

void MirroringActivity::NotifyMediaStatusObservers() {
  for (const mojo::Remote<mojom::MediaStatusObserver>& observer :
       media_status_observers_) {
    observer->OnMediaStatusUpdated(media_status_.Clone());
  }
}

void MirroringActivity::OnMirroringPaused() {
  // Do nothing if we are already paused.
  if (mirroring_pause_timestamp_) {
    return;
  }
  mirroring_pause_timestamp_ = base::Time::Now();
  mirroring_pause_count_++;
}

void MirroringActivity::OnMirroringResumed() {
  if (!mirroring_pause_timestamp_) {
    return;
  }
  AccessCodeCastMetrics::RecordMirroringPauseDuration(
      base::Time::Now() - mirroring_pause_timestamp_.value());
  mirroring_pause_timestamp_.reset();
}

}  // namespace media_router
