| // Copyright (c) 2012 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/audio/audio_output_device.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <cmath> |
| #include <memory> |
| #include <utility> |
| |
| #include "base/callback_helpers.h" |
| #include "base/logging.h" |
| #include "base/macros.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/threading/platform_thread.h" |
| #include "base/threading/sequenced_task_runner_handle.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/timer/timer.h" |
| #include "base/trace_event/trace_event.h" |
| #include "build/build_config.h" |
| #include "media/audio/audio_device_description.h" |
| #include "media/audio/audio_output_device_thread_callback.h" |
| #include "media/base/bind_to_current_loop.h" |
| #include "media/base/limits.h" |
| |
| namespace media { |
| |
| AudioOutputDevice::AudioOutputDevice( |
| std::unique_ptr<AudioOutputIPC> ipc, |
| const scoped_refptr<base::SingleThreadTaskRunner>& io_task_runner, |
| const AudioSinkParameters& sink_params, |
| base::TimeDelta authorization_timeout) |
| : io_task_runner_(io_task_runner), |
| callback_(nullptr), |
| ipc_(std::move(ipc)), |
| state_(IDLE), |
| session_id_(sink_params.session_id), |
| device_id_(sink_params.device_id), |
| processing_id_(sink_params.processing_id), |
| stopping_hack_(false), |
| did_receive_auth_(base::WaitableEvent::ResetPolicy::MANUAL, |
| base::WaitableEvent::InitialState::NOT_SIGNALED), |
| output_params_(AudioParameters::UnavailableDeviceParams()), |
| device_status_(OUTPUT_DEVICE_STATUS_ERROR_INTERNAL), |
| auth_timeout_(authorization_timeout) { |
| DCHECK(ipc_); |
| DCHECK(io_task_runner_); |
| } |
| |
| void AudioOutputDevice::Initialize(const AudioParameters& params, |
| RenderCallback* callback) { |
| io_task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&AudioOutputDevice::InitializeOnIOThread, this, |
| params, callback)); |
| } |
| |
| void AudioOutputDevice::InitializeOnIOThread(const AudioParameters& params, |
| RenderCallback* callback) { |
| DCHECK(!callback_) << "Calling Initialize() twice?"; |
| DCHECK(params.IsValid()); |
| DVLOG(1) << __func__ << ": " << params.AsHumanReadableString(); |
| audio_parameters_ = params; |
| |
| base::AutoLock auto_lock(audio_thread_lock_); |
| // If Stop() has already been called, RenderCallback has already been |
| // destroyed. So |callback| would be a dangling pointer. |
| if (!stopping_hack_) |
| callback_ = callback; |
| } |
| |
| AudioOutputDevice::~AudioOutputDevice() { |
| { |
| // Abort any pending callbacks. Technically we don't need to acquire the |
| // lock here since there should be no other calls outstanding, but because |
| // we've used the GUARDED_BY compiler syntax, we'll get an error without it. |
| base::AutoLock auto_lock(device_info_lock_); |
| if (pending_device_info_cb_) { |
| std::move(pending_device_info_cb_) |
| .Run(OutputDeviceInfo(OUTPUT_DEVICE_STATUS_ERROR_INTERNAL)); |
| } |
| } |
| |
| #if DCHECK_IS_ON() |
| // Make sure we've stopped the stream properly before destructing |this|. |
| DCHECK(audio_thread_lock_.Try()); |
| DCHECK_EQ(state_, IDLE); |
| DCHECK(!audio_thread_); |
| DCHECK(!audio_callback_); |
| DCHECK(!stopping_hack_); |
| audio_thread_lock_.Release(); |
| #endif // DCHECK_IS_ON() |
| } |
| |
| void AudioOutputDevice::RequestDeviceAuthorization() { |
| TRACE_EVENT0("audio", "AudioOutputDevice::RequestDeviceAuthorization"); |
| io_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&AudioOutputDevice::RequestDeviceAuthorizationOnIOThread, |
| this)); |
| } |
| |
| void AudioOutputDevice::Start() { |
| TRACE_EVENT0("audio", "AudioOutputDevice::Start"); |
| io_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&AudioOutputDevice::CreateStreamOnIOThread, this)); |
| } |
| |
| void AudioOutputDevice::Stop() { |
| TRACE_EVENT0("audio", "AudioOutputDevice::Stop"); |
| { |
| base::AutoLock auto_lock(audio_thread_lock_); |
| audio_thread_.reset(); |
| stopping_hack_ = true; |
| } |
| io_task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&AudioOutputDevice::ShutDownOnIOThread, this)); |
| } |
| |
| void AudioOutputDevice::Play() { |
| TRACE_EVENT0("audio", "AudioOutputDevice::Play"); |
| io_task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&AudioOutputDevice::PlayOnIOThread, this)); |
| } |
| |
| void AudioOutputDevice::Pause() { |
| TRACE_EVENT0("audio", "AudioOutputDevice::Pause"); |
| io_task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&AudioOutputDevice::PauseOnIOThread, this)); |
| } |
| |
| void AudioOutputDevice::Flush() { |
| TRACE_EVENT0("audio", "AudioOutputDevice::Flush"); |
| io_task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&AudioOutputDevice::FlushOnIOThread, this)); |
| } |
| |
| bool AudioOutputDevice::SetVolume(double volume) { |
| TRACE_EVENT1("audio", "AudioOutputDevice::Pause", "volume", volume); |
| |
| if (volume < 0 || volume > 1.0) |
| return false; |
| |
| return io_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&AudioOutputDevice::SetVolumeOnIOThread, this, volume)); |
| } |
| |
| OutputDeviceInfo AudioOutputDevice::GetOutputDeviceInfo() { |
| TRACE_EVENT0("audio", "AudioOutputDevice::GetOutputDeviceInfo"); |
| DCHECK(!io_task_runner_->BelongsToCurrentThread()); |
| did_receive_auth_.Wait(); |
| return GetOutputDeviceInfo_Signaled(); |
| } |
| |
| void AudioOutputDevice::GetOutputDeviceInfoAsync(OutputDeviceInfoCB info_cb) { |
| { |
| // Hold the lock while checking the signal and setting the pending callback |
| // to avoid racing with authorization completion on the IO thread. |
| base::AutoLock auto_lock(device_info_lock_); |
| if (!did_receive_auth_.IsSignaled()) { |
| DCHECK(!pending_device_info_cb_); |
| pending_device_info_cb_ = BindToCurrentLoop(std::move(info_cb)); |
| return; |
| } |
| } |
| |
| // Always post to avoid the caller being reentrant. Local testing shows even |
| // on a powerful desktop, we haven't received device authorization by this |
| // point when AOD construction and GetOutputDeviceInfoAsync() happen back to |
| // back (which is the most common use case). |
| base::SequencedTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| base::BindOnce(std::move(info_cb), GetOutputDeviceInfo_Signaled())); |
| } |
| |
| bool AudioOutputDevice::IsOptimizedForHardwareParameters() { |
| return true; |
| } |
| |
| bool AudioOutputDevice::CurrentThreadIsRenderingThread() { |
| // Since this function is supposed to be called on the rendering thread, |
| // it's safe to access |audio_callback_| here. It will always be valid when |
| // the rendering thread is running. |
| return audio_callback_->CurrentThreadIsAudioDeviceThread(); |
| } |
| |
| void AudioOutputDevice::RequestDeviceAuthorizationOnIOThread() { |
| DCHECK(io_task_runner_->BelongsToCurrentThread()); |
| DCHECK_EQ(state_, IDLE); |
| |
| state_ = AUTHORIZATION_REQUESTED; |
| ipc_->RequestDeviceAuthorization(this, session_id_, device_id_); |
| |
| if (auth_timeout_ > base::TimeDelta()) { |
| // Create the timer on the thread it's used on. It's guaranteed to be |
| // deleted on the same thread since users must call Stop() before deleting |
| // AudioOutputDevice; see ShutDownOnIOThread(). |
| auth_timeout_action_ = std::make_unique<base::OneShotTimer>(); |
| auth_timeout_action_->Start( |
| FROM_HERE, auth_timeout_, |
| base::BindOnce(&AudioOutputDevice::OnDeviceAuthorized, this, |
| OUTPUT_DEVICE_STATUS_ERROR_TIMED_OUT, AudioParameters(), |
| std::string())); |
| } |
| } |
| |
| void AudioOutputDevice::CreateStreamOnIOThread() { |
| TRACE_EVENT0("audio", "AudioOutputDevice::Create"); |
| DCHECK(io_task_runner_->BelongsToCurrentThread()); |
| #if DCHECK_IS_ON() |
| { |
| base::AutoLock auto_lock(audio_thread_lock_); |
| if (!stopping_hack_) |
| DCHECK(callback_) << "Initialize hasn't been called"; |
| } |
| #endif |
| DCHECK_NE(state_, STREAM_CREATION_REQUESTED); |
| |
| if (!ipc_) { |
| NotifyRenderCallbackOfError(); |
| return; |
| } |
| |
| if (state_ == IDLE && !(did_receive_auth_.IsSignaled() && device_id_.empty())) |
| RequestDeviceAuthorizationOnIOThread(); |
| |
| ipc_->CreateStream(this, audio_parameters_, processing_id_); |
| // By default, start playing right away. |
| ipc_->PlayStream(); |
| state_ = STREAM_CREATION_REQUESTED; |
| } |
| |
| void AudioOutputDevice::PlayOnIOThread() { |
| DCHECK(io_task_runner_->BelongsToCurrentThread()); |
| if (audio_callback_) |
| audio_callback_->InitializePlayStartTime(); |
| |
| if (ipc_) |
| ipc_->PlayStream(); |
| } |
| |
| void AudioOutputDevice::PauseOnIOThread() { |
| DCHECK(io_task_runner_->BelongsToCurrentThread()); |
| |
| if (ipc_) |
| ipc_->PauseStream(); |
| } |
| |
| void AudioOutputDevice::FlushOnIOThread() { |
| DCHECK(io_task_runner_->BelongsToCurrentThread()); |
| |
| if (ipc_) |
| ipc_->FlushStream(); |
| } |
| |
| void AudioOutputDevice::ShutDownOnIOThread() { |
| DCHECK(io_task_runner_->BelongsToCurrentThread()); |
| |
| if (ipc_) |
| ipc_->CloseStream(); |
| |
| state_ = IDLE; |
| |
| // Destoy the timer on the thread it's used on. |
| auth_timeout_action_.reset(); |
| |
| UMA_HISTOGRAM_ENUMERATION("Media.Audio.Render.StreamCallbackError2", |
| had_error_); |
| had_error_ = kNoError; |
| |
| // We can run into an issue where ShutDownOnIOThread is called right after |
| // OnStreamCreated is called in cases where Start/Stop are called before we |
| // get the OnStreamCreated callback. To handle that corner case, we call |
| // Stop(). In most cases, the thread will already be stopped. |
| // |
| // Another situation is when the IO thread goes away before Stop() is called |
| // in which case, we cannot use the message loop to close the thread handle |
| // and can't rely on the main thread existing either. |
| base::AutoLock auto_lock_(audio_thread_lock_); |
| base::ScopedAllowBaseSyncPrimitivesOutsideBlockingScope allow_thread_join; |
| audio_thread_.reset(); |
| audio_callback_.reset(); |
| stopping_hack_ = false; |
| } |
| |
| void AudioOutputDevice::SetVolumeOnIOThread(double volume) { |
| DCHECK(io_task_runner_->BelongsToCurrentThread()); |
| if (ipc_) |
| ipc_->SetVolume(volume); |
| } |
| |
| void AudioOutputDevice::OnError() { |
| TRACE_EVENT0("audio", "AudioOutputDevice::OnError"); |
| |
| DCHECK(io_task_runner_->BelongsToCurrentThread()); |
| |
| // Do nothing if the stream has been closed. |
| if (state_ == IDLE) |
| return; |
| |
| // Don't dereference the callback object if the audio thread |
| // is stopped or stopping. That could mean that the callback |
| // object has been deleted. |
| // TODO(tommi): Add an explicit contract for clearing the callback |
| // object. Possibly require calling Initialize again or provide |
| // a callback object via Start() and clear it in Stop(). |
| NotifyRenderCallbackOfError(); |
| } |
| |
| void AudioOutputDevice::OnDeviceAuthorized( |
| OutputDeviceStatus device_status, |
| const AudioParameters& output_params, |
| const std::string& matched_device_id) { |
| DCHECK(io_task_runner_->BelongsToCurrentThread()); |
| |
| auth_timeout_action_.reset(); |
| |
| // Do nothing if late authorization is received after timeout. |
| if (!ipc_) |
| return; |
| |
| UMA_HISTOGRAM_BOOLEAN("Media.Audio.Render.OutputDeviceAuthorizationTimedOut", |
| device_status == OUTPUT_DEVICE_STATUS_ERROR_TIMED_OUT); |
| LOG_IF(WARNING, device_status == OUTPUT_DEVICE_STATUS_ERROR_TIMED_OUT) |
| << "Output device authorization timed out"; |
| |
| // It may happen that a second authorization is received as a result to a |
| // call to Start() after Stop(). If the status for the second authorization |
| // differs from the first, it will not be reflected in |device_status_| |
| // to avoid a race. |
| // This scenario is unlikely. If it occurs, the new value will be |
| // different from OUTPUT_DEVICE_STATUS_OK, so the AudioOutputDevice |
| // will enter the |ipc_| == nullptr state anyway, which is the safe thing to |
| // do. This is preferable to holding a lock. |
| if (!did_receive_auth_.IsSignaled()) { |
| device_status_ = device_status; |
| UMA_HISTOGRAM_ENUMERATION("Media.Audio.Render.OutputDeviceStatus", |
| device_status, OUTPUT_DEVICE_STATUS_MAX + 1); |
| } |
| |
| if (device_status == OUTPUT_DEVICE_STATUS_OK) { |
| TRACE_EVENT0("audio", "AudioOutputDevice authorized"); |
| |
| if (!did_receive_auth_.IsSignaled()) { |
| output_params_ = output_params; |
| |
| // It's possible to not have a matched device obtained via session id. It |
| // means matching output device through |session_id_| failed and the |
| // default device is used. |
| DCHECK(AudioDeviceDescription::UseSessionIdToSelectDevice(session_id_, |
| device_id_) || |
| matched_device_id_.empty()); |
| matched_device_id_ = matched_device_id; |
| |
| DVLOG(1) << "AudioOutputDevice authorized, session_id: " << session_id_ |
| << ", device_id: " << device_id_ |
| << ", matched_device_id: " << matched_device_id_; |
| |
| OnAuthSignal(); |
| } |
| } else { |
| TRACE_EVENT1("audio", "AudioOutputDevice not authorized", "auth status", |
| device_status_); |
| |
| // Closing IPC forces a Signal(), so no clients are locked waiting |
| // indefinitely after this method returns. |
| ipc_->CloseStream(); |
| OnIPCClosed(); |
| |
| NotifyRenderCallbackOfError(); |
| } |
| } |
| |
| void AudioOutputDevice::OnStreamCreated( |
| base::UnsafeSharedMemoryRegion shared_memory_region, |
| base::SyncSocket::ScopedHandle socket_handle, |
| bool playing_automatically) { |
| TRACE_EVENT0("audio", "AudioOutputDevice::OnStreamCreated"); |
| |
| DCHECK(io_task_runner_->BelongsToCurrentThread()); |
| DCHECK(shared_memory_region.IsValid()); |
| #if defined(OS_WIN) |
| DCHECK(socket_handle.IsValid()); |
| #else |
| DCHECK(socket_handle.is_valid()); |
| #endif |
| DCHECK_GT(shared_memory_region.GetSize(), 0u); |
| |
| if (state_ != STREAM_CREATION_REQUESTED) |
| return; |
| |
| // We can receive OnStreamCreated() on the IO thread after the client has |
| // called Stop() but before ShutDownOnIOThread() is processed. In such a |
| // situation |callback_| might point to freed memory. Instead of starting |
| // |audio_thread_| do nothing and wait for ShutDownOnIOThread() to get called. |
| // |
| // TODO(scherkus): The real fix is to have sane ownership semantics. The fact |
| // that |callback_| (which should own and outlive this object!) can point to |
| // freed memory is a mess. AudioRendererSink should be non-refcounted so that |
| // owners (WebRtcAudioDeviceImpl, AudioRendererImpl, etc...) can Stop() and |
| // delete as they see fit. AudioOutputDevice should internally use WeakPtr |
| // to handle teardown and thread hopping. See http://crbug.com/151051 for |
| // details. |
| { |
| base::AutoLock auto_lock(audio_thread_lock_); |
| if (stopping_hack_) |
| return; |
| |
| DCHECK(!audio_thread_); |
| DCHECK(!audio_callback_); |
| |
| audio_callback_ = std::make_unique<AudioOutputDeviceThreadCallback>( |
| audio_parameters_, std::move(shared_memory_region), callback_); |
| if (playing_automatically) |
| audio_callback_->InitializePlayStartTime(); |
| audio_thread_ = std::make_unique<AudioDeviceThread>( |
| audio_callback_.get(), std::move(socket_handle), "AudioOutputDevice", |
| base::ThreadPriority::REALTIME_AUDIO); |
| } |
| } |
| |
| void AudioOutputDevice::OnIPCClosed() { |
| TRACE_EVENT0("audio", "AudioOutputDevice::OnIPCClosed"); |
| DCHECK(io_task_runner_->BelongsToCurrentThread()); |
| |
| ipc_.reset(); |
| state_ = IDLE; |
| |
| OnAuthSignal(); |
| } |
| |
| OutputDeviceInfo AudioOutputDevice::GetOutputDeviceInfo_Signaled() { |
| DCHECK(did_receive_auth_.IsSignaled()); |
| return OutputDeviceInfo(AudioDeviceDescription::UseSessionIdToSelectDevice( |
| session_id_, device_id_) |
| ? matched_device_id_ |
| : device_id_, |
| device_status_, output_params_); |
| } |
| |
| void AudioOutputDevice::OnAuthSignal() { |
| DCHECK(io_task_runner_->BelongsToCurrentThread()); |
| |
| // This lock is held while signaling to avoid any thread safety issues while |
| // GetOutputDeviceInfoAsync() may be checking the signal and modifying the |
| // |pending_device_info_cb_| on another thread. |
| // |
| // We might be able to get away with signaling outside of the lock, but this |
| // requires more careful construction for anyone checking the signal and |
| // using the result to set or get the pending callback value. The failure |
| // mode is also more subtle, callbacks will be lost versus a thread hang which |
| // is more easily detectable in the production population. |
| base::AutoLock auto_lock(device_info_lock_); |
| |
| // Signal to unblock any blocked threads waiting for parameters. |
| did_receive_auth_.Signal(); |
| |
| // The callback is always posted by way media::BindToCurrentLoop() usage upon |
| // receipt, so this is safe to run under the lock. |
| if (pending_device_info_cb_) |
| std::move(pending_device_info_cb_).Run(GetOutputDeviceInfo_Signaled()); |
| } |
| |
| void AudioOutputDevice::NotifyRenderCallbackOfError() { |
| TRACE_EVENT0("audio", "AudioOutputDevice::NotifyRenderCallbackOfError"); |
| DCHECK(io_task_runner_->BelongsToCurrentThread()); |
| |
| base::AutoLock auto_lock(audio_thread_lock_); |
| // Avoid signaling error if Initialize() hasn't been called yet, or if |
| // Stop() has already been called. |
| if (callback_ && !stopping_hack_) { |
| // Update |had_error_| for UMA stats. |
| if (audio_callback_) |
| had_error_ = kErrorDuringRendering; |
| else |
| had_error_ = kErrorDuringCreation; |
| callback_->OnRenderError(); |
| } |
| } |
| |
| } // namespace media |