chromium/chromecast/starboard/media/media/starboard_video_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_video_decoder.h"

#include <optional>

#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::MockFunction;
using ::testing::NotNull;
using ::testing::Optional;
using ::testing::Pointee;
using ::testing::StrEq;
using ::testing::WithArg;

// Converts between VideoCodec and StarboardVideoCodec.
StarboardVideoCodec ToStarboardVideoCodec(VideoCodec codec) {
  switch (codec) {
    case kCodecH264:
      return kStarboardVideoCodecH264;
    case kCodecVC1:
      return kStarboardVideoCodecVc1;
    case kCodecMPEG2:
      return kStarboardVideoCodecMpeg2;
    case kCodecTheora:
      return kStarboardVideoCodecTheora;
    case kCodecVP8:
      return kStarboardVideoCodecVp8;
    case kCodecVP9:
      return kStarboardVideoCodecVp9;
    case kCodecHEVC:
      return kStarboardVideoCodecH265;
    case kCodecAV1:
      return kStarboardVideoCodecAv1;
    default:
      return kStarboardVideoCodecNone;
  }
}

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

  if (arg.type != kStarboardMediaTypeVideo) {
    *result_listener << "the StarboardSampleInfo's type is not video";
    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 width/height.
  if (!ExplainMatchResult(
          AllOf(Field(&StarboardVideoSampleInfo::frame_width, config.width),
                Field(&StarboardVideoSampleInfo::frame_height, config.height)),
          arg.video_sample_info, result_listener)) {
    *result_listener << " the frame width/height";
    return false;
  }

  // Check the rest of the fields.
  return ExplainMatchResult(Eq(buffer->timestamp()), arg.timestamp,
                            result_listener) &&
         ExplainMatchResult(Eq(ToStarboardVideoCodec(config.codec)),
                            arg.video_sample_info.codec, result_listener) &&
         ExplainMatchResult(AllOf(Field(&StarboardColorMetadata::primaries,
                                        static_cast<int>(config.primaries)),
                                  Field(&StarboardColorMetadata::transfer,
                                        static_cast<int>(config.transfer)),
                                  Field(&StarboardColorMetadata::matrix,
                                        static_cast<int>(config.matrix)),
                                  Field(&StarboardColorMetadata::range,
                                        static_cast<int>(config.range))),
                            arg.video_sample_info.color_metadata,
                            result_listener);
}

// 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 StarboardVideoDecoderTest : public ::testing::Test {
 protected:
  StarboardVideoDecoderTest()
      : starboard_(std::make_unique<MockStarboardApiWrapper>()) {
    // Ensure that tests begin with a clean slate regarding DRM keys.
    StarboardDrmKeyTracker::GetInstance().ClearStateForTesting();
  }

  ~StarboardVideoDecoderTest() 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 VideoConfig.
VideoConfig GetBasicConfig() {
  VideoConfig config;

  config.codec = VideoCodec::kCodecH264;
  config.encryption_scheme = EncryptionScheme::kUnencrypted;
  config.width = 123;
  config.height = 456;

  return config;
}

