// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/renderer_host/media/video_capture_manager.h"

#include <algorithm>
#include <memory>
#include <set>
#include <utility>

#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/observer_list.h"
#include "base/strings/stringprintf.h"
#include "base/task/bind_post_task.h"
#include "base/task/single_thread_task_runner.h"
#include "base/timer/elapsed_timer.h"
#include "base/trace_event/trace_event.h"
#include "build/build_config.h"
#include "content/browser/media/media_internals.h"
#include "content/browser/renderer_host/media/video_capture_controller.h"
#include "content/browser/screenlock_monitor/screenlock_monitor.h"
#include "content/common/features.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/desktop_media_id.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "media/base/media_switches.h"
#include "media/base/video_facing.h"
#include "media/capture/mojom/video_capture_types.mojom.h"
#include "media/capture/video/video_capture_device.h"
#include "third_party/blink/public/common/mediastream/media_stream_request.h"

namespace {

// Test feature that simulates a hardware limitation where starting a second
// display capture stream automatically stops the first one.
// TODO(crbug.com/485200165): Remove this once testing is completed and the bug
// is fixed.
BASE_FEATURE(kVideoCaptureManagerStopFirstDisplayCaptureAfterSecondStarts,
             base::FEATURE_DISABLED_BY_DEFAULT);

void LogVideoCaptureError(media::VideoCaptureError error) {
  base::UmaHistogramEnumeration("Media.VideoCapture.Error", error);
}

const base::UnguessableToken& FakeSessionId() {
  // TODO(crbug.com/40252973): Investigate whether there's a better way
  // to accomplish this (without using UnguessableToken::Deserialize).
  static const base::UnguessableToken fake_session_id(
      base::UnguessableToken::Deserialize(0xFFFFFFFFFFFFFFFFU,
                                          0xFFFFFFFFFFFFFFFFU)
          .value());
  return fake_session_id;
}

}  // namespace

namespace content {

// Class used for queuing request for starting a device.
class VideoCaptureManager::CaptureDeviceStartRequest {
 public:
  CaptureDeviceStartRequest(scoped_refptr<VideoCaptureController> controller,
                            const media::VideoCaptureSessionId& session_id,
                            const media::VideoCaptureParams& params);
  scoped_refptr<VideoCaptureController> controller() const {
    return controller_;
  }
  const base::UnguessableToken& session_id() const { return session_id_; }
  media::VideoCaptureParams params() const { return params_; }

 private:
  const scoped_refptr<VideoCaptureController> controller_;
  const base::UnguessableToken session_id_;
  const media::VideoCaptureParams params_;
};

VideoCaptureManager::CaptureDeviceStartRequest::CaptureDeviceStartRequest(
    scoped_refptr<VideoCaptureController> controller,
    const media::VideoCaptureSessionId& session_id,
    const media::VideoCaptureParams& params)
    : controller_(std::move(controller)),
      session_id_(session_id),
      params_(params) {}

VideoCaptureManager::VideoCaptureManager(
    std::unique_ptr<VideoCaptureProvider> video_capture_provider,
    base::RepeatingCallback<void(const std::string&)> emit_log_message_cb)
    : video_capture_provider_(std::move(video_capture_provider)),
      emit_log_message_cb_(std::move(emit_log_message_cb)) {
  ScreenlockMonitor* screenlock_monitor = ScreenlockMonitor::Get();
  if (screenlock_monitor) {
    screenlock_monitor->AddObserver(this);
  }
}

VideoCaptureManager::~VideoCaptureManager() {
  CHECK(device_start_request_queue_.empty(), base::NotFatalUntil::M158);
  ScreenlockMonitor* screenlock_monitor = ScreenlockMonitor::Get();
  if (screenlock_monitor) {
    screenlock_monitor->RemoveObserver(this);
  }
}

void VideoCaptureManager::AddVideoCaptureObserver(
    media::VideoCaptureObserver* observer) {
  CHECK(observer, base::NotFatalUntil::M158);
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  capture_observers_.AddObserver(observer);
}

void VideoCaptureManager::RemoveAllVideoCaptureObservers() {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  capture_observers_.Clear();
}

void VideoCaptureManager::RegisterListener(
    MediaStreamProviderListener* listener) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  CHECK(listener, base::NotFatalUntil::M158);
  listeners_.AddObserver(listener);
#if BUILDFLAG(IS_ANDROID)
  // When kAndroidEnableBackgroundMediaCapturing is enabled, video capture
  // is allowed to continue even if the app is in the background.
  // Therefore, we only need to register the ApplicationStatusListener and
  // track foreground/background state if this feature is DISABLED,
  // ensuring that capture is stopped when the app is no longer active.
  if (!base::FeatureList::IsEnabled(
          media::kAndroidEnableBackgroundMediaCapturing)) {
    application_state_has_running_activities_ = true;
    app_status_listener_ =
        base::android::ApplicationStatusListener::New(base::BindRepeating(
            &VideoCaptureManager::OnApplicationStateChange, this));
  }
#endif
}

void VideoCaptureManager::UnregisterListener(
    MediaStreamProviderListener* listener) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  listeners_.RemoveObserver(listener);
}

