| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chromecast/media/audio/cast_audio_output_device.h" |
| |
| #include <cstdint> |
| #include <limits> |
| #include <string> |
| #include <utility> |
| |
| #include "base/check.h" |
| #include "base/functional/bind.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/task/bind_post_task.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/time/time.h" |
| #include "base/timer/timer.h" |
| #include "chromecast/media/audio/audio_io_thread.h" |
| #include "chromecast/media/audio/audio_output_service/audio_output_service.pb.h" |
| #include "chromecast/media/audio/audio_output_service/output_stream_connection.h" |
| #include "chromecast/media/base/default_monotonic_clock.h" |
| #include "media/base/audio_bus.h" |
| #include "media/base/audio_glitch_info.h" |
| #include "media/base/audio_parameters.h" |
| #include "media/base/audio_timestamp_helper.h" |
| #include "net/base/io_buffer.h" |
| |
| namespace chromecast { |
| namespace media { |
| |
| namespace { |
| |
| constexpr base::TimeDelta kNoBufferReadDelay = base::Milliseconds(50); |
| |
| // When to treat lack of audio data in buffer as a underflow. |
| constexpr base::TimeDelta kBufferUnderflowThreshold = base::Milliseconds(400); |
| |
| // Initial renderer buffer size estimation. The value should be smaller than the |
| // audio renderer start capacity. |
| constexpr base::TimeDelta kPrefetchRendererBufferSize = base::Seconds(4); |
| constexpr base::TimeDelta kDefaultRendererBufferSize = base::Milliseconds(160); |
| |
| } // namespace |
| |
| // Internal helper class. The constructor/StartRender/StopRender are called on |
| // caller's thread. The other functions including destructor should be used on |
| // an IO thread. |
| class CastAudioOutputDevice::Internal |
| : public audio_output_service::OutputStreamConnection::Delegate { |
| public: |
| Internal(const ::media::AudioParameters& audio_params, |
| RenderCallback* render_callback) |
| : audio_params_(audio_params), render_callback_(render_callback) { |
| DCHECK(render_callback_); |
| } |
| |
| Internal(const Internal&) = delete; |
| Internal& operator=(const Internal&) = delete; |
| ~Internal() override = default; |
| |
| // One time init on IO thread. |
| void Initialize( |
| mojo::PendingRemote<mojom::AudioSocketBroker> audio_socket_broker, |
| const std::string& session_id) { |
| audio_output_service::CmaBackendParams cma_backend_params; |
| |
| audio_output_service::AudioDecoderConfig* audio_config = |
| cma_backend_params.mutable_audio_decoder_config(); |
| audio_config->set_audio_codec(audio_service::AudioCodec::AUDIO_CODEC_PCM); |
| audio_config->set_sample_rate(audio_params_.sample_rate()); |
| audio_config->set_sample_format( |
| audio_service::SampleFormat::SAMPLE_FORMAT_INT16_I); |
| audio_config->set_num_channels(audio_params_.channels()); |
| |
| audio_output_service::ApplicationMediaInfo* app_media_info = |
| cma_backend_params.mutable_application_media_info(); |
| app_media_info->set_application_session_id(session_id); |
| |
| audio_bus_ = ::media::AudioBus::Create(audio_params_); |
| output_connection_ = |
| std::make_unique<audio_output_service::OutputStreamConnection>( |
| this, cma_backend_params, std::move(audio_socket_broker)); |
| output_connection_->Connect(); |
| } |
| |
| void StartRender() { |
| base::AutoLock lock(callback_lock_); |
| active_render_callback_ = render_callback_; |
| } |
| |
| void StopRender() { |
| base::AutoLock lock(callback_lock_); |
| active_render_callback_ = nullptr; |
| } |
| |
| void Start() { |
| playback_started_ = true; |
| media_pos_frames_ = 0; |
| renderer_buffer_size_estimate_ = |
| (audio_params_.effects() & ::media::AudioParameters::AUDIO_PREFETCH) |
| ? kPrefetchRendererBufferSize |
| : kDefaultRendererBufferSize; |
| DCHECK_GT(renderer_buffer_size_estimate_, |
| audio_params_.GetBufferDuration()); |
| |
| media_start_time_ = base::TimeTicks::Now(); |
| if (!backend_initialized_) { |
| // Wait for initialization to complete before sending messages through |
| // `output_connection_`. |
| return; |
| } |
| output_connection_->StartPlayingFrom(0); |
| } |
| |
| void Pause() { |
| paused_ = true; |
| |
| if (!backend_initialized_) { |
| return; |
| } |
| |
| output_connection_->SetPlaybackRate(0.0f); |
| push_timer_.Stop(); |
| } |
| |
| void Play() { |
| paused_ = false; |
| if (!playback_started_) { |
| Start(); |
| } |
| if (!backend_initialized_) { |
| return; |
| } |
| |
| last_read_buffer_timestamp_ = base::TimeTicks(); |
| output_connection_->SetPlaybackRate(1.0f); |
| } |
| |
| void Flush() { |
| if (!backend_initialized_) { |
| return; |
| } |
| media_pos_frames_ = 0; |
| media_start_time_ = base::TimeTicks::Now(); |
| playback_started_ = false; |
| paused_ = false; |
| push_timer_.Stop(); |
| output_connection_->StopPlayback(); |
| } |
| |
| void SetVolume(double volume) { |
| volume_ = volume; |
| if (!backend_initialized_) { |
| return; |
| } |
| output_connection_->SetVolume(volume_); |
| } |
| |
| private: |
| // audio_output_service::OutputStreamConnection::Delegate implementation: |
| void OnBackendInitialized( |
| const audio_output_service::BackendInitializationStatus& status) |
| override { |
| if (status.status() != |
| audio_output_service::BackendInitializationStatus::SUCCESS) { |
| LOG(ERROR) << "Error initializing the audio backend."; |
| base::AutoLock lock(callback_lock_); |
| if (active_render_callback_) { |
| active_render_callback_->OnRenderError(); |
| } |
| return; |
| } |
| |
| DCHECK(output_connection_); |
| backend_initialized_ = true; |
| output_connection_->SetVolume(volume_); |
| if (paused_) { |
| output_connection_->SetPlaybackRate(0.0f); |
| } |
| if (!playback_started_) { |
| // Wait for Start() to be called before schedule reading buffers. |
| return; |
| } |
| output_connection_->StartPlayingFrom(0); |
| if (!paused_) { |
| last_read_buffer_timestamp_ = base::TimeTicks(); |
| output_connection_->SetPlaybackRate(1.0f); |
| } |
| } |
| |
| void OnNextBuffer(int64_t media_timestamp_microseconds, |
| int64_t reference_timestamp_microseconds, |
| int64_t delay_microseconds, |
| int64_t delay_timestamp_microseconds) override { |
| rendering_delay_ = base::Microseconds(delay_microseconds); |
| rendering_delay_timestamp_us_ = delay_timestamp_microseconds; |
| |
| TryPushBuffer(); |
| } |
| |
| void TryPushBuffer() { |
| if (paused_ || !backend_initialized_ || !playback_started_ || |
| push_timer_.IsRunning()) { |
| return; |
| } |
| |
| PushBuffer(); |
| } |
| |
| void PushBuffer() { |
| auto now = base::TimeTicks::Now(); |
| if (last_read_buffer_timestamp_.is_null()) { |
| last_read_buffer_timestamp_ = now; |
| } |
| |
| base::TimeDelta elapsed_time = now - last_read_buffer_timestamp_; |
| base::TimeDelta time_before_underrun = |
| audio_params_.GetBufferDuration() - |
| (renderer_buffer_size_estimate_ + elapsed_time); |
| if (time_before_underrun > base::TimeDelta()) { |
| // Let renderer buffer more data. |
| push_timer_.Start(FROM_HERE, now + time_before_underrun, this, |
| &Internal::TryPushBuffer, |
| base::subtle::DelayPolicy::kPrecise); |
| return; |
| } |
| |
| int frames_filled = ReadBuffer(GetDelay(), audio_bus_.get()); |
| renderer_buffer_size_estimate_ += elapsed_time; |
| last_read_buffer_timestamp_ = now; |
| auto media_pos = ::media::AudioTimestampHelper::FramesToTime( |
| media_pos_frames_, audio_params_.sample_rate()); |
| if (frames_filled) { |
| renderer_buffer_size_estimate_ -= |
| ::media::AudioTimestampHelper::FramesToTime( |
| frames_filled, audio_params_.sample_rate()); |
| DCHECK_GE(renderer_buffer_size_estimate_, base::TimeDelta()); |
| |
| size_t filled_bytes = frames_filled * audio_params_.GetBytesPerFrame( |
| ::media::kSampleFormatS16); |
| size_t io_buffer_size = |
| audio_output_service::OutputSocket::kAudioMessageHeaderSize + |
| filled_bytes; |
| auto io_buffer = |
| base::MakeRefCounted<net::IOBufferWithSize>(io_buffer_size); |
| audio_bus_->ToInterleaved<::media::SignedInt16SampleTypeTraits>( |
| frames_filled, |
| reinterpret_cast<int16_t*>( |
| io_buffer->data() + |
| audio_output_service::OutputSocket::kAudioMessageHeaderSize)); |
| |
| DCHECK(output_connection_); |
| output_connection_->SendAudioBuffer(std::move(io_buffer), filled_bytes, |
| media_pos.InMicroseconds()); |
| media_pos_frames_ += frames_filled; |
| |
| // No need to schedule buffer read here since |
| // `OnNextBuffer` will be called once the current |
| // buffer is pushed to media backend. |
| return; |
| } |
| |
| // Avoid spam calling Render() since each call will advance the |AudioClock| |
| // a little bit if 0 frames are rendered. |
| // Wait until some rendered data is consumed before retrying Render(). |
| base::TimeDelta time_left_in_buffer = |
| media_pos - (base::TimeTicks::Now() - media_start_time_); |
| if (time_left_in_buffer > kBufferUnderflowThreshold) { |
| push_timer_.Start(FROM_HERE, base::TimeTicks::Now() + time_left_in_buffer, this, |
| &Internal::TryPushBuffer, |
| base::subtle::DelayPolicy::kPrecise); |
| return; |
| } |
| |
| // No frames filled, schedule read immediately with a small delay. |
| push_timer_.Start(FROM_HERE, base::TimeTicks::Now() + kNoBufferReadDelay, |
| this, &Internal::TryPushBuffer, |
| base::subtle::DelayPolicy::kPrecise); |
| } |
| |
| int ReadBuffer(base::TimeDelta delay, ::media::AudioBus* audio_bus) { |
| DCHECK(audio_bus); |
| base::AutoLock lock(callback_lock_); |
| if (!active_render_callback_) { |
| return 0; |
| } |
| return active_render_callback_->Render(delay, base::TimeTicks(), |
| /*glitch_info=*/{}, audio_bus); |
| } |
| |
| base::TimeDelta GetDelay() { |
| base::TimeDelta delay; |
| if (rendering_delay_ < base::TimeDelta() || |
| rendering_delay_timestamp_us_ < 0) { |
| delay = base::TimeDelta(); |
| } else { |
| delay = |
| rendering_delay_ + base::Microseconds(rendering_delay_timestamp_us_ - |
| MonotonicClockNow()); |
| if (delay < base::TimeDelta()) { |
| delay = base::TimeDelta(); |
| } |
| } |
| return delay; |
| } |
| |
| scoped_refptr<CastAudioOutputDevice> output_device_; |
| std::unique_ptr<audio_output_service::OutputStreamConnection> |
| output_connection_; |
| |
| mojo::PendingRemote<mojom::AudioSocketBroker> pending_socket_broker_; |
| ::media::AudioParameters audio_params_; |
| size_t media_pos_frames_ = 0; |
| // When we start playing media. Used to determine the current position in the track. |
| base::TimeTicks media_start_time_ = base::TimeTicks(); |
| base::TimeDelta rendering_delay_; |
| |
| int64_t rendering_delay_timestamp_us_ = INT64_MIN; |
| double volume_ = 1.0; |
| bool paused_ = false; |
| bool playback_started_ = false; |
| bool backend_initialized_ = false; |
| base::DeadlineTimer push_timer_; |
| std::unique_ptr<::media::AudioBus> audio_bus_; |
| |
| // Callback to get audio data. |
| RenderCallback* const render_callback_; |
| |
| // Estimation of the renderer buffer size. We should not pull too much buffer |
| // from renderer to avoid underrun. |
| base::TimeDelta renderer_buffer_size_estimate_; |
| |
| base::TimeTicks last_read_buffer_timestamp_; |
| |
| base::Lock callback_lock_; |
| // Nullable callback that is only available during StartRender/StopRender. |
| RenderCallback* active_render_callback_ GUARDED_BY(callback_lock_) = nullptr; |
| }; |
| |
| CastAudioOutputDevice::CastAudioOutputDevice( |
| mojo::PendingRemote<mojom::AudioSocketBroker> audio_socket_broker, |
| const std::string& session_id) |
| : CastAudioOutputDevice(std::move(audio_socket_broker), |
| session_id, |
| AudioIoThread::Get()->task_runner()) {} |
| |
| CastAudioOutputDevice::CastAudioOutputDevice( |
| mojo::PendingRemote<mojom::AudioSocketBroker> audio_socket_broker, |
| const std::string& session_id, |
| scoped_refptr<base::SequencedTaskRunner> task_runner) |
| : task_runner_(std::move(task_runner)), |
| pending_socket_broker_(std::move(audio_socket_broker)), |
| session_id_(session_id) {} |
| |
| CastAudioOutputDevice::~CastAudioOutputDevice() { |
| if (!internal_) { |
| return; |
| } |
| |
| internal_ptr_->StopRender(); |
| } |
| |
| void CastAudioOutputDevice::Initialize(const ::media::AudioParameters& params, |
| RenderCallback* callback) { |
| DCHECK(callback); |
| DCHECK(!internal_); |
| DCHECK(!internal_ptr_); |
| |
| auto internal = std::make_unique<Internal>(params, callback); |
| internal_ptr_ = internal.get(); |
| internal_.emplace(task_runner_, std::move(internal)); |
| |
| internal_.AsyncCall(&Internal::Initialize) |
| .WithArgs(std::move(pending_socket_broker_), session_id_); |
| } |
| |
| void CastAudioOutputDevice::Start() { |
| internal_ptr_->StartRender(); |
| internal_.AsyncCall(&Internal::Start); |
| } |
| |
| void CastAudioOutputDevice::Stop() { |
| internal_ptr_->StopRender(); |
| Flush(); |
| } |
| |
| void CastAudioOutputDevice::Pause() { |
| internal_.AsyncCall(&Internal::Pause); |
| } |
| |
| void CastAudioOutputDevice::Play() { |
| internal_.AsyncCall(&Internal::Play); |
| } |
| |
| void CastAudioOutputDevice::Flush() { |
| internal_.AsyncCall(&Internal::Flush); |
| } |
| |
| bool CastAudioOutputDevice::SetVolume(double volume) { |
| internal_.AsyncCall(&Internal::SetVolume).WithArgs(volume); |
| return true; |
| } |
| |
| ::media::OutputDeviceInfo CastAudioOutputDevice::GetOutputDeviceInfo() { |
| // Same as the set of parameters returned in |
| // CastAudioManager::GetPreferredOutputStreamParameters. |
| return ::media::OutputDeviceInfo( |
| std::string(), ::media::OUTPUT_DEVICE_STATUS_OK, |
| ::media::AudioParameters(::media::AudioParameters::AUDIO_PCM_LOW_LATENCY, |
| ::media::ChannelLayoutConfig::Stereo(), 48000, |
| 480)); |
| } |
| |
| void CastAudioOutputDevice::GetOutputDeviceInfoAsync( |
| OutputDeviceInfoCB info_cb) { |
| // Always post to avoid the caller being reentrant. |
| base::BindPostTaskToCurrentDefault( |
| base::BindOnce(std::move(info_cb), GetOutputDeviceInfo())) |
| .Run(); |
| } |
| |
| bool CastAudioOutputDevice::IsOptimizedForHardwareParameters() { |
| return false; |
| } |
| |
| bool CastAudioOutputDevice::CurrentThreadIsRenderingThread() { |
| return task_runner_->RunsTasksInCurrentSequence(); |
| } |
| |
| } // namespace media |
| } // namespace chromecast |