chromium/chromeos/ash/services/libassistant/audio/audio_output_provider_impl_unittest.cc

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/services/libassistant/audio/audio_output_provider_impl.h"

#include <memory>
#include <utility>

#include "ash/constants/ash_features.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/run_loop.h"
#include "base/task/bind_post_task.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/threading/thread.h"
#include "base/time/time.h"
#include "chromeos/ash/services/libassistant/test_support/fake_platform_delegate.h"
#include "chromeos/assistant/internal/libassistant/shared_headers.h"
#include "media/base/audio_bus.h"
#include "media/base/audio_glitch_info.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash::libassistant {
namespace {
using assistant::FakePlatformDelegate;
using assistant::mojom::AssistantAudioDecoderFactory;
using ::assistant_client::OutputStreamMetadata;
using ::base::test::ScopedFeatureList;
using ::base::test::SingleThreadTaskEnvironment;

constexpr char kFakeDeviceId[] = "device_id";
}  // namespace

class FakeAudioOutputDelegate : public assistant_client::AudioOutput::Delegate {
 public:
  FakeAudioOutputDelegate() : thread_("assistant") { thread_.Start(); }

  FakeAudioOutputDelegate(const FakeAudioOutputDelegate&) = delete;
  FakeAudioOutputDelegate& operator=(const FakeAudioOutputDelegate&) = delete;

  ~FakeAudioOutputDelegate() override = default;

  // assistant_client::AudioOutput::Delegate overrides:
  void FillBuffer(void* buffer,
                  int buffer_size,
                  int64_t playback_timestamp,
                  assistant_client::Callback1<int> done_cb) override {
    // Fill some arbitrary stuff.
    memset(reinterpret_cast<uint8_t*>(buffer), '1', num_bytes_to_fill_);
    int filled_bytes = num_bytes_to_fill_;
    num_bytes_to_fill_ = 0;

    // We'll need to maintain the multi-threaded async semantics as the real
    // assistant. Otherwise, it'll cause re-entrance of locks.
    thread_.task_runner()->PostTask(
        FROM_HERE, base::BindOnce(&FakeAudioOutputDelegate::FillBufferDone,
                                  base::Unretained(this), std::move(done_cb),
                                  filled_bytes));
  }

  void OnEndOfStream() override { end_of_stream_ = true; }

  void OnError(assistant_client::AudioOutput::Error error) override {}

  void OnStopped() override {}

  void FillBufferDone(assistant_client::Callback1<int> cb, int num_bytes) {
    cb(num_bytes);

    // AudioDeviceOwner::ScheduleFillLocked() will be called repeatedlly until
    // the |num_bytes| is 0. Only call QuitClosure() at the last call to unblock
    // in the test.
    // Otherwise, the |run_loop_| may not block because the QuitClosure() is
    // called before Run(), right after it is created in Reset(), which will
    // cause timing issue in the test.
    if (num_bytes == 0) {
      quit_closure_.Run();
    }
  }

  bool end_of_stream() { return end_of_stream_; }

  void set_num_of_bytes_to_fill(int bytes) { num_bytes_to_fill_ = bytes; }

  void Reset() {
    run_loop_ = std::make_unique<base::RunLoop>();
    quit_closure_ =
        base::BindPostTaskToCurrentDefault(run_loop_->QuitClosure());
  }

  void Wait() { run_loop_->Run(); }

 private:
  base::Thread thread_;
  base::RepeatingClosure quit_closure_;
  std::unique_ptr<base::RunLoop> run_loop_;
  int num_bytes_to_fill_ = 0;
  bool end_of_stream_ = false;
};

class FakeAudioOutputDelegateMojom : public mojom::AudioOutputDelegate {
 public:
  FakeAudioOutputDelegateMojom() = default;
  FakeAudioOutputDelegateMojom(const FakeAudioOutputDelegateMojom&) = delete;
  FakeAudioOutputDelegateMojom& operator=(const FakeAudioOutputDelegateMojom&) =
      delete;
  ~FakeAudioOutputDelegateMojom() override = default;

  // libassistant::mojom::AudioOutputDelegate implementation:
  void RequestAudioFocus(mojom::AudioOutputStreamType stream_type) override {}
  void AbandonAudioFocusIfNeeded() override {}
  void AddMediaSessionObserver(
      mojo::PendingRemote<::media_session::mojom::MediaSessionObserver>
          observer) override {}
};

class FakeAssistantAudioDecoderFactory : public AssistantAudioDecoderFactory {
 public:
  FakeAssistantAudioDecoderFactory() = default;

  void CreateAssistantAudioDecoder(
      ::mojo::PendingReceiver<::ash::assistant::mojom::AssistantAudioDecoder>
          audio_decoder,
      ::mojo::PendingRemote<
          ::ash::assistant::mojom::AssistantAudioDecoderClient> client,
      ::mojo::PendingRemote<::ash::assistant::mojom::AssistantMediaDataSource>
          data_source) override {}
};

