0

Copy starboard cast code into chromium (part 8 of 8).

Contains MediaPipelineBackendStarboard and an implementation of
CastMediaShlib that creates the backend. Also has minor fixes to
test code.

This is part of the work to get cast on starboard building out of
chromium. See go/moving-cwr-to-chromium for more information on the
high-level goal.

Bug: b:333571227
Change-Id: I7480ec9bb295e62a03df080cd525f739ea0b4fc0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5445992
Reviewed-by: Yuchen Liu <yucliu@chromium.org>
Reviewed-by: Shawn Quereshi <shawnq@google.com>
Commit-Queue: Antonio Rivera <antoniori@google.com>
Cr-Commit-Position: refs/heads/main@{#1290905}
This commit is contained in:
Antonio Rivera
2024-04-22 21:45:33 +00:00
committed by Chromium LUCI CQ
parent a97c4e80ab
commit 1f91c91610
9 changed files with 1132 additions and 25 deletions

@ -0,0 +1,16 @@
# Copyright 2024 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import("//chromecast/build/tests/cast_test.gni")
cast_test_group("starboard_media_tests") {
tests = [
"//chromecast/starboard/media/cdm:starboard_decryptor_cast_test",
"//chromecast/starboard/media/media:media_pipeline_backend_starboard_test",
"//chromecast/starboard/media/media:starboard_audio_decoder_test",
"//chromecast/starboard/media/media:starboard_resampler_test",
"//chromecast/starboard/media/media:starboard_video_decoder_test",
"//chromecast/starboard/media/media:starboard_video_plane_test",
]
}

@ -4,8 +4,6 @@
#include "chromecast/starboard/media/cdm/starboard_decryptor_cast.h"
#include <starboard_api_wrapper.h>
#include <algorithm>
#include <memory>
#include <string>
@ -14,6 +12,7 @@
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "chromecast/starboard/media/media/mock_starboard_api_wrapper.h"
#include "chromecast/starboard/media/media/starboard_api_wrapper.h"
#include "media/base/cdm_callback_promise.h"
#include "media/base/provision_fetcher.h"
#include "testing/gmock/include/gmock/gmock.h"
@ -131,7 +130,9 @@ class StarboardDecryptorCastTest : public ::testing::Test {
CdmMessageType message_type,
const std::vector<uint8_t>& message)>
session_message_cb_;
MockFunction<void(const std::string& session_id)> session_closed_cb_;
MockFunction<void(const std::string& session_id,
::media::CdmSessionClosedReason reason)>
session_closed_cb_;
MockFunction<void(const std::string& session_id,
bool has_additional_usable_key,
CdmKeysInfo keys_info)>
@ -395,7 +396,7 @@ TEST_F(StarboardDecryptorCastTest, CreatesSessionAndGeneratesLicenseRequest) {
// Trigger the session creation.
decryptor->CreateSessionAndGenerateRequest(
::media::CdmSessionType::kPersistentLicense, init_type, init_data,
::media::CdmSessionType::kTemporary, init_type, init_data,
std::make_unique<::media::CdmCallbackPromise<std::string>>(
/*resolve_cb=*/base::BindOnce(
+[](bool* b, std::string* out_session_id,
@ -486,7 +487,7 @@ TEST_F(StarboardDecryptorCastTest, CreatesSessionAndGeneratesLicenseRenewal) {
// Trigger the session creation.
decryptor->CreateSessionAndGenerateRequest(
::media::CdmSessionType::kPersistentLicense, init_type, init_data,
::media::CdmSessionType::kTemporary, init_type, init_data,
std::make_unique<::media::CdmCallbackPromise<std::string>>(
/*resolve_cb=*/base::BindOnce(
+[](bool* b, std::string* out_session_id,

@ -13,8 +13,11 @@ if (!remove_starboard_headers) {
source_set("starboard") {
sources = [
"cast_media_starboard.cc",
"drm_util.cc",
"drm_util.h",
"media_pipeline_backend_starboard.cc",
"media_pipeline_backend_starboard.h",
"starboard_audio_decoder.cc",
"starboard_audio_decoder.h",
"starboard_decoder.cc",
@ -44,10 +47,11 @@ source_set("mock_starboard_api_wrapper") {
sources = [
"mock_starboard_api_wrapper.cc",
"mock_starboard_api_wrapper.h",
"starboard_api_wrapper.cc",
"starboard_api_wrapper.h",
]
deps = [ "//testing/gmock" ]
deps = [
":starboard_api_wrapper",
"//testing/gmock",
]
}
cast_source_set("starboard_api_wrapper") {
@ -83,6 +87,19 @@ cast_source_set("starboard_api_wrapper") {
}
}
test("media_pipeline_backend_starboard_test") {
sources = [ "media_pipeline_backend_starboard_test.cc" ]
deps = [
":mock_starboard_api_wrapper",
":starboard",
"//base/test:run_all_unittests",
"//base/test:test_support",
"//chromecast/public",
"//testing/gmock",
"//testing/gtest",
]
}
test("starboard_audio_decoder_test") {
sources = [ "starboard_audio_decoder_test.cc" ]
deps = [

@ -0,0 +1,85 @@
// 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/public/cast_media_shlib.h"
#include "chromecast/public/media/media_capabilities_shlib.h"
#include "chromecast/public/video_plane.h"
#include "media_pipeline_backend_starboard.h"
#include "starboard_video_plane.h"
namespace chromecast {
namespace media {
namespace {
StarboardVideoPlane* g_video_plane = nullptr;
} // namespace
void CastMediaShlib::Initialize(const std::vector<std::string>& argv) {
CHECK(g_video_plane == nullptr);
g_video_plane = new StarboardVideoPlane();
}
void CastMediaShlib::Finalize() {
delete g_video_plane;
g_video_plane = nullptr;
}
VideoPlane* CastMediaShlib::GetVideoPlane() {
return g_video_plane;
}
MediaPipelineBackend* CastMediaShlib::CreateMediaPipelineBackend(
const MediaPipelineDeviceParams& params) {
CHECK(g_video_plane);
return new MediaPipelineBackendStarboard(g_video_plane);
}
double CastMediaShlib::GetMediaClockRate() {
return 0.0;
}
double CastMediaShlib::MediaClockRatePrecision() {
return 0.0;
}
void CastMediaShlib::MediaClockRateRange(double* minimum_rate,
double* maximum_rate) {
*minimum_rate = 0.0;
*maximum_rate = 1.0;
}
bool CastMediaShlib::SetMediaClockRate(double new_rate) {
return false;
}
bool CastMediaShlib::SupportsMediaClockRateChange() {
return false;
}
VideoPlane::Coordinates VideoPlane::GetCoordinates() {
// SbPlayerSetBounds takes coordinates in terms of the graphics resolution.
return VideoPlane::Coordinates::kGraphics;
}
bool MediaCapabilitiesShlib::IsSupportedVideoConfig(VideoCodec codec,
VideoProfile profile,
int level) {
// TODO(b/275430044): potentially call Starboard here, to determine codec
// support.
return codec == kCodecH264 || codec == kCodecVP8 || codec == kCodecVP9 ||
codec == kCodecHEVC;
}
bool MediaCapabilitiesShlib::IsSupportedAudioConfig(const AudioConfig& config) {
// TODO(b/275430044): potentially call Starboard here, to determine codec
// support.
return config.codec == kCodecAAC || config.codec == kCodecMP3 ||
config.codec == kCodecPCM || config.codec == kCodecVorbis ||
config.codec == kCodecOpus || config.codec == kCodecAC3 ||
config.codec == kCodecEAC3 || config.codec == kCodecFLAC;
}
} // namespace media
} // namespace chromecast

@ -0,0 +1,402 @@
// 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 "media_pipeline_backend_starboard.h"
#include <cast_starboard_api_adapter.h>
#include "base/logging.h"
#include "base/task/bind_post_task.h"
#include "chromecast/public/graphics_types.h"
#include "chromecast/starboard/media/media/starboard_api_wrapper.h"
namespace chromecast {
namespace media {
MediaPipelineBackendStarboard::MediaPipelineBackendStarboard(
StarboardVideoPlane* video_plane)
: starboard_(GetStarboardApiWrapper()), video_plane_(video_plane) {
DCHECK(video_plane_);
CHECK(base::SequencedTaskRunner::HasCurrentDefault());
media_task_runner_ = base::SequencedTaskRunner::GetCurrentDefault();
video_plane_callback_token_ =
video_plane_->RegisterCallback(base::BindPostTask(
media_task_runner_,
base::BindRepeating(&MediaPipelineBackendStarboard::OnGeometryChanged,
weak_factory_.GetWeakPtr())));
}
MediaPipelineBackendStarboard::~MediaPipelineBackendStarboard() {
DCHECK(media_task_runner_->RunsTasksInCurrentSequence());
video_plane_->UnregisterCallback(video_plane_callback_token_);
if (player_) {
starboard_->DestroyPlayer(player_);
}
}
MediaPipelineBackend::AudioDecoder*
MediaPipelineBackendStarboard::CreateAudioDecoder() {
DCHECK(media_task_runner_->RunsTasksInCurrentSequence());
if (audio_decoder_) {
return nullptr;
}
audio_decoder_.emplace(starboard_.get());
return &*audio_decoder_;
}
MediaPipelineBackend::VideoDecoder*
MediaPipelineBackendStarboard::CreateVideoDecoder() {
DCHECK(media_task_runner_->RunsTasksInCurrentSequence());
if (video_decoder_) {
return nullptr;
}
video_decoder_.emplace(starboard_.get());
return &*video_decoder_;
}
bool MediaPipelineBackendStarboard::Initialize() {
DCHECK(media_task_runner_->RunsTasksInCurrentSequence());
CHECK(starboard_->EnsureInitialized());
state_ = State::kInitialized;
CreatePlayer();
return true;
}
bool MediaPipelineBackendStarboard::Start(int64_t start_pts) {
DCHECK(media_task_runner_->RunsTasksInCurrentSequence());
DCHECK_EQ(state_, State::kInitialized);
state_ = State::kPlaying;
last_seek_pts_ = start_pts;
if (!player_) {
LOG(WARNING) << "Start was called before starboard initialization "
"finished. Deferring start.";
return true;
}
DoSeek();
DoPlay();
return true;
}
void MediaPipelineBackendStarboard::DoSeek() {
DCHECK(media_task_runner_->RunsTasksInCurrentSequence());
DCHECK(player_);
DCHECK_GE(last_seek_pts_, 0);
if (audio_decoder_ && audio_decoder_->IsInitialized()) {
audio_decoder_->Stop();
}
if (video_decoder_ && video_decoder_->IsInitialized()) {
video_decoder_->Stop();
}
++seek_ticket_;
starboard_->SeekTo(player_, last_seek_pts_, seek_ticket_);
}
void MediaPipelineBackendStarboard::DoPlay() {
DCHECK(media_task_runner_->RunsTasksInCurrentSequence());
DCHECK(player_);
starboard_->SetPlaybackRate(player_, playback_rate_);
}
void MediaPipelineBackendStarboard::Stop() {
DCHECK(media_task_runner_->RunsTasksInCurrentSequence());
DCHECK(state_ == State::kPlaying || state_ == State::kPaused)
<< "Cannot call MediaPipelineBackend::Stop when in state "
<< static_cast<int>(state_);
if (state_ == State::kPlaying) {
DoPause();
}
playback_rate_ = 1.0;
state_ = State::kInitialized;
}
bool MediaPipelineBackendStarboard::Pause() {
DCHECK(media_task_runner_->RunsTasksInCurrentSequence());
DCHECK_EQ(state_, State::kPlaying);
state_ = State::kPaused;
if (!player_) {
LOG(WARNING) << "Pause was called before starboard initialization "
"finished. Deferring pause.";
return true;
}
DoPause();
return true;
}
void MediaPipelineBackendStarboard::DoPause() {
DCHECK(media_task_runner_->RunsTasksInCurrentSequence());
DCHECK(player_);
StarboardPlayerInfo info = {};
starboard_->GetPlayerInfo(player_, &info);
playback_rate_ = info.playback_rate;
// Setting playback rate to 0 signifies that playback is paused.
starboard_->SetPlaybackRate(player_, 0);
}
bool MediaPipelineBackendStarboard::Resume() {
DCHECK(media_task_runner_->RunsTasksInCurrentSequence());
DCHECK_EQ(state_, State::kPaused);
state_ = State::kPlaying;
if (player_) {
DoPlay();
}
return true;
}
int64_t MediaPipelineBackendStarboard::GetCurrentPts() {
DCHECK(media_task_runner_->RunsTasksInCurrentSequence());
if (player_) {
StarboardPlayerInfo info = {};
starboard_->GetPlayerInfo(player_, &info);
return info.current_media_timestamp_micros;
} else {
return std::numeric_limits<int64_t>::min();
}
}
bool MediaPipelineBackendStarboard::SetPlaybackRate(float rate) {
DCHECK(media_task_runner_->RunsTasksInCurrentSequence());
DCHECK(state_ == State::kPlaying || state_ == State::kPaused)
<< "Cannot call MediaPipelineBackend::SetPlaybackRate when in state "
<< static_cast<int>(state_);
DCHECK(player_);
if (rate <= 0.0) {
LOG(ERROR) << "Invalid playback rate: " << rate;
return false;
}
if (starboard_->SetPlaybackRate(player_, rate)) {
// Success case.
playback_rate_ = rate;
return true;
}
LOG(ERROR) << "Failed to set the playback rate in Starboard to " << rate;
return false;
}
void MediaPipelineBackendStarboard::OnGeometryChanged(
const RectF& display_rect,
StarboardVideoPlane::Transform transform) {
DCHECK(media_task_runner_->RunsTasksInCurrentSequence());
if (!player_) {
LOG(INFO) << "Player was not created before OnGeometryChanged was called. "
"Queueing geometry change.";
pending_geometry_change_ = display_rect;
return;
}
LOG(INFO) << "Setting SbPlayer's bounds to z=0, x=" << display_rect.x
<< ", y=" << display_rect.y << ", width=" << display_rect.width
<< ", height=" << display_rect.height;
starboard_->SetPlayerBounds(
player_, /*z_index=*/0, static_cast<int>(display_rect.x),
static_cast<int>(display_rect.y), static_cast<int>(display_rect.width),
static_cast<int>(display_rect.height));
}
void MediaPipelineBackendStarboard::CreatePlayer() {
DCHECK(media_task_runner_->RunsTasksInCurrentSequence());
StarboardPlayerCreationParam params = {};
if (audio_decoder_) {
const std::optional<StarboardAudioSampleInfo>& audio_info =
audio_decoder_->GetAudioSampleInfo();
CHECK(audio_info);
params.audio_sample_info = *audio_info;
}
if (video_decoder_) {
const std::optional<StarboardVideoSampleInfo>& video_info =
video_decoder_->GetVideoSampleInfo();
CHECK(video_info);
params.video_sample_info = *video_info;
}
params.output_mode = kStarboardPlayerOutputModePunchOut;
player_ =
starboard_->CreatePlayer(&params,
/*callback_handler=*/&player_callback_handler_);
CHECK(player_);
if (pending_geometry_change_) {
OnGeometryChanged(*pending_geometry_change_,
StarboardVideoPlane::Transform::TRANSFORM_NONE);
pending_geometry_change_ = std::nullopt;
}
}
void MediaPipelineBackendStarboard::OnSampleDecoded(
void* player,
StarboardMediaType type,
StarboardDecoderState decoder_state,
int ticket) {
if (!media_task_runner_->RunsTasksInCurrentSequence()) {
media_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&MediaPipelineBackendStarboard::OnSampleDecoded,
weak_factory_.GetWeakPtr(), player, type, decoder_state,
ticket));
return;
}
DCHECK_EQ(player, player_);
if (ticket != seek_ticket_) {
// TODO(antoniori): we may still need to trigger a
// Delegate::OnPushBufferComplete callback here for both decoders, even if
// there was a seek. Need to verify the expected behavior.
LOG(INFO) << "Seek ticket mismatch";
return;
}
// If a decoder is not initialized, it means this is the first OnSampleDecoded
// call after a seek. So no buffer was pushed, but we still need to initialize
// the decoder.
if (type == kStarboardMediaTypeAudio) {
DCHECK(audio_decoder_);
if (audio_decoder_->IsInitialized()) {
audio_decoder_->OnBufferWritten();
} else {
LOG(INFO) << "Initializing audio decoder";
audio_decoder_->Initialize(player_);
}
} else {
DCHECK(video_decoder_);
if (video_decoder_->IsInitialized()) {
video_decoder_->OnBufferWritten();
} else {
LOG(INFO) << "Initializing video decoder";
video_decoder_->Initialize(player_);
}
}
}
void MediaPipelineBackendStarboard::DeallocateSample(
void* player,
const void* sample_buffer) {
if (!media_task_runner_->RunsTasksInCurrentSequence()) {
media_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&MediaPipelineBackendStarboard::DeallocateSample,
weak_factory_.GetWeakPtr(), player, sample_buffer));
return;
}
// Unfortunately, the starboard SbPlayer API does not report the type of the
// buffer, so we let both decoders attempt to deallocate it. Each decoder
// knows which buffers it owns, so they will not attempt to deallocate buffers
// that they do not own.
if (audio_decoder_) {
audio_decoder_->Deallocate(static_cast<const uint8_t*>(sample_buffer));
}
if (video_decoder_) {
video_decoder_->Deallocate(static_cast<const uint8_t*>(sample_buffer));
}
}
void MediaPipelineBackendStarboard::CallOnSampleDecoded(
void* player,
void* context,
StarboardMediaType type,
StarboardDecoderState decoder_state,
int ticket) {
static_cast<MediaPipelineBackendStarboard*>(context)->OnSampleDecoded(
player, type, decoder_state, ticket);
}
void MediaPipelineBackendStarboard::CallDeallocateSample(
void* player,
void* context,
const void* sample_buffer) {
static_cast<MediaPipelineBackendStarboard*>(context)->DeallocateSample(
player, sample_buffer);
}
void MediaPipelineBackendStarboard::CallOnPlayerStatus(
void* player,
void* context,
StarboardPlayerState state,
int ticket) {
static_cast<MediaPipelineBackendStarboard*>(context)->OnPlayerStatus(
player, state, ticket);
}
void MediaPipelineBackendStarboard::CallOnPlayerError(
void* player,
void* context,
StarboardPlayerError error,
const char* message) {
static_cast<MediaPipelineBackendStarboard*>(context)->OnPlayerError(
player, error, message);
}
void MediaPipelineBackendStarboard::OnPlayerStatus(void* player,
StarboardPlayerState state,
int ticket) {
if (!media_task_runner_->RunsTasksInCurrentSequence()) {
media_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&MediaPipelineBackendStarboard::OnPlayerStatus,
weak_factory_.GetWeakPtr(), player, state, ticket));
return;
}
DCHECK_EQ(player, player_);
LOG(INFO) << "Received starboard player status: " << state;
if (state == kStarboardPlayerStateEndOfStream) {
// Since playback has stopped, the decoders' delegates must be notified.
if (audio_decoder_) {
audio_decoder_->OnSbPlayerEndOfStream();
}
if (video_decoder_) {
video_decoder_->OnSbPlayerEndOfStream();
}
}
}
void MediaPipelineBackendStarboard::OnPlayerError(void* player,
StarboardPlayerError error,
const std::string& message) {
if (!media_task_runner_->RunsTasksInCurrentSequence()) {
media_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&MediaPipelineBackendStarboard::OnPlayerError,
weak_factory_.GetWeakPtr(), player, error, message));
return;
}
LOG(ERROR) << "Received starboard player error: " << error
<< ", with message " << message;
if (error == kStarboardPlayerErrorDecode) {
if (audio_decoder_) {
audio_decoder_->OnStarboardDecodeError();
}
if (video_decoder_) {
video_decoder_->OnStarboardDecodeError();
}
}
}
void MediaPipelineBackendStarboard::TestOnlySetStarboardApiWrapper(
std::unique_ptr<StarboardApiWrapper> starboard) {
DCHECK(media_task_runner_->RunsTasksInCurrentSequence());
LOG(INFO) << "Replacing the StarboardApiWrapper used by "
"MediaPipelineBackendStarboard and decoders. This should only "
"happen in tests";
starboard_ = std::move(starboard);
}
} // namespace media
} // namespace chromecast

@ -0,0 +1,171 @@
// 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.
#ifndef CHROMECAST_STARBOARD_MEDIA_MEDIA_MEDIA_PIPELINE_BACKEND_STARBOARD_H_
#define CHROMECAST_STARBOARD_MEDIA_MEDIA_MEDIA_PIPELINE_BACKEND_STARBOARD_H_
#include <optional>
#include "base/memory/weak_ptr.h"
#include "base/task/sequenced_task_runner.h"
#include "chromecast/public/graphics_types.h"
#include "chromecast/starboard/media/media/starboard_api_wrapper.h"
#include "chromecast/starboard/media/media/starboard_video_plane.h"
#include "starboard_audio_decoder.h"
#include "starboard_video_decoder.h"
namespace chromecast {
namespace media {
// A backend that uses starboard for decoding/rendering.
//
// Public functions and the destructor must be called on the sequence that
// consntructed the MediaPipelineBackendStarboard.
class MediaPipelineBackendStarboard : public MediaPipelineBackend {
public:
explicit MediaPipelineBackendStarboard(StarboardVideoPlane* video_plane);
~MediaPipelineBackendStarboard() override;
// For testing purposes, `starboard` will be used to call starboard functions.
void TestOnlySetStarboardApiWrapper(
std::unique_ptr<StarboardApiWrapper> starboard);
// MediaPipelineBackend implementation:
AudioDecoder* CreateAudioDecoder() override;
VideoDecoder* CreateVideoDecoder() override;
bool Initialize() override;
bool Start(int64_t start_pts) override;
void Stop() override;
bool Pause() override;
bool Resume() override;
int64_t GetCurrentPts() override;
bool SetPlaybackRate(float rate) override;
// StarboardVideoPlane::Delegate implementation:
private:
// Represents the state of the backend. See the documentation of
// media_pipeline_backend.h for more information about valid transitions
// between states.
enum class State {
kUninitialized,
kInitialized,
kPlaying,
kPaused,
};
// Called when the video plane's geometry changes. This means that we need to
// update the SbPlayer's bounds.
//
// Expects that `display_rect` is in display coordinates, NOT (physical)
// screen coordinates.
//
// This function must be called on media_task_runner_.
void OnGeometryChanged(const RectF& display_rect,
StarboardVideoPlane::Transform transform);
// Creates the starboard player object.
void CreatePlayer();
// Called when a starboard audio or video decoder needs more samples. This
// occurs after either:
// 1. A sample has been decoded, or
// 2. A seek has finished
//
// For case 1, we notify the relevant decoder that the sample has been
// decoded. For case 2, we re-initialize the decoder, meaning it will start
// pushing buffers to starboard again.
void OnSampleDecoded(void* player,
StarboardMediaType type,
StarboardDecoderState decoder_state,
int ticket);
// Called when a sample can be deallocated. The decoders handle the actual
// deallocation.
void DeallocateSample(void* player, const void* sample_buffer);
// Called when the SbPlayer's state has changed.
void OnPlayerStatus(void* player, StarboardPlayerState state, int ticket);
// Called when the SbPlayer has encountered an error.
void OnPlayerError(void* player,
StarboardPlayerError error,
const std::string& message);
// Called when starboard has started. This must be called on the media task
// runner.
void OnStarboardStarted();
// Performs the actual play logic by setting the playback rate to a non-zero
// value. player_ must not be null when this is called.
void DoPlay();
// Performs the actual pause logic by setting the playback rate to 0. player_
// must not be null when this is called.
void DoPause();
// Seeks to last_seek_pts_ and increments seek_ticket_.
void DoSeek();
// A pure function that calls OnSampleDecoded. `context` is a pointer to a
// MediaPipelineBackendStarboard object. The rest of the arguments are
// forwarded to OnSampleDecoded.
static void CallOnSampleDecoded(void* player,
void* context,
StarboardMediaType type,
StarboardDecoderState decoder_state,
int ticket);
// Called by starboard when a sample can be deallocated. Note that this may be
// called after CallOnSampleDecoded is called for a given sample, meaning that
// the sample needs to outlive the call to CallOnSampleDecoded.
static void CallDeallocateSample(void* player,
void* context,
const void* sample_buffer);
// Called by starboard when the player's status changes. Notably, this will be
// called with state kStarboardPlayerStateEndOfStream when the end of stream
// buffer has been rendered.
static void CallOnPlayerStatus(void* player,
void* context,
StarboardPlayerState state,
int ticket);
// Called by starboard when there is a player error.
static void CallOnPlayerError(void* player,
void* context,
StarboardPlayerError error,
const char* message);
StarboardPlayerCallbackHandler player_callback_handler_ = {
/*context=*/this, &CallOnSampleDecoded,
&CallDeallocateSample, &CallOnPlayerStatus,
&CallOnPlayerError,
};
// Calls to Starboard are made through this struct, to allow tests to mock
// their behavior (and not rely on Starboard).
std::unique_ptr<StarboardApiWrapper> starboard_;
State state_ = State::kUninitialized;
float playback_rate_ = 1.0;
int64_t last_seek_pts_ = -1;
// Ticket representing the last seek. This is necessary to recognize callbacks
// from old seeks, which are no longer relevant.
int seek_ticket_ = 0;
scoped_refptr<base::SequencedTaskRunner> media_task_runner_;
// An opaque handle to the SbPlayer.
void* player_ = nullptr;
std::optional<StarboardAudioDecoder> audio_decoder_;
std::optional<StarboardVideoDecoder> video_decoder_;
std::optional<RectF> pending_geometry_change_;
StarboardVideoPlane* video_plane_ = nullptr;
int64_t video_plane_callback_token_ = 0;
// This must be destructed first.
base::WeakPtrFactory<MediaPipelineBackendStarboard> weak_factory_{this};
};
} // namespace media
} // namespace chromecast
#endif // CHROMECAST_STARBOARD_MEDIA_MEDIA_MEDIA_PIPELINE_BACKEND_STARBOARD_H_

@ -0,0 +1,393 @@
// 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 "media_pipeline_backend_starboard.h"
#include "base/test/task_environment.h"
#include "chromecast/public/graphics_types.h"
#include "chromecast/starboard/media/media/starboard_api_wrapper.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::_;
using ::testing::AnyNumber;
using ::testing::DoAll;
using ::testing::DoubleEq;
using ::testing::Mock;
using ::testing::MockFunction;
using ::testing::NotNull;
using ::testing::Return;
using ::testing::SaveArg;
using ::testing::WithArg;
// A mock delegate that can be passed to decoders.
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));
};
// Returns a simple AudioConfig.
AudioConfig GetBasicAudioConfig() {
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;
}
// Returns a simple VideoConfig.
VideoConfig GetBasicVideoConfig() {
VideoConfig config;
config.codec = VideoCodec::kCodecH264;
config.encryption_scheme = EncryptionScheme::kUnencrypted;
config.width = 123;
config.height = 456;
return config;
}
// A test fixture is used to manage the global mock state and to handle the
// lifetime of the SingleThreadTaskEnvironment.
class MediaPipelineBackendStarboardTest : public ::testing::Test {
protected:
MediaPipelineBackendStarboardTest()
: starboard_(std::make_unique<MockStarboardApiWrapper>()) {
// Sets up default behavior for the mock functions that return values, so
// that tests that do not care about this functionality can ignore them.
ON_CALL(*starboard_, CreatePlayer).WillByDefault(Return(&fake_player_));
ON_CALL(*starboard_, EnsureInitialized).WillByDefault(Return(true));
ON_CALL(*starboard_, SetPlaybackRate).WillByDefault(Return(true));
}
~MediaPipelineBackendStarboardTest() 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;
StarboardVideoPlane video_plane_;
};
TEST_F(MediaPipelineBackendStarboardTest, InitializesSuccessfully) {
EXPECT_CALL(*starboard_, EnsureInitialized).WillOnce(Return(true));
EXPECT_CALL(*starboard_, CreatePlayer).WillOnce(Return(&fake_player_));
MediaPipelineBackendStarboard backend(&video_plane_);
backend.TestOnlySetStarboardApiWrapper(std::move(starboard_));
EXPECT_TRUE(backend.Initialize());
}
TEST_F(MediaPipelineBackendStarboardTest,
HandlesGeometryChangeAfterPlayerCreation) {
EXPECT_CALL(*starboard_, CreatePlayer).WillOnce(Return(&fake_player_));
EXPECT_CALL(*starboard_, SetPlayerBounds(&fake_player_, 0, 1, 2, 1920, 1080));
MediaPipelineBackendStarboard backend(&video_plane_);
backend.TestOnlySetStarboardApiWrapper(std::move(starboard_));
MediaPipelineBackend::VideoDecoder* video_decoder =
backend.CreateVideoDecoder();
// The video's width and height match the display's aspect ratio (set above).
// Thus, the player's bound should be set to the full screen, 1920x1080.
VideoConfig config = GetBasicVideoConfig();
config.width = 1280;
config.height = 720;
video_decoder->SetConfig(config);
EXPECT_TRUE(backend.Initialize());
video_plane_.SetGeometry(RectF(1, 2, 1920, 1080),
StarboardVideoPlane::Transform::TRANSFORM_NONE);
task_environment_.RunUntilIdle();
}
TEST_F(MediaPipelineBackendStarboardTest,
HandlesGeometryChangeBeforePlayerCreation) {
EXPECT_CALL(*starboard_, CreatePlayer).WillOnce(Return(&fake_player_));
EXPECT_CALL(*starboard_, SetPlayerBounds(&fake_player_, 0, 3, 4, 1920, 1080));
MediaPipelineBackendStarboard backend(&video_plane_);
backend.TestOnlySetStarboardApiWrapper(std::move(starboard_));
MediaPipelineBackend::VideoDecoder* video_decoder =
backend.CreateVideoDecoder();
// The video's width and height match the display's aspect ratio (set above).
// Thus, the player's bound should be set to the full screen, 1920x1080.
VideoConfig config = GetBasicVideoConfig();
config.width = 1280;
config.height = 720;
video_decoder->SetConfig(config);
video_plane_.SetGeometry(RectF(3, 4, 1920, 1080),
StarboardVideoPlane::Transform::TRANSFORM_NONE);
task_environment_.RunUntilIdle();
// Calling Initialize should trigger the call to set the player's bounds.
EXPECT_TRUE(backend.Initialize());
}
TEST_F(MediaPipelineBackendStarboardTest, DestroysPlayerOnDestruction) {
EXPECT_CALL(*starboard_, DestroyPlayer(&fake_player_)).Times(1);
MediaPipelineBackendStarboard backend(&video_plane_);
backend.TestOnlySetStarboardApiWrapper(std::move(starboard_));
EXPECT_TRUE(backend.Initialize());
}
TEST_F(MediaPipelineBackendStarboardTest,
DoesNotCallDestroyPlayerIfNotInitialized) {
EXPECT_CALL(*starboard_, DestroyPlayer).Times(0);
MediaPipelineBackendStarboard backend(&video_plane_);
backend.TestOnlySetStarboardApiWrapper(std::move(starboard_));
}
TEST_F(MediaPipelineBackendStarboardTest, SeeksToBeginningOnStart) {
constexpr int64_t start_time = 0;
EXPECT_CALL(*starboard_, SeekTo(&fake_player_, start_time, _)).Times(1);
EXPECT_CALL(*starboard_, SetPlaybackRate(&fake_player_, DoubleEq(1.0)))
.Times(1);
MediaPipelineBackendStarboard backend(&video_plane_);
backend.TestOnlySetStarboardApiWrapper(std::move(starboard_));
CHECK(backend.Initialize());
EXPECT_TRUE(backend.Start(start_time));
}
TEST_F(MediaPipelineBackendStarboardTest, SeeksToMidPointOnStart) {
constexpr int64_t start_time = 10;
EXPECT_CALL(*starboard_, SeekTo(&fake_player_, start_time, _)).Times(1);
EXPECT_CALL(*starboard_, SetPlaybackRate(&fake_player_, DoubleEq(1.0)))
.Times(1);
MediaPipelineBackendStarboard backend(&video_plane_);
backend.TestOnlySetStarboardApiWrapper(std::move(starboard_));
CHECK(backend.Initialize());
EXPECT_TRUE(backend.Start(start_time));
}
TEST_F(MediaPipelineBackendStarboardTest, SetsPlaybackRateToZeroOnPause) {
EXPECT_CALL(*starboard_, SetPlaybackRate(&fake_player_, DoubleEq(1.0)))
.Times(AnyNumber());
// Pausing playback means setting the playback rate to 0 in starboard.
EXPECT_CALL(*starboard_, SetPlaybackRate(&fake_player_, DoubleEq(0.0)))
.Times(1);
MediaPipelineBackendStarboard backend(&video_plane_);
backend.TestOnlySetStarboardApiWrapper(std::move(starboard_));
CHECK(backend.Initialize());
CHECK(backend.Start(0));
EXPECT_TRUE(backend.Pause());
}
TEST_F(MediaPipelineBackendStarboardTest,
SetsPlaybackRateToPreviousValueOnResume) {
constexpr double playback_rate = 2.0;
// This should be called when playback is paused.
EXPECT_CALL(*starboard_, GetPlayerInfo)
.WillRepeatedly(WithArg<1>([](StarboardPlayerInfo* player_info) {
CHECK(player_info);
player_info->playback_rate = playback_rate;
}));
EXPECT_CALL(*starboard_, SetPlaybackRate(&fake_player_, DoubleEq(1.0)))
.Times(AnyNumber());
// This should be called twice: once when we set the playback rate, and once
// when we resume playback.
EXPECT_CALL(*starboard_,
SetPlaybackRate(&fake_player_, DoubleEq(playback_rate)))
.Times(2);
// This should be called when we pause playback.
EXPECT_CALL(*starboard_, SetPlaybackRate(&fake_player_, DoubleEq(0.0)))
.Times(1);
MediaPipelineBackendStarboard backend(&video_plane_);
backend.TestOnlySetStarboardApiWrapper(std::move(starboard_));
CHECK(backend.Initialize());
CHECK(backend.Start(0));
EXPECT_TRUE(backend.SetPlaybackRate(playback_rate));
EXPECT_TRUE(backend.Pause());
EXPECT_TRUE(backend.Resume());
}
TEST_F(MediaPipelineBackendStarboardTest, GetsCurrentPts) {
constexpr int64_t current_pts = 123;
EXPECT_CALL(*starboard_, GetPlayerInfo)
.WillRepeatedly(WithArg<1>([](StarboardPlayerInfo* player_info) {
CHECK(player_info);
player_info->current_media_timestamp_micros = current_pts;
player_info->playback_rate = 1.0;
}));
MediaPipelineBackendStarboard backend(&video_plane_);
backend.TestOnlySetStarboardApiWrapper(std::move(starboard_));
CHECK(backend.Initialize());
CHECK(backend.Start(0));
EXPECT_EQ(backend.GetCurrentPts(), current_pts);
}
TEST_F(MediaPipelineBackendStarboardTest,
CallsDelegateEndOfStreamWhenStarboardReportsEoS) {
const StarboardPlayerCallbackHandler* callback_handler = nullptr;
EXPECT_CALL(*starboard_, CreatePlayer)
.WillOnce(DoAll(SaveArg<1>(&callback_handler), Return(&fake_player_)));
MediaPipelineBackendStarboard backend(&video_plane_);
backend.TestOnlySetStarboardApiWrapper(std::move(starboard_));
MediaPipelineBackend::AudioDecoder* audio_decoder =
backend.CreateAudioDecoder();
MediaPipelineBackend::VideoDecoder* video_decoder =
backend.CreateVideoDecoder();
ASSERT_THAT(audio_decoder, NotNull());
ASSERT_THAT(video_decoder, NotNull());
audio_decoder->SetConfig(GetBasicAudioConfig());
video_decoder->SetConfig(GetBasicVideoConfig());
MockDelegate audio_delegate;
MockDelegate video_delegate;
EXPECT_CALL(audio_delegate, OnEndOfStream).Times(1);
EXPECT_CALL(video_delegate, OnEndOfStream).Times(1);
audio_decoder->SetDelegate(&audio_delegate);
video_decoder->SetDelegate(&video_delegate);
CHECK(backend.Initialize());
CHECK(backend.Start(0));
ASSERT_THAT(callback_handler, NotNull());
// This should trigger the calls to audio_delegate.OnEndOfStream() and
// video_delegate.OnEndOfStream().
callback_handler->player_status_fn(&fake_player_, callback_handler->context,
kStarboardPlayerStateEndOfStream,
/*ticket=*/1);
Mock::VerifyAndClearExpectations(&audio_delegate);
Mock::VerifyAndClearExpectations(&video_delegate);
}
TEST_F(MediaPipelineBackendStarboardTest,
DelegatesAreNotifiedOnStarboardDecoderError) {
const StarboardPlayerCallbackHandler* callback_handler = nullptr;
EXPECT_CALL(*starboard_, CreatePlayer)
.WillOnce(DoAll(SaveArg<1>(&callback_handler), Return(&fake_player_)));
MediaPipelineBackendStarboard backend(&video_plane_);
backend.TestOnlySetStarboardApiWrapper(std::move(starboard_));
MediaPipelineBackend::AudioDecoder* audio_decoder =
backend.CreateAudioDecoder();
MediaPipelineBackend::VideoDecoder* video_decoder =
backend.CreateVideoDecoder();
ASSERT_THAT(audio_decoder, NotNull());
ASSERT_THAT(video_decoder, NotNull());
audio_decoder->SetConfig(GetBasicAudioConfig());
video_decoder->SetConfig(GetBasicVideoConfig());
MockDelegate audio_delegate;
MockDelegate video_delegate;
EXPECT_CALL(audio_delegate, OnDecoderError).Times(1);
EXPECT_CALL(video_delegate, OnDecoderError).Times(1);
audio_decoder->SetDelegate(&audio_delegate);
video_decoder->SetDelegate(&video_delegate);
CHECK(backend.Initialize());
CHECK(backend.Start(0));
ASSERT_THAT(callback_handler, NotNull());
// This should trigger the calls to audio_delegate.OnDecoderError() and
// video_delegate.OnDecoderError().
callback_handler->player_error_fn(&fake_player_, callback_handler->context,
kStarboardPlayerErrorDecode,
"A decoder error occurred");
Mock::VerifyAndClearExpectations(&audio_delegate);
Mock::VerifyAndClearExpectations(&video_delegate);
}
TEST_F(MediaPipelineBackendStarboardTest,
DelegatesAreNotNotifiedOnUnrelatedError) {
const StarboardPlayerCallbackHandler* callback_handler = nullptr;
EXPECT_CALL(*starboard_, CreatePlayer)
.WillOnce(DoAll(SaveArg<1>(&callback_handler), Return(&fake_player_)));
MediaPipelineBackendStarboard backend(&video_plane_);
backend.TestOnlySetStarboardApiWrapper(std::move(starboard_));
MediaPipelineBackend::AudioDecoder* audio_decoder =
backend.CreateAudioDecoder();
MediaPipelineBackend::VideoDecoder* video_decoder =
backend.CreateVideoDecoder();
ASSERT_THAT(audio_decoder, NotNull());
ASSERT_THAT(video_decoder, NotNull());
audio_decoder->SetConfig(GetBasicAudioConfig());
video_decoder->SetConfig(GetBasicVideoConfig());
MockDelegate audio_delegate;
MockDelegate video_delegate;
EXPECT_CALL(audio_delegate, OnDecoderError).Times(0);
EXPECT_CALL(video_delegate, OnDecoderError).Times(0);
audio_decoder->SetDelegate(&audio_delegate);
video_decoder->SetDelegate(&video_delegate);
CHECK(backend.Initialize());
CHECK(backend.Start(0));
ASSERT_THAT(callback_handler, NotNull());
// This should NOT trigger the calls to audio_delegate.OnDecoderError() and
// video_delegate.OnDecoderError(), since it's not a decoder error.
callback_handler->player_error_fn(&fake_player_, callback_handler->context,
kStarboardPlayerErrorCapabilityChanged,
"Starboard capabilities changed");
Mock::VerifyAndClearExpectations(&audio_delegate);
Mock::VerifyAndClearExpectations(&video_delegate);
}
} // namespace
} // namespace media
} // namespace chromecast

@ -25,7 +25,8 @@ using ::testing::Pointwise;
TEST(StarboardResamplerTest, PCM8ToS16) {
size_t out_size;
const std::vector<uint8_t> buffer_data = {0, 0x80, 0xFF};
const std::vector<int16_t> expected_s16_data = {0x8000, 0, 0x7FFF};
const std::vector<int16_t> expected_s16_data = {static_cast<int16_t>(0x8000),
0, 0x7FFF};
scoped_refptr<CastDecoderBufferImpl> buffer(
new CastDecoderBufferImpl(sizeof(uint8_t) * buffer_data.size()));
@ -45,7 +46,8 @@ TEST(StarboardResamplerTest, PCM8ToS16) {
TEST(StarboardResamplerTest, PCM8ToS32) {
size_t out_size;
const std::vector<uint8_t> buffer_data = {0, 0x80, 0xFF};
const std::vector<int32_t> expected_s32_data = {0x80000000, 0, 0x7FFFFFFF};
const std::vector<int32_t> expected_s32_data = {
static_cast<int32_t>(0x80000000), 0, 0x7FFFFFFF};
scoped_refptr<CastDecoderBufferImpl> buffer(
new CastDecoderBufferImpl(sizeof(uint8_t) * buffer_data.size()));
@ -108,8 +110,10 @@ TEST(StarboardResamplerTest, PCMS16ToS16) {
TEST(StarboardResamplerTest, PCMS16ToS32) {
size_t out_size;
const std::vector<int16_t> buffer_data = {0x8000, 0, 0x7FFF};
const std::vector<int32_t> expected_s32_data = {0x80000000, 0, 0x7FFFFFFF};
const std::vector<int16_t> buffer_data = {static_cast<int16_t>(0x8000), 0,
0x7FFF};
const std::vector<int32_t> expected_s32_data = {
static_cast<int32_t>(0x80000000), 0, 0x7FFFFFFF};
scoped_refptr<CastDecoderBufferImpl> buffer(
new CastDecoderBufferImpl(sizeof(int16_t) * buffer_data.size()));
@ -128,7 +132,9 @@ TEST(StarboardResamplerTest, PCMS16ToS32) {
TEST(StarboardResamplerTest, PCMS16ToFloat) {
size_t out_size;
const std::vector<int16_t> buffer_data = {0x8000, 0xC000, 0, 0x3FFF, 0x7FFF};
const std::vector<int16_t> buffer_data = {static_cast<int16_t>(0x8000),
static_cast<int16_t>(0xC000), 0,
0x3FFF, 0x7FFF};
const std::vector<float> expected_f32_data = {-1.0f, -0.5f, 0.0f,
0.499984741f, 1.0f};
@ -176,7 +182,8 @@ TEST(StarboardResamplerTest, PCMS24ToS32) {
// order.
const std::vector<uint8_t> buffer_data = {0, 0, 0x80, 0, 0,
0, 0xFF, 0xFF, 0x7F};
const std::vector<int32_t> expected_s32_data = {0x80000000, 0, 0x7FFFFFFF};
const std::vector<int32_t> expected_s32_data = {
static_cast<int32_t>(0x80000000), 0, 0x7FFFFFFF};
scoped_refptr<CastDecoderBufferImpl> buffer(
new CastDecoderBufferImpl(buffer_data.size()));
@ -259,7 +266,8 @@ TEST(StarboardResamplerTest, PCMS32ToS32) {
TEST(StarboardResamplerTest, PCMS32ToFloat) {
size_t out_size;
const std::vector<int32_t> buffer_data = {0x80000000, 0, 0x7FFFFFFF};
const std::vector<int32_t> buffer_data = {static_cast<int32_t>(0x80000000), 0,
0x7FFFFFFF};
const std::vector<float> expected_f32_data = {-1.0f, 0.0f, 1.0f};
scoped_refptr<CastDecoderBufferImpl> buffer(
@ -414,12 +422,28 @@ TEST(StarboardResamplerTest, PushesBufferToStarboardPlanarPCMF32) {
TEST(StarboardResamplerTest, PushesBufferToStarboardPlanarPCM32Mono) {
size_t out_size;
// Everything should be shifted down by 16 bits.
const std::vector<int32_t> buffer_data = {
0x80000000, 0x1000000, 0x2000000, 0x3000000, 0x4000000, 0x5000000,
0x6000000, 0x7000000, 0x8000000, 0x9000000, 0x7FFFFFFF};
const std::vector<int16_t> expected_s16_data = {0x8000, 0x100, 0x200, 0x300,
0x400, 0x500, 0x600, 0x700,
0x800, 0x900, 0x7FFF};
const std::vector<int32_t> buffer_data = {static_cast<int32_t>(0x80000000),
0x1000000,
0x2000000,
0x3000000,
0x4000000,
0x5000000,
0x6000000,
0x7000000,
0x8000000,
0x9000000,
0x7FFFFFFF};
const std::vector<int16_t> expected_s16_data = {static_cast<int16_t>(0x8000),
0x100,
0x200,
0x300,
0x400,
0x500,
0x600,
0x700,
0x800,
0x900,
0x7FFF};
scoped_refptr<CastDecoderBufferImpl> buffer(
new CastDecoderBufferImpl(sizeof(int32_t) * buffer_data.size()));

@ -4,13 +4,12 @@
#include "chromecast/starboard/media/media/starboard_video_decoder.h"
#include <starboard_api_wrapper.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/media/starboard_api_wrapper.h"
#include "media/base/decoder_buffer.h"
#include "media/base/decrypt_config.h"
#include "media/base/encryption_scheme.h"
@ -318,8 +317,7 @@ TEST_F(StarboardVideoDecoderTest, PopulatesHdrInfo) {
config.hdr_metadata.max_content_light_level = 1;
config.hdr_metadata.max_frame_average_light_level = 2;
MasteringMetadata& color_volume_metadata =
config.hdr_metadata.mastering_metadata;
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;