// 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.

#ifndef CONTENT_BROWSER_RENDERER_HOST_MEDIA_VIDEO_CAPTURE_MANAGER_H_
#define CONTENT_BROWSER_RENDERER_HOST_MEDIA_VIDEO_CAPTURE_MANAGER_H_

#include <map>
#include <set>
#include <string>

#include "base/containers/circular_deque.h"
#include "base/containers/flat_set.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "base/process/process_handle.h"
#include "base/threading/thread_checker.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "base/timer/timer.h"
#include "build/build_config.h"
#include "content/browser/renderer_host/media/media_stream_provider.h"
#include "content/browser/renderer_host/media/video_capture_controller_event_handler.h"
#include "content/browser/renderer_host/media/video_capture_device_launch_observer.h"
#include "content/browser/renderer_host/media/video_capture_provider.h"
#include "content/common/content_export.h"
#include "content/public/browser/desktop_capture.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/screenlock_observer.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 "media/capture/video/video_capture_device_info.h"
#include "media/capture/video_capture_types.h"
#include "ui/gfx/native_ui_types.h"

#if BUILDFLAG(IS_ANDROID)
#include "base/android/application_status_listener.h"
#endif

namespace content {
struct GlobalRenderFrameHostId;

class VideoCaptureController;
class VideoCaptureControllerEventHandler;

// VideoCaptureManager is used to open/close, start/stop, enumerate available
// video capture devices, and manage VideoCaptureController's.
// In its main usage in production, an instance is created by MediaStreamManager
// on the Browser::IO thread. All public methods are expected to be called from
// the Browser::IO thread. A device can only be opened once.
class CONTENT_EXPORT VideoCaptureManager
    : public MediaStreamProvider,
      public VideoCaptureDeviceLaunchObserver,
      public ScreenlockObserver {
 public:
  using VideoCaptureDevice = media::VideoCaptureDevice;

  // Callback used to signal the completion of a controller lookup.
  using DoneCB =
      base::OnceCallback<void(const base::WeakPtr<VideoCaptureController>&)>;

  using SetDesktopCaptureWindowIdCallback = base::RepeatingCallback<void(
      const media::VideoCaptureSessionId& session_id,
      gfx::NativeViewId window_id)>;

  explicit VideoCaptureManager(
      std::unique_ptr<VideoCaptureProvider> video_capture_provider,
      base::RepeatingCallback<void(const std::string&)> emit_log_message_cb);

  VideoCaptureManager(const VideoCaptureManager&) = delete;
  VideoCaptureManager& operator=(const VideoCaptureManager&) = delete;

  // AddVideoCaptureObserver() can be called only before any devices are opened.
  // RemoveAllVideoCaptureObservers() can be called only after all devices
  // are closed.
  // They can be called more than once and it's ok to not call at all if the
  // client is not interested in receiving media::VideoCaptureObserver callacks.
  // This methods can be called on whatever thread. The callbacks of
  // media::VideoCaptureObserver arrive on browser IO thread.
  void AddVideoCaptureObserver(media::VideoCaptureObserver* observer);
  void RemoveAllVideoCaptureObservers();

  // Implements MediaStreamProvider.
  void RegisterListener(MediaStreamProviderListener* listener) override;
  void UnregisterListener(MediaStreamProviderListener* listener) override;
  base::UnguessableToken Open(const blink::MediaStreamDevice& device) override;
  void Close(const base::UnguessableToken& capture_session_id) override;

  // Start/stop cropping/restricting the video track.
  //
  // Non-empty |target| sets (or changes) the sub-capture target.
  // Empty |target| reverts the capture to its original state.
  //
  // |sub_capture_target_version| must be incremented by at least one for each
  // call. By including it in frame's metadata, Viz informs Blink what was the
  // latest invocation of cropTo() or restrictTo() before a given frame was
  // produced.
  //
  // The callback reports success/failure.
  void 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);

  // Called by VideoCaptureHost to locate a capture device for `capture_params`,
  // adding the Host as a client of the device's controller if successful. The
  // value of `session_id` controls which device is selected;
  // this value should be a session id previously returned by Open().
  //
  // If the device is not already started (i.e., no other client is currently
  // capturing from this device), this call will cause a VideoCaptureController
  // and VideoCaptureDevice to be created, possibly asynchronously.
  //
  // On success, the controller is returned via calling |done_cb|, indicating
  // that the client was successfully added. A NULL controller is passed to
  // the callback on failure. `done_cb` is not allowed to synchronously call
  // StopCaptureForClient().
  void ConnectClient(const media::VideoCaptureSessionId& session_id,
                     const media::VideoCaptureParams& capture_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);