void VideoCaptureManager::EnumerateDevices(
    EnumerationCallback client_callback) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("video_and_image_capture"),
               "VideoCaptureManager::EnumerateDevices");
  EmitLogMessage("VideoCaptureManager::EnumerateDevices", 1);

  // Pass a timer for UMA histogram collection.
  video_capture_provider_->GetDeviceInfosAsync(
      base::BindPostTaskToCurrentDefault(
          base::BindOnce(&VideoCaptureManager::OnDeviceInfosReceived, this,
                         base::ElapsedTimer(), std::move(client_callback))));
}

base::UnguessableToken VideoCaptureManager::Open(
    const blink::MediaStreamDevice& device) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("video_and_image_capture"),
               "VideoCaptureManager::Open");

  // Generate a new id for the session being opened.
  const base::UnguessableToken capture_session_id =
      base::UnguessableToken::Create();

  CHECK(sessions_.find(capture_session_id) == sessions_.end(),
        base::NotFatalUntil::M158);
  std::ostringstream string_stream;
  string_stream << "VideoCaptureManager::Open, device.name = " << device.name
                << ", device.id = " << device.id
                << ", capture_session_id = " << capture_session_id;
  EmitLogMessage(string_stream.str(), 1);

  // We just save the stream info for processing later.
  sessions_[capture_session_id] = device;

  // Notify our listener asynchronously; this ensures that we return
  // |capture_session_id| to the caller of this function before using that
  // same id in a listener event.
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(&VideoCaptureManager::OnOpened, this,
                                device.type, capture_session_id));
  return capture_session_id;
}

void VideoCaptureManager::Close(
    const base::UnguessableToken& capture_session_id) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("video_and_image_capture"),
               "VideoCaptureManager::Close");

  std::ostringstream string_stream;
  string_stream << "VideoCaptureManager::Close, capture_session_id = "
                << capture_session_id;
  EmitLogMessage(string_stream.str(), 1);

  auto session_it = sessions_.find(capture_session_id);
  if (session_it == sessions_.end()) {
    return;
  }

  if (base::FeatureList::IsEnabled(
          kVideoCaptureManagerStopFirstDisplayCaptureAfterSecondStarts)) {
    std::erase(display_capture_session_ids_, capture_session_id);
  }

  VideoCaptureController* const existing_device =
      LookupControllerByMediaTypeAndDeviceId(session_it->second.type,
                                             session_it->second.id);
  if (existing_device) {
    // Remove any client that is still using the session. This is safe to call
    // even if there are no clients using the session.
    existing_device->StopSession(capture_session_id);

    // StopSession() may have removed the last client, so we might need to
    // close the device.
    DestroyControllerIfNoClients(capture_session_id, existing_device);
  }

  // Notify listeners asynchronously, and forget the session.
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(&VideoCaptureManager::OnClosed, this,
                                session_it->second.type, capture_session_id));

  if (blink::IsDeviceMediaType(session_it->second.type)) {
    auto locked_it = locked_sessions_.find(session_it->first);
    const bool was_locked = locked_it != locked_sessions_.end();
    if (was_locked)
      locked_sessions_.erase(locked_it);
    if (locked_sessions_.empty()) {
      idle_close_timer_.Stop();
    }
  } else {
    CHECK(!locked_sessions_.contains(session_it->first),
          base::NotFatalUntil::M158);
  }
  sessions_.erase(session_it);
}

void VideoCaptureManager::ApplySubCaptureTarget(
    const base::UnguessableToken& session_id,
    media::mojom::SubCaptureTargetType type,
    const base::Token& target,
    uint32_t sub_capture_target_version,
    base::OnceCallback<void(media::mojom::ApplySubCaptureTargetResult)>
        callback) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);

  VideoCaptureController* const controller =
      LookupControllerBySessionId(session_id);
  if (!controller || !controller->IsDeviceAlive()) {
    std::move(callback).Run(
        media::mojom::ApplySubCaptureTargetResult::kErrorGeneric);
    return;
  }
  controller->ApplySubCaptureTarget(type, target, sub_capture_target_version,
                                    std::move(callback));
}

void VideoCaptureManager::QueueStartDevice(
    const media::VideoCaptureSessionId& session_id,
    scoped_refptr<VideoCaptureController> controller,
    const media::VideoCaptureParams& params) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  device_start_request_queue_.push_back(
      CaptureDeviceStartRequest(std::move(controller), session_id, params));
  if (device_start_request_queue_.size() == 1)
    ProcessDeviceStartRequestQueue();
}

