chromium/chromecast/starboard/media/media/starboard_audio_decoder_test.cc

// Copyright 2024 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/starboard/media/media/starboard_audio_decoder.h"

#include <cstdint>
#include <cstring>
#include <memory>
#include <vector>

#include "base/logging.h"
#include "base/test/task_environment.h"
#include "chromecast/media/base/cast_decoder_buffer_impl.h"
#include "chromecast/media/cma/base/decoder_buffer_adapter.h"
#include "chromecast/public/graphics_types.h"
#include "chromecast/public/media/media_pipeline_backend.h"
#include "chromecast/starboard/media/cdm/starboard_drm_key_tracker.h"
#include "chromecast/starboard/media/media/starboard_api_wrapper.h"
#include "media/base/decoder_buffer.h"
#include "media/base/decrypt_config.h"
#include "media/base/encryption_scheme.h"
#include "media/base/subsample_entry.h"
#include "mock_starboard_api_wrapper.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace chromecast {
namespace media {
namespace {

using ::testing::AllOf;
using ::testing::DoubleEq;
using ::testing::ElementsAre;
using ::testing::ElementsAreArray;
using ::testing::Eq;
using ::testing::ExplainMatchResult;
using ::testing::Field;
using ::testing::FloatEq;
using ::testing::MockFunction;
using ::testing::Optional;
using ::testing::Pointee;
using ::testing::Pointwise;
using ::testing::StrEq;
using ::testing::WithArg;

// Converts between AudioCodec and StarboardAudioCodec.
StarboardAudioCodec ToStarboardAudioCodec(AudioCodec codec) {
  switch (codec) {
    case kCodecAAC:
      return kStarboardAudioCodecAac;
    case kCodecMP3:
      return kStarboardAudioCodecMp3;
    case kCodecPCM:
      return kStarboardAudioCodecPcm;
    case kCodecVorbis:
      return kStarboardAudioCodecVorbis;
    case kCodecOpus:
      return kStarboardAudioCodecOpus;
    case kCodecEAC3:
      return kStarboardAudioCodecEac3;
    case kCodecAC3:
      return kStarboardAudioCodecAc3;
    case kCodecFLAC:
      return kStarboardAudioCodecFlac;
    // The rest of these codecs are currently unsupported by starboard.
    case kCodecDTS:
    case kCodecPCM_S16BE:
    case kCodecMpegHAudio:
    default:
      return kStarboardAudioCodecNone;
  }
}

// Returns the number of bits per sample per channel for the given sample
// format.
uint16_t GetBitsPerSample(SampleFormat sample_format) {
  switch (sample_format) {
    case kSampleFormatU8:
      return 8;
    case kSampleFormatS16:
    case kSampleFormatPlanarS16:
      return 16;
    case kSampleFormatS24:
      return 24;
    case kSampleFormatS32:
    case kSampleFormatF32:
    case kSampleFormatPlanarF32:
    case kSampleFormatPlanarS32:
      return 32;
    default:
      return 0;
  }
}

// Compares a StarboardSampleInfo (arg) to an AudioConfig (config) and a
// scoped_refptr<CastDecoderBuffer> (buffer).
MATCHER_P2(MatchesAudioConfigAndBuffer, config, buffer, "") {
  CHECK(buffer) << "Passed a null buffer to the matcher.";

  if (arg.type != kStarboardMediaTypeAudio) {
    *result_listener << "the StarboardSampleInfo's type is not audio";
    return false;
  }

  // Check that the buffer's data matches.
  if (!ExplainMatchResult(
          ElementsAreArray(buffer->data(), buffer->data_size()),
          std::tuple<const uint8_t*, size_t>(
              static_cast<const uint8_t*>(arg.buffer), arg.buffer_size),
          result_listener)) {
    *result_listener << " the expected audio data";
    return false;
  }

  // Check the rest of the fields.
  return ExplainMatchResult(Eq(buffer->timestamp()), arg.timestamp,
                            result_listener) &&
         ExplainMatchResult(
             AllOf(Field(&StarboardAudioSampleInfo::codec,
                         ToStarboardAudioCodec(config.codec)),
                   Field(&StarboardAudioSampleInfo::number_of_channels,
                         config.channel_number),
                   Field(&StarboardAudioSampleInfo::samples_per_second,
                         config.samples_per_second),
                   Field(&StarboardAudioSampleInfo::bits_per_sample,
                         GetBitsPerSample(config.sample_format))),
             arg.audio_sample_info, result_listener);
  return true;
}

// Compares a StarboardSampleInfo (arg) to a
// scoped_refptr<CastDecoderBuffer> (buffer).
MATCHER_P(MatchesAudioBufferPCM, buffer, "") {
  CHECK(buffer) << "Passed a null buffer to the matcher.";
  if (arg.type != kStarboardMediaTypeAudio) {
    *result_listener << "the StarboardSampleInfo's type is not audio";
    return false;
  }

  CHECK_EQ(static_cast<int>(buffer->data_size()) % 4, 0);
  CHECK_EQ(arg.buffer_size % 4, 0);

  const std::vector<float> expected_buffer(
      reinterpret_cast<const float*>(buffer->data()),
      reinterpret_cast<const float*>(buffer->data()) + buffer->data_size() / 4);

  if (!ExplainMatchResult(
          Pointwise(FloatEq(), expected_buffer),
          std::tuple<const float*, size_t>(
              reinterpret_cast<const float*>(arg.buffer), arg.buffer_size / 4),
          result_listener)) {
    *result_listener << "buffer data mismatch";
    return false;
  }
  return true;
}

// A mock delegate that can be passed to the decoder.
class MockDelegate : public MediaPipelineBackend::Decoder::Delegate {
 public:
  MockDelegate() = default;
  ~MockDelegate() override = default;

