blob: bc714dd02876df53ef141c903bff07f9a56ead57 [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 <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "absl/strings/match.h"
#include "absl/strings/string_view.h"
#include "api/scoped_refptr.h"
#include "api/test/frame_generator_interface.h"
#include "api/units/time_delta.h"
#include "api/video/i420_buffer.h"
#include "api/video/resolution.h"
#include "api/video/video_frame.h"
#include "rtc_base/checks.h"
#include "test/testsupport/file_utils.h"
#include "test/testsupport/frame_reader.h"
namespace webrtc {
namespace test {
namespace {
std::string ResolvePath(std::string path) {
if (absl::StartsWith(path, "res://")) {
path = path.substr(6);
}
if (test::FileExists(path)) {
return path;
}
std::string name = path;
size_t dot_pos = path.rfind('.');
if (dot_pos != std::string::npos) {
name = path.substr(0, dot_pos);
std::string ext = path.substr(dot_pos + 1);
std::string res_path = test::ResourcePath(name, ext);
if (test::FileExists(res_path)) {
return res_path;
}
} else {
std::string yuv_path = test::ResourcePath(name, "yuv");
if (test::FileExists(yuv_path)) {
return yuv_path;
}
std::string y4m_path = test::ResourcePath(name, "y4m");
if (test::FileExists(y4m_path)) {
return y4m_path;
}
}
return path;
}
std::vector<std::unique_ptr<FrameReader>> CreateReaders(
const std::vector<std::string>& file_paths,
Resolution target_resolution,
YuvFrameReaderImpl::RepeatMode repeat_mode) {
RTC_CHECK(!file_paths.empty()) << "File paths cannot be empty";
std::vector<std::unique_ptr<FrameReader>> readers;
readers.reserve(file_paths.size());
for (const std::string& raw_path : file_paths) {
std::string path = ResolvePath(raw_path);
std::unique_ptr<FrameReader> reader;
if (absl::EndsWith(path, ".y4m")) {
reader = CreateY4mFrameReader(path, repeat_mode);
} else {
Resolution file_res =
ParseResolutionFromFileName(path).value_or(target_resolution);
reader = CreateYuvFrameReader(path, file_res, repeat_mode);
}
RTC_CHECK(reader != nullptr) << "Failed to open video file: " << path;
readers.push_back(std::move(reader));
}
return readers;
}
} // namespace
int SwitchingFrameReader::RateScaler::Skip(Ratio framerate_scale) {
ticks_ = ticks_.value_or(framerate_scale.num);
int skip = 0;
while (ticks_ <= 0) {
*ticks_ += framerate_scale.num;
++skip;
}
*ticks_ -= framerate_scale.den;
return skip;
}
SwitchingFrameReader::SwitchingFrameReader(
std::vector<std::string> file_paths,
Resolution target_resolution,
int fps,
TimeDelta camera_switching_interval,
YuvFrameReaderImpl::RepeatMode repeat_mode)
: SwitchingFrameReader(
CreateReaders(file_paths, target_resolution, repeat_mode),
target_resolution,
fps,
camera_switching_interval) {}
SwitchingFrameReader::SwitchingFrameReader(
std::vector<std::unique_ptr<FrameReader>> readers,
Resolution target_resolution,
int fps,
TimeDelta camera_switching_interval)
: readers_(std::move(readers)),
target_resolution_(target_resolution),
fps_(fps),
camera_switching_interval_(camera_switching_interval) {
RTC_CHECK(!readers_.empty()) << "Readers cannot be empty";
RTC_CHECK_GT(target_resolution_.width, 0);
RTC_CHECK_GT(target_resolution_.height, 0);
RTC_CHECK_GT(fps_, 0);
RTC_CHECK_GT(camera_switching_interval_.us(), 0);
}
int SwitchingFrameReader::current_reader_index() const {
return GetFrameLocation(frame_num_).reader_index;
}
SwitchingFrameReader::FrameLocation SwitchingFrameReader::GetFrameLocation(
int frame_num) const {
RTC_CHECK_GE(frame_num, 0);
int64_t total_us =
static_cast<int64_t>(fps_) * camera_switching_interval_.us();
if (total_us % 1'000'000 == 0) {
int64_t frames_per_interval = total_us / 1'000'000;
int64_t interval = frame_num / frames_per_interval;
int reader_index = interval % readers_.size();
int64_t cycle = interval / readers_.size();
int64_t sub_frame_num =
cycle * frames_per_interval + (frame_num % frames_per_interval);
return {.reader_index = reader_index,
.sub_frame_num = static_cast<int>(sub_frame_num)};
}
if (static_cast<size_t>(frame_num) < frame_location_cache_.size()) {
return frame_location_cache_[frame_num];
}
if (count_per_reader_.empty()) {
count_per_reader_.assign(readers_.size(), 0);
}
int start = static_cast<int>(frame_location_cache_.size());
for (int i = start; i <= frame_num; ++i) {
int64_t elapsed_us = static_cast<int64_t>(i) * 1'000'000 / fps_;
int r = (elapsed_us / camera_switching_interval_.us()) % readers_.size();
frame_location_cache_.push_back(
{.reader_index = r, .sub_frame_num = count_per_reader_[r]});
++count_per_reader_[r];
}
return frame_location_cache_[frame_num];
}
scoped_refptr<I420Buffer> SwitchingFrameReader::Scale(
scoped_refptr<I420Buffer> buffer,
Resolution resolution) const {
if (!buffer) {
return nullptr;
}
if (buffer->width() == resolution.width &&
buffer->height() == resolution.height) {
return buffer;
}
scoped_refptr<I420Buffer> scaled =
I420Buffer::Create(resolution.width, resolution.height);
scaled->ScaleFrom(*buffer);
return scaled;
}
scoped_refptr<I420Buffer> SwitchingFrameReader::PullFrame() {
return PullFrame(/*frame_num=*/nullptr);
}
scoped_refptr<I420Buffer> SwitchingFrameReader::PullFrame(int* frame_num) {
return PullFrame(frame_num, target_resolution_, /*framerate_scale=*/kNoScale);
}
scoped_refptr<I420Buffer> SwitchingFrameReader::PullFrame(
int* frame_num,
Resolution resolution,
Ratio framerate_scale) {
int skip = framerate_scaler_.Skip(framerate_scale);
if (!last_frame_) {
skip = 1;
}
if (skip == 0) {
if (frame_num != nullptr) {
*frame_num = last_frame_num_;
}
if (resolution.width <= 0 || resolution.height <= 0) {
resolution = target_resolution_;
}
return Scale(last_frame_, resolution);
}
frame_num_ += (skip - 1);
scoped_refptr<I420Buffer> buffer = ReadFrame(frame_num_, resolution);
if (!buffer) {
return nullptr;
}
last_frame_ = buffer;
last_frame_num_ = frame_num_;
if (frame_num != nullptr) {
*frame_num = frame_num_;
}
++frame_num_;
return buffer;
}
scoped_refptr<I420Buffer> SwitchingFrameReader::ReadFrame(int frame_num) {
return ReadFrame(frame_num, target_resolution_);
}
scoped_refptr<I420Buffer> SwitchingFrameReader::ReadFrame(
int frame_num,
Resolution resolution) {
if (resolution.width <= 0 || resolution.height <= 0) {
resolution = target_resolution_;
}
FrameLocation loc = GetFrameLocation(frame_num);
scoped_refptr<I420Buffer> buffer =
readers_[loc.reader_index]->ReadFrame(loc.sub_frame_num, resolution);
return Scale(buffer, resolution);
}
int SwitchingFrameReader::num_frames() const {
int total = 0;
for (const auto& reader : readers_) {
total += reader->num_frames();
}
return total;
}
std::unique_ptr<FrameReader> CreateSwitchingFrameReader(
std::vector<std::string> file_paths,
Resolution target_resolution,
int fps,
TimeDelta camera_switching_interval,
YuvFrameReaderImpl::RepeatMode repeat_mode) {
return std::make_unique<SwitchingFrameReader>(
std::move(file_paths), target_resolution, fps, camera_switching_interval,
repeat_mode);
}
namespace {
class SwitchingFrameGenerator : public FrameGeneratorInterface {
public:
SwitchingFrameGenerator(std::unique_ptr<FrameReader> reader,
webrtc::Resolution resolution,
int fps)
: reader_(std::move(reader)), resolution_(resolution), fps_(fps) {}
VideoFrameData NextFrame() override {
scoped_refptr<I420Buffer> buffer =
reader_->PullFrame(/*frame_num=*/nullptr, resolution_,
/*framerate_scale=*/FrameReader::kNoScale);
if (!buffer) {
return VideoFrameData(nullptr, std::nullopt);
}
VideoFrame::UpdateRect update_rect{.offset_x = 0,
.offset_y = 0,
.width = buffer->width(),
.height = buffer->height()};
return VideoFrameData(buffer, update_rect);
}
void SkipNextFrame() override { reader_->PullFrame(); }
void ChangeResolution(size_t width, size_t height) override {
resolution_ = {.width = static_cast<int>(width),
.height = static_cast<int>(height)};
}
FrameGeneratorInterface::Resolution GetResolution() const override {
return {.width = static_cast<size_t>(resolution_.width),
.height = static_cast<size_t>(resolution_.height)};
}
std::optional<int> fps() const override { return fps_; }
private:
const std::unique_ptr<FrameReader> reader_;
webrtc::Resolution resolution_;
const int fps_;
};
} // namespace
std::unique_ptr<FrameGeneratorInterface> CreateSwitchingFrameGenerator(
std::vector<std::string> file_paths,
Resolution target_resolution,
int fps,
TimeDelta camera_switching_interval,
YuvFrameReaderImpl::RepeatMode repeat_mode) {
auto reader =
CreateSwitchingFrameReader(std::move(file_paths), target_resolution, fps,
camera_switching_interval, repeat_mode);
return std::make_unique<SwitchingFrameGenerator>(std::move(reader),
target_resolution, fps);
}
} // namespace test
} // namespace webrtc