void VideoCaptureManager::DoStopDevice(VideoCaptureController* controller) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("video_and_image_capture"),
               "VideoCaptureManager::DoStopDevice");
  CHECK(std::ranges::contains(controllers_, controller),
        base::NotFatalUntil::M158);

  // If start request has not yet started processing, i.e. if it is not at the
  // beginning of the queue, remove it from the queue.
  if (!device_start_request_queue_.empty()) {
    auto second_request = std::next(device_start_request_queue_.begin());

    for (auto it = second_request; it != device_start_request_queue_.end();) {
      if (it->controller() == controller)
        it = device_start_request_queue_.erase(it);
      else
        ++it;
    }
  }

  const media::VideoCaptureDeviceInfo* device_info =
      GetDeviceInfoById(controller->device_id());
  if (device_info != nullptr) {
    for (auto& observer : capture_observers_)
      observer.OnVideoCaptureStopped(device_info->descriptor.facing);
  }

  // Since we may be removing |controller| from |controllers_| while
  // ReleaseDeviceAsnyc() is executing, we pass it shared ownership to
  // |controller|.
  controller->ReleaseDeviceAsync(
      base::DoNothingWithBoundArgs(GetControllerSharedRef(controller)));
}

void VideoCaptureManager::ProcessDeviceStartRequestQueue() {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("video_and_image_capture"),
               "VideoCaptureManager::ProcessDeviceStartRequestQueue");
  auto request = device_start_request_queue_.begin();
  if (request == device_start_request_queue_.end())
    return;

  scoped_refptr<VideoCaptureController> const controller =
      request->controller();

  EmitLogMessage("VideoCaptureManager::ProcessDeviceStartRequestQueue", 3);
  // The unit test VideoCaptureManagerTest.OpenNotExisting requires us to fail
  // synchronously if the stream_type is MEDIA_DEVICE_VIDEO_CAPTURE and no
  // DeviceInfo matching the requested id is present (which is the case when
  // requesting a device with a bogus id). Note, that since other types of
  // failure during startup of the device are allowed to be reported
  // asynchronously, this requirement is questionable.
  // TODO(chfremer): Check if any production code actually depends on this
  // requirement. If not, relax the requirement in the test and remove the below
  // if block. See crbug.com/708251
  if (controller->stream_type() ==
      blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE) {
    const media::VideoCaptureDeviceInfo* device_info =
        GetDeviceInfoById(controller->device_id());
    if (!device_info) {
      OnDeviceLaunchFailed(
          controller.get(),
          media::VideoCaptureError::
              kVideoCaptureManagerProcessDeviceStartQueueDeviceInfoNotFound);
      return;
    }
    for (auto& observer : capture_observers_)
      observer.OnVideoCaptureStarted(device_info->descriptor.facing);
  }

  // The method CreateAndStartDeviceAsync() is going to run asynchronously.
  // Since we may be removing the controller while it is executing, we need to
  // pass it shared ownership to itself so that it stays alive while executing.
  // And since the execution may make callbacks into |this|, we also need
  // to pass it shared ownership to |this|.
  // TODO(chfremer): Check if request->params() can actually be different from
  // controller->parameters, and simplify if this is not the case.
  controller->CreateAndStartDeviceAsync(
      request->params(), static_cast<VideoCaptureDeviceLaunchObserver*>(this),
      base::BindOnce([](scoped_refptr<VideoCaptureManager>,
                        scoped_refptr<VideoCaptureController>) {},
                     scoped_refptr<VideoCaptureManager>(this),
                     std::move(controller)));
}

void VideoCaptureManager::OnDeviceLaunched(VideoCaptureController* controller) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  std::ostringstream string_stream;
  string_stream << "Launching device has succeeded. device_id = "
                << controller->device_id();
  EmitLogMessage(string_stream.str(), 1);
  CHECK(!device_start_request_queue_.empty(), base::NotFatalUntil::M158);
  CHECK_EQ(controller, device_start_request_queue_.begin()->controller(),
           base::NotFatalUntil::M158);
  CHECK(controller, base::NotFatalUntil::M158);

  // Test feature that simulates a hardware limitation where starting a second
  // display capture stream automatically stops the first one.
  // TODO(crbug.com/485200165): Remove this once testing is completed and the
  // bug is fixed.
  if (base::FeatureList::IsEnabled(
          kVideoCaptureManagerStopFirstDisplayCaptureAfterSecondStarts) &&
      controller->stream_type() ==
          blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE) {
    const media::VideoCaptureSessionId session_id =
        device_start_request_queue_.front().session_id();
    if (!display_capture_session_ids_.empty()) {
      const base::UnguessableToken first_session_id =
          display_capture_session_ids_.front();
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
          FROM_HERE,
          base::BindOnce(&VideoCaptureManager::Close,
                         weak_factory_.GetWeakPtr(), first_session_id),
          base::Milliseconds(30));
    }
    display_capture_session_ids_.push_back(session_id);
  }

  if (blink::IsVideoDesktopCaptureMediaType(controller->stream_type())) {
    const media::VideoCaptureSessionId session_id =
        device_start_request_queue_.front().session_id();
    CHECK_NE(session_id, FakeSessionId(), base::NotFatalUntil::M158);
    MaybePostDesktopCaptureWindowId(session_id);
  }

  auto it = photo_request_queue_.begin();
  while (it != photo_request_queue_.end()) {
    VideoCaptureController* maybe_entry =
        LookupControllerBySessionId(it->first);
    if (maybe_entry && maybe_entry->IsDeviceAlive()) {
      std::move(it->second).Run();
      it = photo_request_queue_.erase(it);
    } else {
      ++it;
    }
  }

  device_start_request_queue_.pop_front();
  ProcessDeviceStartRequestQueue();
}

