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