blob: fac3f241e7def36157c26240bae088811bf17545 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/services/recording/recording_service.h"
#include "ash/services/recording/recording_encoder_muxer.h"
#include "ash/services/recording/recording_service_constants.h"
#include "ash/services/recording/video_capture_params.h"
#include "base/bind.h"
#include "base/check.h"
#include "base/location.h"
#include "base/optional.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "media/audio/audio_device_description.h"
#include "media/base/audio_codecs.h"
#include "media/base/audio_parameters.h"
#include "media/base/status.h"
#include "media/base/video_frame.h"
#include "media/capture/mojom/video_capture_types.mojom.h"
#include "services/audio/public/cpp/device_factory.h"
namespace recording {
namespace {
// For a capture size of 320 by 240, we use a bitrate of 256 kbit/s. Based on
// that, we calculate the bits per second per squared pixel.
constexpr uint64_t kMinBitrateInBitsPerSecond = 256 * 1000;
constexpr float kBitsPerSecondPerSquarePixel =
static_cast<float>(kMinBitrateInBitsPerSecond) / (320.f * 240.f);
// The maximum number of muxed chunks to buffer before sending them over IPC to
// the client. This value has been chosen as half the average number of chunks
// needed to fill a buffer of size 512 KB while recording a screen size of
// 1366 x 768 for about a minute and a half. Note that bombarding the client
// (e.g. Ash) with a ton of IPCs will cause the captured video to sometimes be
// janky.
// TODO(afakhry): Choose a different value if needed, or make it a function of
// the capture size (like the bitrate), or a function of the time since the last
// IPC call to the client.
constexpr int kMaxBufferedChunks = 238;
// Calculates the bitrate used to initialize the video encoder based on the
// given |capture_size|.
uint64_t CalculateVpxEncoderBitrate(const gfx::Size& capture_size) {
return std::max(kMinBitrateInBitsPerSecond,
static_cast<uint64_t>(capture_size.GetArea() *
kBitsPerSecondPerSquarePixel));
}
media::AudioParameters GetAudioParameters() {
static_assert(kAudioSampleRate % 100 == 0,
"Audio sample rate is not divisible by 100");
return media::AudioParameters(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
media::CHANNEL_LAYOUT_STEREO, kAudioSampleRate,
kAudioSampleRate / 100);
}
} // namespace
RecordingService::RecordingService(
mojo::PendingReceiver<mojom::RecordingService> receiver)
: receiver_(this, std::move(receiver)),
consumer_receiver_(this),
main_task_runner_(base::ThreadTaskRunnerHandle::Get()),
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})) {
DETACH_FROM_SEQUENCE(encoding_sequence_checker_);
}
RecordingService::~RecordingService() = default;
void RecordingService::RecordFullscreen(
mojo::PendingRemote<mojom::RecordingServiceClient> client,
mojo::PendingRemote<viz::mojom::FrameSinkVideoCapturer> video_capturer,
mojo::PendingRemote<audio::mojom::StreamFactory> audio_stream_factory,
const viz::FrameSinkId& frame_sink_id,
const gfx::Size& video_size) {
DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);
StartNewRecording(std::move(client), std::move(video_capturer),
std::move(audio_stream_factory),
VideoCaptureParams::CreateForFullscreenCapture(
frame_sink_id, video_size));
}
void RecordingService::RecordWindow(
mojo::PendingRemote<mojom::RecordingServiceClient> client,
mojo::PendingRemote<viz::mojom::FrameSinkVideoCapturer> video_capturer,
mojo::PendingRemote<audio::mojom::StreamFactory> audio_stream_factory,
const viz::FrameSinkId& frame_sink_id,
const gfx::Size& initial_video_size,
const gfx::Size& max_video_size) {
DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);
// TODO(crbug.com/1143930): Window recording doesn't produce any frames at the
// moment.
StartNewRecording(std::move(client), std::move(video_capturer),
std::move(audio_stream_factory),
VideoCaptureParams::CreateForWindowCapture(
frame_sink_id, initial_video_size, max_video_size));
}
void RecordingService::RecordRegion(
mojo::PendingRemote<mojom::RecordingServiceClient> client,
mojo::PendingRemote<viz::mojom::FrameSinkVideoCapturer> video_capturer,
mojo::PendingRemote<audio::mojom::StreamFactory> audio_stream_factory,
const viz::FrameSinkId& frame_sink_id,
const gfx::Size& full_capture_size,
const gfx::Rect& crop_region) {
DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);
StartNewRecording(std::move(client), std::move(video_capturer),
std::move(audio_stream_factory),
VideoCaptureParams::CreateForRegionCapture(
frame_sink_id, full_capture_size, crop_region));
}
void RecordingService::StopRecording() {
DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);
video_capturer_remote_->Stop();
audio_capturer_->Stop();
audio_capturer_.reset();
}
void RecordingService::OnFrameCaptured(
base::ReadOnlySharedMemoryRegion 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_);
// We ignore any subsequent frames after a failure.
if (did_failure_occur_)
return;
if (!data.IsValid()) {
DLOG(ERROR) << "Video frame shared memory is invalid.";
return;
}
base::ReadOnlySharedMemoryMapping mapping = data.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;
}
if (!info->color_space) {
DLOG(ERROR) << "Missing mandatory color space info.";
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<uint8_t*>(const_cast<void*>(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.value());
encoder_muxer_.AsyncCall(&RecordingEncoderMuxer::EncodeVideo).WithArgs(frame);
}
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(/*success=*/true);
}
void RecordingService::OnLog(const std::string& message) {
DLOG(WARNING) << message;
}
void RecordingService::OnCaptureStarted() {}
void RecordingService::Capture(const media::AudioBus* audio_source,
base::TimeTicks audio_capture_time,
double volume,
bool key_pressed) {
// This is called on a worker thread created by the |audio_capturer_| (See
// |media::AudioDeviceThread|. The given |audio_source| wraps audio data in a
// shared memory with the audio service. Calling |audio_capturer_->Stop()|
// will destroy that thread and the shared memory mapping before we get a
// chance to encode and flush the remaining frames (See
// media::AudioInputDevice::Stop(), and
// media::AudioInputDevice::AudioThreadCallback::Process() for details). It is
// safer that we own our AudioBuses that are keep alive until encoded and
// flushed.
auto audio_data =
media::AudioBus::Create(audio_source->channels(), audio_source->frames());
audio_source->CopyTo(audio_data.get());
main_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&RecordingService::OnAudioCaptured, base::Unretained(this),
std::move(audio_data), audio_capture_time));
}
void RecordingService::OnCaptureError(const std::string& message) {
LOG(ERROR) << message;
}
void RecordingService::OnCaptureMuted(bool is_muted) {}
void RecordingService::StartNewRecording(
mojo::PendingRemote<mojom::RecordingServiceClient> client,
mojo::PendingRemote<viz::mojom::FrameSinkVideoCapturer> video_capturer,
mojo::PendingRemote<audio::mojom::StreamFactory> audio_stream_factory,
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));
current_video_capture_params_ = std::move(capture_params);
const auto capture_size = current_video_capture_params_->GetCaptureSize();
media::VideoEncoder::Options video_encoder_options;
video_encoder_options.bitrate = CalculateVpxEncoderBitrate(capture_size);
video_encoder_options.framerate = kMaxFrameRate;
video_encoder_options.width = capture_size.width();
video_encoder_options.height = capture_size.height();
// 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;
const auto audio_params = GetAudioParameters();
encoder_muxer_ = RecordingEncoderMuxer::Create(
encoding_task_runner_, video_encoder_options, audio_params,
base::BindRepeating(&RecordingService::OnMuxerWrite,
base::Unretained(this)),
base::BindOnce(&RecordingService::OnEncodingFailure,
base::Unretained(this)));
ConnectAndStartVideoCapturer(std::move(video_capturer));
audio_capturer_ = audio::CreateInputDevice(
std::move(audio_stream_factory),
std::string(media::AudioDeviceDescription::kDefaultDeviceId),
audio::DeadStreamDetection::kEnabled);
DCHECK(audio_capturer_);
audio_capturer_->Initialize(audio_params, this);
audio_capturer_->Start();
}
void RecordingService::TerminateRecording(bool success) {
DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);
DCHECK(encoder_muxer_);
current_video_capture_params_.reset();
video_capturer_remote_.reset();
consumer_receiver_.reset();
encoder_muxer_.AsyncCall(&RecordingEncoderMuxer::FlushAndFinalize)
.WithArgs(base::BindOnce(&RecordingService::OnEncoderMuxerFlushed,
weak_ptr_factory_.GetWeakPtr(), success));
}
void RecordingService::ConnectAndStartVideoCapturer(
mojo::PendingRemote<viz::mojom::FrameSinkVideoCapturer> video_capturer) {
DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);
DCHECK(current_video_capture_params_);
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_);
video_capturer_remote_->Start(consumer_receiver_.BindNewPipeAndPassRemote());
}
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;
audio_capturer_->Stop();
audio_capturer_.reset();
TerminateRecording(/*success=*/false);
}
void RecordingService::OnAudioCaptured(
std::unique_ptr<media::AudioBus> audio_bus,
base::TimeTicks audio_capture_time) {
DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);
DCHECK(encoder_muxer_);
// We ignore any subsequent frames after a failure.
if (did_failure_occur_)
return;
encoder_muxer_.AsyncCall(&RecordingEncoderMuxer::EncodeAudio)
.WithArgs(std::move(audio_bus), audio_capture_time);
}
void RecordingService::OnEncodingFailure(FailureType type, bool for_video) {
DCHECK_CALLED_ON_VALID_SEQUENCE(encoding_sequence_checker_);
main_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&RecordingService::OnRecordingFailure,
base::Unretained(this)));
}
void RecordingService::OnRecordingFailure() {
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(/*success=*/false);
}
void RecordingService::OnEncoderMuxerFlushed(bool success) {
DCHECK_CALLED_ON_VALID_SEQUENCE(encoding_sequence_checker_);
DCHECK(encoder_muxer_);
// If flushing the encoders and muxers resulted in some chunks being cached
// here, we flush them to the client now.
if (number_of_buffered_chunks_)
FlushBufferedChunks();
encoder_muxer_.Reset();
main_task_runner_->PostNonNestableTask(
FROM_HERE, base::BindOnce(&RecordingService::SignalRecordingEndedToClient,
base::Unretained(this), success));
}
void RecordingService::SignalMuxerOutputToClient(std::string muxer_output) {
DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);
client_remote_->OnMuxerOutput(std::move(muxer_output));
}
void RecordingService::SignalRecordingEndedToClient(bool success) {
DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_);
client_remote_->OnRecordingEnded(success);
}
void RecordingService::OnMuxerWrite(base::StringPiece data) {
DCHECK_CALLED_ON_VALID_SEQUENCE(encoding_sequence_checker_);
++number_of_buffered_chunks_;
muxed_chunks_buffer_.append(data.begin(), data.end());
if (number_of_buffered_chunks_ >= kMaxBufferedChunks)
FlushBufferedChunks();
}
void RecordingService::FlushBufferedChunks() {
DCHECK_CALLED_ON_VALID_SEQUENCE(encoding_sequence_checker_);
DCHECK(number_of_buffered_chunks_);
main_task_runner_->PostNonNestableTask(
FROM_HERE,
base::BindOnce(&RecordingService::SignalMuxerOutputToClient,
base::Unretained(this), std::move(muxed_chunks_buffer_)));
number_of_buffered_chunks_ = 0;
}
} // namespace recording