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

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

#include "chromeos/ash/services/recording/recording_service.h"

#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <memory>
#include <optional>

#include "base/check.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/notreached.h"
#include "base/task/bind_post_task.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "chromeos/ash/services/recording/audio_capture_util.h"
#include "chromeos/ash/services/recording/audio_stream_mixer.h"
#include "chromeos/ash/services/recording/gif_encoder.h"
#include "chromeos/ash/services/recording/recording_encoder.h"
#include "chromeos/ash/services/recording/recording_service_constants.h"
#include "chromeos/ash/services/recording/rgb_video_frame.h"
#include "chromeos/ash/services/recording/video_capture_params.h"
#include "chromeos/ash/services/recording/webm_encoder_muxer.h"
#include "media/audio/audio_device_description.h"
#include "media/base/video_frame.h"
#include "media/base/video_util.h"
#include "media/capture/mojom/video_capture_buffer.mojom.h"
#include "media/renderers/paint_canvas_video_renderer.h"
#include "services/audio/public/cpp/device_factory.h"
#include "ui/gfx/image/image_skia_operations.h"

namespace recording {

namespace {

// For a capture size of 320 by 240, we use a bitrate of 256 kbit/s. This value
// is used as the minimum bitrate that we don't go below regardless of the video
// size.
constexpr uint32_t kMinBitrateInBitsPerSecond = 256 * 1000;

// The size (in DIPs) within which we will try to fit a thumbnail image
// extracted from the first valid video frame. The value was chosen to be
// suitable with the image container in the notification UI.
constexpr gfx::Size kThumbnailSize{328, 184};

// Video frames are generated when the recorded surface has damage. If the
// contents of the surface is static, no new video frames will be generated, and
// as a result, seeking through the output video may produce garbage play-back
// video frames (see http://b/238505291).
// To avoid this issue, we use the below duration as the maximum time to wait
// since the last received video frames before we request a new refresh frame.
// This value was chosen by trial and error, and it also matches the one used by
// Chromecast.
constexpr base::TimeDelta kVideoFramesRefreshInterval = base::Milliseconds(250);

// Calculates the bitrate (in bits/seconds) used to initialize the video encoder
// based on the given |capture_size|.
uint32_t CalculateVpxEncoderBitrate(const gfx::Size& capture_size) {
  // We use the Kush Gauge formula which goes like this:
  // bitrate (bits/s) = width * height * frame rate * motion factor * 0.07.
  // Here we use a motion factor = 1, which works well for our use cases.
  // This formula gives a balance between the video quality and the file size so
  // it doesn't become too large.
  const uint32_t bitrate =
      std::ceil(capture_size.GetArea() * kMaxFrameRate * 0.07f);

  // Make sure to return a value that is divisible by 8 so that we work with
  // whole bytes.
  return std::max(kMinBitrateInBitsPerSecond, (bitrate & ~7));
}

// Given the desired |capture_size|, it creates and returns the options needed
// to configure the video encoder.
media::VideoEncoder::Options CreateVideoEncoderOptions(
    const gfx::Size& capture_size) {
  media::VideoEncoder::Options video_encoder_options;
  video_encoder_options.bitrate =
      media::Bitrate::ConstantBitrate(CalculateVpxEncoderBitrate(capture_size));
  video_encoder_options.framerate = kMaxFrameRate;
  video_encoder_options.frame_size = capture_size;
  // This value, expressed as a number of frames, forces the encoder to code
  // a keyframe if one has not been coded in the last keyframe_interval frames.
  video_encoder_options.keyframe_interval = 100;
  return video_encoder_options;
}

// Extracts a potentially scaled-down RGB image from the given video |frame|,
// which is suitable to use as a thumbnail for the video.
gfx::ImageSkia ExtractImageFromVideoFrame(const media::VideoFrame& frame) {
  const gfx::Size visible_size = frame.visible_rect().size();
  media::PaintCanvasVideoRenderer renderer;
  SkBitmap bitmap;
  bitmap.allocN32Pixels(visible_size.width(), visible_size.height());
  renderer.ConvertVideoFrameToRGBPixels(&frame, bitmap.getPixels(),
                                        bitmap.rowBytes());

  // Since this image will be used as a thumbnail, we can scale it down to save
  // on memory if needed. For example, if recording a FHD display, that will be
  // (for 12 bits/pixel):
  // 1920 * 1080 * 12 / 8, which is approx. = 3 MB, which is a lot to keep
  // around for a thumbnail.
  const gfx::ImageSkia thumbnail = gfx::ImageSkia::CreateFrom1xBitmap(bitmap);
  if (visible_size.width() <= kThumbnailSize.width() &&
      visible_size.height() <= kThumbnailSize.height()) {
    return thumbnail;
  }

  const gfx::Size scaled_size =
      media::ScaleSizeToFitWithinTarget(visible_size, kThumbnailSize);
  return gfx::ImageSkiaOperations::CreateResizedImage(
      thumbnail, skia::ImageOperations::ResizeMethod::RESIZE_BETTER,
      scaled_size);
}

// Called when the channel to the client of the recording service gets
// disconnected. At that point, there's nothing useful to do here, and instead
// of wasting resources encoding/muxing remaining frames, and flushing the
// buffers, we terminate the recording service process immediately.
void TerminateServiceImmediately() {
  LOG(ERROR)
      << "The recording service client was disconnected. Exiting immediately.";
  std::exit(EXIT_FAILURE);
}

// Creates the appropriate encoder capabilities based to the type of the given
// `output_file_path`.
std::unique_ptr<RecordingEncoder::Capabilities> CreateEncoderCapabilities(
    const base::FilePath& output_file_path) {
  return output_file_path.MatchesExtension(".gif")
             ? GifEncoder::CreateCapabilities()
             : WebmEncoderMuxer::CreateCapabilities();
}

// Creates and returns the appropriate encoder based on the type of the given
// `output_file_path`.
base::SequenceBound<RecordingEncoder> CreateEncoder(
    scoped_refptr<base::SequencedTaskRunner> blocking_task_runner,
    const media::VideoEncoder::Options& video_encoder_options,
    const media::AudioParameters* audio_input_params,
    mojo::PendingRemote<mojom::DriveFsQuotaDelegate> drive_fs_quota_delegate,
    const base::FilePath& output_file_path,
    OnFailureCallback on_failure_callback) {
  if (output_file_path.MatchesExtension(".gif")) {
    DCHECK(!audio_input_params);

    return GifEncoder::Create(std::move(blocking_task_runner),
                              video_encoder_options,
                              std::move(drive_fs_quota_delegate),
                              output_file_path, std::move(on_failure_callback));
  }

  DCHECK(output_file_path.MatchesExtension(".webm"));
  return WebmEncoderMuxer::Create(
      std::move(blocking_task_runner), video_encoder_options,
      audio_input_params, std::move(drive_fs_quota_delegate), output_file_path,
      std::move(on_failure_callback));
}

}  // namespace

RecordingService::RecordingService(
    mojo::PendingReceiver<mojom::RecordingService> receiver)
    : audio_parameters_(audio_capture_util::GetAudioCaptureParameters()),
      receiver_(this, std::move(receiver)),
      consumer_receiver_(this),
      main_task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault()),
      encoding_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
          // We use |USER_VISIBLE| here as opposed to |BEST_EFFORT| since the
          // latter is extremely low priority and may stall encoding for random
          // reasons.
          {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
           base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) {}