void VideoCaptureManager::OnDeviceLaunchFailed(
    VideoCaptureController* controller,
    media::VideoCaptureError error) {
  std::ostringstream string_stream;
  string_stream << "Launching device has failed. device_id = "
                << controller->device_id();
  EmitLogMessage(string_stream.str(), 1);
  controller->OnError(error);

  device_start_request_queue_.pop_front();
  ProcessDeviceStartRequestQueue();
}

void VideoCaptureManager::OnDeviceLaunchAborted() {
  EmitLogMessage("Launching device has been aborted.", 1);
  device_start_request_queue_.pop_front();
  ProcessDeviceStartRequestQueue();
}

void VideoCaptureManager::OnDeviceConnectionLost(
    VideoCaptureController* controller) {
  std::ostringstream string_stream;
  string_stream << "Lost connection to device. device_id = "
                << controller->device_id();
  EmitLogMessage(string_stream.str(), 1);
  controller->OnError(
      media::VideoCaptureError::kVideoCaptureManagerDeviceConnectionLost);
}

void VideoCaptureManager::OpenNativeScreenCapturePicker(
    DesktopMediaID::Type type,
    base::OnceCallback<void(DesktopMediaID::Id)> created_callback,
    base::OnceCallback<void(webrtc::DesktopCapturer::Source)> picker_callback,
    base::OnceCallback<void()> cancel_callback,
    base::OnceCallback<void()> error_callback,
    base::OnceCallback<void(DesktopMediaID::Id)> stop_audio_callback) {
  video_capture_provider_->OpenNativeScreenCapturePicker(
      type, std::move(created_callback), std::move(picker_callback),
      std::move(cancel_callback), std::move(error_callback),
      std::move(stop_audio_callback));
}

void VideoCaptureManager::CloseNativeScreenCapturePicker(
    DesktopMediaID device_id) {
  video_capture_provider_->CloseNativeScreenCapturePicker(device_id);
}

#if BUILDFLAG(IS_MAC)
void VideoCaptureManager::GetApplicationAudioCaptureId(
    DesktopMediaID::Id session_id,
    base::OnceCallback<
        void(const std::optional<desktop_capture::ApplicationAudioCaptureId>&)>
        callback) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  video_capture_provider_->GetApplicationAudioCaptureId(session_id,
                                                        std::move(callback));
}
#endif

void VideoCaptureManager::ConnectClient(
    const media::VideoCaptureSessionId& session_id,
    const media::VideoCaptureParams& params,
    VideoCaptureControllerID client_id,
    const GlobalRenderFrameHostId& render_frame_host_id,
    VideoCaptureControllerEventHandler* client_handler,
    std::optional<url::Origin> origin,
    bool is_allowed_on_lock_screen,
    DoneCB done_cb) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("video_and_image_capture"),
               "VideoCaptureManager::ConnectClient");
  {
    std::ostringstream string_stream;
    string_stream << "ConnectClient: session_id = " << session_id
                  << ", request: "
                  << media::VideoCaptureFormat::ToString(
                         params.requested_format);
    EmitLogMessage(string_stream.str(), 1);
  }

  scoped_refptr<VideoCaptureController> controller =
      GetOrCreateController(session_id, params);
  if (!controller) {
    std::move(done_cb).Run(nullptr);
    return;
  }

  // New sessions can't be started from the lock screen unless authorized. It'd
  // be nice to defer these, but the interplay authorized and non-authorized
  // devices is complicated.
  if (is_screen_locked_ && !is_allowed_on_lock_screen) {
    std::move(done_cb).Run(nullptr);
    return;
  }

  bool client_exist =
      controller->HasActiveClient() || controller->HasPausedClient();
  base::UmaHistogramBoolean("Media.VideoCapture.StreamShared", client_exist);
  if (client_exist) {
    std::optional<url::Origin> first_client_origin =
        controller->GetFirstClientOrigin();
    bool same_origin = first_client_origin.has_value() && origin.has_value() &&
                       *first_client_origin == *origin;
    base::UmaHistogramBoolean("Media.VideoCapture.StreamSharedSameOrigin",
                              same_origin);
  }

  // First client starts the device.
  if (!client_exist) {
    std::ostringstream string_stream;
    string_stream
        << "VideoCaptureManager queueing device start for device_id = "
        << controller->device_id();
    EmitLogMessage(string_stream.str(), 1);
    QueueStartDevice(session_id, controller, params);
  }

  // Run the callback first, as AddClient() may trigger OnFrameInfo().
  std::move(done_cb).Run(controller->GetWeakPtrForIOThread());
  controller->AddClient(client_id, render_frame_host_id, client_handler,
                        session_id, params, origin);
}