  MOCK_METHOD(void, OnPushBufferComplete, (BufferStatus status), (override));
  MOCK_METHOD(void, OnEndOfStream, (), (override));
  MOCK_METHOD(void, OnDecoderError, (), (override));
  MOCK_METHOD(void,
              OnKeyStatusChanged,
              (const std::string& key_id,
               CastKeyStatus key_status,
               uint32_t system_code),
              (override));
  MOCK_METHOD(void, OnVideoResolutionChanged, (const Size& size), (override));
};

// A test fixture is used to manage the global mock state and to handle the
// lifetime of the SingleThreadTaskEnvironment.
class StarboardAudioDecoderTest : public ::testing::Test {
 protected:
  StarboardAudioDecoderTest()
      : starboard_(std::make_unique<MockStarboardApiWrapper>()) {
    // Ensure that tests begin with a clean slate regarding DRM keys.
    StarboardDrmKeyTracker::GetInstance().ClearStateForTesting();
  }

  ~StarboardAudioDecoderTest() override = default;

  // This should be destructed last.
  base::test::SingleThreadTaskEnvironment task_environment_;
  // This will be passed to the MediaPipelineBackendStarboard, and all calls to
  // Starboard will go through it. Thus, we can mock out those calls.
  std::unique_ptr<MockStarboardApiWrapper> starboard_;
  // Since SbPlayer is just an opaque blob to the MPB, we will simply use an int
  // to represent it.
  int fake_player_ = 1;
};

// Returns a simple AudioConfig.
AudioConfig GetBasicConfig() {
  AudioConfig config;

  config.codec = AudioCodec::kCodecMP3;
  config.channel_layout = ChannelLayout::STEREO;
  config.sample_format = SampleFormat::kSampleFormatF32;
  config.bytes_per_channel = 4;
  config.channel_number = 2;
  config.samples_per_second = 44100;
  config.encryption_scheme = EncryptionScheme::kUnencrypted;

  return config;
}

TEST_F(StarboardAudioDecoderTest, PushesBufferToStarboard) {
  const AudioConfig config = GetBasicConfig();
  const std::vector<uint8_t> buffer_data = {1, 2, 3, 4, 5};
  scoped_refptr<CastDecoderBufferImpl> buffer(
      new CastDecoderBufferImpl(buffer_data.size()));
  memcpy(buffer->writable_data(), buffer_data.data(), buffer_data.size());

  EXPECT_CALL(
      *starboard_,
      WriteSample(&fake_player_, kStarboardMediaTypeAudio,
                  Pointee(MatchesAudioConfigAndBuffer(config, buffer)), 1))
      .Times(1);

  StarboardAudioDecoder decoder(starboard_.get());
  MockDelegate delegate;

  decoder.Initialize(&fake_player_);
  decoder.SetConfig(config);
  decoder.SetDelegate(&delegate);

  EXPECT_EQ(decoder.PushBuffer(buffer.get()),
            MediaPipelineBackend::BufferStatus::kBufferPending);
}

TEST_F(StarboardAudioDecoderTest, WritesEndOfStreamToStarboard) {
  EXPECT_CALL(*starboard_,
              WriteEndOfStream(&fake_player_, kStarboardMediaTypeAudio))
      .Times(1);

  StarboardAudioDecoder decoder(starboard_.get());
  MockDelegate delegate;

  const AudioConfig config = GetBasicConfig();

  decoder.Initialize(&fake_player_);
  decoder.SetConfig(config);
  decoder.SetDelegate(&delegate);

  EXPECT_EQ(decoder.PushBuffer(CastDecoderBufferImpl::CreateEOSBuffer().get()),
            MediaPipelineBackend::BufferStatus::kBufferSuccess);
}

TEST_F(StarboardAudioDecoderTest, ForwardsSetVolumeCallToStarboard) {
  constexpr float kVolume = 0.77;

  EXPECT_CALL(*starboard_, SetVolume(&fake_player_, DoubleEq(kVolume)))
      .Times(1);

  StarboardAudioDecoder decoder(starboard_.get());
  MockDelegate delegate;

  const AudioConfig config = GetBasicConfig();

  decoder.Initialize(&fake_player_);
  decoder.SetConfig(config);
  decoder.SetDelegate(&delegate);

  EXPECT_TRUE(decoder.SetVolume(kVolume));
}

TEST_F(StarboardAudioDecoderTest, PopulatesDrmInfoInSamples) {
  // The length should be at most 16 bytes.
  constexpr char kKeyId[] = "key_id";
  // This must be 16 bytes.
  constexpr char kIv[] = "abcdefghijklmnop";
  // This must contain at least one subsample.
  const std::vector<::media::SubsampleEntry> subsamples = {
      ::media::SubsampleEntry(/*clear_bytes=*/1, /*cypher_bytes=*/2),
      ::media::SubsampleEntry(/*clear_bytes=*/3, /*cypher_bytes=*/4),
  };

  // If we do not add this key, buffers will not be pushed to starboard.
  StarboardDrmKeyTracker::GetInstance().AddKey(kKeyId, "session_id");

  AudioConfig config = GetBasicConfig();
  // Match the behavior of AudioPipelineImpl::Initialize by setting this to
  // unencrypted even for encrypted content.
  config.encryption_scheme = EncryptionScheme::kUnencrypted;

  const ::media::EncryptionPattern encryption_pattern(5, 6);
  std::unique_ptr<::media::DecryptConfig> decrypt_config =
      ::media::DecryptConfig::CreateCbcsConfig(kKeyId, kIv, subsamples,
                                               encryption_pattern);
  CHECK(decrypt_config);

  const std::vector<uint8_t> buffer_data = {1, 2, 3, 4, 5};
  scoped_refptr<::media::DecoderBuffer> decoder_buffer =
      ::media::DecoderBuffer::CopyFrom(buffer_data);
  CHECK(decoder_buffer);
  decoder_buffer->set_decrypt_config(std::move(decrypt_config));

  scoped_refptr<DecoderBufferAdapter> buffer =
      new DecoderBufferAdapter(decoder_buffer);

  StarboardDrmSampleInfo actual_drm_info = {};
  // The actual subsamples may be deleted after the call to
  // SbPlayerWriteSample2, so we need to store a copy.
  std::vector<StarboardDrmSubSampleMapping> actual_subsamples;

  EXPECT_CALL(
      *starboard_,
      WriteSample(&fake_player_, kStarboardMediaTypeAudio,
                  Pointee(AllOf(MatchesAudioConfigAndBuffer(config, buffer))),
                  1))
      .WillOnce(WithArg<2>([&actual_drm_info, &actual_subsamples](
                               StarboardSampleInfo* sample_infos) {
        // Since this is only called when the fourth argument is 1, that
        // means that sample_infos_count is 1.
        StarboardSampleInfo sample_info = sample_infos[0];
        if (!sample_info.drm_info) {
          return;
        }
        actual_drm_info = *sample_info.drm_info;
        const int subsample_count = actual_drm_info.subsample_count;
        if (subsample_count > 0) {
          actual_subsamples.assign(
              actual_drm_info.subsample_mapping,
              actual_drm_info.subsample_mapping + subsample_count);
        }
      }));

  StarboardAudioDecoder decoder(starboard_.get());
  MockDelegate delegate;

  decoder.Initialize(&fake_player_);
  decoder.SetConfig(config);
  decoder.SetDelegate(&delegate);

  EXPECT_EQ(decoder.PushBuffer(buffer.get()),
            MediaPipelineBackend::BufferStatus::kBufferPending);

  EXPECT_EQ(actual_drm_info.encryption_scheme,
            kStarboardDrmEncryptionSchemeAesCbc);
  EXPECT_EQ(actual_drm_info.encryption_pattern.crypt_byte_block,
            encryption_pattern.crypt_byte_block());
  EXPECT_EQ(actual_drm_info.encryption_pattern.skip_byte_block,
            encryption_pattern.skip_byte_block());
  EXPECT_THAT(std::string(reinterpret_cast<const char*>(
                              actual_drm_info.initialization_vector),
                          actual_drm_info.initialization_vector_size),
              StrEq(kIv));
  EXPECT_THAT(
      std::string(reinterpret_cast<const char*>(actual_drm_info.identifier),
                  actual_drm_info.identifier_size),
      StrEq(kKeyId));
  EXPECT_THAT(
      actual_subsamples,
      ElementsAre(
          AllOf(Field(&StarboardDrmSubSampleMapping::clear_byte_count, 1),
                Field(&StarboardDrmSubSampleMapping::encrypted_byte_count, 2)),
          AllOf(
              Field(&StarboardDrmSubSampleMapping::clear_byte_count, 3),
              Field(&StarboardDrmSubSampleMapping::encrypted_byte_count, 4))));
}

TEST_F(StarboardAudioDecoderTest, DoesNotPushToStarboardIfDrmKeyIsUnavailable) {
  // The length should be at most 16 bytes.
  constexpr char kKeyId[] = "key_id";
  // This must be 16 bytes.
  constexpr char kIv[] = "abcdefghijklmnop";
  // This must contain at least one subsample.
  const std::vector<::media::SubsampleEntry> subsamples = {
      ::media::SubsampleEntry(/*clear_bytes=*/1, /*cypher_bytes=*/2),
      ::media::SubsampleEntry(/*clear_bytes=*/3, /*cypher_bytes=*/4),
  };

  // Since we do not add kKeyId to StarboardDrmKeyTracker, no buffer with this
  // key ID should be pushed to starboard.

  AudioConfig config = GetBasicConfig();
  config.encryption_scheme = EncryptionScheme::kAesCtr;

  const ::media::EncryptionPattern encryption_pattern(5, 6);
  std::unique_ptr<::media::DecryptConfig> decrypt_config =
      ::media::DecryptConfig::CreateCbcsConfig(kKeyId, kIv, subsamples,
                                               encryption_pattern);
  CHECK(decrypt_config);

  const std::vector<uint8_t> buffer_data = {1, 2, 3, 4, 5};
  scoped_refptr<::media::DecoderBuffer> decoder_buffer =
      ::media::DecoderBuffer::CopyFrom(buffer_data);
  CHECK(decoder_buffer);
  decoder_buffer->set_decrypt_config(std::move(decrypt_config));

  scoped_refptr<DecoderBufferAdapter> buffer =
      new DecoderBufferAdapter(decoder_buffer);

  EXPECT_CALL(*starboard_, WriteSample).Times(0);

  StarboardAudioDecoder decoder(starboard_.get());
  MockDelegate delegate;

  decoder.Initialize(&fake_player_);
  decoder.SetConfig(config);
  decoder.SetDelegate(&delegate);

  EXPECT_EQ(decoder.PushBuffer(buffer.get()),
            MediaPipelineBackend::BufferStatus::kBufferPending);
}

TEST_F(StarboardAudioDecoderTest,
       PushesBufferToStarboardAfterDrmKeyIsAvailable) {
  // The length should be at most 16 bytes.
  constexpr char kKeyId[] = "key_id";
  // This must be 16 bytes.
  constexpr char kIv[] = "abcdefghijklmnop";
  // This must contain at least one subsample.
  const std::vector<::media::SubsampleEntry> subsamples = {
      ::media::SubsampleEntry(/*clear_bytes=*/1, /*cypher_bytes=*/2),
      ::media::SubsampleEntry(/*clear_bytes=*/3, /*cypher_bytes=*/4),
  };

  AudioConfig config = GetBasicConfig();
  config.encryption_scheme = EncryptionScheme::kAesCbc;

  const ::media::EncryptionPattern encryption_pattern(5, 6);
  std::unique_ptr<::media::DecryptConfig> decrypt_config =
      ::media::DecryptConfig::CreateCbcsConfig(kKeyId, kIv, subsamples,
                                               encryption_pattern);
  CHECK(decrypt_config);

  const std::vector<uint8_t> buffer_data = {1, 2, 3, 4, 5};
  scoped_refptr<::media::DecoderBuffer> decoder_buffer =
      ::media::DecoderBuffer::CopyFrom(buffer_data);
  CHECK(decoder_buffer);
  decoder_buffer->set_decrypt_config(std::move(decrypt_config));

  scoped_refptr<DecoderBufferAdapter> buffer =
      new DecoderBufferAdapter(decoder_buffer);

  StarboardDrmSampleInfo actual_drm_info = {};
  // The actual subsamples may be deleted after the call to
  // SbPlayerWriteSample2, so we need to store a copy.
  std::vector<StarboardDrmSubSampleMapping> actual_subsamples;

  EXPECT_CALL(
      *starboard_,
      WriteSample(&fake_player_, kStarboardMediaTypeAudio,
                  Pointee(AllOf(MatchesAudioConfigAndBuffer(config, buffer))),
                  1))
      .WillOnce(WithArg<2>([&actual_drm_info, &actual_subsamples](
                               StarboardSampleInfo* sample_infos) {
        // Since this is only called when the fourth argument is 1, that
        // means that sample_infos_count is 1.
        StarboardSampleInfo sample_info = sample_infos[0];
        if (!sample_info.drm_info) {
          return;
        }
        actual_drm_info = *sample_info.drm_info;
        const int subsample_count = actual_drm_info.subsample_count;
        if (subsample_count > 0) {
          actual_subsamples.assign(
              actual_drm_info.subsample_mapping,
              actual_drm_info.subsample_mapping + subsample_count);
        }
      }));

  StarboardAudioDecoder decoder(starboard_.get());
  MockDelegate delegate;

  decoder.Initialize(&fake_player_);
  decoder.SetConfig(config);
  decoder.SetDelegate(&delegate);

  EXPECT_EQ(decoder.PushBuffer(buffer.get()),
            MediaPipelineBackend::BufferStatus::kBufferPending);

  // Now that the key is available, the buffer should be pushed to starboard.
  StarboardDrmKeyTracker::GetInstance().AddKey(kKeyId, "session_id");
  // The callback provided by the decoder will post a task; run until the
  // callback runs.
  task_environment_.RunUntilIdle();

  EXPECT_EQ(actual_drm_info.encryption_scheme,
            kStarboardDrmEncryptionSchemeAesCbc);
  EXPECT_EQ(actual_drm_info.encryption_pattern.crypt_byte_block,
            encryption_pattern.crypt_byte_block());
  EXPECT_EQ(actual_drm_info.encryption_pattern.skip_byte_block,
            encryption_pattern.skip_byte_block());
  EXPECT_THAT(std::string(reinterpret_cast<const char*>(
                              actual_drm_info.initialization_vector),
                          actual_drm_info.initialization_vector_size),
              StrEq(kIv));
  EXPECT_THAT(
      std::string(reinterpret_cast<const char*>(actual_drm_info.identifier),
                  actual_drm_info.identifier_size),
      StrEq(kKeyId));
  EXPECT_THAT(
      actual_subsamples,
      ElementsAre(
          AllOf(Field(&StarboardDrmSubSampleMapping::clear_byte_count, 1),
                Field(&StarboardDrmSubSampleMapping::encrypted_byte_count, 2)),
          AllOf(
              Field(&StarboardDrmSubSampleMapping::clear_byte_count, 3),
              Field(&StarboardDrmSubSampleMapping::encrypted_byte_count, 4))));
}

TEST_F(StarboardAudioDecoderTest, ReturnsNulloptBeforeConfigSet) {
  StarboardAudioDecoder decoder(starboard_.get());

  EXPECT_EQ(decoder.GetAudioSampleInfo(), std::nullopt);
}

TEST_F(StarboardAudioDecoderTest,
       ReturnsPopulatedSampleInfoAfterConfigHasBeenSet) {
  StarboardAudioDecoder decoder(starboard_.get());

  AudioConfig config;
  config.codec = AudioCodec::kCodecAAC;
  config.channel_layout = ChannelLayout::SURROUND_5_1;
  config.sample_format = SampleFormat::kSampleFormatF32;
  config.bytes_per_channel = 4;
  config.channel_number = 6;
  config.samples_per_second = 48000;
  config.encryption_scheme = EncryptionScheme::kUnencrypted;

  EXPECT_TRUE(decoder.SetConfig(config));
  EXPECT_THAT(decoder.GetAudioSampleInfo(),
              Optional(Field(&StarboardAudioSampleInfo::codec,
                             kStarboardAudioCodecAac)));
}

TEST_F(StarboardAudioDecoderTest,
       HandlesMultiplePushBuffersBeforeInitialization) {
  const std::vector<uint8_t> buffer_data_1 = {1, 2, 3, 4, 5};
  scoped_refptr<CastDecoderBufferImpl> buffer_1(
      new CastDecoderBufferImpl(buffer_data_1.size()));
  memcpy(buffer_1->writable_data(), buffer_data_1.data(), buffer_data_1.size());

  const std::vector<uint8_t> buffer_data_2 = {6, 7, 8, 9, 10};
  scoped_refptr<CastDecoderBufferImpl> buffer_2(
      new CastDecoderBufferImpl(buffer_data_2.size()));
  memcpy(buffer_2->writable_data(), buffer_data_2.data(), buffer_data_2.size());

  const AudioConfig config = GetBasicConfig();

  // Only the last buffer -- buffer_2 -- should be sent to starboard once the
  // decoder is initialized.
  EXPECT_CALL(
      *starboard_,
      WriteSample(&fake_player_, kStarboardMediaTypeAudio,
                  Pointee(MatchesAudioConfigAndBuffer(config, buffer_1)), 1))
      .Times(0);
  EXPECT_CALL(
      *starboard_,
      WriteSample(&fake_player_, kStarboardMediaTypeAudio,
                  Pointee(MatchesAudioConfigAndBuffer(config, buffer_2)), 1))
      .Times(1);

  StarboardAudioDecoder decoder(starboard_.get());
  decoder.SetConfig(config);
  EXPECT_EQ(decoder.PushBuffer(buffer_1.get()),
            MediaPipelineBackend::BufferStatus::kBufferPending);
  EXPECT_EQ(decoder.PushBuffer(buffer_2.get()),
            MediaPipelineBackend::BufferStatus::kBufferPending);

  MockDelegate delegate;
  decoder.SetDelegate(&delegate);

  // At this point, the pending buffer (buffer_2) should be pushed.
  decoder.Initialize(&fake_player_);
}

TEST_F(StarboardAudioDecoderTest, DoesNotCallDelegateEoSWhenPushingEoSBuffer) {
  const AudioConfig config = GetBasicConfig();

  StarboardAudioDecoder decoder(starboard_.get());

  decoder.Initialize(&fake_player_);
  decoder.SetConfig(config);
  MockDelegate delegate;
  // This should not be called, since we never call
  // decoder.OnSbPlayerEndOfStream() in this test.
  EXPECT_CALL(delegate, OnEndOfStream).Times(0);
  decoder.SetDelegate(&delegate);

  EXPECT_EQ(decoder.PushBuffer(CastDecoderBufferImpl::CreateEOSBuffer().get()),
            MediaPipelineBackend::BufferStatus::kBufferSuccess);
}

TEST_F(StarboardAudioDecoderTest, CallsDelegateEoSWhenSbPlayerStreamEnds) {
  const AudioConfig config = GetBasicConfig();

  StarboardAudioDecoder decoder(starboard_.get());

  decoder.Initialize(&fake_player_);
  decoder.SetConfig(config);
  MockDelegate delegate;
  EXPECT_CALL(delegate, OnEndOfStream).Times(1);
  decoder.SetDelegate(&delegate);
  decoder.OnSbPlayerEndOfStream();
}

TEST_F(StarboardAudioDecoderTest, ReportsStatistics) {
  StarboardAudioDecoder decoder(starboard_.get());
  MockDelegate delegate;

  const AudioConfig config = GetBasicConfig();
  decoder.Initialize(&fake_player_);
  decoder.SetConfig(config);
  decoder.SetDelegate(&delegate);

  const std::vector<uint8_t> buffer_data = {1, 2, 3, 4, 5};
  scoped_refptr<CastDecoderBufferImpl> buffer(
      new CastDecoderBufferImpl(buffer_data.size()));
  memcpy(buffer->writable_data(), buffer_data.data(), buffer_data.size());

  EXPECT_EQ(decoder.PushBuffer(buffer.get()),
            MediaPipelineBackend::BufferStatus::kBufferPending);

  MediaPipelineBackend::AudioDecoder::Statistics stats = {};
  decoder.GetStatistics(&stats);
  EXPECT_EQ(stats.decoded_bytes, buffer_data.size());
}

}  // namespace
}  // namespace media
}  // namespace chromecast