| /* | |
| * Copyright (c) 2022 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 "api/video_codecs/video_codec.h" | |
| #include <cstddef> | |
| #include <memory> | |
| #include <string> | |
| #include <vector> | |
| #include "absl/functional/any_invocable.h" | |
| #include "api/test/create_video_codec_tester.h" | |
| #include "api/test/videocodec_test_stats.h" | |
| #include "api/units/data_rate.h" | |
| #include "api/units/frequency.h" | |
| #include "api/video/i420_buffer.h" | |
| #include "api/video/resolution.h" | |
| #include "api/video_codecs/builtin_video_decoder_factory.h" | |
| #include "api/video_codecs/builtin_video_encoder_factory.h" | |
| #include "api/video_codecs/scalability_mode.h" | |
| #include "api/video_codecs/video_decoder.h" | |
| #include "api/video_codecs/video_encoder.h" | |
| #include "common_video/libyuv/include/webrtc_libyuv.h" | |
| #include "media/base/media_constants.h" | |
| #include "modules/rtp_rtcp/include/rtp_rtcp_defines.h" | |
| #include "modules/video_coding/include/video_error_codes.h" | |
| #include "modules/video_coding/svc/scalability_mode_util.h" | |
| #include "rtc_base/strings/string_builder.h" | |
| #include "test/gtest.h" | |
| #include "test/testsupport/file_utils.h" | |
| #include "test/testsupport/frame_reader.h" | |
| namespace webrtc { | |
| namespace test { | |
| namespace { | |
| using ::testing::Combine; | |
| using ::testing::Values; | |
| struct VideoInfo { | |
| std::string name; | |
| Resolution resolution; | |
| }; | |
| struct CodecInfo { | |
| std::string type; | |
| std::string encoder; | |
| std::string decoder; | |
| }; | |
| struct LayerId { | |
| int spatial_idx; | |
| int temporal_idx; | |
| bool operator==(const LayerId& o) const { | |
| return spatial_idx == o.spatial_idx && temporal_idx == o.temporal_idx; | |
| } | |
| bool operator<(const LayerId& o) const { | |
| if (spatial_idx < o.spatial_idx) | |
| return true; | |
| if (temporal_idx < o.temporal_idx) | |
| return true; | |
| return false; | |
| } | |
| }; | |
| struct EncodingSettings { | |
| ScalabilityMode scalability_mode; | |
| // Spatial layer resolution. | |
| std::map<int, Resolution> resolution; | |
| // Top temporal layer frame rate. | |
| Frequency framerate; | |
| // Bitrate of spatial and temporal layers. | |
| std::map<LayerId, DataRate> bitrate; | |
| }; | |
| struct EncodingTestSettings { | |
| std::string name; | |
| int num_frames = 1; | |
| std::map<int, EncodingSettings> frame_settings; | |
| }; | |
| struct DecodingTestSettings { | |
| std::string name; | |
| }; | |
| struct QualityExpectations { | |
| double min_apsnr_y; | |
| }; | |
| struct EncodeDecodeTestParams { | |
| CodecInfo codec; | |
| VideoInfo video; | |
| VideoCodecTester::EncoderSettings encoder_settings; | |
| VideoCodecTester::DecoderSettings decoder_settings; | |
| EncodingTestSettings encoding_settings; | |
| DecodingTestSettings decoding_settings; | |
| QualityExpectations quality_expectations; | |
| }; | |
| const EncodingSettings kQvga64Kbps30Fps = { | |
| .scalability_mode = ScalabilityMode::kL1T1, | |
| .resolution = {{0, {.width = 320, .height = 180}}}, | |
| .framerate = Frequency::Hertz(30), | |
| .bitrate = { | |
| {{.spatial_idx = 0, .temporal_idx = 0}, DataRate::KilobitsPerSec(64)}}}; | |
| const EncodingTestSettings kConstantRateQvga64Kbps30Fps = { | |
| .name = "ConstantRateQvga64Kbps30Fps", | |
| .num_frames = 300, | |
| .frame_settings = {{/*frame_num=*/0, kQvga64Kbps30Fps}}}; | |
| const QualityExpectations kLowQuality = {.min_apsnr_y = 30}; | |
| const VideoInfo kFourPeople_1280x720_30 = { | |
| .name = "FourPeople_1280x720_30", | |
| .resolution = {.width = 1280, .height = 720}}; | |
| const CodecInfo kLibvpxVp8 = {.type = "VP8", | |
| .encoder = "libvpx", | |
| .decoder = "libvpx"}; | |
| const CodecInfo kLibvpxVp9 = {.type = "VP9", | |
| .encoder = "libvpx", | |
| .decoder = "libvpx"}; | |
| const CodecInfo kOpenH264 = {.type = "H264", | |
| .encoder = "openh264", | |
| .decoder = "ffmpeg"}; | |
| class TestRawVideoSource : public VideoCodecTester::RawVideoSource { | |
| public: | |
| static constexpr Frequency k90kHz = Frequency::Hertz(90000); | |
| TestRawVideoSource(std::unique_ptr<FrameReader> frame_reader, | |
| const EncodingTestSettings& test_settings) | |
| : frame_reader_(std::move(frame_reader)), | |
| test_settings_(test_settings), | |
| frame_num_(0), | |
| timestamp_rtp_(0) { | |
| // Ensure settings for the first frame are provided. | |
| RTC_CHECK_GT(test_settings_.frame_settings.size(), 0u); | |
| RTC_CHECK_EQ(test_settings_.frame_settings.begin()->first, 0); | |
| } | |
| // Pulls next frame. Frame RTP timestamp is set accordingly to | |
| // `EncodingSettings::framerate`. | |
| absl::optional<VideoFrame> PullFrame() override { | |
| if (frame_num_ >= test_settings_.num_frames) { | |
| // End of stream. | |
| return absl::nullopt; | |
| } | |
| EncodingSettings frame_settings = | |
| std::prev(test_settings_.frame_settings.upper_bound(frame_num_)) | |
| ->second; | |
| int pulled_frame; | |
| auto buffer = frame_reader_->PullFrame( | |
| &pulled_frame, frame_settings.resolution.rbegin()->second, | |
| {.num = 30, .den = static_cast<int>(frame_settings.framerate.hertz())}); | |
| RTC_CHECK(buffer) << "Cannot pull frame " << frame_num_; | |
| auto frame = VideoFrame::Builder() | |
| .set_video_frame_buffer(buffer) | |
| .set_timestamp_rtp(timestamp_rtp_) | |
| .build(); | |
| pulled_frames_[timestamp_rtp_] = pulled_frame; | |
| timestamp_rtp_ += k90kHz / frame_settings.framerate; | |
| ++frame_num_; | |
| return frame; | |
| } | |
| // Reads frame specified by `timestamp_rtp`, scales it to `resolution` and | |
| // returns. Frame with the given `timestamp_rtp` is expected to be pulled | |
| // before. | |
| VideoFrame GetFrame(uint32_t timestamp_rtp, Resolution resolution) override { | |
| RTC_CHECK(pulled_frames_.find(timestamp_rtp) != pulled_frames_.end()) | |
| << "Frame with RTP timestamp " << timestamp_rtp | |
| << " was not pulled before"; | |
| auto buffer = | |
| frame_reader_->ReadFrame(pulled_frames_[timestamp_rtp], resolution); | |
| return VideoFrame::Builder() | |
| .set_video_frame_buffer(buffer) | |
| .set_timestamp_rtp(timestamp_rtp) | |
| .build(); | |
| } | |
| protected: | |
| std::unique_ptr<FrameReader> frame_reader_; | |
| const EncodingTestSettings& test_settings_; | |
| int frame_num_; | |
| uint32_t timestamp_rtp_; | |
| std::map<uint32_t, int> pulled_frames_; | |
| }; | |
| class TestEncoder : public VideoCodecTester::Encoder, | |
| public EncodedImageCallback { | |
| public: | |
| TestEncoder(std::unique_ptr<VideoEncoder> encoder, | |
| const CodecInfo& codec_info, | |
| const std::map<int, EncodingSettings>& frame_settings) | |
| : encoder_(std::move(encoder)), | |
| codec_info_(codec_info), | |
| frame_settings_(frame_settings), | |
| frame_num_(0) { | |
| // Ensure settings for the first frame is provided. | |
| RTC_CHECK_GT(frame_settings_.size(), 0u); | |
| RTC_CHECK_EQ(frame_settings_.begin()->first, 0); | |
| encoder_->RegisterEncodeCompleteCallback(this); | |
| } | |
| void Encode(const VideoFrame& frame, EncodeCallback callback) override { | |
| callbacks_[frame.timestamp()] = std::move(callback); | |
| if (auto fs = frame_settings_.find(frame_num_); | |
| fs != frame_settings_.end()) { | |
| if (fs == frame_settings_.begin() || | |
| ConfigChanged(fs->second, std::prev(fs)->second)) { | |
| Configure(fs->second); | |
| } | |
| if (fs == frame_settings_.begin() || | |
| RateChanged(fs->second, std::prev(fs)->second)) { | |
| SetRates(fs->second); | |
| } | |
| } | |
| int result = encoder_->Encode(frame, nullptr); | |
| RTC_CHECK_EQ(result, WEBRTC_VIDEO_CODEC_OK); | |
| ++frame_num_; | |
| } | |
| protected: | |
| Result OnEncodedImage(const EncodedImage& encoded_image, | |
| const CodecSpecificInfo* codec_specific_info) override { | |
| auto cb = callbacks_.find(encoded_image.Timestamp()); | |
| RTC_CHECK(cb != callbacks_.end()); | |
| cb->second(encoded_image); | |
| callbacks_.erase(callbacks_.begin(), cb); | |
| return Result(Result::Error::OK); | |
| } | |
| void Configure(const EncodingSettings& es) { | |
| VideoCodec vc; | |
| const Resolution& resolution = es.resolution.rbegin()->second; | |
| vc.width = resolution.width; | |
| vc.height = resolution.height; | |
| const DataRate& bitrate = es.bitrate.rbegin()->second; | |
| vc.startBitrate = bitrate.kbps(); | |
| vc.maxBitrate = bitrate.kbps(); | |
| vc.minBitrate = 0; | |
| vc.maxFramerate = static_cast<uint32_t>(es.framerate.hertz()); | |
| vc.active = true; | |
| vc.qpMax = 0; | |
| vc.numberOfSimulcastStreams = 0; | |
| vc.mode = webrtc::VideoCodecMode::kRealtimeVideo; | |
| vc.SetFrameDropEnabled(true); | |
| vc.codecType = PayloadStringToCodecType(codec_info_.type); | |
| if (vc.codecType == kVideoCodecVP8) { | |
| *(vc.VP8()) = VideoEncoder::GetDefaultVp8Settings(); | |
| } else if (vc.codecType == kVideoCodecVP9) { | |
| *(vc.VP9()) = VideoEncoder::GetDefaultVp9Settings(); | |
| } else if (vc.codecType == kVideoCodecH264) { | |
| *(vc.H264()) = VideoEncoder::GetDefaultH264Settings(); | |
| } | |
| VideoEncoder::Settings ves( | |
| VideoEncoder::Capabilities(/*loss_notification=*/false), | |
| /*number_of_cores=*/1, | |
| /*max_payload_size=*/1440); | |
| int result = encoder_->InitEncode(&vc, ves); | |
| RTC_CHECK_EQ(result, WEBRTC_VIDEO_CODEC_OK); | |
| } | |
| void SetRates(const EncodingSettings& es) { | |
| VideoEncoder::RateControlParameters rc; | |
| int num_spatial_layers = | |
| ScalabilityModeToNumSpatialLayers(es.scalability_mode); | |
| int num_temporal_layers = | |
| ScalabilityModeToNumSpatialLayers(es.scalability_mode); | |
| for (int sidx = 0; sidx < num_spatial_layers; ++sidx) { | |
| for (int tidx = 0; tidx < num_temporal_layers; ++tidx) { | |
| LayerId layer_id = {.spatial_idx = sidx, .temporal_idx = tidx}; | |
| RTC_CHECK(es.bitrate.find(layer_id) != es.bitrate.end()) | |
| << "Bitrate for layer S=" << sidx << " T=" << tidx << " is not set"; | |
| rc.bitrate.SetBitrate(sidx, tidx, es.bitrate.at(layer_id).bps()); | |
| } | |
| } | |
| rc.framerate_fps = es.framerate.millihertz() / 1000.0; | |
| encoder_->SetRates(rc); | |
| } | |
| bool ConfigChanged(const EncodingSettings& es, | |
| const EncodingSettings& prev_es) const { | |
| return es.scalability_mode != prev_es.scalability_mode || | |
| es.resolution != prev_es.resolution; | |
| } | |
| bool RateChanged(const EncodingSettings& es, | |
| const EncodingSettings& prev_es) const { | |
| return es.bitrate != prev_es.bitrate || es.framerate != prev_es.framerate; | |
| } | |
| std::unique_ptr<VideoEncoder> encoder_; | |
| const CodecInfo& codec_info_; | |
| const std::map<int, EncodingSettings>& frame_settings_; | |
| int frame_num_; | |
| std::map<uint32_t, EncodeCallback> callbacks_; | |
| }; | |
| class TestDecoder : public VideoCodecTester::Decoder, | |
| public DecodedImageCallback { | |
| public: | |
| TestDecoder(std::unique_ptr<VideoDecoder> decoder, | |
| const CodecInfo& codec_info) | |
| : decoder_(std::move(decoder)), codec_info_(codec_info), frame_num_(0) { | |
| decoder_->RegisterDecodeCompleteCallback(this); | |
| } | |
| void Decode(const EncodedImage& frame, DecodeCallback callback) override { | |
| callbacks_[frame.Timestamp()] = std::move(callback); | |
| if (frame_num_ == 0) { | |
| Configure(); | |
| } | |
| decoder_->Decode(frame, /*missing_frames=*/false, | |
| /*render_time_ms=*/0); | |
| ++frame_num_; | |
| } | |
| void Configure() { | |
| VideoDecoder::Settings ds; | |
| ds.set_codec_type(PayloadStringToCodecType(codec_info_.type)); | |
| ds.set_number_of_cores(1); | |
| bool result = decoder_->Configure(ds); | |
| RTC_CHECK(result); | |
| } | |
| protected: | |
| int Decoded(VideoFrame& decoded_frame) override { | |
| auto cb = callbacks_.find(decoded_frame.timestamp()); | |
| RTC_CHECK(cb != callbacks_.end()); | |
| cb->second(decoded_frame); | |
| callbacks_.erase(callbacks_.begin(), cb); | |
| return WEBRTC_VIDEO_CODEC_OK; | |
| } | |
| std::unique_ptr<VideoDecoder> decoder_; | |
| const CodecInfo& codec_info_; | |
| int frame_num_; | |
| std::map<uint32_t, DecodeCallback> callbacks_; | |
| }; | |
| std::unique_ptr<VideoCodecTester::Encoder> CreateEncoder( | |
| const CodecInfo& codec_info, | |
| const std::map<int, EncodingSettings>& frame_settings) { | |
| auto factory = CreateBuiltinVideoEncoderFactory(); | |
| auto encoder = factory->CreateVideoEncoder(SdpVideoFormat(codec_info.type)); | |
| return std::make_unique<TestEncoder>(std::move(encoder), codec_info, | |
| frame_settings); | |
| } | |
| std::unique_ptr<VideoCodecTester::Decoder> CreateDecoder( | |
| const CodecInfo& codec_info) { | |
| auto factory = CreateBuiltinVideoDecoderFactory(); | |
| auto decoder = factory->CreateVideoDecoder(SdpVideoFormat(codec_info.type)); | |
| return std::make_unique<TestDecoder>(std::move(decoder), codec_info); | |
| } | |
| } // namespace | |
| class EncodeDecodeTest | |
| : public ::testing::TestWithParam<EncodeDecodeTestParams> { | |
| public: | |
| EncodeDecodeTest() : test_params_(GetParam()) {} | |
| void SetUp() override { | |
| std::unique_ptr<FrameReader> frame_reader = | |
| CreateYuvFrameReader(ResourcePath(test_params_.video.name, "yuv"), | |
| test_params_.video.resolution, | |
| YuvFrameReaderImpl::RepeatMode::kPingPong); | |
| video_source_ = std::make_unique<TestRawVideoSource>( | |
| std::move(frame_reader), test_params_.encoding_settings); | |
| encoder_ = CreateEncoder(test_params_.codec, | |
| test_params_.encoding_settings.frame_settings); | |
| decoder_ = CreateDecoder(test_params_.codec); | |
| tester_ = CreateVideoCodecTester(); | |
| } | |
| static std::string TestParametersToStr( | |
| const ::testing::TestParamInfo<EncodeDecodeTest::ParamType>& info) { | |
| return std::string(info.param.encoding_settings.name + | |
| info.param.codec.type + info.param.codec.encoder + | |
| info.param.codec.decoder); | |
| } | |
| protected: | |
| EncodeDecodeTestParams test_params_; | |
| std::unique_ptr<TestRawVideoSource> video_source_; | |
| std::unique_ptr<VideoCodecTester::Encoder> encoder_; | |
| std::unique_ptr<VideoCodecTester::Decoder> decoder_; | |
| std::unique_ptr<VideoCodecTester> tester_; | |
| }; | |
| TEST_P(EncodeDecodeTest, DISABLED_TestEncodeDecode) { | |
| std::unique_ptr<VideoCodecStats> stats = tester_->RunEncodeDecodeTest( | |
| std::move(video_source_), std::move(encoder_), std::move(decoder_), | |
| test_params_.encoder_settings, test_params_.decoder_settings); | |
| const auto& frame_settings = test_params_.encoding_settings.frame_settings; | |
| for (auto fs = frame_settings.begin(); fs != frame_settings.end(); ++fs) { | |
| int first_frame = fs->first; | |
| int last_frame = std::next(fs) != frame_settings.end() | |
| ? std::next(fs)->first - 1 | |
| : test_params_.encoding_settings.num_frames - 1; | |
| VideoCodecStats::Filter slicer = {.first_frame = first_frame, | |
| .last_frame = last_frame}; | |
| std::vector<VideoCodecStats::Frame> frames = stats->Slice(slicer); | |
| VideoCodecStats::Stream stream = stats->Aggregate(frames); | |
| EXPECT_GE(stream.psnr.y.GetAverage(), | |
| test_params_.quality_expectations.min_apsnr_y); | |
| } | |
| } | |
| std::list<EncodeDecodeTestParams> ConstantRateTestParameters() { | |
| std::list<EncodeDecodeTestParams> test_params; | |
| std::vector<CodecInfo> codecs = {kLibvpxVp8}; | |
| std::vector<VideoInfo> videos = {kFourPeople_1280x720_30}; | |
| std::vector<std::pair<EncodingTestSettings, QualityExpectations>> | |
| encoding_settings = {{kConstantRateQvga64Kbps30Fps, kLowQuality}}; | |
| for (const CodecInfo& codec : codecs) { | |
| for (const VideoInfo& video : videos) { | |
| for (const auto& es : encoding_settings) { | |
| EncodeDecodeTestParams p; | |
| p.codec = codec; | |
| p.video = video; | |
| p.encoding_settings = es.first; | |
| p.quality_expectations = es.second; | |
| test_params.push_back(p); | |
| } | |
| } | |
| } | |
| return test_params; | |
| } | |
| INSTANTIATE_TEST_SUITE_P(ConstantRate, | |
| EncodeDecodeTest, | |
| ::testing::ValuesIn(ConstantRateTestParameters()), | |
| EncodeDecodeTest::TestParametersToStr); | |
| } // namespace test | |
| } // namespace webrtc |