blob: d6bf6d941b648ad05ede08cb4af8bb1d249e1eaa [file]
/*
* Copyright (c) 2026 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
#include "test/testsupport/switching_frame_reader.h"
#include <stdio.h>
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "api/scoped_refptr.h"
#include "api/units/time_delta.h"
#include "api/video/i420_buffer.h"
#include "api/video/resolution.h"
#include "test/gtest.h"
#include "test/testsupport/file_utils.h"
#include "test/testsupport/frame_reader.h"
#include "test/testsupport/frame_writer.h"
#include "third_party/libyuv/include/libyuv/planar_functions.h"
namespace webrtc {
namespace test {
namespace {
// A simple fake FrameReader that generates frames with a constant luma (Y)
// value.
class ConstantLumaFrameReader : public FrameReader {
public:
ConstantLumaFrameReader(Resolution resolution,
uint8_t y_value,
int total_frames)
: resolution_(resolution),
y_value_(y_value),
total_frames_(total_frames) {}
scoped_refptr<I420Buffer> PullFrame() override { return PullFrame(nullptr); }
scoped_refptr<I420Buffer> PullFrame(int* frame_num) override {
return PullFrame(frame_num, resolution_, kNoScale);
}
scoped_refptr<I420Buffer> PullFrame(int* frame_num,
Resolution resolution,
Ratio framerate_scale) override {
if (frame_num != nullptr) {
*frame_num = frame_num_;
}
scoped_refptr<I420Buffer> buffer = ReadFrame(frame_num_, resolution);
++frame_num_;
return buffer;
}
scoped_refptr<I420Buffer> ReadFrame(int frame_num) override {
return ReadFrame(frame_num, resolution_);
}
scoped_refptr<I420Buffer> ReadFrame(int frame_num,
Resolution resolution) override {
scoped_refptr<I420Buffer> buffer =
I420Buffer::Create(resolution.width, resolution.height);
libyuv::I420Rect(buffer->MutableDataY(), buffer->StrideY(),
buffer->MutableDataU(), buffer->StrideU(),
buffer->MutableDataV(), buffer->StrideV(), 0, 0,
buffer->width(), buffer->height(), y_value_, 128, 128);
return buffer;
}
int num_frames() const override { return total_frames_; }
private:
const Resolution resolution_;
const uint8_t y_value_;
const int total_frames_;
int frame_num_ = 0;
};
TEST(SwitchingFrameReaderTest, SwitchesBetweenTwoReadersAtInterval) {
constexpr Resolution kResolution = {.width = 640, .height = 360};
constexpr int kFps = 30;
constexpr TimeDelta kSwitchInterval = TimeDelta::Millis(100);
std::vector<std::unique_ptr<FrameReader>> readers;
readers.push_back(std::make_unique<ConstantLumaFrameReader>(
kResolution, /*y_val=*/10, 100));
readers.push_back(std::make_unique<ConstantLumaFrameReader>(
kResolution, /*y_val=*/20, 100));
SwitchingFrameReader switching_reader(std::move(readers), kResolution, kFps,
kSwitchInterval);
// At 30 fps, 100ms corresponds to exactly 3 frames.
// Frames 0, 1, 2 -> Reader 0 (Y = 10).
for (int i = 0; i < 3; ++i) {
EXPECT_EQ(switching_reader.current_reader_index(), 0);
scoped_refptr<I420Buffer> frame = switching_reader.PullFrame();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->DataY()[0], 10);
}
// Frames 3, 4, 5 -> Reader 1 (Y = 20).
for (int i = 0; i < 3; ++i) {
EXPECT_EQ(switching_reader.current_reader_index(), 1);
scoped_refptr<I420Buffer> frame = switching_reader.PullFrame();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->DataY()[0], 20);
}
// Frames 6, 7, 8 -> Loops back to Reader 0 (Y = 10).
for (int i = 0; i < 3; ++i) {
EXPECT_EQ(switching_reader.current_reader_index(), 0);
scoped_refptr<I420Buffer> frame = switching_reader.PullFrame();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->DataY()[0], 10);
}
}
TEST(SwitchingFrameReaderTest, LoopsOverThreeReaders) {
constexpr Resolution kResolution = {.width = 320, .height = 180};
constexpr int kFps = 10;
constexpr TimeDelta kSwitchInterval = TimeDelta::Millis(200);
std::vector<std::unique_ptr<FrameReader>> readers;
readers.push_back(
std::make_unique<ConstantLumaFrameReader>(kResolution, /*y_val=*/10, 50));
readers.push_back(
std::make_unique<ConstantLumaFrameReader>(kResolution, /*y_val=*/20, 50));
readers.push_back(
std::make_unique<ConstantLumaFrameReader>(kResolution, /*y_val=*/30, 50));
SwitchingFrameReader switching_reader(std::move(readers), kResolution, kFps,
kSwitchInterval);
// At 10 fps, 200ms is 2 frames per interval.
// Reader 0
for (int i = 0; i < 2; ++i) {
EXPECT_EQ(switching_reader.current_reader_index(), 0);
scoped_refptr<I420Buffer> frame = switching_reader.PullFrame();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->DataY()[0], 10);
}
// Reader 1
for (int i = 0; i < 2; ++i) {
EXPECT_EQ(switching_reader.current_reader_index(), 1);
scoped_refptr<I420Buffer> frame = switching_reader.PullFrame();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->DataY()[0], 20);
}
// Reader 2
for (int i = 0; i < 2; ++i) {
EXPECT_EQ(switching_reader.current_reader_index(), 2);
scoped_refptr<I420Buffer> frame = switching_reader.PullFrame();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->DataY()[0], 30);
}
// Reader 0 again
for (int i = 0; i < 2; ++i) {
EXPECT_EQ(switching_reader.current_reader_index(), 0);
scoped_refptr<I420Buffer> frame = switching_reader.PullFrame();
ASSERT_TRUE(frame);
EXPECT_EQ(frame->DataY()[0], 10);
}
}
TEST(SwitchingFrameReaderTest, ScalesDifferingInputResolutionToTarget) {
constexpr Resolution kTargetResolution = {.width = 640, .height = 360};
constexpr Resolution kSourceRes0 = {.width = 320, .height = 180};
constexpr Resolution kSourceRes1 = {.width = 1280, .height = 720};
constexpr int kFps = 30;
constexpr TimeDelta kSwitchInterval = TimeDelta::Seconds(1);
std::vector<std::unique_ptr<FrameReader>> readers;
readers.push_back(
std::make_unique<ConstantLumaFrameReader>(kSourceRes0, /*y_val=*/10, 50));
readers.push_back(
std::make_unique<ConstantLumaFrameReader>(kSourceRes1, /*y_val=*/20, 50));
SwitchingFrameReader switching_reader(std::move(readers), kTargetResolution,
kFps, kSwitchInterval);
// Pull from reader 0 (native 320x180) -> scaled to 640x360.
scoped_refptr<I420Buffer> frame0 = switching_reader.PullFrame();
ASSERT_TRUE(frame0);
EXPECT_EQ(frame0->width(), kTargetResolution.width);
EXPECT_EQ(frame0->height(), kTargetResolution.height);
}
TEST(SwitchingFrameReaderTest, ReadFrameAccessesCorrectReaderAndSubIndex) {
constexpr Resolution kResolution = {.width = 320, .height = 180};
constexpr int kFps = 10;
constexpr TimeDelta kSwitchInterval = TimeDelta::Millis(200); // 2 frames
std::vector<std::unique_ptr<FrameReader>> readers;
readers.push_back(
std::make_unique<ConstantLumaFrameReader>(kResolution, /*y_val=*/10, 50));
readers.push_back(
std::make_unique<ConstantLumaFrameReader>(kResolution, /*y_val=*/20, 50));
SwitchingFrameReader switching_reader(std::move(readers), kResolution, kFps,
kSwitchInterval);
// Frame 0 -> Reader 0
scoped_refptr<I420Buffer> frame0 = switching_reader.ReadFrame(0);
ASSERT_TRUE(frame0);
EXPECT_EQ(frame0->DataY()[0], 10);
// Frame 2 -> Reader 1 (at sub-index 0)
scoped_refptr<I420Buffer> frame2 = switching_reader.ReadFrame(2);
ASSERT_TRUE(frame2);
EXPECT_EQ(frame2->DataY()[0], 20);
// Frame 4 -> Reader 0 (at sub-index 2)
scoped_refptr<I420Buffer> frame4 = switching_reader.ReadFrame(4);
ASSERT_TRUE(frame4);
EXPECT_EQ(frame4->DataY()[0], 10);
}
TEST(SwitchingFrameReaderTest, ReportsTotalFramesAsSumOfReaders) {
constexpr Resolution kResolution = {.width = 320, .height = 180};
std::vector<std::unique_ptr<FrameReader>> readers;
readers.push_back(std::make_unique<ConstantLumaFrameReader>(kResolution, 10,
/*frames=*/25));
readers.push_back(std::make_unique<ConstantLumaFrameReader>(kResolution, 20,
/*frames=*/35));
SwitchingFrameReader switching_reader(std::move(readers), kResolution, 30,
TimeDelta::Seconds(1));
EXPECT_EQ(switching_reader.num_frames(), 60);
}
TEST(SwitchingFrameReaderTest, ReadsFromMultipleResourceFiles) {
const std::string file1 = test::ResourcePath("foreman_cif_short", "yuv");
const std::string file2 = test::ResourcePath("foreman_128x96", "yuv");
constexpr Resolution kTargetResolution = {.width = 640, .height = 360};
constexpr int kFps = 30;
constexpr TimeDelta kSwitchInterval = TimeDelta::Millis(100);
std::unique_ptr<FrameReader> reader = CreateSwitchingFrameReader(
{file1, file2}, kTargetResolution, kFps, kSwitchInterval,
YuvFrameReaderImpl::RepeatMode::kRepeat);
ASSERT_NE(reader, nullptr);
// At 30 fps, 100ms corresponds to 3 frames from Reader 0 (foreman_cif_short).
// Scaled from CIF (352x288) to target resolution 640x360.
for (int i = 0; i < 3; ++i) {
scoped_refptr<I420Buffer> frame = reader->PullFrame();
ASSERT_NE(frame, nullptr);
EXPECT_EQ(frame->width(), kTargetResolution.width);
EXPECT_EQ(frame->height(), kTargetResolution.height);
}
// Next 3 frames are from Reader 1 (foreman_128x96).
// Scaled from 128x96 to target resolution 640x360.
for (int i = 0; i < 3; ++i) {
scoped_refptr<I420Buffer> frame = reader->PullFrame();
ASSERT_NE(frame, nullptr);
EXPECT_EQ(frame->width(), kTargetResolution.width);
EXPECT_EQ(frame->height(), kTargetResolution.height);
}
}
TEST(SwitchingFrameReaderTest, MixesYuvAndY4mClips) {
const std::string yuv_file = test::ResourcePath("foreman_128x96", "yuv");
// Create a temporary Y4M file using Y4mFrameWriterImpl.
std::string y4m_file =
test::TempFilename(test::OutputPath(), "switching_frame_reader_y4m");
Y4mFrameWriterImpl writer(y4m_file, /*width=*/320, /*height=*/240,
/*frame_rate=*/30);
ASSERT_TRUE(writer.Init());
std::vector<uint8_t> frame_data(writer.FrameLength(), 42);
for (int i = 0; i < 5; ++i) {
ASSERT_TRUE(writer.WriteFrame(frame_data.data()));
}
writer.Close();
constexpr Resolution kTargetResolution = {.width = 640, .height = 480};
constexpr int kFps = 30;
constexpr TimeDelta kSwitchInterval = TimeDelta::Millis(100);
std::unique_ptr<FrameReader> reader = CreateSwitchingFrameReader(
{yuv_file, y4m_file}, kTargetResolution, kFps, kSwitchInterval,
YuvFrameReaderImpl::RepeatMode::kRepeat);
ASSERT_NE(reader, nullptr);
// First 3 frames from Reader 0 (YUV, foreman_128x96).
for (int i = 0; i < 3; ++i) {
scoped_refptr<I420Buffer> frame = reader->PullFrame();
ASSERT_NE(frame, nullptr);
EXPECT_EQ(frame->width(), kTargetResolution.width);
EXPECT_EQ(frame->height(), kTargetResolution.height);
}
// Next 3 frames from Reader 1 (Y4M, 320x240).
for (int i = 0; i < 3; ++i) {
scoped_refptr<I420Buffer> frame = reader->PullFrame();
ASSERT_NE(frame, nullptr);
EXPECT_EQ(frame->width(), kTargetResolution.width);
EXPECT_EQ(frame->height(), kTargetResolution.height);
}
remove(y4m_file.c_str());
}
TEST(SwitchingFrameReaderTest, HalfFramerateSkipsFrames) {
constexpr Resolution kResolution = {.width = 320, .height = 180};
constexpr int kFps = 30;
constexpr TimeDelta kSwitchInterval = TimeDelta::Millis(100);
std::vector<std::unique_ptr<FrameReader>> readers;
readers.push_back(
std::make_unique<ConstantLumaFrameReader>(kResolution, /*y_val=*/10, 50));
readers.push_back(
std::make_unique<ConstantLumaFrameReader>(kResolution, /*y_val=*/20, 50));
SwitchingFrameReader reader(std::move(readers), kResolution, kFps,
kSwitchInterval);
// For half framerate (1/2), expected pulled frames are 0, 2, 4.
const std::vector<int> expected_frames = {0, 2, 4};
for (int expected : expected_frames) {
int pulled_frame = -1;
scoped_refptr<I420Buffer> frame = reader.PullFrame(
&pulled_frame, kResolution, FrameReader::Ratio({.num = 1, .den = 2}));
ASSERT_NE(frame, nullptr);
EXPECT_EQ(pulled_frame, expected);
}
}
TEST(SwitchingFrameReaderTest, DoubleFramerateRepeatsFrames) {
constexpr Resolution kResolution = {.width = 320, .height = 180};
constexpr int kFps = 30;
constexpr TimeDelta kSwitchInterval = TimeDelta::Millis(100);
std::vector<std::unique_ptr<FrameReader>> readers;
readers.push_back(
std::make_unique<ConstantLumaFrameReader>(kResolution, /*y_val=*/10, 50));
readers.push_back(
std::make_unique<ConstantLumaFrameReader>(kResolution, /*y_val=*/20, 50));
SwitchingFrameReader reader(std::move(readers), kResolution, kFps,
kSwitchInterval);
// For double framerate (2/1), expected pulled frames are 0, 0, 1, 1.
const std::vector<int> expected_frames = {0, 0, 1, 1};
for (int expected : expected_frames) {
int pulled_frame = -1;
scoped_refptr<I420Buffer> frame = reader.PullFrame(
&pulled_frame, kResolution, FrameReader::Ratio({.num = 2, .den = 1}));
ASSERT_NE(frame, nullptr);
EXPECT_EQ(pulled_frame, expected);
}
}
TEST(SwitchingFrameReaderTest, TwoThirdsFramerateScaling) {
constexpr Resolution kResolution = {.width = 320, .height = 180};
constexpr int kFps = 30;
constexpr TimeDelta kSwitchInterval = TimeDelta::Millis(100);
std::vector<std::unique_ptr<FrameReader>> readers;
readers.push_back(
std::make_unique<ConstantLumaFrameReader>(kResolution, /*y_val=*/10, 50));
readers.push_back(
std::make_unique<ConstantLumaFrameReader>(kResolution, /*y_val=*/20, 50));
SwitchingFrameReader reader(std::move(readers), kResolution, kFps,
kSwitchInterval);
// For 2/3 framerate, expected pulled frames are 0, 1, 3, 4, 6.
const std::vector<int> expected_frames = {0, 1, 3, 4, 6};
for (int expected : expected_frames) {
int pulled_frame = -1;
scoped_refptr<I420Buffer> frame = reader.PullFrame(
&pulled_frame, kResolution, FrameReader::Ratio({.num = 2, .den = 3}));
ASSERT_NE(frame, nullptr);
EXPECT_EQ(pulled_frame, expected);
}
}
TEST(SwitchingFrameReaderTest, NonIntegerSwitchingIntervalUsesMemoization) {
constexpr Resolution kResolution = {.width = 320, .height = 180};
constexpr int kFps = 15;
// 15 fps and 100ms interval => 1.5 frames per interval.
// Frame 0 (0 ms): interval 0 -> reader 0 (Y=10)
// Frame 1 (66 ms): interval 0 -> reader 0 (Y=10)
// Frame 2 (133 ms): interval 1 -> reader 1 (Y=20)
// Frame 3 (200 ms): interval 2 -> reader 0 (Y=10)
// Frame 4 (266 ms): interval 2 -> reader 0 (Y=10)
// Frame 5 (333 ms): interval 3 -> reader 1 (Y=20)
constexpr TimeDelta kSwitchInterval = TimeDelta::Millis(100);
std::vector<std::unique_ptr<FrameReader>> readers;
readers.push_back(
std::make_unique<ConstantLumaFrameReader>(kResolution, /*y_val=*/10, 50));
readers.push_back(
std::make_unique<ConstantLumaFrameReader>(kResolution, /*y_val=*/20, 50));
SwitchingFrameReader reader(std::move(readers), kResolution, kFps,
kSwitchInterval);
const std::vector<uint8_t> expected_lumas = {10, 10, 20, 10, 10, 20};
for (size_t i = 0; i < expected_lumas.size(); ++i) {
int pulled_frame = -1;
scoped_refptr<I420Buffer> frame = reader.PullFrame(&pulled_frame);
ASSERT_NE(frame, nullptr);
EXPECT_EQ(pulled_frame, static_cast<int>(i));
EXPECT_EQ(frame->DataY()[0], expected_lumas[i]);
// Also verify ReadFrame gives the identical frame.
scoped_refptr<I420Buffer> read_frame = reader.ReadFrame(pulled_frame);
ASSERT_NE(read_frame, nullptr);
EXPECT_EQ(read_frame->DataY()[0], expected_lumas[i]);
}
}
} // namespace
} // namespace test
} // namespace webrtc