blob: 9d8273e7447679d665a6612605a37d0fc3223369 [file] [log] [blame]
// Copyright 2023 The Cobalt Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "starboard/nplb/player_test_fixture.h"
#include <algorithm>
#include "starboard/common/string.h"
#include "starboard/nplb/drm_helpers.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace starboard {
namespace nplb {
using shared::starboard::player::video_dmp::VideoDmpReader;
using testing::FakeGraphicsContextProvider;
SbPlayerTestFixture::CallbackEvent::CallbackEvent() : event_type(kEmptyEvent) {}
SbPlayerTestFixture::CallbackEvent::CallbackEvent(SbPlayer player,
SbMediaType type,
SbPlayerDecoderState state,
int ticket)
: event_type(kDecoderStateEvent),
player(player),
media_type(type),
decoder_state(state),
ticket(ticket) {}
SbPlayerTestFixture::CallbackEvent::CallbackEvent(SbPlayer player,
SbPlayerState state,
int ticket)
: event_type(kPlayerStateEvent),
player(player),
player_state(state),
ticket(ticket) {}
SbPlayerTestFixture::SbPlayerTestFixture(const SbPlayerTestConfig& config)
: output_mode_(std::get<2>(config)), key_system_(std::get<3>(config)) {
SB_DCHECK(output_mode_ == kSbPlayerOutputModeDecodeToTexture ||
output_mode_ == kSbPlayerOutputModePunchOut);
const char* audio_dmp_filename = std::get<0>(config);
const char* video_dmp_filename = std::get<1>(config);
if (audio_dmp_filename && strlen(audio_dmp_filename) > 0) {
audio_dmp_reader_.reset(new VideoDmpReader(
audio_dmp_filename, VideoDmpReader::kEnableReadOnDemand));
}
if (video_dmp_filename && strlen(video_dmp_filename) > 0) {
video_dmp_reader_.reset(new VideoDmpReader(
video_dmp_filename, VideoDmpReader::kEnableReadOnDemand));
}
Initialize();
}
SbPlayerTestFixture::~SbPlayerTestFixture() {
SB_DCHECK(thread_checker_.CalledOnValidThread());
TearDown();
}
void SbPlayerTestFixture::Seek(const SbTime time) {
SB_DCHECK(thread_checker_.CalledOnValidThread());
SB_DCHECK(SbPlayerIsValid(player_));
ASSERT_FALSE(error_occurred_);
ASSERT_FALSE(HasReceivedPlayerState(kSbPlayerStateDestroyed));
ASSERT_TRUE(HasReceivedPlayerState(kSbPlayerStateInitialized));
can_accept_more_audio_data_ = false;
can_accept_more_video_data_ = false;
player_state_set_.clear();
player_state_set_.insert(kSbPlayerStateInitialized);
audio_end_of_stream_written_ = false;
video_end_of_stream_written_ = false;
#if SB_API_VERSION >= 15
SbPlayerSeek(player_, time, ++ticket_);
#else // SB_API_VERSION >= 15
SbPlayerSeek2(player_, time, ++ticket_);
#endif // SB_API_VERSION >= 15
}
void SbPlayerTestFixture::Write(const GroupedSamples& grouped_samples) {
SB_DCHECK(thread_checker_.CalledOnValidThread());
SB_DCHECK(SbPlayerIsValid(player_));
SB_DCHECK(!audio_end_of_stream_written_);
SB_DCHECK(!video_end_of_stream_written_);
ASSERT_FALSE(error_occurred_);
int audio_start_index = grouped_samples.audio_start_index();
int audio_samples_to_write = grouped_samples.audio_samples_to_write();
int video_start_index = grouped_samples.video_start_index();
int video_samples_to_write = grouped_samples.video_samples_to_write();
bool write_audio_eos = grouped_samples.write_audio_eos();
bool write_video_eos = grouped_samples.write_video_eos();
SB_DCHECK(audio_start_index >= 0);
SB_DCHECK(audio_samples_to_write >= 0);
SB_DCHECK(video_start_index >= 0);
SB_DCHECK(video_samples_to_write >= 0);
if (audio_samples_to_write > 0 || write_audio_eos) {
SB_DCHECK(audio_dmp_reader_);
}
if (video_samples_to_write > 0 || write_video_eos) {
SB_DCHECK(video_dmp_reader_);
}
int max_audio_samples_per_write =
SbPlayerGetMaximumNumberOfSamplesPerWrite(player_, kSbMediaTypeAudio);
int max_video_samples_per_write =
SbPlayerGetMaximumNumberOfSamplesPerWrite(player_, kSbMediaTypeVideo);
// Cap the samples to write to the end of the dmp files.
if (audio_samples_to_write > 0) {
audio_samples_to_write = std::min<int>(
audio_samples_to_write,
audio_dmp_reader_->number_of_audio_buffers() - audio_start_index);
}
if (video_samples_to_write > 0) {
video_samples_to_write = std::min<int>(
video_samples_to_write,
video_dmp_reader_->number_of_video_buffers() - video_start_index);
}
bool has_more_audio = audio_samples_to_write > 0 || write_audio_eos;
bool has_more_video = video_samples_to_write > 0 || write_video_eos;
while (has_more_audio || has_more_video) {
ASSERT_NO_FATAL_FAILURE(WaitForDecoderStateNeedsData());
if (can_accept_more_audio_data_ && has_more_audio) {
if (audio_samples_to_write > 0) {
auto samples_to_write =
std::min(max_audio_samples_per_write, audio_samples_to_write);
ASSERT_NO_FATAL_FAILURE(WriteSamples(
kSbMediaTypeAudio, audio_start_index, samples_to_write));
audio_start_index += samples_to_write;
audio_samples_to_write -= samples_to_write;
} else if (!audio_end_of_stream_written_ && write_audio_eos) {
ASSERT_NO_FATAL_FAILURE(WriteEndOfStream(kSbMediaTypeAudio));
}
has_more_audio = audio_samples_to_write > 0 ||
(!audio_end_of_stream_written_ && write_audio_eos);
}
if (can_accept_more_video_data_ && has_more_video) {
if (video_samples_to_write > 0) {
auto samples_to_write =
std::min(max_video_samples_per_write, video_samples_to_write);
ASSERT_NO_FATAL_FAILURE(WriteSamples(
kSbMediaTypeVideo, video_start_index, samples_to_write));
video_start_index += samples_to_write;
video_samples_to_write -= samples_to_write;
} else if (!video_end_of_stream_written_ && write_video_eos) {
ASSERT_NO_FATAL_FAILURE(WriteEndOfStream(kSbMediaTypeVideo));
}
has_more_video = video_samples_to_write > 0 ||
(!video_end_of_stream_written_ && write_video_eos);
}
}
}
void SbPlayerTestFixture::WaitForPlayerEndOfStream() {
SB_DCHECK(thread_checker_.CalledOnValidThread());
SB_DCHECK(SbPlayerIsValid(player_));
SB_DCHECK(!audio_dmp_reader_ || audio_end_of_stream_written_);
SB_DCHECK(!video_dmp_reader_ || video_end_of_stream_written_);
ASSERT_FALSE(error_occurred_);
ASSERT_NO_FATAL_FAILURE(WaitForPlayerState(kSbPlayerStateEndOfStream));
}
// static
void SbPlayerTestFixture::DecoderStatusCallback(SbPlayer player,
void* context,
SbMediaType type,
SbPlayerDecoderState state,
int ticket) {
auto fixture = static_cast<SbPlayerTestFixture*>(context);
fixture->OnDecoderState(player, type, state, ticket);
}
// static
void SbPlayerTestFixture::PlayerStatusCallback(SbPlayer player,
void* context,
SbPlayerState state,
int ticket) {
auto fixture = static_cast<SbPlayerTestFixture*>(context);
fixture->OnPlayerState(player, state, ticket);
}
// static
void SbPlayerTestFixture::ErrorCallback(SbPlayer player,
void* context,
SbPlayerError error,
const char* message) {
auto fixture = static_cast<SbPlayerTestFixture*>(context);
fixture->OnError(player, error, message);
}
void SbPlayerTestFixture::OnDecoderState(SbPlayer player,
SbMediaType media_type,
SbPlayerDecoderState state,
int ticket) {
callback_event_queue_.Put(CallbackEvent(player, media_type, state, ticket));
}
void SbPlayerTestFixture::OnPlayerState(SbPlayer player,
SbPlayerState state,
int ticket) {
callback_event_queue_.Put(CallbackEvent(player, state, ticket));
}
void SbPlayerTestFixture::OnError(SbPlayer player,
SbPlayerError error,
const char* message) {
SB_LOG(ERROR) << FormatString("Got SbPlayerError %d with message '%s'", error,
message != NULL ? message : "");
error_occurred_ = true;
}
void SbPlayerTestFixture::Initialize() {
SB_DCHECK(thread_checker_.CalledOnValidThread());
// Initialize drm system.
if (!key_system_.empty()) {
drm_system_ = SbDrmCreateSystem(
key_system_.c_str(), NULL /* context */, DummySessionUpdateRequestFunc,
DummySessionUpdatedFunc, DummySessionKeyStatusesChangedFunc,
DummyServerCertificateUpdatedFunc, DummySessionClosedFunc);
ASSERT_TRUE(SbDrmSystemIsValid(drm_system_));
}
// Initialize player.
auto audio_codec = kSbMediaAudioCodecNone;
auto video_codec = kSbMediaVideoCodecNone;
const shared::starboard::media::AudioStreamInfo* audio_stream_info = NULL;
if (audio_dmp_reader_) {
audio_codec = audio_dmp_reader_->audio_codec();
audio_stream_info = &audio_dmp_reader_->audio_stream_info();
}
if (video_dmp_reader_) {
video_codec = video_dmp_reader_->video_codec();
}
player_ = CallSbPlayerCreate(
fake_graphics_context_provider_.window(), video_codec, audio_codec,
drm_system_, audio_stream_info, "", DummyDeallocateSampleFunc,
DecoderStatusCallback, PlayerStatusCallback, ErrorCallback, this,
output_mode_, fake_graphics_context_provider_.decoder_target_provider());
ASSERT_TRUE(SbPlayerIsValid(player_));
ASSERT_NO_FATAL_FAILURE(WaitForPlayerState(kSbPlayerStateInitialized));
ASSERT_NO_FATAL_FAILURE(Seek(0));
SbPlayerSetPlaybackRate(player_, 1.0);
SbPlayerSetVolume(player_, 1.0);
}
void SbPlayerTestFixture::TearDown() {
SB_DCHECK(thread_checker_.CalledOnValidThread());
// We should always destroy |player_| and |drm_system_|, no matter if there's
// any unexpected player error.
if (SbPlayerIsValid(player_)) {
destroy_player_called_ = true;
SbPlayerDestroy(player_);
}
if (SbDrmSystemIsValid(drm_system_)) {
SbDrmDestroySystem(drm_system_);
}
// We expect player resources are released and all events are sent already
// after SbPlayerDestroy() finishes.
while (callback_event_queue_.Size() > 0) {
ASSERT_NO_FATAL_FAILURE(WaitAndProcessNextEvent());
}
ASSERT_TRUE(HasReceivedPlayerState(kSbPlayerStateDestroyed));
ASSERT_FALSE(error_occurred_);
player_ = kSbPlayerInvalid;
drm_system_ = kSbDrmSystemInvalid;
}
void SbPlayerTestFixture::WriteSamples(SbMediaType media_type,
int start_index,
int samples_to_write) {
SB_DCHECK(thread_checker_.CalledOnValidThread());
SB_DCHECK(start_index >= 0);
SB_DCHECK(samples_to_write > 0);
SB_DCHECK(SbPlayerIsValid(player_));
SB_DCHECK(samples_to_write <=
SbPlayerGetMaximumNumberOfSamplesPerWrite(player_, media_type));
if (media_type == kSbMediaTypeAudio) {
SB_DCHECK(audio_dmp_reader_);
SB_DCHECK(start_index + samples_to_write <=
audio_dmp_reader_->number_of_audio_buffers());
CallSbPlayerWriteSamples(player_, kSbMediaTypeAudio,
audio_dmp_reader_.get(), start_index,
samples_to_write);
can_accept_more_audio_data_ = false;
} else {
SB_DCHECK(media_type == kSbMediaTypeVideo);
SB_DCHECK(video_dmp_reader_);
SB_DCHECK(start_index + samples_to_write <=
video_dmp_reader_->number_of_video_buffers());
CallSbPlayerWriteSamples(player_, kSbMediaTypeVideo,
video_dmp_reader_.get(), start_index,
samples_to_write);
can_accept_more_video_data_ = false;
}
}
void SbPlayerTestFixture::WriteEndOfStream(SbMediaType media_type) {
SB_DCHECK(thread_checker_.CalledOnValidThread());
SB_DCHECK(SbPlayerIsValid(player_));
if (media_type == kSbMediaTypeAudio) {
SB_DCHECK(audio_dmp_reader_);
SB_DCHECK(!audio_end_of_stream_written_);
SbPlayerWriteEndOfStream(player_, kSbMediaTypeAudio);
can_accept_more_audio_data_ = false;
audio_end_of_stream_written_ = true;
} else {
SB_DCHECK(media_type == kSbMediaTypeVideo);
SB_DCHECK(video_dmp_reader_);
SB_DCHECK(!video_end_of_stream_written_);
SbPlayerWriteEndOfStream(player_, kSbMediaTypeVideo);
can_accept_more_video_data_ = false;
video_end_of_stream_written_ = true;
}
}
void SbPlayerTestFixture::WaitAndProcessNextEvent(SbTime timeout) {
SB_DCHECK(thread_checker_.CalledOnValidThread());
auto event = callback_event_queue_.GetTimed(timeout);
// Ignore callback events for previous Seek().
if (event.ticket != ticket_) {
return;
}
switch (event.event_type) {
case CallbackEventType::kEmptyEvent:
break;
case CallbackEventType::kDecoderStateEvent: {
ASSERT_EQ(event.player, player_);
// Callbacks may be in-flight at the time that the player is destroyed by
// a call to |SbPlayerDestroy|. In this case, the callbacks are ignored.
// However no new callbacks are expected after receiving the player status
// |kSbPlayerStateDestroyed|.
ASSERT_FALSE(HasReceivedPlayerState(kSbPlayerStateDestroyed));
// There's only one valid SbPlayerDecoderState. The received decoder state
// must be kSbPlayerDecoderStateNeedsData.
ASSERT_EQ(event.decoder_state, kSbPlayerDecoderStateNeedsData);
if (event.media_type == kSbMediaTypeAudio) {
ASSERT_FALSE(can_accept_more_audio_data_);
can_accept_more_audio_data_ = true;
} else {
ASSERT_TRUE(event.media_type == kSbMediaTypeVideo);
ASSERT_FALSE(can_accept_more_video_data_);
can_accept_more_video_data_ = true;
}
break;
}
case CallbackEventType::kPlayerStateEvent: {
ASSERT_EQ(event.player, player_);
ASSERT_NO_FATAL_FAILURE(AssertPlayerStateIsValid(event.player_state));
player_state_set_.insert(event.player_state);
break;
}
}
ASSERT_FALSE(error_occurred_);
}
void SbPlayerTestFixture::WaitForDecoderStateNeedsData(const SbTime timeout) {
SB_DCHECK(thread_checker_.CalledOnValidThread());
SB_DCHECK(!can_accept_more_audio_data_ || !can_accept_more_video_data_);
bool old_can_accept_more_audio_data = can_accept_more_audio_data_;
bool old_can_accept_more_video_data = can_accept_more_video_data_;
SbTimeMonotonic start = SbTimeGetMonotonicNow();
do {
ASSERT_FALSE(error_occurred_);
GetDecodeTargetWhenSupported();
ASSERT_NO_FATAL_FAILURE(WaitAndProcessNextEvent());
if (old_can_accept_more_audio_data != can_accept_more_audio_data_ ||
old_can_accept_more_video_data != can_accept_more_video_data_) {
return;
}
} while (SbTimeGetMonotonicNow() - start < timeout);
FAIL() << "WaitForDecoderStateNeedsData() did not receive expected state.";
}
void SbPlayerTestFixture::WaitForPlayerState(const SbPlayerState desired_state,
const SbTime timeout) {
SB_DCHECK(thread_checker_.CalledOnValidThread());
if (HasReceivedPlayerState(desired_state)) {
return;
}
SbTimeMonotonic start = SbTimeGetMonotonicNow();
do {
ASSERT_FALSE(error_occurred_);
ASSERT_NO_FATAL_FAILURE(GetDecodeTargetWhenSupported());
ASSERT_NO_FATAL_FAILURE(WaitAndProcessNextEvent());
if (HasReceivedPlayerState(desired_state)) {
return;
}
} while (SbTimeGetMonotonicNow() - start < timeout);
FAIL() << "WaitForPlayerState() did not receive expected state.";
}
void SbPlayerTestFixture::GetDecodeTargetWhenSupported() {
if (!SbPlayerIsValid(player_)) {
return;
}
#if SB_HAS(GLES2)
fake_graphics_context_provider_.RunOnGlesContextThread([&]() {
ASSERT_TRUE(SbPlayerIsValid(player_));
if (output_mode_ != kSbPlayerOutputModeDecodeToTexture) {
ASSERT_EQ(SbPlayerGetCurrentFrame(player_), kSbDecodeTargetInvalid);
return;
}
ASSERT_EQ(output_mode_, kSbPlayerOutputModeDecodeToTexture);
SbDecodeTarget frame = SbPlayerGetCurrentFrame(player_);
if (SbDecodeTargetIsValid(frame)) {
SbDecodeTargetRelease(frame);
}
});
#endif // SB_HAS(GLES2)
}
void SbPlayerTestFixture::AssertPlayerStateIsValid(SbPlayerState state) const {
// Note: it is possible to receive the same state that has been previously
// received in the case of multiple Seek() calls. Prior to any Seek commands
// issued in this test, we should reset the |player_state_set_| member.
ASSERT_FALSE(HasReceivedPlayerState(state));
switch (state) {
case kSbPlayerStateInitialized:
// No other states have been received before getting Initialized.
ASSERT_TRUE(player_state_set_.empty());
return;
case kSbPlayerStatePrerolling:
ASSERT_TRUE(HasReceivedPlayerState(kSbPlayerStateInitialized));
ASSERT_FALSE(HasReceivedPlayerState(kSbPlayerStateDestroyed));
return;
case kSbPlayerStatePresenting:
ASSERT_TRUE(HasReceivedPlayerState(kSbPlayerStateInitialized));
ASSERT_TRUE(HasReceivedPlayerState(kSbPlayerStatePrerolling));
ASSERT_FALSE(HasReceivedPlayerState(kSbPlayerStateDestroyed));
return;
case kSbPlayerStateEndOfStream:
if (audio_dmp_reader_) {
ASSERT_TRUE(audio_end_of_stream_written_);
}
if (video_dmp_reader_) {
ASSERT_TRUE(video_end_of_stream_written_);
}
ASSERT_TRUE(HasReceivedPlayerState(kSbPlayerStateInitialized));
ASSERT_TRUE(HasReceivedPlayerState(kSbPlayerStatePrerolling));
ASSERT_FALSE(HasReceivedPlayerState(kSbPlayerStateDestroyed));
return;
case kSbPlayerStateDestroyed:
// Nothing stops the user of the player from destroying the player during
// any of the previous states.
ASSERT_TRUE(destroy_player_called_);
return;
}
FAIL() << "Received an invalid SbPlayerState.";
}
} // namespace nplb
} // namespace starboard