void VideoCaptureManager::DisconnectClient(
    VideoCaptureController* controller,
    VideoCaptureControllerID client_id,
    VideoCaptureControllerEventHandler* client_handler,
    media::VideoCaptureError error) {
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("video_and_image_capture"),
               "VideoCaptureManager::DisconnectClient");
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  CHECK(controller, base::NotFatalUntil::M158);
  CHECK(client_handler, base::NotFatalUntil::M158);
  CHECK(IsControllerPointerValid(controller));

  if (error != media::VideoCaptureError::kNone) {
    LogVideoCaptureError(error);
    std::ostringstream string_stream;
    string_stream << "Video capture session stopped with error code "
                  << static_cast<int>(error);
    EmitLogMessage(string_stream.str(), 1);
    for (auto it : sessions_) {
      if (it.second.type == controller->stream_type() &&
          it.second.id == controller->device_id()) {
        for (auto& listener : listeners_)
          listener.Aborted(it.second.type, it.first);
        // Aborted() call might synchronously destroy |controller|, recheck.
        if (!IsControllerPointerValid(controller))
          return;
        break;
      }
    }
  }

  // Detach client from controller.
  const media::VideoCaptureSessionId session_id =
      controller->RemoveClient(client_id, client_handler);

  if (error == media::VideoCaptureError::kNone &&
      (controller->HasActiveClient() || controller->HasPausedClient())) {
    // Rotate buffers to prevent cross-renderer information leaks if the
    // renderer was compromised and retained buffer handles.
    // There is no need to invalidate buffers if the controller is being
    // destroyed, since the device and its buffer pool will be destroyed anyway.
    InvalidateBuffersForClient(controller);
  }
  std::ostringstream string_stream;
  string_stream << "DisconnectClient: session_id = " << session_id;
  EmitLogMessage(string_stream.str(), 1);

  // If controller has no more clients, delete controller and device.
  DestroyControllerIfNoClients(session_id, controller);
}

void VideoCaptureManager::PauseCaptureForClient(
    VideoCaptureController* controller,
    VideoCaptureControllerID client_id,
    VideoCaptureControllerEventHandler* client_handler) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  CHECK(controller, base::NotFatalUntil::M158);
  CHECK(client_handler, base::NotFatalUntil::M158);
  if (!IsControllerPointerValid(controller)) {
    NOTREACHED() << "Got Null controller while pausing capture";
  }

  const bool had_active_client = controller->HasActiveClient();
  controller->PauseClient(client_id, client_handler);
  if (!had_active_client || controller->HasActiveClient())
    return;
  if (!controller->IsDeviceAlive())
    return;
  controller->MaybeSuspend();
}

void VideoCaptureManager::ResumeCaptureForClient(
    const media::VideoCaptureSessionId& session_id,
    const media::VideoCaptureParams& params,
    VideoCaptureController* controller,
    VideoCaptureControllerID client_id,
    VideoCaptureControllerEventHandler* client_handler) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  CHECK(controller, base::NotFatalUntil::M158);
  CHECK(client_handler, base::NotFatalUntil::M158);

  if (!IsControllerPointerValid(controller)) {
    NOTREACHED() << "Got Null controller while resuming capture";
  }

  if (!controller->ResumeClient(client_id, client_handler)) {
    return;
  }
  if (!controller->IsDeviceAlive())
    return;
  controller->Resume();
}

void VideoCaptureManager::RequestRefreshFrameForClient(
    VideoCaptureController* controller) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);

  if (IsControllerPointerValid(controller)) {
    if (!controller->IsDeviceAlive())
      return;
    controller->RequestRefreshFrame();
  }
}

void VideoCaptureManager::InvalidateBuffersForClient(
    VideoCaptureController* controller) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  if (IsControllerPointerValid(controller)) {
    if (!controller->IsDeviceAlive()) {
      return;
    }
    controller->InvalidateBuffers();
  }
}

bool VideoCaptureManager::GetDeviceSupportedFormats(
    const media::VideoCaptureSessionId& capture_session_id,
    media::VideoCaptureFormats* supported_formats) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  CHECK(supported_formats->empty(), base::NotFatalUntil::M158);

  auto it = sessions_.find(capture_session_id);
  if (it == sessions_.end())
    return false;
  std::ostringstream string_stream;
  string_stream << "GetDeviceSupportedFormats for device: " << it->second.name;
  EmitLogMessage(string_stream.str(), 1);

  return GetDeviceSupportedFormats(it->second.id, supported_formats);
}

bool VideoCaptureManager::GetDeviceSupportedFormats(
    const std::string& device_id,
    media::VideoCaptureFormats* supported_formats) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  CHECK(supported_formats->empty(), base::NotFatalUntil::M158);

  // Return all available formats of the device, regardless its started state.
  media::VideoCaptureDeviceInfo* existing_device = GetDeviceInfoById(device_id);
  if (existing_device)
    *supported_formats = existing_device->supported_formats;
  return true;
}

bool VideoCaptureManager::GetDeviceFormatsInUse(
    const media::VideoCaptureSessionId& capture_session_id,
    media::VideoCaptureFormats* formats_in_use) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  CHECK(formats_in_use->empty(), base::NotFatalUntil::M158);

  auto it = sessions_.find(capture_session_id);
  if (it == sessions_.end())
    return false;
  std::ostringstream string_stream;
  string_stream << "GetDeviceFormatsInUse for device: " << it->second.name;
  EmitLogMessage(string_stream.str(), 1);

  std::optional<media::VideoCaptureFormat> format =
      GetDeviceFormatInUse(it->second.type, it->second.id);
  if (format.has_value())
    formats_in_use->push_back(format.value());

  return true;
}