  // Called by VideoCaptureHost to remove |client_handler|. If this is the last
  // client of the device, the |controller| and its VideoCaptureDevice may be
  // destroyed. The client must not access |controller| after calling this
  // function.
  void DisconnectClient(VideoCaptureController* controller,
                        VideoCaptureControllerID client_id,
                        VideoCaptureControllerEventHandler* client_handler,
                        media::VideoCaptureError error);

  // Called by VideoCaptureHost to pause to update video buffer specified by
  // |client_id| and |client_handler|. If all clients of |controller| are
  // paused, the corresponding device will be closed.
  void PauseCaptureForClient(
      VideoCaptureController* controller,
      VideoCaptureControllerID client_id,
      VideoCaptureControllerEventHandler* client_handler);

  // Called by VideoCaptureHost to resume to update video buffer specified by
  // |client_id| and |client_handler|. The |session_id| and |params| should be
  // same as those used in ConnectClient().
  // If this is first active client of |controller|, device will be allocated
  // and it will take a little time to resume.
  // Allocating device could failed if other app holds the camera, the error
  // will be notified through VideoCaptureControllerEventHandler::OnError().
  void ResumeCaptureForClient(
      const media::VideoCaptureSessionId& session_id,
      const media::VideoCaptureParams& params,
      VideoCaptureController* controller,
      VideoCaptureControllerID client_id,
      VideoCaptureControllerEventHandler* client_handler);

  // Called by VideoCaptureHost to request a refresh frame from the video
  // capture device.
  void RequestRefreshFrameForClient(VideoCaptureController* controller);

  // Called when a client disconnects to request that all buffers be dropped
  // to prevent a compromised renderer from reusing them.
  void InvalidateBuffersForClient(VideoCaptureController* controller);

  // Retrieves all capture supported formats for a particular device. Returns
  // false if the |capture_session_id| is not found. The supported formats are
  // cached during device(s) enumeration, and depending on the underlying
  // implementation, could be an empty list.
  bool GetDeviceSupportedFormats(
      const media::VideoCaptureSessionId& capture_session_id,
      media::VideoCaptureFormats* supported_formats);
  // Retrieves all capture supported formats for a particular device. Returns
  // false if the  |device_id| is not found. The supported formats are cached
  // during device(s) enumeration, and depending on the underlying
  // implementation, could be an empty list.
  bool GetDeviceSupportedFormats(const std::string& device_id,
                                 media::VideoCaptureFormats* supported_formats);

  // Retrieves the format(s) currently in use.  Returns false if the
  // |capture_session_id| is not found. Returns true and |formats_in_use|
  // otherwise. |formats_in_use| is empty if the device is not in use.
  bool GetDeviceFormatsInUse(
      const media::VideoCaptureSessionId& capture_session_id,
      media::VideoCaptureFormats* formats_in_use);
  // Retrieves the format currently in use.  Returns std::nullopt if the
  // |stream_type|, |device_id| pair is not found. Returns in-use format of the
  // device otherwise.
  std::optional<media::VideoCaptureFormat> GetDeviceFormatInUse(
      blink::mojom::MediaStreamType stream_type,
      const std::string& device_id);

  // If there is a capture session associated with |session_id|, and the
  // captured entity is a tab, return the GlobalRenderFrameHostId of the
  // captured tab.
  // Otherwise, returns an empty GlobalRenderFrameHostId.
  GlobalRenderFrameHostId GetGlobalRenderFrameHostId(
      const base::UnguessableToken& session_id) const;

  // Sets the platform-dependent window ID for the desktop capture notification
  // UI for the given session.
  void SetDesktopCaptureWindowId(const media::VideoCaptureSessionId& session_id,
                                 gfx::NativeViewId window_id);

