| /* |
| * 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 |