blob: eaf47a7373cad0811302e928090992473370cce1 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromecast/starboard/media/renderer/client_stats_tracker.h"
#include <array>
#include <cstdint>
#include "base/containers/span.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "chromecast/base/metrics/mock_cast_metrics_helper.h"
#include "chromecast/starboard/media/media/starboard_api_wrapper.h"
#include "media/base/mock_filters.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace chromecast {
namespace media {
namespace {
using ::base::test::SingleThreadTaskEnvironment;
using ::chromecast::metrics::MockCastMetricsHelper;
using ::media::PipelineStatistics;
using ::testing::_;
using ::testing::AllOf;
using ::testing::Eq;
using ::testing::Field;
using ::testing::InSequence;
auto MatchesStats(const PipelineStatistics& stats) {
return AllOf(Field(&PipelineStatistics::audio_bytes_decoded,
Eq(stats.audio_bytes_decoded)),
Field(&PipelineStatistics::video_bytes_decoded,
Eq(stats.video_bytes_decoded)),
Field(&PipelineStatistics::video_frames_decoded,
Eq(stats.video_frames_decoded)),
Field(&PipelineStatistics::video_frames_dropped,
Eq(stats.video_frames_dropped)));
}
// Creates an audio sample containing the given data.
StarboardSampleInfo CreateAudioSample(base::span<const uint8_t> data) {
StarboardSampleInfo sample_info;
sample_info.type = kStarboardMediaTypeAudio;
sample_info.buffer = data.data();
sample_info.buffer_size = data.size();
sample_info.timestamp = 0;
sample_info.side_data = base::span<const StarboardSampleSideData>();
sample_info.audio_sample_info = {};
sample_info.drm_info = nullptr;
return sample_info;
}
// Creates a video sample containing the given data.
StarboardSampleInfo CreateVideoSample(base::span<const uint8_t> data) {
StarboardSampleInfo sample_info;
sample_info.type = kStarboardMediaTypeVideo;
sample_info.buffer = data.data();
sample_info.buffer_size = data.size();
sample_info.timestamp = 0;
sample_info.side_data = base::span<const StarboardSampleSideData>();
sample_info.video_sample_info = {};
sample_info.drm_info = nullptr;
return sample_info;
}
TEST(ClientStatsTrackerTest, UpdatesStatsForAudioBuffer) {
SingleThreadTaskEnvironment task_environment{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
constexpr auto kAudioData = std::to_array<uint8_t>({1, 2, 3});
StarboardPlayerInfo player_info = {};
StarboardSampleInfo sample_info = CreateAudioSample(kAudioData);
PipelineStatistics expected_stats;
expected_stats.audio_bytes_decoded = kAudioData.size();
expected_stats.video_bytes_decoded = 0;
expected_stats.video_frames_decoded = 0;
expected_stats.video_frames_dropped = 0;
MockCastMetricsHelper metrics_helper;
::media::MockRendererClient client;
EXPECT_CALL(client, OnStatisticsUpdate(MatchesStats(expected_stats)))
.Times(1);
ClientStatsTracker stats_tracker(&client, &metrics_helper);
task_environment.FastForwardBy(base::Seconds(5));
stats_tracker.UpdateStats(player_info, sample_info);
}
TEST(ClientStatsTrackerTest, UpdatesStatsForVideoBuffer) {
SingleThreadTaskEnvironment task_environment{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
constexpr auto kVideoData1 = std::to_array<uint8_t>({1, 2, 3, 4, 5});
constexpr auto kVideoData2 = std::to_array<uint8_t>({6, 7, 8});
StarboardPlayerInfo player_info_1 = {};
player_info_1.total_video_frames = 2;
player_info_1.dropped_video_frames = 0;
StarboardSampleInfo sample_info_1 = CreateVideoSample(kVideoData1);
PipelineStatistics expected_stats_1;
expected_stats_1.audio_bytes_decoded = 0;
expected_stats_1.video_bytes_decoded = kVideoData1.size();
expected_stats_1.video_frames_decoded = 2;
expected_stats_1.video_frames_dropped = 0;
StarboardPlayerInfo player_info_2 = {};
player_info_2.total_video_frames = 3;
player_info_2.dropped_video_frames = 0;
StarboardSampleInfo sample_info_2 = CreateVideoSample(kVideoData2);
PipelineStatistics expected_stats_2;
expected_stats_2.audio_bytes_decoded = 0;
expected_stats_2.video_bytes_decoded = kVideoData2.size();
expected_stats_2.video_frames_decoded = 1;
expected_stats_2.video_frames_dropped = 0;
MockCastMetricsHelper metrics_helper;
::media::MockRendererClient client;
EXPECT_CALL(client, OnStatisticsUpdate(MatchesStats(expected_stats_1)))
.Times(1);
EXPECT_CALL(client, OnStatisticsUpdate(MatchesStats(expected_stats_2)))
.Times(1);
ClientStatsTracker stats_tracker(&client, &metrics_helper);
task_environment.FastForwardBy(base::Seconds(5));
stats_tracker.UpdateStats(player_info_1, sample_info_1);
task_environment.FastForwardBy(base::Seconds(5));
stats_tracker.UpdateStats(player_info_2, sample_info_2);
}
TEST(ClientStatsTrackerTest, DoesNotUpdateStatsIfNotEnoughTimeHasPassed) {
SingleThreadTaskEnvironment task_environment{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
constexpr auto kAudioData = std::to_array<uint8_t>({1, 2, 3});
StarboardPlayerInfo player_info = {};
StarboardSampleInfo sample_info = CreateAudioSample(kAudioData);
PipelineStatistics expected_stats;
expected_stats.audio_bytes_decoded = kAudioData.size();
expected_stats.video_bytes_decoded = 0;
expected_stats.video_frames_decoded = 0;
expected_stats.video_frames_dropped = 0;
MockCastMetricsHelper metrics_helper;
::media::MockRendererClient client;
EXPECT_CALL(client, OnStatisticsUpdate(_)).Times(0);
ClientStatsTracker stats_tracker(&client, &metrics_helper);
// No time has elapsed between the creation of stats_tracker and the call to
// UpdateStats.
stats_tracker.UpdateStats(player_info, sample_info);
}
TEST(ClientStatsTrackerTest, AccumulatesStatsFromMultipleBuffers) {
SingleThreadTaskEnvironment task_environment{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
// The size of the buffer is all that matters here.
const std::vector<uint8_t> video_data_1(10000);
const std::vector<uint8_t> video_data_2(5000);
const std::vector<uint8_t> audio_data(5000);
StarboardPlayerInfo player_info_1 = {};
player_info_1.total_video_frames = 3;
player_info_1.dropped_video_frames = 1;
StarboardPlayerInfo player_info_2 = {};
player_info_2.total_video_frames = 5;
player_info_2.dropped_video_frames = 1;
// Not relevant to audio stats.
StarboardPlayerInfo player_info_3 = {};
StarboardSampleInfo sample_info_1 = CreateVideoSample(video_data_1);
StarboardSampleInfo sample_info_2 = CreateVideoSample(video_data_2);
StarboardSampleInfo sample_info_3 = CreateAudioSample(audio_data);
PipelineStatistics expected_stats;
expected_stats.audio_bytes_decoded = audio_data.size();
expected_stats.video_bytes_decoded =
video_data_1.size() + video_data_2.size();
expected_stats.video_frames_decoded = 5;
expected_stats.video_frames_dropped = 1;
MockCastMetricsHelper metrics_helper;
EXPECT_CALL(metrics_helper,
RecordApplicationEventWithValue("Cast.Platform.AudioBitrate", 8));
EXPECT_CALL(metrics_helper, RecordApplicationEventWithValue(
"Cast.Platform.VideoBitrate", 24));
::media::MockRendererClient client;
EXPECT_CALL(client, OnStatisticsUpdate(MatchesStats(expected_stats)))
.Times(1);
ClientStatsTracker stats_tracker(&client, &metrics_helper);
stats_tracker.UpdateStats(player_info_1, sample_info_1);
stats_tracker.UpdateStats(player_info_2, sample_info_2);
task_environment.FastForwardBy(base::Seconds(5));
// Now that enough time has passed, the stats should be updated with info from
// all 3 buffers.
stats_tracker.UpdateStats(player_info_3, sample_info_3);
}
TEST(ClientStatsTrackerTest, UpdatesAreDeltasAndNotCumulative) {
// This test verifies that ClientStatsTracker resets its stats after each
// update to clients.
SingleThreadTaskEnvironment task_environment{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
// The size of the buffer is all that matters here.
const std::vector<uint8_t> video_data_1(10000);
// These buffers will be pushed after the first update.
const std::vector<uint8_t> video_data_2(5000);
const std::vector<uint8_t> audio_data(5000);
StarboardPlayerInfo player_info_1 = {};
player_info_1.total_video_frames = 3;
player_info_1.dropped_video_frames = 1;
StarboardPlayerInfo player_info_2 = {};
player_info_2.total_video_frames = 5;
player_info_2.dropped_video_frames = 1;
// Not relevant to audio stats.
StarboardPlayerInfo player_info_3 = {};
StarboardSampleInfo sample_info_1 = CreateVideoSample(video_data_1);
StarboardSampleInfo sample_info_2 = CreateVideoSample(video_data_2);
StarboardSampleInfo sample_info_3 = CreateAudioSample(audio_data);
// stats for just sample_info_1 and player_info_1.
PipelineStatistics expected_stats_1;
expected_stats_1.audio_bytes_decoded = 0;
expected_stats_1.video_bytes_decoded = video_data_1.size();
expected_stats_1.video_frames_decoded = 3;
expected_stats_1.video_frames_dropped = 1;
// stats for sample_info_2, player_info_2, sample_info_3, and player_info_3.
// Note that these stats should be deltas, not totals.
PipelineStatistics expected_stats_2;
expected_stats_2.audio_bytes_decoded = audio_data.size();
expected_stats_2.video_bytes_decoded = video_data_2.size();
expected_stats_2.video_frames_decoded = 2;
expected_stats_2.video_frames_dropped = 0;
MockCastMetricsHelper metrics_helper;
// The ordering of audio and video stats updates is irrelevant.
EXPECT_CALL(metrics_helper,
RecordApplicationEventWithValue("Cast.Platform.AudioBitrate", 4))
.Times(1);
{
InSequence seq;
EXPECT_CALL(metrics_helper, RecordApplicationEventWithValue(
"Cast.Platform.VideoBitrate", 16))
.Times(1);
EXPECT_CALL(metrics_helper, RecordApplicationEventWithValue(
"Cast.Platform.VideoBitrate", 4))
.Times(1);
}
::media::MockRendererClient client;
{
InSequence seq;
EXPECT_CALL(client, OnStatisticsUpdate(MatchesStats(expected_stats_1)))
.Times(1);
EXPECT_CALL(client, OnStatisticsUpdate(MatchesStats(expected_stats_2)))
.Times(1);
}
ClientStatsTracker stats_tracker(&client, &metrics_helper);
task_environment.FastForwardBy(base::Seconds(5));
// Enough time has passed, so the stats should be sent to clients.
stats_tracker.UpdateStats(player_info_1, sample_info_1);
task_environment.FastForwardBy(base::Seconds(1));
// This update does not get immediately pushed, since not enough time has
// passed.
stats_tracker.UpdateStats(player_info_2, sample_info_2);
// Note that the delta is 10s total here. This affects the bitrate that gets
// reported to the metrics helper.
task_environment.FastForwardBy(base::Seconds(9));
// Now that enough time has passed, stats for the second and third buffer
// should be pushed.
stats_tracker.UpdateStats(player_info_3, sample_info_3);
}
} // namespace
} // namespace media
} // namespace chromecast