blob: 8247f9684a24a63bed66419b3cbe9589cc501e62 [file] [log] [blame]
// Copyright 2016 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 "content/renderer/media/audio/audio_renderer_sink_cache_impl.h"
#include <algorithm>
#include <memory>
#include <utility>
#include "base/bind.h"
#include "base/location.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/synchronization/lock.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/trace_event/trace_event.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_frame_observer.h"
#include "content/renderer/media/audio/audio_device_factory.h"
#include "media/audio/audio_device_description.h"
#include "media/base/audio_renderer_sink.h"
namespace content {
AudioRendererSinkCacheImpl* AudioRendererSinkCacheImpl::instance_ = nullptr;
constexpr int kDeleteTimeoutMs = 5000;
constexpr int kDefaultSessionId = 0;
class AudioRendererSinkCacheImpl::FrameObserver : public RenderFrameObserver {
public:
explicit FrameObserver(content::RenderFrame* render_frame)
: RenderFrameObserver(render_frame) {}
~FrameObserver() override{};
private:
// content::RenderFrameObserver implementation:
void DidCommitProvisionalLoad(bool is_same_document_navigation,
ui::PageTransition transition) override {
if (!is_same_document_navigation)
DropFrameCache();
}
void OnDestruct() override {
DropFrameCache();
delete this;
}
void DropFrameCache() {
if (AudioRendererSinkCacheImpl::instance_)
AudioRendererSinkCacheImpl::instance_->DropSinksForFrame(routing_id());
}
DISALLOW_COPY_AND_ASSIGN(FrameObserver);
};
namespace {
enum GetOutputDeviceInfoCacheUtilization {
// No cached sink found.
SINK_CACHE_MISS_NO_SINK = 0,
// If session id is used to specify a device, we always have to create and
// cache a new sink.
SINK_CACHE_MISS_CANNOT_LOOKUP_BY_SESSION_ID = 1,
// Output parmeters for an already-cached sink are requested.
SINK_CACHE_HIT = 2,
// For UMA.
SINK_CACHE_LAST_ENTRY
};
bool SinkIsHealthy(media::AudioRendererSink* sink) {
return sink->GetOutputDeviceInfo().device_status() ==
media::OUTPUT_DEVICE_STATUS_OK;
}
} // namespace
// Cached sink data.
struct AudioRendererSinkCacheImpl::CacheEntry {
int source_render_frame_id;
std::string device_id;
scoped_refptr<media::AudioRendererSink> sink; // Sink instance
bool used; // True if in use by a client.
};
// static
std::unique_ptr<AudioRendererSinkCache> AudioRendererSinkCache::Create() {
return std::make_unique<AudioRendererSinkCacheImpl>(
base::ThreadTaskRunnerHandle::Get(),
base::Bind(&AudioDeviceFactory::NewAudioRendererMixerSink),
base::TimeDelta::FromMilliseconds(kDeleteTimeoutMs));
}
// static
void AudioRendererSinkCache::ObserveFrame(RenderFrame* frame) {
new AudioRendererSinkCacheImpl::FrameObserver(frame);
}
AudioRendererSinkCacheImpl::AudioRendererSinkCacheImpl(
scoped_refptr<base::SingleThreadTaskRunner> task_runner,
CreateSinkCallback create_sink_cb,
base::TimeDelta delete_timeout)
: task_runner_(std::move(task_runner)),
create_sink_cb_(std::move(create_sink_cb)),
delete_timeout_(delete_timeout),
weak_ptr_factory_(this) {
weak_this_ = weak_ptr_factory_.GetWeakPtr();
if (instance_)
LOG(ERROR) << "More that one AudioRendererSinkCache instance created. "
"Allowed in tests only.";
instance_ = this;
}
AudioRendererSinkCacheImpl::~AudioRendererSinkCacheImpl() {
DCHECK(task_runner_->BelongsToCurrentThread());
// We just release all the cached sinks here. Stop them first.
// We can stop all the sinks, no matter they are used or not, since everything
// is being destroyed anyways.
for (auto& entry : cache_)
entry.sink->Stop();
if (instance_ == this)
instance_ = nullptr;
}
media::OutputDeviceInfo AudioRendererSinkCacheImpl::GetSinkInfo(
int source_render_frame_id,
int session_id,
const std::string& device_id) {
TRACE_EVENT_BEGIN2("audio", "AudioRendererSinkCacheImpl::GetSinkInfo",
"frame_id", source_render_frame_id, "device id",
device_id);
if (media::AudioDeviceDescription::UseSessionIdToSelectDevice(session_id,
device_id)) {
// We are provided with session id instead of device id. Session id is
// unique, so we can't find any matching sink. Creating a new one.
scoped_refptr<media::AudioRendererSink> sink =
create_sink_cb_.Run(source_render_frame_id, {session_id, device_id});
CacheOrStopUnusedSink(source_render_frame_id,
sink->GetOutputDeviceInfo().device_id(), sink);
UMA_HISTOGRAM_ENUMERATION(
"Media.Audio.Render.SinkCache.GetOutputDeviceInfoCacheUtilization",
SINK_CACHE_MISS_CANNOT_LOOKUP_BY_SESSION_ID, SINK_CACHE_LAST_ENTRY);
TRACE_EVENT_END1("audio", "AudioRendererSinkCacheImpl::GetSinkInfo",
"result", "Cache not used due to using |session_id|");
return sink->GetOutputDeviceInfo();
}
// Ignore session id.
{
base::AutoLock auto_lock(cache_lock_);
auto cache_iter = FindCacheEntry_Locked(source_render_frame_id, device_id,
false /* unused_only */);
if (cache_iter != cache_.end()) {
// A matching cached sink is found.
UMA_HISTOGRAM_ENUMERATION(
"Media.Audio.Render.SinkCache.GetOutputDeviceInfoCacheUtilization",
SINK_CACHE_HIT, SINK_CACHE_LAST_ENTRY);
TRACE_EVENT_END1("audio", "AudioRendererSinkCacheImpl::GetSinkInfo",
"result", "Cache hit");
return cache_iter->sink->GetOutputDeviceInfo();
}
}
// No matching sink found, create a new one.
scoped_refptr<media::AudioRendererSink> sink = create_sink_cb_.Run(
source_render_frame_id,
media::AudioSinkParameters(kDefaultSessionId, device_id));
CacheOrStopUnusedSink(source_render_frame_id, device_id, sink);
UMA_HISTOGRAM_ENUMERATION(
"Media.Audio.Render.SinkCache.GetOutputDeviceInfoCacheUtilization",
SINK_CACHE_MISS_NO_SINK, SINK_CACHE_LAST_ENTRY);
TRACE_EVENT_END1("audio", "AudioRendererSinkCacheImpl::GetSinkInfo", "result",
"Cache miss");
// |sink| is ref-counted, so it's ok if it is removed from cache before we get
// here.
return sink->GetOutputDeviceInfo();
}
scoped_refptr<media::AudioRendererSink> AudioRendererSinkCacheImpl::GetSink(
int source_render_frame_id,
const std::string& device_id) {
UMA_HISTOGRAM_BOOLEAN("Media.Audio.Render.SinkCache.UsedForSinkCreation",
true);
TRACE_EVENT_BEGIN2("audio", "AudioRendererSinkCacheImpl::GetSink", "frame_id",
source_render_frame_id, "device id", device_id);
base::AutoLock auto_lock(cache_lock_);
auto cache_iter = FindCacheEntry_Locked(source_render_frame_id, device_id,
true /* unused sink only */);
if (cache_iter != cache_.end()) {
// Found unused sink; mark it as used and return.
cache_iter->used = true;
UMA_HISTOGRAM_BOOLEAN(
"Media.Audio.Render.SinkCache.InfoSinkReusedForOutput", true);
TRACE_EVENT_END1("audio", "AudioRendererSinkCacheImpl::GetSink", "result",
"Cache hit");
return cache_iter->sink;
}
// No unused sink is found, create one, mark it used, cache it and return.
CacheEntry cache_entry = {
source_render_frame_id, device_id,
create_sink_cb_.Run(
source_render_frame_id,
media::AudioSinkParameters(kDefaultSessionId, device_id)),
true /* used */};
if (SinkIsHealthy(cache_entry.sink.get())) {
TRACE_EVENT_INSTANT0(
"audio", "AudioRendererSinkCacheImpl::GetSink: caching new sink",
TRACE_EVENT_SCOPE_THREAD);
cache_.push_back(cache_entry);
}
TRACE_EVENT_END1("audio", "AudioRendererSinkCacheImpl::GetSink", "result",
"Cache miss");
return cache_entry.sink;
}
void AudioRendererSinkCacheImpl::ReleaseSink(
const media::AudioRendererSink* sink_ptr) {
// We don't know the sink state, so won't reuse it. Delete it immediately.
DeleteSink(sink_ptr, true);
}
void AudioRendererSinkCacheImpl::DeleteLaterIfUnused(
const media::AudioRendererSink* sink_ptr) {
task_runner_->PostDelayedTask(
FROM_HERE,
base::BindOnce(&AudioRendererSinkCacheImpl::DeleteSink, weak_this_,
base::RetainedRef(sink_ptr),
false /*do not delete if used*/),
delete_timeout_);
}
void AudioRendererSinkCacheImpl::DeleteSink(
const media::AudioRendererSink* sink_ptr,
bool force_delete_used) {
DCHECK(sink_ptr);
scoped_refptr<media::AudioRendererSink> sink_to_stop;
{
base::AutoLock auto_lock(cache_lock_);
// Looking up the sink by its pointer.
auto cache_iter = std::find_if(cache_.begin(), cache_.end(),
[sink_ptr](const CacheEntry& val) {
return val.sink.get() == sink_ptr;
});
if (cache_iter == cache_.end())
return;
// When |force_delete_used| is set, it's expected that we are deleting a
// used sink.
DCHECK((!force_delete_used) || (force_delete_used && cache_iter->used))
<< "Attempt to delete a non-acquired sink.";
if (!force_delete_used && cache_iter->used)
return;
// To stop the sink before deletion if it's not used, we need to hold
// a ref to it.
if (!cache_iter->used) {
sink_to_stop = cache_iter->sink;
UMA_HISTOGRAM_BOOLEAN(
"Media.Audio.Render.SinkCache.InfoSinkReusedForOutput", false);
}
cache_.erase(cache_iter);
} // Lock scope;
// Stop the sink out of the lock scope.
if (sink_to_stop.get()) {
DCHECK_EQ(sink_ptr, sink_to_stop.get());
sink_to_stop->Stop();
}
}
AudioRendererSinkCacheImpl::CacheContainer::iterator
AudioRendererSinkCacheImpl::FindCacheEntry_Locked(
int source_render_frame_id,
const std::string& device_id,
bool unused_only) {
return std::find_if(
cache_.begin(), cache_.end(),
[source_render_frame_id, &device_id, unused_only](const CacheEntry& val) {
if (val.used && unused_only)
return false;
if (val.source_render_frame_id != source_render_frame_id)
return false;
if (media::AudioDeviceDescription::IsDefaultDevice(device_id) &&
media::AudioDeviceDescription::IsDefaultDevice(val.device_id)) {
// Both device IDs represent the same default device => do not compare
// them;
return true;
}
return val.device_id == device_id;
});
}
void AudioRendererSinkCacheImpl::CacheOrStopUnusedSink(
int source_render_frame_id,
const std::string& device_id,
scoped_refptr<media::AudioRendererSink> sink) {
if (!SinkIsHealthy(sink.get())) {
TRACE_EVENT_INSTANT0("audio", "CacheOrStopUnusedSink: Unhealthy sink",
TRACE_EVENT_SCOPE_THREAD);
// Since |sink| is not cached, we must make sure to Stop it now.
sink->Stop();
return;
}
CacheEntry cache_entry = {source_render_frame_id, device_id, std::move(sink),
false /* not used */};
{
base::AutoLock auto_lock(cache_lock_);
cache_.push_back(cache_entry);
}
DeleteLaterIfUnused(cache_entry.sink.get());
}
void AudioRendererSinkCacheImpl::DropSinksForFrame(int source_render_frame_id) {
base::AutoLock auto_lock(cache_lock_);
cache_.erase(std::remove_if(cache_.begin(), cache_.end(),
[source_render_frame_id](const CacheEntry& val) {
if (val.source_render_frame_id ==
source_render_frame_id) {
val.sink->Stop();
return true;
}
return false;
}),
cache_.end());
}
int AudioRendererSinkCacheImpl::GetCacheSizeForTesting() {
return cache_.size();
}
} // namespace content