[go: nahoru, domu]

blob: c3b41d2baa06951a51b0c4c3df8fc7909165ef40 [file] [log] [blame]
// Copyright 2021 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 "services/audio/output_device_mixer_impl.h"
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/dcheck_is_on.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "media/audio/audio_device_description.h"
#include "media/audio/audio_io.h"
namespace audio {
constexpr base::TimeDelta OutputDeviceMixerImpl::kSwitchToUnmixedPlaybackDelay;
constexpr double OutputDeviceMixerImpl::kDefaultVolume;
namespace {
const char* LatencyToUmaSuffix(media::AudioLatency::LatencyType latency) {
switch (latency) {
case media::AudioLatency::LATENCY_EXACT_MS:
return "LatencyExactMs";
case media::AudioLatency::LATENCY_INTERACTIVE:
return "LatencyInteractive";
case media::AudioLatency::LATENCY_RTC:
return "LatencyRtc";
case media::AudioLatency::LATENCY_PLAYBACK:
return "LatencyPlayback";
default:
return "LatencyUnknown";
}
}
const char* DeviceIdToUmaSuffix(const std::string& device_id) {
if (device_id == "")
return ".Default";
return ".NonDefault";
}
// Do not change: used for UMA reporting, matches
// AudioOutputDeviceMixerStreamStatus from enums.xml.
enum class TrackError {
kNone = 0,
kIndependentOpenFailed,
kIndependentPlaybackFailed,
kMixedOpenFailed,
kMixedPlaybackFailed,
kMaxValue = kMixedPlaybackFailed
};
const char* TrackErrorToString(TrackError error) {
switch (error) {
case TrackError::kIndependentOpenFailed:
return "Failed to open independent rendering stream";
case TrackError::kIndependentPlaybackFailed:
return "Error during independent playback";
case TrackError::kMixedOpenFailed:
return "Failed to open mixing rendering stream";
case TrackError::kMixedPlaybackFailed:
return "Error during mixed playback";
default:
NOTREACHED();
return "No error";
}
}
} // namespace
// Audio data flow though the mixer:
//
// * Independent (ummixed) audio stream playback:
// MixTrack::|audio_source_callback_|
// -> MixTrack::|rendering_stream_|.
//
// * Mixed playback:
// MixTrack::|audio_source_callback_|
// -> MixTrack::|graph_input_|
// --> OutputDeviceMixerImpl::|mixing_graph_|
// ---> OutputDeviceMixerImpl::|mixing_graph_rendering_stream_|
// Helper class which stores all the data associated with a specific audio
// output managed by the mixer. To the clients such an audio output is
// represented as MixableOutputStream (below).
class OutputDeviceMixerImpl::MixTrack final
: public media::AudioOutputStream::AudioSourceCallback {
public:
MixTrack(OutputDeviceMixerImpl* mixer,
std::unique_ptr<MixingGraph::Input> graph_input,
base::OnceClosure on_device_change_callback)
: mixer_(mixer),
on_device_change_callback_(std::move(on_device_change_callback)),
graph_input_(std::move(graph_input)) {}
~MixTrack() final {
DCHECK(!audio_source_callback_);
base::UmaHistogramEnumeration(
"Media.Audio.OutputDeviceMixer.StreamPlaybackStatus", error_);
}
void SetSource(
media::AudioOutputStream::AudioSourceCallback* audio_source_callback) {
DCHECK(!audio_source_callback_ || !audio_source_callback);
audio_source_callback_ = audio_source_callback;
}
void SetVolume(double volume) {
volume_ = volume;
graph_input_->SetVolume(volume);
if (rendering_stream_)
rendering_stream_->SetVolume(volume);
}
double GetVolume() const { return volume_; }
void StartProvidingAudioToMixingGraph() {
TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("audio"),
"MixTrack::StartProvidingAudioToMixingGraph", "this",
static_cast<void*>(this));
DCHECK(audio_source_callback_);
RegisterPlaybackStarted();
graph_input_->Start(audio_source_callback_);
}
void StopProvidingAudioToMixingGraph() {
TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("audio"),
"MixTrack::StopProvidingAudioToMixingGraph", "this",
static_cast<void*>(this));
DCHECK(audio_source_callback_);
graph_input_->Stop();
RegisterPlaybackStopped(PlaybackType::kMixed);
}
bool OpenIndependentRenderingStream() {
TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("audio"),
"MixTrack::OpenIndependentRenderingStream", "this",
static_cast<void*>(this));
DCHECK(!rendering_stream_);
rendering_stream_.reset(
mixer_->CreateAndOpenDeviceStream(graph_input_->GetParams()));
if (rendering_stream_) {
rendering_stream_->SetVolume(volume_);
} else {
LOG(ERROR) << "Failed to open individual rendering stream";
}
return !!rendering_stream_;
}
void StartIndependentRenderingStream() {
TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("audio"),
"MixTrack::StartIndependentRenderingStream", "this",
static_cast<void*>(this));
DCHECK(audio_source_callback_);
if (!rendering_stream_) {
// Open the rendering stream if it's not open yet. It will be closed in
// CloseIndependentRenderingStream() or during destruction.
if (!OpenIndependentRenderingStream()) {
ReportError(TrackError::kIndependentOpenFailed);
return;
}
}
RegisterPlaybackStarted();
rendering_stream_->Start(this);
}
void StopIndependentRenderingStream() {
TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("audio"),
"MixTrack::StopIndependentRenderingStream", "this",
static_cast<void*>(this));
DCHECK(audio_source_callback_);
if (rendering_stream_) {
rendering_stream_->Stop();
RegisterPlaybackStopped(PlaybackType::kIndependent);
}
}
void CloseIndependentRenderingStream() {
TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("audio"),
"MixTrack::CloseIndependentRenderingStream", "this",
static_cast<void*>(this));
DCHECK(!audio_source_callback_);
// Closes the stream.
rendering_stream_.reset();
}
void ReportError(TrackError error) {
TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("audio"), "MixTrack::ReportError",
"this", static_cast<void*>(this));
DCHECK(audio_source_callback_);
DCHECK_NE(error, TrackError::kNone);
LOG(ERROR) << "MixableOutputStream: " << TrackErrorToString(error);
error_ = error;
audio_source_callback_->OnError(ErrorType::kUnknown);
}
void OnDeviceChange() {
TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("audio"), "MixTrack::OnDeviceChange",
"this", static_cast<void*>(this));
DCHECK(!on_device_change_callback_.is_null());
std::move(on_device_change_callback_).Run();
}
private:
enum class PlaybackType { kMixed, kIndependent };
void RegisterPlaybackStarted() {
DCHECK(playback_activation_time_for_uma_.is_null());
playback_activation_time_for_uma_ = base::TimeTicks::Now();
}
void RegisterPlaybackStopped(PlaybackType playback_type) {
if (playback_type == PlaybackType::kIndependent &&
playback_activation_time_for_uma_.is_null()) {
return; // Stop() for an independent stream can be called multiple times.
}
DCHECK(!playback_activation_time_for_uma_.is_null());
base::UmaHistogramLongTimes(
base::StrCat(
{"Media.Audio.OutputDeviceMixer.StreamDuration.",
((playback_type == PlaybackType::kMixed) ? "Mixed." : "Unmixed."),
LatencyToUmaSuffix(graph_input_->GetParams().latency_tag())}),
base::TimeTicks::Now() - playback_activation_time_for_uma_);
playback_activation_time_for_uma_ = base::TimeTicks();
}
// media::AudioOutputStream::AudioSourceCallback implementation to intercept
// error reporting during independent playback.
int OnMoreData(base::TimeDelta delay,
base::TimeTicks delay_timestamp,
int prior_frames_skipped,
media::AudioBus* dest) final {
DCHECK(audio_source_callback_);
return audio_source_callback_->OnMoreData(delay, delay_timestamp,
prior_frames_skipped, dest);
}
void OnError(ErrorType type) final {
// Device change events are intercepted by the underlying physical streams.
DCHECK_EQ(type, ErrorType::kUnknown);
ReportError(TrackError::kIndependentPlaybackFailed);
}
double volume_ = kDefaultVolume;
const raw_ptr<OutputDeviceMixerImpl> mixer_;
// Callback to notify the audio output client of the device change. Note that
// all the device change events are initially routed to MixerManager which
// does the centralized processing and notifies each mixer via
// OutputDeviceMixer::ProcessDeviceChange(). There OutputDeviceMixerImpl does
// the cleanup and then dispatches the device change event to all its clients
// via |on_device_change_callback_| of its MixTracks.
base::OnceClosure on_device_change_callback_;
// Callback to request the audio output data from the client.
raw_ptr<media::AudioOutputStream::AudioSourceCallback>
audio_source_callback_ = nullptr;
// Delivers the audio output into MixingGraph, to be mixed for the reference
// signal playback.
const std::unique_ptr<MixingGraph::Input> graph_input_;
// When non-nullptr, points to an open physical output stream used to render
// the audio output independently when mixing is not required.
std::unique_ptr<media::AudioOutputStream, StreamAutoClose> rendering_stream_ =
nullptr;
base::TimeTicks playback_activation_time_for_uma_;
TrackError error_ = TrackError::kNone;
};
// A proxy which represents MixTrack as media::AudioOutputStream.
class OutputDeviceMixerImpl::MixableOutputStream final
: public media::AudioOutputStream {
public:
MixableOutputStream(base::WeakPtr<OutputDeviceMixerImpl> mixer,
MixTrack* mix_track)
: mixer_(std::move(mixer)), mix_track_(mix_track) {}
MixableOutputStream(const MixableOutputStream&) = delete;
MixableOutputStream& operator=(const MixableOutputStream&) = delete;
~MixableOutputStream() final {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
}
// AudioOutputStream interface.
bool Open() final {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
if (!mixer_) {
LOG(ERROR) << "Stream start failed: device changed";
return false;
}
return mixer_->OpenStream(mix_track_);
}
void Start(AudioSourceCallback* callback) final {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
DCHECK(callback);
TRACE_EVENT_NESTABLE_ASYNC_BEGIN1(
TRACE_DISABLED_BY_DEFAULT("audio"), "MixableOutputStream::IsPlaying",
this, "device_id", mixer_ ? mixer_->device_id() : "device changed");
if (!mixer_) {
LOG(ERROR) << "Stream start failed: device changed";
callback->OnError(ErrorType::kDeviceChange);
return;
}
mixer_->StartStream(mix_track_, callback);
}
void Stop() final {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
TRACE_EVENT_NESTABLE_ASYNC_END0(TRACE_DISABLED_BY_DEFAULT("audio"),
"MixableOutputStream::IsPlaying", this);
if (!mixer_)
return;
mixer_->StopStream(mix_track_);
}
void SetVolume(double volume) final {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
if (!mixer_)
return;
mix_track_->SetVolume(volume);
}
void GetVolume(double* volume) final {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
DCHECK(volume);
if (!mixer_)
return;
*volume = mix_track_->GetVolume();
}
void Close() final {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
if (mixer_) {
mixer_->CloseStream(mix_track_);
}
// To match the typical usage pattern of AudioOutputStream.
delete this;
}
void Flush() final {}
private:
SEQUENCE_CHECKER(owning_sequence_);
// OutputDeviceMixerImpl will release all the resources and invalidate its
// weak pointers in OutputDeviceMixer::ProcessDeviceChange(). After that
// MixableOutputStream becomes a no-op.
base::WeakPtr<OutputDeviceMixerImpl> const mixer_
GUARDED_BY_CONTEXT(owning_sequence_);
const raw_ptr<MixTrack> mix_track_; // Valid only when |mixer_| is valid.
};
// Logs mixing statistics upon the destruction. Should be created when mixing
// playback starts, and destroyed when it ends.
class OutputDeviceMixerImpl::MixingStats {
public:
MixingStats(const std::string& device_id,
int active_track_count,
int listener_count)
: suffix_(DeviceIdToUmaSuffix(device_id)),
active_track_count_(active_track_count),
listener_count_(listener_count),
start_(base::TimeTicks::Now()) {
DCHECK_GT(active_track_count, 0);
DCHECK_GT(listener_count, 0);
}
~MixingStats() {
if (!noop_mixing_start_.is_null()) {
LogNoopMixingDuration();
}
DCHECK(!start_.is_null());
base::TimeDelta duration = base::TimeTicks::Now() - start_;
LogPerDeviceUma(duration, suffix_);
LogPerDeviceUma(duration, ""); // Combined.
}
void AddListener() { listener_count_.Increment(); }
void RemoveListener() { listener_count_.Decrement(); }
void AddActiveTrack() {
active_track_count_.Increment();
if (!noop_mixing_start_.is_null()) {
// First track after a period of feeding silence to the listeners.
DCHECK_EQ(active_track_count_.GetCurrent(), 1);
DCHECK(listener_count_.GetCurrent());
LogNoopMixingDuration();
}
}
void RemoveActiveTrack() {
active_track_count_.Decrement();
if (listener_count_.GetCurrent() && !active_track_count_.GetCurrent()) {
// No more tracks, so we start feeding silence to listeners.
DCHECK(noop_mixing_start_.is_null());
noop_mixing_start_ = base::TimeTicks::Now();
}
}
private:
// A helper to track the max value.
class MaxTracker {
public:
explicit MaxTracker(int value) : value_(value), max_value_(value) {}
int GetCurrent() { return value_; }
int GetMax() { return max_value_; }
void Increment() {
if (++value_ > max_value_)
max_value_ = value_;
}
void Decrement() {
DCHECK(value_ > 0);
value_--;
}
private:
int value_;
int max_value_;
};
void LogNoopMixingDuration() {
DCHECK(!noop_mixing_start_.is_null());
base::UmaHistogramLongTimes(
"Media.Audio.OutputDeviceMixer.NoopMixingDuration",
base::TimeTicks::Now() - noop_mixing_start_);
noop_mixing_start_ = base::TimeTicks();
}
void LogPerDeviceUma(base::TimeDelta duration, const char* suffix) {
constexpr int kMaxActiveStreamCount = 50;
constexpr int kMaxListeners = 20;
base::UmaHistogramLongTimes(
base::StrCat({"Media.Audio.OutputDeviceMixer.MixingDuration", suffix}),
duration);
base::UmaHistogramExactLinear(
base::StrCat(
{"Media.Audio.OutputDeviceMixer.MaxMixedStreamCount", suffix}),
active_track_count_.GetMax(), kMaxActiveStreamCount);
base::UmaHistogramExactLinear(
base::StrCat(
{"Media.Audio.OutputDeviceMixer.MaxListenerCount", suffix}),
listener_count_.GetMax(), kMaxListeners);
}
const char* const suffix_;
MaxTracker active_track_count_;
MaxTracker listener_count_;
const base::TimeTicks start_;
// Start of the period when there are no active tracks and we play and feed
// silence to the listeners.
base::TimeTicks noop_mixing_start_;
};
OutputDeviceMixerImpl::OutputDeviceMixerImpl(
const std::string& device_id,
const media::AudioParameters& output_params,
MixingGraph::CreateCallback create_mixing_graph_callback,
CreateStreamCallback create_stream_callback)
: OutputDeviceMixer(device_id),
create_stream_callback_(std::move(create_stream_callback)),
mixing_graph_output_params_(output_params),
mixing_graph_(std::move(create_mixing_graph_callback)
.Run(output_params,
base::BindRepeating(
&OutputDeviceMixerImpl::BroadcastToListeners,
base::Unretained(this)),
base::BindRepeating(
&OutputDeviceMixerImpl::OnMixingGraphError,
base::Unretained(this)))) {
DCHECK(mixing_graph_output_params_.IsValid());
DCHECK_EQ(mixing_graph_output_params_.format(),
media::AudioParameters::AUDIO_PCM_LOW_LATENCY);
TRACE_EVENT_NESTABLE_ASYNC_BEGIN1(TRACE_DISABLED_BY_DEFAULT("audio"),
"OutputDeviceMixerImpl", this, "device_id",
device_id);
}
OutputDeviceMixerImpl::~OutputDeviceMixerImpl() {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
DCHECK(active_tracks_.empty());
DCHECK(!HasListeners());
DCHECK(!MixingInProgress());
DCHECK(!mixing_graph_output_stream_);
TRACE_EVENT_NESTABLE_ASYNC_END0(TRACE_DISABLED_BY_DEFAULT("audio"),
"OutputDeviceMixerImpl", this);
}
media::AudioOutputStream* OutputDeviceMixerImpl::MakeMixableStream(
const media::AudioParameters& params,
base::OnceClosure on_device_change_callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
NON_REENTRANT_SCOPE(reentrancy_checker_);
if (!(params.IsValid() &&
params.format() == media::AudioParameters::AUDIO_PCM_LOW_LATENCY)) {
LOG(ERROR) << "Invalid output stream patameters for device [" << device_id()
<< "], parameters: " << params.AsHumanReadableString();
return nullptr;
}
auto mix_track =
std::make_unique<MixTrack>(this, mixing_graph_->CreateInput(params),
std::move(on_device_change_callback));
media::AudioOutputStream* mixable_stream =
new MixableOutputStream(weak_factory_.GetWeakPtr(), mix_track.get());
mix_tracks_.insert(std::move(mix_track));
return mixable_stream;
}
void OutputDeviceMixerImpl::ProcessDeviceChange() {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
NON_REENTRANT_SCOPE(reentrancy_checker_);
#if DCHECK_IS_ON()
DCHECK(!device_changed_);
device_changed_ = true;
#endif
// Make all MixableOutputStream instances no-op.
weak_factory_.InvalidateWeakPtrs();
// Stop and close all audio playback.
if (MixingInProgress()) {
StopMixingGraphPlayback(MixingError::kNone);
} else {
for (MixTrack* mix_track : active_tracks_) {
mix_track->StopIndependentRenderingStream();
}
// In case it was opened when listeners came:
mixing_graph_output_stream_.reset();
}
// For consistency.
for (MixTrack* mix_track : active_tracks_)
mix_track->SetSource(nullptr);
active_tracks_.clear();
// Close independent rendering streams: the clients will want to restore
// active playback in mix_track->OnDeviceChange() calls below; we don't want
// to exceed the limit of simultaneously open output streams when they do so.
for (auto&& mix_track : mix_tracks_)
mix_track->CloseIndependentRenderingStream();
{
base::AutoLock scoped_lock(listener_lock_);
listeners_.clear();
}
// Notify MixableOutputStream users of the device change. Normally they should
// close the current stream they are holding to, create/open a new one and
// resume the playback. We already released all the resources; closing a
// MixableOutputStream will be a no-op since weak pointers to |this| have just
// been invalidated.
for (auto&& mix_track : mix_tracks_)
mix_track->OnDeviceChange();
}
void OutputDeviceMixerImpl::StartListening(Listener* listener) {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
NON_REENTRANT_SCOPE(reentrancy_checker_);
#if DCHECK_IS_ON()
DCHECK(!device_changed_);
#endif
DVLOG(1) << "Reference output listener added for device [" << device_id()
<< "]";
TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("audio"),
"OutputDeviceMixerImpl::StartListening", "device_id",
device_id());
// A new listener came: cancel scheduled switch to independent playback.
switch_to_unmixed_playback_delay_timer_.Stop();
{
base::AutoLock scoped_lock(listener_lock_);
DCHECK(listeners_.find(listener) == listeners_.end());
listeners_.insert(listener);
if (MixingInProgress()) {
DCHECK(mixing_graph_output_stream_); // We are mixing.
mixing_session_stats_->AddListener();
return;
}
}
// Since we now have listeners, warm-up |mixing_graph_output_stream_|.
EnsureMixingGraphOutputStreamOpen();
// Start reference playback only if at least one audio stream is playing.
if (active_tracks_.empty())
return;
for (MixTrack* mix_track : active_tracks_)
mix_track->StopIndependentRenderingStream();
StartMixingGraphPlayback();
// Note that if StartMixingGraphPlayback() failed, no audio will be playing
// and each client of a playing MixableOutputStream will receive OnError()
// callback call.
}
void OutputDeviceMixerImpl::StopListening(Listener* listener) {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
#if DCHECK_IS_ON()
DCHECK(!device_changed_);
#endif
DVLOG(1) << "Reference output listener removed for device [" << device_id()
<< "]";
TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("audio"),
"OutputDeviceMixerImpl::StopListening", "device_id",
device_id());
{
base::AutoLock scoped_lock(listener_lock_);
auto iter = listeners_.find(listener);
DCHECK(iter != listeners_.end());
listeners_.erase(iter);
}
if (HasListeners()) {
// We still have some listeners left, so no need to do anything.
return;
}
if (!MixingInProgress()) {
// There is no mixing in progress and no more listeners left: closing
// the warmed up stream.
mixing_graph_output_stream_.reset();
return;
}
mixing_session_stats_->RemoveListener();
DCHECK(mixing_graph_output_stream_); // We are mixing.
if (active_tracks_.empty()) {
// There is no actual playback: we were just sending silence to the
// listener as a reference.
StopMixingGraphPlayback(MixingError::kNone);
} else {
// No listeners left, and we are playing via the mixing graph. Schedule
// switching to independent playback.
switch_to_unmixed_playback_delay_timer_.Start(
FROM_HERE, kSwitchToUnmixedPlaybackDelay, this,
&OutputDeviceMixerImpl::SwitchToUnmixedPlaybackTimerHelper);
}
}
bool OutputDeviceMixerImpl::OpenStream(MixTrack* mix_track) {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
#if DCHECK_IS_ON()
DCHECK(!device_changed_);
#endif
DCHECK(mix_track);
// Defer opening the physical output stream, as the audio mix is requested.
// The physical stream will be open on start if needed.
if (HasListeners())
return true;
return mix_track->OpenIndependentRenderingStream();
}
void OutputDeviceMixerImpl::StartStream(
MixTrack* mix_track,
media::AudioOutputStream::AudioSourceCallback* callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
NON_REENTRANT_SCOPE(reentrancy_checker_);
#if DCHECK_IS_ON()
DCHECK(!device_changed_);
#endif
DCHECK(mix_track);
DCHECK(callback);
DCHECK(!base::Contains(active_tracks_, mix_track));
TRACE_EVENT2(TRACE_DISABLED_BY_DEFAULT("audio"),
"OutputDeviceMixerImpl::StartStream", "device_id", device_id(),
"mix_track", static_cast<void*>(mix_track));
mix_track->SetSource(callback);
active_tracks_.emplace(mix_track);
if (MixingInProgress()) {
// We are playing all audio as a |mixing_graph_| output.
mix_track->StartProvidingAudioToMixingGraph();
mixing_session_stats_->AddActiveTrack();
} else if (HasListeners()) {
// Either we are starting the first active stream, or the previous switch to
// playing via the mixing graph failed because the its output stream failed
// to open. In any case, none of the active streams are playing individually
// at this point.
EnsureMixingGraphOutputStreamOpen();
StartMixingGraphPlayback();
} else {
// No reference signal is requested.
mix_track->StartIndependentRenderingStream();
}
}
void OutputDeviceMixerImpl::StopStream(MixTrack* mix_track) {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
NON_REENTRANT_SCOPE(reentrancy_checker_);
#if DCHECK_IS_ON()
DCHECK(!device_changed_);
#endif
DCHECK(mix_track);
if (!base::Contains(active_tracks_, mix_track)) {
// MixableOutputStream::Stop() can be called multiple times, even if the
// stream has not been started. See media::AudioOutputStream documentation.
return;
}
TRACE_EVENT2(TRACE_DISABLED_BY_DEFAULT("audio"),
"OutputDeviceMixerImpl::StopStream", "device_id", device_id(),
"mix_track", static_cast<void*>(mix_track));
active_tracks_.erase(mix_track);
if (MixingInProgress()) {
// We are playing all audio as a |mixing_graph_| output.
mix_track->StopProvidingAudioToMixingGraph();
mixing_session_stats_->RemoveActiveTrack();
if (!HasListeners() && active_tracks_.empty()) {
// All listeners are gone, which means a switch to an independent playback
// is scheduled. But since we have no active tracks, we can stop mixing
// immediately.
DCHECK(switch_to_unmixed_playback_delay_timer_.IsRunning());
StopMixingGraphPlayback(MixingError::kNone);
}
// Note: if there are listeners, we do not stop the reference playback even
// if there are no active mix members. This way the echo canceller will be
// in a consistent state when the playback is activated again. Drawback: we
// keep playing silent audio. An example would be some occasional
// notification sounds and no other audio output: we start the mixing
// playback at the first notification, and stop it only when the echo
// cancellation session is finished (i.e. all the listeners are gone).
} else {
mix_track->StopIndependentRenderingStream();
}
mix_track->SetSource(nullptr);
}
void OutputDeviceMixerImpl::CloseStream(MixTrack* mix_track) {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
NON_REENTRANT_SCOPE(reentrancy_checker_);
#if DCHECK_IS_ON()
DCHECK(!device_changed_);
#endif
DCHECK(mix_track);
DCHECK(!base::Contains(active_tracks_, mix_track));
auto iter = mix_tracks_.find(mix_track);
DCHECK(iter != mix_tracks_.end());
mix_tracks_.erase(iter);
}
media::AudioOutputStream* OutputDeviceMixerImpl::CreateAndOpenDeviceStream(
const media::AudioParameters& params) {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
DCHECK(params.IsValid());
DCHECK_EQ(params.format(), media::AudioParameters::AUDIO_PCM_LOW_LATENCY);
media::AudioOutputStream* stream =
create_stream_callback_.Run(device_id(), params);
if (!stream)
return nullptr;
if (!stream->Open()) {
LOG(ERROR) << "Failed to open stream";
stream->Close();
return nullptr;
}
return stream;
}
void OutputDeviceMixerImpl::BroadcastToListeners(
const media::AudioBus& audio_bus,
base::TimeDelta delay) {
TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("audio"),
"OutputDeviceMixerImpl::BroadcastToListeners", "delay ms",
delay.InMilliseconds());
base::AutoLock scoped_lock(listener_lock_);
for (Listener* listener : listeners_) {
listener->OnPlayoutData(audio_bus,
mixing_graph_output_params_.sample_rate(), delay);
}
}
// Processes errors coming from |mixing_graph_| during mixed playback.
void OutputDeviceMixerImpl::OnMixingGraphError(ErrorType error) {
// |mixing_graph_output_stream_| guarantees this.
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
// Device change events are intercepted by the underlying physical stream.
DCHECK_EQ(error, ErrorType::kUnknown);
// This call is expected only during mixed playback.
DCHECK(mixing_graph_output_stream_);
StopMixingGraphPlayback(MixingError::kPlaybackFailed);
}
bool OutputDeviceMixerImpl::HasListeners() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
return TS_UNCHECKED_READ(listeners_).size();
}
void OutputDeviceMixerImpl::EnsureMixingGraphOutputStreamOpen() {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
DCHECK(HasListeners());
if (mixing_graph_output_stream_)
return;
TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("audio"),
"OutputDeviceMixerImpl::EnsureMixingGraphOutputStreamOpen",
"device_id", device_id());
mixing_graph_output_stream_.reset(
CreateAndOpenDeviceStream(mixing_graph_output_params_));
}
// Expects |mixing_graph_output_stream_| to be already open; if not - this is
// interpreted as a failure.
void OutputDeviceMixerImpl::StartMixingGraphPlayback() {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
TRACE_EVENT_NESTABLE_ASYNC_BEGIN1(TRACE_DISABLED_BY_DEFAULT("audio"),
"OutputDeviceMixerImpl mixing", this,
"device_id", device_id());
TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("audio"),
"OutputDeviceMixerImpl::StartMixingGraphPlayback", "device_id",
device_id());
if (!mixing_graph_output_stream_) {
StopMixingGraphPlayback(MixingError::kOpenFailed);
return;
}
DCHECK(!mixing_session_stats_);
mixing_session_stats_ = std::make_unique<MixingStats>(
device_id(), active_tracks_.size(), TS_UNCHECKED_READ(listeners_).size());
for (MixTrack* mix_track : active_tracks_)
mix_track->StartProvidingAudioToMixingGraph();
mixing_graph_output_stream_->Start(mixing_graph_.get());
DVLOG(1) << " Mixing started for device [" << device_id() << "]";
}
void OutputDeviceMixerImpl::StopMixingGraphPlayback(MixingError error) {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
if (MixingInProgress()) {
// Mixing was in progress, we should stop it.
DCHECK(mixing_graph_output_stream_);
DCHECK_NE(error, MixingError::kOpenFailed);
TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("audio"),
"OutputDeviceMixerImpl::StopMixingGraphPlayback", "device_id",
device_id());
switch_to_unmixed_playback_delay_timer_.Stop();
mixing_graph_output_stream_->Stop();
mixing_graph_output_stream_.reset(); // Auto-close the stream.
mixing_session_stats_.reset();
DVLOG(1) << " Mixing stopped for device [" << device_id() << "]";
for (MixTrack* mix_track : active_tracks_)
mix_track->StopProvidingAudioToMixingGraph();
TRACE_EVENT_NESTABLE_ASYNC_END0(TRACE_DISABLED_BY_DEFAULT("audio"),
"OutputDeviceMixerImpl mixing", this);
}
DCHECK(!mixing_graph_output_stream_);
if (error != MixingError::kNone) {
LOG(ERROR) << ErrorToString(error) << " for device [" << device_id() << "]";
TrackError track_error = (error == MixingError::kOpenFailed)
? TrackError::kMixedOpenFailed
: TrackError::kMixedPlaybackFailed;
for (MixTrack* mix_track : active_tracks_)
mix_track->ReportError(track_error);
}
base::UmaHistogramEnumeration(
"Media.Audio.OutputDeviceMixer.MixedPlaybackStatus", error);
}
void OutputDeviceMixerImpl::SwitchToUnmixedPlaybackTimerHelper() {
DCHECK_CALLED_ON_VALID_SEQUENCE(owning_sequence_);
NON_REENTRANT_SCOPE(reentrancy_checker_);
DCHECK(MixingInProgress());
#if DCHECK_IS_ON()
DCHECK(!device_changed_);
#endif
TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("audio"),
"OutputDeviceMixerImpl::SwitchToUnmixedPlaybackTimerHelper",
"device_id", device_id());
StopMixingGraphPlayback(MixingError::kNone);
for (MixTrack* mix_track : active_tracks_)
mix_track->StartIndependentRenderingStream();
}
// static
const char* OutputDeviceMixerImpl::ErrorToString(MixingError error) {
switch (error) {
case MixingError::kOpenFailed:
return "Failed to open mixing rendering stream";
case MixingError::kPlaybackFailed:
return "Error during mixed playback";
default:
NOTREACHED();
return "No error";
}
}
} // namespace audio