| // 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 "third_party/blink/renderer/modules/media/audio/audio_renderer_sink_cache.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <utility> |
| |
| #include "base/location.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/stl_util.h" |
| #include "base/synchronization/lock.h" |
| #include "base/trace_event/trace_event.h" |
| #include "media/audio/audio_device_description.h" |
| #include "media/base/audio_renderer_sink.h" |
| #include "third_party/blink/public/web/modules/media/audio/web_audio_device_factory.h" |
| #include "third_party/blink/public/web/web_local_frame.h" |
| #include "third_party/blink/renderer/core/execution_context/execution_context_lifecycle_observer.h" |
| #include "third_party/blink/renderer/core/frame/local_dom_window.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/platform/scheduler/public/post_cross_thread_task.h" |
| #include "third_party/blink/renderer/platform/supplementable.h" |
| #include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h" |
| |
| namespace blink { |
| |
| AudioRendererSinkCache* AudioRendererSinkCache::instance_ = nullptr; |
| |
| class AudioRendererSinkCache::WindowObserver final |
| : public GarbageCollected<AudioRendererSinkCache::WindowObserver>, |
| public Supplement<LocalDOMWindow>, |
| public ExecutionContextLifecycleObserver { |
| public: |
| static const char kSupplementName[]; |
| |
| explicit WindowObserver(LocalDOMWindow& window) |
| : Supplement<LocalDOMWindow>(window), |
| ExecutionContextLifecycleObserver(&window) {} |
| ~WindowObserver() override = default; |
| |
| void Trace(Visitor* visitor) const final { |
| Supplement<LocalDOMWindow>::Trace(visitor); |
| ExecutionContextLifecycleObserver::Trace(visitor); |
| } |
| |
| // ExecutionContextLifecycleObserver implementation. |
| void ContextDestroyed() override { |
| if (auto* cache_instance = AudioRendererSinkCache::instance_) |
| cache_instance->DropSinksForFrame(DomWindow()->GetLocalFrameToken()); |
| } |
| |
| DISALLOW_COPY_AND_ASSIGN(WindowObserver); |
| }; |
| |
| const char AudioRendererSinkCache::WindowObserver::kSupplementName[] = |
| "AudioRendererSinkCache::WindowObserver"; |
| |
| 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 AudioRendererSinkCache::CacheEntry { |
| LocalFrameToken source_frame_token; |
| std::string device_id; |
| scoped_refptr<media::AudioRendererSink> sink; // Sink instance |
| bool used; // True if in use by a client. |
| }; |
| |
| // static |
| void AudioRendererSinkCache::InstallWindowObserver(LocalDOMWindow& window) { |
| if (Supplement<LocalDOMWindow>::From<WindowObserver>(window)) |
| return; |
| Supplement<LocalDOMWindow>::ProvideTo( |
| window, MakeGarbageCollected<WindowObserver>(window)); |
| } |
| |
| AudioRendererSinkCache::AudioRendererSinkCache( |
| scoped_refptr<base::SequencedTaskRunner> cleanup_task_runner, |
| CreateSinkCallback create_sink_cb, |
| base::TimeDelta delete_timeout) |
| : cleanup_task_runner_(std::move(cleanup_task_runner)), |
| create_sink_cb_(std::move(create_sink_cb)), |
| delete_timeout_(delete_timeout) { |
| DCHECK(!instance_); |
| instance_ = this; |
| } |
| |
| AudioRendererSinkCache::~AudioRendererSinkCache() { |
| // 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 AudioRendererSinkCache::GetSinkInfo( |
| const LocalFrameToken& source_frame_token, |
| const base::UnguessableToken& session_id, |
| const std::string& device_id) { |
| TRACE_EVENT_BEGIN2("audio", "AudioRendererSinkCache::GetSinkInfo", |
| "frame_token", source_frame_token.ToString(), "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_frame_token, {session_id, device_id}); |
| |
| CacheOrStopUnusedSink(source_frame_token, |
| 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", "AudioRendererSinkCache::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_frame_token, 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", "AudioRendererSinkCache::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_frame_token, |
| media::AudioSinkParameters(base::UnguessableToken(), device_id)); |
| |
| CacheOrStopUnusedSink(source_frame_token, device_id, sink); |
| |
| UMA_HISTOGRAM_ENUMERATION( |
| "Media.Audio.Render.SinkCache.GetOutputDeviceInfoCacheUtilization", |
| SINK_CACHE_MISS_NO_SINK, SINK_CACHE_LAST_ENTRY); |
| |
| TRACE_EVENT_END1("audio", "AudioRendererSinkCache::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> AudioRendererSinkCache::GetSink( |
| const LocalFrameToken& source_frame_token, |
| const std::string& device_id) { |
| UMA_HISTOGRAM_BOOLEAN("Media.Audio.Render.SinkCache.UsedForSinkCreation", |
| true); |
| TRACE_EVENT_BEGIN2("audio", "AudioRendererSinkCache::GetSink", "frame_token", |
| source_frame_token.ToString(), "device id", device_id); |
| |
| base::AutoLock auto_lock(cache_lock_); |
| |
| auto cache_iter = FindCacheEntry_Locked(source_frame_token, 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", "AudioRendererSinkCache::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_frame_token, device_id, |
| create_sink_cb_.Run( |
| source_frame_token, |
| media::AudioSinkParameters(base::UnguessableToken(), device_id)), |
| true /* used */}; |
| |
| if (SinkIsHealthy(cache_entry.sink.get())) { |
| TRACE_EVENT_INSTANT0("audio", |
| "AudioRendererSinkCache::GetSink: caching new sink", |
| TRACE_EVENT_SCOPE_THREAD); |
| cache_.push_back(cache_entry); |
| } |
| |
| TRACE_EVENT_END1("audio", "AudioRendererSinkCache::GetSink", "result", |
| "Cache miss"); |
| return cache_entry.sink; |
| } |
| |
| void AudioRendererSinkCache::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 AudioRendererSinkCache::DeleteLaterIfUnused( |
| scoped_refptr<media::AudioRendererSink> sink) { |
| PostDelayedCrossThreadTask( |
| *cleanup_task_runner_, FROM_HERE, |
| CrossThreadBindOnce( |
| &AudioRendererSinkCache::DeleteSink, |
| // Unretained is safe here since this is a process-wide |
| // singleton and tests will ensure lifetime. |
| CrossThreadUnretained(this), WTF::RetainedRef(std::move(sink)), |
| false /*do not delete if used*/), |
| delete_timeout_); |
| } |
| |
| void AudioRendererSinkCache::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(); |
| } |
| } |
| |
| AudioRendererSinkCache::CacheContainer::iterator |
| AudioRendererSinkCache::FindCacheEntry_Locked( |
| const LocalFrameToken& source_frame_token, |
| const std::string& device_id, |
| bool unused_only) { |
| return std::find_if( |
| cache_.begin(), cache_.end(), |
| [source_frame_token, &device_id, unused_only](const CacheEntry& val) { |
| if (val.used && unused_only) |
| return false; |
| if (val.source_frame_token != source_frame_token) |
| 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 AudioRendererSinkCache::CacheOrStopUnusedSink( |
| const LocalFrameToken& source_frame_token, |
| 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_frame_token, device_id, std::move(sink), |
| false /* not used */}; |
| |
| { |
| base::AutoLock auto_lock(cache_lock_); |
| cache_.push_back(cache_entry); |
| } |
| |
| DeleteLaterIfUnused(cache_entry.sink); |
| } |
| |
| void AudioRendererSinkCache::DropSinksForFrame( |
| const LocalFrameToken& source_frame_token) { |
| base::AutoLock auto_lock(cache_lock_); |
| base::EraseIf(cache_, [source_frame_token](const CacheEntry& val) { |
| if (val.source_frame_token == source_frame_token) { |
| val.sink->Stop(); |
| return true; |
| } |
| return false; |
| }); |
| } |
| |
| size_t AudioRendererSinkCache::GetCacheSizeForTesting() { |
| return cache_.size(); |
| } |
| |
| } // namespace blink |