mirror of
https://github.com/mollyim/webrtc.git
synced 2025-05-13 05:40:42 +01:00

This CL introduces VideoCodecStats and VideoCodecStatsImpl which provide baseline functionalities for storing, slicing and aggregation of encoded and/or decoded video frame statistics. To facilitate metrics logging (not implemented yet), SamplesStatsCounter is used for stream parameters. VideoCodecStats/VideoCodecStatsImpl will replace existing VideoCodecTestStats/VideoCodecTestStatsImpl. Bug: b/261160916, webrtc:14852 Change-Id: I0f96ce1ed9be3aee2a702804612524676c9882fd Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/291323 Commit-Queue: Sergey Silkin <ssilkin@webrtc.org> Reviewed-by: Rasmus Brandt <brandtr@webrtc.org> Reviewed-by: Mirko Bonadei <mbonadei@webrtc.org> Cr-Commit-Position: refs/heads/main@{#39248}
471 lines
16 KiB
C++
471 lines
16 KiB
C++
/*
|
|
* 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
|