RecordingService::~RecordingService() {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);

  if (!current_video_capture_params_)
    return;

  // If the service gets destructed while recording in progress, the client must
  // be still connected (since otherwise the service process would have been
  // immediately terminated). We attempt to flush whatever we have right now
  // before exiting.
  DCHECK(client_remote_.is_bound());
  DCHECK(client_remote_.is_connected());
  StopRecording();
  video_capturer_remote_.reset();
  consumer_receiver_.reset();
  // Note that we can call FlushAndFinalize() on the |encoder_muxer_| even
  // though it will be done asynchronously on the |encoding_task_runner_| and by
  // then this |RecordingService| instance will have already been gone. This is
  // because the muxer writes directly to the file and does not rely on this
  // instance.
  encoder_muxer_.AsyncCall(&RecordingEncoder::FlushAndFinalize)
      .WithArgs(base::DoNothing());
  SignalRecordingEndedToClient(mojom::RecordingStatus::kServiceClosing);
}

void RecordingService::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) {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);

  StartNewRecording(
      std::move(client), std::move(video_capturer),
      std::move(microphone_stream_factory),
      std::move(system_audio_stream_factory),
      std::move(drive_fs_quota_delegate), output_file_path,
      VideoCaptureParams::CreateForFullscreenCapture(
          frame_sink_id, frame_sink_size_dip, device_scale_factor));
}

void RecordingService::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) {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);

  StartNewRecording(std::move(client), std::move(video_capturer),
                    std::move(microphone_stream_factory),
                    std::move(system_audio_stream_factory),
                    std::move(drive_fs_quota_delegate), output_file_path,
                    VideoCaptureParams::CreateForWindowCapture(
                        frame_sink_id, subtree_capture_id, frame_sink_size_dip,
                        device_scale_factor, window_size_dip));
}

