chromium/chromeos/ash/services/recording/recording_service.h

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

#ifndef CHROMEOS_ASH_SERVICES_RECORDING_RECORDING_SERVICE_H_
#define CHROMEOS_ASH_SERVICES_RECORDING_RECORDING_SERVICE_H_

#include <memory>
#include <string>
#include <utility>

#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/thread_annotations.h"
#include "base/threading/sequence_bound.h"
#include "base/threading/thread_checker.h"
#include "base/timer/timer.h"
#include "chromeos/ash/services/recording/public/mojom/recording_service.mojom.h"
#include "chromeos/ash/services/recording/recording_encoder.h"
#include "chromeos/ash/services/recording/video_capture_params.h"
#include "media/base/audio_bus.h"
#include "media/base/audio_capturer_source.h"
#include "media/base/audio_parameters.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/viz/privileged/mojom/compositing/frame_sink_video_capture.mojom.h"
#include "ui/gfx/image/image_skia.h"

namespace recording {

class AudioStreamMixer;
class RecordingServiceTestApi;

// Implements the mojo interface of the recording service which handles
// recording audio and video of the screen or portion of it, and writes the
// encoded video chunks directly to a file at a path provided to the Record*()
// functions.
class RecordingService : public mojom::RecordingService,
                         public viz::mojom::FrameSinkVideoConsumer {
 public:
  explicit RecordingService(
      mojo::PendingReceiver<mojom::RecordingService> receiver);
  RecordingService(const RecordingService&) = delete;
  RecordingService& operator=(const RecordingService&) = delete;
  ~RecordingService() override;

  // mojom::RecordingService:
  void RecordFullscreen(
      mojo::PendingRemote<mojom::RecordingServiceClient> client,
      mojo::PendingRemote<viz::mojom::FrameSinkVideoCapturer> video_capturer,
      mojo::PendingRemote<media::mojom::AudioStreamFactory>
          microphone_stream_factory,
      mojo::PendingRemote<media::mojom::AudioStreamFactory>
          system_audio_stream_factory,
      mojo::PendingRemote<mojom::DriveFsQuotaDelegate> drive_fs_quota_delegate,
      const base::FilePath& output_file_path,
      const viz::FrameSinkId& frame_sink_id,
      const gfx::Size& frame_sink_size_dip,
      float device_scale_factor) override;
  void RecordWindow(
      mojo::PendingRemote<mojom::RecordingServiceClient> client,
      mojo::PendingRemote<viz::mojom::FrameSinkVideoCapturer> video_capturer,
      mojo::PendingRemote<media::mojom::AudioStreamFactory>
          microphone_stream_factory,
      mojo::PendingRemote<media::mojom::AudioStreamFactory>
          system_audio_stream_factory,
      mojo::PendingRemote<mojom::DriveFsQuotaDelegate> drive_fs_quota_delegate,
      const base::FilePath& output_file_path,
      const viz::FrameSinkId& frame_sink_id,
      const gfx::Size& frame_sink_size_dip,
      float device_scale_factor,
      const viz::SubtreeCaptureId& subtree_capture_id,
      const gfx::Size& window_size_dip) override;
  void RecordRegion(
      mojo::PendingRemote<mojom::RecordingServiceClient> client,
      mojo::PendingRemote<viz::mojom::FrameSinkVideoCapturer> video_capturer,
      mojo::PendingRemote<media::mojom::AudioStreamFactory>
          microphone_stream_factory,
      mojo::PendingRemote<media::mojom::AudioStreamFactory>
          system_audio_stream_factory,
      mojo::PendingRemote<mojom::DriveFsQuotaDelegate> drive_fs_quota_delegate,
      const base::FilePath& output_file_path,
      const viz::FrameSinkId& frame_sink_id,
      const gfx::Size& frame_sink_size_dip,
      float device_scale_factor,
      const gfx::Rect& crop_region_dip) override;
  void StopRecording() override;
  void OnRecordedWindowChangingRoot(const viz::FrameSinkId& new_frame_sink_id,
                                    const gfx::Size& new_frame_sink_size_dip,
                                    float new_device_scale_factor) override;
  void OnRecordedWindowSizeChanged(
      const gfx::Size& new_window_size_dip) override;
  void OnFrameSinkSizeChanged(const gfx::Size& new_frame_sink_size_dip,
                              float new_device_scale_factor) override;

  // viz::mojom::FrameSinkVideoConsumer:
  void OnFrameCaptured(
      media::mojom::VideoBufferHandlePtr data,
      media::mojom::VideoFrameInfoPtr info,
      const gfx::Rect& content_rect,
      mojo::PendingRemote<viz::mojom::FrameSinkVideoConsumerFrameCallbacks>
          callbacks) override;
  void OnNewSubCaptureTargetVersion(
      uint32_t sub_capture_target_version) override;
  void OnFrameWithEmptyRegionCapture() override;
  void OnStopped() override;
  void OnLog(const std::string& message) override;

 private:
  friend class RecordingServiceTestApi;

  void StartNewRecording(
      mojo::PendingRemote<mojom::RecordingServiceClient> client,
      mojo::PendingRemote<viz::mojom::FrameSinkVideoCapturer> video_capturer,
      mojo::PendingRemote<media::mojom::AudioStreamFactory>
          microphone_stream_factory,
      mojo::PendingRemote<media::mojom::AudioStreamFactory>
          system_audio_stream_factory,
      mojo::PendingRemote<mojom::DriveFsQuotaDelegate> drive_fs_quota_delegate,
      const base::FilePath& output_file_path,
      std::unique_ptr<VideoCaptureParams> capture_params);

  // Called asynchronously by the encoder to provide the `callback` that will be
  // called repeatedly by the `audio_stream_mixer_` to provide the mixed audio
  // buses along with their timestamps to the encoder. This happens only if we
  // are recording audio.
  void OnEncodeAudioCallbackReady(EncodeAudioCallback callback);

  // Called on the main thread during an on-going recording to reconfigure an
  // existing video encoder.
  void ReconfigureVideoEncoder();

  // Called on the main thread to end the recording with the given |status|.
  void TerminateRecording(mojom::RecordingStatus status);

  // Binds the given |video_capturer| to |video_capturer_remote_| and starts
  // video according to the current |current_video_capture_params_|.
  void ConnectAndStartVideoCapturer(
      mojo::PendingRemote<viz::mojom::FrameSinkVideoCapturer> video_capturer);

  // If the video capturer gets disconnected (e.g. Viz crashes) during an
  // ongoing recording, this attempts to reconnect to a new capturer and resumes
  // capturing with the same |current_video_capture_params_|.
  void OnVideoCapturerDisconnected();

  // This is called by |encoder_muxer_| on the main thread (since we bound it as
  // a callback to be invoked on the main thread. See BindOnceToMainThread()),
  // when a failure of type |status| occurs during audio or video encoding. This
  // ends the ongoing recording and signals to the client that a failure
  // occurred.
  void OnEncodingFailure(mojom::RecordingStatus status);

  // At the end of recording we ask the |encoder_muxer_| to flush and process
  // any buffered frames. When this completes this function is called on the
  // main thread (since it's bound as a callback to be invoked on the main
  // thread. See BindOnceToMainThread()). |status| indicates the recording
  // termination status that lead to this flushing of the encoders and muxer.
  void OnEncoderMuxerFlushed(mojom::RecordingStatus status);

  // Called on the main thread to tell the client that recording ended,
  // providing it with the |status| of the recording as well as a chached
  // thumbnail of the video (if available).
  void SignalRecordingEndedToClient(mojom::RecordingStatus status);

  // Called when `refresh_timer_` fires, which means it has been more than
  // `kVideoFramesRefreshInterval` since the last video frame was delivered, at
  // which point we request a new refresh video frame.
  void OnRefreshTimerFired();

  // Stops audio recording if any is being done.
  void MaybeStopAudioRecording();

  // By default, the `encoder_muxer_` will invoke any callback we provide it
  // with to notify us of certain events (such as failure errors, or flush done)
  // on the `encoding_task_runner_`'s sequence. But since these callbacks are
  // invoked asynchronously from other threads, they may get invoked after this
  // RecordingService instance had been destroyed. Therefore, we need to bind
  // these callbacks to weak ptrs, to prevent them from invoking after this
  // object's destruction. However, this won't work, since weak ptrs cannot be
  // invalidated except on the sequence on which they were invoked on. Hence, we
  // must make sure these callbacks are invoked on the main thread.
  //
  // The below is a convenience method to bind once callbacks to weak ptrs that
  // would only be invoked on the main thread.
  template <typename Functor, typename... Args>
  auto BindOnceToMainThread(Functor&& functor, Args&&... args) {
    return base::BindPostTask(main_task_runner_,
                              base::BindOnce(std::forward<Functor>(functor),
                                             weak_ptr_factory_.GetWeakPtr(),
                                             std::forward<Args>(args)...));
  }

  THREAD_CHECKER(main_thread_checker_);

  // The audio parameters that will be used when recording audio.
  const media::AudioParameters audio_parameters_;

  // The mojo receiving end of the service.
  mojo::Receiver<mojom::RecordingService> receiver_;

  // The mojo receiving end of the service as a FrameSinkVideoConsumer.
  mojo::Receiver<viz::mojom::FrameSinkVideoConsumer> consumer_receiver_
      GUARDED_BY_CONTEXT(main_thread_checker_);

  // A task runner to post tasks on the main thread of the recording service.
  // It can be accessed from any thread.
  scoped_refptr<base::SingleThreadTaskRunner> main_task_runner_;

  // A sequenced blocking pool task runner used to run all encoding and muxing
  // tasks on. Can be accessed from any thread.
  scoped_refptr<base::SequencedTaskRunner> encoding_task_runner_;

  // A mojo remote end of client of this service (e.g. Ash). There can only be
  // a single client of this service.
  mojo::Remote<mojom::RecordingServiceClient> client_remote_
      GUARDED_BY_CONTEXT(main_thread_checker_);

  // A callback used for testing, which will be triggered when a video frame is
  // delivered to the service from the Viz capturer.
  using OnVideoFrameDeliveredCallback =
      base::OnceCallback<void(const media::VideoFrame& frame,
                              const gfx::Rect& content_rect)>;
  OnVideoFrameDeliveredCallback on_video_frame_delivered_callback_for_testing_
      GUARDED_BY_CONTEXT(main_thread_checker_);

  // A timer used to request refresh video frames if none was delivered within
  // a certain time interval (which can happen when the contents of the surface
  // being recorded is static, resulting in no damage).
  base::RepeatingTimer refresh_timer_ GUARDED_BY_CONTEXT(main_thread_checker_);

  // A cached scaled down rgb image of the first valid video frame which will be
  // used to provide the client with an image thumbnail representing the
  // recorded video.
  gfx::ImageSkia video_thumbnail_ GUARDED_BY_CONTEXT(main_thread_checker_);

  // True if a failure has been propagated from |encoder_muxer_| that we will
  // end recording abruptly and ignore any incoming audio/video frames.
  bool did_failure_occur_ GUARDED_BY_CONTEXT(main_thread_checker_) = false;

  // The parameters of the current ongoing video capture. This object knows how
  // to initialize the video capturer depending on which capture source
  // (fullscreen, window, or region) is currently being recorded. It is set to
  // a nullptr when there's no recording happening.
  std::unique_ptr<VideoCaptureParams> current_video_capture_params_
      GUARDED_BY_CONTEXT(main_thread_checker_);

  // The mojo remote end used to interact with a video capturer living on Viz.
  mojo::Remote<viz::mojom::FrameSinkVideoCapturer> video_capturer_remote_
      GUARDED_BY_CONTEXT(main_thread_checker_);

  // The audio stream mixer that will be created only if we are capturing any
  // audio stream. The mixer creates and owns the audio capturers that we
  // require. If the mixer exists, it must contain at least a single capturer;
  // if either microphone or system audio recording was requested, or contains
  // two capturers if both are desired to be recorded an mixed together in one
  // stream.
  // All the operations performed by the mixer (including its construction and
  // destruction) are done on `encoding_task_runner_` to avoid stalling the main
  // thread (on which the video frames are received).
  base::SequenceBound<AudioStreamMixer> audio_stream_mixer_
      GUARDED_BY_CONTEXT(main_thread_checker_);

  // Abstracts querying the supported capabilities of the currently used encoder
  // type.
  std::unique_ptr<RecordingEncoder::Capabilities> encoder_capabilities_
      GUARDED_BY_CONTEXT(main_thread_checker_);

  // Performs all encoding and muxing operations asynchronously on the
  // |encoding_task_runner_|. However, the |encoder_muxer_| object itself is
  // constructed, used, and destroyed on the main thread sequence.
  base::SequenceBound<RecordingEncoder> encoder_muxer_
      GUARDED_BY_CONTEXT(main_thread_checker_);

  // The number of times the video encoder was reconfigured as a result of a
  // change in the video size in the middle of recording (See
  // ReconfigureVideoEncoder()).
  int number_of_video_encoder_reconfigures_
      GUARDED_BY_CONTEXT(main_thread_checker_) = 0;

  base::WeakPtrFactory<RecordingService> weak_ptr_factory_{this};
};

}  // namespace recording
#endif  // CHROMEOS_ASH_SERVICES_RECORDING_RECORDING_SERVICE_H_