// 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 #include #include #include #include "base/callback_helpers.h" #include "base/macros.h" #include "base/metrics/histogram_macros.h" #include "base/threading/thread_restrictions.h" #include "base/time/time.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_controller.h" #include "media/base/limits.h" namespace media { // Takes care of invoking the render callback on the audio thread. // An instance of this class is created for each capture stream in // OnStreamCreated(). class AudioOutputDevice::AudioThreadCallback : public AudioDeviceThread::Callback { public: AudioThreadCallback(const AudioParameters& audio_parameters, base::SharedMemoryHandle memory, int memory_length, AudioRendererSink::RenderCallback* render_callback); ~AudioThreadCallback() override; void MapSharedMemory() override; // Called whenever we receive notifications about pending data. void Process(uint32_t control_signal) override; // Returns whether the current thread is the audio device thread or not. // Will always return true if DCHECKs are not enabled. bool CurrentThreadIsAudioDeviceThread(); private: AudioRendererSink::RenderCallback* render_callback_; std::unique_ptr output_bus_; uint64_t callback_num_; DISALLOW_COPY_AND_ASSIGN(AudioThreadCallback); }; AudioOutputDevice::AudioOutputDevice( std::unique_ptr ipc, const scoped_refptr& io_task_runner, int session_id, const std::string& device_id, const url::Origin& security_origin, base::TimeDelta authorization_timeout) : ScopedTaskRunnerObserver(io_task_runner), callback_(NULL), ipc_(std::move(ipc)), state_(IDLE), start_on_authorized_(false), play_on_start_(true), session_id_(session_id), device_id_(device_id), security_origin_(security_origin), 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) { CHECK(ipc_); // The correctness of the code depends on the relative values assigned in the // State enum. static_assert(IPC_CLOSED < IDLE, "invalid enum value assignment 0"); static_assert(IDLE < AUTHORIZING, "invalid enum value assignment 1"); static_assert(AUTHORIZING < AUTHORIZED, "invalid enum value assignment 2"); static_assert(AUTHORIZED < CREATING_STREAM, "invalid enum value assignment 3"); static_assert(CREATING_STREAM < PAUSED, "invalid enum value assignment 4"); static_assert(PAUSED < PLAYING, "invalid enum value assignment 5"); } void AudioOutputDevice::Initialize(const AudioParameters& params, RenderCallback* callback) { DCHECK(!callback_) << "Calling Initialize() twice?"; DCHECK(params.IsValid()); audio_parameters_ = params; callback_ = callback; } AudioOutputDevice::~AudioOutputDevice() {} void AudioOutputDevice::RequestDeviceAuthorization() { task_runner()->PostTask( FROM_HERE, base::Bind(&AudioOutputDevice::RequestDeviceAuthorizationOnIOThread, this)); } void AudioOutputDevice::Start() { DCHECK(callback_) << "Initialize hasn't been called"; task_runner()->PostTask(FROM_HERE, base::Bind(&AudioOutputDevice::CreateStreamOnIOThread, this, audio_parameters_)); } void AudioOutputDevice::Stop() { { base::AutoLock auto_lock(audio_thread_lock_); audio_thread_.reset(); stopping_hack_ = true; } task_runner()->PostTask(FROM_HERE, base::Bind(&AudioOutputDevice::ShutDownOnIOThread, this)); } void AudioOutputDevice::Play() { task_runner()->PostTask(FROM_HERE, base::Bind(&AudioOutputDevice::PlayOnIOThread, this)); } void AudioOutputDevice::Pause() { task_runner()->PostTask(FROM_HERE, base::Bind(&AudioOutputDevice::PauseOnIOThread, this)); } bool AudioOutputDevice::SetVolume(double volume) { if (volume < 0 || volume > 1.0) return false; if (!task_runner()->PostTask(FROM_HERE, base::Bind(&AudioOutputDevice::SetVolumeOnIOThread, this, volume))) { return false; } return true; } OutputDeviceInfo AudioOutputDevice::GetOutputDeviceInfo() { CHECK(!task_runner()->BelongsToCurrentThread()); did_receive_auth_.Wait(); return OutputDeviceInfo(AudioDeviceDescription::UseSessionIdToSelectDevice( session_id_, device_id_) ? matched_device_id_ : device_id_, device_status_, output_params_); } 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(task_runner()->BelongsToCurrentThread()); DCHECK_EQ(state_, IDLE); state_ = AUTHORIZING; ipc_->RequestDeviceAuthorization(this, session_id_, device_id_, security_origin_); 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_.reset(new base::OneShotTimer()); auth_timeout_action_->Start( FROM_HERE, auth_timeout_, base::Bind(&AudioOutputDevice::OnDeviceAuthorized, this, OUTPUT_DEVICE_STATUS_ERROR_TIMED_OUT, media::AudioParameters(), std::string())); } } void AudioOutputDevice::CreateStreamOnIOThread(const AudioParameters& params) { DCHECK(task_runner()->BelongsToCurrentThread()); switch (state_) { case IPC_CLOSED: if (callback_) callback_->OnRenderError(); break; case IDLE: if (did_receive_auth_.IsSignaled() && device_id_.empty() && security_origin_.unique()) { state_ = CREATING_STREAM; ipc_->CreateStream(this, params); } else { RequestDeviceAuthorizationOnIOThread(); start_on_authorized_ = true; } break; case AUTHORIZING: start_on_authorized_ = true; break; case AUTHORIZED: state_ = CREATING_STREAM; ipc_->CreateStream(this, params); start_on_authorized_ = false; break; case CREATING_STREAM: case PAUSED: case PLAYING: NOTREACHED(); break; } } void AudioOutputDevice::PlayOnIOThread() { DCHECK(task_runner()->BelongsToCurrentThread()); if (state_ == PAUSED) { TRACE_EVENT_ASYNC_BEGIN0( "audio", "StartingPlayback", audio_callback_.get()); ipc_->PlayStream(); state_ = PLAYING; play_on_start_ = false; } else { play_on_start_ = true; } } void AudioOutputDevice::PauseOnIOThread() { DCHECK(task_runner()->BelongsToCurrentThread()); if (state_ == PLAYING) { TRACE_EVENT_ASYNC_END0( "audio", "StartingPlayback", audio_callback_.get()); ipc_->PauseStream(); state_ = PAUSED; } play_on_start_ = false; } void AudioOutputDevice::ShutDownOnIOThread() { DCHECK(task_runner()->BelongsToCurrentThread()); // Close the stream, if we haven't already. if (state_ >= AUTHORIZING) { ipc_->CloseStream(); state_ = IDLE; } start_on_authorized_ = false; // Destoy the timer on the thread it's used on. auth_timeout_action_.reset(); // 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::ThreadRestrictions::ScopedAllowIO allow_io; audio_thread_.reset(); audio_callback_.reset(); stopping_hack_ = false; } void AudioOutputDevice::SetVolumeOnIOThread(double volume) { DCHECK(task_runner()->BelongsToCurrentThread()); if (state_ >= CREATING_STREAM) ipc_->SetVolume(volume); } void AudioOutputDevice::OnError() { DCHECK(task_runner()->BelongsToCurrentThread()); // Do nothing if the stream has been closed. if (state_ < CREATING_STREAM) return; DLOG(WARNING) << "AudioOutputDevice::OnError()"; // 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(). { base::AutoLock auto_lock_(audio_thread_lock_); if (audio_thread_) callback_->OnRenderError(); } } void AudioOutputDevice::OnDeviceAuthorized( OutputDeviceStatus device_status, const media::AudioParameters& output_params, const std::string& matched_device_id) { DCHECK(task_runner()->BelongsToCurrentThread()); auth_timeout_action_.reset(); // Do nothing if late authorization is received after timeout. if (state_ == IPC_CLOSED) 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"; DCHECK_EQ(state_, AUTHORIZING); // 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_CLOSED 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) { state_ = 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_; did_receive_auth_.Signal(); } if (start_on_authorized_) CreateStreamOnIOThread(audio_parameters_); } else { // Closing IPC forces a Signal(), so no clients are locked waiting // indefinitely after this method returns. ipc_->CloseStream(); OnIPCClosed(); if (callback_) callback_->OnRenderError(); } } void AudioOutputDevice::OnStreamCreated( base::SharedMemoryHandle handle, base::SyncSocket::Handle socket_handle, int length) { DCHECK(task_runner()->BelongsToCurrentThread()); DCHECK(base::SharedMemory::IsHandleValid(handle)); #if defined(OS_WIN) DCHECK(socket_handle); #else DCHECK_GE(socket_handle, 0); #endif DCHECK_GT(length, 0); if (state_ != CREATING_STREAM) 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_.reset(new AudioOutputDevice::AudioThreadCallback( audio_parameters_, handle, length, callback_)); audio_thread_.reset(new AudioDeviceThread( audio_callback_.get(), socket_handle, "AudioOutputDevice")); state_ = PAUSED; // We handle the case where Play() and/or Pause() may have been called // multiple times before OnStreamCreated() gets called. if (play_on_start_) PlayOnIOThread(); } } void AudioOutputDevice::OnIPCClosed() { DCHECK(task_runner()->BelongsToCurrentThread()); state_ = IPC_CLOSED; ipc_.reset(); // Signal to unblock any blocked threads waiting for parameters did_receive_auth_.Signal(); } void AudioOutputDevice::WillDestroyCurrentMessageLoop() { LOG(ERROR) << "IO loop going away before the audio device has been stopped"; ShutDownOnIOThread(); } // AudioOutputDevice::AudioThreadCallback AudioOutputDevice::AudioThreadCallback::AudioThreadCallback( const AudioParameters& audio_parameters, base::SharedMemoryHandle memory, int memory_length, AudioRendererSink::RenderCallback* render_callback) : AudioDeviceThread::Callback(audio_parameters, memory, memory_length, 1), render_callback_(render_callback), callback_num_(0) {} AudioOutputDevice::AudioThreadCallback::~AudioThreadCallback() { } void AudioOutputDevice::AudioThreadCallback::MapSharedMemory() { CHECK_EQ(total_segments_, 1); CHECK(shared_memory_.Map(memory_length_)); DCHECK_EQ(static_cast(memory_length_), sizeof(AudioOutputBufferParameters) + AudioBus::CalculateMemorySize(audio_parameters_)); AudioOutputBuffer* buffer = reinterpret_cast(shared_memory_.memory()); output_bus_ = AudioBus::WrapMemory(audio_parameters_, buffer->audio); } // Called whenever we receive notifications about pending data. void AudioOutputDevice::AudioThreadCallback::Process(uint32_t control_signal) { callback_num_++; TRACE_EVENT1("audio", "AudioOutputDevice::FireRenderCallback", "callback_num", callback_num_); // When playback starts, we get an immediate callback to Process to make sure // that we have some data, we'll get another one after the device is awake and // ingesting data, which is what we want to track with this trace. if (callback_num_ == 2) { TRACE_EVENT_ASYNC_END0("audio", "StartingPlayback", this); } // Read and reset the number of frames skipped. AudioOutputBuffer* buffer = reinterpret_cast(shared_memory_.memory()); uint32_t frames_skipped = buffer->params.frames_skipped; buffer->params.frames_skipped = 0; base::TimeDelta delay = base::TimeDelta::FromMicroseconds(buffer->params.delay); base::TimeTicks delay_timestamp = base::TimeTicks() + base::TimeDelta::FromMicroseconds(buffer->params.delay_timestamp); DVLOG(4) << __func__ << " delay:" << delay << " delay_timestamp:" << delay << " frames_skipped:" << frames_skipped; // Update the audio-delay measurement, inform about the number of skipped // frames, and ask client to render audio. Since |output_bus_| is wrapping // the shared memory the Render() call is writing directly into the shared // memory. render_callback_->Render(delay, delay_timestamp, frames_skipped, output_bus_.get()); } bool AudioOutputDevice::AudioThreadCallback:: CurrentThreadIsAudioDeviceThread() { return thread_checker_.CalledOnValidThread(); } } // namespace media