blob: 0b2b74b7dfb0c21002ef8440d7a16ab1e1ae2797 [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromecast/media/audio/cast_audio_output_device.h"
#include <cstdint>
#include <limits>
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/check.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "base/timer/timer.h"
#include "chromecast/media/audio/audio_io_thread.h"
#include "chromecast/media/audio/audio_output_service/audio_output_service.pb.h"
#include "chromecast/media/audio/audio_output_service/output_stream_connection.h"
#include "chromecast/media/base/default_monotonic_clock.h"
#include "media/base/audio_bus.h"
#include "media/base/audio_parameters.h"
#include "media/base/audio_timestamp_helper.h"
#include "media/base/bind_to_current_loop.h"
#include "net/base/io_buffer.h"
namespace chromecast {
namespace media {
namespace {
constexpr base::TimeDelta kNoBufferReadDelay = base::Milliseconds(4);
} // namespace
// Internal helper class which is constructed and used on an IO thread.
class CastAudioOutputDevice::Internal
: public audio_output_service::OutputStreamConnection::Delegate {
public:
explicit Internal(
mojo::PendingRemote<mojom::AudioSocketBroker> audio_socket_broker,
mojo::PendingRemote<::media::mojom::CastApplicationMediaInfoManager>
pending_app_media_info_manager)
: pending_socket_broker_(std::move(audio_socket_broker)),
app_media_info_manager_(std::move(pending_app_media_info_manager)) {
DCHECK(pending_socket_broker_);
DCHECK(app_media_info_manager_);
}
Internal(const Internal&) = delete;
Internal& operator=(const Internal&) = delete;
~Internal() override = default;
void Initialize(scoped_refptr<CastAudioOutputDevice> output_device,
const ::media::AudioParameters& audio_params) {
DCHECK(!output_device_);
DCHECK(output_device);
output_device_ = std::move(output_device);
audio_params_ = audio_params;
app_media_info_manager_->GetCastApplicationMediaInfo(base::BindOnce(
&Internal::OnApplicationMediaInfoReceived, base::Unretained(this)));
}
void Start() {
playback_started_ = true;
media_pos_frames_ = 0;
if (!backend_initialized_) {
// Wait for initialization to complete before sending messages through
// `output_connection_`.
return;
}
output_connection_->StartPlayingFrom(0);
}
void Pause() {
paused_ = true;
if (!backend_initialized_) {
return;
}
output_connection_->SetPlaybackRate(0.0f);
push_timer_.Stop();
}
void Play() {
paused_ = false;
if (!playback_started_) {
Start();
}
if (!backend_initialized_) {
return;
}
output_connection_->SetPlaybackRate(1.0f);
}
void Flush() {
if (!backend_initialized_) {
return;
}
media_pos_frames_ = 0;
playback_started_ = false;
paused_ = false;
push_timer_.Stop();
output_connection_->StopPlayback();
}
void SetVolume(double volume) {
volume_ = volume;
if (!backend_initialized_) {
return;
}
output_connection_->SetVolume(volume_);
}
private:
// audio_output_service::OutputStreamConnection::Delegate implementation:
void OnBackendInitialized(
const audio_output_service::BackendInitializationStatus& status)
override {
if (status.status() !=
audio_output_service::BackendInitializationStatus::SUCCESS) {
LOG(ERROR) << "Error initializing the audio backend.";
if (output_device_) {
output_device_->OnBackendError();
}
return;
}
DCHECK(output_connection_);
backend_initialized_ = true;
output_connection_->SetVolume(volume_);
if (paused_) {
output_connection_->SetPlaybackRate(0.0f);
}
if (!playback_started_) {
// Wait for Start() to be called before schedule reading buffers.
return;
}
output_connection_->StartPlayingFrom(0);
if (!paused_) {
output_connection_->SetPlaybackRate(1.0f);
}
}
void OnNextBuffer(int64_t media_timestamp_microseconds,
int64_t reference_timestamp_microseconds,
int64_t delay_microseconds,
int64_t delay_timestamp_microseconds) override {
rendering_delay_ = base::Microseconds(delay_microseconds);
rendering_delay_timestamp_us_ = delay_timestamp_microseconds;
TryPushBuffer();
}
void OnApplicationMediaInfoReceived(
::media::mojom::CastApplicationMediaInfoPtr application_media_info) {
audio_output_service::CmaBackendParams cma_backend_params;
audio_output_service::AudioDecoderConfig* audio_config =
cma_backend_params.mutable_audio_decoder_config();
audio_config->set_audio_codec(audio_service::AudioCodec::AUDIO_CODEC_PCM);
audio_config->set_sample_rate(audio_params_.sample_rate());
audio_config->set_sample_format(
audio_service::SampleFormat::SAMPLE_FORMAT_INT16_I);
audio_config->set_num_channels(audio_params_.channels());
audio_output_service::ApplicationMediaInfo* app_media_info =
cma_backend_params.mutable_application_media_info();
app_media_info->set_application_session_id(
application_media_info->application_session_id);
audio_bus_ = ::media::AudioBus::Create(audio_params_);
output_connection_ =
std::make_unique<audio_output_service::OutputStreamConnection>(
this, cma_backend_params, std::move(pending_socket_broker_));
output_connection_->Connect();
}
void TryPushBuffer() {
if (paused_ || !backend_initialized_ || !playback_started_ ||
push_timer_.IsRunning()) {
return;
}
PushBuffer();
}
void PushBuffer() {
if (!output_device_) {
return;
}
base::TimeDelta delay;
if (rendering_delay_ < base::TimeDelta() ||
rendering_delay_timestamp_us_ < 0) {
delay = base::TimeDelta();
} else {
delay =
rendering_delay_ + base::Microseconds(rendering_delay_timestamp_us_ -
MonotonicClockNow());
if (delay < base::TimeDelta()) {
delay = base::TimeDelta();
}
}
int frames_filled = output_device_->ReadBuffer(delay, audio_bus_.get());
if (frames_filled) {
size_t filled_bytes = frames_filled * audio_params_.GetBytesPerFrame(
::media::kSampleFormatS16);
size_t io_buffer_size =
audio_output_service::OutputSocket::kAudioMessageHeaderSize +
filled_bytes;
auto io_buffer = base::MakeRefCounted<net::IOBuffer>(io_buffer_size);
audio_bus_->ToInterleaved<::media::SignedInt16SampleTypeTraits>(
frames_filled,
reinterpret_cast<int16_t*>(
io_buffer->data() +
audio_output_service::OutputSocket::kAudioMessageHeaderSize));
auto media_pos = ::media::AudioTimestampHelper::FramesToTime(
media_pos_frames_, audio_params_.sample_rate());
output_connection_->SendAudioBuffer(std::move(io_buffer), filled_bytes,
media_pos.InMicroseconds());
media_pos_frames_ += frames_filled;
// No need to schedule buffer read here since
// `OnNextBuffer` will be called once the current
// buffer is pushed to media backend.
return;
}
// No frames filled, schedule read immediately with a small delay.
push_timer_.Start(FROM_HERE, base::TimeTicks::Now() + kNoBufferReadDelay,
this, &Internal::TryPushBuffer,
base::ExactDeadline(true));
}
scoped_refptr<CastAudioOutputDevice> output_device_;
std::unique_ptr<audio_output_service::OutputStreamConnection>
output_connection_;
mojo::PendingRemote<mojom::AudioSocketBroker> pending_socket_broker_;
mojo::Remote<::media::mojom::CastApplicationMediaInfoManager>
app_media_info_manager_;
::media::AudioParameters audio_params_;
size_t media_pos_frames_ = 0;
base::TimeDelta rendering_delay_;
int64_t rendering_delay_timestamp_us_ = INT64_MIN;
double volume_ = 1.0;
bool paused_ = false;
bool playback_started_ = false;
bool backend_initialized_ = false;
base::DeadlineTimer push_timer_;
std::unique_ptr<::media::AudioBus> audio_bus_;
};
CastAudioOutputDevice::CastAudioOutputDevice(
mojo::PendingRemote<mojom::AudioSocketBroker> audio_socket_broker,
mojo::PendingRemote<::media::mojom::CastApplicationMediaInfoManager>
application_media_info_manager)
: CastAudioOutputDevice(std::move(audio_socket_broker),
std::move(application_media_info_manager),
AudioIoThread::Get()->task_runner()) {}
CastAudioOutputDevice::CastAudioOutputDevice(
mojo::PendingRemote<mojom::AudioSocketBroker> audio_socket_broker,
mojo::PendingRemote<::media::mojom::CastApplicationMediaInfoManager>
application_media_info_manager,
scoped_refptr<base::SequencedTaskRunner> task_runner)
: task_runner_(std::move(task_runner)),
internal_(task_runner_,
std::move(audio_socket_broker),
std::move(application_media_info_manager)) {}
CastAudioOutputDevice::~CastAudioOutputDevice() = default;
void CastAudioOutputDevice::Initialize(const ::media::AudioParameters& params,
RenderCallback* callback) {
DCHECK(callback);
DCHECK(!render_callback_);
{
base::AutoLock lock(callback_lock_);
active_render_callback_ = callback;
}
render_callback_ = callback;
internal_.AsyncCall(&Internal::Initialize)
.WithArgs(scoped_refptr<CastAudioOutputDevice>(this), params);
}
void CastAudioOutputDevice::Start() {
{
base::AutoLock lock(callback_lock_);
active_render_callback_ = render_callback_;
}
internal_.AsyncCall(&Internal::Start);
}
void CastAudioOutputDevice::Stop() {
{
base::AutoLock lock(callback_lock_);
active_render_callback_ = nullptr;
}
Flush();
}
void CastAudioOutputDevice::Pause() {
internal_.AsyncCall(&Internal::Pause);
}
void CastAudioOutputDevice::Play() {
internal_.AsyncCall(&Internal::Play);
}
void CastAudioOutputDevice::Flush() {
internal_.AsyncCall(&Internal::Flush);
}
bool CastAudioOutputDevice::SetVolume(double volume) {
internal_.AsyncCall(&Internal::SetVolume).WithArgs(volume);
return true;
}
::media::OutputDeviceInfo CastAudioOutputDevice::GetOutputDeviceInfo() {
// Same as the set of parameters returned in
// CastAudioManager::GetPreferredOutputStreamParameters.
return ::media::OutputDeviceInfo(
std::string(), ::media::OUTPUT_DEVICE_STATUS_OK,
::media::AudioParameters(::media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
::media::ChannelLayoutConfig::Stereo(), 48000,
480));
}
void CastAudioOutputDevice::GetOutputDeviceInfoAsync(
OutputDeviceInfoCB info_cb) {
// Always post to avoid the caller being reentrant.
::media::BindToCurrentLoop(
base::BindOnce(std::move(info_cb), GetOutputDeviceInfo()))
.Run();
}
bool CastAudioOutputDevice::IsOptimizedForHardwareParameters() {
return false;
}
bool CastAudioOutputDevice::CurrentThreadIsRenderingThread() {
return task_runner_->RunsTasksInCurrentSequence();
}
void CastAudioOutputDevice::OnBackendError() {
base::AutoLock lock(callback_lock_);
if (active_render_callback_)
active_render_callback_->OnRenderError();
}
int CastAudioOutputDevice::ReadBuffer(base::TimeDelta delay,
::media::AudioBus* audio_bus) {
DCHECK(audio_bus);
base::AutoLock lock(callback_lock_);
if (!active_render_callback_) {
return 0;
}
return active_render_callback_->Render(delay, base::TimeTicks(),
/*frames_skipped=*/0, audio_bus);
}
} // namespace media
} // namespace chromecast