void RecordingService::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) {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);

  StartNewRecording(std::move(client), std::move(video_capturer),
                    std::move(microphone_stream_factory),
                    std::move(system_audio_stream_factory),
                    std::move(drive_fs_quota_delegate), output_file_path,
                    VideoCaptureParams::CreateForRegionCapture(
                        frame_sink_id, frame_sink_size_dip, device_scale_factor,
                        crop_region_dip));
}

void RecordingService::StopRecording() {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);
  refresh_timer_.Stop();
  video_capturer_remote_->Stop();
  MaybeStopAudioRecording();
}

void RecordingService::OnRecordedWindowChangingRoot(
    const viz::FrameSinkId& new_frame_sink_id,
    const gfx::Size& new_frame_sink_size_dip,
    float new_device_scale_factor) {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);

  if (!current_video_capture_params_) {
    // A recording might terminate before we signal the client with an
    // |OnRecordingEnded()| call.
    return;
  }

  // If there's a change in the pixel size of the recorded window as a result of
  // it moving to a different display, we must reconfigure the video encoder so
  // that output video has the correct dimensions.
  if (current_video_capture_params_->OnRecordedWindowChangingRoot(
          video_capturer_remote_, new_frame_sink_id, new_frame_sink_size_dip,
          new_device_scale_factor)) {
    ReconfigureVideoEncoder();
  }
}

void RecordingService::OnRecordedWindowSizeChanged(
    const gfx::Size& new_window_size_dip) {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);

  if (!current_video_capture_params_) {
    // A recording might terminate before we signal the client with an
    // |OnRecordingEnded()| call.
    return;
  }

  if (current_video_capture_params_->OnRecordedWindowSizeChanged(
          video_capturer_remote_, new_window_size_dip)) {
    ReconfigureVideoEncoder();
  }
}

void RecordingService::OnFrameSinkSizeChanged(
    const gfx::Size& new_frame_sink_size_dip,
    float new_device_scale_factor) {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);

  if (!current_video_capture_params_) {
    // A recording might terminate before we signal the client with an
    // |OnRecordingEnded()| call.
    return;
  }

  // A change in the pixel size of the frame sink may result in changing the
  // pixel size of the captured target (e.g. window or region). we must
  // reconfigure the video encoder so that output video has the correct
  // dimensions.
  if (current_video_capture_params_->OnFrameSinkSizeChanged(
          video_capturer_remote_, new_frame_sink_size_dip,
          new_device_scale_factor)) {
    ReconfigureVideoEncoder();
  }
}

void RecordingService::OnFrameCaptured(
    media::mojom::VideoBufferHandlePtr data,
    media::mojom::VideoFrameInfoPtr info,
    const gfx::Rect& content_rect,
    mojo::PendingRemote<viz::mojom::FrameSinkVideoConsumerFrameCallbacks>
        callbacks) {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);
  DCHECK(encoder_muxer_);

  // Once a frame is received, we reset the timer back to the full
  // `kVideoFramesRefreshInterval` so that it fires only if we don't get any
  // new video frames during that interval.
  refresh_timer_.Reset();

  CHECK(data->is_read_only_shmem_region());
  base::ReadOnlySharedMemoryRegion& shmem_region =
      data->get_read_only_shmem_region();

  // The |data| parameter is not nullable and mojo type mapping for
  // `base::ReadOnlySharedMemoryRegion` defines that nullable version of it is
  // the same type, with null check being equivalent to IsValid() check. Given
  // the above, we should never be able to receive a read only shmem region that
  // is not valid - mojo will enforce it for us.
  DCHECK(shmem_region.IsValid());

  // We ignore any subsequent frames after a failure.
  if (did_failure_occur_)
    return;

  base::ReadOnlySharedMemoryMapping mapping = shmem_region.Map();
  if (!mapping.IsValid()) {
    DLOG(ERROR) << "Mapping of video frame shared memory failed.";
    return;
  }

  if (mapping.size() <
      media::VideoFrame::AllocationSize(info->pixel_format, info->coded_size)) {
    DLOG(ERROR) << "Shared memory size was less than expected.";
    return;
  }

  DCHECK(current_video_capture_params_);
  const gfx::Rect& visible_rect =
      current_video_capture_params_->GetVideoFrameVisibleRect(
          info->visible_rect);
  scoped_refptr<media::VideoFrame> frame = media::VideoFrame::WrapExternalData(
      info->pixel_format, info->coded_size, visible_rect, visible_rect.size(),
      reinterpret_cast<const uint8_t*>(mapping.memory()), mapping.size(),
      info->timestamp);
  if (!frame) {
    DLOG(ERROR) << "Failed to create a VideoFrame.";
    return;
  }

  // Takes ownership of |mapping| and |callbacks| to keep them alive until
  // |frame| is released.
  frame->AddDestructionObserver(base::BindOnce(
      [](base::ReadOnlySharedMemoryMapping mapping,
         mojo::PendingRemote<viz::mojom::FrameSinkVideoConsumerFrameCallbacks>
             callbacks) {},
      std::move(mapping), std::move(callbacks)));
  frame->set_metadata(info->metadata);
  frame->set_color_space(info->color_space);

  if (video_thumbnail_.isNull())
    video_thumbnail_ = ExtractImageFromVideoFrame(*frame);

  if (on_video_frame_delivered_callback_for_testing_) {
    std::move(on_video_frame_delivered_callback_for_testing_)
        .Run(*frame, content_rect);
  }

  if (encoder_capabilities_->SupportsRgbVideoFrame()) {
    // This is the GIF encoding path.
    encoder_muxer_.AsyncCall(&RecordingEncoder::EncodeRgbVideo)
        .WithArgs(RgbVideoFrame(*frame));

    // Note that we no longer need `frame`. `RgbVideoFrame` already copied the
    // pixel colors (which is needed to be able to modify them later when we
    // dither the image). Note that the video `frame`'s memory itself cannot be
    // modified, as it is backed by a read-only shared memory region. This
    // allows us to return the frame early to Viz capturer buffer pool, which
    // has a maximum number of in-flight frames (See b/316588576).
    frame.reset();
    return;
  }

  // This is the WebM path.
  encoder_muxer_.AsyncCall(&RecordingEncoder::EncodeVideo)
      .WithArgs(std::move(frame));
}