std::optional<media::VideoCaptureFormat>
VideoCaptureManager::GetDeviceFormatInUse(
    blink::mojom::MediaStreamType stream_type,
    const std::string& device_id) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  // Return the currently in-use format of the device, if it's started.
  VideoCaptureController* device_in_use =
      LookupControllerByMediaTypeAndDeviceId(stream_type, device_id);
  return device_in_use ? device_in_use->GetVideoCaptureFormat() : std::nullopt;
}

GlobalRenderFrameHostId VideoCaptureManager::GetGlobalRenderFrameHostId(
    const base::UnguessableToken& session_id) const {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);

  VideoCaptureController* const controller =
      LookupControllerBySessionId(session_id);
  if (!controller || !controller->IsDeviceAlive() ||
      !blink::IsVideoDesktopCaptureMediaType(controller->stream_type())) {
    return GlobalRenderFrameHostId();
  }

  const DesktopMediaID desktop_media_id =
      DesktopMediaID::Parse(controller->device_id());

  if (desktop_media_id.type != DesktopMediaID::Type::TYPE_WEB_CONTENTS ||
      desktop_media_id.web_contents_id.is_null()) {
    return GlobalRenderFrameHostId();
  }

  return GlobalRenderFrameHostId(
      desktop_media_id.web_contents_id.render_process_id,
      desktop_media_id.web_contents_id.main_render_frame_id);
}

void VideoCaptureManager::SetDesktopCaptureWindowId(
    const media::VideoCaptureSessionId& session_id,
    gfx::NativeViewId window_id) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  VLOG(2) << "SetDesktopCaptureWindowId called for session " << session_id;

  notification_window_ids_[session_id] = window_id;
  MaybePostDesktopCaptureWindowId(session_id);

  if (set_desktop_capture_window_id_callback_for_testing_) {
    set_desktop_capture_window_id_callback_for_testing_.Run(session_id,
                                                            window_id);
  }
}

void VideoCaptureManager::MaybePostDesktopCaptureWindowId(
    const media::VideoCaptureSessionId& session_id) {
  auto session_it = sessions_.find(session_id);
  if (session_it == sessions_.end())
    return;

  VideoCaptureController* const existing_device =
      LookupControllerByMediaTypeAndDeviceId(session_it->second.type,
                                             session_it->second.id);
  if (!existing_device) {
    DVLOG(2) << "Failed to find an existing screen capture device.";
    return;
  }

  if (!existing_device->IsDeviceAlive()) {
    DVLOG(2) << "Screen capture device not yet started.";
    return;
  }

  CHECK(blink::IsVideoDesktopCaptureMediaType(existing_device->stream_type()),
        base::NotFatalUntil::M158);
  DesktopMediaID id = DesktopMediaID::Parse(existing_device->device_id());
  if (id.is_null())
    return;

  auto window_id_it = notification_window_ids_.find(session_id);
  if (window_id_it == notification_window_ids_.end()) {
    DVLOG(2) << "Notification window id not set for screen capture.";
    return;
  }

  existing_device->SetDesktopCaptureWindowIdAsync(
      window_id_it->second,
      base::DoNothingWithBoundArgs(scoped_refptr<VideoCaptureManager>(this)));
  notification_window_ids_.erase(window_id_it);
}

void VideoCaptureManager::GetPhotoState(
    const base::UnguessableToken& session_id,
    media::VideoCaptureDevice::GetPhotoStateCallback callback) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);

  VideoCaptureController* controller = LookupControllerBySessionId(session_id);
  if (!controller)
    return;
  if (controller->IsDeviceAlive()) {
    controller->GetPhotoState(std::move(callback));
    return;
  }
  // Queue up a request for later.
  photo_request_queue_.emplace_back(
      session_id,
      base::BindOnce(&VideoCaptureController::GetPhotoState,
                     controller->GetWeakPtrForIOThread(), std::move(callback)));
}

void VideoCaptureManager::SetPhotoOptions(
    const base::UnguessableToken& session_id,
    media::mojom::PhotoSettingsPtr settings,
    media::VideoCaptureDevice::SetPhotoOptionsCallback callback) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);

  VideoCaptureController* controller = LookupControllerBySessionId(session_id);
  if (!controller)
    return;
  if (controller->IsDeviceAlive()) {
    controller->SetPhotoOptions(std::move(settings), std::move(callback));
    return;
  }
  // Queue up a request for later.
  photo_request_queue_.emplace_back(
      session_id, base::BindOnce(&VideoCaptureController::SetPhotoOptions,
                                 controller->GetWeakPtrForIOThread(),
                                 std::move(settings), std::move(callback)));
}

