| /* | |
| * 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 "test/video_codec_tester.h" | |
| #include <algorithm> | |
| #include <atomic> | |
| #include <cmath> | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <cstdio> | |
| #include <cstring> | |
| #include <iterator> | |
| #include <map> | |
| #include <memory> | |
| #include <numeric> | |
| #include <optional> | |
| #include <set> | |
| #include <span> | |
| #include <string> | |
| #include <tuple> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/functional/any_invocable.h" | |
| #include "absl/strings/match.h" | |
| #include "absl/strings/string_view.h" | |
| #include "api/environment/environment.h" | |
| #include "api/environment/environment_factory.h" | |
| #include "api/field_trials_view.h" | |
| #include "api/make_ref_counted.h" | |
| #include "api/numerics/samples_stats_counter.h" | |
| #include "api/scoped_refptr.h" | |
| #include "api/test/create_frame_generator.h" | |
| #include "api/test/frame_generator_interface.h" | |
| #include "api/test/metrics/metric.h" | |
| #include "api/test/metrics/metrics_logger.h" | |
| #include "api/test/video/video_frame_writer.h" | |
| #include "api/units/data_rate.h" | |
| #include "api/units/data_size.h" | |
| #include "api/units/frequency.h" | |
| #include "api/units/time_delta.h" | |
| #include "api/units/timestamp.h" | |
| #include "api/video/builtin_video_bitrate_allocator_factory.h" | |
| #include "api/video/encoded_image.h" | |
| #include "api/video/resolution.h" | |
| #include "api/video/video_bitrate_allocation.h" | |
| #include "api/video/video_bitrate_allocator.h" | |
| #include "api/video/video_codec_type.h" | |
| #include "api/video/video_frame.h" | |
| #include "api/video/video_frame_buffer.h" | |
| #include "api/video/video_frame_type.h" | |
| #include "api/video_codecs/h264_profile_level_id.h" | |
| #include "api/video_codecs/scalability_mode.h" | |
| #include "api/video_codecs/sdp_video_format.h" | |
| #include "api/video_codecs/simulcast_stream.h" | |
| #include "api/video_codecs/spatial_layer.h" | |
| #include "api/video_codecs/video_codec.h" | |
| #include "api/video_codecs/video_decoder.h" | |
| #include "api/video_codecs/video_decoder_factory.h" | |
| #include "api/video_codecs/video_encoder.h" | |
| #include "api/video_codecs/video_encoder_factory.h" | |
| #include "media/base/media_constants.h" | |
| #include "modules/video_coding/codecs/av1/av1_svc_config.h" | |
| #include "modules/video_coding/codecs/h264/include/h264.h" | |
| #include "modules/video_coding/codecs/vp9/svc_config.h" | |
| #include "modules/video_coding/include/video_codec_interface.h" | |
| #include "modules/video_coding/include/video_error_codes.h" | |
| #include "modules/video_coding/svc/scalability_mode_util.h" | |
| #include "modules/video_coding/utility/ivf_file_writer.h" | |
| #include "rtc_base/checks.h" | |
| #include "rtc_base/event.h" | |
| #include "rtc_base/logging.h" | |
| #include "rtc_base/strings/string_builder.h" | |
| #include "rtc_base/synchronization/mutex.h" | |
| #include "rtc_base/task_queue_for_test.h" | |
| #include "rtc_base/thread.h" | |
| #include "rtc_base/thread_annotations.h" | |
| #include "system_wrappers/include/clock.h" | |
| #include "test/testsupport/frame_reader.h" | |
| #include "test/testsupport/video_frame_writer.h" | |
| #include "third_party/libyuv/include/libyuv/compare.h" | |
| #include "video/config/encoder_stream_factory.h" | |
| #include "video/config/video_encoder_config.h" | |
| namespace webrtc { | |
| namespace test { | |
| namespace { | |
| using CodedVideoSource = VideoCodecTester::CodedVideoSource; | |
| using VideoSourceSettings = VideoCodecTester::VideoSourceSettings; | |
| using EncodingSettings = VideoCodecTester::EncodingSettings; | |
| using LayerSettings = EncodingSettings::LayerSettings; | |
| using LayerId = VideoCodecTester::LayerId; | |
| using EncoderSettings = VideoCodecTester::EncoderSettings; | |
| using DecoderSettings = VideoCodecTester::DecoderSettings; | |
| using PacingSettings = VideoCodecTester::PacingSettings; | |
| using PacingMode = PacingSettings::PacingMode; | |
| using VideoCodecStats = VideoCodecTester::VideoCodecStats; | |
| using DecodeCallback = | |
| absl::AnyInvocable<void(const VideoFrame& decoded_frame)>; | |
| using test::ImprovementDirection; | |
| constexpr Frequency k90kHz = Frequency::Hertz(90000); | |
| const std::set<ScalabilityMode> kFullSvcScalabilityModes{ | |
| ScalabilityMode::kL2T1, ScalabilityMode::kL2T1h, ScalabilityMode::kL2T2, | |
| ScalabilityMode::kL2T2h, ScalabilityMode::kL2T3, ScalabilityMode::kL2T3h, | |
| ScalabilityMode::kL3T1, ScalabilityMode::kL3T1h, ScalabilityMode::kL3T2, | |
| ScalabilityMode::kL3T2h, ScalabilityMode::kL3T3, ScalabilityMode::kL3T3h}; | |
| const std::set<ScalabilityMode> kKeySvcScalabilityModes{ | |
| ScalabilityMode::kL2T1_KEY, ScalabilityMode::kL2T2_KEY, | |
| ScalabilityMode::kL2T2_KEY_SHIFT, ScalabilityMode::kL2T3_KEY, | |
| ScalabilityMode::kL3T1_KEY, ScalabilityMode::kL3T2_KEY, | |
| ScalabilityMode::kL3T3_KEY}; | |
| scoped_refptr<VideoFrameBuffer> ScaleFrame( | |
| scoped_refptr<VideoFrameBuffer> buffer, | |
| int scaled_width, | |
| int scaled_height) { | |
| if (buffer->width() == scaled_width && buffer->height() == scaled_height) { | |
| return buffer; | |
| } | |
| return buffer->Scale(scaled_width, scaled_height); | |
| } | |
| // A video source that reads frames from YUV, Y4M or IVF (compressed with VPx, | |
| // AV1 or H264) files. | |
| class VideoSource { | |
| public: | |
| VideoSource(const Environment& env, VideoSourceSettings source_settings) | |
| : source_settings_(source_settings) { | |
| if (absl::EndsWith(source_settings.file_path, "ivf")) { | |
| ivf_reader_ = | |
| CreateFromIvfFileFrameGenerator(env, source_settings.file_path); | |
| } else if (absl::EndsWith(source_settings.file_path, "y4m")) { | |
| yuv_reader_ = | |
| CreateY4mFrameReader(source_settings_.file_path, | |
| YuvFrameReaderImpl::RepeatMode::kPingPong); | |
| } else { | |
| yuv_reader_ = CreateYuvFrameReader( | |
| source_settings_.file_path, source_settings_.resolution, | |
| YuvFrameReaderImpl::RepeatMode::kPingPong); | |
| } | |
| RTC_CHECK(ivf_reader_ || yuv_reader_); | |
| } | |
| VideoFrame PullFrame(uint32_t timestamp_rtp, | |
| Resolution output_resolution, | |
| Frequency output_framerate) { | |
| // If the source and output frame rates differ, resampling is performed by | |
| // skipping or repeating source frames. | |
| time_delta_ = time_delta_.value_or(1 / source_settings_.framerate); | |
| int seek = 0; | |
| while (time_delta_->us() <= 0) { | |
| *time_delta_ += 1 / source_settings_.framerate; | |
| ++seek; | |
| } | |
| *time_delta_ -= 1 / output_framerate; | |
| if (seek > 0 || last_frame_ == nullptr) { | |
| scoped_refptr<VideoFrameBuffer> buffer; | |
| do { | |
| if (yuv_reader_) { | |
| buffer = yuv_reader_->PullFrame(); | |
| } else { | |
| buffer = ivf_reader_->NextFrame().buffer; | |
| } | |
| } while (--seek > 0); | |
| RTC_CHECK(buffer) << "Could not read frame. timestamp_rtp " | |
| << timestamp_rtp; | |
| last_frame_ = buffer; | |
| } | |
| scoped_refptr<VideoFrameBuffer> buffer = ScaleFrame( | |
| last_frame_, output_resolution.width, output_resolution.height); | |
| return VideoFrame::Builder() | |
| .set_video_frame_buffer(buffer) | |
| .set_rtp_timestamp(timestamp_rtp) | |
| .set_timestamp_us((timestamp_rtp / k90kHz).us()) | |
| .build(); | |
| } | |
| private: | |
| VideoSourceSettings source_settings_; | |
| std::unique_ptr<FrameReader> yuv_reader_; | |
| std::unique_ptr<FrameGeneratorInterface> ivf_reader_; | |
| scoped_refptr<VideoFrameBuffer> last_frame_; | |
| // Time delta between the source and output video. Used for frame rate | |
| // scaling. This value increases by the source frame duration each time a | |
| // frame is read from the source, and decreases by the output frame duration | |
| // each time an output frame is delivered. | |
| std::optional<TimeDelta> time_delta_; | |
| }; | |
| // Pacer calculates delay necessary to keep frame encode or decode call spaced | |
| // from the previous calls by the pacing time. `Schedule` is expected to be | |
| // called as close as possible to posting frame encode or decode task. This | |
| // class is not thread safe. | |
| class Pacer { | |
| public: | |
| Pacer(Clock* clock, PacingSettings settings) | |
| : clock_(clock), settings_(settings), delay_(TimeDelta::Zero()) {} | |
| Timestamp Schedule(Timestamp timestamp) { | |
| Timestamp now = clock_->CurrentTime(); | |
| if (settings_.mode == PacingMode::kNoPacing) { | |
| return now; | |
| } | |
| Timestamp scheduled = now; | |
| if (prev_scheduled_) { | |
| scheduled = *prev_scheduled_ + PacingTime(timestamp); | |
| if (scheduled < now) { | |
| scheduled = now; | |
| } | |
| } | |
| prev_timestamp_ = timestamp; | |
| prev_scheduled_ = scheduled; | |
| return scheduled; | |
| } | |
| private: | |
| TimeDelta PacingTime(Timestamp timestamp) { | |
| if (settings_.mode == PacingMode::kRealTime) { | |
| return timestamp - *prev_timestamp_; | |
| } | |
| RTC_CHECK_EQ(PacingMode::kConstantRate, settings_.mode); | |
| return 1 / settings_.constant_rate; | |
| } | |
| Clock* const clock_; | |
| PacingSettings settings_; | |
| std::optional<Timestamp> prev_timestamp_; | |
| std::optional<Timestamp> prev_scheduled_; | |
| TimeDelta delay_; | |
| }; | |
| // A task queue that limits its maximum size and guarantees FIFO execution of | |
| // the scheduled tasks. | |
| class LimitedTaskQueue { | |
| public: | |
| // Frame reading, encoding and decoding are handled in separate threads. If | |
| // encoding or decoding is slow, the frame reader may run far ahead, loading | |
| // many large frames into memory. To prevent this, we limit the maximum size | |
| // of the task queue. When this limit is reached, posting new tasks is blocked | |
| // until the queue size is reduced by executing previous tasks. | |
| static constexpr int kMaxTaskQueueSize = 3; | |
| explicit LimitedTaskQueue(Clock* clock) : clock_(clock), queue_size_(0) {} | |
| void PostScheduledTask(absl::AnyInvocable<void() &&> task, | |
| Timestamp scheduled) { | |
| { | |
| // Block posting new tasks until the queue size is reduced. | |
| MutexLock lock(&mutex_); | |
| while (queue_size_ >= kMaxTaskQueueSize) { | |
| task_executed_.Wait(TimeDelta::Seconds(10)); | |
| task_executed_.Reset(); | |
| } | |
| } | |
| ++queue_size_; | |
| task_queue_.PostTask([this, task = std::move(task), scheduled]() mutable { | |
| Timestamp now = clock_->CurrentTime(); | |
| int64_t wait_ms = (scheduled - now).ms(); | |
| if (wait_ms > 0) { | |
| RTC_CHECK_LT(wait_ms, 10000) << "Too high wait_ms " << wait_ms; | |
| Thread::SleepMs(wait_ms); | |
| } | |
| std::move(task)(); | |
| --queue_size_; | |
| task_executed_.Set(); | |
| }); | |
| } | |
| void PostTask(absl::AnyInvocable<void() &&> task) { | |
| Timestamp now = clock_->CurrentTime(); | |
| PostScheduledTask(std::move(task), now); | |
| } | |
| void PostTaskAndWait(absl::AnyInvocable<void() &&> task) { | |
| PostTask(std::move(task)); | |
| WaitForPreviouslyPostedTasks(); | |
| } | |
| void WaitForPreviouslyPostedTasks() { | |
| task_queue_.WaitForPreviouslyPostedTasks(); | |
| } | |
| private: | |
| Clock* const clock_; | |
| TaskQueueForTest task_queue_; | |
| std::atomic_int queue_size_; | |
| Event task_executed_; | |
| Mutex mutex_; | |
| }; | |
| class TesterY4mWriter { | |
| public: | |
| explicit TesterY4mWriter(absl::string_view base_path) | |
| : base_path_(base_path) {} | |
| ~TesterY4mWriter() { | |
| task_queue_.SendTask([] {}); | |
| } | |
| void Write(const VideoFrame& frame, int spatial_idx) { | |
| task_queue_.PostTask([this, frame, spatial_idx] { | |
| if (y4m_writers_.find(spatial_idx) == y4m_writers_.end()) { | |
| std::string file_path = | |
| base_path_ + "-s" + std::to_string(spatial_idx) + ".y4m"; | |
| Y4mVideoFrameWriterImpl* y4m_writer = new Y4mVideoFrameWriterImpl( | |
| file_path, frame.width(), frame.height(), /*fps=*/30); | |
| RTC_CHECK(y4m_writer); | |
| y4m_writers_[spatial_idx] = | |
| std::unique_ptr<VideoFrameWriter>(y4m_writer); | |
| } | |
| y4m_writers_.at(spatial_idx)->WriteFrame(frame); | |
| }); | |
| } | |
| private: | |
| std::string base_path_; | |
| std::map<int, std::unique_ptr<VideoFrameWriter>> y4m_writers_; | |
| TaskQueueForTest task_queue_; | |
| }; | |
| class TesterIvfWriter { | |
| public: | |
| explicit TesterIvfWriter(absl::string_view base_path) | |
| : base_path_(base_path) {} | |
| ~TesterIvfWriter() { | |
| task_queue_.SendTask([] {}); | |
| } | |
| void Write(const EncodedImage& encoded_frame, VideoCodecType codec_type) { | |
| task_queue_.PostTask([this, encoded_frame, codec_type] { | |
| int spatial_idx = encoded_frame.SpatialIndex().value_or( | |
| encoded_frame.SimulcastIndex().value_or(0)); | |
| if (ivf_file_writers_.find(spatial_idx) == ivf_file_writers_.end()) { | |
| std::string ivf_path = | |
| base_path_ + "-s" + std::to_string(spatial_idx) + ".ivf"; | |
| int error = 0; | |
| std::unique_ptr<IvfFileWriter> ivf_writer = | |
| IvfFileWriter::Wrap(ivf_path, /*byte_limit=*/0, &error); | |
| RTC_CHECK(error == 0); | |
| RTC_CHECK(ivf_writer); | |
| ivf_file_writers_[spatial_idx] = std::move(ivf_writer); | |
| } | |
| // IVF writer splits superframe into spatial layer frames. We want to dump | |
| // whole superframe so that decoders can correctly decode the dump. Reset | |
| // spatial index to get desired behavior. | |
| EncodedImage frame_copy = encoded_frame; | |
| frame_copy.SetSpatialIndex(std::nullopt); | |
| frame_copy.SetSpatialLayerFrameSize(0, 0); | |
| // To play: ffplay -vcodec vp8|vp9|av1|hevc|h264 filename | |
| ivf_file_writers_.at(spatial_idx)->WriteFrame(frame_copy, codec_type); | |
| }); | |
| } | |
| private: | |
| std::string base_path_; | |
| std::map<int, std::unique_ptr<IvfFileWriter>> ivf_file_writers_; | |
| TaskQueueForTest task_queue_; | |
| }; | |
| class LeakyBucket { | |
| public: | |
| LeakyBucket() : level_bits_(0) {} | |
| // Updates bucket level and returns its current level in bits. Data is removed | |
| // from bucket with rate equal to target bitrate of previous frame. Bucket | |
| // level is tracked with floating point precision. Returned value of bucket | |
| // level is rounded up. | |
| int Update(const VideoCodecStats::Frame& frame) { | |
| RTC_CHECK(frame.target_bitrate) << "Bitrate must be specified."; | |
| if (prev_frame_) { | |
| RTC_CHECK_GT(frame.timestamp_rtp, prev_frame_->timestamp_rtp) | |
| << "Timestamp must increase."; | |
| TimeDelta passed = | |
| (frame.timestamp_rtp - prev_frame_->timestamp_rtp) / k90kHz; | |
| level_bits_ -= | |
| prev_frame_->target_bitrate->bps<double>() * passed.seconds<double>(); | |
| level_bits_ = std::max(level_bits_, 0.0); | |
| } | |
| prev_frame_ = frame; | |
| level_bits_ += frame.frame_size.bytes() * 8; | |
| return static_cast<int>(std::ceil(level_bits_)); | |
| } | |
| private: | |
| std::optional<VideoCodecStats::Frame> prev_frame_; | |
| double level_bits_; | |
| }; | |
| class VideoCodecAnalyzer : public VideoCodecTester::VideoCodecStats { | |
| public: | |
| explicit VideoCodecAnalyzer(Clock* clock) | |
| : clock_(clock), task_queue_(clock) {} | |
| void StartEncode(const VideoFrame& video_frame, | |
| const EncodingSettings& encoding_settings) { | |
| Timestamp encode_start = clock_->CurrentTime(); | |
| task_queue_.PostTask([this, timestamp_rtp = video_frame.rtp_timestamp(), | |
| encoding_settings, encode_start]() { | |
| RTC_CHECK(frames_.find(timestamp_rtp) == frames_.end()) | |
| << "Duplicate frame. Frame with timestamp " << timestamp_rtp | |
| << " was seen before"; | |
| Frame frame; | |
| frame.timestamp_rtp = timestamp_rtp; | |
| frame.encode_start = encode_start, | |
| frames_.emplace(timestamp_rtp, | |
| std::map<int, Frame>{{/*spatial_idx=*/0, frame}}); | |
| encoding_settings_.emplace(timestamp_rtp, encoding_settings); | |
| }); | |
| } | |
| void FinishEncode(const EncodedImage& encoded_frame) { | |
| Timestamp encode_finished = clock_->CurrentTime(); | |
| task_queue_.PostTask( | |
| [this, timestamp_rtp = encoded_frame.RtpTimestamp(), | |
| spatial_idx = encoded_frame.SpatialIndex().value_or( | |
| encoded_frame.SimulcastIndex().value_or(0)), | |
| temporal_idx = encoded_frame.TemporalIndex().value_or(0), | |
| width = encoded_frame._encodedWidth, | |
| height = encoded_frame._encodedHeight, | |
| v_frame_type = encoded_frame.frame_type(), | |
| frame_size_bytes = encoded_frame.size(), qp = encoded_frame.qp_, | |
| encode_finished]() { | |
| if (spatial_idx > 0) { | |
| RTC_CHECK(frames_.find(timestamp_rtp) != frames_.end()) | |
| << "Spatial layer 0 frame with timestamp " << timestamp_rtp | |
| << " was not seen before"; | |
| const Frame& base_frame = | |
| frames_.at(timestamp_rtp).at(/*spatial_idx=*/0); | |
| frames_.at(timestamp_rtp).emplace(spatial_idx, base_frame); | |
| } | |
| Frame& frame = frames_.at(timestamp_rtp).at(spatial_idx); | |
| frame.layer_id = {.spatial_idx = spatial_idx, | |
| .temporal_idx = temporal_idx}; | |
| frame.width = width; | |
| frame.height = height; | |
| frame.frame_size = DataSize::Bytes(frame_size_bytes); | |
| frame.qp = qp; | |
| frame.keyframe = v_frame_type == VideoFrameType::kVideoFrameKey; | |
| frame.encode_time = encode_finished - frame.encode_start; | |
| frame.encoded = true; | |
| }); | |
| } | |
| void StartDecode(const EncodedImage& encoded_frame) { | |
| Timestamp decode_start = clock_->CurrentTime(); | |
| task_queue_.PostTask( | |
| [this, timestamp_rtp = encoded_frame.RtpTimestamp(), | |
| spatial_idx = encoded_frame.SpatialIndex().value_or( | |
| encoded_frame.SimulcastIndex().value_or(0)), | |
| temporal_idx = encoded_frame.TemporalIndex().value_or(0), | |
| width = encoded_frame._encodedWidth, | |
| height = encoded_frame._encodedHeight, | |
| v_frame_type = encoded_frame.frame_type(), qp = encoded_frame.qp_, | |
| frame_size_bytes = encoded_frame.size(), decode_start]() { | |
| bool decode_only = frames_.find(timestamp_rtp) == frames_.end(); | |
| if (decode_only || frames_.at(timestamp_rtp).find(spatial_idx) == | |
| frames_.at(timestamp_rtp).end()) { | |
| Frame frame; | |
| frame.timestamp_rtp = timestamp_rtp; | |
| frame.layer_id = {.spatial_idx = spatial_idx, | |
| .temporal_idx = temporal_idx}; | |
| frame.width = width; | |
| frame.height = height; | |
| frame.keyframe = v_frame_type == VideoFrameType::kVideoFrameKey; | |
| frame.qp = qp; | |
| if (decode_only) { | |
| frame.frame_size = DataSize::Bytes(frame_size_bytes); | |
| frames_[timestamp_rtp] = {{spatial_idx, frame}}; | |
| } else { | |
| frames_[timestamp_rtp][spatial_idx] = frame; | |
| } | |
| } | |
| Frame& frame = frames_.at(timestamp_rtp).at(spatial_idx); | |
| frame.decode_start = decode_start; | |
| }); | |
| } | |
| void FinishDecode(const VideoFrame& decoded_frame, | |
| int spatial_idx, | |
| std::optional<VideoFrame> ref_frame = std::nullopt) { | |
| Timestamp decode_finished = clock_->CurrentTime(); | |
| task_queue_.PostTask([this, timestamp_rtp = decoded_frame.rtp_timestamp(), | |
| spatial_idx, width = decoded_frame.width(), | |
| height = decoded_frame.height(), decode_finished]() { | |
| Frame& frame = frames_.at(timestamp_rtp).at(spatial_idx); | |
| frame.decode_time = decode_finished - frame.decode_start; | |
| if (!frame.encoded) { | |
| frame.width = width; | |
| frame.height = height; | |
| } | |
| frame.decoded = true; | |
| }); | |
| if (ref_frame.has_value()) { | |
| // Copy hardware-backed frame into main memory to release output buffers | |
| // which number may be limited in hardware decoders. | |
| scoped_refptr<I420BufferInterface> decoded_buffer = | |
| decoded_frame.video_frame_buffer()->ToI420(); | |
| task_queue_.PostTask([this, decoded_buffer, ref_frame, | |
| timestamp_rtp = decoded_frame.rtp_timestamp(), | |
| spatial_idx]() { | |
| scoped_refptr<I420BufferInterface> ref_buffer = | |
| ScaleFrame(ref_frame->video_frame_buffer(), decoded_buffer->width(), | |
| decoded_buffer->height()) | |
| ->ToI420(); | |
| Frame& frame = frames_.at(timestamp_rtp).at(spatial_idx); | |
| frame.psnr = CalcPsnr(*decoded_buffer, *ref_buffer); | |
| }); | |
| } | |
| } | |
| std::vector<Frame> Slice(Filter filter, bool merge) const override { | |
| std::vector<Frame> slice; | |
| for (const auto& [timestamp_rtp, temporal_unit_frames] : frames_) { | |
| if (temporal_unit_frames.empty()) { | |
| continue; | |
| } | |
| bool is_svc = false; | |
| if (!encoding_settings_.empty()) { | |
| ScalabilityMode scalability_mode = | |
| encoding_settings_.at(timestamp_rtp).scalability_mode; | |
| if (kFullSvcScalabilityModes.count(scalability_mode) > 0 || | |
| (kKeySvcScalabilityModes.count(scalability_mode) > 0 && | |
| temporal_unit_frames.at(0).keyframe)) { | |
| is_svc = true; | |
| } | |
| } | |
| std::vector<Frame> subframes; | |
| for (const auto& [spatial_idx, frame] : temporal_unit_frames) { | |
| if (frame.timestamp_rtp < filter.min_timestamp_rtp || | |
| frame.timestamp_rtp > filter.max_timestamp_rtp) { | |
| continue; | |
| } | |
| if (filter.layer_id) { | |
| if (is_svc && | |
| frame.layer_id.spatial_idx > filter.layer_id->spatial_idx) { | |
| continue; | |
| } | |
| if (!is_svc && | |
| frame.layer_id.spatial_idx != filter.layer_id->spatial_idx) { | |
| continue; | |
| } | |
| if (frame.layer_id.temporal_idx > filter.layer_id->temporal_idx) { | |
| continue; | |
| } | |
| } | |
| subframes.push_back(frame); | |
| } | |
| if (subframes.empty()) { | |
| continue; | |
| } | |
| if (!merge) { | |
| std::copy(subframes.begin(), subframes.end(), | |
| std::back_inserter(slice)); | |
| continue; | |
| } | |
| Frame superframe = subframes.back(); | |
| for (const Frame& frame : | |
| std::span<Frame>(subframes).subspan(0, subframes.size() - 1)) { | |
| superframe.decoded |= frame.decoded; | |
| superframe.encoded |= frame.encoded; | |
| superframe.frame_size += frame.frame_size; | |
| superframe.keyframe |= frame.keyframe; | |
| superframe.encode_time = | |
| std::max(superframe.encode_time, frame.encode_time); | |
| superframe.decode_time = | |
| std::max(superframe.decode_time, frame.decode_time); | |
| } | |
| if (!encoding_settings_.empty()) { | |
| RTC_CHECK(encoding_settings_.find(superframe.timestamp_rtp) != | |
| encoding_settings_.end()) | |
| << "No encoding settings for frame " << superframe.timestamp_rtp; | |
| const EncodingSettings& es = | |
| encoding_settings_.at(superframe.timestamp_rtp); | |
| superframe.target_bitrate = GetTargetBitrate(es, filter.layer_id); | |
| superframe.target_framerate = GetTargetFramerate(es, filter.layer_id); | |
| } | |
| slice.push_back(superframe); | |
| } | |
| return slice; | |
| } | |
| Stream Aggregate(Filter filter) const override { | |
| std::vector<Frame> frames = Slice(filter, /*merge=*/true); | |
| Stream stream; | |
| LeakyBucket leaky_bucket; | |
| for (const Frame& frame : frames) { | |
| Timestamp time = Timestamp::Micros((frame.timestamp_rtp / k90kHz).us()); | |
| if (!frame.frame_size.IsZero()) { | |
| stream.width.AddSample(StatsSample(frame.width, time)); | |
| stream.height.AddSample(StatsSample(frame.height, time)); | |
| stream.frame_size_bytes.AddSample( | |
| StatsSample(frame.frame_size.bytes(), time)); | |
| stream.keyframe.AddSample(StatsSample(frame.keyframe, time)); | |
| if (frame.qp) { | |
| stream.qp.AddSample(StatsSample(*frame.qp, time)); | |
| } | |
| } | |
| if (frame.encoded) { | |
| stream.encode_time_us.AddSample( | |
| StatsSample(frame.encode_time.us<double>(), time)); | |
| } | |
| if (frame.decoded) { | |
| stream.decode_time_us.AddSample( | |
| StatsSample(frame.decode_time.us<double>(), time)); | |
| } | |
| if (frame.psnr) { | |
| stream.psnr.y.AddSample(StatsSample(frame.psnr->y, time)); | |
| stream.psnr.u.AddSample(StatsSample(frame.psnr->u, time)); | |
| stream.psnr.v.AddSample(StatsSample(frame.psnr->v, time)); | |
| } | |
| if (frame.target_framerate) { | |
| stream.target_framerate_fps.AddSample( | |
| StatsSample(frame.target_framerate->hertz<double>(), time)); | |
| } | |
| if (frame.target_bitrate) { | |
| stream.target_bitrate_kbps.AddSample( | |
| StatsSample(frame.target_bitrate->kbps<double>(), time)); | |
| int buffer_level_bits = leaky_bucket.Update(frame); | |
| stream.buffer_delay_ms.AddSample(StatsSample( | |
| 1000.0 * buffer_level_bits / frame.target_bitrate->bps<double>(), | |
| time)); | |
| } | |
| } | |
| int num_encoded_frames = stream.frame_size_bytes.NumSamples(); | |
| if (num_encoded_frames == 0) { | |
| return stream; | |
| } | |
| const Frame& first_frame = frames.front(); | |
| Filter filter_all_layers{.min_timestamp_rtp = filter.min_timestamp_rtp, | |
| .max_timestamp_rtp = filter.max_timestamp_rtp}; | |
| std::vector<Frame> frames_all_layers = | |
| Slice(filter_all_layers, /*merge=*/true); | |
| const Frame& last_frame = frames_all_layers.back(); | |
| TimeDelta duration = | |
| (last_frame.timestamp_rtp - first_frame.timestamp_rtp) / k90kHz; | |
| if (last_frame.target_framerate) { | |
| duration += 1 / *last_frame.target_framerate; | |
| } | |
| DataRate encoded_bitrate = | |
| DataSize::Bytes(stream.frame_size_bytes.GetSum()) / duration; | |
| Frequency encoded_framerate = num_encoded_frames / duration; | |
| double bitrate_mismatch_pct = 0.0; | |
| if (const auto& target_bitrate = first_frame.target_bitrate; | |
| target_bitrate) { | |
| bitrate_mismatch_pct = 100 * (encoded_bitrate / *target_bitrate - 1); | |
| } | |
| double framerate_mismatch_pct = 0.0; | |
| if (const auto& target_framerate = first_frame.target_framerate; | |
| target_framerate) { | |
| framerate_mismatch_pct = | |
| 100 * (encoded_framerate / *target_framerate - 1); | |
| } | |
| for (Frame& frame : frames) { | |
| Timestamp time = Timestamp::Micros((frame.timestamp_rtp / k90kHz).us()); | |
| stream.encoded_bitrate_kbps.AddSample( | |
| StatsSample(encoded_bitrate.kbps<double>(), time)); | |
| stream.encoded_framerate_fps.AddSample( | |
| StatsSample(encoded_framerate.hertz<double>(), time)); | |
| stream.bitrate_mismatch_pct.AddSample( | |
| StatsSample(bitrate_mismatch_pct, time)); | |
| stream.framerate_mismatch_pct.AddSample( | |
| StatsSample(framerate_mismatch_pct, time)); | |
| } | |
| return stream; | |
| } | |
| void LogMetrics(absl::string_view csv_path, | |
| std::vector<Frame> frames, | |
| std::map<std::string, std::string> metadata) const override { | |
| RTC_LOG(LS_INFO) << "Write metrics to " << csv_path; | |
| FILE* csv_file = fopen(csv_path.data(), "w"); | |
| const std::string delimiter = ";"; | |
| StringBuilder header; | |
| header | |
| << "timestamp_rtp;spatial_idx;temporal_idx;width;height;frame_size_" | |
| "bytes;keyframe;qp;encode_time_us;decode_time_us;psnr_y_db;psnr_u_" | |
| "db;psnr_v_db;target_bitrate_kbps;target_framerate_fps"; | |
| for (const auto& data : metadata) { | |
| header << ";" << data.first; | |
| } | |
| fwrite(header.str().c_str(), 1, header.size(), csv_file); | |
| for (const Frame& f : frames) { | |
| StringBuilder row; | |
| row << "\n" << f.timestamp_rtp; | |
| row << ";" << f.layer_id.spatial_idx; | |
| row << ";" << f.layer_id.temporal_idx; | |
| row << ";" << f.width; | |
| row << ";" << f.height; | |
| row << ";" << f.frame_size.bytes(); | |
| row << ";" << f.keyframe; | |
| row << ";"; | |
| if (f.qp) { | |
| row << *f.qp; | |
| } | |
| row << ";" << f.encode_time.us(); | |
| row << ";" << f.decode_time.us(); | |
| if (f.psnr) { | |
| row << ";" << f.psnr->y; | |
| row << ";" << f.psnr->u; | |
| row << ";" << f.psnr->v; | |
| } else { | |
| row << ";;;"; | |
| } | |
| const auto& es = encoding_settings_.at(f.timestamp_rtp); | |
| row << ";" | |
| << f.target_bitrate.value_or(GetTargetBitrate(es, f.layer_id)).kbps(); | |
| row << ";" | |
| << f.target_framerate.value_or(GetTargetFramerate(es, f.layer_id)) | |
| .hertz<double>(); | |
| for (const auto& data : metadata) { | |
| row << ";" << data.second; | |
| } | |
| fwrite(row.str().c_str(), 1, row.size(), csv_file); | |
| } | |
| fclose(csv_file); | |
| } | |
| void Flush() { task_queue_.WaitForPreviouslyPostedTasks(); } | |
| private: | |
| struct FrameId { | |
| uint32_t timestamp_rtp; | |
| int spatial_idx; | |
| bool operator==(const FrameId& o) const { | |
| return timestamp_rtp == o.timestamp_rtp && spatial_idx == o.spatial_idx; | |
| } | |
| bool operator<(const FrameId& o) const { | |
| return timestamp_rtp < o.timestamp_rtp || | |
| (timestamp_rtp == o.timestamp_rtp && spatial_idx < o.spatial_idx); | |
| } | |
| }; | |
| Frame::Psnr CalcPsnr(const I420BufferInterface& ref_buffer, | |
| const I420BufferInterface& dec_buffer) { | |
| RTC_CHECK_EQ(ref_buffer.width(), dec_buffer.width()); | |
| RTC_CHECK_EQ(ref_buffer.height(), dec_buffer.height()); | |
| uint64_t sse_y = libyuv::ComputeSumSquareErrorPlane( | |
| dec_buffer.DataY(), dec_buffer.StrideY(), ref_buffer.DataY(), | |
| ref_buffer.StrideY(), dec_buffer.width(), dec_buffer.height()); | |
| uint64_t sse_u = libyuv::ComputeSumSquareErrorPlane( | |
| dec_buffer.DataU(), dec_buffer.StrideU(), ref_buffer.DataU(), | |
| ref_buffer.StrideU(), dec_buffer.width() / 2, dec_buffer.height() / 2); | |
| uint64_t sse_v = libyuv::ComputeSumSquareErrorPlane( | |
| dec_buffer.DataV(), dec_buffer.StrideV(), ref_buffer.DataV(), | |
| ref_buffer.StrideV(), dec_buffer.width() / 2, dec_buffer.height() / 2); | |
| int num_y_samples = dec_buffer.width() * dec_buffer.height(); | |
| Frame::Psnr psnr; | |
| psnr.y = libyuv::SumSquareErrorToPsnr(sse_y, num_y_samples); | |
| psnr.u = libyuv::SumSquareErrorToPsnr(sse_u, num_y_samples / 4); | |
| psnr.v = libyuv::SumSquareErrorToPsnr(sse_v, num_y_samples / 4); | |
| return psnr; | |
| } | |
| DataRate GetTargetBitrate(const EncodingSettings& encoding_settings, | |
| std::optional<LayerId> layer_id) const { | |
| int base_spatial_idx; | |
| if (layer_id.has_value()) { | |
| bool is_svc = | |
| kFullSvcScalabilityModes.count(encoding_settings.scalability_mode); | |
| base_spatial_idx = is_svc ? 0 : layer_id->spatial_idx; | |
| } else { | |
| int num_spatial_layers = | |
| ScalabilityModeToNumSpatialLayers(encoding_settings.scalability_mode); | |
| int num_temporal_layers = ScalabilityModeToNumTemporalLayers( | |
| encoding_settings.scalability_mode); | |
| layer_id = LayerId({.spatial_idx = num_spatial_layers - 1, | |
| .temporal_idx = num_temporal_layers - 1}); | |
| base_spatial_idx = 0; | |
| } | |
| DataRate bitrate = DataRate::Zero(); | |
| for (int sidx = base_spatial_idx; sidx <= layer_id->spatial_idx; ++sidx) { | |
| for (int tidx = 0; tidx <= layer_id->temporal_idx; ++tidx) { | |
| auto layer_settings = encoding_settings.layers_settings.find( | |
| {.spatial_idx = sidx, .temporal_idx = tidx}); | |
| RTC_CHECK(layer_settings != encoding_settings.layers_settings.end()) | |
| << "bitrate is not specified for layer sidx=" << sidx | |
| << " tidx=" << tidx; | |
| bitrate += layer_settings->second.bitrate; | |
| } | |
| } | |
| return bitrate; | |
| } | |
| Frequency GetTargetFramerate(const EncodingSettings& encoding_settings, | |
| std::optional<LayerId> layer_id) const { | |
| if (layer_id.has_value()) { | |
| auto layer_settings = encoding_settings.layers_settings.find( | |
| {.spatial_idx = layer_id->spatial_idx, | |
| .temporal_idx = layer_id->temporal_idx}); | |
| RTC_CHECK(layer_settings != encoding_settings.layers_settings.end()) | |
| << "framerate is not specified for layer sidx=" | |
| << layer_id->spatial_idx << " tidx=" << layer_id->temporal_idx; | |
| return layer_settings->second.framerate; | |
| } | |
| return encoding_settings.layers_settings.rbegin()->second.framerate; | |
| } | |
| SamplesStatsCounter::StatsSample StatsSample(double value, | |
| Timestamp time) const { | |
| return SamplesStatsCounter::StatsSample{.value = value, .time = time}; | |
| } | |
| Clock* const clock_; | |
| LimitedTaskQueue task_queue_; | |
| // RTP timestamp -> spatial layer -> Frame | |
| std::map<uint32_t, std::map<int, Frame>> frames_; | |
| std::map<uint32_t, EncodingSettings> encoding_settings_; | |
| }; | |
| class Decoder : public DecodedImageCallback { | |
| public: | |
| Decoder(const Environment& env, | |
| VideoDecoderFactory* decoder_factory, | |
| const DecoderSettings& decoder_settings, | |
| VideoCodecAnalyzer* analyzer) | |
| : env_(env), | |
| decoder_clock_( | |
| decoder_settings.pacing_settings.mode == PacingMode::kRealTime | |
| ? nullptr | |
| : std::make_unique<SimulatedClock>(env.clock().CurrentTime())), | |
| decoder_factory_(decoder_factory), | |
| decoder_settings_(decoder_settings), | |
| analyzer_(analyzer), | |
| pacer_(&env.clock(), decoder_settings.pacing_settings), | |
| task_queue_(&env.clock()) { | |
| RTC_CHECK(analyzer_) << "Analyzer must be provided"; | |
| if (decoder_settings.decoder_input_base_path) { | |
| ivf_writer_ = std::make_unique<TesterIvfWriter>( | |
| *decoder_settings.decoder_input_base_path); | |
| } | |
| if (decoder_settings.decoder_output_base_path) { | |
| y4m_writer_ = std::make_unique<TesterY4mWriter>( | |
| *decoder_settings.decoder_output_base_path); | |
| } | |
| } | |
| void Initialize(const SdpVideoFormat& sdp_video_format) { | |
| Environment codec_env = env_; | |
| if (decoder_clock_) { | |
| codec_env = CreateEnvironment(&env_.field_trials(), decoder_clock_.get()); | |
| } | |
| decoder_ = decoder_factory_->Create(codec_env, sdp_video_format); | |
| RTC_CHECK(decoder_) << "Could not create decoder for video format " | |
| << sdp_video_format.ToString(); | |
| codec_type_ = PayloadStringToCodecType(sdp_video_format.name); | |
| task_queue_.PostTaskAndWait([this] { | |
| decoder_->RegisterDecodeCompleteCallback(this); | |
| VideoDecoder::Settings ds; | |
| ds.set_codec_type(*codec_type_); | |
| ds.set_number_of_cores(decoder_settings_.num_cores); | |
| ds.set_max_render_resolution({1280, 720}); | |
| bool result = decoder_->Configure(ds); | |
| RTC_CHECK(result) << "Failed to configure decoder"; | |
| }); | |
| } | |
| void Decode(const EncodedImage& encoded_frame, | |
| std::optional<VideoFrame> ref_frame = std::nullopt) { | |
| int spatial_idx = encoded_frame.SpatialIndex().value_or( | |
| encoded_frame.SimulcastIndex().value_or(0)); | |
| { | |
| MutexLock lock(&mutex_); | |
| RTC_CHECK_EQ(spatial_idx_.value_or(spatial_idx), spatial_idx) | |
| << "Spatial index changed from " << *spatial_idx_ << " to " | |
| << spatial_idx; | |
| spatial_idx_ = spatial_idx; | |
| if (ref_frame.has_value()) { | |
| ref_frames_.insert({encoded_frame.RtpTimestamp(), *ref_frame}); | |
| } | |
| } | |
| Timestamp pts = | |
| Timestamp::Micros((encoded_frame.RtpTimestamp() / k90kHz).us()); | |
| task_queue_.PostScheduledTask( | |
| [this, encoded_frame] { | |
| if (decoder_clock_) { | |
| Timestamp pts = | |
| Timestamp::Micros((encoded_frame.RtpTimestamp() / k90kHz).us()); | |
| if (pts >= decoder_clock_->CurrentTime()) { | |
| decoder_clock_->AdvanceTime(pts - decoder_clock_->CurrentTime()); | |
| } | |
| } | |
| analyzer_->StartDecode(encoded_frame); | |
| int error = decoder_->Decode(encoded_frame, /*render_time_ms*/ 0); | |
| if (error != 0) { | |
| RTC_LOG(LS_WARNING) | |
| << "Decode failed with error code " << error | |
| << " RTP timestamp " << encoded_frame.RtpTimestamp(); | |
| } | |
| }, | |
| pacer_.Schedule(pts)); | |
| if (ivf_writer_) { | |
| ivf_writer_->Write(encoded_frame, *codec_type_); | |
| } | |
| } | |
| void Flush() { | |
| // TODO(webrtc:14852): Add Flush() to VideoDecoder API. | |
| task_queue_.PostTaskAndWait([this] { decoder_->Release(); }); | |
| } | |
| private: | |
| int Decoded(VideoFrame& decoded_frame) override { | |
| int spatial_idx; | |
| std::optional<VideoFrame> ref_frame; | |
| { | |
| MutexLock lock(&mutex_); | |
| spatial_idx = *spatial_idx_; | |
| if (!ref_frames_.empty()) { | |
| auto it = ref_frames_.find(decoded_frame.rtp_timestamp()); | |
| RTC_CHECK(it != ref_frames_.end()); | |
| ref_frame = it->second; | |
| ref_frames_.erase(ref_frames_.begin(), std::next(it)); | |
| } | |
| } | |
| analyzer_->FinishDecode(decoded_frame, spatial_idx, ref_frame); | |
| if (y4m_writer_) { | |
| y4m_writer_->Write(decoded_frame, spatial_idx); | |
| } | |
| return WEBRTC_VIDEO_CODEC_OK; | |
| } | |
| const Environment env_; | |
| std::unique_ptr<SimulatedClock> decoder_clock_; | |
| VideoDecoderFactory* decoder_factory_; | |
| const DecoderSettings decoder_settings_; | |
| std::unique_ptr<VideoDecoder> decoder_; | |
| VideoCodecAnalyzer* const analyzer_; | |
| Pacer pacer_; | |
| LimitedTaskQueue task_queue_; | |
| std::unique_ptr<TesterIvfWriter> ivf_writer_; | |
| std::unique_ptr<TesterY4mWriter> y4m_writer_; | |
| std::optional<VideoCodecType> codec_type_; | |
| std::optional<int> spatial_idx_ RTC_GUARDED_BY(mutex_); | |
| std::map<uint32_t, VideoFrame> ref_frames_ RTC_GUARDED_BY(mutex_); | |
| Mutex mutex_; | |
| }; | |
| class Encoder : public EncodedImageCallback { | |
| public: | |
| using EncodeCallback = | |
| absl::AnyInvocable<void(const EncodedImage& encoded_frame)>; | |
| Encoder(const Environment& env, | |
| VideoEncoderFactory* encoder_factory, | |
| const EncoderSettings& encoder_settings, | |
| VideoCodecAnalyzer* analyzer) | |
| : env_(env), | |
| encoder_clock_( | |
| encoder_settings.pacing_settings.mode == PacingMode::kRealTime | |
| ? nullptr | |
| : std::make_unique<SimulatedClock>(env.clock().CurrentTime())), | |
| encoder_factory_(encoder_factory), | |
| encoder_settings_(encoder_settings), | |
| analyzer_(analyzer), | |
| pacer_(&env.clock(), encoder_settings.pacing_settings), | |
| task_queue_(&env.clock()) { | |
| RTC_CHECK(analyzer_) << "Analyzer must be provided"; | |
| if (encoder_settings.encoder_input_base_path) { | |
| y4m_writer_ = std::make_unique<TesterY4mWriter>( | |
| *encoder_settings.encoder_input_base_path); | |
| } | |
| if (encoder_settings.encoder_output_base_path) { | |
| ivf_writer_ = std::make_unique<TesterIvfWriter>( | |
| *encoder_settings.encoder_output_base_path); | |
| } | |
| } | |
| void Initialize(const EncodingSettings& encoding_settings) { | |
| Environment codec_env = env_; | |
| if (encoder_clock_) { | |
| codec_env = CreateEnvironment(&env_.field_trials(), encoder_clock_.get()); | |
| } | |
| encoder_ = | |
| encoder_factory_->Create(codec_env, encoding_settings.sdp_video_format); | |
| RTC_CHECK(encoder_) << "Could not create encoder for video format " | |
| << encoding_settings.sdp_video_format.ToString(); | |
| codec_type_ = | |
| PayloadStringToCodecType(encoding_settings.sdp_video_format.name); | |
| task_queue_.PostTaskAndWait([this, encoding_settings] { | |
| encoder_->RegisterEncodeCompleteCallback(this); | |
| Configure(encoding_settings); | |
| SetRates(encoding_settings); | |
| }); | |
| } | |
| void Encode(const VideoFrame& input_frame, | |
| const EncodingSettings& encoding_settings, | |
| EncodeCallback callback) { | |
| { | |
| MutexLock lock(&mutex_); | |
| callbacks_[input_frame.rtp_timestamp()] = std::move(callback); | |
| } | |
| Timestamp pts = | |
| Timestamp::Micros((input_frame.rtp_timestamp() / k90kHz).us()); | |
| task_queue_.PostScheduledTask( | |
| [this, input_frame, encoding_settings] { | |
| if (encoder_clock_) { | |
| Timestamp pts = | |
| Timestamp::Micros((input_frame.rtp_timestamp() / k90kHz).us()); | |
| if (pts >= encoder_clock_->CurrentTime()) { | |
| encoder_clock_->AdvanceTime(pts - encoder_clock_->CurrentTime()); | |
| } | |
| } | |
| analyzer_->StartEncode(input_frame, encoding_settings); | |
| if (!last_encoding_settings_ || | |
| !IsSameRate(encoding_settings, *last_encoding_settings_)) { | |
| SetRates(encoding_settings); | |
| } | |
| last_encoding_settings_ = encoding_settings; | |
| std::vector<VideoFrameType> frame_types = { | |
| encoding_settings.keyframe ? VideoFrameType::kVideoFrameKey | |
| : VideoFrameType::kVideoFrameDelta}; | |
| int error = encoder_->Encode(input_frame, &frame_types); | |
| if (error != 0) { | |
| RTC_LOG(LS_WARNING) | |
| << "Encode failed with error code " << error | |
| << " RTP timestamp " << input_frame.rtp_timestamp(); | |
| } | |
| }, | |
| pacer_.Schedule(pts)); | |
| if (y4m_writer_) { | |
| y4m_writer_->Write(input_frame, /*spatial_idx=*/0); | |
| } | |
| } | |
| void Flush() { | |
| task_queue_.PostTaskAndWait([this] { encoder_->Release(); }); | |
| if (last_superframe_) { | |
| int num_spatial_layers = | |
| ScalabilityModeToNumSpatialLayers(last_superframe_->scalability_mode); | |
| for (int sidx = *last_superframe_->encoded_frame.SpatialIndex() + 1; | |
| sidx < num_spatial_layers; ++sidx) { | |
| last_superframe_->encoded_frame.SetSpatialIndex(sidx); | |
| DeliverEncodedFrame(last_superframe_->encoded_frame); | |
| } | |
| last_superframe_.reset(); | |
| } | |
| } | |
| private: | |
| struct Superframe { | |
| EncodedImage encoded_frame; | |
| scoped_refptr<EncodedImageBuffer> encoded_data; | |
| ScalabilityMode scalability_mode; | |
| }; | |
| void OnFrameDropped(uint32_t /*rtp_timestamp*/, | |
| int /*spatial_id*/, | |
| bool /*is_end_of_temporal_unit*/) override {} | |
| Result OnEncodedImage(const EncodedImage& encoded_frame, | |
| const CodecSpecificInfo* codec_specific_info) override { | |
| analyzer_->FinishEncode(encoded_frame); | |
| if (last_superframe_ && last_superframe_->encoded_frame.RtpTimestamp() != | |
| encoded_frame.RtpTimestamp()) { | |
| // New temporal unit. We have frame of previous temporal unit (TU) stored | |
| // which means that the previous TU used spatial prediction. If encoder | |
| // dropped a frame of layer X in the previous TU, mark the stored frame | |
| // as a frame belonging to layer >X and deliver it such that decoders of | |
| // layer >X receive encoded lower layers. | |
| int num_spatial_layers = | |
| ScalabilityModeToNumSpatialLayers(last_superframe_->scalability_mode); | |
| for (int sidx = | |
| last_superframe_->encoded_frame.SpatialIndex().value_or(0) + 1; | |
| sidx < num_spatial_layers; ++sidx) { | |
| last_superframe_->encoded_frame.SetSpatialIndex(sidx); | |
| DeliverEncodedFrame(last_superframe_->encoded_frame); | |
| } | |
| last_superframe_.reset(); | |
| } | |
| const EncodedImage& superframe = | |
| MakeSuperFrame(encoded_frame, codec_specific_info); | |
| DeliverEncodedFrame(superframe); | |
| return Result(Result::Error::OK); | |
| } | |
| void DeliverEncodedFrame(const EncodedImage& encoded_frame) { | |
| { | |
| MutexLock lock(&mutex_); | |
| auto it = callbacks_.find(encoded_frame.RtpTimestamp()); | |
| RTC_CHECK(it != callbacks_.end()); | |
| it->second(encoded_frame); | |
| callbacks_.erase(callbacks_.begin(), it); | |
| } | |
| if (ivf_writer_ != nullptr) { | |
| ivf_writer_->Write(encoded_frame, codec_type_); | |
| } | |
| } | |
| void Configure(const EncodingSettings& es) { | |
| const LayerSettings& top_layer_settings = | |
| es.layers_settings.rbegin()->second; | |
| const int num_spatial_layers = | |
| ScalabilityModeToNumSpatialLayers(es.scalability_mode); | |
| const int num_temporal_layers = | |
| ScalabilityModeToNumTemporalLayers(es.scalability_mode); | |
| DataRate total_bitrate = std::accumulate( | |
| es.layers_settings.begin(), es.layers_settings.end(), DataRate::Zero(), | |
| [](DataRate acc, const std::pair<const LayerId, LayerSettings> layer) { | |
| return acc + layer.second.bitrate; | |
| }); | |
| VideoCodec vc; | |
| vc.width = top_layer_settings.resolution.width; | |
| vc.height = top_layer_settings.resolution.height; | |
| vc.startBitrate = total_bitrate.kbps(); | |
| vc.maxBitrate = total_bitrate.kbps(); | |
| vc.minBitrate = 0; | |
| vc.maxFramerate = top_layer_settings.framerate.hertz<uint32_t>(); | |
| vc.active = true; | |
| vc.numberOfSimulcastStreams = 0; | |
| vc.mode = es.content_type; | |
| vc.SetFrameDropEnabled(es.frame_drop); | |
| vc.SetScalabilityMode(es.scalability_mode); | |
| vc.SetVideoEncoderComplexity(VideoCodecComplexity::kComplexityNormal); | |
| vc.codecType = PayloadStringToCodecType(es.sdp_video_format.name); | |
| switch (vc.codecType) { | |
| case kVideoCodecVP8: | |
| *(vc.VP8()) = VideoEncoder::GetDefaultVp8Settings(); | |
| vc.VP8()->SetNumberOfTemporalLayers(num_temporal_layers); | |
| vc.SetScalabilityMode(std::vector<ScalabilityMode>{ | |
| ScalabilityMode::kL1T1, ScalabilityMode::kL1T2, | |
| ScalabilityMode::kL1T3}[num_temporal_layers - 1]); | |
| vc.qpMax = kDefaultVideoMaxQpVpx; | |
| break; | |
| case kVideoCodecVP9: | |
| *(vc.VP9()) = VideoEncoder::GetDefaultVp9Settings(); | |
| vc.qpMax = kDefaultVideoMaxQpVpx; | |
| break; | |
| case kVideoCodecAV1: | |
| vc.qpMax = kDefaultVideoMaxQpAv1; | |
| break; | |
| case kVideoCodecH264: | |
| *(vc.H264()) = VideoEncoder::GetDefaultH264Settings(); | |
| vc.H264()->SetNumberOfTemporalLayers(num_temporal_layers); | |
| vc.qpMax = kDefaultVideoMaxQpH26x; | |
| break; | |
| case kVideoCodecH265: | |
| vc.qpMax = kDefaultVideoMaxQpH26x; | |
| break; | |
| case kVideoCodecGeneric: | |
| RTC_CHECK_NOTREACHED(); | |
| break; | |
| } | |
| bool is_simulcast = | |
| num_spatial_layers > 1 && | |
| (vc.codecType == kVideoCodecVP8 || vc.codecType == kVideoCodecH264 || | |
| vc.codecType == kVideoCodecH265); | |
| if (is_simulcast) { | |
| vc.numberOfSimulcastStreams = num_spatial_layers; | |
| for (int sidx = 0; sidx < num_spatial_layers; ++sidx) { | |
| const Resolution& resolution = | |
| es.layers_settings | |
| .at(LayerId{.spatial_idx = sidx, .temporal_idx = 0}) | |
| .resolution; | |
| DataRate total_layer_bitrate = DataRate::Zero(); | |
| for (int tidx = 0; tidx < num_temporal_layers; ++tidx) { | |
| total_layer_bitrate += | |
| es.layers_settings | |
| .at(LayerId{.spatial_idx = sidx, .temporal_idx = tidx}) | |
| .bitrate; | |
| } | |
| SimulcastStream& ss = vc.simulcastStream[sidx]; | |
| ss.width = resolution.width; | |
| ss.height = resolution.height; | |
| ss.numberOfTemporalLayers = num_temporal_layers; | |
| ss.maxBitrate = total_layer_bitrate.kbps(); | |
| ss.targetBitrate = total_layer_bitrate.kbps(); | |
| ss.minBitrate = 0; | |
| ss.maxFramerate = vc.maxFramerate; | |
| ss.qpMax = vc.qpMax; | |
| ss.active = true; | |
| } | |
| } else if (vc.codecType == kVideoCodecVP9 || | |
| vc.codecType == kVideoCodecAV1) { | |
| for (int sidx = 0; sidx < num_spatial_layers; ++sidx) { | |
| const Resolution& resolution = | |
| es.layers_settings | |
| .at(LayerId{.spatial_idx = sidx, .temporal_idx = 0}) | |
| .resolution; | |
| DataRate total_layer_bitrate = DataRate::Zero(); | |
| for (int tidx = 0; tidx < num_temporal_layers; ++tidx) { | |
| total_layer_bitrate += | |
| es.layers_settings | |
| .at(LayerId{.spatial_idx = sidx, .temporal_idx = tidx}) | |
| .bitrate; | |
| } | |
| SpatialLayer& ss = vc.spatialLayers[sidx]; | |
| ss.width = resolution.width; | |
| ss.height = resolution.height; | |
| ss.numberOfTemporalLayers = num_temporal_layers; | |
| ss.maxBitrate = total_layer_bitrate.kbps(); | |
| ss.targetBitrate = total_layer_bitrate.kbps(); | |
| ss.minBitrate = 0; | |
| ss.maxFramerate = vc.maxFramerate; | |
| ss.qpMax = vc.qpMax; | |
| ss.active = true; | |
| } | |
| } | |
| VideoEncoder::Settings ves( | |
| VideoEncoder::Capabilities(/*loss_notification=*/false), | |
| /*number_of_cores=*/encoder_settings_.num_cores, | |
| /*max_payload_size=*/1440); | |
| int result = encoder_->InitEncode(&vc, ves); | |
| RTC_CHECK(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 = | |
| ScalabilityModeToNumTemporalLayers(es.scalability_mode); | |
| for (int sidx = 0; sidx < num_spatial_layers; ++sidx) { | |
| for (int tidx = 0; tidx < num_temporal_layers; ++tidx) { | |
| auto layers_settings = es.layers_settings.find( | |
| {.spatial_idx = sidx, .temporal_idx = tidx}); | |
| RTC_CHECK(layers_settings != es.layers_settings.end()) | |
| << "Bitrate for layer S=" << sidx << " T=" << tidx << " is not set"; | |
| rc.bitrate.SetBitrate(sidx, tidx, | |
| layers_settings->second.bitrate.bps()); | |
| } | |
| } | |
| rc.framerate_fps = | |
| es.layers_settings.rbegin()->second.framerate.hertz<double>(); | |
| encoder_->SetRates(rc); | |
| } | |
| bool IsSameRate(const EncodingSettings& a, const EncodingSettings& b) const { | |
| for (auto [layer_id, layer] : a.layers_settings) { | |
| const auto& other_layer = b.layers_settings.at(layer_id); | |
| if (layer.bitrate != other_layer.bitrate || | |
| layer.framerate != other_layer.framerate) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| static bool IsSvc(const EncodedImage& encoded_frame, | |
| const CodecSpecificInfo& codec_specific_info) { | |
| if (!codec_specific_info.scalability_mode) { | |
| return false; | |
| } | |
| ScalabilityMode scalability_mode = *codec_specific_info.scalability_mode; | |
| return (kFullSvcScalabilityModes.count(scalability_mode) || | |
| (kKeySvcScalabilityModes.count(scalability_mode) && | |
| encoded_frame.IsKey())); | |
| } | |
| const EncodedImage& MakeSuperFrame( | |
| const EncodedImage& encoded_frame, | |
| const CodecSpecificInfo* codec_specific_info) { | |
| if (last_superframe_) { | |
| // Append to base spatial layer frame(s). | |
| RTC_CHECK_EQ(*encoded_frame.SpatialIndex(), | |
| *last_superframe_->encoded_frame.SpatialIndex() + 1) | |
| << "Inter-layer frame drops are not supported."; | |
| size_t current_size = last_superframe_->encoded_data->size(); | |
| last_superframe_->encoded_data->Realloc(current_size + | |
| encoded_frame.size()); | |
| memcpy(last_superframe_->encoded_data->data() + current_size, | |
| encoded_frame.data(), encoded_frame.size()); | |
| last_superframe_->encoded_frame.SetEncodedData( | |
| last_superframe_->encoded_data); | |
| last_superframe_->encoded_frame.SetSpatialIndex( | |
| encoded_frame.SpatialIndex()); | |
| return last_superframe_->encoded_frame; | |
| } | |
| RTC_CHECK(codec_specific_info != nullptr); | |
| if (IsSvc(encoded_frame, *codec_specific_info)) { | |
| last_superframe_ = Superframe{ | |
| .encoded_frame = EncodedImage(encoded_frame), | |
| .encoded_data = EncodedImageBuffer::Create(encoded_frame.data(), | |
| encoded_frame.size()), | |
| .scalability_mode = *codec_specific_info->scalability_mode}; | |
| last_superframe_->encoded_frame.SetEncodedData( | |
| last_superframe_->encoded_data); | |
| return last_superframe_->encoded_frame; | |
| } | |
| return encoded_frame; | |
| } | |
| const Environment env_; | |
| std::unique_ptr<SimulatedClock> encoder_clock_; | |
| VideoEncoderFactory* const encoder_factory_; | |
| const EncoderSettings encoder_settings_; | |
| std::unique_ptr<VideoEncoder> encoder_; | |
| VideoCodecAnalyzer* const analyzer_; | |
| Pacer pacer_; | |
| std::optional<EncodingSettings> last_encoding_settings_; | |
| std::unique_ptr<VideoBitrateAllocator> bitrate_allocator_; | |
| LimitedTaskQueue task_queue_; | |
| std::unique_ptr<TesterY4mWriter> y4m_writer_; | |
| std::unique_ptr<TesterIvfWriter> ivf_writer_; | |
| std::map<uint32_t, int> sidx_ RTC_GUARDED_BY(mutex_); | |
| std::map<uint32_t, EncodeCallback> callbacks_ RTC_GUARDED_BY(mutex_); | |
| VideoCodecType codec_type_; | |
| std::optional<Superframe> last_superframe_; | |
| Mutex mutex_; | |
| }; | |
| void ConfigureSimulcast(const FieldTrialsView& field_trials, VideoCodec* vc) { | |
| int num_spatial_layers = | |
| ScalabilityModeToNumSpatialLayers(*vc->GetScalabilityMode()); | |
| int num_temporal_layers = | |
| ScalabilityModeToNumTemporalLayers(*vc->GetScalabilityMode()); | |
| if (num_spatial_layers == 1) { | |
| SimulcastStream* ss = &vc->simulcastStream[0]; | |
| ss->width = vc->width; | |
| ss->height = vc->height; | |
| ss->numberOfTemporalLayers = num_temporal_layers; | |
| ss->maxBitrate = vc->maxBitrate; | |
| ss->targetBitrate = vc->maxBitrate; | |
| ss->minBitrate = vc->minBitrate; | |
| ss->qpMax = vc->qpMax; | |
| ss->active = true; | |
| return; | |
| } | |
| VideoEncoderConfig encoder_config; | |
| encoder_config.codec_type = vc->codecType; | |
| encoder_config.number_of_streams = num_spatial_layers; | |
| encoder_config.simulcast_layers.resize(num_spatial_layers); | |
| VideoEncoder::EncoderInfo encoder_info; | |
| auto stream_factory = make_ref_counted<EncoderStreamFactory>(encoder_info); | |
| const std::vector<VideoStream> streams = stream_factory->CreateEncoderStreams( | |
| field_trials, vc->width, vc->height, encoder_config); | |
| vc->numberOfSimulcastStreams = streams.size(); | |
| RTC_CHECK_LE(vc->numberOfSimulcastStreams, num_spatial_layers); | |
| if (vc->numberOfSimulcastStreams < num_spatial_layers) { | |
| vc->SetScalabilityMode(LimitNumSpatialLayers(*vc->GetScalabilityMode(), | |
| vc->numberOfSimulcastStreams)); | |
| } | |
| for (int i = 0; i < vc->numberOfSimulcastStreams; ++i) { | |
| SimulcastStream* ss = &vc->simulcastStream[i]; | |
| ss->width = streams[i].width; | |
| ss->height = streams[i].height; | |
| ss->numberOfTemporalLayers = num_temporal_layers; | |
| ss->maxBitrate = streams[i].max_bitrate_bps / 1000; | |
| ss->targetBitrate = streams[i].target_bitrate_bps / 1000; | |
| ss->minBitrate = streams[i].min_bitrate_bps / 1000; | |
| ss->qpMax = vc->qpMax; | |
| ss->active = true; | |
| } | |
| } | |
| void SetDefaultCodecSpecificSettings(VideoCodec* vc, int num_temporal_layers) { | |
| switch (vc->codecType) { | |
| case kVideoCodecVP8: | |
| *(vc->VP8()) = VideoEncoder::GetDefaultVp8Settings(); | |
| vc->VP8()->SetNumberOfTemporalLayers(num_temporal_layers); | |
| break; | |
| case kVideoCodecVP9: { | |
| *(vc->VP9()) = VideoEncoder::GetDefaultVp9Settings(); | |
| vc->VP9()->SetNumberOfTemporalLayers(num_temporal_layers); | |
| } break; | |
| case kVideoCodecH264: { | |
| *(vc->H264()) = VideoEncoder::GetDefaultH264Settings(); | |
| vc->H264()->SetNumberOfTemporalLayers(num_temporal_layers); | |
| } break; | |
| case kVideoCodecAV1: | |
| case kVideoCodecH265: | |
| break; | |
| case kVideoCodecGeneric: | |
| RTC_CHECK_NOTREACHED(); | |
| } | |
| } | |
| std::tuple<std::vector<DataRate>, ScalabilityMode> | |
| SplitBitrateAndUpdateScalabilityMode(const Environment& env, | |
| std::string codec_type, | |
| ScalabilityMode scalability_mode, | |
| int width, | |
| int height, | |
| std::vector<DataRate> layer_bitrate, | |
| Frequency framerate, | |
| VideoCodecMode content_type) { | |
| int num_spatial_layers = ScalabilityModeToNumSpatialLayers(scalability_mode); | |
| int num_temporal_layers = | |
| ScalabilityModeToNumTemporalLayers(scalability_mode); | |
| int num_bitrates = static_cast<int>(layer_bitrate.size()); | |
| RTC_CHECK(num_bitrates == 1 || num_bitrates == num_spatial_layers || | |
| num_bitrates == num_spatial_layers * num_temporal_layers); | |
| if (num_bitrates == num_spatial_layers * num_temporal_layers) { | |
| return std::make_tuple(layer_bitrate, scalability_mode); | |
| } | |
| DataRate total_bitrate = std::accumulate( | |
| layer_bitrate.begin(), layer_bitrate.end(), DataRate::Zero()); | |
| VideoCodec vc; | |
| vc.codecType = PayloadStringToCodecType(codec_type); | |
| vc.width = width; | |
| vc.height = height; | |
| vc.startBitrate = total_bitrate.kbps(); | |
| vc.maxBitrate = total_bitrate.kbps(); | |
| vc.minBitrate = 0; | |
| vc.maxFramerate = framerate.hertz(); | |
| vc.numberOfSimulcastStreams = 0; | |
| vc.mode = content_type; | |
| vc.SetScalabilityMode(scalability_mode); | |
| SetDefaultCodecSpecificSettings(&vc, num_temporal_layers); | |
| if (num_bitrates == num_spatial_layers) { | |
| switch (vc.codecType) { | |
| case kVideoCodecVP8: | |
| case kVideoCodecH264: | |
| case kVideoCodecH265: | |
| vc.numberOfSimulcastStreams = num_spatial_layers; | |
| for (int sidx = 0; sidx < num_spatial_layers; ++sidx) { | |
| SimulcastStream* ss = &vc.simulcastStream[sidx]; | |
| ss->width = width >> (num_spatial_layers - sidx - 1); | |
| ss->height = height >> (num_spatial_layers - sidx - 1); | |
| ss->maxFramerate = vc.maxFramerate; | |
| ss->numberOfTemporalLayers = num_temporal_layers; | |
| ss->maxBitrate = layer_bitrate[sidx].kbps(); | |
| ss->targetBitrate = layer_bitrate[sidx].kbps(); | |
| ss->minBitrate = 0; | |
| ss->qpMax = 0; | |
| ss->active = true; | |
| } | |
| break; | |
| case kVideoCodecVP9: | |
| case kVideoCodecAV1: | |
| for (int sidx = num_spatial_layers - 1; sidx >= 0; --sidx) { | |
| SpatialLayer* ss = &vc.spatialLayers[sidx]; | |
| ss->width = width >> (num_spatial_layers - sidx - 1); | |
| ss->height = height >> (num_spatial_layers - sidx - 1); | |
| ss->maxFramerate = vc.maxFramerate; | |
| ss->numberOfTemporalLayers = num_temporal_layers; | |
| ss->maxBitrate = layer_bitrate[sidx].kbps(); | |
| ss->targetBitrate = layer_bitrate[sidx].kbps(); | |
| ss->minBitrate = 0; | |
| ss->qpMax = 0; | |
| ss->active = true; | |
| } | |
| break; | |
| case kVideoCodecGeneric: | |
| RTC_CHECK_NOTREACHED(); | |
| } | |
| } else { | |
| switch (vc.codecType) { | |
| case kVideoCodecVP8: | |
| case kVideoCodecH264: | |
| case kVideoCodecH265: | |
| ConfigureSimulcast(env.field_trials(), &vc); | |
| break; | |
| case kVideoCodecVP9: { | |
| const std::vector<SpatialLayer> spatialLayers = GetVp9SvcConfig(vc); | |
| for (size_t i = 0; i < spatialLayers.size(); ++i) { | |
| vc.spatialLayers[i] = spatialLayers[i]; | |
| vc.spatialLayers[i].active = true; | |
| } | |
| } break; | |
| case kVideoCodecAV1: { | |
| bool result = | |
| SetAv1SvcConfig(vc, num_spatial_layers, num_temporal_layers); | |
| RTC_CHECK(result) << "SetAv1SvcConfig failed"; | |
| } break; | |
| case kVideoCodecGeneric: | |
| RTC_CHECK_NOTREACHED(); | |
| } | |
| if (*vc.GetScalabilityMode() != scalability_mode) { | |
| RTC_LOG(LS_WARNING) << "Scalability mode changed from " | |
| << ScalabilityModeToString(scalability_mode) << " to " | |
| << ScalabilityModeToString(*vc.GetScalabilityMode()); | |
| num_spatial_layers = | |
| ScalabilityModeToNumSpatialLayers(*vc.GetScalabilityMode()); | |
| num_temporal_layers = | |
| ScalabilityModeToNumTemporalLayers(*vc.GetScalabilityMode()); | |
| } | |
| } | |
| std::unique_ptr<VideoBitrateAllocator> bitrate_allocator = | |
| CreateBuiltinVideoBitrateAllocatorFactory()->Create(env, vc); | |
| VideoBitrateAllocation bitrate_allocation = | |
| bitrate_allocator->Allocate(VideoBitrateAllocationParameters( | |
| total_bitrate.bps(), framerate.hertz<double>())); | |
| std::vector<DataRate> bitrates; | |
| for (int sidx = 0; sidx < num_spatial_layers; ++sidx) { | |
| for (int tidx = 0; tidx < num_temporal_layers; ++tidx) { | |
| int bitrate_bps = bitrate_allocation.GetBitrate(sidx, tidx); | |
| bitrates.push_back(DataRate::BitsPerSec(bitrate_bps)); | |
| } | |
| } | |
| return std::make_tuple(bitrates, *vc.GetScalabilityMode()); | |
| } | |
| } // namespace | |
| void VideoCodecStats::Stream::LogMetrics( | |
| MetricsLogger* logger, | |
| std::string test_case_name, | |
| std::string prefix, | |
| std::map<std::string, std::string> metadata) const { | |
| logger->LogMetric(prefix + "width", test_case_name, width, Unit::kCount, | |
| ImprovementDirection::kBiggerIsBetter, metadata); | |
| logger->LogMetric(prefix + "height", test_case_name, height, Unit::kCount, | |
| ImprovementDirection::kBiggerIsBetter, metadata); | |
| logger->LogMetric(prefix + "frame_size_bytes", test_case_name, | |
| frame_size_bytes, Unit::kBytes, | |
| ImprovementDirection::kNeitherIsBetter, metadata); | |
| logger->LogMetric(prefix + "keyframe", test_case_name, keyframe, Unit::kCount, | |
| ImprovementDirection::kSmallerIsBetter, metadata); | |
| logger->LogMetric(prefix + "qp", test_case_name, qp, Unit::kUnitless, | |
| ImprovementDirection::kSmallerIsBetter, metadata); | |
| logger->LogMetric(prefix + "encode_time_us", test_case_name, encode_time_us, | |
| Unit::kUnitless, ImprovementDirection::kSmallerIsBetter, | |
| metadata); | |
| logger->LogMetric(prefix + "decode_time_us", test_case_name, decode_time_us, | |
| Unit::kUnitless, ImprovementDirection::kSmallerIsBetter, | |
| metadata); | |
| // We use Unit::kUnitless for kbps bitrate metrics to prevent the Chromeperf | |
| // exporter from converting them to bytes per second (which alters the raw | |
| // values). | |
| logger->LogMetric(prefix + "target_bitrate_kbps", test_case_name, | |
| target_bitrate_kbps, Unit::kUnitless, | |
| ImprovementDirection::kBiggerIsBetter, metadata); | |
| logger->LogMetric(prefix + "target_framerate_fps", test_case_name, | |
| target_framerate_fps, Unit::kHertz, | |
| ImprovementDirection::kBiggerIsBetter, metadata); | |
| logger->LogMetric(prefix + "encoded_bitrate_kbps", test_case_name, | |
| encoded_bitrate_kbps, Unit::kUnitless, | |
| ImprovementDirection::kBiggerIsBetter, metadata); | |
| logger->LogMetric(prefix + "encoded_framerate_fps", test_case_name, | |
| encoded_framerate_fps, Unit::kHertz, | |
| ImprovementDirection::kBiggerIsBetter, metadata); | |
| logger->LogMetric(prefix + "bitrate_mismatch_pct", test_case_name, | |
| bitrate_mismatch_pct, Unit::kPercent, | |
| ImprovementDirection::kNeitherIsBetter, metadata); | |
| logger->LogMetric(prefix + "framerate_mismatch_pct", test_case_name, | |
| framerate_mismatch_pct, Unit::kPercent, | |
| ImprovementDirection::kNeitherIsBetter, metadata); | |
| logger->LogMetric(prefix + "buffer_delay_ms", test_case_name, buffer_delay_ms, | |
| Unit::kMilliseconds, ImprovementDirection::kSmallerIsBetter, | |
| metadata); | |
| logger->LogMetric(prefix + "psnr_y_db", test_case_name, psnr.y, | |
| Unit::kUnitless, ImprovementDirection::kBiggerIsBetter, | |
| metadata); | |
| logger->LogMetric(prefix + "psnr_u_db", test_case_name, psnr.u, | |
| Unit::kUnitless, ImprovementDirection::kBiggerIsBetter, | |
| metadata); | |
| logger->LogMetric(prefix + "psnr_v_db", test_case_name, psnr.v, | |
| Unit::kUnitless, ImprovementDirection::kBiggerIsBetter, | |
| metadata); | |
| } | |
| EncodingSettings VideoCodecTester::CreateEncodingSettings( | |
| const Environment& env, | |
| std::string codec_type, | |
| std::string scalability_name, | |
| int width, | |
| int height, | |
| std::vector<DataRate> bitrate, | |
| Frequency framerate, | |
| bool screencast, | |
| bool frame_drop) { | |
| VideoCodecMode content_type = screencast ? VideoCodecMode::kScreensharing | |
| : VideoCodecMode::kRealtimeVideo; | |
| auto [adjusted_bitrate, scalability_mode] = | |
| SplitBitrateAndUpdateScalabilityMode( | |
| env, codec_type, *ScalabilityModeFromString(scalability_name), width, | |
| height, bitrate, framerate, content_type); | |
| int num_spatial_layers = ScalabilityModeToNumSpatialLayers(scalability_mode); | |
| int num_temporal_layers = | |
| ScalabilityModeToNumTemporalLayers(scalability_mode); | |
| std::map<LayerId, LayerSettings> layers_settings; | |
| for (int sidx = 0; sidx < num_spatial_layers; ++sidx) { | |
| int layer_width = width >> (num_spatial_layers - sidx - 1); | |
| int layer_height = height >> (num_spatial_layers - sidx - 1); | |
| for (int tidx = 0; tidx < num_temporal_layers; ++tidx) { | |
| layers_settings.emplace( | |
| LayerId{.spatial_idx = sidx, .temporal_idx = tidx}, | |
| LayerSettings{ | |
| .resolution = {.width = layer_width, .height = layer_height}, | |
| .framerate = framerate / (1 << (num_temporal_layers - tidx - 1)), | |
| .bitrate = adjusted_bitrate[sidx * num_temporal_layers + tidx]}); | |
| } | |
| } | |
| SdpVideoFormat sdp_video_format = SdpVideoFormat(codec_type); | |
| if (codec_type == "H264") { | |
| const std::string packetization_mode = | |
| "1"; // H264PacketizationMode::SingleNalUnit | |
| sdp_video_format.parameters = | |
| CreateH264Format(H264Profile::kProfileConstrainedBaseline, | |
| H264Level::kLevel3_1, packetization_mode, | |
| /*add_scalability_modes=*/false) | |
| .parameters; | |
| } | |
| return EncodingSettings{.sdp_video_format = sdp_video_format, | |
| .scalability_mode = scalability_mode, | |
| .content_type = content_type, | |
| .frame_drop = frame_drop, | |
| .layers_settings = layers_settings}; | |
| } | |
| std::map<uint32_t, EncodingSettings> VideoCodecTester::CreateFrameSettings( | |
| const EncodingSettings& encoding_settings, | |
| int num_frames, | |
| uint32_t timestamp_rtp) { | |
| std::map<uint32_t, EncodingSettings> frame_settings; | |
| Frequency framerate = | |
| encoding_settings.layers_settings.rbegin()->second.framerate; | |
| for (int frame_num = 0; frame_num < num_frames; ++frame_num) { | |
| frame_settings.emplace(timestamp_rtp, encoding_settings); | |
| timestamp_rtp += k90kHz / framerate; | |
| } | |
| return frame_settings; | |
| } | |
| std::unique_ptr<VideoCodecTester::VideoCodecStats> | |
| VideoCodecTester::RunDecodeTest(const Environment& env, | |
| CodedVideoSource* video_source, | |
| VideoDecoderFactory* decoder_factory, | |
| const DecoderSettings& decoder_settings, | |
| const SdpVideoFormat& sdp_video_format) { | |
| std::unique_ptr<VideoCodecAnalyzer> analyzer = | |
| std::make_unique<VideoCodecAnalyzer>(&env.clock()); | |
| Decoder decoder(env, decoder_factory, decoder_settings, analyzer.get()); | |
| decoder.Initialize(sdp_video_format); | |
| while (auto frame = video_source->PullFrame()) { | |
| decoder.Decode(*frame); | |
| } | |
| decoder.Flush(); | |
| analyzer->Flush(); | |
| return std::move(analyzer); | |
| } | |
| std::unique_ptr<VideoCodecTester::VideoCodecStats> | |
| VideoCodecTester::RunEncodeTest( | |
| const Environment& env, | |
| const VideoSourceSettings& source_settings, | |
| VideoEncoderFactory* encoder_factory, | |
| const EncoderSettings& encoder_settings, | |
| const std::map<uint32_t, EncodingSettings>& encoding_settings) { | |
| VideoSource video_source(env, source_settings); | |
| std::unique_ptr<VideoCodecAnalyzer> analyzer = | |
| std::make_unique<VideoCodecAnalyzer>(&env.clock()); | |
| Encoder encoder(env, encoder_factory, encoder_settings, analyzer.get()); | |
| encoder.Initialize(encoding_settings.begin()->second); | |
| for (const auto& [timestamp_rtp, frame_settings] : encoding_settings) { | |
| const EncodingSettings::LayerSettings& top_layer = | |
| frame_settings.layers_settings.rbegin()->second; | |
| VideoFrame source_frame = video_source.PullFrame( | |
| timestamp_rtp, top_layer.resolution, top_layer.framerate); | |
| encoder.Encode(source_frame, frame_settings, | |
| [](const EncodedImage& encoded_frame) {}); | |
| } | |
| encoder.Flush(); | |
| analyzer->Flush(); | |
| return std::move(analyzer); | |
| } | |
| std::unique_ptr<VideoCodecTester::VideoCodecStats> | |
| VideoCodecTester::RunEncodeDecodeTest( | |
| const Environment& env, | |
| const VideoSourceSettings& source_settings, | |
| VideoEncoderFactory* encoder_factory, | |
| VideoDecoderFactory* decoder_factory, | |
| const EncoderSettings& encoder_settings, | |
| const DecoderSettings& decoder_settings, | |
| const std::map<uint32_t, EncodingSettings>& encoding_settings) { | |
| VideoSource video_source(env, source_settings); | |
| std::unique_ptr<VideoCodecAnalyzer> analyzer = | |
| std::make_unique<VideoCodecAnalyzer>(&env.clock()); | |
| const EncodingSettings& first_frame_settings = | |
| encoding_settings.begin()->second; | |
| Encoder encoder(env, encoder_factory, encoder_settings, analyzer.get()); | |
| encoder.Initialize(first_frame_settings); | |
| int num_spatial_layers = | |
| ScalabilityModeToNumSpatialLayers(first_frame_settings.scalability_mode); | |
| std::vector<std::unique_ptr<Decoder>> decoders; | |
| for (int sidx = 0; sidx < num_spatial_layers; ++sidx) { | |
| auto decoder = std::make_unique<Decoder>(env, decoder_factory, | |
| decoder_settings, analyzer.get()); | |
| decoder->Initialize(first_frame_settings.sdp_video_format); | |
| decoders.push_back(std::move(decoder)); | |
| } | |
| for (const auto& [timestamp_rtp, frame_settings] : encoding_settings) { | |
| const EncodingSettings::LayerSettings& top_layer = | |
| frame_settings.layers_settings.rbegin()->second; | |
| VideoFrame source_frame = video_source.PullFrame( | |
| timestamp_rtp, top_layer.resolution, top_layer.framerate); | |
| encoder.Encode( | |
| source_frame, frame_settings, | |
| [&decoders, source_frame](const EncodedImage& encoded_frame) { | |
| int sidx = encoded_frame.SpatialIndex().value_or( | |
| encoded_frame.SimulcastIndex().value_or(0)); | |
| decoders.at(sidx)->Decode(encoded_frame, source_frame); | |
| }); | |
| } | |
| encoder.Flush(); | |
| for (auto& decoder : decoders) { | |
| decoder->Flush(); | |
| } | |
| analyzer->Flush(); | |
| return std::move(analyzer); | |
| } | |
| } // namespace test | |
| } // namespace webrtc |