void RecordingService::OnNewSubCaptureTargetVersion(
    uint32_t sub_capture_target_version) {}

void RecordingService::OnFrameWithEmptyRegionCapture() {}

void RecordingService::OnStopped() {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);

  // If a failure occurred, we don't wait till the capturer sends us this
  // signal. The recording had already been terminated by now.
  if (!did_failure_occur_)
    TerminateRecording(mojom::RecordingStatus::kSuccess);
}

void RecordingService::OnLog(const std::string& message) {
  DLOG(WARNING) << message;
}

void RecordingService::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) {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);

  if (current_video_capture_params_) {
    LOG(ERROR) << "Cannot start a new recording while another is in progress.";
    return;
  }

  client_remote_.reset();
  client_remote_.Bind(std::move(client));
  client_remote_.set_disconnect_handler(
      base::BindOnce(&TerminateServiceImmediately));

  current_video_capture_params_ = std::move(capture_params);
  const bool should_record_audio = microphone_stream_factory.is_valid() ||
                                   system_audio_stream_factory.is_valid();

  encoder_capabilities_ = CreateEncoderCapabilities(output_file_path);
  encoder_muxer_ = CreateEncoder(
      encoding_task_runner_,
      CreateVideoEncoderOptions(current_video_capture_params_->GetVideoSize()),
      should_record_audio ? &audio_parameters_ : nullptr,
      std::move(drive_fs_quota_delegate), output_file_path,
      BindOnceToMainThread(&RecordingService::OnEncodingFailure));

  ConnectAndStartVideoCapturer(std::move(video_capturer));

  if (!should_record_audio)
    return;

  audio_stream_mixer_ = AudioStreamMixer::Create(encoding_task_runner_);

  if (microphone_stream_factory) {
    // Ideally, we should be able to use echo cancellation with the microphone,
    // but due to observed distortion to the user's voice that it may cause, we
    // decided to hold off for now.
    // We use automatic gain control for the microphone capture since depending
    // on the users voice and their environment, the strength and clarity may
    // vary. System audio is just captured as is.
    audio_stream_mixer_.AsyncCall(&AudioStreamMixer::AddAudioCapturer)
        .WithArgs(media::AudioDeviceDescription::kDefaultDeviceId,
                  std::move(microphone_stream_factory),
                  /*use_automatic_gain_control=*/true,
                  /*use_echo_canceller=*/false);
  }

  if (system_audio_stream_factory) {
    audio_stream_mixer_.AsyncCall(&AudioStreamMixer::AddAudioCapturer)
        .WithArgs(media::AudioDeviceDescription::kLoopbackInputDeviceId,
                  std::move(system_audio_stream_factory),
                  /*use_automatic_gain_control=*/false,
                  /*use_echo_canceller=*/false);
  }

  encoder_muxer_.AsyncCall(&RecordingEncoder::GetEncodeAudioCallback)
      .Then(base::BindOnce(&RecordingService::OnEncodeAudioCallbackReady,
                           weak_ptr_factory_.GetWeakPtr()));
}

