blob: 94bb9360f1360bba01b14f120ea7d676146751b5 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/mediastream/track_audio_renderer.h"
#include <utility>
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/synchronization/lock.h"
#include "base/trace_event/trace_event.h"
#include "media/audio/audio_sink_parameters.h"
#include "media/base/audio_bus.h"
#include "media/base/audio_latency.h"
#include "media/base/audio_shifter.h"
#include "third_party/blink/public/platform/modules/mediastream/web_media_stream_track.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/platform/mediastream/media_stream_audio_track.h"
#include "third_party/blink/renderer/platform/scheduler/public/post_cross_thread_task.h"
#include "third_party/blink/renderer/platform/wtf/cross_thread_copier_base.h"
#include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
namespace blink {
template <>
struct CrossThreadCopier<media::AudioParameters> {
STATIC_ONLY(CrossThreadCopier);
using Type = media::AudioParameters;
static Type Copy(Type pointer) { return pointer; }
};
namespace {
// Translates |num_samples_rendered| into a TimeDelta duration and adds it to
// |prior_elapsed_render_time|.
base::TimeDelta ComputeTotalElapsedRenderTime(
base::TimeDelta prior_elapsed_render_time,
int64_t num_samples_rendered,
int sample_rate) {
return prior_elapsed_render_time +
base::Microseconds(num_samples_rendered *
base::Time::kMicrosecondsPerSecond / sample_rate);
}
WebLocalFrame* ToWebLocalFrame(LocalFrame* frame) {
if (!frame)
return nullptr;
return static_cast<WebLocalFrame*>(WebFrame::FromCoreFrame(frame));
}
bool RequiresSinkReconfig(const media::AudioParameters& old_format,
const media::AudioParameters& new_format) {
// Always favor |new_format| if our current params are invalid. This avoids
// the edge case where |current_params| is valid except for 0
// frames_per_buffer(), and never gets replaced by an almost identical
// |new_format| with a valid frames_per_buffer().
if (!old_format.IsValid())
return true;
// Ignore frames_per_buffer(), since the AudioRendererSink and the
// AudioShifter handle those variations adequately.
media::AudioParameters new_format_copy = new_format;
new_format_copy.set_frames_per_buffer(old_format.frames_per_buffer());
return !old_format.Equals(new_format_copy);
}
} // namespace
TrackAudioRenderer::PendingData::PendingData(const media::AudioBus& audio_bus,
base::TimeTicks ref_time)
: reference_time(ref_time),
audio(media::AudioBus::Create(audio_bus.channels(), audio_bus.frames())) {
audio_bus.CopyTo(audio.get());
}
TrackAudioRenderer::PendingReconfig::PendingReconfig(
const media::AudioParameters& format,
int reconfig_number)
: reconfig_number(reconfig_number), format(format) {}
// media::AudioRendererSink::RenderCallback implementation
int TrackAudioRenderer::Render(base::TimeDelta delay,
base::TimeTicks delay_timestamp,
const media::AudioGlitchInfo& glitch_info,
media::AudioBus* audio_bus) {
TRACE_EVENT("audio", "TrackAudioRenderer::Render", "playout_delay (ms)",
delay.InMillisecondsF(), "delay_timestamp (ms)",
(delay_timestamp - base::TimeTicks()).InMillisecondsF());
base::AutoLock auto_lock(thread_lock_);
if (!audio_shifter_) {
audio_bus->Zero();
return 0;
}
const base::TimeTicks playout_time = delay_timestamp + delay;
DVLOG(2) << "Pulling audio out of shifter to be played "
<< delay.InMilliseconds() << " ms from now.";
audio_shifter_->Pull(audio_bus, playout_time);
num_samples_rendered_ += audio_bus->frames();
return audio_bus->frames();
}
void TrackAudioRenderer::OnRenderErrorCrossThread() {
DCHECK(task_runner_->BelongsToCurrentThread());
on_render_error_callback_.Run();
}
void TrackAudioRenderer::OnRenderError() {
DCHECK(on_render_error_callback_);
PostCrossThreadTask(
*task_runner_, FROM_HERE,
CrossThreadBindOnce(&TrackAudioRenderer::OnRenderErrorCrossThread,
WrapRefCounted(this)));
}
// WebMediaStreamAudioSink implementation
void TrackAudioRenderer::OnData(const media::AudioBus& audio_bus,
base::TimeTicks reference_time) {
TRACE_EVENT("audio", "TrackAudioRenderer::OnData", "capture_time (ms)",
(reference_time - base::TimeTicks()).InMillisecondsF(),
"capture_delay (ms)",
(base::TimeTicks::Now() - reference_time).InMillisecondsF());
base::AutoLock auto_lock(thread_lock_);
// There is a pending ReconfigureSink() call. Copy |audio_bus| so it can be
// pushed in to the |audio_shifter_| (or dropped later).
if (!pending_reconfigs_.empty()) {
// Copies |audio_bus| internally.
pending_reconfigs_.back().data.emplace_back(audio_bus, reference_time);
return;
}
if (!audio_shifter_)
return;
std::unique_ptr<media::AudioBus> audio_data(
media::AudioBus::Create(audio_bus.channels(), audio_bus.frames()));
audio_bus.CopyTo(audio_data.get());
// Note: For remote audio sources, |reference_time| is the local playout time,
// the ideal point-in-time at which the first audio sample should be played
// out in the future. For local sources, |reference_time| is the
// point-in-time at which the first audio sample was captured in the past. In
// either case, AudioShifter will auto-detect and do the right thing when
// audio is pulled from it.
PushDataIntoShifter_Locked(std::move(audio_data), reference_time);
}
void TrackAudioRenderer::OnSetFormat(const media::AudioParameters& params) {
DVLOG(1) << "TrackAudioRenderer::OnSetFormat: "
<< params.AsHumanReadableString();
// Don't attempt call ReconfigureSink() if the |last_reconfig_format_|
// is compatible (e.g. identical, or varies only by frames_per_buffer()).
if (!RequiresSinkReconfig(last_reconfig_format_, params))
return;
int reconfig_number;
{
base::AutoLock lock(thread_lock_);
// Keep track of how many ReconfigureSink() calls we have made. This allows
// us to drop all but the latest ReconfigureSink() calls on the main thread.
reconfig_number = ++sink_reconfig_count_;
// As long as there is an entry in |pending_reconfigs_|, we save data
// instead of dropping it, or pushing it into |audio_shifter_|. This queue
// entry is popped in ReconfigureSink().
pending_reconfigs_.push_back(PendingReconfig(params, reconfig_number));
}
// Post a task on the main render thread to reconfigure the |sink_| with the
// new format.
PostCrossThreadTask(
*task_runner_, FROM_HERE,
CrossThreadBindOnce(&TrackAudioRenderer::ReconfigureSink,
WrapRefCounted(this), params, reconfig_number));
last_reconfig_format_ = params;
}
TrackAudioRenderer::TrackAudioRenderer(
MediaStreamComponent* audio_component,
LocalFrame& playout_frame,
const String& device_id,
base::RepeatingClosure on_render_error_callback)
: audio_component_(audio_component),
playout_frame_(playout_frame),
task_runner_(
playout_frame.GetTaskRunner(blink::TaskType::kInternalMedia)),
on_render_error_callback_(std::move(on_render_error_callback)),
output_device_id_(device_id) {
DCHECK(MediaStreamAudioTrack::From(audio_component_.Get()));
DCHECK(task_runner_->BelongsToCurrentThread());
DVLOG(1) << "TrackAudioRenderer::TrackAudioRenderer()";
}
TrackAudioRenderer::~TrackAudioRenderer() {
DCHECK(task_runner_->BelongsToCurrentThread());
DCHECK(!sink_);
DVLOG(1) << "TrackAudioRenderer::~TrackAudioRenderer()";
}
void TrackAudioRenderer::Start() {
DVLOG(1) << "TrackAudioRenderer::Start()";
DCHECK(task_runner_->BelongsToCurrentThread());
DCHECK_EQ(playing_, false);
// We get audio data from |audio_component_|...
WebMediaStreamAudioSink::AddToAudioTrack(
this, WebMediaStreamTrack(audio_component_.Get()));
// ...and |sink_| will get audio data from us.
DCHECK(!sink_);
sink_ = Platform::Current()->NewAudioRendererSink(
WebAudioDeviceSourceType::kNonRtcAudioTrack,
ToWebLocalFrame(playout_frame_),
{base::UnguessableToken(), output_device_id_.Utf8()});
base::AutoLock auto_lock(thread_lock_);
prior_elapsed_render_time_ = base::TimeDelta();
num_samples_rendered_ = 0;
}
void TrackAudioRenderer::Stop() {
DVLOG(1) << "TrackAudioRenderer::Stop()";
DCHECK(task_runner_->BelongsToCurrentThread());
Pause();
// Stop the output audio stream, i.e, stop asking for data to render.
// It is safer to call Stop() on the |sink_| to clean up the resources even
// when the |sink_| is never started.
if (sink_) {
sink_->Stop();
sink_ = nullptr;
}
sink_started_ = false;
// Ensure that the capturer stops feeding us with captured audio.
WebMediaStreamAudioSink::RemoveFromAudioTrack(
this, WebMediaStreamTrack(audio_component_.Get()));
}
void TrackAudioRenderer::Play() {
DVLOG(1) << "TrackAudioRenderer::Play()";
DCHECK(task_runner_->BelongsToCurrentThread());
if (!sink_)
return;
playing_ = true;
MaybeStartSink();
}
void TrackAudioRenderer::Pause() {
DVLOG(1) << "TrackAudioRenderer::Pause()";
DCHECK(task_runner_->BelongsToCurrentThread());
if (!sink_)
return;
playing_ = false;
base::AutoLock auto_lock(thread_lock_);
HaltAudioFlow_Locked();
}
void TrackAudioRenderer::SetVolume(float volume) {
DVLOG(1) << "TrackAudioRenderer::SetVolume(" << volume << ")";
DCHECK(task_runner_->BelongsToCurrentThread());
// Cache the volume. Whenever |sink_| is re-created, call SetVolume() with
// this cached volume.
volume_ = volume;
if (sink_)
sink_->SetVolume(volume);
}
base::TimeDelta TrackAudioRenderer::GetCurrentRenderTime() {
DCHECK(task_runner_->BelongsToCurrentThread());
base::AutoLock auto_lock(thread_lock_);
if (source_params_.IsValid()) {
return ComputeTotalElapsedRenderTime(prior_elapsed_render_time_,
num_samples_rendered_,
source_params_.sample_rate());
}
return prior_elapsed_render_time_;
}
void TrackAudioRenderer::SwitchOutputDevice(
const std::string& device_id,
media::OutputDeviceStatusCB callback) {
DVLOG(1) << "TrackAudioRenderer::SwitchOutputDevice()";
DCHECK(task_runner_->BelongsToCurrentThread());
{
base::AutoLock auto_lock(thread_lock_);
HaltAudioFlow_Locked();
}
scoped_refptr<media::AudioRendererSink> new_sink =
Platform::Current()->NewAudioRendererSink(
WebAudioDeviceSourceType::kNonRtcAudioTrack,
ToWebLocalFrame(playout_frame_),
{base::UnguessableToken(), device_id});
media::OutputDeviceStatus new_sink_status =
new_sink->GetOutputDeviceInfo().device_status();
UMA_HISTOGRAM_ENUMERATION("Media.Audio.TrackAudioRenderer.SwitchDeviceStatus",
new_sink_status,
media::OUTPUT_DEVICE_STATUS_MAX + 1);
if (new_sink_status != media::OUTPUT_DEVICE_STATUS_OK) {
new_sink->Stop();
std::move(callback).Run(new_sink_status);
return;
}
output_device_id_ = String(device_id);
bool was_sink_started = sink_started_;
if (sink_)
sink_->Stop();
sink_started_ = false;
sink_ = new_sink;
if (was_sink_started)
MaybeStartSink();
std::move(callback).Run(media::OUTPUT_DEVICE_STATUS_OK);
}
void TrackAudioRenderer::MaybeStartSink(bool reconfiguring) {
DCHECK(task_runner_->BelongsToCurrentThread());
DVLOG(1) << "TrackAudioRenderer::MaybeStartSink()";
if (!sink_ || !source_params_.IsValid() || !playing_)
return;
// Re-create the AudioShifter to drop old audio data and reset to a starting
// state. MaybeStartSink() is always called in a situation where either the
// source or sink has changed somehow and so all of AudioShifter's internal
// time-sync state is invalid.
CreateAudioShifter(reconfiguring);
if (sink_started_)
return;
const media::OutputDeviceInfo& device_info = sink_->GetOutputDeviceInfo();
UMA_HISTOGRAM_ENUMERATION("Media.Audio.TrackAudioRenderer.DeviceStatus",
device_info.device_status(),
media::OUTPUT_DEVICE_STATUS_MAX + 1);
if (device_info.device_status() != media::OUTPUT_DEVICE_STATUS_OK)
return;
// Output parameters consist of the same channel layout and sample rate as the
// source, but having the buffer duration preferred by the hardware.
const media::AudioParameters& hardware_params = device_info.output_params();
media::AudioParameters sink_params(
hardware_params.format(), source_params_.channel_layout_config(),
source_params_.sample_rate(),
media::AudioLatency::GetRtcBufferSize(
source_params_.sample_rate(), hardware_params.frames_per_buffer()));
if (sink_params.channel_layout() == media::CHANNEL_LAYOUT_DISCRETE) {
DCHECK_LE(source_params_.channels(), 2);
}
DVLOG(1) << ("TrackAudioRenderer::MaybeStartSink() -- Starting sink. "
"source_params={")
<< source_params_.AsHumanReadableString() << "}, hardware_params={"
<< hardware_params.AsHumanReadableString() << "}, sink parameters={"
<< sink_params.AsHumanReadableString() << '}';
// Specify the latency info to be passed to the browser side.
sink_params.set_latency_tag(Platform::Current()->GetAudioSourceLatencyType(
WebAudioDeviceSourceType::kNonRtcAudioTrack));
sink_->Initialize(sink_params, this);
sink_->Start();
sink_->SetVolume(volume_);
sink_->Play(); // Not all the sinks play on start.
sink_started_ = true;
}
void TrackAudioRenderer::ReconfigureSink(
const media::AudioParameters new_format,
int reconfig_number) {
DCHECK(task_runner_->BelongsToCurrentThread());
{
base::AutoLock lock(thread_lock_);
DCHECK(!pending_reconfigs_.empty());
DCHECK_EQ(pending_reconfigs_.front().reconfig_number, reconfig_number);
// ReconfigureSink() is only posted by OnSetFormat() when an incoming format
// is incompatible with |last_reconfig_format_|. A mismatch between
// |reconfig_number| and |sink_reconfig_count_| means there is at least
// one more pending ReconfigureSink() call, which is definitively
// incompatible with |new_format|. If so, ignore this reconfiguration, to
// avoid creating a sink which would be immediately destroyed by the next
// ReconfigureSink() call.
if (reconfig_number != sink_reconfig_count_) {
// Drop any pending data for this |reconfig_number|, as we won't have
// an |audio_shifter_| or a |sink_| configured to ingest this data.
pending_reconfigs_.pop_front();
return;
}
// The |new_format| is compatible with the existing one. Skip this
// reconfiguration.
if (!RequiresSinkReconfig(source_params_, new_format)) {
// Push pending data into |audio_shifter_|, if we have one, or clear
// the entry corresponding to this |reconfig_number|.
if (audio_shifter_)
ConsumePendingReconfigsFront_Locked();
else
pending_reconfigs_.pop_front();
return;
}
// If we need to reconfigure, drop all existing |audio_shifter_| data, as it
// won't be compatible with the new shifter and data in
// |pending_reconfigs_.front()|.
if (audio_shifter_)
HaltAudioFlow_Locked();
}
source_params_ = new_format;
if (!sink_)
return; // TrackAudioRenderer has not yet been started.
// Stop |sink_| and re-create a new one to be initialized with different audio
// parameters. Then, invoke MaybeStartSink() to restart everything again.
sink_->Stop();
sink_started_ = false;
sink_ = Platform::Current()->NewAudioRendererSink(
WebAudioDeviceSourceType::kNonRtcAudioTrack,
ToWebLocalFrame(playout_frame_),
{base::UnguessableToken(), output_device_id_.Utf8()});
MaybeStartSink(/*reconfiguring=*/true);
{
base::AutoLock lock(thread_lock_);
// We may have never created |audio_shifter_| (e.g. if the sink isn't
// playing). Clear the corresponding |pending_reconfigs_| entry, so
// we start dropping incoming data in OnData().
if (!audio_shifter_)
pending_reconfigs_.pop_front();
}
}
void TrackAudioRenderer::CreateAudioShifter(bool reconfiguring) {
DCHECK(task_runner_->BelongsToCurrentThread());
// Note 1: The max buffer is fairly large to cover the case where
// remotely-sourced audio is delivered well ahead of its scheduled playout
// time (e.g., content streaming with a very large end-to-end
// latency). However, there is no penalty for making it large in the
// low-latency use cases since AudioShifter will discard data as soon as it is
// no longer needed.
//
// Note 2: The clock accuracy is set to 20ms because clock accuracy is
// ~15ms on Windows machines without a working high-resolution clock. See
// comments in base/time/time.h for details.
media::AudioShifter* const new_shifter = new media::AudioShifter(
base::Seconds(5), base::Milliseconds(20), base::Seconds(20),
source_params_.sample_rate(), source_params_.channels());
base::AutoLock auto_lock(thread_lock_);
audio_shifter_.reset(new_shifter);
// There might be pending data that needs to be pushed into |audio_shifter_|.
if (reconfiguring)
ConsumePendingReconfigsFront_Locked();
}
void TrackAudioRenderer::HaltAudioFlow_Locked() {
thread_lock_.AssertAcquired();
audio_shifter_.reset();
if (source_params_.IsValid()) {
prior_elapsed_render_time_ = ComputeTotalElapsedRenderTime(
prior_elapsed_render_time_, num_samples_rendered_,
source_params_.sample_rate());
num_samples_rendered_ = 0;
}
}
void TrackAudioRenderer::ConsumePendingReconfigsFront_Locked() {
thread_lock_.AssertAcquired();
DCHECK(audio_shifter_);
PendingReconfig& current_reconfig = pending_reconfigs_.front();
DCHECK(!RequiresSinkReconfig(source_params_, current_reconfig.format));
auto& pending_data = current_reconfig.data;
for (auto& data : pending_data)
PushDataIntoShifter_Locked(std::move(data.audio), data.reference_time);
// Once |pending_reconfigs_| is empty, new data will be pushed directly
// into |audio_shifter_|. If it isn't empty, there is another
// ReconfigureSink() in flight.
pending_reconfigs_.pop_front();
}
void TrackAudioRenderer::PushDataIntoShifter_Locked(
std::unique_ptr<media::AudioBus> data,
base::TimeTicks reference_time) {
thread_lock_.AssertAcquired();
DCHECK(audio_shifter_);
total_frames_pushed_for_testing_ += data->frames();
audio_shifter_->Push(std::move(data), reference_time);
}
int TrackAudioRenderer::TotalFramesPushedForTesting() const {
base::AutoLock auto_lock(thread_lock_);
return total_frames_pushed_for_testing_;
}
int TrackAudioRenderer::FramesInAudioShifterForTesting() const {
base::AutoLock auto_lock(thread_lock_);
return audio_shifter_ ? audio_shifter_->frames_pushed_for_testing() : 0;
}
} // namespace blink