blob: 425d3cd6ae72ba7dd35d50e51b61fa47cfab0310 [file] [log] [blame]
// Copyright 2016 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 "cobalt/media/base/sbplayer_bridge.h"
#include <algorithm>
#include <iomanip>
#include <string>
#include <vector>
#include "base/bind.h"
#include "base/compiler_specific.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/trace_event/trace_event.h"
#include "cobalt/base/statistics.h"
#include "cobalt/media/base/format_support_query_metrics.h"
#include "media/base/starboard_utils.h"
#include "starboard/common/media.h"
#include "starboard/common/player.h"
#include "starboard/common/string.h"
#include "starboard/configuration.h"
#include "starboard/extension/player_set_max_video_input_size.h"
#include "starboard/memory.h"
#include "starboard/once.h"
namespace cobalt {
namespace media {
namespace {
using base::Time;
using base::TimeDelta;
using starboard::FormatString;
using starboard::GetPlayerOutputModeName;
class StatisticsWrapper {
public:
static StatisticsWrapper* GetInstance();
base::Statistics<int64_t, int, 1024> startup_latency{
"Media.PlaybackStartupLatency"};
};
void SetStreamInfo(
const SbMediaAudioStreamInfo& stream_info,
CobaltExtensionEnhancedAudioMediaAudioSampleInfo* sample_info) {
DCHECK(sample_info);
sample_info->stream_info.codec = stream_info.codec;
sample_info->stream_info.mime = stream_info.mime;
sample_info->stream_info.number_of_channels = stream_info.number_of_channels;
sample_info->stream_info.samples_per_second = stream_info.samples_per_second;
sample_info->stream_info.bits_per_sample = stream_info.bits_per_sample;
sample_info->stream_info.audio_specific_config_size =
stream_info.audio_specific_config_size;
sample_info->stream_info.audio_specific_config =
stream_info.audio_specific_config;
}
void SetStreamInfo(const SbMediaAudioStreamInfo& stream_info,
SbMediaAudioSampleInfo* sample_info) {
DCHECK(sample_info);
#if SB_API_VERSION >= 15
sample_info->stream_info = stream_info;
#else // SB_API_VERSION >= 15
*sample_info = stream_info;
#endif // SB_API_VERSION >= 15}
}
void SetStreamInfo(
const SbMediaVideoStreamInfo& stream_info,
CobaltExtensionEnhancedAudioMediaVideoSampleInfo* sample_info) {
DCHECK(sample_info);
sample_info->stream_info.codec = stream_info.codec;
sample_info->stream_info.mime = stream_info.mime;
sample_info->stream_info.max_video_capabilities =
stream_info.max_video_capabilities;
sample_info->stream_info.frame_width = stream_info.frame_width;
sample_info->stream_info.frame_height = stream_info.frame_height;
}
void SetStreamInfo(const SbMediaVideoStreamInfo& stream_info,
SbMediaVideoSampleInfo* sample_info) {
DCHECK(sample_info);
#if SB_API_VERSION >= 15
sample_info->stream_info = stream_info;
#else // SB_API_VERSION >= 15
*sample_info = stream_info;
#endif // SB_API_VERSION >= 15}
}
void SetDiscardPadding(
const ::media::DecoderBuffer::DiscardPadding& discard_padding,
CobaltExtensionEnhancedAudioMediaAudioSampleInfo* sample_info) {
DCHECK(sample_info);
sample_info->discarded_duration_from_front =
discard_padding.first.InMicroseconds();
sample_info->discarded_duration_from_back =
discard_padding.second.InMicroseconds();
}
void SetDiscardPadding(
const ::media::DecoderBuffer::DiscardPadding& discard_padding,
SbMediaAudioSampleInfo* sample_info) {
DCHECK(sample_info);
#if SB_API_VERSION >= 15
sample_info->discarded_duration_from_front =
discard_padding.first.InMicroseconds();
sample_info->discarded_duration_from_back =
discard_padding.second.InMicroseconds();
#endif // SB_API_VERSION >= 15}
}
} // namespace
SB_ONCE_INITIALIZE_FUNCTION(StatisticsWrapper, StatisticsWrapper::GetInstance);
SbPlayerBridge::CallbackHelper::CallbackHelper(SbPlayerBridge* player_bridge)
: player_bridge_(player_bridge) {}
void SbPlayerBridge::CallbackHelper::ClearDecoderBufferCache() {
base::AutoLock auto_lock(lock_);
if (player_bridge_) {
player_bridge_->ClearDecoderBufferCache();
}
}
void SbPlayerBridge::CallbackHelper::OnDecoderStatus(SbPlayer player,
SbMediaType type,
SbPlayerDecoderState state,
int ticket) {
base::AutoLock auto_lock(lock_);
if (player_bridge_) {
player_bridge_->OnDecoderStatus(player, type, state, ticket);
}
}
void SbPlayerBridge::CallbackHelper::OnPlayerStatus(SbPlayer player,
SbPlayerState state,
int ticket) {
base::AutoLock auto_lock(lock_);
if (player_bridge_) {
player_bridge_->OnPlayerStatus(player, state, ticket);
}
}
void SbPlayerBridge::CallbackHelper::OnPlayerError(SbPlayer player,
SbPlayerError error,
const std::string& message) {
base::AutoLock auto_lock(lock_);
if (player_bridge_) {
player_bridge_->OnPlayerError(player, error, message);
}
}
void SbPlayerBridge::CallbackHelper::OnDeallocateSample(
const void* sample_buffer) {
base::AutoLock auto_lock(lock_);
if (player_bridge_) {
player_bridge_->OnDeallocateSample(sample_buffer);
}
}
void SbPlayerBridge::CallbackHelper::ResetPlayer() {
base::AutoLock auto_lock(lock_);
player_bridge_ = NULL;
}
#if SB_HAS(PLAYER_WITH_URL)
SbPlayerBridge::SbPlayerBridge(
SbPlayerInterface* interface,
const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
const std::string& url, SbWindow window, Host* host,
SbPlayerSetBoundsHelper* set_bounds_helper, bool allow_resume_after_suspend,
SbPlayerOutputMode default_output_mode,
const OnEncryptedMediaInitDataEncounteredCB&
on_encrypted_media_init_data_encountered_cb,
DecodeTargetProvider* const decode_target_provider,
std::string pipeline_identifier)
: url_(url),
sbplayer_interface_(interface),
task_runner_(task_runner),
callback_helper_(
new CallbackHelper(ALLOW_THIS_IN_INITIALIZER_LIST(this))),
window_(window),
host_(host),
set_bounds_helper_(set_bounds_helper),
allow_resume_after_suspend_(allow_resume_after_suspend),
on_encrypted_media_init_data_encountered_cb_(
on_encrypted_media_init_data_encountered_cb),
decode_target_provider_(decode_target_provider),
cval_stats_(&interface->cval_stats_),
pipeline_identifier_(pipeline_identifier),
is_url_based_(true) {
DCHECK(host_);
DCHECK(set_bounds_helper_);
output_mode_ = ComputeSbUrlPlayerOutputMode(default_output_mode);
CreateUrlPlayer(url_);
task_runner->PostTask(
FROM_HERE,
base::Bind(&SbPlayerBridge::CallbackHelper::ClearDecoderBufferCache,
callback_helper_));
}
#endif // SB_HAS(PLAYER_WITH_URL)
SbPlayerBridge::SbPlayerBridge(
SbPlayerInterface* interface,
const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
const GetDecodeTargetGraphicsContextProviderFunc&
get_decode_target_graphics_context_provider_func,
const AudioDecoderConfig& audio_config, const std::string& audio_mime_type,
const VideoDecoderConfig& video_config, const std::string& video_mime_type,
SbWindow window, SbDrmSystem drm_system, Host* host,
SbPlayerSetBoundsHelper* set_bounds_helper, bool allow_resume_after_suspend,
SbPlayerOutputMode default_output_mode,
DecodeTargetProvider* const decode_target_provider,
const std::string& max_video_capabilities, int max_video_input_size,
std::string pipeline_identifier)
: sbplayer_interface_(interface),
task_runner_(task_runner),
get_decode_target_graphics_context_provider_func_(
get_decode_target_graphics_context_provider_func),
callback_helper_(
new CallbackHelper(ALLOW_THIS_IN_INITIALIZER_LIST(this))),
window_(window),
drm_system_(drm_system),
host_(host),
set_bounds_helper_(set_bounds_helper),
allow_resume_after_suspend_(allow_resume_after_suspend),
audio_config_(audio_config),
video_config_(video_config),
decode_target_provider_(decode_target_provider),
max_video_capabilities_(max_video_capabilities),
max_video_input_size_(max_video_input_size),
cval_stats_(&interface->cval_stats_),
pipeline_identifier_(pipeline_identifier)
#if SB_HAS(PLAYER_WITH_URL)
,
is_url_based_(false)
#endif // SB_HAS(PLAYER_WITH_URL
{
DCHECK(!get_decode_target_graphics_context_provider_func_.is_null());
DCHECK(audio_config.IsValidConfig() || video_config.IsValidConfig());
DCHECK(host_);
DCHECK(set_bounds_helper_);
DCHECK(decode_target_provider_);
audio_stream_info_.codec = kSbMediaAudioCodecNone;
video_stream_info_.codec = kSbMediaVideoCodecNone;
if (audio_config.IsValidConfig()) {
UpdateAudioConfig(audio_config, audio_mime_type);
}
if (video_config.IsValidConfig()) {
UpdateVideoConfig(video_config, video_mime_type);
}
output_mode_ = ComputeSbPlayerOutputMode(default_output_mode);
CreatePlayer();
if (SbPlayerIsValid(player_)) {
task_runner->PostTask(
FROM_HERE,
base::Bind(&SbPlayerBridge::CallbackHelper::ClearDecoderBufferCache,
callback_helper_));
}
}
SbPlayerBridge::~SbPlayerBridge() {
DCHECK(task_runner_->BelongsToCurrentThread());
callback_helper_->ResetPlayer();
set_bounds_helper_->SetPlayerBridge(NULL);
decode_target_provider_->SetOutputMode(
DecodeTargetProvider::kOutputModeInvalid);
decode_target_provider_->ResetGetCurrentSbDecodeTargetFunction();
if (SbPlayerIsValid(player_)) {
cval_stats_->StartTimer(MediaTiming::SbPlayerDestroy, pipeline_identifier_);
sbplayer_interface_->Destroy(player_);
cval_stats_->StopTimer(MediaTiming::SbPlayerDestroy, pipeline_identifier_);
}
}
void SbPlayerBridge::UpdateAudioConfig(const AudioDecoderConfig& audio_config,
const std::string& mime_type) {
DCHECK(task_runner_->BelongsToCurrentThread());
DCHECK(audio_config.IsValidConfig());
LOG(INFO) << "Updated AudioDecoderConfig -- "
<< audio_config.AsHumanReadableString();
audio_config_ = audio_config;
audio_mime_type_ = mime_type;
audio_stream_info_ = MediaAudioConfigToSbMediaAudioStreamInfo(
audio_config_, audio_mime_type_.c_str());
LOG(INFO) << "Converted to SbMediaAudioStreamInfo -- " << audio_stream_info_;
}
void SbPlayerBridge::UpdateVideoConfig(const VideoDecoderConfig& video_config,
const std::string& mime_type) {
DCHECK(task_runner_->BelongsToCurrentThread());
DCHECK(video_config.IsValidConfig());
LOG(INFO) << "Updated VideoDecoderConfig -- "
<< video_config.AsHumanReadableString();
video_config_ = video_config;
video_stream_info_.frame_width =
static_cast<int>(video_config_.natural_size().width());
video_stream_info_.frame_height =
static_cast<int>(video_config_.natural_size().height());
video_stream_info_.codec =
MediaVideoCodecToSbMediaVideoCodec(video_config_.codec());
video_stream_info_.color_metadata =
MediaToSbMediaColorMetadata(video_config_.color_space_info(),
video_config_.hdr_metadata(), mime_type);
video_mime_type_ = mime_type;
video_stream_info_.mime = video_mime_type_.c_str();
video_stream_info_.max_video_capabilities = max_video_capabilities_.c_str();
LOG(INFO) << "Converted to SbMediaVideoStreamInfo -- " << video_stream_info_;
}
void SbPlayerBridge::WriteBuffers(
DemuxerStream::Type type,
const std::vector<scoped_refptr<DecoderBuffer>>& buffers) {
DCHECK(task_runner_->BelongsToCurrentThread());
#if SB_HAS(PLAYER_WITH_URL)
DCHECK(!is_url_based_);
#endif // SB_HAS(PLAYER_WITH_URL)
if (allow_resume_after_suspend_) {
if (type == DemuxerStream::Type::AUDIO) {
decoder_buffer_cache_.AddBuffers(buffers, audio_stream_info_);
} else {
DCHECK(type == DemuxerStream::Type::VIDEO);
decoder_buffer_cache_.AddBuffers(buffers, video_stream_info_);
}
if (state_ != kSuspended) {
WriteNextBuffersFromCache(type, buffers.size());
}
return;
}
if (sbplayer_interface_->IsEnhancedAudioExtensionEnabled()) {
WriteBuffersInternal<CobaltExtensionEnhancedAudioPlayerSampleInfo>(
type, buffers, &audio_stream_info_, &video_stream_info_);
} else {
WriteBuffersInternal<SbPlayerSampleInfo>(type, buffers, &audio_stream_info_,
&video_stream_info_);
}
}
void SbPlayerBridge::SetBounds(int z_index, const gfx::Rect& rect) {
base::AutoLock auto_lock(lock_);
set_bounds_z_index_ = z_index;
set_bounds_rect_ = rect;
if (state_ == kSuspended) {
return;
}
UpdateBounds_Locked();
}
void SbPlayerBridge::PrepareForSeek() {
DCHECK(task_runner_->BelongsToCurrentThread());
seek_pending_ = true;
if (state_ == kSuspended) {
return;
}
++ticket_;
sbplayer_interface_->SetPlaybackRate(player_, 0.f);
}
void SbPlayerBridge::Seek(TimeDelta time) {
DCHECK(task_runner_->BelongsToCurrentThread());
decoder_buffer_cache_.ClearAll();
seek_pending_ = false;
pending_audio_eos_buffer_ = false;
pending_video_eos_buffer_ = false;
if (state_ == kSuspended) {
preroll_timestamp_ = time;
return;
}
// If a seek happens during resuming, the pipeline will write samples from the
// seek target time again so resuming can be aborted.
if (state_ == kResuming) {
state_ = kPlaying;
}
DCHECK(SbPlayerIsValid(player_));
++ticket_;
sbplayer_interface_->Seek(player_, time, ticket_);
sbplayer_interface_->SetPlaybackRate(player_, playback_rate_);
}
void SbPlayerBridge::SetVolume(float volume) {
DCHECK(task_runner_->BelongsToCurrentThread());
volume_ = volume;
if (state_ == kSuspended) {
return;
}
DCHECK(SbPlayerIsValid(player_));
sbplayer_interface_->SetVolume(player_, volume);
}
void SbPlayerBridge::SetPlaybackRate(double playback_rate) {
DCHECK(task_runner_->BelongsToCurrentThread());
playback_rate_ = playback_rate;
if (state_ == kSuspended) {
return;
}
if (seek_pending_) {
return;
}
sbplayer_interface_->SetPlaybackRate(player_, playback_rate);
}
void SbPlayerBridge::GetInfo(uint32* video_frames_decoded,
uint32* video_frames_dropped,
TimeDelta* media_time) {
DCHECK(video_frames_decoded || video_frames_dropped || media_time);
base::AutoLock auto_lock(lock_);
GetInfo_Locked(video_frames_decoded, video_frames_dropped, media_time);
}
#if SB_API_VERSION >= 15
std::vector<SbMediaAudioConfiguration>
SbPlayerBridge::GetAudioConfigurations() {
base::AutoLock auto_lock(lock_);
if (!SbPlayerIsValid(player_)) {
return std::vector<SbMediaAudioConfiguration>();
}
std::vector<SbMediaAudioConfiguration> configurations;
// Set a limit to avoid infinite loop.
constexpr int kMaxAudioConfigurations = 32;
for (int i = 0; i < kMaxAudioConfigurations; ++i) {
SbMediaAudioConfiguration configuration;
if (!sbplayer_interface_->GetAudioConfiguration(player_, i,
&configuration)) {
break;
}
configurations.push_back(configuration);
}
LOG_IF(WARNING, configurations.empty())
<< "Failed to find any audio configurations.";
return configurations;
}
#endif // SB_API_VERSION >= 15
#if SB_HAS(PLAYER_WITH_URL)
void SbPlayerBridge::GetUrlPlayerBufferedTimeRanges(
TimeDelta* buffer_start_time, TimeDelta* buffer_length_time) {
DCHECK(buffer_start_time || buffer_length_time);
DCHECK(is_url_based_);
if (state_ == kSuspended) {
*buffer_start_time = TimeDelta();
*buffer_length_time = TimeDelta();
return;
}
DCHECK(SbPlayerIsValid(player_));
SbUrlPlayerExtraInfo url_player_info;
sbplayer_interface_->GetUrlPlayerExtraInfo(player_, &url_player_info);
if (buffer_start_time) {
*buffer_start_time =
TimeDelta::FromMicroseconds(url_player_info.buffer_start_timestamp);
}
if (buffer_length_time) {
*buffer_length_time =
TimeDelta::FromMicroseconds(url_player_info.buffer_duration);
}
}
void SbPlayerBridge::GetVideoResolution(int* frame_width, int* frame_height) {
DCHECK(frame_width);
DCHECK(frame_height);
DCHECK(is_url_based_);
if (state_ == kSuspended) {
*frame_width = video_stream_info_.frame_width;
*frame_height = video_stream_info_.frame_height;
return;
}
DCHECK(SbPlayerIsValid(player_));
#if SB_API_VERSION >= 15
SbPlayerInfo out_player_info;
#else // SB_API_VERSION >= 15
SbPlayerInfo2 out_player_info;
#endif // SB_API_VERSION >= 15
sbplayer_interface_->GetInfo(player_, &out_player_info);
video_stream_info_.frame_width = out_player_info.frame_width;
video_stream_info_.frame_height = out_player_info.frame_height;
*frame_width = video_stream_info_.frame_width;
*frame_height = video_stream_info_.frame_height;
}
TimeDelta SbPlayerBridge::GetDuration() {
DCHECK(is_url_based_);
if (state_ == kSuspended) {
return TimeDelta();
}
DCHECK(SbPlayerIsValid(player_));
#if SB_API_VERSION >= 15
SbPlayerInfo info;
#else // SB_API_VERSION >= 15
SbPlayerInfo2 info;
#endif // SB_API_VERSION >= 15
sbplayer_interface_->GetInfo(player_, &info);
if (info.duration == SB_PLAYER_NO_DURATION) {
// URL-based player may not have loaded asset yet, so map no duration to 0.
return TimeDelta();
}
return TimeDelta::FromMicroseconds(info.duration);
}
TimeDelta SbPlayerBridge::GetStartDate() {
DCHECK(is_url_based_);
if (state_ == kSuspended) {
return TimeDelta();
}
DCHECK(SbPlayerIsValid(player_));
#if SB_API_VERSION >= 15
SbPlayerInfo info;
#else // SB_API_VERSION >= 15
SbPlayerInfo2 info;
#endif // SB_API_VERSION >= 15
sbplayer_interface_->GetInfo(player_, &info);
return TimeDelta::FromMicroseconds(info.start_date);
}
void SbPlayerBridge::SetDrmSystem(SbDrmSystem drm_system) {
DCHECK(is_url_based_);
drm_system_ = drm_system;
sbplayer_interface_->SetUrlPlayerDrmSystem(player_, drm_system);
}
#endif // SB_HAS(PLAYER_WITH_URL)
void SbPlayerBridge::Suspend() {
DCHECK(task_runner_->BelongsToCurrentThread());
// Check if the player is already suspended.
if (state_ == kSuspended) {
return;
}
DCHECK(SbPlayerIsValid(player_));
sbplayer_interface_->SetPlaybackRate(player_, 0.0);
set_bounds_helper_->SetPlayerBridge(NULL);
base::AutoLock auto_lock(lock_);
GetInfo_Locked(&cached_video_frames_decoded_, &cached_video_frames_dropped_,
&preroll_timestamp_);
state_ = kSuspended;
decode_target_provider_->SetOutputMode(
DecodeTargetProvider::kOutputModeInvalid);
decode_target_provider_->ResetGetCurrentSbDecodeTargetFunction();
cval_stats_->StartTimer(MediaTiming::SbPlayerDestroy, pipeline_identifier_);
sbplayer_interface_->Destroy(player_);
cval_stats_->StopTimer(MediaTiming::SbPlayerDestroy, pipeline_identifier_);
player_ = kSbPlayerInvalid;
}
void SbPlayerBridge::Resume(SbWindow window) {
DCHECK(task_runner_->BelongsToCurrentThread());
window_ = window;
// Check if the player is already resumed.
if (state_ != kSuspended) {
DCHECK(SbPlayerIsValid(player_));
return;
}
decoder_buffer_cache_.StartResuming();
#if SB_HAS(PLAYER_WITH_URL)
if (is_url_based_) {
CreateUrlPlayer(url_);
if (SbDrmSystemIsValid(drm_system_)) {
sbplayer_interface_->SetUrlPlayerDrmSystem(player_, drm_system_);
}
} else {
CreatePlayer();
}
#else // SB_HAS(PLAYER_WITH_URL)
CreatePlayer();
#endif // SB_HAS(PLAYER_WITH_URL)
if (SbPlayerIsValid(player_)) {
base::AutoLock auto_lock(lock_);
state_ = kResuming;
UpdateBounds_Locked();
}
}
namespace {
DecodeTargetProvider::OutputMode ToVideoFrameProviderOutputMode(
SbPlayerOutputMode output_mode) {
switch (output_mode) {
case kSbPlayerOutputModeDecodeToTexture:
return DecodeTargetProvider::kOutputModeDecodeToTexture;
case kSbPlayerOutputModePunchOut:
return DecodeTargetProvider::kOutputModePunchOut;
case kSbPlayerOutputModeInvalid:
return DecodeTargetProvider::kOutputModeInvalid;
}
NOTREACHED();
return DecodeTargetProvider::kOutputModeInvalid;
}
} // namespace
#if SB_HAS(PLAYER_WITH_URL)
// static
void SbPlayerBridge::EncryptedMediaInitDataEncounteredCB(
SbPlayer player, void* context, const char* init_data_type,
const unsigned char* init_data, unsigned int init_data_length) {
SbPlayerBridge* helper = static_cast<SbPlayerBridge*>(context);
DCHECK(!helper->on_encrypted_media_init_data_encountered_cb_.is_null());
// TODO: Use callback_helper here.
helper->on_encrypted_media_init_data_encountered_cb_.Run(
init_data_type, init_data, init_data_length);
}
void SbPlayerBridge::CreateUrlPlayer(const std::string& url) {
TRACE_EVENT0("cobalt::media", "SbPlayerBridge::CreateUrlPlayer");
DCHECK(task_runner_->BelongsToCurrentThread());
DCHECK(!on_encrypted_media_init_data_encountered_cb_.is_null());
LOG(INFO) << "CreateUrlPlayer passed url " << url;
if (max_video_capabilities_.empty()) {
FormatSupportQueryMetrics::PrintAndResetMetrics();
}
player_creation_time_ = Time::Now();
cval_stats_->StartTimer(MediaTiming::SbPlayerCreate, pipeline_identifier_);
player_ = sbplayer_interface_->CreateUrlPlayer(
url.c_str(), window_, &SbPlayerBridge::PlayerStatusCB,
&SbPlayerBridge::EncryptedMediaInitDataEncounteredCB,
&SbPlayerBridge::PlayerErrorCB, this);
cval_stats_->StopTimer(MediaTiming::SbPlayerCreate, pipeline_identifier_);
DCHECK(SbPlayerIsValid(player_));
if (output_mode_ == kSbPlayerOutputModeDecodeToTexture) {
// If the player is setup to decode to texture, then provide Cobalt with
// a method of querying that texture.
decode_target_provider_->SetGetCurrentSbDecodeTargetFunction(base::Bind(
&SbPlayerBridge::GetCurrentSbDecodeTarget, base::Unretained(this)));
LOG(INFO) << "Playing in decode-to-texture mode.";
} else {
LOG(INFO) << "Playing in punch-out mode.";
}
decode_target_provider_->SetOutputMode(
ToVideoFrameProviderOutputMode(output_mode_));
set_bounds_helper_->SetPlayerBridge(this);
base::AutoLock auto_lock(lock_);
UpdateBounds_Locked();
}
#endif // SB_HAS(PLAYER_WITH_URL)
void SbPlayerBridge::CreatePlayer() {
TRACE_EVENT0("cobalt::media", "SbPlayerBridge::CreatePlayer");
DCHECK(task_runner_->BelongsToCurrentThread());
bool is_visible = SbWindowIsValid(window_);
bool has_audio = audio_stream_info_.codec != kSbMediaAudioCodecNone;
is_creating_player_ = true;
if (output_mode_ == kSbPlayerOutputModeInvalid) {
PlayerErrorCB(kSbPlayerInvalid, this, kSbPlayerErrorDecode,
"Invalid output mode returned by "
"SbPlayerBridge::ComputeSbPlayerOutputMode()");
is_creating_player_ = false;
player_ = kSbPlayerInvalid;
return;
}
if (max_video_capabilities_.empty()) {
FormatSupportQueryMetrics::PrintAndResetMetrics();
}
player_creation_time_ = Time::Now();
SbPlayerCreationParam creation_param = {};
creation_param.drm_system = drm_system_;
#if SB_API_VERSION >= 15
creation_param.audio_stream_info = audio_stream_info_;
creation_param.video_stream_info = video_stream_info_;
#else // SB_API_VERSION >= 15
creation_param.audio_sample_info = audio_stream_info_;
creation_param.video_sample_info = video_stream_info_;
#endif // SB_API_VERSION >= 15
// TODO: This is temporary for supporting background media playback.
// Need to be removed with media refactor.
if (!is_visible) {
#if SB_API_VERSION >= 15
creation_param.video_stream_info.codec = kSbMediaVideoCodecNone;
#else // SB_API_VERSION >= 15
creation_param.video_sample_info.codec = kSbMediaVideoCodecNone;
#endif // SB_API_VERSION >= 15
}
creation_param.output_mode = output_mode_;
DCHECK_EQ(sbplayer_interface_->GetPreferredOutputMode(&creation_param),
output_mode_);
cval_stats_->StartTimer(MediaTiming::SbPlayerCreate, pipeline_identifier_);
const StarboardExtensionPlayerSetMaxVideoInputSizeApi*
player_set_max_video_input_size_extension =
static_cast<const StarboardExtensionPlayerSetMaxVideoInputSizeApi*>(
SbSystemGetExtension(
kStarboardExtensionPlayerSetMaxVideoInputSizeName));
if (player_set_max_video_input_size_extension &&
strcmp(player_set_max_video_input_size_extension->name,
kStarboardExtensionPlayerSetMaxVideoInputSizeName) == 0 &&
player_set_max_video_input_size_extension->version >= 1) {
player_set_max_video_input_size_extension
->SetMaxVideoInputSizeForCurrentThread(max_video_input_size_);
}
player_ = sbplayer_interface_->Create(
window_, &creation_param, &SbPlayerBridge::DeallocateSampleCB,
&SbPlayerBridge::DecoderStatusCB, &SbPlayerBridge::PlayerStatusCB,
&SbPlayerBridge::PlayerErrorCB, this,
get_decode_target_graphics_context_provider_func_.Run());
cval_stats_->StopTimer(MediaTiming::SbPlayerCreate, pipeline_identifier_);
is_creating_player_ = false;
if (!SbPlayerIsValid(player_)) {
return;
}
if (output_mode_ == kSbPlayerOutputModeDecodeToTexture) {
// If the player is setup to decode to texture, then provide Cobalt with
// a method of querying that texture.
decode_target_provider_->SetGetCurrentSbDecodeTargetFunction(base::Bind(
&SbPlayerBridge::GetCurrentSbDecodeTarget, base::Unretained(this)));
LOG(INFO) << "Playing in decode-to-texture mode.";
} else {
LOG(INFO) << "Playing in punch-out mode.";
}
decode_target_provider_->SetOutputMode(
ToVideoFrameProviderOutputMode(output_mode_));
set_bounds_helper_->SetPlayerBridge(this);
base::AutoLock auto_lock(lock_);
UpdateBounds_Locked();
}
void SbPlayerBridge::WriteNextBuffersFromCache(DemuxerStream::Type type,
int max_buffers_per_write) {
DCHECK(state_ != kSuspended);
#if SB_HAS(PLAYER_WITH_URL)
DCHECK(!is_url_based_);
#endif // SB_HAS(PLAYER_WITH_URL)
DCHECK(SbPlayerIsValid(player_));
if (type == DemuxerStream::AUDIO) {
std::vector<scoped_refptr<DecoderBuffer>> buffers;
SbMediaAudioStreamInfo stream_info;
decoder_buffer_cache_.ReadBuffers(&buffers, max_buffers_per_write,
&stream_info);
if (buffers.size() > 0) {
if (sbplayer_interface_->IsEnhancedAudioExtensionEnabled()) {
WriteBuffersInternal<CobaltExtensionEnhancedAudioPlayerSampleInfo>(
type, buffers, &stream_info, nullptr);
} else {
WriteBuffersInternal<SbPlayerSampleInfo>(type, buffers, &stream_info,
nullptr);
}
}
} else {
DCHECK_EQ(type, DemuxerStream::VIDEO);
std::vector<scoped_refptr<DecoderBuffer>> buffers;
SbMediaVideoStreamInfo stream_info;
decoder_buffer_cache_.ReadBuffers(&buffers, max_buffers_per_write,
&stream_info);
if (buffers.size() > 0) {
if (sbplayer_interface_->IsEnhancedAudioExtensionEnabled()) {
WriteBuffersInternal<CobaltExtensionEnhancedAudioPlayerSampleInfo>(
type, buffers, nullptr, &stream_info);
} else {
WriteBuffersInternal<SbPlayerSampleInfo>(type, buffers, nullptr,
&stream_info);
}
}
}
}
template <typename PlayerSampleInfo>
void SbPlayerBridge::WriteBuffersInternal(
DemuxerStream::Type type,
const std::vector<scoped_refptr<DecoderBuffer>>& buffers,
const SbMediaAudioStreamInfo* audio_stream_info,
const SbMediaVideoStreamInfo* video_stream_info) {
#if SB_HAS(PLAYER_WITH_URL)
DCHECK(!is_url_based_);
#endif // SB_HAS(PLAYER_WITH_URL)
auto sample_type = DemuxerStreamTypeToSbMediaType(type);
if (buffers.size() == 1 && buffers[0]->end_of_stream()) {
sbplayer_interface_->WriteEndOfStream(player_,
DemuxerStreamTypeToSbMediaType(type));
return;
}
std::vector<PlayerSampleInfo> gathered_sbplayer_sample_infos;
std::vector<SbDrmSampleInfo> gathered_sbplayer_sample_infos_drm_info;
std::vector<SbDrmSubSampleMapping>
gathered_sbplayer_sample_infos_subsample_mapping;
std::vector<SbPlayerSampleSideData> gathered_sbplayer_sample_infos_side_data;
gathered_sbplayer_sample_infos.reserve(buffers.size());
gathered_sbplayer_sample_infos_drm_info.reserve(buffers.size());
gathered_sbplayer_sample_infos_subsample_mapping.reserve(buffers.size());
gathered_sbplayer_sample_infos_side_data.reserve(buffers.size());
for (int i = 0; i < buffers.size(); i++) {
const auto& buffer = buffers[i];
if (buffer->end_of_stream()) {
DCHECK_EQ(i, buffers.size() - 1);
if (type == DemuxerStream::AUDIO) {
pending_audio_eos_buffer_ = true;
} else {
pending_video_eos_buffer_ = true;
}
break;
}
DecodingBuffers::iterator iter = decoding_buffers_.find(buffer->data());
if (iter == decoding_buffers_.end()) {
decoding_buffers_[buffer->data()] = std::make_pair(buffer, 1);
} else {
++iter->second.second;
}
if (sample_type == kSbMediaTypeAudio &&
first_audio_sample_time_.is_null()) {
first_audio_sample_time_ = Time::Now();
} else if (sample_type == kSbMediaTypeVideo &&
first_video_sample_time_.is_null()) {
first_video_sample_time_ = Time::Now();
}
gathered_sbplayer_sample_infos_drm_info.push_back(SbDrmSampleInfo());
SbDrmSampleInfo* drm_info = &gathered_sbplayer_sample_infos_drm_info[i];
gathered_sbplayer_sample_infos_subsample_mapping.push_back(
SbDrmSubSampleMapping());
SbDrmSubSampleMapping* subsample_mapping =
&gathered_sbplayer_sample_infos_subsample_mapping[i];
drm_info->subsample_count = 0;
if (buffer->decrypt_config()) {
FillDrmSampleInfo(buffer, drm_info, subsample_mapping);
}
gathered_sbplayer_sample_infos_side_data.push_back(
SbPlayerSampleSideData());
SbPlayerSampleSideData* side_data =
&gathered_sbplayer_sample_infos_side_data[i];
PlayerSampleInfo sample_info = {};
sample_info.type = sample_type;
sample_info.buffer = buffer->data();
sample_info.buffer_size = buffer->data_size();
sample_info.timestamp = buffer->timestamp().InMicroseconds();
if (buffer->side_data_size() > 0) {
// We only support at most one side data currently.
side_data->data = buffer->side_data();
side_data->size = buffer->side_data_size();
sample_info.side_data = side_data;
sample_info.side_data_count = 1;
}
if (sample_type == kSbMediaTypeAudio) {
DCHECK(audio_stream_info);
SetStreamInfo(*audio_stream_info, &sample_info.audio_sample_info);
SetDiscardPadding(buffer->discard_padding(),
&sample_info.audio_sample_info);
} else {
DCHECK(sample_type == kSbMediaTypeVideo);
DCHECK(video_stream_info);
SetStreamInfo(*video_stream_info, &sample_info.video_sample_info);
sample_info.video_sample_info.is_key_frame = buffer->is_key_frame();
}
if (drm_info->subsample_count > 0) {
sample_info.drm_info = drm_info;
} else {
sample_info.drm_info = NULL;
}
gathered_sbplayer_sample_infos.push_back(sample_info);
}
if (!gathered_sbplayer_sample_infos.empty()) {
cval_stats_->StartTimer(MediaTiming::SbPlayerWriteSamples,
pipeline_identifier_);
sbplayer_interface_->WriteSamples(player_, sample_type,
gathered_sbplayer_sample_infos.data(),
gathered_sbplayer_sample_infos.size());
cval_stats_->StopTimer(MediaTiming::SbPlayerWriteSamples,
pipeline_identifier_);
}
}
SbDecodeTarget SbPlayerBridge::GetCurrentSbDecodeTarget() {
return sbplayer_interface_->GetCurrentFrame(player_);
}
SbPlayerOutputMode SbPlayerBridge::GetSbPlayerOutputMode() {
return output_mode_;
}
void SbPlayerBridge::GetInfo_Locked(uint32* video_frames_decoded,
uint32* video_frames_dropped,
TimeDelta* media_time) {
lock_.AssertAcquired();
if (state_ == kSuspended) {
if (video_frames_decoded) {
*video_frames_decoded = cached_video_frames_decoded_;
}
if (video_frames_dropped) {
*video_frames_dropped = cached_video_frames_dropped_;
}
if (media_time) {
*media_time = preroll_timestamp_;
}
return;
}
DCHECK(SbPlayerIsValid(player_));
#if SB_API_VERSION >= 15
SbPlayerInfo info;
#else // SB_API_VERSION >= 15
SbPlayerInfo2 info;
#endif // SB_API_VERSION >= 15
sbplayer_interface_->GetInfo(player_, &info);
if (media_time) {
*media_time = TimeDelta::FromMicroseconds(info.current_media_timestamp);
}
if (video_frames_decoded) {
*video_frames_decoded = info.total_video_frames;
}
if (video_frames_dropped) {
*video_frames_dropped = info.dropped_video_frames;
}
}
void SbPlayerBridge::UpdateBounds_Locked() {
lock_.AssertAcquired();
DCHECK(SbPlayerIsValid(player_));
if (!set_bounds_z_index_ || !set_bounds_rect_) {
return;
}
auto& rect = *set_bounds_rect_;
sbplayer_interface_->SetBounds(player_, *set_bounds_z_index_, rect.x(),
rect.y(), rect.width(), rect.height());
}
void SbPlayerBridge::ClearDecoderBufferCache() {
DCHECK(task_runner_->BelongsToCurrentThread());
if (state_ != kResuming) {
TimeDelta media_time;
GetInfo(NULL, NULL, &media_time);
decoder_buffer_cache_.ClearSegmentsBeforeMediaTime(media_time);
}
task_runner_->PostDelayedTask(
FROM_HERE,
base::Bind(&SbPlayerBridge::CallbackHelper::ClearDecoderBufferCache,
callback_helper_),
TimeDelta::FromMilliseconds(kClearDecoderCacheIntervalInMilliseconds));
}
void SbPlayerBridge::OnDecoderStatus(SbPlayer player, SbMediaType type,
SbPlayerDecoderState state, int ticket) {
#if SB_HAS(PLAYER_WITH_URL)
DCHECK(!is_url_based_);
#endif // SB_HAS(PLAYER_WITH_URL)
DCHECK(task_runner_->BelongsToCurrentThread());
if (player_ != player || ticket != ticket_) {
return;
}
DCHECK_NE(state_, kSuspended);
switch (state) {
case kSbPlayerDecoderStateNeedsData:
break;
}
DemuxerStream::Type stream_type =
::media::SbMediaTypeToDemuxerStreamType(type);
if (stream_type == DemuxerStream::AUDIO && pending_audio_eos_buffer_) {
SbPlayerWriteEndOfStream(player_, type);
pending_audio_eos_buffer_ = false;
return;
} else if (stream_type == DemuxerStream::VIDEO && pending_video_eos_buffer_) {
SbPlayerWriteEndOfStream(player_, type);
pending_video_eos_buffer_ = false;
return;
}
auto max_number_of_samples_to_write =
SbPlayerGetMaximumNumberOfSamplesPerWrite(player_, type);
if (state_ == kResuming) {
if (decoder_buffer_cache_.HasMoreBuffers(stream_type)) {
WriteNextBuffersFromCache(stream_type, max_number_of_samples_to_write);
return;
}
if (!decoder_buffer_cache_.HasMoreBuffers(DemuxerStream::AUDIO) &&
!decoder_buffer_cache_.HasMoreBuffers(DemuxerStream::VIDEO)) {
state_ = kPlaying;
}
}
host_->OnNeedData(stream_type, max_number_of_samples_to_write);
}
void SbPlayerBridge::OnPlayerStatus(SbPlayer player, SbPlayerState state,
int ticket) {
TRACE_EVENT1("cobalt::media", "SbPlayerBridge::OnPlayerStatus", "state",
state);
DCHECK(task_runner_->BelongsToCurrentThread());
if (player_ != player) {
return;
}
DCHECK_NE(state_, kSuspended);
if (ticket != SB_PLAYER_INITIAL_TICKET && ticket != ticket_) {
return;
}
if (state == kSbPlayerStateInitialized) {
if (ticket_ == SB_PLAYER_INITIAL_TICKET) {
++ticket_;
}
if (sb_player_state_initialized_time_.is_null()) {
sb_player_state_initialized_time_ = Time::Now();
}
sbplayer_interface_->Seek(player_, preroll_timestamp_, ticket_);
SetVolume(volume_);
sbplayer_interface_->SetPlaybackRate(player_, playback_rate_);
return;
}
if (state == kSbPlayerStatePrerolling &&
sb_player_state_prerolling_time_.is_null()) {
sb_player_state_prerolling_time_ = Time::Now();
} else if (state == kSbPlayerStatePresenting &&
sb_player_state_presenting_time_.is_null()) {
sb_player_state_presenting_time_ = Time::Now();
#if !defined(COBALT_BUILD_TYPE_GOLD)
LogStartupLatency();
#endif // !defined(COBALT_BUILD_TYPE_GOLD)
}
host_->OnPlayerStatus(state);
}
void SbPlayerBridge::OnPlayerError(SbPlayer player, SbPlayerError error,
const std::string& message) {
DCHECK(task_runner_->BelongsToCurrentThread());
if (player_ != player) {
return;
}
host_->OnPlayerError(error, message);
}
void SbPlayerBridge::OnDeallocateSample(const void* sample_buffer) {
#if SB_HAS(PLAYER_WITH_URL)
DCHECK(!is_url_based_);
#endif // SB_HAS(PLAYER_WITH_URL)
DCHECK(task_runner_->BelongsToCurrentThread());
DecodingBuffers::iterator iter = decoding_buffers_.find(sample_buffer);
DCHECK(iter != decoding_buffers_.end());
if (iter == decoding_buffers_.end()) {
LOG(ERROR) << "SbPlayerBridge::OnDeallocateSample encounters unknown "
<< "sample_buffer " << sample_buffer;
return;
}
--iter->second.second;
if (iter->second.second == 0) {
decoding_buffers_.erase(iter);
}
}
bool SbPlayerBridge::TryToSetPlayerCreationErrorMessage(
const std::string& message) {
DCHECK(task_runner_->BelongsToCurrentThread());
if (is_creating_player_) {
player_creation_error_message_ = message;
return true;
}
LOG(INFO) << "TryToSetPlayerCreationErrorMessage() "
"is called when |is_creating_player_| "
"is false. Error message is ignored.";
return false;
}
// static
void SbPlayerBridge::DecoderStatusCB(SbPlayer player, void* context,
SbMediaType type,
SbPlayerDecoderState state, int ticket) {
SbPlayerBridge* helper = static_cast<SbPlayerBridge*>(context);
helper->task_runner_->PostTask(
FROM_HERE,
base::Bind(&SbPlayerBridge::CallbackHelper::OnDecoderStatus,
helper->callback_helper_, player, type, state, ticket));
}
// static
void SbPlayerBridge::PlayerStatusCB(SbPlayer player, void* context,
SbPlayerState state, int ticket) {
SbPlayerBridge* helper = static_cast<SbPlayerBridge*>(context);
helper->task_runner_->PostTask(
FROM_HERE, base::Bind(&SbPlayerBridge::CallbackHelper::OnPlayerStatus,
helper->callback_helper_, player, state, ticket));
}
// static
void SbPlayerBridge::PlayerErrorCB(SbPlayer player, void* context,
SbPlayerError error, const char* message) {
SbPlayerBridge* helper = static_cast<SbPlayerBridge*>(context);
if (player == kSbPlayerInvalid) {
// TODO: Simplify by combining the functionality of
// TryToSetPlayerCreationErrorMessage() with OnPlayerError().
if (helper->TryToSetPlayerCreationErrorMessage(message)) {
return;
}
}
helper->task_runner_->PostTask(
FROM_HERE, base::Bind(&SbPlayerBridge::CallbackHelper::OnPlayerError,
helper->callback_helper_, player, error,
message ? std::string(message) : ""));
}
// static
void SbPlayerBridge::DeallocateSampleCB(SbPlayer player, void* context,
const void* sample_buffer) {
SbPlayerBridge* helper = static_cast<SbPlayerBridge*>(context);
helper->task_runner_->PostTask(
FROM_HERE, base::Bind(&SbPlayerBridge::CallbackHelper::OnDeallocateSample,
helper->callback_helper_, sample_buffer));
}
#if SB_HAS(PLAYER_WITH_URL)
SbPlayerOutputMode SbPlayerBridge::ComputeSbUrlPlayerOutputMode(
SbPlayerOutputMode default_output_mode) {
// Try to choose the output mode according to the passed in value of
// |prefer_decode_to_texture|. If the preferred output mode is unavailable
// though, fallback to an output mode that is available.
SbPlayerOutputMode output_mode = kSbPlayerOutputModeInvalid;
if (sbplayer_interface_->GetUrlPlayerOutputModeSupported(
kSbPlayerOutputModePunchOut)) {
output_mode = kSbPlayerOutputModePunchOut;
}
if ((default_output_mode == kSbPlayerOutputModeDecodeToTexture ||
output_mode == kSbPlayerOutputModeInvalid) &&
sbplayer_interface_->GetUrlPlayerOutputModeSupported(
kSbPlayerOutputModeDecodeToTexture)) {
output_mode = kSbPlayerOutputModeDecodeToTexture;
}
CHECK_NE(kSbPlayerOutputModeInvalid, output_mode);
return output_mode;
}
#endif // SB_HAS(PLAYER_WITH_URL)
SbPlayerOutputMode SbPlayerBridge::ComputeSbPlayerOutputMode(
SbPlayerOutputMode default_output_mode) const {
SbPlayerCreationParam creation_param = {};
creation_param.drm_system = drm_system_;
#if SB_API_VERSION >= 15
creation_param.audio_stream_info = audio_stream_info_;
creation_param.video_stream_info = video_stream_info_;
#else // SB_API_VERSION >= 15
creation_param.audio_sample_info = audio_stream_info_;
creation_param.video_sample_info = video_stream_info_;
#endif // SB_API_VERSION >= 15
if (default_output_mode != kSbPlayerOutputModeDecodeToTexture &&
video_stream_info_.codec != kSbMediaVideoCodecNone) {
DCHECK(video_stream_info_.mime);
DCHECK(video_stream_info_.max_video_capabilities);
// Set the `default_output_mode` to `kSbPlayerOutputModeDecodeToTexture` if
// any of the mime associated with it has `decode-to-texture=true` set.
// TODO(b/232559177): Make the check below more flexible, to work with
// other well formed inputs (e.g. with extra space).
bool is_decode_to_texture_preferred =
strstr(video_stream_info_.mime, "decode-to-texture=true") ||
strstr(video_stream_info_.max_video_capabilities,
"decode-to-texture=true");
if (is_decode_to_texture_preferred) {
LOG(INFO) << "Setting `default_output_mode` from \""
<< GetPlayerOutputModeName(default_output_mode) << "\" to \""
<< GetPlayerOutputModeName(kSbPlayerOutputModeDecodeToTexture)
<< "\" because mime is set to \"" << video_stream_info_.mime
<< "\", and max_video_capabilities is set to \""
<< video_stream_info_.max_video_capabilities << "\"";
default_output_mode = kSbPlayerOutputModeDecodeToTexture;
}
}
creation_param.output_mode = default_output_mode;
auto output_mode =
sbplayer_interface_->GetPreferredOutputMode(&creation_param);
LOG(INFO) << "Output mode is set to " << GetPlayerOutputModeName(output_mode);
return output_mode;
}
void SbPlayerBridge::LogStartupLatency() const {
std::string first_events_str;
if (set_drm_system_ready_cb_time_.is_null()) {
first_events_str = FormatString("%-40s0 us", "SbPlayerCreate() called");
} else if (set_drm_system_ready_cb_time_ < player_creation_time_) {
first_events_str = FormatString(
"%-40s0 us\n%-40s%" PRId64 " us", "set_drm_system_ready_cb called",
"SbPlayerCreate() called",
(player_creation_time_ - set_drm_system_ready_cb_time_)
.InMicroseconds());
} else {
first_events_str = FormatString(
"%-40s0 us\n%-40s%" PRId64 " us", "SbPlayerCreate() called",
"set_drm_system_ready_cb called",
(set_drm_system_ready_cb_time_ - player_creation_time_)
.InMicroseconds());
}
Time first_event_time =
std::max(player_creation_time_, set_drm_system_ready_cb_time_);
TimeDelta player_initialization_time_delta =
sb_player_state_initialized_time_ - first_event_time;
TimeDelta player_preroll_time_delta =
sb_player_state_prerolling_time_ - sb_player_state_initialized_time_;
TimeDelta first_audio_sample_time_delta = std::max(
first_audio_sample_time_ - sb_player_state_prerolling_time_, TimeDelta());
TimeDelta first_video_sample_time_delta = std::max(
first_video_sample_time_ - sb_player_state_prerolling_time_, TimeDelta());
TimeDelta player_presenting_time_delta =
sb_player_state_presenting_time_ -
std::max(first_audio_sample_time_, first_video_sample_time_);
TimeDelta startup_latency =
sb_player_state_presenting_time_ - first_event_time;
StatisticsWrapper::GetInstance()->startup_latency.AddSample(
startup_latency.InMicroseconds(), 1);
// clang-format off
LOG(INFO) << FormatString(
"\nSbPlayer startup latencies: %" PRId64 " us\n"
" Event name time since last event\n"
" %s\n" // |first_events_str| populated above
" kSbPlayerStateInitialized received %" PRId64 " us\n"
" kSbPlayerStatePrerolling received %" PRId64 " us\n"
" First media sample(s) written [a/v] %" PRId64 "/%" PRId64 " us\n"
" kSbPlayerStatePresenting received %" PRId64 " us\n"
" Startup latency statistics (us):"
" min: %" PRId64 ", median: %" PRId64 ", average: %" PRId64
", max: %" PRId64,
startup_latency.InMicroseconds(),
first_events_str.c_str(), player_initialization_time_delta.InMicroseconds(),
player_preroll_time_delta.InMicroseconds(), first_audio_sample_time_delta.InMicroseconds(),
first_video_sample_time_delta.InMicroseconds(), player_presenting_time_delta.InMicroseconds(),
StatisticsWrapper::GetInstance()->startup_latency.min(),
StatisticsWrapper::GetInstance()->startup_latency.GetMedian(),
StatisticsWrapper::GetInstance()->startup_latency.average(),
StatisticsWrapper::GetInstance()->startup_latency.max());
// clang-format on
}
} // namespace media
} // namespace cobalt