void RecordingService::OnEncodeAudioCallbackReady(
    EncodeAudioCallback callback) {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);

  // This can be triggered after we have stopped recording already, e.g. in
  // tests.
  if (audio_stream_mixer_) {
    audio_stream_mixer_.AsyncCall(&AudioStreamMixer::Start)
        .WithArgs(std::move(callback));
  }
}

void RecordingService::ReconfigureVideoEncoder() {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);
  DCHECK(current_video_capture_params_);
  DCHECK(encoder_capabilities_);

  if (!encoder_capabilities_->SupportsVideoFrameSizeChanges()) {
    OnEncodingFailure(
        mojom::RecordingStatus::kVideoEncoderReconfigurationFailure);
    return;
  }

  ++number_of_video_encoder_reconfigures_;
  encoder_muxer_.AsyncCall(&RecordingEncoder::InitializeVideoEncoder)
      .WithArgs(CreateVideoEncoderOptions(
          current_video_capture_params_->GetVideoSize()));
}

void RecordingService::TerminateRecording(mojom::RecordingStatus status) {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);
  DCHECK(encoder_muxer_);

  refresh_timer_.Stop();
  current_video_capture_params_.reset();
  encoder_capabilities_.reset();
  video_capturer_remote_.reset();
  consumer_receiver_.reset();

  encoder_muxer_.AsyncCall(&RecordingEncoder::FlushAndFinalize)
      .WithArgs(BindOnceToMainThread(&RecordingService::OnEncoderMuxerFlushed,
                                     status));
}

void RecordingService::ConnectAndStartVideoCapturer(
    mojo::PendingRemote<viz::mojom::FrameSinkVideoCapturer> video_capturer) {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);
  DCHECK(current_video_capture_params_);
  DCHECK(encoder_capabilities_);

  video_capturer_remote_.reset();
  video_capturer_remote_.Bind(std::move(video_capturer));
  // The GPU process could crash while recording is in progress, and the video
  // capturer will be disconnected. We need to handle this event gracefully.
  video_capturer_remote_.set_disconnect_handler(base::BindOnce(
      &RecordingService::OnVideoCapturerDisconnected, base::Unretained(this)));
  current_video_capture_params_->InitializeVideoCapturer(
      video_capturer_remote_, encoder_capabilities_->GetSupportedPixelFormat());
  video_capturer_remote_->Start(consumer_receiver_.BindNewPipeAndPassRemote(),
                                viz::mojom::BufferFormatPreference::kDefault);

  // Always request the very first frame, and don't rely on damage to produce
  // it.
  video_capturer_remote_->RequestRefreshFrame();

  refresh_timer_.Start(FROM_HERE, kVideoFramesRefreshInterval, this,
                       &RecordingService::OnRefreshTimerFired);
}

void RecordingService::OnVideoCapturerDisconnected() {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);

  // On a crash in the GPU, the video capturer gets disconnected, so we can't
  // communicate with it any longer, but we can still communicate with the audio
  // capturer. We will stop the recording and flush whatever video chunks we
  // currently have.
  did_failure_occur_ = true;
  MaybeStopAudioRecording();
  TerminateRecording(mojom::RecordingStatus::kVizVideoCapturerDisconnected);
}

void RecordingService::OnEncodingFailure(mojom::RecordingStatus status) {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);

  did_failure_occur_ = true;
  StopRecording();
  // We don't wait for the video capturer to send us the OnStopped() signal, we
  // terminate recording immediately. We still need to flush the encoders, and
  // muxer since they may contain valid frames from before the failure occurred,
  // that we can propagate to the client.
  TerminateRecording(status);
}

void RecordingService::OnEncoderMuxerFlushed(mojom::RecordingStatus status) {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);

  SignalRecordingEndedToClient(status);
}

void RecordingService::SignalRecordingEndedToClient(
    mojom::RecordingStatus status) {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);
  DCHECK(encoder_muxer_);

  encoder_muxer_.Reset();
  client_remote_->OnRecordingEnded(status, video_thumbnail_);
}

void RecordingService::OnRefreshTimerFired() {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);

  if (video_capturer_remote_)
    video_capturer_remote_->RequestRefreshFrame();
}

void RecordingService::MaybeStopAudioRecording() {
  DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);

  if (audio_stream_mixer_) {
    audio_stream_mixer_.AsyncCall(&AudioStreamMixer::Stop);
    audio_stream_mixer_.Reset();
  }
}

}  // namespace recording