class AssistantAudioDeviceOwnerTest : public testing::Test {
 public:
  AssistantAudioDeviceOwnerTest()
      : task_env_(
            base::test::TaskEnvironment::MainThreadType::DEFAULT,
            base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED) {}

  AssistantAudioDeviceOwnerTest(const AssistantAudioDeviceOwnerTest&) = delete;
  AssistantAudioDeviceOwnerTest& operator=(
      const AssistantAudioDeviceOwnerTest&) = delete;

  ~AssistantAudioDeviceOwnerTest() override { task_env_.RunUntilIdle(); }

 private:
  base::test::TaskEnvironment task_env_;
};

TEST(AudioOutputProviderImplTest, StartDecoderServiceWithBindCall) {
  ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitAndDisableFeature(
      features::kStartAssistantAudioDecoderOnDemand);

  SingleThreadTaskEnvironment task_environment;

  auto provider = std::make_unique<AudioOutputProviderImpl>(kFakeDeviceId);

  FakePlatformDelegate platform_delegate;
  mojo::PendingRemote<mojom::AudioOutputDelegate> audio_output_delegate;
  { auto unused = audio_output_delegate.InitWithNewPipeAndPassReceiver(); }
  provider->Bind(std::move(audio_output_delegate), &platform_delegate);

  provider->BindAudioDecoderFactory();

  mojo::PendingReceiver<AssistantAudioDecoderFactory>
      audio_decoder_factory_pending_receiver =
          platform_delegate.audio_decoder_factory_receiver();
  FakeAssistantAudioDecoderFactory fake_assistant_audio_decoder_factory;
  mojo::Receiver<AssistantAudioDecoderFactory>
      assistant_audio_decoder_factory_receiver(
          &fake_assistant_audio_decoder_factory,
          std::move(audio_decoder_factory_pending_receiver));
  // If the flag is off, we expect that AudioDecoderFactory will be bound after
  // BindAudioDecoderFactory call.
  EXPECT_TRUE(assistant_audio_decoder_factory_receiver.is_bound());

  bool disconnected = false;
  assistant_audio_decoder_factory_receiver.set_disconnect_handler(
      base::BindLambdaForTesting([&]() { disconnected = true; }));

  provider->UnBindAudioDecoderFactory();
  task_environment.RunUntilIdle();

  // Confirm that it's disconnected after UnBindAudioDecoderFactory call.
  EXPECT_TRUE(disconnected);
}

TEST(AudioOutputProviderImplTest, StartDecoderServiceOnDemand) {
  ASSERT_TRUE(features::IsStartAssistantAudioDecoderOnDemandEnabled());
  SingleThreadTaskEnvironment task_environment;

  auto provider = std::make_unique<AudioOutputProviderImpl>(kFakeDeviceId);

  FakePlatformDelegate platform_delegate;
  mojo::PendingRemote<mojom::AudioOutputDelegate> audio_output_delegate;
  { auto unused = audio_output_delegate.InitWithNewPipeAndPassReceiver(); }
  provider->Bind(std::move(audio_output_delegate), &platform_delegate);

  provider->BindAudioDecoderFactory();
  // If the flag is on, AudioDecoderFactory should not be bound with
  // BindAudioDecoderFactory, i.e. It should not be valid.
  EXPECT_FALSE(platform_delegate.audio_decoder_factory_receiver().is_valid());

  // Set encoding format to MP3 as we use AudioDecoder only if it's in encoded
  // format.
  OutputStreamMetadata metadata = {
      .buffer_stream_format = {
          .encoding = assistant_client::OutputStreamEncoding::STREAM_MP3,
      }};

  std::unique_ptr<assistant_client::AudioOutput> first_output(
      provider->CreateAudioOutput(metadata));
  FakeAudioOutputDelegate first_fake_audio_output_delegate;
  first_output->Start(&first_fake_audio_output_delegate);
  task_environment.RunUntilIdle();

  FakeAssistantAudioDecoderFactory fake_assistant_audio_decoder_factory;
  auto receiver =
      std::make_unique<mojo::Receiver<AssistantAudioDecoderFactory>>(
          &fake_assistant_audio_decoder_factory,
          platform_delegate.audio_decoder_factory_receiver());

  // Confirm that AudioDecoderFactory is now bound after Start call.
  EXPECT_TRUE(receiver->is_bound());

  // Create/Start another output as |second_output|.
  std::unique_ptr<assistant_client::AudioOutput> second_output(
      provider->CreateAudioOutput(metadata));
  FakeAudioOutputDelegate second_fake_audio_output_delegate;
  second_output->Start(&second_fake_audio_output_delegate);
  task_environment.RunUntilIdle();

  bool disconnected = false;
  receiver->set_disconnect_handler(
      base::BindLambdaForTesting([&]() { disconnected = true; }));

  // Delete the first output and confirm that the connection is not disconnected
  // as we still have the second output.
  first_output.reset();
  task_environment.RunUntilIdle();
  EXPECT_TRUE(receiver->is_bound());
  EXPECT_FALSE(disconnected);

  second_output.reset();
  task_environment.RunUntilIdle();
  // Confirm that AudioDecoderFactory is disconnected once all outputs got
  // deleted.
  EXPECT_TRUE(disconnected);

  // Create another one and confirm that it still works correctly.
  std::unique_ptr<assistant_client::AudioOutput> third_output(
      provider->CreateAudioOutput(metadata));
  FakeAudioOutputDelegate third_fake_audio_output_delegate;
  third_output->Start(&third_fake_audio_output_delegate);
  task_environment.RunUntilIdle();

  receiver = std::make_unique<mojo::Receiver<AssistantAudioDecoderFactory>>(
      &fake_assistant_audio_decoder_factory,
      platform_delegate.audio_decoder_factory_receiver());
  EXPECT_TRUE(receiver->is_bound());
  disconnected = false;
  receiver->set_disconnect_handler(
      base::BindLambdaForTesting([&]() { disconnected = true; }));

  third_output.reset();
  task_environment.RunUntilIdle();
  EXPECT_TRUE(disconnected);

  provider->UnBindAudioDecoderFactory();
}

