chromium/chromecast/media/audio/cast_audio_output_device.cc

// 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