  void GetPhotoState(const base::UnguessableToken& session_id,
                     VideoCaptureDevice::GetPhotoStateCallback callback);
  void SetPhotoOptions(const base::UnguessableToken& session_id,
                       media::mojom::PhotoSettingsPtr settings,
                       VideoCaptureDevice::SetPhotoOptionsCallback callback);
  void TakePhoto(const base::UnguessableToken& session_id,
                 VideoCaptureDevice::TakePhotoCallback callback);

#if BUILDFLAG(IS_ANDROID)
  // Some devices had troubles when stopped and restarted quickly, so the device
  // is only stopped when Chrome is sent to background and not when, e.g., a tab
  // is hidden, see http://crbug.com/582295.
  void OnApplicationStateChange(base::android::ApplicationState state);
#endif

  using EnumerationCallback =
      base::OnceCallback<void(media::mojom::DeviceEnumerationResult result_code,
                              const media::VideoCaptureDeviceDescriptors&)>;
  // Asynchronously obtains descriptors for the available devices.
  // As a side-effect, updates |devices_info_cache_|.
  void EnumerateDevices(EnumerationCallback client_callback);

  // VideoCaptureDeviceLaunchObserver implementation:
  void OnDeviceLaunched(VideoCaptureController* controller) override;
  void OnDeviceLaunchFailed(VideoCaptureController* controller,
                            media::VideoCaptureError error) override;
  void OnDeviceLaunchAborted() override;
  void OnDeviceConnectionLost(VideoCaptureController* controller) override;

  void 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 =
          base::DoNothing());

  void CloseNativeScreenCapturePicker(DesktopMediaID device_id);

#if BUILDFLAG(IS_MAC)
  void GetApplicationAudioCaptureId(
      DesktopMediaID::Id session_id,
      base::OnceCallback<void(
          const std::optional<desktop_capture::ApplicationAudioCaptureId>&)>
          callback);
#endif

  VideoCaptureProvider& video_capture_provider() {
    return *video_capture_provider_.get();
  }

  bool is_idle_close_timer_running_for_testing() const {
    return idle_close_timer_.IsRunning();
  }
  void set_idle_close_timeout_for_testing(base::TimeDelta timeout) {
    idle_close_timeout_ = timeout;
  }

  void set_desktop_capture_window_id_callback_for_testing(
      SetDesktopCaptureWindowIdCallback callback) {
    set_desktop_capture_window_id_callback_for_testing_ = callback;
  }

 private:
  class CaptureDeviceStartRequest;

  using SessionMap =
      std::map<media::VideoCaptureSessionId, blink::MediaStreamDevice>;
  using DeviceStartQueue = base::circular_deque<CaptureDeviceStartRequest>;
  using VideoCaptureDeviceDescriptor = media::VideoCaptureDeviceDescriptor;
  using VideoCaptureDeviceDescriptors = media::VideoCaptureDeviceDescriptors;

  ~VideoCaptureManager() override;

  void OnDeviceInfosReceived(
      base::ElapsedTimer timer,
      EnumerationCallback client_callback,
      media::mojom::DeviceEnumerationResult error_code,
      const std::vector<media::VideoCaptureDeviceInfo>& device_infos);

  // Helpers to report an event to our Listener.
  void OnOpened(blink::mojom::MediaStreamType type,
                const media::VideoCaptureSessionId& capture_session_id);
  void OnClosed(blink::mojom::MediaStreamType type,
                const media::VideoCaptureSessionId& capture_session_id);

  // Checks to see if |controller| has no clients left. If so, remove it from
  // the list of controllers, and delete it asynchronously. |controller| may be
  // freed by this function.
  void DestroyControllerIfNoClients(
      const base::UnguessableToken& capture_session_id,
      VideoCaptureController* controller);

  // Finds a VideoCaptureController in different ways: by |session_id|, by its
  // |device_id| and |type| (if it is already opened), by its |controller| or by
  // its |serial_id|. In all cases, if not found, nullptr is returned.
  VideoCaptureController* LookupControllerBySessionId(
      const base::UnguessableToken& session_id) const;
  VideoCaptureController* LookupControllerByMediaTypeAndDeviceId(
      blink::mojom::MediaStreamType type,
      const std::string& device_id) const;
  bool IsControllerPointerValid(const VideoCaptureController* controller) const;
  scoped_refptr<VideoCaptureController> GetControllerSharedRef(
      VideoCaptureController* controller) const;

  // Finds the device info by |id| in |devices_info_cache_|, or nullptr.
  media::VideoCaptureDeviceInfo* GetDeviceInfoById(const std::string& id);

  // Finds a VideoCaptureController for the indicated |capture_session_id|,
  // creating a fresh one if necessary. Returns nullptr if said
  // |capture_session_id| is invalid.
  scoped_refptr<VideoCaptureController> GetOrCreateController(
      const media::VideoCaptureSessionId& capture_session_id,
      const media::VideoCaptureParams& params);

  // Starting a capture device can take 1-2 seconds.
  // To avoid multiple unnecessary start/stop commands to the OS, each start
  // request is queued in |device_start_request_queue_|.
  // QueueStartDevice creates a new entry in |device_start_request_queue_| and
  // posts a request to start the device on the device thread unless there is
  // another request pending start.
  void QueueStartDevice(const media::VideoCaptureSessionId& session_id,
                        scoped_refptr<VideoCaptureController> controller,
                        const media::VideoCaptureParams& params);
  void DoStopDevice(VideoCaptureController* controller);
  void ProcessDeviceStartRequestQueue();

  void MaybePostDesktopCaptureWindowId(
      const media::VideoCaptureSessionId& session_id);

  void ReleaseDevices();
  void ResumeDevices();