// We do not use AssistantAudioDecoder if audio format is in raw format.
TEST(AudioOutputProviderImplTest, DoNotStartAudioServiceForRawFormat) {
  ScopedFeatureList scoped_feature_list(
      features::kStartAssistantAudioDecoderOnDemand);
  SingleThreadTaskEnvironment task_environment;
  FakeAssistantAudioDecoderFactory fake_assistant_audio_decoder_factory;

  auto provider = std::make_unique<AudioOutputProviderImpl>(kFakeDeviceId);

  FakePlatformDelegate platform_delegate;
  mojo::PendingRemote<mojom::AudioOutputDelegate> audio_output_delegate;
  { auto unused = audio_output_delegate.InitWithNewPipeAndPassReceiver(); }
  provider->Bind(std::move(audio_output_delegate), &platform_delegate);

  provider->BindAudioDecoderFactory();
  EXPECT_FALSE(platform_delegate.audio_decoder_factory_receiver().is_valid());

  OutputStreamMetadata metadata = {
      .buffer_stream_format = {
          .encoding = assistant_client::OutputStreamEncoding::STREAM_PCM_S16,
          .pcm_sample_rate = 44800,
          .pcm_num_channels = 2,
      }};

  std::unique_ptr<assistant_client::AudioOutput> output(
      provider->CreateAudioOutput(metadata));
  FakeAudioOutputDelegate fake_audio_output_delegate;
  output->Start(&fake_audio_output_delegate);
  fake_audio_output_delegate.Reset();
  fake_audio_output_delegate.Wait();
  task_environment.RunUntilIdle();

  // Confirm that AudioDecoderFactory is not bound even after Start call if it's
  // in raw format.
  EXPECT_FALSE(platform_delegate.audio_decoder_factory_receiver().is_valid());

  output.reset();
  task_environment.RunUntilIdle();

  provider->UnBindAudioDecoderFactory();
  task_environment.RunUntilIdle();
}

// TODO(b/234874756): Move AssistantAudioDeviceOwner test under
// audio_device_owner_unittest.cc
TEST_F(AssistantAudioDeviceOwnerTest, BufferFilling) {
  FakeAudioOutputDelegateMojom audio_output_delegate_mojom;
  FakeAudioOutputDelegate audio_output_delegate;
  auto audio_bus = media::AudioBus::Create(2, 4480);
  assistant_client::OutputStreamFormat format{
      assistant_client::OutputStreamEncoding::STREAM_PCM_S16,
      44800,  // pcm_sample rate.
      2       // pcm_num_channels,
  };

  audio_output_delegate.set_num_of_bytes_to_fill(200);
  audio_output_delegate.Reset();

  auto owner = std::make_unique<AudioDeviceOwner>(kFakeDeviceId);
  // Upon start, it will start to fill the buffer. The fill should stop after
  // Wait().
  owner->Start(&audio_output_delegate_mojom, &audio_output_delegate,
               mojo::NullRemote(), format);
  audio_output_delegate.Wait();

  audio_output_delegate.Reset();
  audio_bus->Zero();
  // On first render, it will push the data to |audio_bus|.
  owner->Render(base::Microseconds(0), base::TimeTicks::Now(), {},
                audio_bus.get());
  audio_output_delegate.Wait();
  EXPECT_FALSE(audio_bus->AreFramesZero());
  EXPECT_FALSE(audio_output_delegate.end_of_stream());

  // The subsequent Render call will detect no data available and notify
  // delegate for OnEndOfStream().
  owner->Render(base::Microseconds(0), base::TimeTicks::Now(), {},
                audio_bus.get());
  EXPECT_TRUE(audio_output_delegate.end_of_stream());
}

}  // namespace ash::libassistant