void VideoCaptureManager::TakePhoto(
    const base::UnguessableToken& session_id,
    media::VideoCaptureDevice::TakePhotoCallback callback) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("video_and_image_capture"),
               "VideoCaptureManager::TakePhoto");

  VideoCaptureController* controller = LookupControllerBySessionId(session_id);
  if (!controller)
    return;
  if (controller->IsDeviceAlive()) {
    controller->TakePhoto(std::move(callback));
    return;
  }
  // Queue up a request for later.
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("video_and_image_capture"),
               "VideoCaptureManager::TakePhoto enqueuing request");
  photo_request_queue_.emplace_back(
      session_id,
      base::BindOnce(&VideoCaptureController::TakePhoto,
                     controller->GetWeakPtrForIOThread(), std::move(callback)));
}

void VideoCaptureManager::OnOpened(
    blink::mojom::MediaStreamType stream_type,
    const media::VideoCaptureSessionId& capture_session_id) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  for (auto& listener : listeners_)
    listener.Opened(stream_type, capture_session_id);
}

void VideoCaptureManager::OnClosed(
    blink::mojom::MediaStreamType stream_type,
    const media::VideoCaptureSessionId& capture_session_id) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  for (auto& listener : listeners_)
    listener.Closed(stream_type, capture_session_id);
}

void VideoCaptureManager::OnDeviceInfosReceived(
    base::ElapsedTimer timer,
    EnumerationCallback client_callback,
    media::mojom::DeviceEnumerationResult error_code,
    const std::vector<media::VideoCaptureDeviceInfo>& device_infos) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("video_and_image_capture"),
               "VideoCaptureManager::OnDeviceInfosReceived");


  if (error_code != media::mojom::DeviceEnumerationResult::kSuccess) {
    EmitLogMessage(
        base::StringPrintf("VideoCaptureManager::OnDeviceInfosReceived: Failed "
                           "to list device infos with error_code %d",
                           static_cast<int>(error_code)),
        0);
    std::move(client_callback).Run(error_code, {});
    return;
  }

  devices_info_cache_ = device_infos;

  std::ostringstream string_stream;
  string_stream << "VideoCaptureManager::OnDeviceInfosReceived: Recevied "
                << device_infos.size() << " device infos.";
  for (const auto& entry : device_infos) {
    string_stream << std::endl
                  << "device_id: " << entry.descriptor.device_id
                  << ", display_name: " << entry.descriptor.display_name();
  }
  EmitLogMessage(string_stream.str(), 1);

  // Walk the |devices_info_cache_| and produce a
  // media::VideoCaptureDeviceDescriptors for |client_callback|.
  media::VideoCaptureDeviceDescriptors devices;
  std::vector<std::tuple<media::VideoCaptureDeviceDescriptor,
                         media::VideoCaptureFormats>>
      descriptors_and_formats;
  for (const auto& it : devices_info_cache_) {
    devices.emplace_back(it.descriptor);
    descriptors_and_formats.emplace_back(it.descriptor, it.supported_formats);
    MediaInternals::GetInstance()->UpdateVideoCaptureDeviceCapabilities(
        descriptors_and_formats);
  }

  std::move(client_callback).Run(error_code, devices);
}

void VideoCaptureManager::DestroyControllerIfNoClients(
    const base::UnguessableToken& capture_session_id,
    VideoCaptureController* controller) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  // Removal of the last client stops the device.
  if (!controller->HasActiveClient() && !controller->HasPausedClient()) {
    std::ostringstream string_stream;
    string_stream << "VideoCaptureManager stopping device (stream_type = "
                  << controller->stream_type()
                  << ", device_id = " << controller->device_id() << ")";
    EmitLogMessage(string_stream.str(), 1);

    // Close the native OS picker as the associated VideoCaptureDevice is being
    // closed.
    CloseNativeScreenCapturePicker(
        DesktopMediaID::Parse(controller->device_id()));

    // The VideoCaptureController is removed from |controllers_| immediately.
    // The controller is deleted immediately, and the device is freed
    // asynchronously. After this point, subsequent requests to open this same
    // device ID will create a new VideoCaptureController,
    // VideoCaptureController, and VideoCaptureDevice.
    DoStopDevice(controller);
    // TODO(mcasas): use a helper function https://crbug.com/624854.
    auto controller_iter = std::ranges::find(
        controllers_, controller, &scoped_refptr<VideoCaptureController>::get);
    controllers_.erase(controller_iter);
    // Check if there are any associated pending callbacks and delete them.
    base::EraseIf(photo_request_queue_,
                  [capture_session_id](const auto& request) {
                    return request.first == capture_session_id;
                  });
  }
}

VideoCaptureController* VideoCaptureManager::LookupControllerBySessionId(
    const base::UnguessableToken& session_id) const {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  SessionMap::const_iterator session_it = sessions_.find(session_id);
  if (session_it == sessions_.end())
    return nullptr;

  return LookupControllerByMediaTypeAndDeviceId(session_it->second.type,
                                                session_it->second.id);
}

VideoCaptureController*
VideoCaptureManager::LookupControllerByMediaTypeAndDeviceId(
    blink::mojom::MediaStreamType type,
    const std::string& device_id) const {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);

  for (const auto& entry : controllers_) {
    if (type == entry->stream_type() && device_id == entry->device_id())
      return entry.get();
  }
  return nullptr;
}

bool VideoCaptureManager::IsControllerPointerValid(
    const VideoCaptureController* controller) const {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  return std::ranges::contains(controllers_, controller,
                               &scoped_refptr<VideoCaptureController>::get);
}

