Repo created
This commit is contained in:
parent
81b91f4139
commit
f8c34fa5ee
22732 changed files with 4815320 additions and 2 deletions
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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 "modules/video_coding/utility/bandwidth_quality_scaler.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "api/video/video_adaptation_reason.h"
|
||||
#include "api/video_codecs/video_encoder.h"
|
||||
#include "rtc_base/checks.h"
|
||||
#include "rtc_base/experiments/bandwidth_quality_scaler_settings.h"
|
||||
#include "rtc_base/logging.h"
|
||||
#include "rtc_base/numerics/exp_filter.h"
|
||||
#include "rtc_base/time_utils.h"
|
||||
#include "rtc_base/weak_ptr.h"
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kDefaultMaxWindowSizeMs = 5000;
|
||||
constexpr float kHigherMaxBitrateTolerationFactor = 0.95;
|
||||
constexpr float kLowerMinBitrateTolerationFactor = 0.8;
|
||||
constexpr int kDefaultBitrateStateUpdateIntervalSeconds = 5;
|
||||
} // namespace
|
||||
|
||||
BandwidthQualityScaler::BandwidthQualityScaler(
|
||||
BandwidthQualityScalerUsageHandlerInterface* handler)
|
||||
: kBitrateStateUpdateInterval(TimeDelta::Seconds(
|
||||
BandwidthQualityScalerSettings::ParseFromFieldTrials()
|
||||
.BitrateStateUpdateInterval()
|
||||
.value_or(kDefaultBitrateStateUpdateIntervalSeconds))),
|
||||
handler_(handler),
|
||||
encoded_bitrate_(kDefaultMaxWindowSizeMs, RateStatistics::kBpsScale),
|
||||
weak_ptr_factory_(this) {
|
||||
RTC_DCHECK_RUN_ON(&task_checker_);
|
||||
RTC_DCHECK(handler_ != nullptr);
|
||||
|
||||
StartCheckForBitrate();
|
||||
}
|
||||
|
||||
BandwidthQualityScaler::~BandwidthQualityScaler() {
|
||||
RTC_DCHECK_RUN_ON(&task_checker_);
|
||||
}
|
||||
|
||||
void BandwidthQualityScaler::StartCheckForBitrate() {
|
||||
RTC_DCHECK_RUN_ON(&task_checker_);
|
||||
TaskQueueBase::Current()->PostDelayedTask(
|
||||
[this_weak_ptr = weak_ptr_factory_.GetWeakPtr(), this] {
|
||||
if (!this_weak_ptr) {
|
||||
// The caller BandwidthQualityScaler has been deleted.
|
||||
return;
|
||||
}
|
||||
RTC_DCHECK_RUN_ON(&task_checker_);
|
||||
switch (CheckBitrate()) {
|
||||
case BandwidthQualityScaler::CheckBitrateResult::kHighBitRate: {
|
||||
handler_->OnReportUsageBandwidthHigh();
|
||||
last_frame_size_pixels_.reset();
|
||||
break;
|
||||
}
|
||||
case BandwidthQualityScaler::CheckBitrateResult::kLowBitRate: {
|
||||
handler_->OnReportUsageBandwidthLow();
|
||||
last_frame_size_pixels_.reset();
|
||||
break;
|
||||
}
|
||||
case BandwidthQualityScaler::CheckBitrateResult::kNormalBitrate: {
|
||||
break;
|
||||
}
|
||||
case BandwidthQualityScaler::CheckBitrateResult::
|
||||
kInsufficientSamples: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
StartCheckForBitrate();
|
||||
},
|
||||
kBitrateStateUpdateInterval);
|
||||
}
|
||||
|
||||
void BandwidthQualityScaler::ReportEncodeInfo(int frame_size_bytes,
|
||||
int64_t time_sent_in_ms,
|
||||
uint32_t encoded_width,
|
||||
uint32_t encoded_height) {
|
||||
RTC_DCHECK_RUN_ON(&task_checker_);
|
||||
last_time_sent_in_ms_ = time_sent_in_ms;
|
||||
last_frame_size_pixels_ = encoded_width * encoded_height;
|
||||
encoded_bitrate_.Update(frame_size_bytes, time_sent_in_ms);
|
||||
}
|
||||
|
||||
void BandwidthQualityScaler::SetResolutionBitrateLimits(
|
||||
const std::vector<VideoEncoder::ResolutionBitrateLimits>&
|
||||
resolution_bitrate_limits) {
|
||||
if (resolution_bitrate_limits.empty()) {
|
||||
resolution_bitrate_limits_ = EncoderInfoSettings::
|
||||
GetDefaultSinglecastBitrateLimitsWhenQpIsUntrusted();
|
||||
} else {
|
||||
resolution_bitrate_limits_ = resolution_bitrate_limits;
|
||||
}
|
||||
}
|
||||
|
||||
BandwidthQualityScaler::CheckBitrateResult
|
||||
BandwidthQualityScaler::CheckBitrate() {
|
||||
RTC_DCHECK_RUN_ON(&task_checker_);
|
||||
if (!last_frame_size_pixels_.has_value() ||
|
||||
!last_time_sent_in_ms_.has_value()) {
|
||||
return BandwidthQualityScaler::CheckBitrateResult::kInsufficientSamples;
|
||||
}
|
||||
|
||||
absl::optional<int64_t> current_bitrate_bps =
|
||||
encoded_bitrate_.Rate(last_time_sent_in_ms_.value());
|
||||
if (!current_bitrate_bps.has_value()) {
|
||||
// We can't get a valid bitrate due to not enough data points.
|
||||
return BandwidthQualityScaler::CheckBitrateResult::kInsufficientSamples;
|
||||
}
|
||||
absl::optional<VideoEncoder::ResolutionBitrateLimits> suitable_bitrate_limit =
|
||||
EncoderInfoSettings::
|
||||
GetSinglecastBitrateLimitForResolutionWhenQpIsUntrusted(
|
||||
last_frame_size_pixels_, resolution_bitrate_limits_);
|
||||
|
||||
if (!suitable_bitrate_limit.has_value()) {
|
||||
return BandwidthQualityScaler::CheckBitrateResult::kInsufficientSamples;
|
||||
}
|
||||
|
||||
// Multiply by toleration factor to solve the frequent adaptation due to
|
||||
// critical value.
|
||||
if (current_bitrate_bps > suitable_bitrate_limit->max_bitrate_bps *
|
||||
kHigherMaxBitrateTolerationFactor) {
|
||||
return BandwidthQualityScaler::CheckBitrateResult::kLowBitRate;
|
||||
} else if (current_bitrate_bps <
|
||||
suitable_bitrate_limit->min_start_bitrate_bps *
|
||||
kLowerMinBitrateTolerationFactor) {
|
||||
return BandwidthQualityScaler::CheckBitrateResult::kHighBitRate;
|
||||
}
|
||||
return BandwidthQualityScaler::CheckBitrateResult::kNormalBitrate;
|
||||
}
|
||||
|
||||
BandwidthQualityScalerUsageHandlerInterface::
|
||||
~BandwidthQualityScalerUsageHandlerInterface() {}
|
||||
} // namespace webrtc
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.
|
||||
*/
|
||||
|
||||
#ifndef MODULES_VIDEO_CODING_UTILITY_BANDWIDTH_QUALITY_SCALER_H_
|
||||
#define MODULES_VIDEO_CODING_UTILITY_BANDWIDTH_QUALITY_SCALER_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/types/optional.h"
|
||||
#include "api/scoped_refptr.h"
|
||||
#include "api/sequence_checker.h"
|
||||
#include "api/video_codecs/video_encoder.h"
|
||||
#include "rtc_base/experiments/encoder_info_settings.h"
|
||||
#include "rtc_base/logging.h"
|
||||
#include "rtc_base/numerics/exp_filter.h"
|
||||
#include "rtc_base/rate_statistics.h"
|
||||
#include "rtc_base/ref_count.h"
|
||||
#include "rtc_base/system/no_unique_address.h"
|
||||
#include "rtc_base/weak_ptr.h"
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
class BandwidthQualityScalerUsageHandlerInterface {
|
||||
public:
|
||||
virtual ~BandwidthQualityScalerUsageHandlerInterface();
|
||||
|
||||
virtual void OnReportUsageBandwidthHigh() = 0;
|
||||
virtual void OnReportUsageBandwidthLow() = 0;
|
||||
};
|
||||
|
||||
// BandwidthQualityScaler runs asynchronously and monitors bandwidth values of
|
||||
// encoded frames. It holds a reference to a
|
||||
// BandwidthQualityScalerUsageHandlerInterface implementation to signal an
|
||||
// overuse or underuse of bandwidth (which indicate a desire to scale the video
|
||||
// stream down or up).
|
||||
class BandwidthQualityScaler {
|
||||
public:
|
||||
explicit BandwidthQualityScaler(
|
||||
BandwidthQualityScalerUsageHandlerInterface* handler);
|
||||
virtual ~BandwidthQualityScaler();
|
||||
|
||||
void ReportEncodeInfo(int frame_size_bytes,
|
||||
int64_t time_sent_in_ms,
|
||||
uint32_t encoded_width,
|
||||
uint32_t encoded_height);
|
||||
|
||||
// We prioritise to using the |resolution_bitrate_limits| provided by the
|
||||
// current decoder. If not provided, we will use the default data by
|
||||
// GetDefaultResolutionBitrateLimits().
|
||||
void SetResolutionBitrateLimits(
|
||||
const std::vector<VideoEncoder::ResolutionBitrateLimits>&
|
||||
resolution_bitrate_limits);
|
||||
|
||||
const TimeDelta kBitrateStateUpdateInterval;
|
||||
|
||||
private:
|
||||
enum class CheckBitrateResult {
|
||||
kInsufficientSamples,
|
||||
kNormalBitrate,
|
||||
kHighBitRate,
|
||||
kLowBitRate,
|
||||
};
|
||||
|
||||
// We will periodically check encode bitrate, this function will make
|
||||
// resolution up or down decisions and report the decision to the adapter.
|
||||
void StartCheckForBitrate();
|
||||
CheckBitrateResult CheckBitrate();
|
||||
|
||||
RTC_NO_UNIQUE_ADDRESS SequenceChecker task_checker_;
|
||||
BandwidthQualityScalerUsageHandlerInterface* const handler_
|
||||
RTC_GUARDED_BY(&task_checker_);
|
||||
|
||||
absl::optional<int64_t> last_time_sent_in_ms_ RTC_GUARDED_BY(&task_checker_);
|
||||
RateStatistics encoded_bitrate_ RTC_GUARDED_BY(&task_checker_);
|
||||
absl::optional<int> last_frame_size_pixels_ RTC_GUARDED_BY(&task_checker_);
|
||||
rtc::WeakPtrFactory<BandwidthQualityScaler> weak_ptr_factory_;
|
||||
|
||||
std::vector<VideoEncoder::ResolutionBitrateLimits> resolution_bitrate_limits_;
|
||||
};
|
||||
|
||||
} // namespace webrtc
|
||||
#endif // MODULES_VIDEO_CODING_UTILITY_BANDWIDTH_QUALITY_SCALER_H_
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright (c) 2019 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 "modules/video_coding/utility/decoded_frames_history.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "rtc_base/checks.h"
|
||||
#include "rtc_base/logging.h"
|
||||
|
||||
namespace webrtc {
|
||||
namespace video_coding {
|
||||
|
||||
DecodedFramesHistory::DecodedFramesHistory(size_t window_size)
|
||||
: buffer_(window_size) {}
|
||||
|
||||
DecodedFramesHistory::~DecodedFramesHistory() = default;
|
||||
|
||||
void DecodedFramesHistory::InsertDecoded(int64_t frame_id, uint32_t timestamp) {
|
||||
last_decoded_frame_ = frame_id;
|
||||
last_decoded_frame_timestamp_ = timestamp;
|
||||
int new_index = FrameIdToIndex(frame_id);
|
||||
|
||||
RTC_DCHECK(last_frame_id_ < frame_id);
|
||||
|
||||
// Clears expired values from the cyclic buffer_.
|
||||
if (last_frame_id_) {
|
||||
int64_t id_jump = frame_id - *last_frame_id_;
|
||||
int last_index = FrameIdToIndex(*last_frame_id_);
|
||||
|
||||
if (id_jump >= static_cast<int64_t>(buffer_.size())) {
|
||||
std::fill(buffer_.begin(), buffer_.end(), false);
|
||||
} else if (new_index > last_index) {
|
||||
std::fill(buffer_.begin() + last_index + 1, buffer_.begin() + new_index,
|
||||
false);
|
||||
} else {
|
||||
std::fill(buffer_.begin() + last_index + 1, buffer_.end(), false);
|
||||
std::fill(buffer_.begin(), buffer_.begin() + new_index, false);
|
||||
}
|
||||
}
|
||||
|
||||
buffer_[new_index] = true;
|
||||
last_frame_id_ = frame_id;
|
||||
}
|
||||
|
||||
bool DecodedFramesHistory::WasDecoded(int64_t frame_id) const {
|
||||
if (!last_frame_id_)
|
||||
return false;
|
||||
|
||||
// Reference to the picture_id out of the stored should happen.
|
||||
if (frame_id <= *last_frame_id_ - static_cast<int64_t>(buffer_.size())) {
|
||||
RTC_LOG(LS_WARNING) << "Referencing a frame out of the window. "
|
||||
"Assuming it was undecoded to avoid artifacts.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (frame_id > last_frame_id_)
|
||||
return false;
|
||||
|
||||
return buffer_[FrameIdToIndex(frame_id)];
|
||||
}
|
||||
|
||||
void DecodedFramesHistory::Clear() {
|
||||
last_decoded_frame_timestamp_.reset();
|
||||
last_decoded_frame_.reset();
|
||||
std::fill(buffer_.begin(), buffer_.end(), false);
|
||||
last_frame_id_.reset();
|
||||
}
|
||||
|
||||
absl::optional<int64_t> DecodedFramesHistory::GetLastDecodedFrameId() const {
|
||||
return last_decoded_frame_;
|
||||
}
|
||||
|
||||
absl::optional<uint32_t> DecodedFramesHistory::GetLastDecodedFrameTimestamp()
|
||||
const {
|
||||
return last_decoded_frame_timestamp_;
|
||||
}
|
||||
|
||||
int DecodedFramesHistory::FrameIdToIndex(int64_t frame_id) const {
|
||||
int m = frame_id % buffer_.size();
|
||||
return m >= 0 ? m : m + buffer_.size();
|
||||
}
|
||||
|
||||
} // namespace video_coding
|
||||
} // namespace webrtc
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (c) 2019 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.
|
||||
*/
|
||||
|
||||
#ifndef MODULES_VIDEO_CODING_UTILITY_DECODED_FRAMES_HISTORY_H_
|
||||
#define MODULES_VIDEO_CODING_UTILITY_DECODED_FRAMES_HISTORY_H_
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <bitset>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/types/optional.h"
|
||||
#include "api/video/encoded_frame.h"
|
||||
|
||||
namespace webrtc {
|
||||
namespace video_coding {
|
||||
|
||||
class DecodedFramesHistory {
|
||||
public:
|
||||
// window_size - how much frames back to the past are actually remembered.
|
||||
explicit DecodedFramesHistory(size_t window_size);
|
||||
~DecodedFramesHistory();
|
||||
// Called for each decoded frame. Assumes frame id's are non-decreasing.
|
||||
void InsertDecoded(int64_t frame_id, uint32_t timestamp);
|
||||
// Query if the following (frame_id, spatial_id) pair was inserted before.
|
||||
// Should be at most less by window_size-1 than the last inserted frame id.
|
||||
bool WasDecoded(int64_t frame_id) const;
|
||||
|
||||
void Clear();
|
||||
|
||||
absl::optional<int64_t> GetLastDecodedFrameId() const;
|
||||
absl::optional<uint32_t> GetLastDecodedFrameTimestamp() const;
|
||||
|
||||
private:
|
||||
int FrameIdToIndex(int64_t frame_id) const;
|
||||
|
||||
std::vector<bool> buffer_;
|
||||
absl::optional<int64_t> last_frame_id_;
|
||||
absl::optional<int64_t> last_decoded_frame_;
|
||||
absl::optional<uint32_t> last_decoded_frame_timestamp_;
|
||||
};
|
||||
|
||||
} // namespace video_coding
|
||||
} // namespace webrtc
|
||||
#endif // MODULES_VIDEO_CODING_UTILITY_DECODED_FRAMES_HISTORY_H_
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
/*
|
||||
* Copyright (c) 2011 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 "modules/video_coding/utility/frame_dropper.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
namespace {
|
||||
|
||||
const float kDefaultFrameSizeAlpha = 0.9f;
|
||||
const float kDefaultKeyFrameRatioAlpha = 0.99f;
|
||||
// 1 key frame every 10th second in 30 fps.
|
||||
const float kDefaultKeyFrameRatioValue = 1 / 300.0f;
|
||||
|
||||
const float kDefaultDropRatioAlpha = 0.9f;
|
||||
const float kDefaultDropRatioValue = 0.96f;
|
||||
// Maximum duration over which frames are continuously dropped.
|
||||
const float kDefaultMaxDropDurationSecs = 4.0f;
|
||||
|
||||
// Default target bitrate.
|
||||
// TODO(isheriff): Should this be higher to avoid dropping too many packets when
|
||||
// the bandwidth is unknown at the start ?
|
||||
const float kDefaultTargetBitrateKbps = 300.0f;
|
||||
const float kDefaultIncomingFrameRate = 30;
|
||||
const float kLeakyBucketSizeSeconds = 0.5f;
|
||||
|
||||
// A delta frame that is bigger than `kLargeDeltaFactor` times the average
|
||||
// delta frame is a large frame that is spread out for accumulation.
|
||||
const int kLargeDeltaFactor = 3;
|
||||
|
||||
// Cap on the frame size accumulator to prevent excessive drops.
|
||||
const float kAccumulatorCapBufferSizeSecs = 3.0f;
|
||||
} // namespace
|
||||
|
||||
FrameDropper::FrameDropper()
|
||||
: key_frame_ratio_(kDefaultKeyFrameRatioAlpha),
|
||||
delta_frame_size_avg_kbits_(kDefaultFrameSizeAlpha),
|
||||
drop_ratio_(kDefaultDropRatioAlpha, kDefaultDropRatioValue),
|
||||
enabled_(true),
|
||||
max_drop_duration_secs_(kDefaultMaxDropDurationSecs) {
|
||||
Reset();
|
||||
}
|
||||
|
||||
FrameDropper::~FrameDropper() = default;
|
||||
|
||||
void FrameDropper::Reset() {
|
||||
key_frame_ratio_.Reset(kDefaultKeyFrameRatioAlpha);
|
||||
key_frame_ratio_.Apply(1.0f, kDefaultKeyFrameRatioValue);
|
||||
delta_frame_size_avg_kbits_.Reset(kDefaultFrameSizeAlpha);
|
||||
|
||||
accumulator_ = 0.0f;
|
||||
accumulator_max_ = kDefaultTargetBitrateKbps / 2;
|
||||
target_bitrate_ = kDefaultTargetBitrateKbps;
|
||||
incoming_frame_rate_ = kDefaultIncomingFrameRate;
|
||||
|
||||
large_frame_accumulation_count_ = 0;
|
||||
large_frame_accumulation_chunk_size_ = 0;
|
||||
large_frame_accumulation_spread_ = 0.5 * kDefaultIncomingFrameRate;
|
||||
|
||||
drop_next_ = false;
|
||||
drop_ratio_.Reset(0.9f);
|
||||
drop_ratio_.Apply(0.0f, 0.0f);
|
||||
drop_count_ = 0;
|
||||
was_below_max_ = true;
|
||||
}
|
||||
|
||||
void FrameDropper::Enable(bool enable) {
|
||||
enabled_ = enable;
|
||||
}
|
||||
|
||||
void FrameDropper::Fill(size_t framesize_bytes, bool delta_frame) {
|
||||
if (!enabled_) {
|
||||
return;
|
||||
}
|
||||
float framesize_kbits = 8.0f * static_cast<float>(framesize_bytes) / 1000.0f;
|
||||
if (!delta_frame) {
|
||||
key_frame_ratio_.Apply(1.0, 1.0);
|
||||
// Do not spread if we are already doing it (or we risk dropping bits that
|
||||
// need accumulation). Given we compute the key frame ratio and spread
|
||||
// based on that, this should not normally happen.
|
||||
if (large_frame_accumulation_count_ == 0) {
|
||||
if (key_frame_ratio_.filtered() > 1e-5 &&
|
||||
1 / key_frame_ratio_.filtered() < large_frame_accumulation_spread_) {
|
||||
large_frame_accumulation_count_ =
|
||||
static_cast<int32_t>(1 / key_frame_ratio_.filtered() + 0.5);
|
||||
} else {
|
||||
large_frame_accumulation_count_ =
|
||||
static_cast<int32_t>(large_frame_accumulation_spread_ + 0.5);
|
||||
}
|
||||
large_frame_accumulation_chunk_size_ =
|
||||
framesize_kbits / large_frame_accumulation_count_;
|
||||
framesize_kbits = 0;
|
||||
}
|
||||
} else {
|
||||
// Identify if it is an unusually large delta frame and spread accumulation
|
||||
// if that is the case.
|
||||
if (delta_frame_size_avg_kbits_.filtered() != -1 &&
|
||||
(framesize_kbits >
|
||||
kLargeDeltaFactor * delta_frame_size_avg_kbits_.filtered()) &&
|
||||
large_frame_accumulation_count_ == 0) {
|
||||
large_frame_accumulation_count_ =
|
||||
static_cast<int32_t>(large_frame_accumulation_spread_ + 0.5);
|
||||
large_frame_accumulation_chunk_size_ =
|
||||
framesize_kbits / large_frame_accumulation_count_;
|
||||
framesize_kbits = 0;
|
||||
} else {
|
||||
delta_frame_size_avg_kbits_.Apply(1, framesize_kbits);
|
||||
}
|
||||
key_frame_ratio_.Apply(1.0, 0.0);
|
||||
}
|
||||
// Change the level of the accumulator (bucket)
|
||||
accumulator_ += framesize_kbits;
|
||||
CapAccumulator();
|
||||
}
|
||||
|
||||
void FrameDropper::Leak(uint32_t input_framerate) {
|
||||
if (!enabled_) {
|
||||
return;
|
||||
}
|
||||
if (input_framerate < 1) {
|
||||
return;
|
||||
}
|
||||
if (target_bitrate_ < 0.0f) {
|
||||
return;
|
||||
}
|
||||
// Add lower bound for large frame accumulation spread.
|
||||
large_frame_accumulation_spread_ = std::max(0.5 * input_framerate, 5.0);
|
||||
// Expected bits per frame based on current input frame rate.
|
||||
float expected_bits_per_frame = target_bitrate_ / input_framerate;
|
||||
if (large_frame_accumulation_count_ > 0) {
|
||||
expected_bits_per_frame -= large_frame_accumulation_chunk_size_;
|
||||
--large_frame_accumulation_count_;
|
||||
}
|
||||
accumulator_ -= expected_bits_per_frame;
|
||||
if (accumulator_ < 0.0f) {
|
||||
accumulator_ = 0.0f;
|
||||
}
|
||||
UpdateRatio();
|
||||
}
|
||||
|
||||
void FrameDropper::UpdateRatio() {
|
||||
if (accumulator_ > 1.3f * accumulator_max_) {
|
||||
// Too far above accumulator max, react faster.
|
||||
drop_ratio_.UpdateBase(0.8f);
|
||||
} else {
|
||||
// Go back to normal reaction.
|
||||
drop_ratio_.UpdateBase(0.9f);
|
||||
}
|
||||
if (accumulator_ > accumulator_max_) {
|
||||
// We are above accumulator max, and should ideally drop a frame. Increase
|
||||
// the drop_ratio_ and drop the frame later.
|
||||
if (was_below_max_) {
|
||||
drop_next_ = true;
|
||||
}
|
||||
drop_ratio_.Apply(1.0f, 1.0f);
|
||||
drop_ratio_.UpdateBase(0.9f);
|
||||
} else {
|
||||
drop_ratio_.Apply(1.0f, 0.0f);
|
||||
}
|
||||
was_below_max_ = accumulator_ < accumulator_max_;
|
||||
}
|
||||
|
||||
// This function signals when to drop frames to the caller. It makes use of the
|
||||
// drop_ratio_ to smooth out the drops over time.
|
||||
bool FrameDropper::DropFrame() {
|
||||
if (!enabled_) {
|
||||
return false;
|
||||
}
|
||||
if (drop_next_) {
|
||||
drop_next_ = false;
|
||||
drop_count_ = 0;
|
||||
}
|
||||
|
||||
if (drop_ratio_.filtered() >= 0.5f) { // Drops per keep
|
||||
// Limit is the number of frames we should drop between each kept frame
|
||||
// to keep our drop ratio. limit is positive in this case.
|
||||
float denom = 1.0f - drop_ratio_.filtered();
|
||||
if (denom < 1e-5) {
|
||||
denom = 1e-5f;
|
||||
}
|
||||
int32_t limit = static_cast<int32_t>(1.0f / denom - 1.0f + 0.5f);
|
||||
// Put a bound on the max amount of dropped frames between each kept
|
||||
// frame, in terms of frame rate and window size (secs).
|
||||
int max_limit =
|
||||
static_cast<int>(incoming_frame_rate_ * max_drop_duration_secs_);
|
||||
if (limit > max_limit) {
|
||||
limit = max_limit;
|
||||
}
|
||||
if (drop_count_ < 0) {
|
||||
// Reset the drop_count_ since it was negative and should be positive.
|
||||
drop_count_ = -drop_count_;
|
||||
}
|
||||
if (drop_count_ < limit) {
|
||||
// As long we are below the limit we should drop frames.
|
||||
drop_count_++;
|
||||
return true;
|
||||
} else {
|
||||
// Only when we reset drop_count_ a frame should be kept.
|
||||
drop_count_ = 0;
|
||||
return false;
|
||||
}
|
||||
} else if (drop_ratio_.filtered() > 0.0f &&
|
||||
drop_ratio_.filtered() < 0.5f) { // Keeps per drop
|
||||
// Limit is the number of frames we should keep between each drop
|
||||
// in order to keep the drop ratio. limit is negative in this case,
|
||||
// and the drop_count_ is also negative.
|
||||
float denom = drop_ratio_.filtered();
|
||||
if (denom < 1e-5) {
|
||||
denom = 1e-5f;
|
||||
}
|
||||
int32_t limit = -static_cast<int32_t>(1.0f / denom - 1.0f + 0.5f);
|
||||
if (drop_count_ > 0) {
|
||||
// Reset the drop_count_ since we have a positive
|
||||
// drop_count_, and it should be negative.
|
||||
drop_count_ = -drop_count_;
|
||||
}
|
||||
if (drop_count_ > limit) {
|
||||
if (drop_count_ == 0) {
|
||||
// Drop frames when we reset drop_count_.
|
||||
drop_count_--;
|
||||
return true;
|
||||
} else {
|
||||
// Keep frames as long as we haven't reached limit.
|
||||
drop_count_--;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
drop_count_ = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
drop_count_ = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
void FrameDropper::SetRates(float bitrate, float incoming_frame_rate) {
|
||||
// Bit rate of -1 means infinite bandwidth.
|
||||
accumulator_max_ = bitrate * kLeakyBucketSizeSeconds;
|
||||
if (target_bitrate_ > 0.0f && bitrate < target_bitrate_ &&
|
||||
accumulator_ > accumulator_max_) {
|
||||
// Rescale the accumulator level if the accumulator max decreases
|
||||
accumulator_ = bitrate / target_bitrate_ * accumulator_;
|
||||
}
|
||||
target_bitrate_ = bitrate;
|
||||
CapAccumulator();
|
||||
incoming_frame_rate_ = incoming_frame_rate;
|
||||
}
|
||||
|
||||
// Put a cap on the accumulator, i.e., don't let it grow beyond some level.
|
||||
// This is a temporary fix for screencasting where very large frames from
|
||||
// encoder will cause very slow response (too many frame drops).
|
||||
// TODO(isheriff): Remove this now that large delta frames are also spread out ?
|
||||
void FrameDropper::CapAccumulator() {
|
||||
float max_accumulator = target_bitrate_ * kAccumulatorCapBufferSizeSecs;
|
||||
if (accumulator_ > max_accumulator) {
|
||||
accumulator_ = max_accumulator;
|
||||
}
|
||||
}
|
||||
} // namespace webrtc
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright (c) 2011 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.
|
||||
*/
|
||||
|
||||
#ifndef MODULES_VIDEO_CODING_UTILITY_FRAME_DROPPER_H_
|
||||
#define MODULES_VIDEO_CODING_UTILITY_FRAME_DROPPER_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "rtc_base/numerics/exp_filter.h"
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
// The Frame Dropper implements a variant of the leaky bucket algorithm
|
||||
// for keeping track of when to drop frames to avoid bit rate
|
||||
// over use when the encoder can't keep its bit rate.
|
||||
class FrameDropper {
|
||||
public:
|
||||
FrameDropper();
|
||||
~FrameDropper();
|
||||
|
||||
// Resets the FrameDropper to its initial state.
|
||||
void Reset();
|
||||
|
||||
void Enable(bool enable);
|
||||
|
||||
// Answers the question if it's time to drop a frame if we want to reach a
|
||||
// given frame rate. Must be called for every frame.
|
||||
//
|
||||
// Return value : True if we should drop the current frame.
|
||||
bool DropFrame();
|
||||
|
||||
// Updates the FrameDropper with the size of the latest encoded frame.
|
||||
// The FrameDropper calculates a new drop ratio (can be seen as the
|
||||
// probability to drop a frame) and updates its internal statistics.
|
||||
//
|
||||
// Input:
|
||||
// - framesize_bytes : The size of the latest frame returned
|
||||
// from the encoder.
|
||||
// - delta_frame : True if the encoder returned a delta frame.
|
||||
void Fill(size_t framesize_bytes, bool delta_frame);
|
||||
|
||||
void Leak(uint32_t input_framerate);
|
||||
|
||||
// Sets the target bit rate and the frame rate produced by the camera.
|
||||
//
|
||||
// Input:
|
||||
// - bitrate : The target bit rate.
|
||||
void SetRates(float bitrate, float incoming_frame_rate);
|
||||
|
||||
private:
|
||||
void UpdateRatio();
|
||||
void CapAccumulator();
|
||||
|
||||
rtc::ExpFilter key_frame_ratio_;
|
||||
rtc::ExpFilter delta_frame_size_avg_kbits_;
|
||||
|
||||
// Key frames and large delta frames are not immediately accumulated in the
|
||||
// bucket since they can immediately overflow the bucket leading to large
|
||||
// drops on the following packets that may be much smaller. Instead these
|
||||
// large frames are accumulated over several frames when the bucket leaks.
|
||||
|
||||
// `large_frame_accumulation_spread_` represents the number of frames over
|
||||
// which a large frame is accumulated.
|
||||
float large_frame_accumulation_spread_;
|
||||
// `large_frame_accumulation_count_` represents the number of frames left
|
||||
// to finish accumulating a large frame.
|
||||
int large_frame_accumulation_count_;
|
||||
// `large_frame_accumulation_chunk_size_` represents the size of a single
|
||||
// chunk for large frame accumulation.
|
||||
float large_frame_accumulation_chunk_size_;
|
||||
|
||||
float accumulator_;
|
||||
float accumulator_max_;
|
||||
float target_bitrate_;
|
||||
bool drop_next_;
|
||||
rtc::ExpFilter drop_ratio_;
|
||||
int drop_count_;
|
||||
float incoming_frame_rate_;
|
||||
bool was_below_max_;
|
||||
bool enabled_;
|
||||
const float max_drop_duration_secs_;
|
||||
};
|
||||
|
||||
} // namespace webrtc
|
||||
|
||||
#endif // MODULES_VIDEO_CODING_UTILITY_FRAME_DROPPER_H_
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright (c) 2018 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 "modules/video_coding/utility/framerate_controller_deprecated.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
FramerateControllerDeprecated::FramerateControllerDeprecated(
|
||||
float target_framerate_fps)
|
||||
: min_frame_interval_ms_(0), framerate_estimator_(1000.0, 1000.0) {
|
||||
SetTargetRate(target_framerate_fps);
|
||||
}
|
||||
|
||||
void FramerateControllerDeprecated::SetTargetRate(float target_framerate_fps) {
|
||||
if (target_framerate_fps_ != target_framerate_fps) {
|
||||
framerate_estimator_.Reset();
|
||||
if (last_timestamp_ms_) {
|
||||
framerate_estimator_.Update(1, *last_timestamp_ms_);
|
||||
}
|
||||
|
||||
const size_t target_frame_interval_ms = 1000 / target_framerate_fps;
|
||||
target_framerate_fps_ = target_framerate_fps;
|
||||
min_frame_interval_ms_ = 85 * target_frame_interval_ms / 100;
|
||||
}
|
||||
}
|
||||
|
||||
float FramerateControllerDeprecated::GetTargetRate() {
|
||||
return *target_framerate_fps_;
|
||||
}
|
||||
|
||||
void FramerateControllerDeprecated::Reset() {
|
||||
framerate_estimator_.Reset();
|
||||
last_timestamp_ms_.reset();
|
||||
}
|
||||
|
||||
bool FramerateControllerDeprecated::DropFrame(uint32_t timestamp_ms) const {
|
||||
if (timestamp_ms < last_timestamp_ms_) {
|
||||
// Timestamp jumps backward. We can't make adequate drop decision. Don't
|
||||
// drop this frame. Stats will be reset in AddFrame().
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Rate(timestamp_ms).value_or(*target_framerate_fps_) >
|
||||
target_framerate_fps_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (last_timestamp_ms_) {
|
||||
const int64_t diff_ms =
|
||||
static_cast<int64_t>(timestamp_ms) - *last_timestamp_ms_;
|
||||
if (diff_ms < min_frame_interval_ms_) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void FramerateControllerDeprecated::AddFrame(uint32_t timestamp_ms) {
|
||||
if (timestamp_ms < last_timestamp_ms_) {
|
||||
// Timestamp jumps backward.
|
||||
Reset();
|
||||
}
|
||||
|
||||
framerate_estimator_.Update(1, timestamp_ms);
|
||||
last_timestamp_ms_ = timestamp_ms;
|
||||
}
|
||||
|
||||
absl::optional<float> FramerateControllerDeprecated::Rate(
|
||||
uint32_t timestamp_ms) const {
|
||||
return framerate_estimator_.Rate(timestamp_ms);
|
||||
}
|
||||
|
||||
} // namespace webrtc
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2018 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.
|
||||
*/
|
||||
|
||||
#ifndef MODULES_VIDEO_CODING_UTILITY_FRAMERATE_CONTROLLER_DEPRECATED_H_
|
||||
#define MODULES_VIDEO_CODING_UTILITY_FRAMERATE_CONTROLLER_DEPRECATED_H_
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "absl/types/optional.h"
|
||||
#include "rtc_base/rate_statistics.h"
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
// Please use webrtc::FramerateController instead.
|
||||
class FramerateControllerDeprecated {
|
||||
public:
|
||||
explicit FramerateControllerDeprecated(float target_framerate_fps);
|
||||
|
||||
void SetTargetRate(float target_framerate_fps);
|
||||
float GetTargetRate();
|
||||
|
||||
// Advices user to drop next frame in order to reach target framerate.
|
||||
bool DropFrame(uint32_t timestamp_ms) const;
|
||||
|
||||
void AddFrame(uint32_t timestamp_ms);
|
||||
|
||||
void Reset();
|
||||
|
||||
private:
|
||||
absl::optional<float> Rate(uint32_t timestamp_ms) const;
|
||||
|
||||
absl::optional<float> target_framerate_fps_;
|
||||
absl::optional<uint32_t> last_timestamp_ms_;
|
||||
uint32_t min_frame_interval_ms_;
|
||||
RateStatistics framerate_estimator_;
|
||||
};
|
||||
|
||||
} // namespace webrtc
|
||||
|
||||
#endif // MODULES_VIDEO_CODING_UTILITY_FRAMERATE_CONTROLLER_DEPRECATED_H_
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file contains definitions that are common to the IvfFileReader and
|
||||
* IvfFileWriter classes.
|
||||
*/
|
||||
|
||||
#ifndef MODULES_VIDEO_CODING_UTILITY_IVF_DEFINES_H_
|
||||
#define MODULES_VIDEO_CODING_UTILITY_IVF_DEFINES_H_
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
namespace webrtc {
|
||||
constexpr size_t kIvfHeaderSize = 32;
|
||||
} // namespace webrtc
|
||||
|
||||
#endif // MODULES_VIDEO_CODING_UTILITY_IVF_DEFINES_H_
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
* Copyright (c) 2019 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 "modules/video_coding/utility/ivf_file_reader.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "api/video_codecs/video_codec.h"
|
||||
#include "modules/rtp_rtcp/source/byte_io.h"
|
||||
#include "modules/video_coding/utility/ivf_defines.h"
|
||||
#include "rtc_base/logging.h"
|
||||
|
||||
namespace webrtc {
|
||||
namespace {
|
||||
|
||||
constexpr size_t kIvfFrameHeaderSize = 12;
|
||||
constexpr int kCodecTypeBytesCount = 4;
|
||||
|
||||
constexpr uint8_t kFileHeaderStart[kCodecTypeBytesCount] = {'D', 'K', 'I', 'F'};
|
||||
constexpr uint8_t kVp8Header[kCodecTypeBytesCount] = {'V', 'P', '8', '0'};
|
||||
constexpr uint8_t kVp9Header[kCodecTypeBytesCount] = {'V', 'P', '9', '0'};
|
||||
constexpr uint8_t kAv1Header[kCodecTypeBytesCount] = {'A', 'V', '0', '1'};
|
||||
constexpr uint8_t kH264Header[kCodecTypeBytesCount] = {'H', '2', '6', '4'};
|
||||
constexpr uint8_t kH265Header[kCodecTypeBytesCount] = {'H', '2', '6', '5'};
|
||||
|
||||
// RTP standard required 90kHz clock rate.
|
||||
constexpr int32_t kRtpClockRateHz = 90000;
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<IvfFileReader> IvfFileReader::Create(FileWrapper file) {
|
||||
auto reader =
|
||||
std::unique_ptr<IvfFileReader>(new IvfFileReader(std::move(file)));
|
||||
if (!reader->Reset()) {
|
||||
return nullptr;
|
||||
}
|
||||
return reader;
|
||||
}
|
||||
IvfFileReader::~IvfFileReader() {
|
||||
Close();
|
||||
}
|
||||
|
||||
bool IvfFileReader::Reset() {
|
||||
// Set error to true while initialization.
|
||||
has_error_ = true;
|
||||
if (!file_.Rewind()) {
|
||||
RTC_LOG(LS_ERROR) << "Failed to rewind IVF file";
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t ivf_header[kIvfHeaderSize] = {0};
|
||||
size_t read = file_.Read(&ivf_header, kIvfHeaderSize);
|
||||
if (read != kIvfHeaderSize) {
|
||||
RTC_LOG(LS_ERROR) << "Failed to read IVF header";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (memcmp(&ivf_header[0], kFileHeaderStart, 4) != 0) {
|
||||
RTC_LOG(LS_ERROR) << "File is not in IVF format: DKIF header expected";
|
||||
return false;
|
||||
}
|
||||
|
||||
absl::optional<VideoCodecType> codec_type = ParseCodecType(ivf_header, 8);
|
||||
if (!codec_type) {
|
||||
return false;
|
||||
}
|
||||
codec_type_ = *codec_type;
|
||||
|
||||
width_ = ByteReader<uint16_t>::ReadLittleEndian(&ivf_header[12]);
|
||||
height_ = ByteReader<uint16_t>::ReadLittleEndian(&ivf_header[14]);
|
||||
if (width_ == 0 || height_ == 0) {
|
||||
RTC_LOG(LS_ERROR) << "Invalid IVF header: width or height is 0";
|
||||
return false;
|
||||
}
|
||||
|
||||
time_scale_ = ByteReader<uint32_t>::ReadLittleEndian(&ivf_header[16]);
|
||||
if (time_scale_ == 0) {
|
||||
RTC_LOG(LS_ERROR) << "Invalid IVF header: time scale can't be 0";
|
||||
return false;
|
||||
}
|
||||
|
||||
num_frames_ = static_cast<size_t>(
|
||||
ByteReader<uint32_t>::ReadLittleEndian(&ivf_header[24]));
|
||||
if (num_frames_ <= 0) {
|
||||
RTC_LOG(LS_ERROR) << "Invalid IVF header: number of frames 0 or negative";
|
||||
return false;
|
||||
}
|
||||
|
||||
num_read_frames_ = 0;
|
||||
next_frame_header_ = ReadNextFrameHeader();
|
||||
if (!next_frame_header_) {
|
||||
RTC_LOG(LS_ERROR) << "Failed to read 1st frame header";
|
||||
return false;
|
||||
}
|
||||
// Initialization succeed: reset error.
|
||||
has_error_ = false;
|
||||
|
||||
const char* codec_name = CodecTypeToPayloadString(codec_type_);
|
||||
RTC_LOG(LS_INFO) << "Opened IVF file with codec data of type " << codec_name
|
||||
<< " at resolution " << width_ << " x " << height_
|
||||
<< ", using " << time_scale_ << "Hz clock resolution.";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
absl::optional<EncodedImage> IvfFileReader::NextFrame() {
|
||||
if (has_error_ || !HasMoreFrames()) {
|
||||
return absl::nullopt;
|
||||
}
|
||||
|
||||
rtc::scoped_refptr<EncodedImageBuffer> payload = EncodedImageBuffer::Create();
|
||||
std::vector<size_t> layer_sizes;
|
||||
// next_frame_header_ have to be presented by the way how it was loaded. If it
|
||||
// is missing it means there is a bug in error handling.
|
||||
RTC_DCHECK(next_frame_header_);
|
||||
int64_t current_timestamp = next_frame_header_->timestamp;
|
||||
// The first frame from the file should be marked as Key frame.
|
||||
bool is_first_frame = num_read_frames_ == 0;
|
||||
while (next_frame_header_ &&
|
||||
current_timestamp == next_frame_header_->timestamp) {
|
||||
// Resize payload to fit next spatial layer.
|
||||
size_t current_layer_size = next_frame_header_->frame_size;
|
||||
size_t current_layer_start_pos = payload->size();
|
||||
payload->Realloc(payload->size() + current_layer_size);
|
||||
layer_sizes.push_back(current_layer_size);
|
||||
|
||||
// Read next layer into payload
|
||||
size_t read = file_.Read(&payload->data()[current_layer_start_pos],
|
||||
current_layer_size);
|
||||
if (read != current_layer_size) {
|
||||
RTC_LOG(LS_ERROR) << "Frame #" << num_read_frames_
|
||||
<< ": failed to read frame payload";
|
||||
has_error_ = true;
|
||||
return absl::nullopt;
|
||||
}
|
||||
num_read_frames_++;
|
||||
|
||||
current_timestamp = next_frame_header_->timestamp;
|
||||
next_frame_header_ = ReadNextFrameHeader();
|
||||
}
|
||||
if (!next_frame_header_) {
|
||||
// If EOF was reached, we need to check that all frames were met.
|
||||
if (!has_error_ && num_read_frames_ != num_frames_) {
|
||||
RTC_LOG(LS_ERROR) << "Unexpected EOF";
|
||||
has_error_ = true;
|
||||
return absl::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
EncodedImage image;
|
||||
image.capture_time_ms_ = current_timestamp;
|
||||
image.SetRtpTimestamp(
|
||||
static_cast<uint32_t>(current_timestamp * kRtpClockRateHz / time_scale_));
|
||||
image.SetEncodedData(payload);
|
||||
image.SetSpatialIndex(static_cast<int>(layer_sizes.size()) - 1);
|
||||
for (size_t i = 0; i < layer_sizes.size(); ++i) {
|
||||
image.SetSpatialLayerFrameSize(static_cast<int>(i), layer_sizes[i]);
|
||||
}
|
||||
if (is_first_frame) {
|
||||
image._frameType = VideoFrameType::kVideoFrameKey;
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
bool IvfFileReader::Close() {
|
||||
if (!file_.is_open())
|
||||
return false;
|
||||
|
||||
file_.Close();
|
||||
return true;
|
||||
}
|
||||
|
||||
absl::optional<VideoCodecType> IvfFileReader::ParseCodecType(uint8_t* buffer,
|
||||
size_t start_pos) {
|
||||
if (memcmp(&buffer[start_pos], kVp8Header, kCodecTypeBytesCount) == 0) {
|
||||
return VideoCodecType::kVideoCodecVP8;
|
||||
}
|
||||
if (memcmp(&buffer[start_pos], kVp9Header, kCodecTypeBytesCount) == 0) {
|
||||
return VideoCodecType::kVideoCodecVP9;
|
||||
}
|
||||
if (memcmp(&buffer[start_pos], kAv1Header, kCodecTypeBytesCount) == 0) {
|
||||
return VideoCodecType::kVideoCodecAV1;
|
||||
}
|
||||
if (memcmp(&buffer[start_pos], kH264Header, kCodecTypeBytesCount) == 0) {
|
||||
return VideoCodecType::kVideoCodecH264;
|
||||
}
|
||||
if (memcmp(&buffer[start_pos], kH265Header, kCodecTypeBytesCount) == 0) {
|
||||
return VideoCodecType::kVideoCodecH265;
|
||||
}
|
||||
has_error_ = true;
|
||||
RTC_LOG(LS_ERROR) << "Unknown codec type: "
|
||||
<< std::string(
|
||||
reinterpret_cast<char const*>(&buffer[start_pos]),
|
||||
kCodecTypeBytesCount);
|
||||
return absl::nullopt;
|
||||
}
|
||||
|
||||
absl::optional<IvfFileReader::FrameHeader>
|
||||
IvfFileReader::ReadNextFrameHeader() {
|
||||
uint8_t ivf_frame_header[kIvfFrameHeaderSize] = {0};
|
||||
size_t read = file_.Read(&ivf_frame_header, kIvfFrameHeaderSize);
|
||||
if (read != kIvfFrameHeaderSize) {
|
||||
if (read != 0 || !file_.ReadEof()) {
|
||||
has_error_ = true;
|
||||
RTC_LOG(LS_ERROR) << "Frame #" << num_read_frames_
|
||||
<< ": failed to read IVF frame header";
|
||||
}
|
||||
return absl::nullopt;
|
||||
}
|
||||
FrameHeader header;
|
||||
header.frame_size = static_cast<size_t>(
|
||||
ByteReader<uint32_t>::ReadLittleEndian(&ivf_frame_header[0]));
|
||||
header.timestamp =
|
||||
ByteReader<uint64_t>::ReadLittleEndian(&ivf_frame_header[4]);
|
||||
|
||||
if (header.frame_size == 0) {
|
||||
has_error_ = true;
|
||||
RTC_LOG(LS_ERROR) << "Frame #" << num_read_frames_
|
||||
<< ": invalid frame size";
|
||||
return absl::nullopt;
|
||||
}
|
||||
|
||||
if (header.timestamp < 0) {
|
||||
has_error_ = true;
|
||||
RTC_LOG(LS_ERROR) << "Frame #" << num_read_frames_
|
||||
<< ": negative timestamp";
|
||||
return absl::nullopt;
|
||||
}
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
} // namespace webrtc
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright (c) 2019 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.
|
||||
*/
|
||||
|
||||
#ifndef MODULES_VIDEO_CODING_UTILITY_IVF_FILE_READER_H_
|
||||
#define MODULES_VIDEO_CODING_UTILITY_IVF_FILE_READER_H_
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "absl/types/optional.h"
|
||||
#include "api/video/encoded_image.h"
|
||||
#include "api/video_codecs/video_codec.h"
|
||||
#include "rtc_base/system/file_wrapper.h"
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
class IvfFileReader {
|
||||
public:
|
||||
// Creates IvfFileReader. Returns nullptr if error acquired.
|
||||
static std::unique_ptr<IvfFileReader> Create(FileWrapper file);
|
||||
~IvfFileReader();
|
||||
|
||||
IvfFileReader(const IvfFileReader&) = delete;
|
||||
IvfFileReader& operator=(const IvfFileReader&) = delete;
|
||||
|
||||
// Reinitializes reader. Returns false if any error acquired.
|
||||
bool Reset();
|
||||
|
||||
// Returns codec type which was used to create this IVF file and which should
|
||||
// be used to decode EncodedImages from this file.
|
||||
VideoCodecType GetVideoCodecType() const { return codec_type_; }
|
||||
// Returns count of frames in this file.
|
||||
size_t GetFramesCount() const { return num_frames_; }
|
||||
|
||||
// Returns next frame or absl::nullopt if any error acquired. Always returns
|
||||
// absl::nullopt after first error was spotted.
|
||||
absl::optional<EncodedImage> NextFrame();
|
||||
bool HasMoreFrames() const { return num_read_frames_ < num_frames_; }
|
||||
bool HasError() const { return has_error_; }
|
||||
|
||||
uint16_t GetFrameWidth() const { return width_; }
|
||||
uint16_t GetFrameHeight() const { return height_; }
|
||||
|
||||
bool Close();
|
||||
|
||||
private:
|
||||
struct FrameHeader {
|
||||
size_t frame_size;
|
||||
int64_t timestamp;
|
||||
};
|
||||
|
||||
explicit IvfFileReader(FileWrapper file) : file_(std::move(file)) {}
|
||||
|
||||
// Parses codec type from specified position of the buffer. Codec type
|
||||
// contains kCodecTypeBytesCount bytes and caller has to ensure that buffer
|
||||
// won't overflow.
|
||||
absl::optional<VideoCodecType> ParseCodecType(uint8_t* buffer,
|
||||
size_t start_pos);
|
||||
absl::optional<FrameHeader> ReadNextFrameHeader();
|
||||
|
||||
VideoCodecType codec_type_;
|
||||
size_t num_frames_;
|
||||
size_t num_read_frames_;
|
||||
uint16_t width_;
|
||||
uint16_t height_;
|
||||
uint32_t time_scale_;
|
||||
FileWrapper file_;
|
||||
|
||||
absl::optional<FrameHeader> next_frame_header_;
|
||||
bool has_error_;
|
||||
};
|
||||
|
||||
} // namespace webrtc
|
||||
|
||||
#endif // MODULES_VIDEO_CODING_UTILITY_IVF_FILE_READER_H_
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
/*
|
||||
* Copyright (c) 2016 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 "modules/video_coding/utility/ivf_file_writer.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "api/video/encoded_image.h"
|
||||
#include "api/video/video_codec_type.h"
|
||||
#include "api/video_codecs/video_codec.h"
|
||||
#include "modules/rtp_rtcp/source/byte_io.h"
|
||||
#include "modules/video_coding/utility/ivf_defines.h"
|
||||
#include "rtc_base/checks.h"
|
||||
#include "rtc_base/logging.h"
|
||||
#include "rtc_base/system/file_wrapper.h"
|
||||
|
||||
// TODO(palmkvist): make logging more informative in the absence of a file name
|
||||
// (or get one)
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kDefaultWidth = 1280;
|
||||
constexpr int kDefaultHeight = 720;
|
||||
} // namespace
|
||||
|
||||
IvfFileWriter::IvfFileWriter(FileWrapper file, size_t byte_limit)
|
||||
: codec_type_(kVideoCodecGeneric),
|
||||
bytes_written_(0),
|
||||
byte_limit_(byte_limit),
|
||||
num_frames_(0),
|
||||
width_(0),
|
||||
height_(0),
|
||||
last_timestamp_(-1),
|
||||
using_capture_timestamps_(false),
|
||||
file_(std::move(file)) {
|
||||
RTC_DCHECK(byte_limit == 0 || kIvfHeaderSize <= byte_limit)
|
||||
<< "The byte_limit is too low, not even the header will fit.";
|
||||
}
|
||||
|
||||
IvfFileWriter::~IvfFileWriter() {
|
||||
Close();
|
||||
}
|
||||
|
||||
std::unique_ptr<IvfFileWriter> IvfFileWriter::Wrap(FileWrapper file,
|
||||
size_t byte_limit) {
|
||||
return std::unique_ptr<IvfFileWriter>(
|
||||
new IvfFileWriter(std::move(file), byte_limit));
|
||||
}
|
||||
|
||||
std::unique_ptr<IvfFileWriter> IvfFileWriter::Wrap(absl::string_view filename,
|
||||
size_t byte_limit) {
|
||||
return std::unique_ptr<IvfFileWriter>(
|
||||
new IvfFileWriter(FileWrapper::OpenWriteOnly(filename), byte_limit));
|
||||
}
|
||||
|
||||
bool IvfFileWriter::WriteHeader() {
|
||||
if (!file_.Rewind()) {
|
||||
RTC_LOG(LS_WARNING) << "Unable to rewind ivf output file.";
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t ivf_header[kIvfHeaderSize] = {0};
|
||||
ivf_header[0] = 'D';
|
||||
ivf_header[1] = 'K';
|
||||
ivf_header[2] = 'I';
|
||||
ivf_header[3] = 'F';
|
||||
ByteWriter<uint16_t>::WriteLittleEndian(&ivf_header[4], 0); // Version.
|
||||
ByteWriter<uint16_t>::WriteLittleEndian(&ivf_header[6], 32); // Header size.
|
||||
|
||||
switch (codec_type_) {
|
||||
case kVideoCodecVP8:
|
||||
ivf_header[8] = 'V';
|
||||
ivf_header[9] = 'P';
|
||||
ivf_header[10] = '8';
|
||||
ivf_header[11] = '0';
|
||||
break;
|
||||
case kVideoCodecVP9:
|
||||
ivf_header[8] = 'V';
|
||||
ivf_header[9] = 'P';
|
||||
ivf_header[10] = '9';
|
||||
ivf_header[11] = '0';
|
||||
break;
|
||||
case kVideoCodecAV1:
|
||||
ivf_header[8] = 'A';
|
||||
ivf_header[9] = 'V';
|
||||
ivf_header[10] = '0';
|
||||
ivf_header[11] = '1';
|
||||
break;
|
||||
case kVideoCodecH264:
|
||||
ivf_header[8] = 'H';
|
||||
ivf_header[9] = '2';
|
||||
ivf_header[10] = '6';
|
||||
ivf_header[11] = '4';
|
||||
break;
|
||||
case kVideoCodecH265:
|
||||
ivf_header[8] = 'H';
|
||||
ivf_header[9] = '2';
|
||||
ivf_header[10] = '6';
|
||||
ivf_header[11] = '5';
|
||||
break;
|
||||
default:
|
||||
// For unknown codec type use **** code. You can specify actual payload
|
||||
// format when playing the video with ffplay: ffplay -f H263 file.ivf
|
||||
ivf_header[8] = '*';
|
||||
ivf_header[9] = '*';
|
||||
ivf_header[10] = '*';
|
||||
ivf_header[11] = '*';
|
||||
break;
|
||||
}
|
||||
|
||||
ByteWriter<uint16_t>::WriteLittleEndian(&ivf_header[12], width_);
|
||||
ByteWriter<uint16_t>::WriteLittleEndian(&ivf_header[14], height_);
|
||||
// Render timestamps are in ms (1/1000 scale), while RTP timestamps use a
|
||||
// 90kHz clock.
|
||||
ByteWriter<uint32_t>::WriteLittleEndian(
|
||||
&ivf_header[16], using_capture_timestamps_ ? 1000 : 90000);
|
||||
ByteWriter<uint32_t>::WriteLittleEndian(&ivf_header[20], 1);
|
||||
ByteWriter<uint32_t>::WriteLittleEndian(&ivf_header[24],
|
||||
static_cast<uint32_t>(num_frames_));
|
||||
ByteWriter<uint32_t>::WriteLittleEndian(&ivf_header[28], 0); // Reserved.
|
||||
|
||||
if (!file_.Write(ivf_header, kIvfHeaderSize)) {
|
||||
RTC_LOG(LS_ERROR) << "Unable to write IVF header for ivf output file.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bytes_written_ < kIvfHeaderSize) {
|
||||
bytes_written_ = kIvfHeaderSize;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IvfFileWriter::InitFromFirstFrame(const EncodedImage& encoded_image,
|
||||
VideoCodecType codec_type) {
|
||||
if (encoded_image._encodedWidth == 0 || encoded_image._encodedHeight == 0) {
|
||||
width_ = kDefaultWidth;
|
||||
height_ = kDefaultHeight;
|
||||
} else {
|
||||
width_ = encoded_image._encodedWidth;
|
||||
height_ = encoded_image._encodedHeight;
|
||||
}
|
||||
|
||||
using_capture_timestamps_ = encoded_image.RtpTimestamp() == 0;
|
||||
|
||||
codec_type_ = codec_type;
|
||||
|
||||
if (!WriteHeader())
|
||||
return false;
|
||||
|
||||
const char* codec_name = CodecTypeToPayloadString(codec_type_);
|
||||
RTC_LOG(LS_WARNING) << "Created IVF file for codec data of type "
|
||||
<< codec_name << " at resolution " << width_ << " x "
|
||||
<< height_ << ", using "
|
||||
<< (using_capture_timestamps_ ? "1" : "90")
|
||||
<< "kHz clock resolution.";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IvfFileWriter::WriteFrame(const EncodedImage& encoded_image,
|
||||
VideoCodecType codec_type) {
|
||||
if (!file_.is_open())
|
||||
return false;
|
||||
|
||||
if (num_frames_ == 0 && !InitFromFirstFrame(encoded_image, codec_type))
|
||||
return false;
|
||||
RTC_DCHECK_EQ(codec_type_, codec_type);
|
||||
|
||||
int64_t timestamp = using_capture_timestamps_
|
||||
? encoded_image.capture_time_ms_
|
||||
: wrap_handler_.Unwrap(encoded_image.RtpTimestamp());
|
||||
if (last_timestamp_ != -1 && timestamp < last_timestamp_) {
|
||||
RTC_LOG(LS_WARNING) << "Timestamp not increasing: " << last_timestamp_
|
||||
<< " -> " << timestamp;
|
||||
}
|
||||
last_timestamp_ = timestamp;
|
||||
|
||||
bool written_frames = false;
|
||||
size_t max_sl_index = encoded_image.SpatialIndex().value_or(0);
|
||||
const uint8_t* data = encoded_image.data();
|
||||
for (size_t sl_idx = 0; sl_idx <= max_sl_index; ++sl_idx) {
|
||||
size_t cur_size = encoded_image.SpatialLayerFrameSize(sl_idx).value_or(0);
|
||||
if (cur_size > 0) {
|
||||
written_frames = true;
|
||||
if (!WriteOneSpatialLayer(timestamp, data, cur_size)) {
|
||||
return false;
|
||||
}
|
||||
data += cur_size;
|
||||
}
|
||||
}
|
||||
|
||||
// If frame has only one spatial layer it won't have any spatial layers'
|
||||
// sizes. Therefore this case should be addressed separately.
|
||||
if (!written_frames) {
|
||||
return WriteOneSpatialLayer(timestamp, data, encoded_image.size());
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool IvfFileWriter::WriteOneSpatialLayer(int64_t timestamp,
|
||||
const uint8_t* data,
|
||||
size_t size) {
|
||||
const size_t kFrameHeaderSize = 12;
|
||||
if (byte_limit_ != 0 &&
|
||||
bytes_written_ + kFrameHeaderSize + size > byte_limit_) {
|
||||
RTC_LOG(LS_WARNING) << "Closing IVF file due to reaching size limit: "
|
||||
<< byte_limit_ << " bytes.";
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
uint8_t frame_header[kFrameHeaderSize] = {};
|
||||
ByteWriter<uint32_t>::WriteLittleEndian(&frame_header[0],
|
||||
static_cast<uint32_t>(size));
|
||||
ByteWriter<uint64_t>::WriteLittleEndian(&frame_header[4], timestamp);
|
||||
if (!file_.Write(frame_header, kFrameHeaderSize) ||
|
||||
!file_.Write(data, size)) {
|
||||
RTC_LOG(LS_ERROR) << "Unable to write frame to file.";
|
||||
return false;
|
||||
}
|
||||
|
||||
bytes_written_ += kFrameHeaderSize + size;
|
||||
|
||||
++num_frames_;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IvfFileWriter::Close() {
|
||||
if (!file_.is_open())
|
||||
return false;
|
||||
|
||||
if (num_frames_ == 0) {
|
||||
file_.Close();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ret = WriteHeader();
|
||||
file_.Close();
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace webrtc
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (c) 2016 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.
|
||||
*/
|
||||
|
||||
#ifndef MODULES_VIDEO_CODING_UTILITY_IVF_FILE_WRITER_H_
|
||||
#define MODULES_VIDEO_CODING_UTILITY_IVF_FILE_WRITER_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "api/video/encoded_image.h"
|
||||
#include "api/video/video_codec_type.h"
|
||||
#include "rtc_base/numerics/sequence_number_unwrapper.h"
|
||||
#include "rtc_base/system/file_wrapper.h"
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
class IvfFileWriter {
|
||||
public:
|
||||
// Takes ownership of the file, which will be closed either through
|
||||
// Close or ~IvfFileWriter. If writing a frame would take the file above the
|
||||
// `byte_limit` the file will be closed, the write (and all future writes)
|
||||
// will fail. A `byte_limit` of 0 is equivalent to no limit.
|
||||
static std::unique_ptr<IvfFileWriter> Wrap(FileWrapper file,
|
||||
size_t byte_limit);
|
||||
static std::unique_ptr<IvfFileWriter> Wrap(absl::string_view filename,
|
||||
size_t byte_limit);
|
||||
~IvfFileWriter();
|
||||
|
||||
IvfFileWriter(const IvfFileWriter&) = delete;
|
||||
IvfFileWriter& operator=(const IvfFileWriter&) = delete;
|
||||
|
||||
bool WriteFrame(const EncodedImage& encoded_image, VideoCodecType codec_type);
|
||||
bool Close();
|
||||
|
||||
private:
|
||||
explicit IvfFileWriter(FileWrapper file, size_t byte_limit);
|
||||
|
||||
bool WriteHeader();
|
||||
bool InitFromFirstFrame(const EncodedImage& encoded_image,
|
||||
VideoCodecType codec_type);
|
||||
bool WriteOneSpatialLayer(int64_t timestamp,
|
||||
const uint8_t* data,
|
||||
size_t size);
|
||||
|
||||
VideoCodecType codec_type_;
|
||||
size_t bytes_written_;
|
||||
size_t byte_limit_;
|
||||
size_t num_frames_;
|
||||
uint16_t width_;
|
||||
uint16_t height_;
|
||||
int64_t last_timestamp_;
|
||||
bool using_capture_timestamps_;
|
||||
RtpTimestampUnwrapper wrap_handler_;
|
||||
FileWrapper file_;
|
||||
};
|
||||
|
||||
} // namespace webrtc
|
||||
|
||||
#endif // MODULES_VIDEO_CODING_UTILITY_IVF_FILE_WRITER_H_
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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 "modules/video_coding/utility/qp_parser.h"
|
||||
|
||||
#include "modules/video_coding/utility/vp8_header_parser.h"
|
||||
#include "modules/video_coding/utility/vp9_uncompressed_header_parser.h"
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
absl::optional<uint32_t> QpParser::Parse(VideoCodecType codec_type,
|
||||
size_t spatial_idx,
|
||||
const uint8_t* frame_data,
|
||||
size_t frame_size) {
|
||||
if (frame_data == nullptr || frame_size == 0 ||
|
||||
spatial_idx >= kMaxSimulcastStreams) {
|
||||
return absl::nullopt;
|
||||
}
|
||||
|
||||
if (codec_type == kVideoCodecVP8) {
|
||||
int qp = -1;
|
||||
if (vp8::GetQp(frame_data, frame_size, &qp)) {
|
||||
return qp;
|
||||
}
|
||||
} else if (codec_type == kVideoCodecVP9) {
|
||||
int qp = -1;
|
||||
if (vp9::GetQp(frame_data, frame_size, &qp)) {
|
||||
return qp;
|
||||
}
|
||||
} else if (codec_type == kVideoCodecH264) {
|
||||
return h264_parsers_[spatial_idx].Parse(frame_data, frame_size);
|
||||
} else if (codec_type == kVideoCodecH265) {
|
||||
// H.265 bitstream parser is conditionally built.
|
||||
#ifdef RTC_ENABLE_H265
|
||||
return h265_parsers_[spatial_idx].Parse(frame_data, frame_size);
|
||||
#endif
|
||||
}
|
||||
|
||||
return absl::nullopt;
|
||||
}
|
||||
|
||||
absl::optional<uint32_t> QpParser::H264QpParser::Parse(
|
||||
const uint8_t* frame_data,
|
||||
size_t frame_size) {
|
||||
MutexLock lock(&mutex_);
|
||||
bitstream_parser_.ParseBitstream(
|
||||
rtc::ArrayView<const uint8_t>(frame_data, frame_size));
|
||||
return bitstream_parser_.GetLastSliceQp();
|
||||
}
|
||||
|
||||
#ifdef RTC_ENABLE_H265
|
||||
absl::optional<uint32_t> QpParser::H265QpParser::Parse(
|
||||
const uint8_t* frame_data,
|
||||
size_t frame_size) {
|
||||
MutexLock lock(&mutex_);
|
||||
bitstream_parser_.ParseBitstream(
|
||||
rtc::ArrayView<const uint8_t>(frame_data, frame_size));
|
||||
return bitstream_parser_.GetLastSliceQp();
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace webrtc
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.
|
||||
*/
|
||||
|
||||
#ifndef MODULES_VIDEO_CODING_UTILITY_QP_PARSER_H_
|
||||
#define MODULES_VIDEO_CODING_UTILITY_QP_PARSER_H_
|
||||
|
||||
#include "absl/types/optional.h"
|
||||
#include "api/video/video_codec_constants.h"
|
||||
#include "api/video/video_codec_type.h"
|
||||
#include "common_video/h264/h264_bitstream_parser.h"
|
||||
#ifdef RTC_ENABLE_H265
|
||||
#include "common_video/h265/h265_bitstream_parser.h"
|
||||
#endif
|
||||
#include "rtc_base/synchronization/mutex.h"
|
||||
|
||||
namespace webrtc {
|
||||
class QpParser {
|
||||
public:
|
||||
absl::optional<uint32_t> Parse(VideoCodecType codec_type,
|
||||
size_t spatial_idx,
|
||||
const uint8_t* frame_data,
|
||||
size_t frame_size);
|
||||
|
||||
private:
|
||||
// A thread safe wrapper for H264 bitstream parser.
|
||||
class H264QpParser {
|
||||
public:
|
||||
absl::optional<uint32_t> Parse(const uint8_t* frame_data,
|
||||
size_t frame_size);
|
||||
|
||||
private:
|
||||
Mutex mutex_;
|
||||
H264BitstreamParser bitstream_parser_ RTC_GUARDED_BY(mutex_);
|
||||
};
|
||||
|
||||
H264QpParser h264_parsers_[kMaxSimulcastStreams];
|
||||
|
||||
#ifdef RTC_ENABLE_H265
|
||||
// A thread safe wrapper for H.265 bitstream parser.
|
||||
class H265QpParser {
|
||||
public:
|
||||
absl::optional<uint32_t> Parse(const uint8_t* frame_data,
|
||||
size_t frame_size);
|
||||
|
||||
private:
|
||||
Mutex mutex_;
|
||||
H265BitstreamParser bitstream_parser_ RTC_GUARDED_BY(mutex_);
|
||||
};
|
||||
|
||||
H265QpParser h265_parsers_[kMaxSimulcastStreams];
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace webrtc
|
||||
|
||||
#endif // MODULES_VIDEO_CODING_UTILITY_QP_PARSER_H_
|
||||
|
|
@ -0,0 +1,336 @@
|
|||
/*
|
||||
* Copyright (c) 2014 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 "modules/video_coding/utility/quality_scaler.h"
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "api/field_trials_view.h"
|
||||
#include "api/units/time_delta.h"
|
||||
#include "api/video/video_adaptation_reason.h"
|
||||
#include "rtc_base/checks.h"
|
||||
#include "rtc_base/experiments/quality_scaler_settings.h"
|
||||
#include "rtc_base/logging.h"
|
||||
#include "rtc_base/numerics/exp_filter.h"
|
||||
#include "rtc_base/weak_ptr.h"
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
namespace {
|
||||
// Threshold constant used until first downscale (to permit fast rampup).
|
||||
static const int kMeasureMs = 2000;
|
||||
static const float kSamplePeriodScaleFactor = 2.5;
|
||||
static const int kFramedropPercentThreshold = 60;
|
||||
static const size_t kMinFramesNeededToScale = 2 * 30;
|
||||
|
||||
} // namespace
|
||||
|
||||
class QualityScaler::QpSmoother {
|
||||
public:
|
||||
explicit QpSmoother(float alpha)
|
||||
: alpha_(alpha),
|
||||
// The initial value of last_sample_ms doesn't matter since the smoother
|
||||
// will ignore the time delta for the first update.
|
||||
last_sample_ms_(0),
|
||||
smoother_(alpha) {}
|
||||
|
||||
absl::optional<int> GetAvg() const {
|
||||
float value = smoother_.filtered();
|
||||
if (value == rtc::ExpFilter::kValueUndefined) {
|
||||
return absl::nullopt;
|
||||
}
|
||||
return static_cast<int>(value);
|
||||
}
|
||||
|
||||
void Add(float sample, int64_t time_sent_us) {
|
||||
int64_t now_ms = time_sent_us / 1000;
|
||||
smoother_.Apply(static_cast<float>(now_ms - last_sample_ms_), sample);
|
||||
last_sample_ms_ = now_ms;
|
||||
}
|
||||
|
||||
void Reset() { smoother_.Reset(alpha_); }
|
||||
|
||||
private:
|
||||
const float alpha_;
|
||||
int64_t last_sample_ms_;
|
||||
rtc::ExpFilter smoother_;
|
||||
};
|
||||
|
||||
// The QualityScaler checks for QP periodically by queuing CheckQpTasks. The
|
||||
// task will either run to completion and trigger a new task being queued, or it
|
||||
// will be destroyed because the QualityScaler is destroyed.
|
||||
//
|
||||
// When high or low QP is reported, the task will be pending until a callback is
|
||||
// invoked. This lets the QualityScalerQpUsageHandlerInterface react to QP usage
|
||||
// asynchronously and prevents checking for QP until the stream has potentially
|
||||
// been reconfigured.
|
||||
class QualityScaler::CheckQpTask {
|
||||
public:
|
||||
// The result of one CheckQpTask may influence the delay of the next
|
||||
// CheckQpTask.
|
||||
struct Result {
|
||||
bool observed_enough_frames = false;
|
||||
bool qp_usage_reported = false;
|
||||
};
|
||||
|
||||
CheckQpTask(QualityScaler* quality_scaler, Result previous_task_result)
|
||||
: quality_scaler_(quality_scaler),
|
||||
state_(State::kNotStarted),
|
||||
previous_task_result_(previous_task_result),
|
||||
weak_ptr_factory_(this) {}
|
||||
|
||||
void StartDelayedTask() {
|
||||
RTC_DCHECK_EQ(state_, State::kNotStarted);
|
||||
state_ = State::kCheckingQp;
|
||||
TaskQueueBase::Current()->PostDelayedTask(
|
||||
[this_weak_ptr = weak_ptr_factory_.GetWeakPtr(), this] {
|
||||
if (!this_weak_ptr) {
|
||||
// The task has been cancelled through destruction.
|
||||
return;
|
||||
}
|
||||
RTC_DCHECK_EQ(state_, State::kCheckingQp);
|
||||
RTC_DCHECK_RUN_ON(&quality_scaler_->task_checker_);
|
||||
switch (quality_scaler_->CheckQp()) {
|
||||
case QualityScaler::CheckQpResult::kInsufficientSamples: {
|
||||
result_.observed_enough_frames = false;
|
||||
// After this line, `this` may be deleted.
|
||||
break;
|
||||
}
|
||||
case QualityScaler::CheckQpResult::kNormalQp: {
|
||||
result_.observed_enough_frames = true;
|
||||
break;
|
||||
}
|
||||
case QualityScaler::CheckQpResult::kHighQp: {
|
||||
result_.observed_enough_frames = true;
|
||||
result_.qp_usage_reported = true;
|
||||
quality_scaler_->fast_rampup_ = false;
|
||||
quality_scaler_->handler_->OnReportQpUsageHigh();
|
||||
quality_scaler_->ClearSamples();
|
||||
break;
|
||||
}
|
||||
case QualityScaler::CheckQpResult::kLowQp: {
|
||||
result_.observed_enough_frames = true;
|
||||
result_.qp_usage_reported = true;
|
||||
quality_scaler_->handler_->OnReportQpUsageLow();
|
||||
quality_scaler_->ClearSamples();
|
||||
break;
|
||||
}
|
||||
}
|
||||
state_ = State::kCompleted;
|
||||
// Starting the next task deletes the pending task. After this line,
|
||||
// `this` has been deleted.
|
||||
quality_scaler_->StartNextCheckQpTask();
|
||||
},
|
||||
TimeDelta::Millis(GetCheckingQpDelayMs()));
|
||||
}
|
||||
|
||||
bool HasCompletedTask() const { return state_ == State::kCompleted; }
|
||||
|
||||
Result result() const {
|
||||
RTC_DCHECK(HasCompletedTask());
|
||||
return result_;
|
||||
}
|
||||
|
||||
private:
|
||||
enum class State {
|
||||
kNotStarted,
|
||||
kCheckingQp,
|
||||
kCompleted,
|
||||
};
|
||||
|
||||
// Determines the sampling period of CheckQpTasks.
|
||||
int64_t GetCheckingQpDelayMs() const {
|
||||
RTC_DCHECK_RUN_ON(&quality_scaler_->task_checker_);
|
||||
if (quality_scaler_->fast_rampup_) {
|
||||
return quality_scaler_->sampling_period_ms_;
|
||||
}
|
||||
if (quality_scaler_->experiment_enabled_ &&
|
||||
!previous_task_result_.observed_enough_frames) {
|
||||
// Use half the interval while waiting for enough frames.
|
||||
return quality_scaler_->sampling_period_ms_ / 2;
|
||||
}
|
||||
if (quality_scaler_->scale_factor_ &&
|
||||
!previous_task_result_.qp_usage_reported) {
|
||||
// Last CheckQp did not call AdaptDown/Up, possibly reduce interval.
|
||||
return quality_scaler_->sampling_period_ms_ *
|
||||
quality_scaler_->scale_factor_.value();
|
||||
}
|
||||
return quality_scaler_->sampling_period_ms_ *
|
||||
quality_scaler_->initial_scale_factor_;
|
||||
}
|
||||
|
||||
QualityScaler* const quality_scaler_;
|
||||
State state_;
|
||||
const Result previous_task_result_;
|
||||
Result result_;
|
||||
|
||||
rtc::WeakPtrFactory<CheckQpTask> weak_ptr_factory_;
|
||||
};
|
||||
|
||||
QualityScaler::QualityScaler(QualityScalerQpUsageHandlerInterface* handler,
|
||||
VideoEncoder::QpThresholds thresholds,
|
||||
const FieldTrialsView& field_trials)
|
||||
: QualityScaler(handler, thresholds, field_trials, kMeasureMs) {}
|
||||
|
||||
// Protected ctor, should not be called directly.
|
||||
QualityScaler::QualityScaler(QualityScalerQpUsageHandlerInterface* handler,
|
||||
VideoEncoder::QpThresholds thresholds,
|
||||
const FieldTrialsView& field_trials,
|
||||
int64_t default_sampling_period_ms)
|
||||
: handler_(handler),
|
||||
thresholds_(thresholds),
|
||||
sampling_period_ms_(QualityScalerSettings(field_trials)
|
||||
.SamplingPeriodMs()
|
||||
.value_or(default_sampling_period_ms)),
|
||||
fast_rampup_(true),
|
||||
// Arbitrarily choose size based on 30 fps for 5 seconds.
|
||||
average_qp_(QualityScalerSettings(field_trials)
|
||||
.AverageQpWindow()
|
||||
.value_or(5 * 30)),
|
||||
framedrop_percent_media_opt_(5 * 30),
|
||||
framedrop_percent_all_(5 * 30),
|
||||
experiment_enabled_(QualityScalingExperiment::Enabled(field_trials)),
|
||||
min_frames_needed_(QualityScalerSettings(field_trials)
|
||||
.MinFrames()
|
||||
.value_or(kMinFramesNeededToScale)),
|
||||
initial_scale_factor_(QualityScalerSettings(field_trials)
|
||||
.InitialScaleFactor()
|
||||
.value_or(kSamplePeriodScaleFactor)),
|
||||
scale_factor_(QualityScalerSettings(field_trials).ScaleFactor()) {
|
||||
RTC_DCHECK_RUN_ON(&task_checker_);
|
||||
if (experiment_enabled_) {
|
||||
config_ = QualityScalingExperiment::GetConfig(field_trials);
|
||||
qp_smoother_high_.reset(new QpSmoother(config_.alpha_high));
|
||||
qp_smoother_low_.reset(new QpSmoother(config_.alpha_low));
|
||||
}
|
||||
RTC_DCHECK(handler_ != nullptr);
|
||||
StartNextCheckQpTask();
|
||||
RTC_LOG(LS_INFO) << "QP thresholds: low: " << thresholds_.low
|
||||
<< ", high: " << thresholds_.high;
|
||||
}
|
||||
|
||||
QualityScaler::~QualityScaler() {
|
||||
RTC_DCHECK_RUN_ON(&task_checker_);
|
||||
}
|
||||
|
||||
void QualityScaler::StartNextCheckQpTask() {
|
||||
RTC_DCHECK_RUN_ON(&task_checker_);
|
||||
RTC_DCHECK(!pending_qp_task_ || pending_qp_task_->HasCompletedTask())
|
||||
<< "A previous CheckQpTask has not completed yet!";
|
||||
CheckQpTask::Result previous_task_result;
|
||||
if (pending_qp_task_) {
|
||||
previous_task_result = pending_qp_task_->result();
|
||||
}
|
||||
pending_qp_task_ = std::make_unique<CheckQpTask>(this, previous_task_result);
|
||||
pending_qp_task_->StartDelayedTask();
|
||||
}
|
||||
|
||||
void QualityScaler::SetQpThresholds(VideoEncoder::QpThresholds thresholds) {
|
||||
RTC_DCHECK_RUN_ON(&task_checker_);
|
||||
thresholds_ = thresholds;
|
||||
}
|
||||
|
||||
void QualityScaler::ReportDroppedFrameByMediaOpt() {
|
||||
RTC_DCHECK_RUN_ON(&task_checker_);
|
||||
framedrop_percent_media_opt_.AddSample(100);
|
||||
framedrop_percent_all_.AddSample(100);
|
||||
}
|
||||
|
||||
void QualityScaler::ReportDroppedFrameByEncoder() {
|
||||
RTC_DCHECK_RUN_ON(&task_checker_);
|
||||
framedrop_percent_all_.AddSample(100);
|
||||
}
|
||||
|
||||
void QualityScaler::ReportQp(int qp, int64_t time_sent_us) {
|
||||
RTC_DCHECK_RUN_ON(&task_checker_);
|
||||
framedrop_percent_media_opt_.AddSample(0);
|
||||
framedrop_percent_all_.AddSample(0);
|
||||
average_qp_.AddSample(qp);
|
||||
if (qp_smoother_high_)
|
||||
qp_smoother_high_->Add(qp, time_sent_us);
|
||||
if (qp_smoother_low_)
|
||||
qp_smoother_low_->Add(qp, time_sent_us);
|
||||
}
|
||||
|
||||
bool QualityScaler::QpFastFilterLow() const {
|
||||
RTC_DCHECK_RUN_ON(&task_checker_);
|
||||
size_t num_frames = config_.use_all_drop_reasons
|
||||
? framedrop_percent_all_.Size()
|
||||
: framedrop_percent_media_opt_.Size();
|
||||
const size_t kMinNumFrames = 10;
|
||||
if (num_frames < kMinNumFrames) {
|
||||
return false; // Wait for more frames before making a decision.
|
||||
}
|
||||
absl::optional<int> avg_qp_high = qp_smoother_high_
|
||||
? qp_smoother_high_->GetAvg()
|
||||
: average_qp_.GetAverageRoundedDown();
|
||||
return (avg_qp_high) ? (avg_qp_high.value() <= thresholds_.low) : false;
|
||||
}
|
||||
|
||||
QualityScaler::CheckQpResult QualityScaler::CheckQp() const {
|
||||
RTC_DCHECK_RUN_ON(&task_checker_);
|
||||
// Should be set through InitEncode -> Should be set by now.
|
||||
RTC_DCHECK_GE(thresholds_.low, 0);
|
||||
|
||||
// If we have not observed at least this many frames we can't make a good
|
||||
// scaling decision.
|
||||
const size_t frames = config_.use_all_drop_reasons
|
||||
? framedrop_percent_all_.Size()
|
||||
: framedrop_percent_media_opt_.Size();
|
||||
if (frames < min_frames_needed_) {
|
||||
return CheckQpResult::kInsufficientSamples;
|
||||
}
|
||||
|
||||
// Check if we should scale down due to high frame drop.
|
||||
const absl::optional<int> drop_rate =
|
||||
config_.use_all_drop_reasons
|
||||
? framedrop_percent_all_.GetAverageRoundedDown()
|
||||
: framedrop_percent_media_opt_.GetAverageRoundedDown();
|
||||
if (drop_rate && *drop_rate >= kFramedropPercentThreshold) {
|
||||
RTC_LOG(LS_INFO) << "Reporting high QP, framedrop percent " << *drop_rate;
|
||||
return CheckQpResult::kHighQp;
|
||||
}
|
||||
|
||||
// Check if we should scale up or down based on QP.
|
||||
const absl::optional<int> avg_qp_high =
|
||||
qp_smoother_high_ ? qp_smoother_high_->GetAvg()
|
||||
: average_qp_.GetAverageRoundedDown();
|
||||
const absl::optional<int> avg_qp_low =
|
||||
qp_smoother_low_ ? qp_smoother_low_->GetAvg()
|
||||
: average_qp_.GetAverageRoundedDown();
|
||||
if (avg_qp_high && avg_qp_low) {
|
||||
RTC_LOG(LS_INFO) << "Checking average QP " << *avg_qp_high << " ("
|
||||
<< *avg_qp_low << ").";
|
||||
if (*avg_qp_high > thresholds_.high) {
|
||||
return CheckQpResult::kHighQp;
|
||||
}
|
||||
if (*avg_qp_low <= thresholds_.low) {
|
||||
// QP has been low. We want to try a higher resolution.
|
||||
return CheckQpResult::kLowQp;
|
||||
}
|
||||
}
|
||||
return CheckQpResult::kNormalQp;
|
||||
}
|
||||
|
||||
void QualityScaler::ClearSamples() {
|
||||
RTC_DCHECK_RUN_ON(&task_checker_);
|
||||
framedrop_percent_media_opt_.Reset();
|
||||
framedrop_percent_all_.Reset();
|
||||
average_qp_.Reset();
|
||||
if (qp_smoother_high_)
|
||||
qp_smoother_high_->Reset();
|
||||
if (qp_smoother_low_)
|
||||
qp_smoother_low_->Reset();
|
||||
}
|
||||
|
||||
QualityScalerQpUsageHandlerInterface::~QualityScalerQpUsageHandlerInterface() {}
|
||||
|
||||
} // namespace webrtc
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright (c) 2014 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.
|
||||
*/
|
||||
|
||||
#ifndef MODULES_VIDEO_CODING_UTILITY_QUALITY_SCALER_H_
|
||||
#define MODULES_VIDEO_CODING_UTILITY_QUALITY_SCALER_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "absl/types/optional.h"
|
||||
#include "api/field_trials_view.h"
|
||||
#include "api/scoped_refptr.h"
|
||||
#include "api/sequence_checker.h"
|
||||
#include "api/video_codecs/video_encoder.h"
|
||||
#include "rtc_base/experiments/quality_scaling_experiment.h"
|
||||
#include "rtc_base/numerics/moving_average.h"
|
||||
#include "rtc_base/ref_count.h"
|
||||
#include "rtc_base/system/no_unique_address.h"
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
class QualityScalerQpUsageHandlerCallbackInterface;
|
||||
class QualityScalerQpUsageHandlerInterface;
|
||||
|
||||
// QualityScaler runs asynchronously and monitors QP values of encoded frames.
|
||||
// It holds a reference to a QualityScalerQpUsageHandlerInterface implementation
|
||||
// to signal an overuse or underuse of QP (which indicate a desire to scale the
|
||||
// video stream down or up).
|
||||
class QualityScaler {
|
||||
public:
|
||||
// Construct a QualityScaler with given `thresholds` and `handler`.
|
||||
// This starts the quality scaler periodically checking what the average QP
|
||||
// has been recently.
|
||||
QualityScaler(QualityScalerQpUsageHandlerInterface* handler,
|
||||
VideoEncoder::QpThresholds thresholds,
|
||||
const FieldTrialsView& field_trials);
|
||||
virtual ~QualityScaler();
|
||||
// Should be called each time a frame is dropped at encoding.
|
||||
void ReportDroppedFrameByMediaOpt();
|
||||
void ReportDroppedFrameByEncoder();
|
||||
// Inform the QualityScaler of the last seen QP.
|
||||
void ReportQp(int qp, int64_t time_sent_us);
|
||||
|
||||
void SetQpThresholds(VideoEncoder::QpThresholds thresholds);
|
||||
bool QpFastFilterLow() const;
|
||||
|
||||
// The following members declared protected for testing purposes.
|
||||
protected:
|
||||
QualityScaler(QualityScalerQpUsageHandlerInterface* handler,
|
||||
VideoEncoder::QpThresholds thresholds,
|
||||
const FieldTrialsView& field_trials,
|
||||
int64_t sampling_period_ms);
|
||||
|
||||
private:
|
||||
class QpSmoother;
|
||||
class CheckQpTask;
|
||||
class CheckQpTaskHandlerCallback;
|
||||
|
||||
enum class CheckQpResult {
|
||||
kInsufficientSamples,
|
||||
kNormalQp,
|
||||
kHighQp,
|
||||
kLowQp,
|
||||
};
|
||||
|
||||
// Starts checking for QP in a delayed task. When the resulting CheckQpTask
|
||||
// completes, it will invoke this method again, ensuring that we always
|
||||
// periodically check for QP. See CheckQpTask for more details. We never run
|
||||
// more than one CheckQpTask at a time.
|
||||
void StartNextCheckQpTask();
|
||||
|
||||
CheckQpResult CheckQp() const;
|
||||
void ClearSamples();
|
||||
|
||||
std::unique_ptr<CheckQpTask> pending_qp_task_ RTC_GUARDED_BY(&task_checker_);
|
||||
QualityScalerQpUsageHandlerInterface* const handler_
|
||||
RTC_GUARDED_BY(&task_checker_);
|
||||
RTC_NO_UNIQUE_ADDRESS SequenceChecker task_checker_;
|
||||
|
||||
VideoEncoder::QpThresholds thresholds_ RTC_GUARDED_BY(&task_checker_);
|
||||
const int64_t sampling_period_ms_;
|
||||
bool fast_rampup_ RTC_GUARDED_BY(&task_checker_);
|
||||
rtc::MovingAverage average_qp_ RTC_GUARDED_BY(&task_checker_);
|
||||
rtc::MovingAverage framedrop_percent_media_opt_
|
||||
RTC_GUARDED_BY(&task_checker_);
|
||||
rtc::MovingAverage framedrop_percent_all_ RTC_GUARDED_BY(&task_checker_);
|
||||
|
||||
// Used by QualityScalingExperiment.
|
||||
const bool experiment_enabled_;
|
||||
QualityScalingExperiment::Config config_ RTC_GUARDED_BY(&task_checker_);
|
||||
std::unique_ptr<QpSmoother> qp_smoother_high_ RTC_GUARDED_BY(&task_checker_);
|
||||
std::unique_ptr<QpSmoother> qp_smoother_low_ RTC_GUARDED_BY(&task_checker_);
|
||||
|
||||
const size_t min_frames_needed_;
|
||||
const double initial_scale_factor_;
|
||||
const absl::optional<double> scale_factor_;
|
||||
};
|
||||
|
||||
// Reacts to QP being too high or too low. For best quality, when QP is high it
|
||||
// is desired to decrease the resolution or frame rate of the stream and when QP
|
||||
// is low it is desired to increase the resolution or frame rate of the stream.
|
||||
// Whether to reconfigure the stream is ultimately up to the handler, which is
|
||||
// able to respond asynchronously.
|
||||
class QualityScalerQpUsageHandlerInterface {
|
||||
public:
|
||||
virtual ~QualityScalerQpUsageHandlerInterface();
|
||||
|
||||
virtual void OnReportQpUsageHigh() = 0;
|
||||
virtual void OnReportQpUsageLow() = 0;
|
||||
};
|
||||
|
||||
} // namespace webrtc
|
||||
|
||||
#endif // MODULES_VIDEO_CODING_UTILITY_QUALITY_SCALER_H_
|
||||
|
|
@ -0,0 +1,343 @@
|
|||
/*
|
||||
* Copyright (c) 2016 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 "modules/video_coding/utility/simulcast_rate_allocator.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <numeric>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include "rtc_base/checks.h"
|
||||
#include "rtc_base/experiments/rate_control_settings.h"
|
||||
#include "system_wrappers/include/field_trial.h"
|
||||
|
||||
namespace webrtc {
|
||||
namespace {
|
||||
// Ratio allocation between temporal streams:
|
||||
// Values as required for the VP8 codec (accumulating).
|
||||
static const float
|
||||
kLayerRateAllocation[kMaxTemporalStreams][kMaxTemporalStreams] = {
|
||||
{1.0f, 1.0f, 1.0f, 1.0f}, // 1 layer
|
||||
{0.6f, 1.0f, 1.0f, 1.0f}, // 2 layers {60%, 40%}
|
||||
{0.4f, 0.6f, 1.0f, 1.0f}, // 3 layers {40%, 20%, 40%}
|
||||
{0.25f, 0.4f, 0.6f, 1.0f} // 4 layers {25%, 15%, 20%, 40%}
|
||||
};
|
||||
|
||||
static const float kBaseHeavy3TlRateAllocation[kMaxTemporalStreams] = {
|
||||
0.6f, 0.8f, 1.0f, 1.0f // 3 layers {60%, 20%, 20%}
|
||||
};
|
||||
|
||||
const uint32_t kLegacyScreenshareTl0BitrateKbps = 200;
|
||||
const uint32_t kLegacyScreenshareTl1BitrateKbps = 1000;
|
||||
} // namespace
|
||||
|
||||
float SimulcastRateAllocator::GetTemporalRateAllocation(
|
||||
int num_layers,
|
||||
int temporal_id,
|
||||
bool base_heavy_tl3_alloc) {
|
||||
RTC_CHECK_GT(num_layers, 0);
|
||||
RTC_CHECK_LE(num_layers, kMaxTemporalStreams);
|
||||
RTC_CHECK_GE(temporal_id, 0);
|
||||
RTC_CHECK_LT(temporal_id, num_layers);
|
||||
if (num_layers == 3 && base_heavy_tl3_alloc) {
|
||||
return kBaseHeavy3TlRateAllocation[temporal_id];
|
||||
}
|
||||
return kLayerRateAllocation[num_layers - 1][temporal_id];
|
||||
}
|
||||
|
||||
SimulcastRateAllocator::SimulcastRateAllocator(const VideoCodec& codec)
|
||||
: codec_(codec),
|
||||
stable_rate_settings_(StableTargetRateExperiment::ParseFromFieldTrials()),
|
||||
rate_control_settings_(RateControlSettings::ParseFromFieldTrials()),
|
||||
legacy_conference_mode_(false) {}
|
||||
|
||||
SimulcastRateAllocator::~SimulcastRateAllocator() = default;
|
||||
|
||||
VideoBitrateAllocation SimulcastRateAllocator::Allocate(
|
||||
VideoBitrateAllocationParameters parameters) {
|
||||
VideoBitrateAllocation allocated_bitrates;
|
||||
DataRate stable_rate = parameters.total_bitrate;
|
||||
if (stable_rate_settings_.IsEnabled() &&
|
||||
parameters.stable_bitrate > DataRate::Zero()) {
|
||||
stable_rate = std::min(parameters.stable_bitrate, parameters.total_bitrate);
|
||||
}
|
||||
DistributeAllocationToSimulcastLayers(parameters.total_bitrate, stable_rate,
|
||||
&allocated_bitrates);
|
||||
DistributeAllocationToTemporalLayers(&allocated_bitrates);
|
||||
return allocated_bitrates;
|
||||
}
|
||||
|
||||
void SimulcastRateAllocator::DistributeAllocationToSimulcastLayers(
|
||||
DataRate total_bitrate,
|
||||
DataRate stable_bitrate,
|
||||
VideoBitrateAllocation* allocated_bitrates) {
|
||||
DataRate left_in_total_allocation = total_bitrate;
|
||||
DataRate left_in_stable_allocation = stable_bitrate;
|
||||
|
||||
if (codec_.maxBitrate) {
|
||||
DataRate max_rate = DataRate::KilobitsPerSec(codec_.maxBitrate);
|
||||
left_in_total_allocation = std::min(left_in_total_allocation, max_rate);
|
||||
left_in_stable_allocation = std::min(left_in_stable_allocation, max_rate);
|
||||
}
|
||||
|
||||
if (codec_.numberOfSimulcastStreams == 0) {
|
||||
// No simulcast, just set the target as this has been capped already.
|
||||
if (codec_.active) {
|
||||
allocated_bitrates->SetBitrate(
|
||||
0, 0,
|
||||
std::max(DataRate::KilobitsPerSec(codec_.minBitrate),
|
||||
left_in_total_allocation)
|
||||
.bps());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort the layers by maxFramerate, they might not always be from smallest
|
||||
// to biggest
|
||||
std::vector<size_t> layer_index(codec_.numberOfSimulcastStreams);
|
||||
std::iota(layer_index.begin(), layer_index.end(), 0);
|
||||
std::stable_sort(layer_index.begin(), layer_index.end(),
|
||||
[this](size_t a, size_t b) {
|
||||
return std::tie(codec_.simulcastStream[a].maxBitrate) <
|
||||
std::tie(codec_.simulcastStream[b].maxBitrate);
|
||||
});
|
||||
|
||||
// Find the first active layer. We don't allocate to inactive layers.
|
||||
size_t active_layer = 0;
|
||||
for (; active_layer < codec_.numberOfSimulcastStreams; ++active_layer) {
|
||||
if (codec_.simulcastStream[layer_index[active_layer]].active) {
|
||||
// Found the first active layer.
|
||||
break;
|
||||
}
|
||||
}
|
||||
// All streams could be inactive, and nothing more to do.
|
||||
if (active_layer == codec_.numberOfSimulcastStreams) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Always allocate enough bitrate for the minimum bitrate of the first
|
||||
// active layer. Suspending below min bitrate is controlled outside the
|
||||
// codec implementation and is not overridden by this.
|
||||
DataRate min_rate = DataRate::KilobitsPerSec(
|
||||
codec_.simulcastStream[layer_index[active_layer]].minBitrate);
|
||||
left_in_total_allocation = std::max(left_in_total_allocation, min_rate);
|
||||
left_in_stable_allocation = std::max(left_in_stable_allocation, min_rate);
|
||||
|
||||
// Begin by allocating bitrate to simulcast streams, putting all bitrate in
|
||||
// temporal layer 0. We'll then distribute this bitrate, across potential
|
||||
// temporal layers, when stream allocation is done.
|
||||
|
||||
bool first_allocation = false;
|
||||
if (stream_enabled_.empty()) {
|
||||
// First time allocating, this means we should not include hysteresis in
|
||||
// case this is a reconfiguration of an existing enabled stream.
|
||||
first_allocation = true;
|
||||
stream_enabled_.resize(codec_.numberOfSimulcastStreams, false);
|
||||
}
|
||||
|
||||
size_t top_active_layer = active_layer;
|
||||
// Allocate up to the target bitrate for each active simulcast layer.
|
||||
for (; active_layer < codec_.numberOfSimulcastStreams; ++active_layer) {
|
||||
const SimulcastStream& stream =
|
||||
codec_.simulcastStream[layer_index[active_layer]];
|
||||
if (!stream.active) {
|
||||
stream_enabled_[layer_index[active_layer]] = false;
|
||||
continue;
|
||||
}
|
||||
// If we can't allocate to the current layer we can't allocate to higher
|
||||
// layers because they require a higher minimum bitrate.
|
||||
DataRate min_bitrate = DataRate::KilobitsPerSec(stream.minBitrate);
|
||||
DataRate target_bitrate = DataRate::KilobitsPerSec(stream.targetBitrate);
|
||||
double hysteresis_factor =
|
||||
codec_.mode == VideoCodecMode::kRealtimeVideo
|
||||
? stable_rate_settings_.GetVideoHysteresisFactor()
|
||||
: stable_rate_settings_.GetScreenshareHysteresisFactor();
|
||||
if (!first_allocation && !stream_enabled_[layer_index[active_layer]]) {
|
||||
min_bitrate = std::min(hysteresis_factor * min_bitrate, target_bitrate);
|
||||
}
|
||||
if (left_in_stable_allocation < min_bitrate) {
|
||||
allocated_bitrates->set_bw_limited(true);
|
||||
break;
|
||||
}
|
||||
|
||||
// We are allocating to this layer so it is the current active allocation.
|
||||
top_active_layer = layer_index[active_layer];
|
||||
stream_enabled_[layer_index[active_layer]] = true;
|
||||
DataRate layer_rate = std::min(left_in_total_allocation, target_bitrate);
|
||||
allocated_bitrates->SetBitrate(layer_index[active_layer], 0,
|
||||
layer_rate.bps());
|
||||
left_in_total_allocation -= layer_rate;
|
||||
left_in_stable_allocation -=
|
||||
std::min(left_in_stable_allocation, target_bitrate);
|
||||
}
|
||||
|
||||
// All layers above this one are not active.
|
||||
for (; active_layer < codec_.numberOfSimulcastStreams; ++active_layer) {
|
||||
stream_enabled_[layer_index[active_layer]] = false;
|
||||
}
|
||||
|
||||
// Next, try allocate remaining bitrate, up to max bitrate, in top active
|
||||
// stream.
|
||||
// TODO(sprang): Allocate up to max bitrate for all layers once we have a
|
||||
// better idea of possible performance implications.
|
||||
if (left_in_total_allocation > DataRate::Zero()) {
|
||||
const SimulcastStream& stream = codec_.simulcastStream[top_active_layer];
|
||||
DataRate initial_layer_rate = DataRate::BitsPerSec(
|
||||
allocated_bitrates->GetSpatialLayerSum(top_active_layer));
|
||||
DataRate additional_allocation = std::min(
|
||||
left_in_total_allocation,
|
||||
DataRate::KilobitsPerSec(stream.maxBitrate) - initial_layer_rate);
|
||||
allocated_bitrates->SetBitrate(
|
||||
top_active_layer, 0,
|
||||
(initial_layer_rate + additional_allocation).bps());
|
||||
}
|
||||
}
|
||||
|
||||
void SimulcastRateAllocator::DistributeAllocationToTemporalLayers(
|
||||
VideoBitrateAllocation* allocated_bitrates_bps) const {
|
||||
const int num_spatial_streams =
|
||||
std::max(1, static_cast<int>(codec_.numberOfSimulcastStreams));
|
||||
|
||||
// Finally, distribute the bitrate for the simulcast streams across the
|
||||
// available temporal layers.
|
||||
for (int simulcast_id = 0; simulcast_id < num_spatial_streams;
|
||||
++simulcast_id) {
|
||||
uint32_t target_bitrate_kbps =
|
||||
allocated_bitrates_bps->GetBitrate(simulcast_id, 0) / 1000;
|
||||
if (target_bitrate_kbps == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t expected_allocated_bitrate_kbps = target_bitrate_kbps;
|
||||
RTC_DCHECK_EQ(
|
||||
target_bitrate_kbps,
|
||||
allocated_bitrates_bps->GetSpatialLayerSum(simulcast_id) / 1000);
|
||||
const int num_temporal_streams = NumTemporalStreams(simulcast_id);
|
||||
uint32_t max_bitrate_kbps;
|
||||
// Legacy temporal-layered only screenshare, or simulcast screenshare
|
||||
// with legacy mode for simulcast stream 0.
|
||||
if (codec_.mode == VideoCodecMode::kScreensharing &&
|
||||
legacy_conference_mode_ && simulcast_id == 0) {
|
||||
// TODO(holmer): This is a "temporary" hack for screensharing, where we
|
||||
// interpret the startBitrate as the encoder target bitrate. This is
|
||||
// to allow for a different max bitrate, so if the codec can't meet
|
||||
// the target we still allow it to overshoot up to the max before dropping
|
||||
// frames. This hack should be improved.
|
||||
max_bitrate_kbps =
|
||||
std::min(kLegacyScreenshareTl1BitrateKbps, target_bitrate_kbps);
|
||||
target_bitrate_kbps =
|
||||
std::min(kLegacyScreenshareTl0BitrateKbps, target_bitrate_kbps);
|
||||
} else if (num_spatial_streams == 1) {
|
||||
max_bitrate_kbps = codec_.maxBitrate;
|
||||
} else {
|
||||
max_bitrate_kbps = codec_.simulcastStream[simulcast_id].maxBitrate;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> tl_allocation;
|
||||
if (num_temporal_streams == 1) {
|
||||
tl_allocation.push_back(target_bitrate_kbps);
|
||||
} else {
|
||||
if (codec_.mode == VideoCodecMode::kScreensharing &&
|
||||
legacy_conference_mode_ && simulcast_id == 0) {
|
||||
tl_allocation = ScreenshareTemporalLayerAllocation(
|
||||
target_bitrate_kbps, max_bitrate_kbps, simulcast_id);
|
||||
} else {
|
||||
tl_allocation = DefaultTemporalLayerAllocation(
|
||||
target_bitrate_kbps, max_bitrate_kbps, simulcast_id);
|
||||
}
|
||||
}
|
||||
RTC_DCHECK_GT(tl_allocation.size(), 0);
|
||||
RTC_DCHECK_LE(tl_allocation.size(), num_temporal_streams);
|
||||
|
||||
uint64_t tl_allocation_sum_kbps = 0;
|
||||
for (size_t tl_index = 0; tl_index < tl_allocation.size(); ++tl_index) {
|
||||
uint32_t layer_rate_kbps = tl_allocation[tl_index];
|
||||
if (layer_rate_kbps > 0) {
|
||||
allocated_bitrates_bps->SetBitrate(simulcast_id, tl_index,
|
||||
layer_rate_kbps * 1000);
|
||||
}
|
||||
tl_allocation_sum_kbps += layer_rate_kbps;
|
||||
}
|
||||
RTC_DCHECK_LE(tl_allocation_sum_kbps, expected_allocated_bitrate_kbps);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint32_t> SimulcastRateAllocator::DefaultTemporalLayerAllocation(
|
||||
int bitrate_kbps,
|
||||
int max_bitrate_kbps,
|
||||
int simulcast_id) const {
|
||||
const size_t num_temporal_layers = NumTemporalStreams(simulcast_id);
|
||||
std::vector<uint32_t> bitrates;
|
||||
for (size_t i = 0; i < num_temporal_layers; ++i) {
|
||||
float layer_bitrate =
|
||||
bitrate_kbps *
|
||||
GetTemporalRateAllocation(
|
||||
num_temporal_layers, i,
|
||||
rate_control_settings_.Vp8BaseHeavyTl3RateAllocation());
|
||||
bitrates.push_back(static_cast<uint32_t>(layer_bitrate + 0.5));
|
||||
}
|
||||
|
||||
// Allocation table is of aggregates, transform to individual rates.
|
||||
uint32_t sum = 0;
|
||||
for (size_t i = 0; i < num_temporal_layers; ++i) {
|
||||
uint32_t layer_bitrate = bitrates[i];
|
||||
RTC_DCHECK_LE(sum, bitrates[i]);
|
||||
bitrates[i] -= sum;
|
||||
sum = layer_bitrate;
|
||||
|
||||
if (sum >= static_cast<uint32_t>(bitrate_kbps)) {
|
||||
// Sum adds up; any subsequent layers will be 0.
|
||||
bitrates.resize(i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return bitrates;
|
||||
}
|
||||
|
||||
std::vector<uint32_t>
|
||||
SimulcastRateAllocator::ScreenshareTemporalLayerAllocation(
|
||||
int bitrate_kbps,
|
||||
int max_bitrate_kbps,
|
||||
int simulcast_id) const {
|
||||
if (simulcast_id > 0) {
|
||||
return DefaultTemporalLayerAllocation(bitrate_kbps, max_bitrate_kbps,
|
||||
simulcast_id);
|
||||
}
|
||||
std::vector<uint32_t> allocation;
|
||||
allocation.push_back(bitrate_kbps);
|
||||
if (max_bitrate_kbps > bitrate_kbps)
|
||||
allocation.push_back(max_bitrate_kbps - bitrate_kbps);
|
||||
return allocation;
|
||||
}
|
||||
|
||||
const VideoCodec& webrtc::SimulcastRateAllocator::GetCodec() const {
|
||||
return codec_;
|
||||
}
|
||||
|
||||
int SimulcastRateAllocator::NumTemporalStreams(size_t simulcast_id) const {
|
||||
return std::max<uint8_t>(
|
||||
1,
|
||||
codec_.codecType == kVideoCodecVP8 && codec_.numberOfSimulcastStreams == 0
|
||||
? codec_.VP8().numberOfTemporalLayers
|
||||
: codec_.simulcastStream[simulcast_id].numberOfTemporalLayers);
|
||||
}
|
||||
|
||||
void SimulcastRateAllocator::SetLegacyConferenceMode(bool enabled) {
|
||||
legacy_conference_mode_ = enabled;
|
||||
}
|
||||
|
||||
} // namespace webrtc
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2016 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.
|
||||
*/
|
||||
|
||||
#ifndef MODULES_VIDEO_CODING_UTILITY_SIMULCAST_RATE_ALLOCATOR_H_
|
||||
#define MODULES_VIDEO_CODING_UTILITY_SIMULCAST_RATE_ALLOCATOR_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "api/video/video_bitrate_allocation.h"
|
||||
#include "api/video/video_bitrate_allocator.h"
|
||||
#include "api/video_codecs/video_codec.h"
|
||||
#include "rtc_base/experiments/rate_control_settings.h"
|
||||
#include "rtc_base/experiments/stable_target_rate_experiment.h"
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
class SimulcastRateAllocator : public VideoBitrateAllocator {
|
||||
public:
|
||||
explicit SimulcastRateAllocator(const VideoCodec& codec);
|
||||
~SimulcastRateAllocator() override;
|
||||
|
||||
SimulcastRateAllocator(const SimulcastRateAllocator&) = delete;
|
||||
SimulcastRateAllocator& operator=(const SimulcastRateAllocator&) = delete;
|
||||
|
||||
VideoBitrateAllocation Allocate(
|
||||
VideoBitrateAllocationParameters parameters) override;
|
||||
const VideoCodec& GetCodec() const;
|
||||
|
||||
static float GetTemporalRateAllocation(int num_layers,
|
||||
int temporal_id,
|
||||
bool base_heavy_tl3_alloc);
|
||||
|
||||
void SetLegacyConferenceMode(bool mode) override;
|
||||
|
||||
private:
|
||||
void DistributeAllocationToSimulcastLayers(
|
||||
DataRate total_bitrate,
|
||||
DataRate stable_bitrate,
|
||||
VideoBitrateAllocation* allocated_bitrates);
|
||||
void DistributeAllocationToTemporalLayers(
|
||||
VideoBitrateAllocation* allocated_bitrates) const;
|
||||
std::vector<uint32_t> DefaultTemporalLayerAllocation(int bitrate_kbps,
|
||||
int max_bitrate_kbps,
|
||||
int simulcast_id) const;
|
||||
std::vector<uint32_t> ScreenshareTemporalLayerAllocation(
|
||||
int bitrate_kbps,
|
||||
int max_bitrate_kbps,
|
||||
int simulcast_id) const;
|
||||
int NumTemporalStreams(size_t simulcast_id) const;
|
||||
|
||||
const VideoCodec codec_;
|
||||
const StableTargetRateExperiment stable_rate_settings_;
|
||||
const RateControlSettings rate_control_settings_;
|
||||
std::vector<bool> stream_enabled_;
|
||||
bool legacy_conference_mode_;
|
||||
};
|
||||
|
||||
} // namespace webrtc
|
||||
|
||||
#endif // MODULES_VIDEO_CODING_UTILITY_SIMULCAST_RATE_ALLOCATOR_H_
|
||||
|
|
@ -0,0 +1,970 @@
|
|||
/*
|
||||
* Copyright (c) 2014 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 "modules/video_coding/utility/simulcast_test_fixture_impl.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "api/environment/environment.h"
|
||||
#include "api/environment/environment_factory.h"
|
||||
#include "api/video/encoded_image.h"
|
||||
#include "api/video_codecs/sdp_video_format.h"
|
||||
#include "api/video_codecs/video_encoder.h"
|
||||
#include "common_video/libyuv/include/webrtc_libyuv.h"
|
||||
#include "modules/video_coding/include/video_codec_interface.h"
|
||||
#include "modules/video_coding/include/video_coding_defines.h"
|
||||
#include "rtc_base/checks.h"
|
||||
#include "test/gtest.h"
|
||||
|
||||
using ::testing::_;
|
||||
using ::testing::AllOf;
|
||||
using ::testing::Field;
|
||||
using ::testing::Return;
|
||||
|
||||
namespace webrtc {
|
||||
namespace test {
|
||||
|
||||
namespace {
|
||||
|
||||
const int kDefaultWidth = 1280;
|
||||
const int kDefaultHeight = 720;
|
||||
const int kNumberOfSimulcastStreams = 3;
|
||||
const int kColorY = 66;
|
||||
const int kColorU = 22;
|
||||
const int kColorV = 33;
|
||||
const int kMaxBitrates[kNumberOfSimulcastStreams] = {150, 600, 1200};
|
||||
const int kMinBitrates[kNumberOfSimulcastStreams] = {50, 150, 600};
|
||||
const int kTargetBitrates[kNumberOfSimulcastStreams] = {100, 450, 1000};
|
||||
const float kMaxFramerates[kNumberOfSimulcastStreams] = {30, 30, 30};
|
||||
const int kScaleResolutionDownBy[kNumberOfSimulcastStreams] = {4, 2, 1};
|
||||
const int kDefaultTemporalLayerProfile[3] = {3, 3, 3};
|
||||
const int kNoTemporalLayerProfile[3] = {0, 0, 0};
|
||||
|
||||
const VideoEncoder::Capabilities kCapabilities(false);
|
||||
const VideoEncoder::Settings kSettings(kCapabilities, 1, 1200);
|
||||
|
||||
template <typename T>
|
||||
void SetExpectedValues3(T value0, T value1, T value2, T* expected_values) {
|
||||
expected_values[0] = value0;
|
||||
expected_values[1] = value1;
|
||||
expected_values[2] = value2;
|
||||
}
|
||||
|
||||
enum PlaneType {
|
||||
kYPlane = 0,
|
||||
kUPlane = 1,
|
||||
kVPlane = 2,
|
||||
kNumOfPlanes = 3,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class SimulcastTestFixtureImpl::TestEncodedImageCallback
|
||||
: public EncodedImageCallback {
|
||||
public:
|
||||
TestEncodedImageCallback() {
|
||||
memset(temporal_layer_, -1, sizeof(temporal_layer_));
|
||||
memset(layer_sync_, false, sizeof(layer_sync_));
|
||||
}
|
||||
|
||||
Result OnEncodedImage(const EncodedImage& encoded_image,
|
||||
const CodecSpecificInfo* codec_specific_info) override {
|
||||
bool is_vp8 = (codec_specific_info->codecType == kVideoCodecVP8);
|
||||
bool is_h264 = (codec_specific_info->codecType == kVideoCodecH264);
|
||||
// Only store the base layer.
|
||||
if (encoded_image.SimulcastIndex().value_or(0) == 0) {
|
||||
if (encoded_image._frameType == VideoFrameType::kVideoFrameKey) {
|
||||
encoded_key_frame_.SetEncodedData(EncodedImageBuffer::Create(
|
||||
encoded_image.data(), encoded_image.size()));
|
||||
encoded_key_frame_._frameType = VideoFrameType::kVideoFrameKey;
|
||||
} else {
|
||||
encoded_frame_.SetEncodedData(EncodedImageBuffer::Create(
|
||||
encoded_image.data(), encoded_image.size()));
|
||||
}
|
||||
}
|
||||
if (is_vp8) {
|
||||
layer_sync_[encoded_image.SimulcastIndex().value_or(0)] =
|
||||
codec_specific_info->codecSpecific.VP8.layerSync;
|
||||
temporal_layer_[encoded_image.SimulcastIndex().value_or(0)] =
|
||||
codec_specific_info->codecSpecific.VP8.temporalIdx;
|
||||
} else if (is_h264) {
|
||||
layer_sync_[encoded_image.SimulcastIndex().value_or(0)] =
|
||||
codec_specific_info->codecSpecific.H264.base_layer_sync;
|
||||
temporal_layer_[encoded_image.SimulcastIndex().value_or(0)] =
|
||||
codec_specific_info->codecSpecific.H264.temporal_idx;
|
||||
}
|
||||
return Result(Result::OK, encoded_image.RtpTimestamp());
|
||||
}
|
||||
// This method only makes sense for VP8.
|
||||
void GetLastEncodedFrameInfo(int* temporal_layer,
|
||||
bool* layer_sync,
|
||||
int stream) {
|
||||
*temporal_layer = temporal_layer_[stream];
|
||||
*layer_sync = layer_sync_[stream];
|
||||
}
|
||||
void GetLastEncodedKeyFrame(EncodedImage* encoded_key_frame) {
|
||||
*encoded_key_frame = encoded_key_frame_;
|
||||
}
|
||||
void GetLastEncodedFrame(EncodedImage* encoded_frame) {
|
||||
*encoded_frame = encoded_frame_;
|
||||
}
|
||||
|
||||
private:
|
||||
EncodedImage encoded_key_frame_;
|
||||
EncodedImage encoded_frame_;
|
||||
int temporal_layer_[kNumberOfSimulcastStreams];
|
||||
bool layer_sync_[kNumberOfSimulcastStreams];
|
||||
};
|
||||
|
||||
class SimulcastTestFixtureImpl::TestDecodedImageCallback
|
||||
: public DecodedImageCallback {
|
||||
public:
|
||||
TestDecodedImageCallback() : decoded_frames_(0) {}
|
||||
int32_t Decoded(VideoFrame& decoded_image) override {
|
||||
rtc::scoped_refptr<I420BufferInterface> i420_buffer =
|
||||
decoded_image.video_frame_buffer()->ToI420();
|
||||
for (int i = 0; i < decoded_image.width(); ++i) {
|
||||
EXPECT_NEAR(kColorY, i420_buffer->DataY()[i], 1);
|
||||
}
|
||||
|
||||
// TODO(mikhal): Verify the difference between U,V and the original.
|
||||
for (int i = 0; i < i420_buffer->ChromaWidth(); ++i) {
|
||||
EXPECT_NEAR(kColorU, i420_buffer->DataU()[i], 4);
|
||||
EXPECT_NEAR(kColorV, i420_buffer->DataV()[i], 4);
|
||||
}
|
||||
decoded_frames_++;
|
||||
return 0;
|
||||
}
|
||||
int32_t Decoded(VideoFrame& decoded_image, int64_t decode_time_ms) override {
|
||||
RTC_DCHECK_NOTREACHED();
|
||||
return -1;
|
||||
}
|
||||
void Decoded(VideoFrame& decoded_image,
|
||||
absl::optional<int32_t> decode_time_ms,
|
||||
absl::optional<uint8_t> qp) override {
|
||||
Decoded(decoded_image);
|
||||
}
|
||||
int DecodedFrames() { return decoded_frames_; }
|
||||
|
||||
private:
|
||||
int decoded_frames_;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
void SetPlane(uint8_t* data, uint8_t value, int width, int height, int stride) {
|
||||
for (int i = 0; i < height; i++, data += stride) {
|
||||
// Setting allocated area to zero - setting only image size to
|
||||
// requested values - will make it easier to distinguish between image
|
||||
// size and frame size (accounting for stride).
|
||||
memset(data, value, width);
|
||||
memset(data + width, 0, stride - width);
|
||||
}
|
||||
}
|
||||
|
||||
// Fills in an I420Buffer from `plane_colors`.
|
||||
void CreateImage(const rtc::scoped_refptr<I420Buffer>& buffer,
|
||||
int plane_colors[kNumOfPlanes]) {
|
||||
SetPlane(buffer->MutableDataY(), plane_colors[0], buffer->width(),
|
||||
buffer->height(), buffer->StrideY());
|
||||
|
||||
SetPlane(buffer->MutableDataU(), plane_colors[1], buffer->ChromaWidth(),
|
||||
buffer->ChromaHeight(), buffer->StrideU());
|
||||
|
||||
SetPlane(buffer->MutableDataV(), plane_colors[2], buffer->ChromaWidth(),
|
||||
buffer->ChromaHeight(), buffer->StrideV());
|
||||
}
|
||||
|
||||
void ConfigureStream(int width,
|
||||
int height,
|
||||
int max_bitrate,
|
||||
int min_bitrate,
|
||||
int target_bitrate,
|
||||
float max_framerate,
|
||||
SimulcastStream* stream,
|
||||
int num_temporal_layers) {
|
||||
RTC_DCHECK(stream);
|
||||
stream->width = width;
|
||||
stream->height = height;
|
||||
stream->maxBitrate = max_bitrate;
|
||||
stream->minBitrate = min_bitrate;
|
||||
stream->targetBitrate = target_bitrate;
|
||||
stream->maxFramerate = max_framerate;
|
||||
if (num_temporal_layers >= 0) {
|
||||
stream->numberOfTemporalLayers = num_temporal_layers;
|
||||
}
|
||||
stream->qpMax = 45;
|
||||
stream->active = true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SimulcastTestFixtureImpl::DefaultSettings(
|
||||
VideoCodec* settings,
|
||||
const int* temporal_layer_profile,
|
||||
VideoCodecType codec_type,
|
||||
bool reverse_layer_order) {
|
||||
RTC_CHECK(settings);
|
||||
*settings = {};
|
||||
settings->codecType = codec_type;
|
||||
settings->startBitrate = 300;
|
||||
settings->minBitrate = 30;
|
||||
settings->maxBitrate = 0;
|
||||
settings->maxFramerate = 30;
|
||||
settings->width = kDefaultWidth;
|
||||
settings->height = kDefaultHeight;
|
||||
settings->numberOfSimulcastStreams = kNumberOfSimulcastStreams;
|
||||
settings->active = true;
|
||||
ASSERT_EQ(3, kNumberOfSimulcastStreams);
|
||||
int layer_order[3] = {0, 1, 2};
|
||||
if (reverse_layer_order) {
|
||||
layer_order[0] = 2;
|
||||
layer_order[2] = 0;
|
||||
}
|
||||
settings->timing_frame_thresholds = {kDefaultTimingFramesDelayMs,
|
||||
kDefaultOutlierFrameSizePercent};
|
||||
ConfigureStream(kDefaultWidth / 4, kDefaultHeight / 4, kMaxBitrates[0],
|
||||
kMinBitrates[0], kTargetBitrates[0], kMaxFramerates[0],
|
||||
&settings->simulcastStream[layer_order[0]],
|
||||
temporal_layer_profile[0]);
|
||||
ConfigureStream(kDefaultWidth / 2, kDefaultHeight / 2, kMaxBitrates[1],
|
||||
kMinBitrates[1], kTargetBitrates[1], kMaxFramerates[1],
|
||||
&settings->simulcastStream[layer_order[1]],
|
||||
temporal_layer_profile[1]);
|
||||
ConfigureStream(kDefaultWidth, kDefaultHeight, kMaxBitrates[2],
|
||||
kMinBitrates[2], kTargetBitrates[2], kMaxFramerates[2],
|
||||
&settings->simulcastStream[layer_order[2]],
|
||||
temporal_layer_profile[2]);
|
||||
settings->SetFrameDropEnabled(true);
|
||||
if (codec_type == kVideoCodecVP8) {
|
||||
settings->VP8()->denoisingOn = true;
|
||||
settings->VP8()->automaticResizeOn = false;
|
||||
settings->VP8()->keyFrameInterval = 3000;
|
||||
} else {
|
||||
settings->H264()->keyFrameInterval = 3000;
|
||||
}
|
||||
}
|
||||
|
||||
SimulcastTestFixtureImpl::SimulcastTestFixtureImpl(
|
||||
std::unique_ptr<VideoEncoderFactory> encoder_factory,
|
||||
std::unique_ptr<VideoDecoderFactory> decoder_factory,
|
||||
SdpVideoFormat video_format)
|
||||
: codec_type_(PayloadStringToCodecType(video_format.name)) {
|
||||
Environment env = CreateEnvironment();
|
||||
encoder_ = encoder_factory->CreateVideoEncoder(video_format);
|
||||
decoder_ = decoder_factory->Create(env, video_format);
|
||||
SetUpCodec((codec_type_ == kVideoCodecVP8 || codec_type_ == kVideoCodecH264)
|
||||
? kDefaultTemporalLayerProfile
|
||||
: kNoTemporalLayerProfile);
|
||||
}
|
||||
|
||||
SimulcastTestFixtureImpl::~SimulcastTestFixtureImpl() {
|
||||
encoder_->Release();
|
||||
decoder_->Release();
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::SetUpCodec(const int* temporal_layer_profile) {
|
||||
encoder_->RegisterEncodeCompleteCallback(&encoder_callback_);
|
||||
decoder_->RegisterDecodeCompleteCallback(&decoder_callback_);
|
||||
DefaultSettings(&settings_, temporal_layer_profile, codec_type_);
|
||||
SetUpRateAllocator();
|
||||
EXPECT_EQ(0, encoder_->InitEncode(&settings_, kSettings));
|
||||
VideoDecoder::Settings decoder_settings;
|
||||
decoder_settings.set_max_render_resolution({kDefaultWidth, kDefaultHeight});
|
||||
decoder_settings.set_codec_type(codec_type_);
|
||||
EXPECT_TRUE(decoder_->Configure(decoder_settings));
|
||||
input_buffer_ = I420Buffer::Create(kDefaultWidth, kDefaultHeight);
|
||||
input_buffer_->InitializeData();
|
||||
input_frame_ = std::make_unique<webrtc::VideoFrame>(
|
||||
webrtc::VideoFrame::Builder()
|
||||
.set_video_frame_buffer(input_buffer_)
|
||||
.set_rotation(webrtc::kVideoRotation_0)
|
||||
.set_timestamp_us(0)
|
||||
.build());
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::SetUpRateAllocator() {
|
||||
rate_allocator_.reset(new SimulcastRateAllocator(settings_));
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::SetRates(uint32_t bitrate_kbps, uint32_t fps) {
|
||||
encoder_->SetRates(VideoEncoder::RateControlParameters(
|
||||
rate_allocator_->Allocate(
|
||||
VideoBitrateAllocationParameters(bitrate_kbps * 1000, fps)),
|
||||
static_cast<double>(fps)));
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::RunActiveStreamsTest(
|
||||
const std::vector<bool> active_streams) {
|
||||
std::vector<VideoFrameType> frame_types(kNumberOfSimulcastStreams,
|
||||
VideoFrameType::kVideoFrameDelta);
|
||||
UpdateActiveStreams(active_streams);
|
||||
// Set sufficient bitrate for all streams so we can test active without
|
||||
// bitrate being an issue.
|
||||
SetRates(kMaxBitrates[0] + kMaxBitrates[1] + kMaxBitrates[2], 30);
|
||||
|
||||
ExpectStreams(VideoFrameType::kVideoFrameKey, active_streams);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
ExpectStreams(VideoFrameType::kVideoFrameDelta, active_streams);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::UpdateActiveStreams(
|
||||
const std::vector<bool> active_streams) {
|
||||
ASSERT_EQ(static_cast<int>(active_streams.size()), kNumberOfSimulcastStreams);
|
||||
for (size_t i = 0; i < active_streams.size(); ++i) {
|
||||
settings_.simulcastStream[i].active = active_streams[i];
|
||||
}
|
||||
// Re initialize the allocator and encoder with the new settings.
|
||||
// TODO(bugs.webrtc.org/8807): Currently, we do a full "hard"
|
||||
// reconfiguration of the allocator and encoder. When the video bitrate
|
||||
// allocator has support for updating active streams without a
|
||||
// reinitialization, we can just call that here instead.
|
||||
SetUpRateAllocator();
|
||||
EXPECT_EQ(0, encoder_->InitEncode(&settings_, kSettings));
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::ExpectStream(VideoFrameType frame_type,
|
||||
int scaleResolutionDownBy) {
|
||||
EXPECT_CALL(
|
||||
encoder_callback_,
|
||||
OnEncodedImage(AllOf(Field(&EncodedImage::_frameType, frame_type),
|
||||
Field(&EncodedImage::_encodedWidth,
|
||||
kDefaultWidth / scaleResolutionDownBy),
|
||||
Field(&EncodedImage::_encodedHeight,
|
||||
kDefaultHeight / scaleResolutionDownBy)),
|
||||
_))
|
||||
.Times(1)
|
||||
.WillRepeatedly(Return(
|
||||
EncodedImageCallback::Result(EncodedImageCallback::Result::OK, 0)));
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::ExpectStreams(
|
||||
VideoFrameType frame_type,
|
||||
const std::vector<bool> expected_streams_active) {
|
||||
ASSERT_EQ(static_cast<int>(expected_streams_active.size()),
|
||||
kNumberOfSimulcastStreams);
|
||||
for (size_t i = 0; i < kNumberOfSimulcastStreams; i++) {
|
||||
if (expected_streams_active[i]) {
|
||||
ExpectStream(frame_type, kScaleResolutionDownBy[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::ExpectStreams(VideoFrameType frame_type,
|
||||
int expected_video_streams) {
|
||||
ASSERT_GE(expected_video_streams, 0);
|
||||
ASSERT_LE(expected_video_streams, kNumberOfSimulcastStreams);
|
||||
std::vector<bool> expected_streams_active(kNumberOfSimulcastStreams, false);
|
||||
for (int i = 0; i < expected_video_streams; ++i) {
|
||||
expected_streams_active[i] = true;
|
||||
}
|
||||
ExpectStreams(frame_type, expected_streams_active);
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::VerifyTemporalIdxAndSyncForAllSpatialLayers(
|
||||
TestEncodedImageCallback* encoder_callback,
|
||||
const int* expected_temporal_idx,
|
||||
const bool* expected_layer_sync,
|
||||
int num_spatial_layers) {
|
||||
int temporal_layer = -1;
|
||||
bool layer_sync = false;
|
||||
for (int i = 0; i < num_spatial_layers; i++) {
|
||||
encoder_callback->GetLastEncodedFrameInfo(&temporal_layer, &layer_sync, i);
|
||||
EXPECT_EQ(expected_temporal_idx[i], temporal_layer);
|
||||
EXPECT_EQ(expected_layer_sync[i], layer_sync);
|
||||
}
|
||||
}
|
||||
|
||||
// For some codecs (VP8) expect all active streams to generate a key frame even
|
||||
// though a key frame was only requested for some of them.
|
||||
void SimulcastTestFixtureImpl::TestKeyFrameRequestsOnAllStreams() {
|
||||
SetRates(kMaxBitrates[2], 30); // To get all three streams.
|
||||
std::vector<VideoFrameType> frame_types(kNumberOfSimulcastStreams,
|
||||
VideoFrameType::kVideoFrameDelta);
|
||||
ExpectStreams(VideoFrameType::kVideoFrameKey, kNumberOfSimulcastStreams);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
ExpectStreams(VideoFrameType::kVideoFrameDelta, kNumberOfSimulcastStreams);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
frame_types[0] = VideoFrameType::kVideoFrameKey;
|
||||
ExpectStreams(VideoFrameType::kVideoFrameKey, kNumberOfSimulcastStreams);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
std::fill(frame_types.begin(), frame_types.end(),
|
||||
VideoFrameType::kVideoFrameDelta);
|
||||
frame_types[1] = VideoFrameType::kVideoFrameKey;
|
||||
ExpectStreams(VideoFrameType::kVideoFrameKey, kNumberOfSimulcastStreams);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
std::fill(frame_types.begin(), frame_types.end(),
|
||||
VideoFrameType::kVideoFrameDelta);
|
||||
frame_types[2] = VideoFrameType::kVideoFrameKey;
|
||||
ExpectStreams(VideoFrameType::kVideoFrameKey, kNumberOfSimulcastStreams);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
std::fill(frame_types.begin(), frame_types.end(),
|
||||
VideoFrameType::kVideoFrameDelta);
|
||||
ExpectStreams(VideoFrameType::kVideoFrameDelta, kNumberOfSimulcastStreams);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
}
|
||||
|
||||
// For some codecs (H264) expect only particular active streams to generate a
|
||||
// key frame when a key frame was only requested for some of them.
|
||||
void SimulcastTestFixtureImpl::TestKeyFrameRequestsOnSpecificStreams() {
|
||||
SetRates(kMaxBitrates[2], 30); // To get all three streams.
|
||||
std::vector<VideoFrameType> frame_types(kNumberOfSimulcastStreams,
|
||||
VideoFrameType::kVideoFrameDelta);
|
||||
ExpectStreams(VideoFrameType::kVideoFrameKey, kNumberOfSimulcastStreams);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
ExpectStreams(VideoFrameType::kVideoFrameDelta, kNumberOfSimulcastStreams);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
frame_types[0] = VideoFrameType::kVideoFrameKey;
|
||||
ExpectStream(VideoFrameType::kVideoFrameKey, kScaleResolutionDownBy[0]);
|
||||
ExpectStream(VideoFrameType::kVideoFrameDelta, kScaleResolutionDownBy[1]);
|
||||
ExpectStream(VideoFrameType::kVideoFrameDelta, kScaleResolutionDownBy[2]);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
std::fill(frame_types.begin(), frame_types.end(),
|
||||
VideoFrameType::kVideoFrameDelta);
|
||||
frame_types[1] = VideoFrameType::kVideoFrameKey;
|
||||
ExpectStream(VideoFrameType::kVideoFrameDelta, kScaleResolutionDownBy[0]);
|
||||
ExpectStream(VideoFrameType::kVideoFrameKey, kScaleResolutionDownBy[1]);
|
||||
ExpectStream(VideoFrameType::kVideoFrameDelta, kScaleResolutionDownBy[2]);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
std::fill(frame_types.begin(), frame_types.end(),
|
||||
VideoFrameType::kVideoFrameDelta);
|
||||
frame_types[2] = VideoFrameType::kVideoFrameKey;
|
||||
ExpectStream(VideoFrameType::kVideoFrameDelta, kScaleResolutionDownBy[0]);
|
||||
ExpectStream(VideoFrameType::kVideoFrameDelta, kScaleResolutionDownBy[1]);
|
||||
ExpectStream(VideoFrameType::kVideoFrameKey, kScaleResolutionDownBy[2]);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
std::fill(frame_types.begin(), frame_types.end(),
|
||||
VideoFrameType::kVideoFrameDelta);
|
||||
frame_types[0] = VideoFrameType::kVideoFrameKey;
|
||||
frame_types[2] = VideoFrameType::kVideoFrameKey;
|
||||
ExpectStream(VideoFrameType::kVideoFrameKey, kScaleResolutionDownBy[0]);
|
||||
ExpectStream(VideoFrameType::kVideoFrameDelta, kScaleResolutionDownBy[1]);
|
||||
ExpectStream(VideoFrameType::kVideoFrameKey, kScaleResolutionDownBy[2]);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
std::fill(frame_types.begin(), frame_types.end(),
|
||||
VideoFrameType::kVideoFrameKey);
|
||||
ExpectStream(VideoFrameType::kVideoFrameKey, kScaleResolutionDownBy[0]);
|
||||
ExpectStream(VideoFrameType::kVideoFrameKey, kScaleResolutionDownBy[1]);
|
||||
ExpectStream(VideoFrameType::kVideoFrameKey, kScaleResolutionDownBy[2]);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
std::fill(frame_types.begin(), frame_types.end(),
|
||||
VideoFrameType::kVideoFrameDelta);
|
||||
ExpectStreams(VideoFrameType::kVideoFrameDelta, kNumberOfSimulcastStreams);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::TestPaddingAllStreams() {
|
||||
// We should always encode the base layer.
|
||||
SetRates(kMinBitrates[0] - 1, 30);
|
||||
std::vector<VideoFrameType> frame_types(kNumberOfSimulcastStreams,
|
||||
VideoFrameType::kVideoFrameDelta);
|
||||
ExpectStreams(VideoFrameType::kVideoFrameKey, 1);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
ExpectStreams(VideoFrameType::kVideoFrameDelta, 1);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::TestPaddingTwoStreams() {
|
||||
// We have just enough to get only the first stream and padding for two.
|
||||
SetRates(kMinBitrates[0], 30);
|
||||
std::vector<VideoFrameType> frame_types(kNumberOfSimulcastStreams,
|
||||
VideoFrameType::kVideoFrameDelta);
|
||||
ExpectStreams(VideoFrameType::kVideoFrameKey, 1);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
ExpectStreams(VideoFrameType::kVideoFrameDelta, 1);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::TestPaddingTwoStreamsOneMaxedOut() {
|
||||
// We are just below limit of sending second stream, so we should get
|
||||
// the first stream maxed out (at `maxBitrate`), and padding for two.
|
||||
SetRates(kTargetBitrates[0] + kMinBitrates[1] - 1, 30);
|
||||
std::vector<VideoFrameType> frame_types(kNumberOfSimulcastStreams,
|
||||
VideoFrameType::kVideoFrameDelta);
|
||||
ExpectStreams(VideoFrameType::kVideoFrameKey, 1);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
ExpectStreams(VideoFrameType::kVideoFrameDelta, 1);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::TestPaddingOneStream() {
|
||||
// We have just enough to send two streams, so padding for one stream.
|
||||
SetRates(kTargetBitrates[0] + kMinBitrates[1], 30);
|
||||
std::vector<VideoFrameType> frame_types(kNumberOfSimulcastStreams,
|
||||
VideoFrameType::kVideoFrameDelta);
|
||||
ExpectStreams(VideoFrameType::kVideoFrameKey, 2);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
ExpectStreams(VideoFrameType::kVideoFrameDelta, 2);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::TestPaddingOneStreamTwoMaxedOut() {
|
||||
// We are just below limit of sending third stream, so we should get
|
||||
// first stream's rate maxed out at `targetBitrate`, second at `maxBitrate`.
|
||||
SetRates(kTargetBitrates[0] + kTargetBitrates[1] + kMinBitrates[2] - 1, 30);
|
||||
std::vector<VideoFrameType> frame_types(kNumberOfSimulcastStreams,
|
||||
VideoFrameType::kVideoFrameDelta);
|
||||
ExpectStreams(VideoFrameType::kVideoFrameKey, 2);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
ExpectStreams(VideoFrameType::kVideoFrameDelta, 2);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::TestSendAllStreams() {
|
||||
// We have just enough to send all streams.
|
||||
SetRates(kTargetBitrates[0] + kTargetBitrates[1] + kMinBitrates[2], 30);
|
||||
std::vector<VideoFrameType> frame_types(kNumberOfSimulcastStreams,
|
||||
VideoFrameType::kVideoFrameDelta);
|
||||
ExpectStreams(VideoFrameType::kVideoFrameKey, 3);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
ExpectStreams(VideoFrameType::kVideoFrameDelta, 3);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::TestDisablingStreams() {
|
||||
// We should get three media streams.
|
||||
SetRates(kMaxBitrates[0] + kMaxBitrates[1] + kMaxBitrates[2], 30);
|
||||
std::vector<VideoFrameType> frame_types(kNumberOfSimulcastStreams,
|
||||
VideoFrameType::kVideoFrameDelta);
|
||||
ExpectStreams(VideoFrameType::kVideoFrameKey, 3);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
ExpectStreams(VideoFrameType::kVideoFrameDelta, 3);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
// We should only get two streams and padding for one.
|
||||
SetRates(kTargetBitrates[0] + kTargetBitrates[1] + kMinBitrates[2] / 2, 30);
|
||||
ExpectStreams(VideoFrameType::kVideoFrameDelta, 2);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
// We should only get the first stream and padding for two.
|
||||
SetRates(kTargetBitrates[0] + kMinBitrates[1] / 2, 30);
|
||||
ExpectStreams(VideoFrameType::kVideoFrameDelta, 1);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
// We don't have enough bitrate for the thumbnail stream, but we should get
|
||||
// it anyway with current configuration.
|
||||
SetRates(kTargetBitrates[0] - 1, 30);
|
||||
ExpectStreams(VideoFrameType::kVideoFrameDelta, 1);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
// We should only get two streams and padding for one.
|
||||
SetRates(kTargetBitrates[0] + kTargetBitrates[1] + kMinBitrates[2] / 2, 30);
|
||||
// We get a key frame because a new stream is being enabled.
|
||||
ExpectStreams(VideoFrameType::kVideoFrameKey, 2);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
// We should get all three streams.
|
||||
SetRates(kTargetBitrates[0] + kTargetBitrates[1] + kTargetBitrates[2], 30);
|
||||
// We get a key frame because a new stream is being enabled.
|
||||
ExpectStreams(VideoFrameType::kVideoFrameKey, 3);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::TestActiveStreams() {
|
||||
// All streams on.
|
||||
RunActiveStreamsTest({true, true, true});
|
||||
// All streams off.
|
||||
RunActiveStreamsTest({false, false, false});
|
||||
// Low stream off.
|
||||
RunActiveStreamsTest({false, true, true});
|
||||
// Middle stream off.
|
||||
RunActiveStreamsTest({true, false, true});
|
||||
// High stream off.
|
||||
RunActiveStreamsTest({true, true, false});
|
||||
// Only low stream turned on.
|
||||
RunActiveStreamsTest({true, false, false});
|
||||
// Only middle stream turned on.
|
||||
RunActiveStreamsTest({false, true, false});
|
||||
// Only high stream turned on.
|
||||
RunActiveStreamsTest({false, false, true});
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::SwitchingToOneStream(int width, int height) {
|
||||
const int* temporal_layer_profile = nullptr;
|
||||
// Disable all streams except the last and set the bitrate of the last to
|
||||
// 100 kbps. This verifies the way GTP switches to screenshare mode.
|
||||
if (codec_type_ == kVideoCodecVP8) {
|
||||
settings_.VP8()->numberOfTemporalLayers = 1;
|
||||
temporal_layer_profile = kDefaultTemporalLayerProfile;
|
||||
} else {
|
||||
settings_.H264()->numberOfTemporalLayers = 1;
|
||||
temporal_layer_profile = kNoTemporalLayerProfile;
|
||||
}
|
||||
settings_.maxBitrate = 100;
|
||||
settings_.startBitrate = 100;
|
||||
settings_.width = width;
|
||||
settings_.height = height;
|
||||
for (int i = 0; i < settings_.numberOfSimulcastStreams - 1; ++i) {
|
||||
settings_.simulcastStream[i].maxBitrate = 0;
|
||||
settings_.simulcastStream[i].width = settings_.width;
|
||||
settings_.simulcastStream[i].height = settings_.height;
|
||||
settings_.simulcastStream[i].numberOfTemporalLayers = 1;
|
||||
}
|
||||
// Setting input image to new resolution.
|
||||
input_buffer_ = I420Buffer::Create(settings_.width, settings_.height);
|
||||
input_buffer_->InitializeData();
|
||||
|
||||
input_frame_ = std::make_unique<webrtc::VideoFrame>(
|
||||
webrtc::VideoFrame::Builder()
|
||||
.set_video_frame_buffer(input_buffer_)
|
||||
.set_rotation(webrtc::kVideoRotation_0)
|
||||
.set_timestamp_us(0)
|
||||
.build());
|
||||
|
||||
// The for loop above did not set the bitrate of the highest layer.
|
||||
settings_.simulcastStream[settings_.numberOfSimulcastStreams - 1].maxBitrate =
|
||||
0;
|
||||
// The highest layer has to correspond to the non-simulcast resolution.
|
||||
settings_.simulcastStream[settings_.numberOfSimulcastStreams - 1].width =
|
||||
settings_.width;
|
||||
settings_.simulcastStream[settings_.numberOfSimulcastStreams - 1].height =
|
||||
settings_.height;
|
||||
SetUpRateAllocator();
|
||||
EXPECT_EQ(0, encoder_->InitEncode(&settings_, kSettings));
|
||||
|
||||
// Encode one frame and verify.
|
||||
SetRates(kMaxBitrates[0] + kMaxBitrates[1], 30);
|
||||
std::vector<VideoFrameType> frame_types(kNumberOfSimulcastStreams,
|
||||
VideoFrameType::kVideoFrameDelta);
|
||||
EXPECT_CALL(
|
||||
encoder_callback_,
|
||||
OnEncodedImage(AllOf(Field(&EncodedImage::_frameType,
|
||||
VideoFrameType::kVideoFrameKey),
|
||||
Field(&EncodedImage::_encodedWidth, width),
|
||||
Field(&EncodedImage::_encodedHeight, height)),
|
||||
_))
|
||||
.Times(1)
|
||||
.WillRepeatedly(Return(
|
||||
EncodedImageCallback::Result(EncodedImageCallback::Result::OK, 0)));
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
|
||||
// Switch back.
|
||||
DefaultSettings(&settings_, temporal_layer_profile, codec_type_);
|
||||
// Start at the lowest bitrate for enabling base stream.
|
||||
settings_.startBitrate = kMinBitrates[0];
|
||||
SetUpRateAllocator();
|
||||
EXPECT_EQ(0, encoder_->InitEncode(&settings_, kSettings));
|
||||
SetRates(settings_.startBitrate, 30);
|
||||
ExpectStreams(VideoFrameType::kVideoFrameKey, 1);
|
||||
// Resize `input_frame_` to the new resolution.
|
||||
input_buffer_ = I420Buffer::Create(settings_.width, settings_.height);
|
||||
input_buffer_->InitializeData();
|
||||
input_frame_ = std::make_unique<webrtc::VideoFrame>(
|
||||
webrtc::VideoFrame::Builder()
|
||||
.set_video_frame_buffer(input_buffer_)
|
||||
.set_rotation(webrtc::kVideoRotation_0)
|
||||
.set_timestamp_us(0)
|
||||
.build());
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, &frame_types));
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::TestSwitchingToOneStream() {
|
||||
SwitchingToOneStream(1024, 768);
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::TestSwitchingToOneOddStream() {
|
||||
SwitchingToOneStream(1023, 769);
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::TestSwitchingToOneSmallStream() {
|
||||
SwitchingToOneStream(4, 4);
|
||||
}
|
||||
|
||||
// Test the layer pattern and sync flag for various spatial-temporal patterns.
|
||||
// 3-3-3 pattern: 3 temporal layers for all spatial streams, so same
|
||||
// temporal_layer id and layer_sync is expected for all streams.
|
||||
void SimulcastTestFixtureImpl::TestSpatioTemporalLayers333PatternEncoder() {
|
||||
bool is_h264 = codec_type_ == kVideoCodecH264;
|
||||
TestEncodedImageCallback encoder_callback;
|
||||
encoder_->RegisterEncodeCompleteCallback(&encoder_callback);
|
||||
SetRates(kMaxBitrates[2], 30); // To get all three streams.
|
||||
|
||||
int expected_temporal_idx[3] = {-1, -1, -1};
|
||||
bool expected_layer_sync[3] = {false, false, false};
|
||||
|
||||
// First frame: #0.
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, NULL));
|
||||
SetExpectedValues3<int>(0, 0, 0, expected_temporal_idx);
|
||||
SetExpectedValues3<bool>(!is_h264, !is_h264, !is_h264, expected_layer_sync);
|
||||
VerifyTemporalIdxAndSyncForAllSpatialLayers(
|
||||
&encoder_callback, expected_temporal_idx, expected_layer_sync, 3);
|
||||
|
||||
// Next frame: #1.
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, NULL));
|
||||
SetExpectedValues3<int>(2, 2, 2, expected_temporal_idx);
|
||||
SetExpectedValues3<bool>(true, true, true, expected_layer_sync);
|
||||
VerifyTemporalIdxAndSyncForAllSpatialLayers(
|
||||
&encoder_callback, expected_temporal_idx, expected_layer_sync, 3);
|
||||
|
||||
// Next frame: #2.
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, NULL));
|
||||
SetExpectedValues3<int>(1, 1, 1, expected_temporal_idx);
|
||||
SetExpectedValues3<bool>(true, true, true, expected_layer_sync);
|
||||
VerifyTemporalIdxAndSyncForAllSpatialLayers(
|
||||
&encoder_callback, expected_temporal_idx, expected_layer_sync, 3);
|
||||
|
||||
// Next frame: #3.
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, NULL));
|
||||
SetExpectedValues3<int>(2, 2, 2, expected_temporal_idx);
|
||||
SetExpectedValues3<bool>(false, false, false, expected_layer_sync);
|
||||
VerifyTemporalIdxAndSyncForAllSpatialLayers(
|
||||
&encoder_callback, expected_temporal_idx, expected_layer_sync, 3);
|
||||
|
||||
// Next frame: #4.
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, NULL));
|
||||
SetExpectedValues3<int>(0, 0, 0, expected_temporal_idx);
|
||||
SetExpectedValues3<bool>(false, false, false, expected_layer_sync);
|
||||
VerifyTemporalIdxAndSyncForAllSpatialLayers(
|
||||
&encoder_callback, expected_temporal_idx, expected_layer_sync, 3);
|
||||
|
||||
// Next frame: #5.
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, NULL));
|
||||
SetExpectedValues3<int>(2, 2, 2, expected_temporal_idx);
|
||||
SetExpectedValues3<bool>(is_h264, is_h264, is_h264, expected_layer_sync);
|
||||
VerifyTemporalIdxAndSyncForAllSpatialLayers(
|
||||
&encoder_callback, expected_temporal_idx, expected_layer_sync, 3);
|
||||
}
|
||||
|
||||
// Test the layer pattern and sync flag for various spatial-temporal patterns.
|
||||
// 3-2-1 pattern: 3 temporal layers for lowest resolution, 2 for middle, and
|
||||
// 1 temporal layer for highest resolution.
|
||||
// For this profile, we expect the temporal index pattern to be:
|
||||
// 1st stream: 0, 2, 1, 2, ....
|
||||
// 2nd stream: 0, 1, 0, 1, ...
|
||||
// 3rd stream: -1, -1, -1, -1, ....
|
||||
// Regarding the 3rd stream, note that a stream/encoder with 1 temporal layer
|
||||
// should always have temporal layer idx set to kNoTemporalIdx = -1.
|
||||
// Since CodecSpecificInfoVP8.temporalIdx is uint8_t, this will wrap to 255.
|
||||
// TODO(marpan): Although this seems safe for now, we should fix this.
|
||||
void SimulcastTestFixtureImpl::TestSpatioTemporalLayers321PatternEncoder() {
|
||||
EXPECT_EQ(codec_type_, kVideoCodecVP8);
|
||||
int temporal_layer_profile[3] = {3, 2, 1};
|
||||
SetUpCodec(temporal_layer_profile);
|
||||
TestEncodedImageCallback encoder_callback;
|
||||
encoder_->RegisterEncodeCompleteCallback(&encoder_callback);
|
||||
SetRates(kMaxBitrates[2], 30); // To get all three streams.
|
||||
|
||||
int expected_temporal_idx[3] = {-1, -1, -1};
|
||||
bool expected_layer_sync[3] = {false, false, false};
|
||||
|
||||
// First frame: #0.
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, NULL));
|
||||
SetExpectedValues3<int>(0, 0, 255, expected_temporal_idx);
|
||||
SetExpectedValues3<bool>(true, true, false, expected_layer_sync);
|
||||
VerifyTemporalIdxAndSyncForAllSpatialLayers(
|
||||
&encoder_callback, expected_temporal_idx, expected_layer_sync, 3);
|
||||
|
||||
// Next frame: #1.
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, NULL));
|
||||
SetExpectedValues3<int>(2, 1, 255, expected_temporal_idx);
|
||||
SetExpectedValues3<bool>(true, true, false, expected_layer_sync);
|
||||
VerifyTemporalIdxAndSyncForAllSpatialLayers(
|
||||
&encoder_callback, expected_temporal_idx, expected_layer_sync, 3);
|
||||
|
||||
// Next frame: #2.
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, NULL));
|
||||
SetExpectedValues3<int>(1, 0, 255, expected_temporal_idx);
|
||||
SetExpectedValues3<bool>(true, false, false, expected_layer_sync);
|
||||
VerifyTemporalIdxAndSyncForAllSpatialLayers(
|
||||
&encoder_callback, expected_temporal_idx, expected_layer_sync, 3);
|
||||
|
||||
// Next frame: #3.
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, NULL));
|
||||
SetExpectedValues3<int>(2, 1, 255, expected_temporal_idx);
|
||||
SetExpectedValues3<bool>(false, false, false, expected_layer_sync);
|
||||
VerifyTemporalIdxAndSyncForAllSpatialLayers(
|
||||
&encoder_callback, expected_temporal_idx, expected_layer_sync, 3);
|
||||
|
||||
// Next frame: #4.
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, NULL));
|
||||
SetExpectedValues3<int>(0, 0, 255, expected_temporal_idx);
|
||||
SetExpectedValues3<bool>(false, false, false, expected_layer_sync);
|
||||
VerifyTemporalIdxAndSyncForAllSpatialLayers(
|
||||
&encoder_callback, expected_temporal_idx, expected_layer_sync, 3);
|
||||
|
||||
// Next frame: #5.
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, NULL));
|
||||
SetExpectedValues3<int>(2, 1, 255, expected_temporal_idx);
|
||||
SetExpectedValues3<bool>(false, true, false, expected_layer_sync);
|
||||
VerifyTemporalIdxAndSyncForAllSpatialLayers(
|
||||
&encoder_callback, expected_temporal_idx, expected_layer_sync, 3);
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::TestStrideEncodeDecode() {
|
||||
TestEncodedImageCallback encoder_callback;
|
||||
TestDecodedImageCallback decoder_callback;
|
||||
encoder_->RegisterEncodeCompleteCallback(&encoder_callback);
|
||||
decoder_->RegisterDecodeCompleteCallback(&decoder_callback);
|
||||
|
||||
SetRates(kMaxBitrates[2], 30); // To get all three streams.
|
||||
// Setting two (possibly) problematic use cases for stride:
|
||||
// 1. stride > width 2. stride_y != stride_uv/2
|
||||
int stride_y = kDefaultWidth + 20;
|
||||
int stride_uv = ((kDefaultWidth + 1) / 2) + 5;
|
||||
input_buffer_ = I420Buffer::Create(kDefaultWidth, kDefaultHeight, stride_y,
|
||||
stride_uv, stride_uv);
|
||||
input_frame_ = std::make_unique<webrtc::VideoFrame>(
|
||||
webrtc::VideoFrame::Builder()
|
||||
.set_video_frame_buffer(input_buffer_)
|
||||
.set_rotation(webrtc::kVideoRotation_0)
|
||||
.set_timestamp_us(0)
|
||||
.build());
|
||||
|
||||
// Set color.
|
||||
int plane_offset[kNumOfPlanes];
|
||||
plane_offset[kYPlane] = kColorY;
|
||||
plane_offset[kUPlane] = kColorU;
|
||||
plane_offset[kVPlane] = kColorV;
|
||||
CreateImage(input_buffer_, plane_offset);
|
||||
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, NULL));
|
||||
|
||||
// Change color.
|
||||
plane_offset[kYPlane] += 1;
|
||||
plane_offset[kUPlane] += 1;
|
||||
plane_offset[kVPlane] += 1;
|
||||
CreateImage(input_buffer_, plane_offset);
|
||||
input_frame_->set_timestamp(input_frame_->timestamp() + 3000);
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, NULL));
|
||||
|
||||
EncodedImage encoded_frame;
|
||||
// Only encoding one frame - so will be a key frame.
|
||||
encoder_callback.GetLastEncodedKeyFrame(&encoded_frame);
|
||||
EXPECT_EQ(0, decoder_->Decode(encoded_frame, 0));
|
||||
encoder_callback.GetLastEncodedFrame(&encoded_frame);
|
||||
decoder_->Decode(encoded_frame, 0);
|
||||
EXPECT_EQ(2, decoder_callback.DecodedFrames());
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::TestDecodeWidthHeightSet() {
|
||||
MockEncodedImageCallback encoder_callback;
|
||||
MockDecodedImageCallback decoder_callback;
|
||||
|
||||
EncodedImage encoded_frame[3];
|
||||
SetRates(kMaxBitrates[2], 30); // To get all three streams.
|
||||
encoder_->RegisterEncodeCompleteCallback(&encoder_callback);
|
||||
decoder_->RegisterDecodeCompleteCallback(&decoder_callback);
|
||||
|
||||
EXPECT_CALL(encoder_callback, OnEncodedImage(_, _))
|
||||
.Times(3)
|
||||
.WillRepeatedly(
|
||||
::testing::Invoke([&](const EncodedImage& encoded_image,
|
||||
const CodecSpecificInfo* codec_specific_info) {
|
||||
EXPECT_EQ(encoded_image._frameType, VideoFrameType::kVideoFrameKey);
|
||||
|
||||
size_t index = encoded_image.SimulcastIndex().value_or(0);
|
||||
encoded_frame[index].SetEncodedData(EncodedImageBuffer::Create(
|
||||
encoded_image.data(), encoded_image.size()));
|
||||
encoded_frame[index]._frameType = encoded_image._frameType;
|
||||
return EncodedImageCallback::Result(
|
||||
EncodedImageCallback::Result::OK, 0);
|
||||
}));
|
||||
EXPECT_EQ(0, encoder_->Encode(*input_frame_, NULL));
|
||||
|
||||
EXPECT_CALL(decoder_callback, Decoded(_, _, _))
|
||||
.WillOnce(::testing::Invoke([](VideoFrame& decodedImage,
|
||||
absl::optional<int32_t> decode_time_ms,
|
||||
absl::optional<uint8_t> qp) {
|
||||
EXPECT_EQ(decodedImage.width(), kDefaultWidth / 4);
|
||||
EXPECT_EQ(decodedImage.height(), kDefaultHeight / 4);
|
||||
}));
|
||||
EXPECT_EQ(0, decoder_->Decode(encoded_frame[0], 0));
|
||||
|
||||
EXPECT_CALL(decoder_callback, Decoded(_, _, _))
|
||||
.WillOnce(::testing::Invoke([](VideoFrame& decodedImage,
|
||||
absl::optional<int32_t> decode_time_ms,
|
||||
absl::optional<uint8_t> qp) {
|
||||
EXPECT_EQ(decodedImage.width(), kDefaultWidth / 2);
|
||||
EXPECT_EQ(decodedImage.height(), kDefaultHeight / 2);
|
||||
}));
|
||||
EXPECT_EQ(0, decoder_->Decode(encoded_frame[1], 0));
|
||||
|
||||
EXPECT_CALL(decoder_callback, Decoded(_, _, _))
|
||||
.WillOnce(::testing::Invoke([](VideoFrame& decodedImage,
|
||||
absl::optional<int32_t> decode_time_ms,
|
||||
absl::optional<uint8_t> qp) {
|
||||
EXPECT_EQ(decodedImage.width(), kDefaultWidth);
|
||||
EXPECT_EQ(decodedImage.height(), kDefaultHeight);
|
||||
}));
|
||||
EXPECT_EQ(0, decoder_->Decode(encoded_frame[2], 0));
|
||||
}
|
||||
|
||||
void SimulcastTestFixtureImpl::
|
||||
TestEncoderInfoForDefaultTemporalLayerProfileHasFpsAllocation() {
|
||||
VideoEncoder::EncoderInfo encoder_info = encoder_->GetEncoderInfo();
|
||||
EXPECT_EQ(encoder_info.fps_allocation[0].size(),
|
||||
static_cast<size_t>(kDefaultTemporalLayerProfile[0]));
|
||||
EXPECT_EQ(encoder_info.fps_allocation[1].size(),
|
||||
static_cast<size_t>(kDefaultTemporalLayerProfile[1]));
|
||||
EXPECT_EQ(encoder_info.fps_allocation[2].size(),
|
||||
static_cast<size_t>(kDefaultTemporalLayerProfile[2]));
|
||||
}
|
||||
} // namespace test
|
||||
} // namespace webrtc
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright (c) 2018 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.
|
||||
*/
|
||||
|
||||
#ifndef MODULES_VIDEO_CODING_UTILITY_SIMULCAST_TEST_FIXTURE_IMPL_H_
|
||||
#define MODULES_VIDEO_CODING_UTILITY_SIMULCAST_TEST_FIXTURE_IMPL_H_
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "api/test/mock_video_decoder.h"
|
||||
#include "api/test/mock_video_encoder.h"
|
||||
#include "api/test/simulcast_test_fixture.h"
|
||||
#include "api/video/i420_buffer.h"
|
||||
#include "api/video/video_frame.h"
|
||||
#include "api/video_codecs/video_decoder_factory.h"
|
||||
#include "api/video_codecs/video_encoder_factory.h"
|
||||
#include "modules/video_coding/utility/simulcast_rate_allocator.h"
|
||||
|
||||
namespace webrtc {
|
||||
namespace test {
|
||||
|
||||
class SimulcastTestFixtureImpl final : public SimulcastTestFixture {
|
||||
public:
|
||||
SimulcastTestFixtureImpl(std::unique_ptr<VideoEncoderFactory> encoder_factory,
|
||||
std::unique_ptr<VideoDecoderFactory> decoder_factory,
|
||||
SdpVideoFormat video_format);
|
||||
~SimulcastTestFixtureImpl() final;
|
||||
|
||||
// Implements SimulcastTestFixture.
|
||||
void TestKeyFrameRequestsOnAllStreams() override;
|
||||
void TestKeyFrameRequestsOnSpecificStreams() override;
|
||||
void TestPaddingAllStreams() override;
|
||||
void TestPaddingTwoStreams() override;
|
||||
void TestPaddingTwoStreamsOneMaxedOut() override;
|
||||
void TestPaddingOneStream() override;
|
||||
void TestPaddingOneStreamTwoMaxedOut() override;
|
||||
void TestSendAllStreams() override;
|
||||
void TestDisablingStreams() override;
|
||||
void TestActiveStreams() override;
|
||||
void TestSwitchingToOneStream() override;
|
||||
void TestSwitchingToOneOddStream() override;
|
||||
void TestSwitchingToOneSmallStream() override;
|
||||
void TestSpatioTemporalLayers333PatternEncoder() override;
|
||||
void TestSpatioTemporalLayers321PatternEncoder() override;
|
||||
void TestStrideEncodeDecode() override;
|
||||
void TestDecodeWidthHeightSet() override;
|
||||
void TestEncoderInfoForDefaultTemporalLayerProfileHasFpsAllocation() override;
|
||||
|
||||
static void DefaultSettings(VideoCodec* settings,
|
||||
const int* temporal_layer_profile,
|
||||
VideoCodecType codec_type,
|
||||
bool reverse_layer_order = false);
|
||||
|
||||
private:
|
||||
class TestEncodedImageCallback;
|
||||
class TestDecodedImageCallback;
|
||||
|
||||
void SetUpCodec(const int* temporal_layer_profile);
|
||||
void SetUpRateAllocator();
|
||||
void SetRates(uint32_t bitrate_kbps, uint32_t fps);
|
||||
void RunActiveStreamsTest(std::vector<bool> active_streams);
|
||||
void UpdateActiveStreams(std::vector<bool> active_streams);
|
||||
void ExpectStream(VideoFrameType frame_type, int scaleResolutionDownBy);
|
||||
void ExpectStreams(VideoFrameType frame_type,
|
||||
std::vector<bool> expected_streams_active);
|
||||
void ExpectStreams(VideoFrameType frame_type, int expected_video_streams);
|
||||
void VerifyTemporalIdxAndSyncForAllSpatialLayers(
|
||||
TestEncodedImageCallback* encoder_callback,
|
||||
const int* expected_temporal_idx,
|
||||
const bool* expected_layer_sync,
|
||||
int num_spatial_layers);
|
||||
void SwitchingToOneStream(int width, int height);
|
||||
|
||||
std::unique_ptr<VideoEncoder> encoder_;
|
||||
MockEncodedImageCallback encoder_callback_;
|
||||
std::unique_ptr<VideoDecoder> decoder_;
|
||||
MockDecodedImageCallback decoder_callback_;
|
||||
VideoCodec settings_;
|
||||
rtc::scoped_refptr<I420Buffer> input_buffer_;
|
||||
std::unique_ptr<VideoFrame> input_frame_;
|
||||
std::unique_ptr<SimulcastRateAllocator> rate_allocator_;
|
||||
VideoCodecType codec_type_;
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
} // namespace webrtc
|
||||
|
||||
#endif // MODULES_VIDEO_CODING_UTILITY_SIMULCAST_TEST_FIXTURE_IMPL_H_
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* Copyright (c) 2018 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 "modules/video_coding/utility/simulcast_utility.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "modules/video_coding/svc/scalability_mode_util.h"
|
||||
#include "rtc_base/checks.h"
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
uint32_t SimulcastUtility::SumStreamMaxBitrate(int streams,
|
||||
const VideoCodec& codec) {
|
||||
uint32_t bitrate_sum = 0;
|
||||
for (int i = 0; i < streams; ++i) {
|
||||
bitrate_sum += codec.simulcastStream[i].maxBitrate;
|
||||
}
|
||||
return bitrate_sum;
|
||||
}
|
||||
|
||||
int SimulcastUtility::NumberOfSimulcastStreams(const VideoCodec& codec) {
|
||||
int streams =
|
||||
codec.numberOfSimulcastStreams < 1 ? 1 : codec.numberOfSimulcastStreams;
|
||||
uint32_t simulcast_max_bitrate = SumStreamMaxBitrate(streams, codec);
|
||||
if (simulcast_max_bitrate == 0) {
|
||||
streams = 1;
|
||||
}
|
||||
return streams;
|
||||
}
|
||||
|
||||
bool SimulcastUtility::ValidSimulcastParameters(const VideoCodec& codec,
|
||||
int num_streams) {
|
||||
// Check resolution.
|
||||
if (codec.width != codec.simulcastStream[num_streams - 1].width ||
|
||||
codec.height != codec.simulcastStream[num_streams - 1].height) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < num_streams; ++i) {
|
||||
if (codec.width * codec.simulcastStream[i].height !=
|
||||
codec.height * codec.simulcastStream[i].width) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (int i = 1; i < num_streams; ++i) {
|
||||
if (codec.simulcastStream[i].width < codec.simulcastStream[i - 1].width) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check frame-rate.
|
||||
for (int i = 1; i < num_streams; ++i) {
|
||||
if (fabs(codec.simulcastStream[i].maxFramerate -
|
||||
codec.simulcastStream[i - 1].maxFramerate) > 1e-9) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check temporal layers.
|
||||
for (int i = 0; i < num_streams - 1; ++i) {
|
||||
if (codec.simulcastStream[i].numberOfTemporalLayers !=
|
||||
codec.simulcastStream[i + 1].numberOfTemporalLayers)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SimulcastUtility::IsConferenceModeScreenshare(const VideoCodec& codec) {
|
||||
return codec.mode == VideoCodecMode::kScreensharing &&
|
||||
codec.legacy_conference_mode;
|
||||
}
|
||||
|
||||
int SimulcastUtility::NumberOfTemporalLayers(const VideoCodec& codec,
|
||||
int spatial_id) {
|
||||
int num_temporal_layers = 0;
|
||||
if (auto scalability_mode = codec.GetScalabilityMode(); scalability_mode) {
|
||||
num_temporal_layers = ScalabilityModeToNumTemporalLayers(*scalability_mode);
|
||||
} else {
|
||||
switch (codec.codecType) {
|
||||
case kVideoCodecVP8:
|
||||
num_temporal_layers = codec.VP8().numberOfTemporalLayers;
|
||||
break;
|
||||
case kVideoCodecVP9:
|
||||
num_temporal_layers = codec.VP9().numberOfTemporalLayers;
|
||||
break;
|
||||
case kVideoCodecH264:
|
||||
num_temporal_layers = codec.H264().numberOfTemporalLayers;
|
||||
break;
|
||||
case kVideoCodecH265:
|
||||
// TODO(bugs.webrtc.org/13485)
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (codec.numberOfSimulcastStreams > 0) {
|
||||
RTC_DCHECK_LT(spatial_id, codec.numberOfSimulcastStreams);
|
||||
num_temporal_layers =
|
||||
std::max(num_temporal_layers,
|
||||
static_cast<int>(
|
||||
codec.simulcastStream[spatial_id].numberOfTemporalLayers));
|
||||
}
|
||||
return std::max(1, num_temporal_layers);
|
||||
}
|
||||
|
||||
} // namespace webrtc
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2018 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.
|
||||
*/
|
||||
|
||||
#ifndef MODULES_VIDEO_CODING_UTILITY_SIMULCAST_UTILITY_H_
|
||||
#define MODULES_VIDEO_CODING_UTILITY_SIMULCAST_UTILITY_H_
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "api/video_codecs/video_codec.h"
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
class SimulcastUtility {
|
||||
public:
|
||||
static uint32_t SumStreamMaxBitrate(int streams, const VideoCodec& codec);
|
||||
static int NumberOfSimulcastStreams(const VideoCodec& codec);
|
||||
static bool ValidSimulcastParameters(const VideoCodec& codec,
|
||||
int num_streams);
|
||||
static int NumberOfTemporalLayers(const VideoCodec& codec, int spatial_id);
|
||||
// TODO(sprang): Remove this hack when ScreenshareLayers is gone.
|
||||
static bool IsConferenceModeScreenshare(const VideoCodec& codec);
|
||||
};
|
||||
|
||||
} // namespace webrtc
|
||||
|
||||
#endif // MODULES_VIDEO_CODING_UTILITY_SIMULCAST_UTILITY_H_
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef MODULES_VIDEO_CODING_UTILITY_VP8_CONSTANTS_H_
|
||||
#define MODULES_VIDEO_CODING_UTILITY_VP8_CONSTANTS_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
// QP level below which VP8 variable framerate and zero hertz screencast reduces
|
||||
// framerate due to diminishing quality enhancement returns.
|
||||
constexpr int kVp8SteadyStateQpThreshold = 15;
|
||||
|
||||
} // namespace webrtc
|
||||
|
||||
#endif // MODULES_VIDEO_CODING_UTILITY_VP8_CONSTANTS_H_
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* Copyright (c) 2015 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 "modules/video_coding/utility/vp8_header_parser.h"
|
||||
|
||||
#include "rtc_base/logging.h"
|
||||
#include "rtc_base/system/arch.h"
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
namespace vp8 {
|
||||
namespace {
|
||||
const size_t kCommonPayloadHeaderLength = 3;
|
||||
const size_t kKeyPayloadHeaderLength = 10;
|
||||
const int kMbFeatureTreeProbs = 3;
|
||||
const int kNumMbSegments = 4;
|
||||
const int kNumRefLfDeltas = 4;
|
||||
const int kNumModeLfDeltas = 4;
|
||||
|
||||
} // namespace
|
||||
|
||||
// Bitstream parser according to
|
||||
// https://tools.ietf.org/html/rfc6386#section-7.3
|
||||
void VP8InitBitReader(VP8BitReader* const br,
|
||||
const uint8_t* start,
|
||||
const uint8_t* end) {
|
||||
br->range_ = 255;
|
||||
br->buf_ = start;
|
||||
br->buf_end_ = end;
|
||||
br->value_ = 0;
|
||||
br->bits_ = 0;
|
||||
|
||||
// Read 2 bytes.
|
||||
int i = 0;
|
||||
while (++i <= 2) {
|
||||
if (br->buf_ != br->buf_end_) {
|
||||
br->value_ = br->value_ << 8 | *br->buf_++;
|
||||
} else {
|
||||
br->value_ = br->value_ << 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bit decoder according to https://tools.ietf.org/html/rfc6386#section-7.3
|
||||
// Reads one bit from the bitstream, given that it has probability prob/256 to
|
||||
// be 1.
|
||||
int Vp8BitReaderGetBool(VP8BitReader* br, int prob) {
|
||||
uint32_t split = 1 + (((br->range_ - 1) * prob) >> 8);
|
||||
uint32_t split_hi = split << 8;
|
||||
int retval = 0;
|
||||
if (br->value_ >= split_hi) {
|
||||
retval = 1;
|
||||
br->range_ -= split;
|
||||
br->value_ -= split_hi;
|
||||
} else {
|
||||
retval = 0;
|
||||
br->range_ = split;
|
||||
}
|
||||
|
||||
while (br->range_ < 128) {
|
||||
br->value_ <<= 1;
|
||||
br->range_ <<= 1;
|
||||
if (++br->bits_ == 8) {
|
||||
br->bits_ = 0;
|
||||
if (br->buf_ != br->buf_end_) {
|
||||
br->value_ |= *br->buf_++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
uint32_t VP8GetValue(VP8BitReader* br, int num_bits) {
|
||||
uint32_t v = 0;
|
||||
while (num_bits--) {
|
||||
// According to https://tools.ietf.org/html/rfc6386
|
||||
// Probability 128/256 is used to encode header fields.
|
||||
v = (v << 1) | Vp8BitReaderGetBool(br, 128);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// Not a read_signed_literal() from RFC 6386!
|
||||
// This one is used to read e.g. quantizer_update, which is written as:
|
||||
// L(num_bits), sign-bit.
|
||||
int32_t VP8GetSignedValue(VP8BitReader* br, int num_bits) {
|
||||
int v = VP8GetValue(br, num_bits);
|
||||
int sign = VP8GetValue(br, 1);
|
||||
return sign ? -v : v;
|
||||
}
|
||||
|
||||
static void ParseSegmentHeader(VP8BitReader* br) {
|
||||
int use_segment = VP8GetValue(br, 1);
|
||||
if (use_segment) {
|
||||
int update_map = VP8GetValue(br, 1);
|
||||
if (VP8GetValue(br, 1)) { // update_segment_feature_data.
|
||||
VP8GetValue(br, 1); // segment_feature_mode.
|
||||
int s;
|
||||
for (s = 0; s < kNumMbSegments; ++s) {
|
||||
bool quantizer_update = VP8GetValue(br, 1);
|
||||
if (quantizer_update) {
|
||||
VP8GetSignedValue(br, 7);
|
||||
}
|
||||
}
|
||||
for (s = 0; s < kNumMbSegments; ++s) {
|
||||
bool loop_filter_update = VP8GetValue(br, 1);
|
||||
if (loop_filter_update) {
|
||||
VP8GetSignedValue(br, 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (update_map) {
|
||||
int s;
|
||||
for (s = 0; s < kMbFeatureTreeProbs; ++s) {
|
||||
bool segment_prob_update = VP8GetValue(br, 1);
|
||||
if (segment_prob_update) {
|
||||
VP8GetValue(br, 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void ParseFilterHeader(VP8BitReader* br) {
|
||||
VP8GetValue(br, 1); // filter_type.
|
||||
VP8GetValue(br, 6); // loop_filter_level.
|
||||
VP8GetValue(br, 3); // sharpness_level.
|
||||
|
||||
// mb_lf_adjustments.
|
||||
int loop_filter_adj_enable = VP8GetValue(br, 1);
|
||||
if (loop_filter_adj_enable) {
|
||||
int mode_ref_lf_delta_update = VP8GetValue(br, 1);
|
||||
if (mode_ref_lf_delta_update) {
|
||||
int i;
|
||||
for (i = 0; i < kNumRefLfDeltas; ++i) {
|
||||
int ref_frame_delta_update_flag = VP8GetValue(br, 1);
|
||||
if (ref_frame_delta_update_flag) {
|
||||
VP8GetSignedValue(br, 6); // delta_magnitude.
|
||||
}
|
||||
}
|
||||
for (i = 0; i < kNumModeLfDeltas; ++i) {
|
||||
int mb_mode_delta_update_flag = VP8GetValue(br, 1);
|
||||
if (mb_mode_delta_update_flag) {
|
||||
VP8GetSignedValue(br, 6); // delta_magnitude.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool GetQp(const uint8_t* buf, size_t length, int* qp) {
|
||||
if (length < kCommonPayloadHeaderLength) {
|
||||
RTC_LOG(LS_WARNING) << "Failed to get QP, invalid length.";
|
||||
return false;
|
||||
}
|
||||
VP8BitReader br;
|
||||
const uint32_t bits = buf[0] | (buf[1] << 8) | (buf[2] << 16);
|
||||
int key_frame = !(bits & 1);
|
||||
// Size of first partition in bytes.
|
||||
uint32_t partition_length = (bits >> 5);
|
||||
size_t header_length = kCommonPayloadHeaderLength;
|
||||
if (key_frame) {
|
||||
header_length = kKeyPayloadHeaderLength;
|
||||
}
|
||||
if (header_length + partition_length > length) {
|
||||
RTC_LOG(LS_WARNING) << "Failed to get QP, invalid length: " << length;
|
||||
return false;
|
||||
}
|
||||
buf += header_length;
|
||||
|
||||
VP8InitBitReader(&br, buf, buf + partition_length);
|
||||
if (key_frame) {
|
||||
// Color space and pixel type.
|
||||
VP8GetValue(&br, 1);
|
||||
VP8GetValue(&br, 1);
|
||||
}
|
||||
ParseSegmentHeader(&br);
|
||||
ParseFilterHeader(&br);
|
||||
// Parse log2_nbr_of_dct_partitions value.
|
||||
VP8GetValue(&br, 2);
|
||||
// Base QP.
|
||||
const int base_q0 = VP8GetValue(&br, 7);
|
||||
if (br.buf_ == br.buf_end_) {
|
||||
RTC_LOG(LS_WARNING) << "Failed to get QP, bitstream is truncated or"
|
||||
" corrupted.";
|
||||
return false;
|
||||
}
|
||||
*qp = base_q0;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace vp8
|
||||
|
||||
} // namespace webrtc
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (c) 2015 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.
|
||||
*/
|
||||
|
||||
#ifndef MODULES_VIDEO_CODING_UTILITY_VP8_HEADER_PARSER_H_
|
||||
#define MODULES_VIDEO_CODING_UTILITY_VP8_HEADER_PARSER_H_
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
namespace vp8 {
|
||||
|
||||
typedef struct VP8BitReader VP8BitReader;
|
||||
struct VP8BitReader {
|
||||
// Boolean decoder.
|
||||
uint32_t value_; // Current value (2 bytes).
|
||||
uint32_t range_; // Current range (always in [128..255] interval).
|
||||
int bits_; // Number of bits shifted out of value, at most 7.
|
||||
// Read buffer.
|
||||
const uint8_t* buf_; // Next byte to be read.
|
||||
const uint8_t* buf_end_; // End of read buffer.
|
||||
};
|
||||
|
||||
// Gets the QP, QP range: [0, 127].
|
||||
// Returns true on success, false otherwise.
|
||||
bool GetQp(const uint8_t* buf, size_t length, int* qp);
|
||||
|
||||
} // namespace vp8
|
||||
|
||||
} // namespace webrtc
|
||||
|
||||
#endif // MODULES_VIDEO_CODING_UTILITY_VP8_HEADER_PARSER_H_
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.
|
||||
*/
|
||||
|
||||
#ifndef MODULES_VIDEO_CODING_UTILITY_VP9_CONSTANTS_H_
|
||||
#define MODULES_VIDEO_CODING_UTILITY_VP9_CONSTANTS_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
// Number of frames that can be stored for future reference.
|
||||
constexpr size_t kVp9NumRefFrames = 8;
|
||||
// Number of frame contexts that can be store for future reference.
|
||||
constexpr size_t kVp9NumFrameContexts = 4;
|
||||
// Each inter frame can use up to 3 frames for reference.
|
||||
constexpr size_t kVp9RefsPerFrame = 3;
|
||||
// Number of values that can be decoded for mv_fr.
|
||||
constexpr size_t kVp9MvFrSize = 4;
|
||||
// Number of positions to search in motion vector prediction.
|
||||
constexpr size_t kVp9MvrefNeighbours = 8;
|
||||
// Number of contexts when decoding intra_mode .
|
||||
constexpr size_t kVp9BlockSizeGroups = 4;
|
||||
// Number of different block sizes used.
|
||||
constexpr size_t kVp9BlockSizes = 13;
|
||||
// Sentinel value to mark partition choices that are illegal.
|
||||
constexpr size_t kVp9BlockInvalid = 14;
|
||||
// Number of contexts when decoding partition.
|
||||
constexpr size_t kVp9PartitionContexts = 16;
|
||||
// Smallest size of a mode info block.
|
||||
constexpr size_t kVp9MiSize = 8;
|
||||
// Minimum width of a tile in units of superblocks (although tiles on
|
||||
// the right hand edge can be narrower).
|
||||
constexpr size_t kVp9MinTileWidth_B64 = 4;
|
||||
// Maximum width of a tile in units of superblocks.
|
||||
constexpr size_t kVp9MaxTileWidth_B64 = 64;
|
||||
// Number of motion vectors returned by find_mv_refs process.
|
||||
constexpr size_t kVp9MaxMvRefCandidates = 2;
|
||||
// Number of values that can be derived for ref_frame.
|
||||
constexpr size_t kVp9MaxRefFrames = 4;
|
||||
// Number of contexts for is_inter.
|
||||
constexpr size_t kVp9IsInterContexts = 4;
|
||||
// Number of contexts for comp_mode.
|
||||
constexpr size_t kVp9CompModeContexts = 5;
|
||||
// Number of contexts for single_ref and comp_ref.
|
||||
constexpr size_t kVp9RefContexts = 5;
|
||||
// Number of segments allowed in segmentation map.
|
||||
constexpr size_t kVp9MaxSegments = 8;
|
||||
// Index for quantizer segment feature.
|
||||
constexpr size_t kVp9SegLvlAlt_Q = 0;
|
||||
// Index for loop filter segment feature.
|
||||
constexpr size_t kVp9SegLvlAlt_L = 1;
|
||||
// Index for reference frame segment feature.
|
||||
constexpr size_t kVp9SegLvlRefFrame = 2;
|
||||
// Index for skip segment feature.
|
||||
constexpr size_t kVp9SegLvlSkip = 3;
|
||||
// Number of segment features.
|
||||
constexpr size_t kVp9SegLvlMax = 4;
|
||||
// Number of different plane types (Y or UV).
|
||||
constexpr size_t kVp9BlockTypes = 2;
|
||||
// Number of different prediction types (intra or inter).
|
||||
constexpr size_t kVp9RefTypes = 2;
|
||||
// Number of coefficient bands.
|
||||
constexpr size_t kVp9CoefBands = 6;
|
||||
// Number of contexts for decoding coefficients.
|
||||
constexpr size_t kVp9PrevCoefContexts = 6;
|
||||
// Number of coefficient probabilities that are directly transmitted.
|
||||
constexpr size_t kVp9UnconstrainedNodes = 3;
|
||||
// Number of contexts for transform size.
|
||||
constexpr size_t kVp9TxSizeContexts = 2;
|
||||
// Number of values for interp_filter.
|
||||
constexpr size_t kVp9SwitchableFilters = 3;
|
||||
// Number of contexts for interp_filter.
|
||||
constexpr size_t kVp9InterpFilterContexts = 4;
|
||||
// Number of contexts for decoding skip.
|
||||
constexpr size_t kVp9SkipContexts = 3;
|
||||
// Number of values for partition.
|
||||
constexpr size_t kVp9PartitionTypes = 4;
|
||||
// Number of values for tx_size.
|
||||
constexpr size_t kVp9TxSizes = 4;
|
||||
// Number of values for tx_mode.
|
||||
constexpr size_t kVp9TxModes = 5;
|
||||
// Inverse transform rows with DCT and columns with DCT.
|
||||
constexpr size_t kVp9DctDct = 0;
|
||||
// Inverse transform rows with DCT and columns with ADST.
|
||||
constexpr size_t kVp9AdstDct = 1;
|
||||
// Inverse transform rows with ADST and columns with DCT.
|
||||
constexpr size_t kVp9DctAdst = 2;
|
||||
// Inverse transform rows with ADST and columns with ADST.
|
||||
constexpr size_t kVp9AdstAdst = 3;
|
||||
// Number of values for y_mode.
|
||||
constexpr size_t kVp9MbModeCount = 14;
|
||||
// Number of values for intra_mode.
|
||||
constexpr size_t kVp9IntraModes = 10;
|
||||
// Number of values for inter_mode.
|
||||
constexpr size_t kVp9InterModes = 4;
|
||||
// Number of contexts for inter_mode.
|
||||
constexpr size_t kVp9InterModeContexts = 7;
|
||||
// Number of values for mv_joint.
|
||||
constexpr size_t kVp9MvJoints = 4;
|
||||
// Number of values for mv_class.
|
||||
constexpr size_t kVp9MvClasses = 11;
|
||||
// Number of values for mv_class0_bit.
|
||||
constexpr size_t kVp9Class0Size = 2;
|
||||
// Maximum number of bits for decoding motion vectors.
|
||||
constexpr size_t kVp9MvOffsetBits = 10;
|
||||
// Number of values allowed for a probability adjustment.
|
||||
constexpr size_t kVp9MaxProb = 255;
|
||||
// Number of different mode types for loop filtering.
|
||||
constexpr size_t kVp9MaxModeLfDeltas = 2;
|
||||
// Threshold at which motion vectors are considered large.
|
||||
constexpr size_t kVp9CompandedMvrefThresh = 8;
|
||||
// Maximum value used for loop filtering.
|
||||
constexpr size_t kVp9MaxLoopFilter = 63;
|
||||
// Number of bits of precision when scaling reference frames.
|
||||
constexpr size_t kVp9RefScaleShift = 14;
|
||||
// Number of bits of precision when performing inter prediction.
|
||||
constexpr size_t kVp9SubpelBits = 4;
|
||||
// 1 << kVp9SubpelBits.
|
||||
constexpr size_t kVp9SubpelShifts = 16;
|
||||
// kVp9SubpelShifts - 1.
|
||||
constexpr size_t kVp9SubpelMask = 15;
|
||||
// Value used when clipping motion vectors.
|
||||
constexpr size_t kVp9MvBorder = 128;
|
||||
// Value used when clipping motion vectors.
|
||||
constexpr size_t kVp9InterpExtend = 4;
|
||||
// Value used when clipping motion vectors.
|
||||
constexpr size_t kVp9Borderinpixels = 160;
|
||||
// Value used in adapting probabilities.
|
||||
constexpr size_t kVp9MaxUpdateFactor = 128;
|
||||
// Value used in adapting probabilities.
|
||||
constexpr size_t kVp9CountSat = 20;
|
||||
// Both candidates use ZEROMV.
|
||||
constexpr size_t kVp9BothZero = 0;
|
||||
// One candidate uses ZEROMV, one uses NEARMV or NEARESTMV.
|
||||
constexpr size_t kVp9ZeroPlusPredicted = 1;
|
||||
// Both candidates use NEARMV or NEARESTMV.
|
||||
constexpr size_t kVp9BothPredicted = 2;
|
||||
// One candidate uses NEWMV, one uses ZEROMV.
|
||||
constexpr size_t kVp9NewPlusNonIntra = 3;
|
||||
// Both candidates use NEWMV.
|
||||
constexpr size_t kVp9BothNew = 4;
|
||||
// One candidate uses intra prediction, one uses inter prediction.
|
||||
constexpr size_t kVp9IntraPlusNonIntra = 5;
|
||||
// Both candidates use intra prediction.
|
||||
constexpr size_t kVp9BothIntra = 6;
|
||||
// Sentinel value marking a case that can never occur.
|
||||
constexpr size_t kVp9InvalidCase = 9;
|
||||
|
||||
enum class Vp9TxMode : uint8_t {
|
||||
kOnly4X4 = 0,
|
||||
kAllow8X8 = 1,
|
||||
kAllow16x16 = 2,
|
||||
kAllow32x32 = 3,
|
||||
kTxModeSelect = 4
|
||||
};
|
||||
|
||||
enum Vp9BlockSize : uint8_t {
|
||||
kBlock4X4 = 0,
|
||||
kBlock4X8 = 1,
|
||||
kBlock8X4 = 2,
|
||||
kBlock8X8 = 3,
|
||||
kBlock8X16 = 4,
|
||||
kBlock16X8 = 5,
|
||||
kBlock16X16 = 6,
|
||||
kBlock16X32 = 7,
|
||||
kBlock32X16 = 8,
|
||||
kBlock32X32 = 9,
|
||||
kBlock32X64 = 10,
|
||||
kBlock64X32 = 11,
|
||||
kBlock64X64 = 12
|
||||
};
|
||||
|
||||
enum Vp9Partition : uint8_t {
|
||||
kPartitionNone = 0,
|
||||
kPartitionHorizontal = 1,
|
||||
kPartitionVertical = 2,
|
||||
kPartitionSplit = 3
|
||||
};
|
||||
|
||||
enum class Vp9ReferenceMode : uint8_t {
|
||||
kSingleReference = 0,
|
||||
kCompoundReference = 1,
|
||||
kReferenceModeSelect = 2,
|
||||
};
|
||||
|
||||
} // namespace webrtc
|
||||
|
||||
#endif // MODULES_VIDEO_CODING_UTILITY_VP9_CONSTANTS_H_
|
||||
|
|
@ -0,0 +1,533 @@
|
|||
/*
|
||||
* Copyright (c) 2017 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 "modules/video_coding/utility/vp9_uncompressed_header_parser.h"
|
||||
|
||||
#include "absl/numeric/bits.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "rtc_base/bitstream_reader.h"
|
||||
#include "rtc_base/logging.h"
|
||||
#include "rtc_base/strings/string_builder.h"
|
||||
|
||||
namespace webrtc {
|
||||
namespace {
|
||||
const size_t kVp9NumRefsPerFrame = 3;
|
||||
const size_t kVp9MaxRefLFDeltas = 4;
|
||||
const size_t kVp9MaxModeLFDeltas = 2;
|
||||
const size_t kVp9MinTileWidthB64 = 4;
|
||||
const size_t kVp9MaxTileWidthB64 = 64;
|
||||
|
||||
void Vp9ReadColorConfig(BitstreamReader& br,
|
||||
Vp9UncompressedHeader* frame_info) {
|
||||
if (frame_info->profile == 2 || frame_info->profile == 3) {
|
||||
frame_info->bit_detph =
|
||||
br.Read<bool>() ? Vp9BitDept::k12Bit : Vp9BitDept::k10Bit;
|
||||
} else {
|
||||
frame_info->bit_detph = Vp9BitDept::k8Bit;
|
||||
}
|
||||
|
||||
frame_info->color_space = static_cast<Vp9ColorSpace>(br.ReadBits(3));
|
||||
|
||||
if (frame_info->color_space != Vp9ColorSpace::CS_RGB) {
|
||||
frame_info->color_range =
|
||||
br.Read<bool>() ? Vp9ColorRange::kFull : Vp9ColorRange::kStudio;
|
||||
|
||||
if (frame_info->profile == 1 || frame_info->profile == 3) {
|
||||
static constexpr Vp9YuvSubsampling kSubSamplings[] = {
|
||||
Vp9YuvSubsampling::k444, Vp9YuvSubsampling::k440,
|
||||
Vp9YuvSubsampling::k422, Vp9YuvSubsampling::k420};
|
||||
frame_info->sub_sampling = kSubSamplings[br.ReadBits(2)];
|
||||
|
||||
if (br.Read<bool>()) {
|
||||
RTC_LOG(LS_WARNING) << "Failed to parse header. Reserved bit set.";
|
||||
br.Invalidate();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Profile 0 or 2.
|
||||
frame_info->sub_sampling = Vp9YuvSubsampling::k420;
|
||||
}
|
||||
} else {
|
||||
// SRGB
|
||||
frame_info->color_range = Vp9ColorRange::kFull;
|
||||
if (frame_info->profile == 1 || frame_info->profile == 3) {
|
||||
frame_info->sub_sampling = Vp9YuvSubsampling::k444;
|
||||
if (br.Read<bool>()) {
|
||||
RTC_LOG(LS_WARNING) << "Failed to parse header. Reserved bit set.";
|
||||
br.Invalidate();
|
||||
}
|
||||
} else {
|
||||
RTC_LOG(LS_WARNING) << "Failed to parse header. 4:4:4 color not supported"
|
||||
" in profile 0 or 2.";
|
||||
br.Invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ReadRefreshFrameFlags(BitstreamReader& br,
|
||||
Vp9UncompressedHeader* frame_info) {
|
||||
// Refresh frame flags.
|
||||
uint8_t flags = br.Read<uint8_t>();
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
frame_info->updated_buffers.set(i, (flags & (0x01 << (7 - i))) != 0);
|
||||
}
|
||||
}
|
||||
|
||||
void Vp9ReadFrameSize(BitstreamReader& br, Vp9UncompressedHeader* frame_info) {
|
||||
// 16 bits: frame (width|height) - 1.
|
||||
frame_info->frame_width = br.Read<uint16_t>() + 1;
|
||||
frame_info->frame_height = br.Read<uint16_t>() + 1;
|
||||
}
|
||||
|
||||
void Vp9ReadRenderSize(size_t total_buffer_size_bits,
|
||||
BitstreamReader& br,
|
||||
Vp9UncompressedHeader* frame_info) {
|
||||
// render_and_frame_size_different
|
||||
if (br.Read<bool>()) {
|
||||
frame_info->render_size_offset_bits =
|
||||
total_buffer_size_bits - br.RemainingBitCount();
|
||||
// 16 bits: render (width|height) - 1.
|
||||
frame_info->render_width = br.Read<uint16_t>() + 1;
|
||||
frame_info->render_height = br.Read<uint16_t>() + 1;
|
||||
} else {
|
||||
frame_info->render_height = frame_info->frame_height;
|
||||
frame_info->render_width = frame_info->frame_width;
|
||||
}
|
||||
}
|
||||
|
||||
void Vp9ReadFrameSizeFromRefs(BitstreamReader& br,
|
||||
Vp9UncompressedHeader* frame_info) {
|
||||
for (size_t i = 0; i < kVp9NumRefsPerFrame; i++) {
|
||||
// Size in refs.
|
||||
if (br.Read<bool>()) {
|
||||
frame_info->infer_size_from_reference = frame_info->reference_buffers[i];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Vp9ReadFrameSize(br, frame_info);
|
||||
}
|
||||
|
||||
void Vp9ReadLoopfilter(BitstreamReader& br) {
|
||||
// 6 bits: filter level.
|
||||
// 3 bits: sharpness level.
|
||||
br.ConsumeBits(9);
|
||||
|
||||
if (!br.Read<bool>()) { // mode_ref_delta_enabled
|
||||
return;
|
||||
}
|
||||
if (!br.Read<bool>()) { // mode_ref_delta_update
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < kVp9MaxRefLFDeltas; i++) {
|
||||
if (br.Read<bool>()) { // update_ref_delta
|
||||
br.ConsumeBits(7);
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < kVp9MaxModeLFDeltas; i++) {
|
||||
if (br.Read<bool>()) { // update_mode_delta
|
||||
br.ConsumeBits(7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Vp9ReadQp(BitstreamReader& br, Vp9UncompressedHeader* frame_info) {
|
||||
frame_info->base_qp = br.Read<uint8_t>();
|
||||
|
||||
// yuv offsets
|
||||
frame_info->is_lossless = frame_info->base_qp == 0;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (br.Read<bool>()) { // if delta_coded
|
||||
// delta_q is a signed integer with leading 4 bits containing absolute
|
||||
// value and last bit containing sign. There are are two ways to represent
|
||||
// zero with such encoding.
|
||||
if ((br.ReadBits(5) & 0b1111'0) != 0) { // delta_q
|
||||
frame_info->is_lossless = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Vp9ReadSegmentationParams(BitstreamReader& br,
|
||||
Vp9UncompressedHeader* frame_info) {
|
||||
constexpr int kSegmentationFeatureBits[kVp9SegLvlMax] = {8, 6, 2, 0};
|
||||
constexpr bool kSegmentationFeatureSigned[kVp9SegLvlMax] = {true, true, false,
|
||||
false};
|
||||
|
||||
frame_info->segmentation_enabled = br.Read<bool>();
|
||||
if (!frame_info->segmentation_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (br.Read<bool>()) { // update_map
|
||||
frame_info->segmentation_tree_probs.emplace();
|
||||
for (int i = 0; i < 7; ++i) {
|
||||
if (br.Read<bool>()) {
|
||||
(*frame_info->segmentation_tree_probs)[i] = br.Read<uint8_t>();
|
||||
} else {
|
||||
(*frame_info->segmentation_tree_probs)[i] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
// temporal_update
|
||||
frame_info->segmentation_pred_prob.emplace();
|
||||
if (br.Read<bool>()) {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (br.Read<bool>()) {
|
||||
(*frame_info->segmentation_pred_prob)[i] = br.Read<uint8_t>();
|
||||
} else {
|
||||
(*frame_info->segmentation_pred_prob)[i] = 255;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
frame_info->segmentation_pred_prob->fill(255);
|
||||
}
|
||||
}
|
||||
|
||||
if (br.Read<bool>()) { // segmentation_update_data
|
||||
frame_info->segmentation_is_delta = br.Read<bool>();
|
||||
for (size_t i = 0; i < kVp9MaxSegments; ++i) {
|
||||
for (size_t j = 0; j < kVp9SegLvlMax; ++j) {
|
||||
if (!br.Read<bool>()) { // feature_enabled
|
||||
continue;
|
||||
}
|
||||
if (kSegmentationFeatureBits[j] == 0) {
|
||||
// No feature bits used and no sign, just mark it and return.
|
||||
frame_info->segmentation_features[i][j] = 1;
|
||||
continue;
|
||||
}
|
||||
frame_info->segmentation_features[i][j] =
|
||||
br.ReadBits(kSegmentationFeatureBits[j]);
|
||||
if (kSegmentationFeatureSigned[j] && br.Read<bool>()) {
|
||||
(*frame_info->segmentation_features[i][j]) *= -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Vp9ReadTileInfo(BitstreamReader& br, Vp9UncompressedHeader* frame_info) {
|
||||
size_t mi_cols = (frame_info->frame_width + 7) >> 3;
|
||||
size_t sb64_cols = (mi_cols + 7) >> 3;
|
||||
|
||||
size_t min_log2 = 0;
|
||||
while ((kVp9MaxTileWidthB64 << min_log2) < sb64_cols) {
|
||||
++min_log2;
|
||||
}
|
||||
|
||||
size_t max_log2 = 1;
|
||||
while ((sb64_cols >> max_log2) >= kVp9MinTileWidthB64) {
|
||||
++max_log2;
|
||||
}
|
||||
--max_log2;
|
||||
|
||||
frame_info->tile_cols_log2 = min_log2;
|
||||
while (frame_info->tile_cols_log2 < max_log2) {
|
||||
if (br.Read<bool>()) {
|
||||
++frame_info->tile_cols_log2;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
frame_info->tile_rows_log2 = 0;
|
||||
if (br.Read<bool>()) {
|
||||
++frame_info->tile_rows_log2;
|
||||
if (br.Read<bool>()) {
|
||||
++frame_info->tile_rows_log2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Vp9InterpolationFilter kLiteralToType[4] = {
|
||||
Vp9InterpolationFilter::kEightTapSmooth, Vp9InterpolationFilter::kEightTap,
|
||||
Vp9InterpolationFilter::kEightTapSharp, Vp9InterpolationFilter::kBilinear};
|
||||
} // namespace
|
||||
|
||||
std::string Vp9UncompressedHeader::ToString() const {
|
||||
char buf[1024];
|
||||
rtc::SimpleStringBuilder oss(buf);
|
||||
|
||||
oss << "Vp9UncompressedHeader { "
|
||||
<< "profile = " << profile;
|
||||
|
||||
if (show_existing_frame) {
|
||||
oss << ", show_existing_frame = " << *show_existing_frame << " }";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
oss << ", frame type = " << (is_keyframe ? "key" : "delta")
|
||||
<< ", show_frame = " << (show_frame ? "true" : "false")
|
||||
<< ", error_resilient = " << (error_resilient ? "true" : "false");
|
||||
|
||||
oss << ", bit_depth = ";
|
||||
switch (bit_detph) {
|
||||
case Vp9BitDept::k8Bit:
|
||||
oss << "8bit";
|
||||
break;
|
||||
case Vp9BitDept::k10Bit:
|
||||
oss << "10bit";
|
||||
break;
|
||||
case Vp9BitDept::k12Bit:
|
||||
oss << "12bit";
|
||||
break;
|
||||
}
|
||||
|
||||
if (color_space) {
|
||||
oss << ", color_space = ";
|
||||
switch (*color_space) {
|
||||
case Vp9ColorSpace::CS_UNKNOWN:
|
||||
oss << "unknown";
|
||||
break;
|
||||
case Vp9ColorSpace::CS_BT_601:
|
||||
oss << "CS_BT_601 Rec. ITU-R BT.601-7";
|
||||
break;
|
||||
case Vp9ColorSpace::CS_BT_709:
|
||||
oss << "Rec. ITU-R BT.709-6";
|
||||
break;
|
||||
case Vp9ColorSpace::CS_SMPTE_170:
|
||||
oss << "SMPTE-170";
|
||||
break;
|
||||
case Vp9ColorSpace::CS_SMPTE_240:
|
||||
oss << "SMPTE-240";
|
||||
break;
|
||||
case Vp9ColorSpace::CS_BT_2020:
|
||||
oss << "Rec. ITU-R BT.2020-2";
|
||||
break;
|
||||
case Vp9ColorSpace::CS_RESERVED:
|
||||
oss << "Reserved";
|
||||
break;
|
||||
case Vp9ColorSpace::CS_RGB:
|
||||
oss << "sRGB (IEC 61966-2-1)";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (color_range) {
|
||||
oss << ", color_range = ";
|
||||
switch (*color_range) {
|
||||
case Vp9ColorRange::kFull:
|
||||
oss << "full";
|
||||
break;
|
||||
case Vp9ColorRange::kStudio:
|
||||
oss << "studio";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (sub_sampling) {
|
||||
oss << ", sub_sampling = ";
|
||||
switch (*sub_sampling) {
|
||||
case Vp9YuvSubsampling::k444:
|
||||
oss << "444";
|
||||
break;
|
||||
case Vp9YuvSubsampling::k440:
|
||||
oss << "440";
|
||||
break;
|
||||
case Vp9YuvSubsampling::k422:
|
||||
oss << "422";
|
||||
break;
|
||||
case Vp9YuvSubsampling::k420:
|
||||
oss << "420";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (infer_size_from_reference) {
|
||||
oss << ", infer_frame_resolution_from = " << *infer_size_from_reference;
|
||||
} else {
|
||||
oss << ", frame_width = " << frame_width
|
||||
<< ", frame_height = " << frame_height;
|
||||
}
|
||||
if (render_width != 0 && render_height != 0) {
|
||||
oss << ", render_width = " << render_width
|
||||
<< ", render_height = " << render_height;
|
||||
}
|
||||
|
||||
oss << ", base qp = " << base_qp;
|
||||
if (reference_buffers[0] != -1) {
|
||||
oss << ", last_buffer = " << reference_buffers[0];
|
||||
}
|
||||
if (reference_buffers[1] != -1) {
|
||||
oss << ", golden_buffer = " << reference_buffers[1];
|
||||
}
|
||||
if (reference_buffers[2] != -1) {
|
||||
oss << ", altref_buffer = " << reference_buffers[2];
|
||||
}
|
||||
|
||||
oss << ", updated buffers = { ";
|
||||
bool first = true;
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
if (updated_buffers.test(i)) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
oss << ", ";
|
||||
}
|
||||
oss << i;
|
||||
}
|
||||
}
|
||||
oss << " }";
|
||||
|
||||
oss << ", compressed_header_size_bytes = " << compressed_header_size;
|
||||
|
||||
oss << " }";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
void Parse(BitstreamReader& br,
|
||||
Vp9UncompressedHeader* frame_info,
|
||||
bool qp_only) {
|
||||
const size_t total_buffer_size_bits = br.RemainingBitCount();
|
||||
|
||||
// Frame marker.
|
||||
if (br.ReadBits(2) != 0b10) {
|
||||
RTC_LOG(LS_WARNING) << "Failed to parse header. Frame marker should be 2.";
|
||||
br.Invalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
// Profile has low bit first.
|
||||
frame_info->profile = br.ReadBit();
|
||||
frame_info->profile |= br.ReadBit() << 1;
|
||||
if (frame_info->profile > 2 && br.Read<bool>()) {
|
||||
RTC_LOG(LS_WARNING)
|
||||
<< "Failed to parse header. Unsupported bitstream profile.";
|
||||
br.Invalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
// Show existing frame.
|
||||
if (br.Read<bool>()) {
|
||||
frame_info->show_existing_frame = br.ReadBits(3);
|
||||
return;
|
||||
}
|
||||
|
||||
// Frame type: KEY_FRAME(0), INTER_FRAME(1).
|
||||
frame_info->is_keyframe = !br.Read<bool>();
|
||||
frame_info->show_frame = br.Read<bool>();
|
||||
frame_info->error_resilient = br.Read<bool>();
|
||||
|
||||
if (frame_info->is_keyframe) {
|
||||
if (br.ReadBits(24) != 0x498342) {
|
||||
RTC_LOG(LS_WARNING) << "Failed to parse header. Invalid sync code.";
|
||||
br.Invalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
Vp9ReadColorConfig(br, frame_info);
|
||||
Vp9ReadFrameSize(br, frame_info);
|
||||
Vp9ReadRenderSize(total_buffer_size_bits, br, frame_info);
|
||||
|
||||
// Key-frames implicitly update all buffers.
|
||||
frame_info->updated_buffers.set();
|
||||
} else {
|
||||
// Non-keyframe.
|
||||
bool is_intra_only = false;
|
||||
if (!frame_info->show_frame) {
|
||||
is_intra_only = br.Read<bool>();
|
||||
}
|
||||
if (!frame_info->error_resilient) {
|
||||
br.ConsumeBits(2); // Reset frame context.
|
||||
}
|
||||
|
||||
if (is_intra_only) {
|
||||
if (br.ReadBits(24) != 0x498342) {
|
||||
RTC_LOG(LS_WARNING) << "Failed to parse header. Invalid sync code.";
|
||||
br.Invalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame_info->profile > 0) {
|
||||
Vp9ReadColorConfig(br, frame_info);
|
||||
} else {
|
||||
frame_info->color_space = Vp9ColorSpace::CS_BT_601;
|
||||
frame_info->sub_sampling = Vp9YuvSubsampling::k420;
|
||||
frame_info->bit_detph = Vp9BitDept::k8Bit;
|
||||
}
|
||||
frame_info->reference_buffers.fill(-1);
|
||||
ReadRefreshFrameFlags(br, frame_info);
|
||||
Vp9ReadFrameSize(br, frame_info);
|
||||
Vp9ReadRenderSize(total_buffer_size_bits, br, frame_info);
|
||||
} else {
|
||||
ReadRefreshFrameFlags(br, frame_info);
|
||||
|
||||
frame_info->reference_buffers_sign_bias[0] = false;
|
||||
for (size_t i = 0; i < kVp9NumRefsPerFrame; i++) {
|
||||
frame_info->reference_buffers[i] = br.ReadBits(3);
|
||||
frame_info->reference_buffers_sign_bias[Vp9ReferenceFrame::kLast + i] =
|
||||
br.Read<bool>();
|
||||
}
|
||||
|
||||
Vp9ReadFrameSizeFromRefs(br, frame_info);
|
||||
Vp9ReadRenderSize(total_buffer_size_bits, br, frame_info);
|
||||
|
||||
frame_info->allow_high_precision_mv = br.Read<bool>();
|
||||
|
||||
// Interpolation filter.
|
||||
if (br.Read<bool>()) {
|
||||
frame_info->interpolation_filter = Vp9InterpolationFilter::kSwitchable;
|
||||
} else {
|
||||
frame_info->interpolation_filter = kLiteralToType[br.ReadBits(2)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!frame_info->error_resilient) {
|
||||
// 1 bit: Refresh frame context.
|
||||
// 1 bit: Frame parallel decoding mode.
|
||||
br.ConsumeBits(2);
|
||||
}
|
||||
|
||||
// Frame context index.
|
||||
frame_info->frame_context_idx = br.ReadBits(2);
|
||||
|
||||
Vp9ReadLoopfilter(br);
|
||||
|
||||
// Read base QP.
|
||||
Vp9ReadQp(br, frame_info);
|
||||
|
||||
if (qp_only) {
|
||||
// Not interested in the rest of the header, return early.
|
||||
return;
|
||||
}
|
||||
|
||||
Vp9ReadSegmentationParams(br, frame_info);
|
||||
Vp9ReadTileInfo(br, frame_info);
|
||||
frame_info->compressed_header_size = br.Read<uint16_t>();
|
||||
frame_info->uncompressed_header_size =
|
||||
(total_buffer_size_bits / 8) - (br.RemainingBitCount() / 8);
|
||||
}
|
||||
|
||||
absl::optional<Vp9UncompressedHeader> ParseUncompressedVp9Header(
|
||||
rtc::ArrayView<const uint8_t> buf) {
|
||||
BitstreamReader reader(buf);
|
||||
Vp9UncompressedHeader frame_info;
|
||||
Parse(reader, &frame_info, /*qp_only=*/false);
|
||||
if (reader.Ok() && frame_info.frame_width > 0) {
|
||||
return frame_info;
|
||||
}
|
||||
return absl::nullopt;
|
||||
}
|
||||
|
||||
namespace vp9 {
|
||||
|
||||
bool GetQp(const uint8_t* buf, size_t length, int* qp) {
|
||||
BitstreamReader reader(rtc::MakeArrayView(buf, length));
|
||||
Vp9UncompressedHeader frame_info;
|
||||
Parse(reader, &frame_info, /*qp_only=*/true);
|
||||
if (!reader.Ok()) {
|
||||
return false;
|
||||
}
|
||||
*qp = frame_info.base_qp;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace vp9
|
||||
} // namespace webrtc
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* Copyright (c) 2017 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.
|
||||
*/
|
||||
|
||||
#ifndef MODULES_VIDEO_CODING_UTILITY_VP9_UNCOMPRESSED_HEADER_PARSER_H_
|
||||
#define MODULES_VIDEO_CODING_UTILITY_VP9_UNCOMPRESSED_HEADER_PARSER_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <array>
|
||||
#include <bitset>
|
||||
#include <string>
|
||||
|
||||
#include "absl/types/optional.h"
|
||||
#include "api/array_view.h"
|
||||
#include "modules/video_coding/utility/vp9_constants.h"
|
||||
|
||||
namespace webrtc {
|
||||
|
||||
namespace vp9 {
|
||||
|
||||
// Gets the QP, QP range: [0, 255].
|
||||
// Returns true on success, false otherwise.
|
||||
bool GetQp(const uint8_t* buf, size_t length, int* qp);
|
||||
|
||||
} // namespace vp9
|
||||
|
||||
// Bit depth per channel. Support varies by profile.
|
||||
enum class Vp9BitDept : uint8_t {
|
||||
k8Bit = 8,
|
||||
k10Bit = 10,
|
||||
k12Bit = 12,
|
||||
};
|
||||
|
||||
enum class Vp9ColorSpace : uint8_t {
|
||||
CS_UNKNOWN = 0, // Unknown (in this case the color space must be signaled
|
||||
// outside the VP9 bitstream).
|
||||
CS_BT_601 = 1, // CS_BT_601 Rec. ITU-R BT.601-7
|
||||
CS_BT_709 = 2, // Rec. ITU-R BT.709-6
|
||||
CS_SMPTE_170 = 3, // SMPTE-170
|
||||
CS_SMPTE_240 = 4, // SMPTE-240
|
||||
CS_BT_2020 = 5, // Rec. ITU-R BT.2020-2
|
||||
CS_RESERVED = 6, // Reserved
|
||||
CS_RGB = 7, // sRGB (IEC 61966-2-1)
|
||||
};
|
||||
|
||||
enum class Vp9ColorRange {
|
||||
kStudio, // Studio swing:
|
||||
// For BitDepth equals 8:
|
||||
// Y is between 16 and 235 inclusive.
|
||||
// U and V are between 16 and 240 inclusive.
|
||||
// For BitDepth equals 10:
|
||||
// Y is between 64 and 940 inclusive.
|
||||
// U and V are between 64 and 960 inclusive.
|
||||
// For BitDepth equals 12:
|
||||
// Y is between 256 and 3760.
|
||||
// U and V are between 256 and 3840 inclusive.
|
||||
kFull // Full swing; no restriction on Y, U, V values.
|
||||
};
|
||||
|
||||
enum class Vp9YuvSubsampling {
|
||||
k444,
|
||||
k440,
|
||||
k422,
|
||||
k420,
|
||||
};
|
||||
|
||||
enum Vp9ReferenceFrame : int {
|
||||
kNone = -1,
|
||||
kIntra = 0,
|
||||
kLast = 1,
|
||||
kGolden = 2,
|
||||
kAltref = 3,
|
||||
};
|
||||
|
||||
enum class Vp9InterpolationFilter : uint8_t {
|
||||
kEightTap = 0,
|
||||
kEightTapSmooth = 1,
|
||||
kEightTapSharp = 2,
|
||||
kBilinear = 3,
|
||||
kSwitchable = 4
|
||||
};
|
||||
|
||||
struct Vp9UncompressedHeader {
|
||||
int profile = 0; // Profiles 0-3 are valid.
|
||||
absl::optional<uint8_t> show_existing_frame;
|
||||
bool is_keyframe = false;
|
||||
bool show_frame = false;
|
||||
bool error_resilient = false;
|
||||
Vp9BitDept bit_detph = Vp9BitDept::k8Bit;
|
||||
absl::optional<Vp9ColorSpace> color_space;
|
||||
absl::optional<Vp9ColorRange> color_range;
|
||||
absl::optional<Vp9YuvSubsampling> sub_sampling;
|
||||
int frame_width = 0;
|
||||
int frame_height = 0;
|
||||
int render_width = 0;
|
||||
int render_height = 0;
|
||||
// Width/height of the tiles used (in units of 8x8 blocks).
|
||||
size_t tile_cols_log2 = 0; // tile_cols = 1 << tile_cols_log2
|
||||
size_t tile_rows_log2 = 0; // tile_rows = 1 << tile_rows_log2
|
||||
absl::optional<size_t> render_size_offset_bits;
|
||||
Vp9InterpolationFilter interpolation_filter =
|
||||
Vp9InterpolationFilter::kEightTap;
|
||||
bool allow_high_precision_mv = false;
|
||||
int base_qp = 0;
|
||||
bool is_lossless = false;
|
||||
uint8_t frame_context_idx = 0;
|
||||
|
||||
bool segmentation_enabled = false;
|
||||
absl::optional<std::array<uint8_t, 7>> segmentation_tree_probs;
|
||||
absl::optional<std::array<uint8_t, 3>> segmentation_pred_prob;
|
||||
bool segmentation_is_delta = false;
|
||||
std::array<std::array<absl::optional<int>, kVp9SegLvlMax>, kVp9MaxSegments>
|
||||
segmentation_features;
|
||||
|
||||
// Which of the 8 reference buffers may be used as references for this frame.
|
||||
// -1 indicates not used (e.g. {-1, -1, -1} for intra-only frames).
|
||||
std::array<int, kVp9RefsPerFrame> reference_buffers = {-1, -1, -1};
|
||||
// Sign bias corresponding to reference buffers, where the index is a
|
||||
// ReferenceFrame.
|
||||
// false/0 indidate backwards reference, true/1 indicate forwards reference).
|
||||
std::bitset<kVp9MaxRefFrames> reference_buffers_sign_bias = 0;
|
||||
|
||||
// Indicates which reference buffer [0,7] to infer the frame size from.
|
||||
absl::optional<int> infer_size_from_reference;
|
||||
// Which of the 8 reference buffers are updated by this frame.
|
||||
std::bitset<kVp9NumRefFrames> updated_buffers = 0;
|
||||
|
||||
// Header sizes, in bytes.
|
||||
uint32_t uncompressed_header_size = 0;
|
||||
uint32_t compressed_header_size = 0;
|
||||
|
||||
bool is_intra_only() const {
|
||||
return reference_buffers[0] == -1 && reference_buffers[1] == -1 &&
|
||||
reference_buffers[2] == -1;
|
||||
}
|
||||
|
||||
std::string ToString() const;
|
||||
};
|
||||
|
||||
// Parses the uncompressed header and populates (most) values in a
|
||||
// UncompressedHeader struct. Returns nullopt on failure.
|
||||
absl::optional<Vp9UncompressedHeader> ParseUncompressedVp9Header(
|
||||
rtc::ArrayView<const uint8_t> buf);
|
||||
|
||||
} // namespace webrtc
|
||||
|
||||
#endif // MODULES_VIDEO_CODING_UTILITY_VP9_UNCOMPRESSED_HEADER_PARSER_H_
|
||||
Loading…
Add table
Add a link
Reference in a new issue