blob: d84b381a94622e27281e80996dd745e47baa3e5b [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 "media/fuchsia/audio/fuchsia_audio_output_device.h"
#include "base/fuchsia/fuchsia_logging.h"
#include "base/logging.h"
#include "base/memory/shared_memory_mapping.h"
#include "base/memory/writable_shared_memory_region.h"
#include "base/no_destructor.h"
#include "base/threading/thread.h"
#include "base/threading/thread_task_runner_handle.h"
#include "media/base/audio_timestamp_helper.h"
namespace media {
namespace {
// Total number of buffers used for AudioConsumer.
constexpr size_t kNumBuffers = 4;
// Extra lead time added to min_lead_time reported by AudioConsumer when
// scheduling PumpSamples() timer. This is necessary to make it more likely
// that each packet is sent on time, even if the timer is delayed. Higher values
// increase playback latency, but make underflow less likely. 20ms allows to
// keep latency reasonably low, while making playback reliable under normal
// conditions.
//
// TODO(crbug.com/1153909): It may be possible to reduce this value to reduce
// total latency, but that requires that an elevated scheduling profile is
// applied to this thread.
constexpr base::TimeDelta kLeadTimeExtra = base::Milliseconds(20);
class DefaultAudioThread {
public:
DefaultAudioThread() : thread_("FuchsiaAudioOutputDevice") {
base::Thread::Options options(base::MessagePumpType::IO, 0);
options.priority = base::ThreadPriority::REALTIME_AUDIO;
thread_.StartWithOptions(std::move(options));
}
~DefaultAudioThread() = default;
scoped_refptr<base::SingleThreadTaskRunner> task_runner() {
return thread_.task_runner();
}
private:
base::Thread thread_;
};
scoped_refptr<base::SingleThreadTaskRunner> GetDefaultAudioTaskRunner() {
static base::NoDestructor<DefaultAudioThread> default_audio_thread;
return default_audio_thread->task_runner();
}
} // namespace
// static
scoped_refptr<FuchsiaAudioOutputDevice> FuchsiaAudioOutputDevice::Create(
fidl::InterfaceHandle<fuchsia::media::AudioConsumer> audio_consumer_handle,
scoped_refptr<base::SingleThreadTaskRunner> task_runner) {
scoped_refptr<FuchsiaAudioOutputDevice> result(
new FuchsiaAudioOutputDevice(task_runner));
task_runner->PostTask(
FROM_HERE,
base::BindOnce(&FuchsiaAudioOutputDevice::BindAudioConsumerOnAudioThread,
result, std::move(audio_consumer_handle)));
return result;
}
// static
scoped_refptr<FuchsiaAudioOutputDevice>
FuchsiaAudioOutputDevice::CreateOnDefaultThread(
fidl::InterfaceHandle<fuchsia::media::AudioConsumer>
audio_consumer_handle) {
return Create(std::move(audio_consumer_handle), GetDefaultAudioTaskRunner());
}
FuchsiaAudioOutputDevice::FuchsiaAudioOutputDevice(
scoped_refptr<base::SingleThreadTaskRunner> task_runner)
: task_runner_(std::move(task_runner)) {}
FuchsiaAudioOutputDevice::~FuchsiaAudioOutputDevice() = default;
void FuchsiaAudioOutputDevice::Initialize(const AudioParameters& params,
RenderCallback* callback) {
DCHECK(callback);
// Save |callback| synchronously here to handle the case when Stop() is called
// before the DoInitialize() task is processed.
{
base::AutoLock auto_lock(callback_lock_);
DCHECK(!callback_);
callback_ = callback;
}
task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&FuchsiaAudioOutputDevice::InitializeOnAudioThread, this,
params));
}
void FuchsiaAudioOutputDevice::Start() {
task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&FuchsiaAudioOutputDevice::StartOnAudioThread, this));
}
void FuchsiaAudioOutputDevice::Stop() {
{
base::AutoLock auto_lock(callback_lock_);
callback_ = nullptr;
}
task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&FuchsiaAudioOutputDevice::StopOnAudioThread, this));
}
void FuchsiaAudioOutputDevice::Pause() {
task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&FuchsiaAudioOutputDevice::PauseOnAudioThread, this));
}
void FuchsiaAudioOutputDevice::Play() {
task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&FuchsiaAudioOutputDevice::PlayOnAudioThread, this));
}
void FuchsiaAudioOutputDevice::Flush() {
task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&FuchsiaAudioOutputDevice::FlushOnAudioThread, this));
}
bool FuchsiaAudioOutputDevice::SetVolume(double volume) {
task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&FuchsiaAudioOutputDevice::SetVolumeOnAudioThread, this,
volume));
return true;
}
OutputDeviceInfo FuchsiaAudioOutputDevice::GetOutputDeviceInfo() {
// AudioConsumer doesn't provider any information about the output device.
//
// TODO(crbug.com/852834): Update this method when that functionality is
// implemented.
return OutputDeviceInfo(
std::string(), OUTPUT_DEVICE_STATUS_OK,
AudioParameters(AudioParameters::AUDIO_PCM_LOW_LATENCY,
CHANNEL_LAYOUT_STEREO, 48000, 480));
}
void FuchsiaAudioOutputDevice::GetOutputDeviceInfoAsync(
OutputDeviceInfoCB info_cb) {
std::move(info_cb).Run(GetOutputDeviceInfo());
}
bool FuchsiaAudioOutputDevice::IsOptimizedForHardwareParameters() {
// AudioConsumer doesn't provide device parameters (since target device may
// change).
return false;
}
bool FuchsiaAudioOutputDevice::CurrentThreadIsRenderingThread() {
return task_runner_->BelongsToCurrentThread();
}
void FuchsiaAudioOutputDevice::BindAudioConsumerOnAudioThread(
fidl::InterfaceHandle<fuchsia::media::AudioConsumer>
audio_consumer_handle) {
DCHECK(CurrentThreadIsRenderingThread());
DCHECK(!audio_consumer_);
audio_consumer_.Bind(std::move(audio_consumer_handle));
audio_consumer_.set_error_handler([this](zx_status_t status) {
ZX_LOG(ERROR, status) << "AudioConsumer disconnected.";
ReportError();
});
}
void FuchsiaAudioOutputDevice::InitializeOnAudioThread(
const AudioParameters& params) {
DCHECK(CurrentThreadIsRenderingThread());
params_ = params;
audio_bus_ = AudioBus::Create(params_);
UpdateVolume();
WatchAudioConsumerStatus();
}
void FuchsiaAudioOutputDevice::StartOnAudioThread() {
DCHECK(CurrentThreadIsRenderingThread());
if (!audio_consumer_)
return;
CreateStreamSink();
media_pos_frames_ = 0;
audio_consumer_->Start(fuchsia::media::AudioConsumerStartFlags::LOW_LATENCY,
fuchsia::media::NO_TIMESTAMP, 0);
// When AudioConsumer handles the Start() message sent above, it will update
// its state and sent WatchStatus() response. OnAudioConsumerStatusChanged()
// will then call SchedulePumpSamples() to start sending audio packets.
}
void FuchsiaAudioOutputDevice::StopOnAudioThread() {
DCHECK(CurrentThreadIsRenderingThread());
if (!audio_consumer_)
return;
audio_consumer_->Stop();
pump_samples_timer_.Stop();
audio_consumer_.Unbind();
stream_sink_.Unbind();
volume_control_.Unbind();
}
void FuchsiaAudioOutputDevice::PauseOnAudioThread() {
DCHECK(CurrentThreadIsRenderingThread());
if (!audio_consumer_)
return;
paused_ = true;
audio_consumer_->SetRate(0.0);
pump_samples_timer_.Stop();
}
void FuchsiaAudioOutputDevice::PlayOnAudioThread() {
DCHECK(CurrentThreadIsRenderingThread());
if (!audio_consumer_)
return;
paused_ = false;
audio_consumer_->SetRate(1.0);
}
void FuchsiaAudioOutputDevice::FlushOnAudioThread() {
DCHECK(CurrentThreadIsRenderingThread());
if (!stream_sink_)
return;
stream_sink_->DiscardAllPacketsNoReply();
}
void FuchsiaAudioOutputDevice::SetVolumeOnAudioThread(double volume) {
DCHECK(CurrentThreadIsRenderingThread());
volume_ = volume;
if (audio_consumer_)
UpdateVolume();
}
void FuchsiaAudioOutputDevice::CreateStreamSink() {
DCHECK(CurrentThreadIsRenderingThread());
DCHECK(audio_consumer_);
// Allocate buffers for the StreamSink.
size_t buffer_size = params_.GetBytesPerBuffer(kSampleFormatF32);
stream_sink_buffers_.reserve(kNumBuffers);
available_buffers_indices_.clear();
std::vector<zx::vmo> vmos_for_stream_sink;
vmos_for_stream_sink.reserve(kNumBuffers);
for (size_t i = 0; i < kNumBuffers; ++i) {
auto region = base::WritableSharedMemoryRegion::Create(buffer_size);
auto mapping = region.Map();
if (!mapping.IsValid()) {
LOG(WARNING) << "Failed to allocate VMO of size " << buffer_size;
ReportError();
return;
}
stream_sink_buffers_.push_back(std::move(mapping));
available_buffers_indices_.push_back(i);
auto read_only_region =
base::WritableSharedMemoryRegion::ConvertToReadOnly(std::move(region));
vmos_for_stream_sink.push_back(
base::ReadOnlySharedMemoryRegion::TakeHandleForSerialization(
std::move(read_only_region))
.PassPlatformHandle());
}
// Configure StreamSink.
fuchsia::media::AudioStreamType stream_type;
stream_type.channels = params_.channels();
stream_type.frames_per_second = params_.sample_rate();
stream_type.sample_format = fuchsia::media::AudioSampleFormat::FLOAT;
audio_consumer_->CreateStreamSink(std::move(vmos_for_stream_sink),
std::move(stream_type), nullptr,
stream_sink_.NewRequest());
stream_sink_.set_error_handler([this](zx_status_t status) {
ZX_LOG(ERROR, status) << "StreamSink disconnected.";
ReportError();
});
}
void FuchsiaAudioOutputDevice::UpdateVolume() {
DCHECK(CurrentThreadIsRenderingThread());
DCHECK(audio_consumer_);
if (!volume_control_) {
audio_consumer_->BindVolumeControl(volume_control_.NewRequest());
volume_control_.set_error_handler([](zx_status_t status) {
ZX_LOG(ERROR, status) << "VolumeControl disconnected.";
});
}
volume_control_->SetVolume(volume_);
}
void FuchsiaAudioOutputDevice::WatchAudioConsumerStatus() {
DCHECK(CurrentThreadIsRenderingThread());
audio_consumer_->WatchStatus(fit::bind_member(
this, &FuchsiaAudioOutputDevice::OnAudioConsumerStatusChanged));
}
void FuchsiaAudioOutputDevice::OnAudioConsumerStatusChanged(
fuchsia::media::AudioConsumerStatus status) {
DCHECK(CurrentThreadIsRenderingThread());
if (!status.has_min_lead_time()) {
DLOG(ERROR) << "AudioConsumerStatus.min_lead_time isn't set.";
ReportError();
return;
}
min_lead_time_ = base::Nanoseconds(status.min_lead_time());
if (status.has_presentation_timeline()) {
timeline_reference_time_ = base::TimeTicks::FromZxTime(
status.presentation_timeline().reference_time);
timeline_subject_time_ =
base::Nanoseconds(status.presentation_timeline().subject_time);
timeline_reference_delta_ = status.presentation_timeline().reference_delta;
timeline_subject_delta_ = status.presentation_timeline().subject_delta;
} else {
// Reset |timeline_reference_time_| to null value, which is used to indicate
// that there is no presentation timeline.
timeline_reference_time_ = base::TimeTicks();
}
// Reschedule the timer for the new timeline.
pump_samples_timer_.Stop();
SchedulePumpSamples();
WatchAudioConsumerStatus();
}
void FuchsiaAudioOutputDevice::SchedulePumpSamples() {
DCHECK(CurrentThreadIsRenderingThread());
if (paused_ || timeline_reference_time_.is_null() ||
pump_samples_timer_.IsRunning() || available_buffers_indices_.empty()) {
return;
}
// Current position in the stream.
auto media_pos = AudioTimestampHelper::FramesToTime(media_pos_frames_,
params_.sample_rate());
// Calculate expected playback time for the next sample based on the
// presentation timeline provided by the AudioConsumer.
// See https://fuchsia.dev/reference/fidl/fuchsia.media#formulas .
// AudioConsumer uses monotonic clock (aka base::TimeTicks) as a reference
// timeline. Subject timeline corresponds to position within the stream, which
// is stored as |media_pos_frames_| and then passed in the |pts| field in each
// packet produced in PumpSamples().
auto playback_time = timeline_reference_time_ +
(media_pos - timeline_subject_time_) *
timeline_reference_delta_ / timeline_subject_delta_;
base::TimeTicks now = base::TimeTicks::Now();
// Target time for when PumpSamples() should run.
base::TimeTicks target_time = playback_time - min_lead_time_ - kLeadTimeExtra;
base::TimeDelta delay = target_time - now;
pump_samples_timer_.Start(
FROM_HERE, delay,
base::BindOnce(&FuchsiaAudioOutputDevice::PumpSamples, this,
playback_time));
}
void FuchsiaAudioOutputDevice::PumpSamples(base::TimeTicks playback_time) {
DCHECK(CurrentThreadIsRenderingThread());
auto now = base::TimeTicks::Now();
int skipped_frames = 0;
// Check if it's too late to send the next packet. If it is, then advance
// current stream position.
auto lead_time = playback_time - now;
if (lead_time < min_lead_time_) {
auto new_playback_time = now + min_lead_time_;
auto skipped_time = new_playback_time - playback_time;
skipped_frames =
AudioTimestampHelper::TimeToFrames(skipped_time, params_.sample_rate());
media_pos_frames_ += skipped_frames;
playback_time += skipped_time;
}
int frames_filled;
{
base::AutoLock auto_lock(callback_lock_);
// |callback_| may be reset in Stop(). No need to keep rendering the stream
// in that case.
if (!callback_)
return;
frames_filled = callback_->Render(playback_time - now, now, skipped_frames,
audio_bus_.get());
}
if (frames_filled) {
DCHECK(!available_buffers_indices_.empty());
int buffer_index = available_buffers_indices_.back();
available_buffers_indices_.pop_back();
audio_bus_->ToInterleaved<Float32SampleTypeTraitsNoClip>(
frames_filled,
static_cast<float*>(stream_sink_buffers_[buffer_index].memory()));
fuchsia::media::StreamPacket packet;
packet.payload_buffer_id = buffer_index;
packet.pts = AudioTimestampHelper::FramesToTime(media_pos_frames_,
params_.sample_rate())
.InNanoseconds();
packet.payload_offset = 0;
packet.payload_size = frames_filled * sizeof(float) * params_.channels();
stream_sink_->SendPacket(std::move(packet), [this, buffer_index]() {
OnStreamSendDone(buffer_index);
});
media_pos_frames_ += frames_filled;
}
SchedulePumpSamples();
}
void FuchsiaAudioOutputDevice::OnStreamSendDone(size_t buffer_index) {
DCHECK(CurrentThreadIsRenderingThread());
available_buffers_indices_.push_back(buffer_index);
SchedulePumpSamples();
}
void FuchsiaAudioOutputDevice::ReportError() {
DCHECK(CurrentThreadIsRenderingThread());
audio_consumer_.Unbind();
stream_sink_.Unbind();
volume_control_.Unbind();
pump_samples_timer_.Stop();
{
base::AutoLock auto_lock(callback_lock_);
if (callback_)
callback_->OnRenderError();
}
}
} // namespace media