scoped_refptr<VideoCaptureController>
VideoCaptureManager::GetControllerSharedRef(
    VideoCaptureController* controller) const {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);

  for (const auto& entry : controllers_) {
    if (entry.get() == controller)
      return entry;
  }
  return nullptr;
}

media::VideoCaptureDeviceInfo* VideoCaptureManager::GetDeviceInfoById(
    const std::string& id) {
  for (auto& it : devices_info_cache_) {
    if (it.descriptor.device_id == id)
      return &it;
  }
  return nullptr;
}

scoped_refptr<VideoCaptureController>
VideoCaptureManager::GetOrCreateController(
    const media::VideoCaptureSessionId& capture_session_id,
    const media::VideoCaptureParams& params) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);

  auto session_it = sessions_.find(capture_session_id);
  if (session_it == sessions_.end())
    return nullptr;
  const blink::MediaStreamDevice& device_info = session_it->second;

  // Check if another session has already opened this device. If so, just
  // use that opened device.
  VideoCaptureController* const existing_device =
      LookupControllerByMediaTypeAndDeviceId(device_info.type, device_info.id);
  if (existing_device) {
    CHECK_EQ(device_info.type, existing_device->stream_type(),
             base::NotFatalUntil::M158);
    if (existing_device->was_crop_ever_called()) {
      return nullptr;
    }
    return existing_device;
  }

  scoped_refptr<VideoCaptureController> new_controller =
      base::MakeRefCounted<VideoCaptureController>(
          device_info.id, device_info.type, params,
          video_capture_provider_->CreateDeviceLauncher(),
          emit_log_message_cb_);
  controllers_.push_back(new_controller);
  return new_controller;
}

#if BUILDFLAG(IS_ANDROID)
void VideoCaptureManager::OnApplicationStateChange(
    base::android::ApplicationState state) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);

  // Only release/resume devices when the Application state changes from
  // RUNNING->STOPPED->RUNNING.
  if (state == base::android::APPLICATION_STATE_HAS_RUNNING_ACTIVITIES &&
      !application_state_has_running_activities_) {
    ResumeDevices();
    application_state_has_running_activities_ = true;
  } else if (state == base::android::APPLICATION_STATE_HAS_STOPPED_ACTIVITIES) {
    ReleaseDevices();
    application_state_has_running_activities_ = false;
  }
}
#endif

void VideoCaptureManager::ReleaseDevices() {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);

  for (auto& controller : controllers_) {
    // Do not stop Content Video Capture devices, e.g. Tab or Screen capture.
    if (controller->stream_type() !=
        blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE)
      continue;

    DoStopDevice(controller.get());
  }
}

void VideoCaptureManager::ResumeDevices() {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);

  for (auto& controller : controllers_) {
    // Do not resume Content Video Capture devices, e.g. Tab or Screen capture.
    // Do not try to restart already running devices.
    if (controller->stream_type() !=
            blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE ||
        controller->IsDeviceAlive())
      continue;

    // Check if the device is already in the start queue.
    bool device_in_queue = false;
    for (auto& request : device_start_request_queue_) {
      if (request.controller() == controller.get()) {
        device_in_queue = true;
        break;
      }
    }

    if (!device_in_queue) {
      // Session ID is only valid for Screen capture. So we can fake it to
      // resume video capture devices here.
      QueueStartDevice(FakeSessionId(), controller.get(),
                       controller->parameters());
    }
  }
}

void VideoCaptureManager::OnScreenLocked() {
#if !BUILDFLAG(IS_ANDROID)
  // Stop screen sharing when screen is locked on desktop platforms only.
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M158);
  EmitLogMessage("VideoCaptureManager::OnScreenLocked", 1);

  is_screen_locked_ = true;

  std::vector<media::VideoCaptureSessionId> desktopcapture_session_ids;
  for (auto it : sessions_) {
    if (blink::IsDesktopCaptureMediaType(it.second.type))
      desktopcapture_session_ids.push_back(it.first);

    if (blink::IsDeviceMediaType(it.second.type))
      locked_sessions_.insert(it.first);
  }

  if (!locked_sessions_.empty()) {
    idle_close_timer_.Start(FROM_HERE, idle_close_timeout_, this,
                            &VideoCaptureManager::ReleaseDevices);
  }

  for (auto session_id : desktopcapture_session_ids) {
    Close(session_id);
  }
#endif  // !BUILDFLAG(IS_ANDROID)
}

void VideoCaptureManager::OnScreenUnlocked() {
  EmitLogMessage("VideoCaptureManager::OnScreenUnlocked", 1);
  if (!is_screen_locked_) {
    return;
  }
  is_screen_locked_ = false;

  if (!locked_sessions_.empty()) {
    idle_close_timer_.Stop();
    locked_sessions_.clear();
  }
  ResumeDevices();
}

void VideoCaptureManager::EmitLogMessage(const std::string& message,
                                         int verbose_log_level) {
  DVLOG(verbose_log_level) << message;
  emit_log_message_cb_.Run(message);
}

}  // namespace content