TEST_F(StarboardVideoDecoderTest, PushesBufferToStarboard) {
  const VideoConfig 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_, kStarboardMediaTypeVideo,
                  Pointee(MatchesVideoConfigAndBuffer(config, buffer)), 1))
      .Times(1);

  StarboardVideoDecoder 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(StarboardVideoDecoderTest, WritesEndOfStreamToStarboard) {
  EXPECT_CALL(*starboard_,
              WriteEndOfStream(&fake_player_, kStarboardMediaTypeVideo))
      .Times(1);

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

  const VideoConfig config = GetBasicConfig();

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

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

TEST_F(StarboardVideoDecoderTest, 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");

  VideoConfig 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_, kStarboardMediaTypeVideo,
                  Pointee(AllOf(MatchesVideoConfigAndBuffer(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);
        }
      }));

  StarboardVideoDecoder 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(StarboardVideoDecoderTest, 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.

  VideoConfig 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);

  StarboardVideoDecoder 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(StarboardVideoDecoderTest,
       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),
  };

  VideoConfig 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_, kStarboardMediaTypeVideo,
                  Pointee(AllOf(MatchesVideoConfigAndBuffer(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);
        }
      }));

  StarboardVideoDecoder 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(StarboardVideoDecoderTest, ReturnsNulloptBeforeConfigSet) {
  StarboardVideoDecoder decoder(starboard_.get());

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

TEST_F(StarboardVideoDecoderTest,
       ReturnsPopulatedSampleInfoAfterConfigHasBeenSet) {
  StarboardVideoDecoder decoder(starboard_.get());

  VideoConfig config;
  config.codec = VideoCodec::kCodecVP8;
  config.encryption_scheme = EncryptionScheme::kUnencrypted;

  EXPECT_TRUE(decoder.SetConfig(config));
  EXPECT_THAT(decoder.GetVideoSampleInfo(),
              Optional(Field(&StarboardVideoSampleInfo::codec,
                             kStarboardVideoCodecVp8)));
}

TEST_F(StarboardVideoDecoderTest, PopulatesHdrInfo) {
  StarboardVideoDecoder decoder(starboard_.get());

  VideoConfig config;
  config.codec = VideoCodec::kCodecVP8;
  config.encryption_scheme = EncryptionScheme::kUnencrypted;
  config.have_hdr_metadata = true;
  config.hdr_metadata.max_content_light_level = 1;
  config.hdr_metadata.max_frame_average_light_level = 2;

  auto& color_volume_metadata = config.hdr_metadata.color_volume_metadata;
  color_volume_metadata.primary_r_chromaticity_x = 1;
  color_volume_metadata.primary_r_chromaticity_y = 2;
  color_volume_metadata.primary_g_chromaticity_x = 3;
  color_volume_metadata.primary_g_chromaticity_y = 4;
  color_volume_metadata.primary_b_chromaticity_x = 5;
  color_volume_metadata.primary_b_chromaticity_y = 6;
  color_volume_metadata.white_point_chromaticity_x = 7;
  color_volume_metadata.white_point_chromaticity_y = 8;
  color_volume_metadata.luminance_min = 9;
  color_volume_metadata.luminance_max = 10;

  EXPECT_TRUE(decoder.SetConfig(config));
  const std::optional<StarboardVideoSampleInfo> sample_info =
      decoder.GetVideoSampleInfo();

  ASSERT_TRUE(sample_info.has_value());
  EXPECT_EQ(sample_info->codec, kStarboardVideoCodecVp8);
  EXPECT_EQ(sample_info->color_metadata.max_cll, 1UL);
  EXPECT_EQ(sample_info->color_metadata.max_fall, 2UL);
  EXPECT_EQ(
      sample_info->color_metadata.mastering_metadata.primary_r_chromaticity_x,
      1);
  EXPECT_EQ(
      sample_info->color_metadata.mastering_metadata.primary_r_chromaticity_y,
      2);
  EXPECT_EQ(
      sample_info->color_metadata.mastering_metadata.primary_g_chromaticity_x,
      3);
  EXPECT_EQ(
      sample_info->color_metadata.mastering_metadata.primary_g_chromaticity_y,
      4);
  EXPECT_EQ(
      sample_info->color_metadata.mastering_metadata.primary_b_chromaticity_x,
      5);
  EXPECT_EQ(
      sample_info->color_metadata.mastering_metadata.primary_b_chromaticity_y,
      6);
  EXPECT_EQ(
      sample_info->color_metadata.mastering_metadata.white_point_chromaticity_x,
      7);
  EXPECT_EQ(
      sample_info->color_metadata.mastering_metadata.white_point_chromaticity_y,
      8);
  EXPECT_EQ(sample_info->color_metadata.mastering_metadata.luminance_min, 9);
  EXPECT_EQ(sample_info->color_metadata.mastering_metadata.luminance_max, 10);
}

TEST_F(StarboardVideoDecoderTest,
       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 VideoConfig config = GetBasicConfig();

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

  StarboardVideoDecoder 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(StarboardVideoDecoderTest, DoesNotCallDelegateEoSWhenPushingEoSBuffer) {
  const VideoConfig config = GetBasicConfig();

  StarboardVideoDecoder 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(StarboardVideoDecoderTest, CallsDelegateEoSWhenSbPlayerStreamEnds) {
  const VideoConfig config = GetBasicConfig();

  StarboardVideoDecoder 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(StarboardVideoDecoderTest, ReportsStatistics) {
  constexpr int kTotalVideoFrames = 3;
  constexpr int kDroppedFrames = 1;

  EXPECT_CALL(*starboard_, GetPlayerInfo(&fake_player_, NotNull()))
      .WillOnce(WithArg<1>([](StarboardPlayerInfo* player_info) {
        *player_info = {};
        player_info->total_video_frames = kTotalVideoFrames;
        player_info->dropped_video_frames = kDroppedFrames;
      }));

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

  const VideoConfig 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::VideoDecoder::Statistics stats = {};
  decoder.GetStatistics(&stats);
  EXPECT_EQ(stats.decoded_bytes, buffer_data.size());
  EXPECT_EQ(stats.decoded_frames, static_cast<uint64_t>(kTotalVideoFrames));
  EXPECT_EQ(stats.dropped_frames, static_cast<uint64_t>(kDroppedFrames));
}

TEST_F(StarboardVideoDecoderTest,
       DoesNotInformDelegateWhenResolutionChangesBeforePushBuffer) {
  constexpr int kNewWidth = 10;
  constexpr int kNewHeight = 20;

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

  EXPECT_CALL(delegate, OnVideoResolutionChanged).Times(0);

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

  // Update the config. This should NOT notify the delegate, because the
  // pipeline may not be in the kPlaying state.
  config.width = kNewWidth;
  config.height = kNewHeight;
  decoder.SetConfig(config);
}

TEST_F(StarboardVideoDecoderTest,
       InformsDelegateWhenResolutionChangesAtNextPushBuffer) {
  constexpr int kNewWidth = 10;
  constexpr int kNewHeight = 20;

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

  EXPECT_CALL(delegate, OnVideoResolutionChanged(AllOf(
                            Field(&chromecast::Size::width, Eq(kNewWidth)),
                            Field(&chromecast::Size::height, Eq(kNewHeight)))))
      .Times(1);

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

  // Update the config with the new resolution.
  config.width = kNewWidth;
  config.height = kNewHeight;
  decoder.SetConfig(config);

  scoped_refptr<CastDecoderBufferImpl> buffer(new CastDecoderBufferImpl(10));
  // Pushing the buffer should trigger the call to OnVideoResolutionChanged.
  EXPECT_EQ(decoder.PushBuffer(buffer.get()),
            MediaPipelineBackend::BufferStatus::kBufferPending);
}

TEST_F(StarboardVideoDecoderTest, PopulatesMimeTypeForHEVCDolbyVision) {
  VideoConfig config = GetBasicConfig();
  config.codec = kCodecDolbyVisionHEVC;
  config.profile = kDolbyVisionProfile5;
  config.codec_profile_level = 6;

  StarboardVideoDecoder decoder(starboard_.get());
  decoder.Initialize(&fake_player_);
  decoder.SetConfig(config);

  const std::optional<StarboardVideoSampleInfo>& video_info =
      decoder.GetVideoSampleInfo();

  ASSERT_NE(video_info, std::nullopt);
  EXPECT_THAT(video_info->mime, StrEq(R"-(video/mp4; codecs="dvhe.05.06")-"));
}

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