#if BUILDFLAG(IS_ANDROID)
  std::unique_ptr<base::android::ApplicationStatusListener>
      app_status_listener_;
  bool application_state_has_running_activities_;
#endif

  // ScreenlockObserver implementation:
  void OnScreenLocked() override;
  void OnScreenUnlocked() override;

  void EmitLogMessage(const std::string& message, int verbose_log_level);

  // Only accessed on Browser::IO thread.
  base::ObserverList<MediaStreamProviderListener>::UncheckedAndDanglingUntriaged
      listeners_;

  // An entry is kept in this map for every session that has been created via
  // the Open() entry point. The keys are session_id's. This map is used to
  // determine which device to use when ConnectClient() occurs. Used
  // only on the IO thread.
  SessionMap sessions_;

  // True between OnScreenLocked() and OnScreenUnlocked() on platforms where
  // the screen lock state is observed. Used to defer starting capture devices
  // while the screen is locked.
  bool is_screen_locked_ = false;

  // A set of sessions that have encountered screen lock.
  base::flat_set<media::VideoCaptureSessionId> locked_sessions_;

  // Currently opened VideoCaptureController instances. The device may or may
  // not be started. This member is only accessed on IO thread.
  std::vector<scoped_refptr<VideoCaptureController>> controllers_;

  // TODO(chfremer): Consider using CancellableTaskTracker, see
  // crbug.com/598465.
  DeviceStartQueue device_start_request_queue_;

  // Queue to keep photo-associated requests waiting for a device to initialize,
  // bundles a session id token and an associated photo-related request.
  base::circular_deque<std::pair<base::UnguessableToken, base::OnceClosure>>
      photo_request_queue_;

  const std::unique_ptr<VideoCaptureProvider> video_capture_provider_;
  base::RepeatingCallback<void(const std::string&)> emit_log_message_cb_;

  base::ObserverList<media::VideoCaptureObserver> capture_observers_;

  // Local cache of the enumerated DeviceInfos. GetDeviceSupportedFormats() will
  // use this list if the device is not started, otherwise it will retrieve the
  // active device capture format from the VideoCaptureController associated.
  std::vector<media::VideoCaptureDeviceInfo> devices_info_cache_;

  // Map used by DesktopCapture.
  std::map<media::VideoCaptureSessionId, gfx::NativeViewId>
      notification_window_ids_;

  // Closes video device capture sessions after a timeout. Idle timeout value
  // chosen based on UMA metrics. See https://crbug.com/1163105#c28
  base::TimeDelta idle_close_timeout_ = base::Seconds(15);
  base::OneShotTimer idle_close_timer_;

  SetDesktopCaptureWindowIdCallback
      set_desktop_capture_window_id_callback_for_testing_;

  // Stores the session IDs of display capture streams in the order they were
  // started. Used for testing purposes.
  // TODO(crbug.com/485200165): Remove this once testing is completed and the
  // bug is fixed.
  std::vector<base::UnguessableToken> display_capture_session_ids_;
  base::WeakPtrFactory<VideoCaptureManager> weak_factory_{this};
};

}  // namespace content

#endif  // CONTENT_BROWSER_RENDERER_HOST_MEDIA_VIDEO_CAPTURE_MANAGER_H_
