test: add benchmark for toxav audio and video

This includes RTP, but no networking.
This commit is contained in:
iphydf committed 2026-01-02 01:58:07 +00:00
1 parent 46bfdc2df7
commit 1936d42965
11 files changed
+1280 -257

No files matched your search

+12
View File
@@ -578,12 +578,24 @@ endfunction()
#
if(UNITTEST AND TARGET GTest::gtest AND TARGET GTest::gmock)
if(BUILD_TOXAV)
add_library(av_test_support STATIC
toxav/av_test_support.cc
toxav/av_test_support.hh)
if(TARGET toxcore_static)
target_link_libraries(av_test_support PRIVATE toxcore_static)
else()
target_link_libraries(av_test_support PRIVATE toxcore_shared)
endif()
target_link_libraries(av_test_support PRIVATE GTest::gtest)
unit_test(toxav audio)
target_link_libraries(unit_audio_test PRIVATE av_test_support)
unit_test(toxav bwcontroller)
unit_test(toxav msi)
unit_test(toxav ring_buffer)
unit_test(toxav rtp)
unit_test(toxav video)
target_link_libraries(unit_video_test PRIVATE av_test_support)
endif()
unit_test(toxcore DHT)
+4
View File
@@ -62,6 +62,10 @@ FIND_QUERY="$FIND_QUERY -and -not -wholename './testing/fuzzing/*'"
FIND_QUERY="$FIND_QUERY -and -not -wholename './third_party/cmp/examples/*'"
FIND_QUERY="$FIND_QUERY -and -not -wholename './third_party/cmp/test/*'"
if [ "$SKIP_BENCHMARK" == 1 ]; then
FIND_QUERY="$FIND_QUERY -and -not -name '*_bench.cc'"
fi
if [ "$SKIP_GTEST" == 1 ]; then
FIND_QUERY="$FIND_QUERY -and -not -name '*_test.cc'"
fi
+1 -2
View File
@@ -1,6 +1,7 @@
#!/bin/bash
SKIP_GTEST=1
SKIP_BENCHMARK=1
. other/analysis/gen-file.sh
@@ -24,8 +25,6 @@ CPPCHECK+=("--suppress=knownConditionTrueFalse")
CPPCHECK+=("--suppress=missingIncludeSystem")
# TODO(iphydf): Maybe fix?
CPPCHECK+=("--suppress=signConversion")
# We have suppressions in the code for the misra extension.
CPPCHECK+=("--suppress=unmatchedSuppression")
# We use this for VLAs.
CPPCHECK_CXX+=("--suppress=allocaCalled")
+63 -2
View File
@@ -1,6 +1,5 @@
load("@rules_cc//cc:defs.bzl", "cc_test")
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "cc_test")
load("@rules_fuzzing//fuzzing:cc_defs.bzl", "cc_fuzz_test")
load("//tools:no_undefined.bzl", "cc_library")
exports_files(
srcs = ["toxav.h"],
@@ -131,12 +130,29 @@ cc_library(
],
)
cc_library(
name = "av_test_support",
testonly = True,
srcs = ["av_test_support.cc"],
hdrs = ["av_test_support.hh"],
deps = [
":audio",
":rtp",
":video",
"//c-toxcore/toxcore:logger",
"//c-toxcore/toxcore:mono_time",
"//c-toxcore/toxcore:os_memory",
"@com_google_googletest//:gtest",
],
)
cc_test(
name = "audio_test",
timeout = "moderate",
srcs = ["audio_test.cc"],
deps = [
":audio",
":av_test_support",
":rtp",
"//c-toxcore/toxcore:logger",
"//c-toxcore/toxcore:mono_time",
@@ -166,6 +182,7 @@ cc_test(
timeout = "moderate",
srcs = ["video_test.cc"],
deps = [
":av_test_support",
":rtp",
":video",
"//c-toxcore/toxcore:logger",
@@ -176,6 +193,50 @@ cc_test(
],
)
cc_binary(
name = "video_bench",
testonly = True,
srcs = ["video_bench.cc"],
deps = [
":av_test_support",
":rtp",
":video",
"//c-toxcore/toxcore:logger",
"//c-toxcore/toxcore:mono_time",
"//c-toxcore/toxcore:os_memory",
"@benchmark",
],
)
cc_binary(
name = "audio_bench",
testonly = True,
srcs = ["audio_bench.cc"],
deps = [
":audio",
":av_test_support",
":rtp",
"//c-toxcore/toxcore:logger",
"//c-toxcore/toxcore:mono_time",
"//c-toxcore/toxcore:os_memory",
"@benchmark",
],
)
cc_binary(
name = "rtp_bench",
testonly = True,
srcs = ["rtp_bench.cc"],
deps = [
":av_test_support",
":rtp",
"//c-toxcore/toxcore:logger",
"//c-toxcore/toxcore:mono_time",
"//c-toxcore/toxcore:os_memory",
"@benchmark",
],
)
cc_library(
name = "msi",
srcs = ["msi.c"],
+199
View File
@@ -0,0 +1,199 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Copyright © 2025 The TokTok team.
*/
#include <benchmark/benchmark.h>
#include <cmath>
#include <cstring>
#include <vector>
#include "../toxcore/logger.h"
#include "../toxcore/mono_time.h"
#include "../toxcore/network.h"
#include "../toxcore/os_memory.h"
#include "audio.h"
#include "av_test_support.hh"
#include "rtp.h"
namespace {
class AudioBench : public benchmark::Fixture {
public:
void SetUp(const ::benchmark::State &state) override
{
const Memory *mem = os_memory();
log = logger_new(mem);
tm.t = 1000;
mono_time = mono_time_new(mem, mock_time_cb, &tm);
ac = ac_new(mono_time, log, 123, nullptr, nullptr);
sampling_rate = static_cast<uint32_t>(state.range(0));
channels = static_cast<uint8_t>(state.range(1));
uint32_t bitrate = (channels == 1) ? 32000 : 64000;
ac_reconfigure_encoder(ac, bitrate, sampling_rate, channels);
sample_count = sampling_rate / 50; // 20ms frames
pcm.resize(sample_count * channels);
rtp_mock.capture_packets = false; // Disable capturing for benchmarks
rtp_mock.auto_forward = true;
rtp_mock.recv_session = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
}
void TearDown(const ::benchmark::State &state) override
{
const Memory *mem = os_memory();
if (rtp_mock.recv_session) {
rtp_kill(log, rtp_mock.recv_session);
}
if (ac) {
ac_kill(ac);
}
if (mono_time) {
mono_time_free(mem, mono_time);
}
if (log) {
logger_kill(log);
}
}
Logger *log = nullptr;
Mono_Time *mono_time = nullptr;
MockTime tm;
ACSession *ac = nullptr;
RtpMock rtp_mock;
uint32_t sampling_rate = 0;
uint8_t channels = 0;
size_t sample_count = 0;
std::vector<int16_t> pcm;
};
// Benchmark encoding a sequence of silent audio frames.
BENCHMARK_DEFINE_F(AudioBench, EncodeSilentSequence)(benchmark::State &state)
{
std::vector<int16_t> silent_pcm(sample_count * channels);
fill_silent_frame(channels, sample_count, silent_pcm);
std::vector<uint8_t> encoded(2000);
for (auto _ : state) {
int encoded_size
= ac_encode(ac, silent_pcm.data(), sample_count, encoded.data(), encoded.size());
benchmark::DoNotOptimize(encoded_size);
}
}
BENCHMARK_REGISTER_F(AudioBench, EncodeSilentSequence)
->Args({8000, 1})
->Args({48000, 1})
->Args({48000, 2});
// Benchmark encoding a sequence of audio frames.
BENCHMARK_DEFINE_F(AudioBench, EncodeSequence)(benchmark::State &state)
{
int frame_index = 0;
const int num_prefilled = 50;
std::vector<std::vector<int16_t>> pcms(
num_prefilled, std::vector<int16_t>(sample_count * channels));
for (int i = 0; i < num_prefilled; ++i) {
fill_audio_frame(sampling_rate, channels, i, sample_count, pcms[i]);
}
std::vector<uint8_t> encoded(2000);
for (auto _ : state) {
int idx = frame_index % num_prefilled;
int encoded_size
= ac_encode(ac, pcms[idx].data(), sample_count, encoded.data(), encoded.size());
benchmark::DoNotOptimize(encoded_size);
frame_index++;
}
}
BENCHMARK_REGISTER_F(AudioBench, EncodeSequence)
->Args({8000, 1})
->Args({16000, 1})
->Args({24000, 1})
->Args({48000, 1})
->Args({48000, 2});
// Benchmark decoding a sequence of audio frames.
BENCHMARK_DEFINE_F(AudioBench, DecodeSequence)(benchmark::State &state)
{
const int num_frames = 50;
std::vector<std::vector<uint8_t>> encoded_frames(num_frames);
// Pre-encode
std::vector<uint8_t> encoded_tmp(2000);
for (int i = 0; i < num_frames; ++i) {
fill_audio_frame(sampling_rate, channels, i, sample_count, pcm);
int size = ac_encode(ac, pcm.data(), sample_count, encoded_tmp.data(), encoded_tmp.size());
encoded_frames[i].resize(4 + size);
uint32_t net_sr = net_htonl(sampling_rate);
std::memcpy(encoded_frames[i].data(), &net_sr, 4);
std::memcpy(encoded_frames[i].data() + 4, encoded_tmp.data(), size);
}
int frame_index = 0;
for (auto _ : state) {
int idx = frame_index % num_frames;
rtp_send_data(log, rtp_mock.recv_session, encoded_frames[idx].data(),
static_cast<uint32_t>(encoded_frames[idx].size()), false);
ac_iterate(ac);
frame_index++;
}
}
BENCHMARK_REGISTER_F(AudioBench, DecodeSequence)
->Args({8000, 1})
->Args({16000, 1})
->Args({24000, 1})
->Args({48000, 1})
->Args({48000, 2});
// Full end-to-end sequence benchmark (Encode -> RTP -> Decode)
BENCHMARK_DEFINE_F(AudioBench, FullSequence)(benchmark::State &state)
{
int frame_index = 0;
const int num_prefilled = 50;
std::vector<std::vector<int16_t>> pcms(
num_prefilled, std::vector<int16_t>(sample_count * channels));
for (int i = 0; i < num_prefilled; ++i) {
fill_audio_frame(sampling_rate, channels, i, sample_count, pcms[i]);
}
std::vector<uint8_t> encoded_tmp(2000);
for (auto _ : state) {
int idx = frame_index % num_prefilled;
int size
= ac_encode(ac, pcms[idx].data(), sample_count, encoded_tmp.data(), encoded_tmp.size());
std::vector<uint8_t> payload(4 + size);
uint32_t net_sr = net_htonl(sampling_rate);
std::memcpy(payload.data(), &net_sr, 4);
std::memcpy(payload.data() + 4, encoded_tmp.data(), size);
rtp_send_data(log, rtp_mock.recv_session, payload.data(),
static_cast<uint32_t>(payload.size()), false);
ac_iterate(ac);
frame_index++;
}
}
BENCHMARK_REGISTER_F(AudioBench, FullSequence)
->Args({8000, 1})
->Args({16000, 1})
->Args({24000, 1})
->Args({48000, 1})
->Args({48000, 2});
}
BENCHMARK_MAIN();
+260 -128
View File
@@ -3,84 +3,19 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <cmath>
#include <vector>
#include "../toxcore/logger.h"
#include "../toxcore/mono_time.h"
#include "../toxcore/network.h"
#include "../toxcore/os_memory.h"
#include "av_test_support.hh"
#include "rtp.h"
namespace {
struct AudioTimeMock {
uint64_t t;
};
uint64_t audio_mock_time_cb(void *ud) { return static_cast<AudioTimeMock *>(ud)->t; }
struct AudioTestData {
uint32_t friend_number = 0;
std::vector<int16_t> last_pcm;
size_t sample_count = 0;
uint8_t channels = 0;
uint32_t sampling_rate = 0;
static void receive_frame(uint32_t friend_number, const int16_t *pcm, size_t sample_count,
uint8_t channels, uint32_t sampling_rate, void *user_data)
{
auto *self = static_cast<AudioTestData *>(user_data);
self->friend_number = friend_number;
self->last_pcm.assign(pcm, pcm + sample_count * channels);
self->sample_count = sample_count;
self->channels = channels;
self->sampling_rate = sampling_rate;
}
};
struct AudioRtpMock {
RTPSession *recv_session = nullptr;
std::vector<std::vector<uint8_t>> captured_packets;
bool auto_forward = true;
static int send_packet(void *user_data, const uint8_t *data, uint16_t length)
{
auto *self = static_cast<AudioRtpMock *>(user_data);
self->captured_packets.push_back(std::vector<uint8_t>(data, data + length));
if (self->auto_forward && self->recv_session) {
rtp_receive_packet(self->recv_session, data, length);
}
return 0;
}
static int audio_cb(const Mono_Time *mono_time, void *cs, RTPMessage *msg)
{
return ac_queue_message(mono_time, cs, msg);
}
};
class AudioTest : public ::testing::Test {
protected:
void SetUp() override
{
const Memory *mem = os_memory();
log = logger_new(mem);
tm.t = 1000;
mono_time = mono_time_new(mem, audio_mock_time_cb, &tm);
mono_time_update(mono_time);
}
void TearDown() override
{
const Memory *mem = os_memory();
mono_time_free(mem, mono_time);
logger_kill(log);
}
Logger *log;
Mono_Time *mono_time;
AudioTimeMock tm;
};
using AudioTest = AvTest;
TEST_F(AudioTest, BasicNewKill)
{
@@ -96,11 +31,11 @@ TEST_F(AudioTest, EncodeDecodeLoop)
ACSession *ac = ac_new(mono_time, log, 123, AudioTestData::receive_frame, &data);
ASSERT_NE(ac, nullptr);
AudioRtpMock rtp_mock;
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RtpMock rtp_mock;
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
rtp_mock.recv_session = recv_rtp;
uint32_t sampling_rate = 48000;
@@ -144,6 +79,203 @@ TEST_F(AudioTest, EncodeDecodeLoop)
ac_kill(ac);
}
TEST_F(AudioTest, EncodeDecodeRealistic)
{
AudioTestData data;
ACSession *ac = ac_new(mono_time, log, 123, AudioTestData::receive_frame, &data);
ASSERT_NE(ac, nullptr);
RtpMock rtp_mock;
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
rtp_mock.recv_session = recv_rtp;
uint32_t sampling_rate = 48000;
uint8_t channels = 1;
size_t sample_count = 960; // 20ms at 48kHz
uint32_t bitrate = 48000;
ASSERT_EQ(ac_reconfigure_encoder(ac, bitrate, sampling_rate, channels), 0);
double frequency = 440.0;
double amplitude = 10000.0;
const double pi = std::acos(-1.0);
std::vector<int16_t> all_sent;
std::vector<int16_t> all_recv;
for (int frame = 0; frame < 50; ++frame) {
std::vector<int16_t> pcm(sample_count * channels);
for (size_t i = 0; i < sample_count; ++i) {
double t = static_cast<double>(frame * sample_count + i) / sampling_rate;
pcm[i] = static_cast<int16_t>(std::sin(2.0 * pi * frequency * t) * amplitude);
}
all_sent.insert(all_sent.end(), pcm.begin(), pcm.end());
std::vector<uint8_t> encoded(2000);
int encoded_size = ac_encode(ac, pcm.data(), sample_count, encoded.data(), encoded.size());
ASSERT_GT(encoded_size, 0);
std::vector<uint8_t> payload(4 + static_cast<size_t>(encoded_size));
uint32_t net_sr = net_htonl(sampling_rate);
memcpy(payload.data(), &net_sr, 4);
memcpy(payload.data() + 4, encoded.data(), static_cast<size_t>(encoded_size));
rtp_send_data(log, send_rtp, payload.data(), static_cast<uint32_t>(payload.size()), false);
ac_iterate(ac);
if (data.sample_count > 0) {
all_recv.insert(all_recv.end(), data.last_pcm.begin(), data.last_pcm.end());
}
}
ASSERT_FALSE(all_recv.empty());
// Find the best match by trying different delays.
// Jitter buffer delay (3 frames = 2880 samples) + Opus lookahead (~312 samples) = ~3192.
double min_mse = 1e18;
int best_delay = 0;
// Search around the expected delay
for (int delay = 3000; delay < 3500; ++delay) {
double mse = 0;
int count = 0;
for (size_t i = 0; i < 2000; ++i) { // Compare a decent chunk
if (i + delay < all_sent.size() && i < all_recv.size()) {
int diff = all_sent[i + delay] - all_recv[i];
mse += static_cast<double>(diff) * diff;
count++;
}
}
if (count > 1000) {
mse /= count;
if (mse < min_mse) {
min_mse = mse;
best_delay = delay;
}
}
}
printf("Best audio delay found: %d samples, Min MSE: %f\n", best_delay, min_mse);
// For 48kbps Opus, the MSE for a sine wave should be quite low once aligned.
// 10M is about 20% of the signal power (50M), which is a safe threshold for verification.
EXPECT_LT(min_mse, 10000000.0);
rtp_kill(log, send_rtp);
rtp_kill(log, recv_rtp);
ac_kill(ac);
}
TEST_F(AudioTest, EncodeDecodeSiren)
{
AudioTestData data;
ACSession *ac = ac_new(mono_time, log, 123, AudioTestData::receive_frame, &data);
ASSERT_NE(ac, nullptr);
RtpMock rtp_mock;
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
rtp_mock.recv_session = recv_rtp;
uint32_t sampling_rate = 48000;
uint8_t channels = 1;
size_t sample_count = 960; // 20ms at 48kHz
uint32_t bitrate = 64000;
ASSERT_EQ(ac_reconfigure_encoder(ac, bitrate, sampling_rate, channels), 0);
double amplitude = 10000.0;
const double pi = std::acos(-1.0);
std::vector<int16_t> all_sent;
std::vector<int16_t> all_recv;
// 1 second of audio (50 frames) is enough for a siren test
for (int frame = 0; frame < 50; ++frame) {
std::vector<int16_t> pcm(sample_count * channels);
for (size_t i = 0; i < sample_count; ++i) {
double t = static_cast<double>(frame * sample_count + i) / sampling_rate;
// Linear frequency sweep from 50Hz to 440Hz over 1 second
// f(t) = 50 + (440-50)/1 * t = 50 + 390t
// phi(t) = 2*pi * integral(f(t)) = 2*pi * (50t + 195t^2)
double phi = 2.0 * pi * (50.0 * t + 195.0 * t * t);
pcm[i] = static_cast<int16_t>(std::sin(phi) * amplitude);
}
all_sent.insert(all_sent.end(), pcm.begin(), pcm.end());
std::vector<uint8_t> encoded(2000);
int encoded_size = ac_encode(ac, pcm.data(), sample_count, encoded.data(), encoded.size());
ASSERT_GT(encoded_size, 0);
std::vector<uint8_t> payload(4 + static_cast<size_t>(encoded_size));
uint32_t net_sr = net_htonl(sampling_rate);
memcpy(payload.data(), &net_sr, 4);
memcpy(payload.data() + 4, encoded.data(), static_cast<size_t>(encoded_size));
rtp_send_data(log, send_rtp, payload.data(), static_cast<uint32_t>(payload.size()), false);
ac_iterate(ac);
if (data.sample_count > 0) {
all_recv.insert(all_recv.end(), data.last_pcm.begin(), data.last_pcm.end());
}
}
ASSERT_FALSE(all_recv.empty());
auto calculate_mse_at = [&](int delay, size_t window) {
double mse = 0;
int count = 0;
for (size_t i = 0; i < window; ++i) {
int sent_idx = static_cast<int>(i) + delay;
if (sent_idx >= 0 && static_cast<size_t>(sent_idx) < all_sent.size()
&& i < all_recv.size()) {
int diff = all_sent[static_cast<size_t>(sent_idx)] - all_recv[i];
mse += static_cast<double>(diff) * diff;
count++;
}
}
return count > 0 ? mse / count : 1e18;
};
// Two-stage search for speed
double min_mse = 1e18;
int coarse_best = 0;
// 1. Coarse search
for (int delay = -5000; delay < 5000; delay += 100) {
double mse = calculate_mse_at(delay, 5000);
if (mse < min_mse) {
min_mse = mse;
coarse_best = delay;
}
}
// 2. Fine search around coarse best
int best_delay = coarse_best;
for (int delay = coarse_best - 100; delay <= coarse_best + 100; ++delay) {
double mse = calculate_mse_at(delay, 10000);
if (mse < min_mse) {
min_mse = mse;
best_delay = delay;
}
}
printf("Best siren audio delay found: %d samples, Min MSE: %f\n", best_delay, min_mse);
// For 64kbps Opus, the MSE for a siren wave should be reasonably low once aligned.
EXPECT_LT(min_mse, 20000000.0);
rtp_kill(log, send_rtp);
rtp_kill(log, recv_rtp);
ac_kill(ac);
}
TEST_F(AudioTest, ReconfigureEncoder)
{
AudioTestData data;
@@ -184,12 +316,12 @@ TEST_F(AudioTest, QueueInvalidMessage)
ACSession *ac = ac_new(mono_time, log, 123, AudioTestData::receive_frame, &data);
ASSERT_NE(ac, nullptr);
AudioRtpMock rtp_mock;
RtpMock rtp_mock;
// Create a video RTP session but try to queue to audio session
RTPSession *video_rtp = rtp_new(log, RTP_TYPE_VIDEO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *audio_recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *video_rtp = rtp_new(log, RTP_TYPE_VIDEO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
RTPSession *audio_recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
rtp_mock.recv_session = audio_recv_rtp;
std::vector<uint8_t> dummy_video(100, 0);
@@ -212,12 +344,12 @@ TEST_F(AudioTest, JitterBufferDuplicate)
ACSession *ac = ac_new(mono_time, log, 123, AudioTestData::receive_frame, &data);
ASSERT_NE(ac, nullptr);
AudioRtpMock rtp_mock;
RtpMock rtp_mock;
rtp_mock.auto_forward = false;
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
rtp_mock.recv_session = recv_rtp;
uint8_t dummy_data[100] = {0};
@@ -253,12 +385,12 @@ TEST_F(AudioTest, JitterBufferOutOfOrder)
ACSession *ac = ac_new(mono_time, log, 123, AudioTestData::receive_frame, &data);
ASSERT_NE(ac, nullptr);
AudioRtpMock rtp_mock;
RtpMock rtp_mock;
rtp_mock.auto_forward = false;
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
rtp_mock.recv_session = recv_rtp;
uint8_t dummy_data[100] = {0};
@@ -300,12 +432,12 @@ TEST_F(AudioTest, PacketLossConcealment)
ACSession *ac = ac_new(mono_time, log, 123, AudioTestData::receive_frame, &data);
ASSERT_NE(ac, nullptr);
AudioRtpMock rtp_mock;
RtpMock rtp_mock;
rtp_mock.auto_forward = false;
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
rtp_mock.recv_session = recv_rtp;
uint8_t dummy_data[100] = {0};
@@ -346,12 +478,12 @@ TEST_F(AudioTest, JitterBufferReset)
ACSession *ac = ac_new(mono_time, log, 123, AudioTestData::receive_frame, &data);
ASSERT_NE(ac, nullptr);
AudioRtpMock rtp_mock;
RtpMock rtp_mock;
rtp_mock.auto_forward = false;
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
rtp_mock.recv_session = recv_rtp;
uint8_t dummy_data[100] = {0};
@@ -391,12 +523,12 @@ TEST_F(AudioTest, DecoderReconfigureCooldown)
ACSession *ac = ac_new(mono_time, log, 123, AudioTestData::receive_frame, &data);
ASSERT_NE(ac, nullptr);
AudioRtpMock rtp_mock;
RtpMock rtp_mock;
rtp_mock.auto_forward = false;
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
rtp_mock.recv_session = recv_rtp;
uint8_t dummy_data[100] = {0};
@@ -452,12 +584,12 @@ TEST_F(AudioTest, QueueDummyMessage)
ACSession *ac = ac_new(mono_time, log, 123, AudioTestData::receive_frame, &data);
ASSERT_NE(ac, nullptr);
AudioRtpMock rtp_mock;
RtpMock rtp_mock;
// RTP_TYPE_AUDIO + 2 is the dummy type
RTPSession *dummy_rtp = rtp_new(log, RTP_TYPE_AUDIO + 2, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *audio_recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *dummy_rtp = rtp_new(log, RTP_TYPE_AUDIO + 2, mono_time, RtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
RTPSession *audio_recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
rtp_mock.recv_session = audio_recv_rtp;
std::vector<uint8_t> dummy_payload(100, 0);
@@ -480,12 +612,12 @@ TEST_F(AudioTest, LatePacketReset)
ACSession *ac = ac_new(mono_time, log, 123, AudioTestData::receive_frame, &data);
ASSERT_NE(ac, nullptr);
AudioRtpMock rtp_mock;
RtpMock rtp_mock;
rtp_mock.auto_forward = false;
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
rtp_mock.recv_session = recv_rtp;
uint8_t dummy_data[100] = {0};
@@ -531,12 +663,12 @@ TEST_F(AudioTest, InvalidSamplingRate)
ACSession *ac = ac_new(mono_time, log, 123, AudioTestData::receive_frame, &data);
ASSERT_NE(ac, nullptr);
AudioRtpMock rtp_mock;
RtpMock rtp_mock;
rtp_mock.auto_forward = false;
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
rtp_mock.recv_session = recv_rtp;
// 1. Send a packet with an absurdly large sampling rate.
@@ -578,12 +710,12 @@ TEST_F(AudioTest, ShortPacket)
ACSession *ac = ac_new(mono_time, log, 123, AudioTestData::receive_frame, &data);
ASSERT_NE(ac, nullptr);
AudioRtpMock rtp_mock;
RtpMock rtp_mock;
rtp_mock.auto_forward = false;
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
rtp_mock.recv_session = recv_rtp;
// 1. Send a packet that is too short (only sampling rate, no Opus data).
@@ -610,12 +742,12 @@ TEST_F(AudioTest, JitterBufferWrapAround)
ACSession *ac = ac_new(mono_time, log, 123, AudioTestData::receive_frame, &data);
ASSERT_NE(ac, nullptr);
AudioRtpMock rtp_mock;
RtpMock rtp_mock;
rtp_mock.auto_forward = false;
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, AudioRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, ac, AudioRtpMock::audio_cb);
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, ac, RtpMock::audio_cb);
rtp_mock.recv_session = recv_rtp;
uint8_t dummy_data[100] = {0};
+171
View File
@@ -0,0 +1,171 @@
#include "av_test_support.hh"
#include <algorithm>
#include <cmath>
#include <cstring>
#include "../toxcore/os_memory.h"
// Mock Time
uint64_t mock_time_cb(void *ud) { return static_cast<MockTime *>(ud)->t; }
// RTP Mock
int RtpMock::send_packet(void *user_data, const uint8_t *data, uint16_t length)
{
auto *self = static_cast<RtpMock *>(user_data);
if (self->capture_packets) {
if (self->store_last_packet_only) {
if (self->captured_packets.empty()) {
self->captured_packets.emplace_back(data, data + length);
} else {
self->captured_packets[0].assign(data, data + length);
}
} else {
self->captured_packets.push_back(std::vector<uint8_t>(data, data + length));
}
}
if (self->auto_forward && self->recv_session) {
rtp_receive_packet(self->recv_session, data, length);
}
return 0;
}
int RtpMock::audio_cb(const Mono_Time *mono_time, void *cs, RTPMessage *msg)
{
return ac_queue_message(mono_time, cs, msg);
}
int RtpMock::video_cb(const Mono_Time *mono_time, void *cs, RTPMessage *msg)
{
return vc_queue_message(mono_time, cs, msg);
}
int RtpMock::noop_cb(const Mono_Time * /*mono_time*/, void * /*cs*/, RTPMessage *msg)
{
std::free(msg);
return 0;
}
// Audio Helpers
void fill_audio_frame(uint32_t sampling_rate, uint8_t channels, int frame_index,
size_t sample_count, std::vector<int16_t> &pcm)
{
const double pi = std::acos(-1.0);
double amplitude = 10000.0;
for (size_t i = 0; i < sample_count; ++i) {
double t = static_cast<double>(frame_index * sample_count + i) / sampling_rate;
// Linear frequency sweep from 50Hz to 440Hz over 1 second (50 frames)
// f(t) = 50 + 390t
// phi(t) = 2*pi * (50t + 195t^2)
double phi = 2.0 * pi * (50.0 * t + 195.0 * t * t);
int16_t val = static_cast<int16_t>(std::sin(phi) * amplitude);
for (uint8_t c = 0; c < channels; ++c) {
pcm[i * channels + c] = val;
}
}
}
void fill_silent_frame(uint8_t channels, size_t sample_count, std::vector<int16_t> &pcm)
{
for (size_t i = 0; i < sample_count * channels; ++i) {
// Very low amplitude white noise (simulating silence with background hiss)
pcm[i] = (std::rand() % 21) - 10;
}
}
AudioTestData::AudioTestData() = default;
AudioTestData::~AudioTestData() = default;
void AudioTestData::receive_frame(uint32_t friend_number, const int16_t *pcm, size_t sample_count,
uint8_t channels, uint32_t sampling_rate, void *user_data)
{
auto *self = static_cast<AudioTestData *>(user_data);
self->friend_number = friend_number;
self->last_pcm.assign(pcm, pcm + sample_count * channels);
self->sample_count = sample_count;
self->channels = channels;
self->sampling_rate = sampling_rate;
}
// Video Helpers
void fill_video_frame(uint16_t width, uint16_t height, int frame_index, std::vector<uint8_t> &y,
std::vector<uint8_t> &u, std::vector<uint8_t> &v)
{
// Background (dark gray)
std::fill(y.begin(), y.end(), 32);
std::fill(u.begin(), u.end(), 128);
std::fill(v.begin(), v.end(), 128);
// Moving square (light gray)
int sq_size = height / 4;
if (sq_size < 16)
sq_size = 16;
int x0 = (frame_index * 8) % (width - sq_size);
int y0 = (frame_index * 4) % (height - sq_size);
for (int r = 0; r < sq_size; ++r) {
for (int c = 0; c < sq_size; ++c) {
y[(y0 + r) * width + (x0 + c)] = 200;
}
}
}
double calculate_video_mse(uint16_t width, uint16_t height, int32_t ystride,
const std::vector<uint8_t> &y_recv, const std::vector<uint8_t> &y_orig)
{
if (y_recv.empty() || y_orig.size() != static_cast<size_t>(width) * height) {
return 1e10;
}
double mse = 0;
for (int r = 0; r < height; ++r) {
for (int c = 0; c < width; ++c) {
int diff = static_cast<int>(y_orig[r * width + c]) - y_recv[r * std::abs(ystride) + c];
mse += diff * diff;
}
}
return mse / (static_cast<size_t>(width) * height);
}
// Video Test Data Helper
VideoTestData::VideoTestData() = default;
VideoTestData::~VideoTestData() = default;
void VideoTestData::receive_frame(uint32_t friend_number, uint16_t width, uint16_t height,
const uint8_t *y, const uint8_t *u, const uint8_t *v, int32_t ystride, int32_t ustride,
int32_t vstride, void *user_data)
{
auto *self = static_cast<VideoTestData *>(user_data);
self->friend_number = friend_number;
self->width = width;
self->height = height;
self->ystride = ystride;
self->ustride = ustride;
self->vstride = vstride;
self->y.assign(y, y + static_cast<size_t>(std::abs(ystride)) * height);
self->u.assign(u, u + static_cast<size_t>(std::abs(ustride)) * (height / 2));
self->v.assign(v, v + static_cast<size_t>(std::abs(vstride)) * (height / 2));
}
double VideoTestData::calculate_mse(const std::vector<uint8_t> &y_orig) const
{
return calculate_video_mse(width, height, ystride, y, y_orig);
}
// Common Test Fixture
void AvTest::SetUp()
{
const Memory *mem = os_memory();
log = logger_new(mem);
tm.t = 1000;
mono_time = mono_time_new(mem, mock_time_cb, &tm);
mono_time_update(mono_time);
}
void AvTest::TearDown()
{
const Memory *mem = os_memory();
mono_time_free(mem, mono_time);
logger_kill(log);
}
+89
View File
@@ -0,0 +1,89 @@
#ifndef TOXAV_AV_TEST_SUPPORT_H
#define TOXAV_AV_TEST_SUPPORT_H
#include <gtest/gtest.h>
#include <cstdint>
#include <vector>
#include "../toxcore/logger.h"
#include "../toxcore/mono_time.h"
#include "audio.h"
#include "rtp.h"
#include "video.h"
// Mock Time
struct MockTime {
uint64_t t = 1000;
};
uint64_t mock_time_cb(void *ud);
// RTP Mock
struct RtpMock {
RTPSession *recv_session = nullptr;
std::vector<std::vector<uint8_t>> captured_packets;
bool auto_forward = true;
bool capture_packets = true;
bool store_last_packet_only = false;
static int send_packet(void *user_data, const uint8_t *data, uint16_t length);
static int audio_cb(const Mono_Time *mono_time, void *cs, RTPMessage *msg);
static int video_cb(const Mono_Time *mono_time, void *cs, RTPMessage *msg);
static int noop_cb(const Mono_Time *mono_time, void *cs, RTPMessage *msg);
};
// Audio Helpers
void fill_audio_frame(uint32_t sampling_rate, uint8_t channels, int frame_index,
size_t sample_count, std::vector<int16_t> &pcm);
void fill_silent_frame(uint8_t channels, size_t sample_count, std::vector<int16_t> &pcm);
struct AudioTestData {
uint32_t friend_number = 0;
std::vector<int16_t> last_pcm;
size_t sample_count = 0;
uint8_t channels = 0;
uint32_t sampling_rate = 0;
AudioTestData();
~AudioTestData();
static void receive_frame(uint32_t friend_number, const int16_t *pcm, size_t sample_count,
uint8_t channels, uint32_t sampling_rate, void *user_data);
};
// Video Helpers
void fill_video_frame(uint16_t width, uint16_t height, int frame_index, std::vector<uint8_t> &y,
std::vector<uint8_t> &u, std::vector<uint8_t> &v);
double calculate_video_mse(uint16_t width, uint16_t height, int32_t ystride,
const std::vector<uint8_t> &y_recv, const std::vector<uint8_t> &y_orig);
// Video Test Data Helper
struct VideoTestData {
uint32_t friend_number = 0;
uint16_t width = 0;
uint16_t height = 0;
std::vector<uint8_t> y, u, v;
int32_t ystride = 0, ustride = 0, vstride = 0;
VideoTestData();
~VideoTestData();
static void receive_frame(uint32_t friend_number, uint16_t width, uint16_t height,
const uint8_t *y, const uint8_t *u, const uint8_t *v, int32_t ystride, int32_t ustride,
int32_t vstride, void *user_data);
double calculate_mse(const std::vector<uint8_t> &y_orig) const;
};
// Common Test Fixture
class AvTest : public ::testing::Test {
protected:
void SetUp() override;
void TearDown() override;
Logger *log = nullptr;
Mono_Time *mono_time = nullptr;
MockTime tm;
};
#endif // TOXAV_AV_TEST_SUPPORT_H
+73
View File
@@ -0,0 +1,73 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Copyright © 2025 The TokTok team.
*/
#include <benchmark/benchmark.h>
#include <cstring>
#include <vector>
#include "../toxcore/logger.h"
#include "../toxcore/mono_time.h"
#include "../toxcore/os_memory.h"
#include "av_test_support.hh"
#include "rtp.h"
namespace {
class RtpBench : public benchmark::Fixture {
public:
void SetUp(const ::benchmark::State &) override
{
const Memory *mem = os_memory();
log = logger_new(mem);
mono_time = mono_time_new(mem, nullptr, nullptr);
mock.store_last_packet_only = true;
session = rtp_new(log, RTP_TYPE_VIDEO, mono_time, RtpMock::send_packet, &mock, nullptr,
nullptr, nullptr, &mock, RtpMock::noop_cb);
}
void TearDown(const ::benchmark::State &) override
{
const Memory *mem = os_memory();
rtp_kill(log, session);
mono_time_free(mem, mono_time);
logger_kill(log);
}
Logger *log = nullptr;
Mono_Time *mono_time = nullptr;
RTPSession *session = nullptr;
RtpMock mock;
};
BENCHMARK_DEFINE_F(RtpBench, SendData)(benchmark::State &state)
{
size_t data_size = static_cast<size_t>(state.range(0));
std::vector<uint8_t> data(data_size, 0xAA);
for (auto _ : state) {
rtp_send_data(log, session, data.data(), static_cast<uint32_t>(data.size()), false);
benchmark::DoNotOptimize(mock.captured_packets.back());
}
}
BENCHMARK_REGISTER_F(RtpBench, SendData)->Arg(100)->Arg(1000)->Arg(5000);
BENCHMARK_DEFINE_F(RtpBench, ReceivePacket)(benchmark::State &state)
{
size_t data_size = static_cast<size_t>(state.range(0));
std::vector<uint8_t> data(data_size, 0xAA);
rtp_send_data(log, session, data.data(), static_cast<uint32_t>(data.size()), false);
std::vector<uint8_t> packet = mock.captured_packets.back();
for (auto _ : state) {
rtp_receive_packet(session, packet.data(), packet.size());
}
}
BENCHMARK_REGISTER_F(RtpBench, ReceivePacket)->Arg(100)->Arg(1000);
} // namespace
BENCHMARK_MAIN();
+196
View File
@@ -0,0 +1,196 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Copyright © 2025 The TokTok team.
*/
#include <benchmark/benchmark.h>
#include <vector>
#include "../toxcore/logger.h"
#include "../toxcore/mono_time.h"
#include "../toxcore/os_memory.h"
#include "av_test_support.hh"
#include "rtp.h"
#include "video.h"
namespace {
class VideoBench : public benchmark::Fixture {
public:
void SetUp(const ::benchmark::State &state) override
{
const Memory *mem = os_memory();
log = logger_new(mem);
tm.t = 1000;
mono_time = mono_time_new(mem, mock_time_cb, &tm);
vc = vc_new(log, mono_time, 123, nullptr, nullptr);
width = static_cast<uint16_t>(state.range(0));
height = static_cast<uint16_t>(state.range(1));
// Use a standard bitrate for benchmarks
vc_reconfigure_encoder(vc, 2000, width, height, -1);
y.resize(static_cast<size_t>(width) * height);
u.resize((static_cast<size_t>(width) / 2) * (static_cast<size_t>(height) / 2));
v.resize((static_cast<size_t>(width) / 2) * (static_cast<size_t>(height) / 2));
rtp_mock.capture_packets = false; // Disable capturing for benchmarks
rtp_mock.auto_forward = true;
rtp_mock.recv_session = rtp_new(log, RTP_TYPE_VIDEO, mono_time, RtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, vc, RtpMock::video_cb);
}
void TearDown(const ::benchmark::State &state) override
{
const Memory *mem = os_memory();
if (rtp_mock.recv_session) {
rtp_kill(log, rtp_mock.recv_session);
}
if (vc) {
vc_kill(vc);
}
if (mono_time) {
mono_time_free(mem, mono_time);
}
if (log) {
logger_kill(log);
}
}
Logger *log = nullptr;
Mono_Time *mono_time = nullptr;
MockTime tm;
VCSession *vc = nullptr;
RtpMock rtp_mock;
uint16_t width = 0, height = 0;
std::vector<uint8_t> y, u, v;
};
// Benchmark encoding a sequence of frames.
// Measures how the encoder performs as it builds up temporal state.
BENCHMARK_DEFINE_F(VideoBench, EncodeSequence)(benchmark::State &state)
{
int frame_index = 0;
// Pre-fill frames to avoid measuring fill_frame time
const int num_prefilled = 100;
std::vector<std::vector<uint8_t>> ys(num_prefilled, std::vector<uint8_t>(width * height));
std::vector<std::vector<uint8_t>> us(
num_prefilled, std::vector<uint8_t>((width / 2) * (height / 2)));
std::vector<std::vector<uint8_t>> vs(
num_prefilled, std::vector<uint8_t>((width / 2) * (height / 2)));
for (int i = 0; i < num_prefilled; ++i) {
fill_video_frame(width, height, i, ys[i], us[i], vs[i]);
}
for (auto _ : state) {
int idx = frame_index % num_prefilled;
// Force a keyframe every 100 frames to simulate real-world periodic keyframes
int flags = (frame_index % 100 == 0) ? VC_EFLAG_FORCE_KF : VC_EFLAG_NONE;
vc_encode(vc, width, height, ys[idx].data(), us[idx].data(), vs[idx].data(), flags);
vc_increment_frame_counter(vc);
uint8_t *pkt_data;
uint32_t pkt_size;
bool is_keyframe;
while (vc_get_cx_data(vc, &pkt_data, &pkt_size, &is_keyframe)) {
benchmark::DoNotOptimize(pkt_data);
benchmark::DoNotOptimize(pkt_size);
}
frame_index++;
}
}
BENCHMARK_REGISTER_F(VideoBench, EncodeSequence)
->Args({320, 240})
->Args({640, 480})
->Args({1280, 720})
->Args({1920, 1080});
// Benchmark decoding a sequence of frames.
// First pre-encodes a sequence, then measures decoding performance.
BENCHMARK_DEFINE_F(VideoBench, DecodeSequence)(benchmark::State &state)
{
const int num_frames = 100;
std::vector<std::vector<uint8_t>> encoded_frames(num_frames);
std::vector<bool> is_keyframe_list(num_frames);
// Pre-encode
for (int i = 0; i < num_frames; ++i) {
fill_video_frame(width, height, i, y, u, v);
int flags = (i == 0) ? VC_EFLAG_FORCE_KF : VC_EFLAG_NONE;
vc_encode(vc, width, height, y.data(), u.data(), v.data(), flags);
vc_increment_frame_counter(vc);
uint8_t *pkt_data;
uint32_t pkt_size;
bool is_kf;
while (vc_get_cx_data(vc, &pkt_data, &pkt_size, &is_kf)) {
encoded_frames[i].insert(encoded_frames[i].end(), pkt_data, pkt_data + pkt_size);
is_keyframe_list[i] = is_kf;
}
}
int frame_index = 0;
for (auto _ : state) {
int idx = frame_index % num_frames;
const auto &encoded_data = encoded_frames[idx];
rtp_send_data(log, rtp_mock.recv_session, encoded_data.data(),
static_cast<uint32_t>(encoded_data.size()), is_keyframe_list[idx]);
vc_iterate(vc);
frame_index++;
}
}
BENCHMARK_REGISTER_F(VideoBench, DecodeSequence)
->Args({320, 240})
->Args({640, 480})
->Args({1280, 720})
->Args({1920, 1080});
// Full end-to-end sequence benchmark (Encode -> RTP -> Decode)
BENCHMARK_DEFINE_F(VideoBench, FullSequence)(benchmark::State &state)
{
int frame_index = 0;
const int num_prefilled = 100;
std::vector<std::vector<uint8_t>> ys(num_prefilled, std::vector<uint8_t>(width * height));
std::vector<std::vector<uint8_t>> us(
num_prefilled, std::vector<uint8_t>((width / 2) * (height / 2)));
std::vector<std::vector<uint8_t>> vs(
num_prefilled, std::vector<uint8_t>((width / 2) * (height / 2)));
for (int i = 0; i < num_prefilled; ++i) {
fill_video_frame(width, height, i, ys[i], us[i], vs[i]);
}
for (auto _ : state) {
int idx = frame_index % num_prefilled;
int flags = (frame_index % 100 == 0) ? VC_EFLAG_FORCE_KF : VC_EFLAG_NONE;
vc_encode(vc, width, height, ys[idx].data(), us[idx].data(), vs[idx].data(), flags);
vc_increment_frame_counter(vc);
uint8_t *pkt_data;
uint32_t pkt_size;
bool is_keyframe = false;
// We need to collect all packets for the frame before sending to decoder
std::vector<uint8_t> frame_data;
while (vc_get_cx_data(vc, &pkt_data, &pkt_size, &is_keyframe)) {
frame_data.insert(frame_data.end(), pkt_data, pkt_data + pkt_size);
}
rtp_send_data(log, rtp_mock.recv_session, frame_data.data(),
static_cast<uint32_t>(frame_data.size()), is_keyframe);
vc_iterate(vc);
frame_index++;
}
}
BENCHMARK_REGISTER_F(VideoBench, FullSequence)
->Args({320, 240})
->Args({640, 480})
->Args({1280, 720})
->Args({1920, 1080});
}
BENCHMARK_MAIN();
+212 -125
View File
@@ -3,123 +3,19 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <cmath>
#include <vector>
#include "../toxcore/logger.h"
#include "../toxcore/mono_time.h"
#include "../toxcore/network.h"
#include "../toxcore/os_memory.h"
#include "av_test_support.hh"
#include "rtp.h"
namespace {
struct VideoTimeMock {
uint64_t t;
};
uint64_t video_mock_time_cb(void *ud) { return static_cast<VideoTimeMock *>(ud)->t; }
void test_logger_cb(void *context, Logger_Level level, const char *file, uint32_t line,
const char *func, const char *message, void *userdata)
{
(void)context;
(void)userdata;
const char *level_str = "UNKNOWN";
switch (level) {
case LOGGER_LEVEL_TRACE:
level_str = "TRACE";
break;
case LOGGER_LEVEL_DEBUG:
level_str = "DEBUG";
break;
case LOGGER_LEVEL_INFO:
level_str = "INFO";
break;
case LOGGER_LEVEL_WARNING:
level_str = "WARN";
break;
case LOGGER_LEVEL_ERROR:
level_str = "ERROR";
break;
}
printf("[%s] %s:%u %s: %s\n", level_str, file, line, func, message);
}
struct VideoTestData {
uint32_t friend_number = 0;
uint16_t width = 0;
uint16_t height = 0;
std::vector<uint8_t> y, u, v;
int32_t ystride = 0, ustride = 0, vstride = 0;
VideoTestData();
~VideoTestData();
static void receive_frame(uint32_t friend_number, uint16_t width, uint16_t height,
const uint8_t *y, const uint8_t *u, const uint8_t *v, int32_t ystride, int32_t ustride,
int32_t vstride, void *user_data)
{
auto *self = static_cast<VideoTestData *>(user_data);
self->friend_number = friend_number;
self->width = width;
self->height = height;
self->ystride = ystride;
self->ustride = ustride;
self->vstride = vstride;
self->y.assign(y, y + static_cast<size_t>(std::abs(ystride)) * height);
self->u.assign(u, u + static_cast<size_t>(std::abs(ustride)) * (height / 2));
self->v.assign(v, v + static_cast<size_t>(std::abs(vstride)) * (height / 2));
}
};
VideoTestData::VideoTestData() = default;
VideoTestData::~VideoTestData() = default;
struct VideoRtpMock {
RTPSession *recv_session = nullptr;
std::vector<std::vector<uint8_t>> captured_packets;
bool auto_forward = true;
static int send_packet(void *user_data, const uint8_t *data, uint16_t length)
{
auto *self = static_cast<VideoRtpMock *>(user_data);
self->captured_packets.push_back(std::vector<uint8_t>(data, data + length));
if (self->auto_forward && self->recv_session) {
rtp_receive_packet(self->recv_session, data, length);
}
return 0;
}
static int video_cb(const Mono_Time *mono_time, void *cs, RTPMessage *msg)
{
return vc_queue_message(mono_time, cs, msg);
}
};
class VideoTest : public ::testing::Test {
protected:
void SetUp() override
{
const Memory *mem = os_memory();
log = logger_new(mem);
logger_callback_log(log, test_logger_cb, nullptr, nullptr);
tm.t = 1000;
mono_time = mono_time_new(mem, video_mock_time_cb, &tm);
mono_time_update(mono_time);
}
void TearDown() override
{
const Memory *mem = os_memory();
mono_time_free(mem, mono_time);
logger_kill(log);
}
Logger *log;
Mono_Time *mono_time;
VideoTimeMock tm;
};
using VideoTest = AvTest;
TEST_F(VideoTest, BasicNewKill)
{
@@ -135,11 +31,11 @@ TEST_F(VideoTest, EncodeDecodeLoop)
VCSession *vc = vc_new(log, mono_time, 123, VideoTestData::receive_frame, &data);
ASSERT_NE(vc, nullptr);
VideoRtpMock rtp_mock;
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_VIDEO, mono_time, VideoRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, vc, VideoRtpMock::video_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_VIDEO, mono_time, VideoRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, vc, VideoRtpMock::video_cb);
RtpMock rtp_mock;
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_VIDEO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, vc, RtpMock::video_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_VIDEO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, vc, RtpMock::video_cb);
rtp_mock.recv_session = recv_rtp;
uint16_t width = 320;
@@ -176,6 +72,197 @@ TEST_F(VideoTest, EncodeDecodeLoop)
vc_kill(vc);
}
TEST_F(VideoTest, EncodeDecodeSequence)
{
VideoTestData data;
VCSession *vc = vc_new(log, mono_time, 123, VideoTestData::receive_frame, &data);
ASSERT_NE(vc, nullptr);
RtpMock rtp_mock;
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_VIDEO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, vc, RtpMock::video_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_VIDEO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, vc, RtpMock::video_cb);
rtp_mock.recv_session = recv_rtp;
uint16_t width = 320;
uint16_t height = 240;
uint32_t bitrate = 2000;
ASSERT_EQ(vc_reconfigure_encoder(vc, bitrate, width, height, -1), 0);
for (int i = 0; i < 20; ++i) {
std::vector<uint8_t> y(width * height);
std::vector<uint8_t> u((width / 2) * (height / 2));
std::vector<uint8_t> v((width / 2) * (height / 2));
// Background
std::fill(y.begin(), y.end(), 16);
std::fill(u.begin(), u.end(), 128);
std::fill(v.begin(), v.end(), 128);
// Moving square
int sq_size = 64;
int x0 = (i * 8) % (width - sq_size);
int y0 = (i * 4) % (height - sq_size);
for (int r = 0; r < sq_size; ++r) {
for (int c = 0; c < sq_size; ++c) {
y[(y0 + r) * width + (x0 + c)] = 200;
}
}
ASSERT_EQ(vc_encode(vc, width, height, y.data(), u.data(), v.data(),
i == 0 ? VC_EFLAG_FORCE_KF : VC_EFLAG_NONE),
0);
vc_increment_frame_counter(vc);
uint8_t *pkt_data;
uint32_t pkt_size;
bool is_keyframe;
while (vc_get_cx_data(vc, &pkt_data, &pkt_size, &is_keyframe)) {
rtp_send_data(log, send_rtp, pkt_data, pkt_size, is_keyframe);
}
vc_iterate(vc);
ASSERT_EQ(data.width, width);
ASSERT_EQ(data.height, height);
double mse = data.calculate_mse(y);
// Expect MSE to be reasonably low for high bitrate
EXPECT_LT(mse, 100.0) << "Frame " << i << " MSE too high: " << mse;
}
rtp_kill(log, send_rtp);
rtp_kill(log, recv_rtp);
vc_kill(vc);
}
TEST_F(VideoTest, EncodeDecodeResolutionChange)
{
VideoTestData data;
VCSession *vc = vc_new(log, mono_time, 123, VideoTestData::receive_frame, &data);
ASSERT_NE(vc, nullptr);
RtpMock rtp_mock;
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_VIDEO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, vc, RtpMock::video_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_VIDEO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, vc, RtpMock::video_cb);
rtp_mock.recv_session = recv_rtp;
uint16_t widths[] = {320, 160, 480};
uint16_t heights[] = {240, 120, 360};
for (int res = 0; res < 3; ++res) {
uint16_t width = widths[res];
uint16_t height = heights[res];
ASSERT_EQ(vc_reconfigure_encoder(vc, 2000, width, height, -1), 0);
for (int i = 0; i < 5; ++i) {
std::vector<uint8_t> y(width * height);
std::vector<uint8_t> u((width / 2) * (height / 2));
std::vector<uint8_t> v((width / 2) * (height / 2));
std::fill(y.begin(), y.end(), static_cast<uint8_t>((res * 50 + i * 10) % 256));
std::fill(u.begin(), u.end(), 128);
std::fill(v.begin(), v.end(), 128);
ASSERT_EQ(vc_encode(vc, width, height, y.data(), u.data(), v.data(),
i == 0 ? VC_EFLAG_FORCE_KF : VC_EFLAG_NONE),
0);
vc_increment_frame_counter(vc);
uint8_t *pkt_data;
uint32_t pkt_size;
bool is_keyframe;
while (vc_get_cx_data(vc, &pkt_data, &pkt_size, &is_keyframe)) {
rtp_send_data(log, send_rtp, pkt_data, pkt_size, is_keyframe);
}
vc_iterate(vc);
ASSERT_EQ(data.width, width);
ASSERT_EQ(data.height, height);
double mse = data.calculate_mse(y);
EXPECT_LT(mse, 100.0) << "Res " << res << " Frame " << i << " MSE too high: " << mse;
}
}
rtp_kill(log, send_rtp);
rtp_kill(log, recv_rtp);
vc_kill(vc);
}
TEST_F(VideoTest, EncodeDecodeBitrateImpact)
{
uint32_t bitrates[] = {100, 500, 2000};
double mses[3];
uint16_t width = 320;
uint16_t height = 240;
for (int b = 0; b < 3; ++b) {
VideoTestData data;
VCSession *vc = vc_new(log, mono_time, 123, VideoTestData::receive_frame, &data);
ASSERT_NE(vc, nullptr);
RtpMock rtp_mock;
RTPSession *send_rtp = rtp_new(log, RTP_TYPE_VIDEO, mono_time, RtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, vc, RtpMock::video_cb);
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_VIDEO, mono_time, RtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, vc, RtpMock::video_cb);
rtp_mock.recv_session = recv_rtp;
ASSERT_EQ(vc_reconfigure_encoder(vc, bitrates[b], width, height, -1), 0);
double total_mse = 0;
int frames = 10;
for (int i = 0; i < frames; ++i) {
std::vector<uint8_t> y(width * height);
std::vector<uint8_t> u((width / 2) * (height / 2));
std::vector<uint8_t> v((width / 2) * (height / 2));
for (size_t j = 0; j < y.size(); ++j)
y[j] = static_cast<uint8_t>((j + i * 10) % 256);
std::fill(u.begin(), u.end(), 128);
std::fill(v.begin(), v.end(), 128);
ASSERT_EQ(vc_encode(vc, width, height, y.data(), u.data(), v.data(),
i == 0 ? VC_EFLAG_FORCE_KF : VC_EFLAG_NONE),
0);
vc_increment_frame_counter(vc);
uint8_t *pkt_data;
uint32_t pkt_size;
bool is_keyframe;
while (vc_get_cx_data(vc, &pkt_data, &pkt_size, &is_keyframe)) {
rtp_send_data(log, send_rtp, pkt_data, pkt_size, is_keyframe);
}
vc_iterate(vc);
total_mse += data.calculate_mse(y);
}
mses[b] = total_mse / frames;
rtp_kill(log, send_rtp);
rtp_kill(log, recv_rtp);
vc_kill(vc);
}
// Quality should generally improve (MSE decrease) as bitrate increases.
// 100kbps should have significantly higher MSE than 2000kbps for this complex pattern.
EXPECT_GT(mses[0], mses[1]);
EXPECT_GT(mses[1], mses[2]);
printf("MSE results: 100kbps: %f, 500kbps: %f, 2000kbps: %f\n", mses[0], mses[1], mses[2]);
}
TEST_F(VideoTest, ReconfigureEncoder)
{
VideoTestData data;
@@ -215,12 +302,12 @@ TEST_F(VideoTest, QueueInvalidMessage)
VCSession *vc = vc_new(log, mono_time, 123, VideoTestData::receive_frame, &data);
ASSERT_NE(vc, nullptr);
VideoRtpMock rtp_mock;
RtpMock rtp_mock;
// Create an audio RTP session but try to queue to video session
RTPSession *audio_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, VideoRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, vc, VideoRtpMock::video_cb);
RTPSession *video_recv_rtp = rtp_new(log, RTP_TYPE_VIDEO, mono_time, VideoRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, vc, VideoRtpMock::video_cb);
RTPSession *audio_rtp = rtp_new(log, RTP_TYPE_AUDIO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, vc, RtpMock::video_cb);
RTPSession *video_recv_rtp = rtp_new(log, RTP_TYPE_VIDEO, mono_time, RtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, vc, RtpMock::video_cb);
rtp_mock.recv_session = video_recv_rtp;
std::vector<uint8_t> dummy_audio(100, 0);
@@ -262,9 +349,9 @@ TEST_F(VideoTest, LcfdAndSpecialPackets)
VCSession *vc = vc_new(log, mono_time, 123, VideoTestData::receive_frame, &data);
ASSERT_NE(vc, nullptr);
VideoRtpMock rtp_mock;
RTPSession *video_recv_rtp = rtp_new(log, RTP_TYPE_VIDEO, mono_time, VideoRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, vc, VideoRtpMock::video_cb);
RtpMock rtp_mock;
RTPSession *video_recv_rtp = rtp_new(log, RTP_TYPE_VIDEO, mono_time, RtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, vc, RtpMock::video_cb);
rtp_mock.recv_session = video_recv_rtp;
// 1. Test lcfd update
@@ -286,8 +373,8 @@ TEST_F(VideoTest, LcfdAndSpecialPackets)
EXPECT_EQ(vc_get_lcfd(vc), 50u); // Should still be 50
// 3. Test dummy packet PT = (RTP_TYPE_VIDEO + 2) % 128
RTPSession *dummy_rtp = rtp_new(log, (RTP_TYPE_VIDEO + 2), mono_time, VideoRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, vc, VideoRtpMock::video_cb);
RTPSession *dummy_rtp = rtp_new(log, (RTP_TYPE_VIDEO + 2), mono_time, RtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, vc, RtpMock::video_cb);
rtp_mock.recv_session = dummy_rtp;
rtp_send_data(
log, dummy_rtp, dummy_frame.data(), static_cast<uint32_t>(dummy_frame.size()), false);
@@ -354,9 +441,9 @@ TEST_F(VideoTest, LyingLengthOOB)
VCSession *vc = vc_new(log, mono_time, 123, VideoTestData::receive_frame, &data);
ASSERT_NE(vc, nullptr);
VideoRtpMock rtp_mock;
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_VIDEO, mono_time, VideoRtpMock::send_packet,
&rtp_mock, nullptr, nullptr, nullptr, vc, VideoRtpMock::video_cb);
RtpMock rtp_mock;
RTPSession *recv_rtp = rtp_new(log, RTP_TYPE_VIDEO, mono_time, RtpMock::send_packet, &rtp_mock,
nullptr, nullptr, nullptr, vc, RtpMock::video_cb);
rtp_mock.recv_session = recv_rtp;
// Craft a malicious RTP packet