blob: 40bbfcac242f9ff0b47d77765d18ca1f6fcc2990 [file]
/*
* Copyright 2025 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 "video/corruption_detection/frame_instrumentation_generator_impl.h"
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <map>
#include <memory>
#include <optional>
#include <queue>
#include <utility>
#include <vector>
#include "absl/algorithm/container.h"
#include "api/environment/environment.h"
#include "api/video/corruption_detection/corruption_detection_filter_settings.h"
#include "api/video/corruption_detection/frame_instrumentation_data.h"
#include "api/video/encoded_image.h"
#include "api/video/video_codec_type.h"
#include "api/video/video_frame.h"
#include "api/video_codecs/scalability_mode.h"
#include "api/video_codecs/video_codec.h"
#include "modules/include/module_common_types_public.h"
#include "modules/video_coding/utility/qp_parser.h"
#include "rtc_base/checks.h"
#include "rtc_base/experiments/corruption_detection_frame_selector_settings.h"
#include "rtc_base/logging.h"
#include "rtc_base/synchronization/mutex.h"
#include "video/corruption_detection/frame_selector.h"
#include "video/corruption_detection/generic_mapping_functions.h"
#include "video/corruption_detection/halton_frame_sampler.h"
#include "video/corruption_detection/utils.h"
namespace webrtc {
namespace {
// Avoid holding on to frames that might have been dropped by encoder, as that
// can lead to frame buffer pools draining.
// TODO: bugs.webrtc.org/358039777 - Once we have a reliable signal for dropped
// and completed frames, update this logic with a smarter culling logic.
constexpr size_t kMaxPendingFrames = 2;
std::optional<CorruptionDetectionFilterSettings> GetCorruptionFilterSettings(
const EncodedImage& encoded_image,
VideoCodecType video_codec_type,
int layer_id) {
std::optional<CorruptionDetectionFilterSettings> filter_settings =
encoded_image.corruption_detection_filter_settings();
if (!filter_settings.has_value()) {
// No implementation specific filter settings available, using a generic
// QP-based settings instead.
int qp = encoded_image.qp_;
if (qp == -1) {
std::optional<uint32_t> parsed_qp =
QpParser().Parse(video_codec_type, layer_id, encoded_image.data(),
encoded_image.size());
if (!parsed_qp.has_value()) {
RTC_LOG(LS_VERBOSE)
<< "Missing QP for " << CodecTypeToPayloadString(video_codec_type)
<< " layer " << layer_id << ".";
return std::nullopt;
}
qp = *parsed_qp;
}
filter_settings = GetCorruptionFilterSettings(qp, video_codec_type);
}
return filter_settings;
}
std::unique_ptr<FrameSelector> CreateFrameSelector(
const Environment* environment,
VideoCodecType video_codec_type,
std::optional<ScalabilityMode> scalability_mode) {
if (!environment) {
return nullptr;
}
CorruptionDetectionFrameSelectorSettings settings(
environment->field_trials());
if (!settings.is_enabled()) {
return nullptr;
}
return std::make_unique<FrameSelector>(
*environment, scalability_mode.value_or(ScalabilityMode::kL1T1),
FrameSelector::Timespan{
.lower_bound = settings.low_overhead_lower_bound(),
.upper_bound = settings.low_overhead_upper_bound()},
FrameSelector::Timespan{
.lower_bound = settings.high_overhead_lower_bound(),
.upper_bound = settings.high_overhead_upper_bound()});
}
} // namespace
FrameInstrumentationGeneratorImpl::FrameInstrumentationGeneratorImpl(
const Environment* environment,
VideoCodecType video_codec_type,
std::optional<ScalabilityMode> scalability_mode)
: video_codec_type_(video_codec_type),
frame_selector_(CreateFrameSelector(environment,
video_codec_type,
scalability_mode)) {}
void FrameInstrumentationGeneratorImpl::OnCapturedFrame(VideoFrame frame) {
MutexLock lock(&mutex_);
while (captured_frames_.size() >= kMaxPendingFrames) {
captured_frames_.pop();
}
captured_frames_.push(frame);
}
std::optional<FrameInstrumentationData>
FrameInstrumentationGeneratorImpl::OnEncodedImage(
const EncodedImage& encoded_image) {
uint32_t rtp_timestamp_encoded_image = encoded_image.RtpTimestamp();
std::optional<VideoFrame> captured_frame;
int layer_id;
FrameInstrumentationData data;
std::vector<HaltonFrameSampler::Coordinates> sample_coordinates;
{
MutexLock lock(&mutex_);
while (!captured_frames_.empty() &&
IsNewerTimestamp(rtp_timestamp_encoded_image,
captured_frames_.front().rtp_timestamp())) {
captured_frames_.pop();
}
if (captured_frames_.empty() || captured_frames_.front().rtp_timestamp() !=
rtp_timestamp_encoded_image) {
RTC_LOG(LS_VERBOSE) << "No captured frames for RTC timestamp "
<< rtp_timestamp_encoded_image << ".";
return std::nullopt;
}
captured_frame = captured_frames_.front();
if (encoded_image.is_end_of_temporal_unit()) {
captured_frames_.pop();
}
layer_id = GetSpatialLayerId(encoded_image);
bool is_key_frame = encoded_image.IsKey();
if (!is_key_frame) {
for (const auto& [unused, context] : contexts_) {
if (context.rtp_timestamp_of_last_key_frame ==
rtp_timestamp_encoded_image) {
// Upper layer of an SVC key frame.
is_key_frame = true;
break;
}
}
}
if (is_key_frame) {
contexts_[layer_id].rtp_timestamp_of_last_key_frame =
encoded_image.RtpTimestamp();
} else if (contexts_.find(layer_id) == contexts_.end()) {
// TODO: bugs.webrtc.org/358039777 - Update this if statement such that
// LxTy scalability modes work properly. It is not a problem for LxTy_KEY
// scalability.
//
// For LxTy, it sometimes hinders calculating corruption score on the
// higher spatial layers. Because e.g. in L3T1 the first frame might not
// create 3 spatial layers but, only 2. Then, we end up not creating this
// in the map and will therefore not get any corruption score until a new
// key frame is sent.
RTC_LOG(LS_INFO) << "The first frame of a spatial or simulcast layer is "
"not a key frame.";
return std::nullopt;
}
int sequence_index = contexts_[layer_id].frame_sampler.GetCurrentIndex();
if (is_key_frame && ((sequence_index & 0b0111'1111) != 0)) {
// Increase until all the last 7 bits are zeroes.
sequence_index >>= 7;
sequence_index += 1;
sequence_index <<= 7;
contexts_[layer_id].frame_sampler.SetCurrentIndex(sequence_index);
}
if (sequence_index >= (1 << 14)) {
// Overflow of 14 bit counter, reset to 0.
sequence_index = 0;
contexts_[layer_id].frame_sampler.SetCurrentIndex(sequence_index);
}
RTC_CHECK(data.SetSequenceIndex(sequence_index));
// TODO: bugs.webrtc.org/358039777 - Maybe allow other sample sizes as well
if (frame_selector_) {
if (frame_selector_->ShouldInstrumentFrame(*captured_frame,
encoded_image)) {
sample_coordinates =
contexts_[layer_id].frame_sampler.GetSampleCoordinatesForFrame(
/*num_samples=*/13);
}
} else {
sample_coordinates =
contexts_[layer_id]
.frame_sampler.GetSampleCoordinatesForFrameIfFrameShouldBeSampled(
is_key_frame, captured_frame->rtp_timestamp(),
/*num_samples=*/13);
}
if (sample_coordinates.empty()) {
if (!is_key_frame) {
return std::nullopt;
}
// Sync message only.
return data;
}
}
RTC_DCHECK(captured_frame.has_value());
RTC_DCHECK(!sample_coordinates.empty());
std::optional<CorruptionDetectionFilterSettings> filter_settings =
GetCorruptionFilterSettings(encoded_image, video_codec_type_, layer_id);
if (!filter_settings.has_value()) {
return std::nullopt;
}
RTC_CHECK(data.SetStdDev(filter_settings->std_dev));
RTC_CHECK(data.SetLumaErrorThreshold(filter_settings->luma_error_threshold));
RTC_CHECK(
data.SetChromaErrorThreshold(filter_settings->chroma_error_threshold));
std::vector<double> plain_values;
std::vector<FilteredSample> samples = GetSampleValuesForFrame(
*captured_frame, sample_coordinates, encoded_image._encodedWidth,
encoded_image._encodedHeight, filter_settings->std_dev);
plain_values.reserve(samples.size());
absl::c_transform(samples, std::back_inserter(plain_values),
[](const FilteredSample& sample) { return sample.value; });
RTC_CHECK(data.SetSampleValues(std::move(plain_values)));
return data;
}
void FrameInstrumentationGeneratorImpl::OnFrameReleased(
uint32_t rtp_timestamp) {
MutexLock lock(&mutex_);
// Remove the frame and any preceding ones from the queue.
while (!captured_frames_.empty() &&
(IsNewerTimestamp(rtp_timestamp,
captured_frames_.front().rtp_timestamp()) ||
rtp_timestamp == captured_frames_.front().rtp_timestamp())) {
captured_frames_.pop();
}
}
std::optional<int> FrameInstrumentationGeneratorImpl::GetHaltonSequenceIndex(
int layer_id) const {
MutexLock lock(&mutex_);
auto it = contexts_.find(layer_id);
if (it == contexts_.end()) {
return std::nullopt;
}
return it->second.frame_sampler.GetCurrentIndex();
}
void FrameInstrumentationGeneratorImpl::SetHaltonSequenceIndex(int index,
int layer_id) {
MutexLock lock(&mutex_);
if (index <= 0x3FFF) {
contexts_[layer_id].frame_sampler.SetCurrentIndex(index);
}
RTC_DCHECK_LE(index, 0x3FFF) << "Index must not be larger than 0x3FFF";
}
} // namespace webrtc