Add ability to downscale content to improve quality.
BUG=3712 R=marpan@google.com, stefan@webrtc.org Review URL: https://webrtc-codereview.appspot.com/18169004 git-svn-id: http://webrtc.googlecode.com/svn/trunk@7164 4adac7df-926f-26a2-2b94-8c16560cd09d
This commit is contained in:
parent
b5e6bfc76a
commit
a0d7827b16
@ -246,6 +246,7 @@
|
|||||||
'video_coding/main/source/qm_select_unittest.cc',
|
'video_coding/main/source/qm_select_unittest.cc',
|
||||||
'video_coding/main/source/test/stream_generator.cc',
|
'video_coding/main/source/test/stream_generator.cc',
|
||||||
'video_coding/main/source/test/stream_generator.h',
|
'video_coding/main/source/test/stream_generator.h',
|
||||||
|
'video_coding/utility/quality_scaler_unittest.cc',
|
||||||
'video_processing/main/test/unit_test/brightness_detection_test.cc',
|
'video_processing/main/test/unit_test/brightness_detection_test.cc',
|
||||||
'video_processing/main/test/unit_test/color_enhancement_test.cc',
|
'video_processing/main/test/unit_test/color_enhancement_test.cc',
|
||||||
'video_processing/main/test/unit_test/content_metrics_test.cc',
|
'video_processing/main/test/unit_test/content_metrics_test.cc',
|
||||||
|
@ -703,34 +703,26 @@ TEST_F(VideoProcessorIntegrationTest,
|
|||||||
rc_metrics);
|
rc_metrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run with no packet loss, at low bitrate, then increase rate somewhat.
|
// Run with no packet loss, at low bitrate. During this time we should've
|
||||||
// Key frame is thrown in every 120 frames. Can expect some frame drops after
|
// resized once.
|
||||||
// key frame, even at high rate. The internal spatial resizer is on, so expect
|
|
||||||
// spatial resize down at first key frame, and back up at second key frame.
|
|
||||||
// Error_concealment is off in this test since there is a memory leak with
|
|
||||||
// resizing and error concealment.
|
|
||||||
TEST_F(VideoProcessorIntegrationTest,
|
TEST_F(VideoProcessorIntegrationTest,
|
||||||
DISABLED_ON_ANDROID(ProcessNoLossSpatialResizeFrameDrop)) {
|
DISABLED_ON_ANDROID(ProcessNoLossSpatialResizeFrameDrop)) {
|
||||||
config_.networking_config.packet_loss_probability = 0;
|
config_.networking_config.packet_loss_probability = 0;
|
||||||
// Bitrate and frame rate profile.
|
// Bitrate and frame rate profile.
|
||||||
RateProfile rate_profile;
|
RateProfile rate_profile;
|
||||||
SetRateProfilePars(&rate_profile, 0, 100, 30, 0);
|
SetRateProfilePars(&rate_profile, 0, 50, 30, 0);
|
||||||
SetRateProfilePars(&rate_profile, 1, 200, 30, 120);
|
rate_profile.frame_index_rate_update[1] = kNbrFramesLong + 1;
|
||||||
SetRateProfilePars(&rate_profile, 2, 200, 30, 240);
|
|
||||||
rate_profile.frame_index_rate_update[3] = kNbrFramesLong + 1;
|
|
||||||
rate_profile.num_frames = kNbrFramesLong;
|
rate_profile.num_frames = kNbrFramesLong;
|
||||||
// Codec/network settings.
|
// Codec/network settings.
|
||||||
CodecConfigPars process_settings;
|
CodecConfigPars process_settings;
|
||||||
SetCodecParameters(&process_settings, 0.0f, 120, 1, false, true, true, true);
|
SetCodecParameters(
|
||||||
// Metrics for expected quality.: lower quality on average from up-sampling
|
&process_settings, 0.0f, kNbrFramesLong, 1, false, true, true, true);
|
||||||
// the down-sampled portion of the run, in case resizer is on.
|
// Metrics for expected quality.
|
||||||
QualityMetrics quality_metrics;
|
QualityMetrics quality_metrics;
|
||||||
SetQualityMetrics(&quality_metrics, 29.0, 20.0, 0.75, 0.60);
|
SetQualityMetrics(&quality_metrics, 25.0, 15.0, 0.70, 0.40);
|
||||||
// Metrics for rate control.
|
// Metrics for rate control.
|
||||||
RateControlMetrics rc_metrics[3];
|
RateControlMetrics rc_metrics[1];
|
||||||
SetRateControlMetrics(rc_metrics, 0, 45, 30, 75, 20, 70, 0);
|
SetRateControlMetrics(rc_metrics, 0, 160, 60, 120, 20, 70, 1);
|
||||||
SetRateControlMetrics(rc_metrics, 1, 20, 35, 30, 20, 15, 1);
|
|
||||||
SetRateControlMetrics(rc_metrics, 2, 0, 30, 30, 15, 25, 1);
|
|
||||||
ProcessFramesAndVerify(quality_metrics,
|
ProcessFramesAndVerify(quality_metrics,
|
||||||
rate_profile,
|
rate_profile,
|
||||||
process_settings,
|
process_settings,
|
||||||
|
@ -105,6 +105,7 @@ int VP8EncoderImpl::SetRates(uint32_t new_bitrate_kbit,
|
|||||||
temporal_layers_->ConfigureBitrates(new_bitrate_kbit, codec_.maxBitrate,
|
temporal_layers_->ConfigureBitrates(new_bitrate_kbit, codec_.maxBitrate,
|
||||||
new_framerate, config_);
|
new_framerate, config_);
|
||||||
codec_.maxFramerate = new_framerate;
|
codec_.maxFramerate = new_framerate;
|
||||||
|
quality_scaler_.ReportFramerate(new_framerate);
|
||||||
|
|
||||||
// update encoder context
|
// update encoder context
|
||||||
if (vpx_codec_enc_config_set(encoder_, config_)) {
|
if (vpx_codec_enc_config_set(encoder_, config_)) {
|
||||||
@ -230,8 +231,8 @@ int VP8EncoderImpl::InitEncode(const VideoCodec* inst,
|
|||||||
30 : 0;
|
30 : 0;
|
||||||
config_->rc_end_usage = VPX_CBR;
|
config_->rc_end_usage = VPX_CBR;
|
||||||
config_->g_pass = VPX_RC_ONE_PASS;
|
config_->g_pass = VPX_RC_ONE_PASS;
|
||||||
config_->rc_resize_allowed = inst->codecSpecific.VP8.automaticResizeOn ?
|
// Handle resizing outside of libvpx.
|
||||||
1 : 0;
|
config_->rc_resize_allowed = 0;
|
||||||
config_->rc_min_quantizer = 2;
|
config_->rc_min_quantizer = 2;
|
||||||
config_->rc_max_quantizer = inst->qpMax;
|
config_->rc_max_quantizer = inst->qpMax;
|
||||||
config_->rc_undershoot_pct = 100;
|
config_->rc_undershoot_pct = 100;
|
||||||
@ -272,6 +273,8 @@ int VP8EncoderImpl::InitEncode(const VideoCodec* inst,
|
|||||||
cpu_speed_ = -12;
|
cpu_speed_ = -12;
|
||||||
#endif
|
#endif
|
||||||
rps_->Init();
|
rps_->Init();
|
||||||
|
quality_scaler_.Init(codec_.qpMax);
|
||||||
|
quality_scaler_.ReportFramerate(codec_.maxFramerate);
|
||||||
return InitAndSetControlSettings(inst);
|
return InitAndSetControlSettings(inst);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,6 +299,7 @@ int VP8EncoderImpl::InitAndSetControlSettings(const VideoCodec* inst) {
|
|||||||
vpx_codec_control(encoder_, VP8E_SET_MAX_INTRA_BITRATE_PCT,
|
vpx_codec_control(encoder_, VP8E_SET_MAX_INTRA_BITRATE_PCT,
|
||||||
rc_max_intra_target_);
|
rc_max_intra_target_);
|
||||||
inited_ = true;
|
inited_ = true;
|
||||||
|
|
||||||
return WEBRTC_VIDEO_CODEC_OK;
|
return WEBRTC_VIDEO_CODEC_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -315,15 +319,15 @@ uint32_t VP8EncoderImpl::MaxIntraTarget(uint32_t optimalBuffersize) {
|
|||||||
return (targetPct < minIntraTh) ? minIntraTh: targetPct;
|
return (targetPct < minIntraTh) ? minIntraTh: targetPct;
|
||||||
}
|
}
|
||||||
|
|
||||||
int VP8EncoderImpl::Encode(const I420VideoFrame& input_image,
|
int VP8EncoderImpl::Encode(const I420VideoFrame& input_frame,
|
||||||
const CodecSpecificInfo* codec_specific_info,
|
const CodecSpecificInfo* codec_specific_info,
|
||||||
const std::vector<VideoFrameType>* frame_types) {
|
const std::vector<VideoFrameType>* frame_types) {
|
||||||
TRACE_EVENT1("webrtc", "VP8::Encode", "timestamp", input_image.timestamp());
|
TRACE_EVENT1("webrtc", "VP8::Encode", "timestamp", input_frame.timestamp());
|
||||||
|
|
||||||
if (!inited_) {
|
if (!inited_) {
|
||||||
return WEBRTC_VIDEO_CODEC_UNINITIALIZED;
|
return WEBRTC_VIDEO_CODEC_UNINITIALIZED;
|
||||||
}
|
}
|
||||||
if (input_image.IsZeroSize()) {
|
if (input_frame.IsZeroSize()) {
|
||||||
return WEBRTC_VIDEO_CODEC_ERR_PARAMETER;
|
return WEBRTC_VIDEO_CODEC_ERR_PARAMETER;
|
||||||
}
|
}
|
||||||
if (encoded_complete_callback_ == NULL) {
|
if (encoded_complete_callback_ == NULL) {
|
||||||
@ -336,25 +340,31 @@ int VP8EncoderImpl::Encode(const I420VideoFrame& input_image,
|
|||||||
frame_type = (*frame_types)[0];
|
frame_type = (*frame_types)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const I420VideoFrame& frame =
|
||||||
|
config_->rc_dropframe_thresh > 0 &&
|
||||||
|
codec_.codecSpecific.VP8.automaticResizeOn
|
||||||
|
? quality_scaler_.GetScaledFrame(input_frame)
|
||||||
|
: input_frame;
|
||||||
|
|
||||||
// Check for change in frame size.
|
// Check for change in frame size.
|
||||||
if (input_image.width() != codec_.width ||
|
if (frame.width() != codec_.width ||
|
||||||
input_image.height() != codec_.height) {
|
frame.height() != codec_.height) {
|
||||||
int ret = UpdateCodecFrameSize(input_image);
|
int ret = UpdateCodecFrameSize(frame);
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Image in vpx_image_t format.
|
// Image in vpx_image_t format.
|
||||||
// Input image is const. VP8's raw image is not defined as const.
|
// Input frame is const. VP8's raw frame is not defined as const.
|
||||||
raw_->planes[PLANE_Y] = const_cast<uint8_t*>(input_image.buffer(kYPlane));
|
raw_->planes[PLANE_Y] = const_cast<uint8_t*>(frame.buffer(kYPlane));
|
||||||
raw_->planes[PLANE_U] = const_cast<uint8_t*>(input_image.buffer(kUPlane));
|
raw_->planes[PLANE_U] = const_cast<uint8_t*>(frame.buffer(kUPlane));
|
||||||
raw_->planes[PLANE_V] = const_cast<uint8_t*>(input_image.buffer(kVPlane));
|
raw_->planes[PLANE_V] = const_cast<uint8_t*>(frame.buffer(kVPlane));
|
||||||
// TODO(mikhal): Stride should be set in initialization.
|
// TODO(mikhal): Stride should be set in initialization.
|
||||||
raw_->stride[VPX_PLANE_Y] = input_image.stride(kYPlane);
|
raw_->stride[VPX_PLANE_Y] = frame.stride(kYPlane);
|
||||||
raw_->stride[VPX_PLANE_U] = input_image.stride(kUPlane);
|
raw_->stride[VPX_PLANE_U] = frame.stride(kUPlane);
|
||||||
raw_->stride[VPX_PLANE_V] = input_image.stride(kVPlane);
|
raw_->stride[VPX_PLANE_V] = frame.stride(kVPlane);
|
||||||
|
|
||||||
int flags = temporal_layers_->EncodeFlags(input_image.timestamp());
|
int flags = temporal_layers_->EncodeFlags(frame.timestamp());
|
||||||
|
|
||||||
bool send_keyframe = (frame_type == kKeyFrame);
|
bool send_keyframe = (frame_type == kKeyFrame);
|
||||||
if (send_keyframe) {
|
if (send_keyframe) {
|
||||||
@ -370,11 +380,11 @@ int VP8EncoderImpl::Encode(const I420VideoFrame& input_image,
|
|||||||
codec_specific_info->codecSpecific.VP8.pictureIdRPSI);
|
codec_specific_info->codecSpecific.VP8.pictureIdRPSI);
|
||||||
}
|
}
|
||||||
if (codec_specific_info->codecSpecific.VP8.hasReceivedSLI) {
|
if (codec_specific_info->codecSpecific.VP8.hasReceivedSLI) {
|
||||||
sendRefresh = rps_->ReceivedSLI(input_image.timestamp());
|
sendRefresh = rps_->ReceivedSLI(frame.timestamp());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
flags = rps_->EncodeFlags(picture_id_, sendRefresh,
|
flags = rps_->EncodeFlags(picture_id_, sendRefresh,
|
||||||
input_image.timestamp());
|
frame.timestamp());
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(holmer): Ideally the duration should be the timestamp diff of this
|
// TODO(holmer): Ideally the duration should be the timestamp diff of this
|
||||||
@ -390,7 +400,7 @@ int VP8EncoderImpl::Encode(const I420VideoFrame& input_image,
|
|||||||
}
|
}
|
||||||
timestamp_ += duration;
|
timestamp_ += duration;
|
||||||
|
|
||||||
return GetEncodedPartitions(input_image);
|
return GetEncodedPartitions(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
int VP8EncoderImpl::UpdateCodecFrameSize(const I420VideoFrame& input_image) {
|
int VP8EncoderImpl::UpdateCodecFrameSize(const I420VideoFrame& input_image) {
|
||||||
@ -480,6 +490,11 @@ int VP8EncoderImpl::GetEncodedPartitions(const I420VideoFrame& input_image) {
|
|||||||
encoded_image_._encodedWidth = codec_.width;
|
encoded_image_._encodedWidth = codec_.width;
|
||||||
encoded_complete_callback_->Encoded(encoded_image_, &codec_specific,
|
encoded_complete_callback_->Encoded(encoded_image_, &codec_specific,
|
||||||
&frag_info);
|
&frag_info);
|
||||||
|
int qp;
|
||||||
|
vpx_codec_control(encoder_, VP8E_GET_LAST_QUANTIZER_64, &qp);
|
||||||
|
quality_scaler_.ReportEncodedFrame(qp);
|
||||||
|
} else {
|
||||||
|
quality_scaler_.ReportDroppedFrame();
|
||||||
}
|
}
|
||||||
return WEBRTC_VIDEO_CODEC_OK;
|
return WEBRTC_VIDEO_CODEC_OK;
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
#define WEBRTC_MODULES_VIDEO_CODING_CODECS_VP8_IMPL_H_
|
#define WEBRTC_MODULES_VIDEO_CODING_CODECS_VP8_IMPL_H_
|
||||||
|
|
||||||
#include "webrtc/modules/video_coding/codecs/vp8/include/vp8.h"
|
#include "webrtc/modules/video_coding/codecs/vp8/include/vp8.h"
|
||||||
|
#include "webrtc/modules/video_coding/utility/quality_scaler.h"
|
||||||
|
|
||||||
// VPX forward declaration
|
// VPX forward declaration
|
||||||
typedef struct vpx_codec_ctx vpx_codec_ctx_t;
|
typedef struct vpx_codec_ctx vpx_codec_ctx_t;
|
||||||
@ -139,6 +140,7 @@ class VP8EncoderImpl : public VP8Encoder {
|
|||||||
vpx_codec_ctx_t* encoder_;
|
vpx_codec_ctx_t* encoder_;
|
||||||
vpx_codec_enc_cfg_t* config_;
|
vpx_codec_enc_cfg_t* config_;
|
||||||
vpx_image_t* raw_;
|
vpx_image_t* raw_;
|
||||||
|
QualityScaler quality_scaler_;
|
||||||
}; // end of VP8Encoder class
|
}; // end of VP8Encoder class
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,8 +44,8 @@
|
|||||||
'../test/codec_database_test.cc',
|
'../test/codec_database_test.cc',
|
||||||
'../test/generic_codec_test.cc',
|
'../test/generic_codec_test.cc',
|
||||||
'../test/media_opt_test.cc',
|
'../test/media_opt_test.cc',
|
||||||
'../test/mt_test_common.cc',
|
|
||||||
'../test/mt_rx_tx_test.cc',
|
'../test/mt_rx_tx_test.cc',
|
||||||
|
'../test/mt_test_common.cc',
|
||||||
'../test/normal_test.cc',
|
'../test/normal_test.cc',
|
||||||
'../test/quality_modes_test.cc',
|
'../test/quality_modes_test.cc',
|
||||||
'../test/rtp_player.cc',
|
'../test/rtp_player.cc',
|
||||||
@ -53,8 +53,8 @@
|
|||||||
'../test/test_util.cc',
|
'../test/test_util.cc',
|
||||||
'../test/tester_main.cc',
|
'../test/tester_main.cc',
|
||||||
'../test/vcm_payload_sink_factory.cc',
|
'../test/vcm_payload_sink_factory.cc',
|
||||||
'../test/video_rtp_play_mt.cc',
|
|
||||||
'../test/video_rtp_play.cc',
|
'../test/video_rtp_play.cc',
|
||||||
|
'../test/video_rtp_play_mt.cc',
|
||||||
'../test/video_source.cc',
|
'../test/video_source.cc',
|
||||||
], # sources
|
], # sources
|
||||||
},
|
},
|
||||||
|
137
webrtc/modules/video_coding/utility/quality_scaler.cc
Normal file
137
webrtc/modules/video_coding/utility/quality_scaler.cc
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
* 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 "webrtc/modules/video_coding/utility/quality_scaler.h"
|
||||||
|
|
||||||
|
namespace webrtc {
|
||||||
|
|
||||||
|
static const int kMinFps = 10;
|
||||||
|
static const int kMeasureSeconds = 5;
|
||||||
|
static const int kFramedropPercentThreshold = 60;
|
||||||
|
static const int kLowQpThresholdDenominator = 3;
|
||||||
|
|
||||||
|
QualityScaler::QualityScaler()
|
||||||
|
: num_samples_(0), low_qp_threshold_(-1), downscale_shift_(0) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void QualityScaler::Init(int max_qp) {
|
||||||
|
ClearSamples();
|
||||||
|
downscale_shift_ = 0;
|
||||||
|
low_qp_threshold_ = max_qp / kLowQpThresholdDenominator ;
|
||||||
|
}
|
||||||
|
|
||||||
|
void QualityScaler::ReportFramerate(int framerate) {
|
||||||
|
num_samples_ = static_cast<size_t>(
|
||||||
|
kMeasureSeconds * (framerate < kMinFps ? kMinFps : framerate));
|
||||||
|
}
|
||||||
|
|
||||||
|
void QualityScaler::ReportEncodedFrame(int qp) {
|
||||||
|
average_qp_.AddSample(qp);
|
||||||
|
framedrop_percent_.AddSample(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void QualityScaler::ReportDroppedFrame() {
|
||||||
|
framedrop_percent_.AddSample(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
QualityScaler::Resolution QualityScaler::GetScaledResolution(
|
||||||
|
const I420VideoFrame& frame) {
|
||||||
|
// Both of these should be set through InitEncode -> Should be set by now.
|
||||||
|
assert(low_qp_threshold_ >= 0);
|
||||||
|
assert(num_samples_ > 0);
|
||||||
|
// Update scale factor.
|
||||||
|
int avg;
|
||||||
|
if (framedrop_percent_.GetAverage(num_samples_, &avg) &&
|
||||||
|
avg >= kFramedropPercentThreshold) {
|
||||||
|
AdjustScale(false);
|
||||||
|
} else if (average_qp_.GetAverage(num_samples_, &avg) &&
|
||||||
|
avg <= low_qp_threshold_) {
|
||||||
|
AdjustScale(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Resolution res;
|
||||||
|
res.width = frame.width();
|
||||||
|
res.height = frame.height();
|
||||||
|
|
||||||
|
assert(downscale_shift_ >= 0);
|
||||||
|
for (int shift = downscale_shift_;
|
||||||
|
shift > 0 && res.width > 1 && res.height > 1;
|
||||||
|
--shift) {
|
||||||
|
res.width >>= 1;
|
||||||
|
res.height >>= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
const I420VideoFrame& QualityScaler::GetScaledFrame(
|
||||||
|
const I420VideoFrame& frame) {
|
||||||
|
Resolution res = GetScaledResolution(frame);
|
||||||
|
if (res.width == frame.width())
|
||||||
|
return frame;
|
||||||
|
|
||||||
|
scaler_.Set(frame.width(),
|
||||||
|
frame.height(),
|
||||||
|
res.width,
|
||||||
|
res.height,
|
||||||
|
kI420,
|
||||||
|
kI420,
|
||||||
|
kScaleBox);
|
||||||
|
if (scaler_.Scale(frame, &scaled_frame_) != 0)
|
||||||
|
return frame;
|
||||||
|
|
||||||
|
scaled_frame_.set_ntp_time_ms(frame.ntp_time_ms());
|
||||||
|
scaled_frame_.set_timestamp(frame.timestamp());
|
||||||
|
scaled_frame_.set_render_time_ms(frame.render_time_ms());
|
||||||
|
|
||||||
|
return scaled_frame_;
|
||||||
|
}
|
||||||
|
|
||||||
|
QualityScaler::MovingAverage::MovingAverage() : sum_(0) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void QualityScaler::MovingAverage::AddSample(int sample) {
|
||||||
|
samples_.push_back(sample);
|
||||||
|
sum_ += sample;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QualityScaler::MovingAverage::GetAverage(size_t num_samples, int* avg) {
|
||||||
|
assert(num_samples > 0);
|
||||||
|
if (num_samples > samples_.size())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Remove old samples.
|
||||||
|
while (num_samples < samples_.size()) {
|
||||||
|
sum_ -= samples_.front();
|
||||||
|
samples_.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
*avg = sum_ / static_cast<int>(num_samples);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void QualityScaler::MovingAverage::Reset() {
|
||||||
|
sum_ = 0;
|
||||||
|
samples_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void QualityScaler::ClearSamples() {
|
||||||
|
average_qp_.Reset();
|
||||||
|
framedrop_percent_.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void QualityScaler::AdjustScale(bool up) {
|
||||||
|
downscale_shift_ += up ? -1 : 1;
|
||||||
|
if (downscale_shift_ < 0)
|
||||||
|
downscale_shift_ = 0;
|
||||||
|
ClearSamples();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace webrtc
|
65
webrtc/modules/video_coding/utility/quality_scaler.h
Normal file
65
webrtc/modules/video_coding/utility/quality_scaler.h
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* 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 WEBRTC_MODULES_VIDEO_CODING_UTILITY_QUALITY_SCALER_H_
|
||||||
|
#define WEBRTC_MODULES_VIDEO_CODING_UTILITY_QUALITY_SCALER_H_
|
||||||
|
|
||||||
|
#include <list>
|
||||||
|
|
||||||
|
#include "webrtc/common_video/libyuv/include/scaler.h"
|
||||||
|
|
||||||
|
namespace webrtc {
|
||||||
|
class QualityScaler {
|
||||||
|
public:
|
||||||
|
struct Resolution {
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
};
|
||||||
|
|
||||||
|
QualityScaler();
|
||||||
|
void Init(int max_qp);
|
||||||
|
|
||||||
|
void ReportFramerate(int framerate);
|
||||||
|
void ReportEncodedFrame(int qp);
|
||||||
|
void ReportDroppedFrame();
|
||||||
|
|
||||||
|
Resolution GetScaledResolution(const I420VideoFrame& frame);
|
||||||
|
const I420VideoFrame& GetScaledFrame(const I420VideoFrame& frame);
|
||||||
|
|
||||||
|
private:
|
||||||
|
class MovingAverage {
|
||||||
|
public:
|
||||||
|
MovingAverage();
|
||||||
|
void AddSample(int sample);
|
||||||
|
bool GetAverage(size_t num_samples, int* average);
|
||||||
|
void Reset();
|
||||||
|
|
||||||
|
private:
|
||||||
|
int sum_;
|
||||||
|
std::list<int> samples_;
|
||||||
|
};
|
||||||
|
|
||||||
|
void AdjustScale(bool up);
|
||||||
|
void ClearSamples();
|
||||||
|
|
||||||
|
Scaler scaler_;
|
||||||
|
I420VideoFrame scaled_frame_;
|
||||||
|
|
||||||
|
size_t num_samples_;
|
||||||
|
int low_qp_threshold_;
|
||||||
|
MovingAverage average_qp_;
|
||||||
|
MovingAverage framedrop_percent_;
|
||||||
|
|
||||||
|
int downscale_shift_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace webrtc
|
||||||
|
|
||||||
|
#endif // WEBRTC_MODULES_VIDEO_CODING_UTILITY_QUALITY_SCALER_H_
|
198
webrtc/modules/video_coding/utility/quality_scaler_unittest.cc
Normal file
198
webrtc/modules/video_coding/utility/quality_scaler_unittest.cc
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
/*
|
||||||
|
* 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 "webrtc/modules/video_coding/utility/quality_scaler.h"
|
||||||
|
|
||||||
|
#include "testing/gtest/include/gtest/gtest.h"
|
||||||
|
|
||||||
|
namespace webrtc {
|
||||||
|
namespace {
|
||||||
|
static const int kNumSeconds = 10;
|
||||||
|
static const int kWidth = 1920;
|
||||||
|
static const int kHalfWidth = kWidth / 2;
|
||||||
|
static const int kHeight = 1080;
|
||||||
|
static const int kFramerate = 30;
|
||||||
|
static const int kLowQp = 15;
|
||||||
|
static const int kNormalQp = 30;
|
||||||
|
static const int kMaxQp = 56;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class QualityScalerTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
enum ScaleDirection { kScaleDown, kScaleUp };
|
||||||
|
|
||||||
|
QualityScalerTest() {
|
||||||
|
input_frame_.CreateEmptyFrame(
|
||||||
|
kWidth, kHeight, kWidth, kHalfWidth, kHalfWidth);
|
||||||
|
qs_.Init(kMaxQp);
|
||||||
|
qs_.ReportFramerate(kFramerate);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TriggerScale(ScaleDirection scale_direction) {
|
||||||
|
int initial_width = qs_.GetScaledResolution(input_frame_).width;
|
||||||
|
for (int i = 0; i < kFramerate * kNumSeconds; ++i) {
|
||||||
|
switch (scale_direction) {
|
||||||
|
case kScaleUp:
|
||||||
|
qs_.ReportEncodedFrame(kLowQp);
|
||||||
|
break;
|
||||||
|
case kScaleDown:
|
||||||
|
qs_.ReportDroppedFrame();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qs_.GetScaledResolution(input_frame_).width != initial_width)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FAIL() << "No downscale within " << kNumSeconds << " seconds.";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExpectOriginalFrame() {
|
||||||
|
EXPECT_EQ(&input_frame_, &qs_.GetScaledFrame(input_frame_))
|
||||||
|
<< "Using scaled frame instead of original input.";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExpectScaleUsingReportedResolution() {
|
||||||
|
QualityScaler::Resolution res = qs_.GetScaledResolution(input_frame_);
|
||||||
|
const I420VideoFrame& scaled_frame = qs_.GetScaledFrame(input_frame_);
|
||||||
|
EXPECT_EQ(res.width, scaled_frame.width());
|
||||||
|
EXPECT_EQ(res.height, scaled_frame.height());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ContinuouslyDownscalesByHalfDimensionsAndBackUp();
|
||||||
|
|
||||||
|
void DoesNotDownscaleFrameDimensions(int width, int height);
|
||||||
|
|
||||||
|
QualityScaler qs_;
|
||||||
|
I420VideoFrame input_frame_;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(QualityScalerTest, UsesOriginalFrameInitially) {
|
||||||
|
ExpectOriginalFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QualityScalerTest, ReportsOriginalResolutionInitially) {
|
||||||
|
QualityScaler::Resolution res = qs_.GetScaledResolution(input_frame_);
|
||||||
|
EXPECT_EQ(input_frame_.width(), res.width);
|
||||||
|
EXPECT_EQ(input_frame_.height(), res.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QualityScalerTest, DownscalesAfterContinuousFramedrop) {
|
||||||
|
TriggerScale(kScaleDown);
|
||||||
|
QualityScaler::Resolution res = qs_.GetScaledResolution(input_frame_);
|
||||||
|
EXPECT_LT(res.width, input_frame_.width());
|
||||||
|
EXPECT_LT(res.height, input_frame_.height());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QualityScalerTest, DownscalesAfterTwoThirdsFramedrop) {
|
||||||
|
for (int i = 0; i < kFramerate * kNumSeconds / 3; ++i) {
|
||||||
|
qs_.ReportEncodedFrame(kNormalQp);
|
||||||
|
qs_.ReportDroppedFrame();
|
||||||
|
qs_.ReportDroppedFrame();
|
||||||
|
if (qs_.GetScaledResolution(input_frame_).width < input_frame_.width())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FAIL() << "No downscale within " << kNumSeconds << " seconds.";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QualityScalerTest, DoesNotDownscaleOnNormalQp) {
|
||||||
|
for (int i = 0; i < kFramerate * kNumSeconds; ++i) {
|
||||||
|
qs_.ReportEncodedFrame(kNormalQp);
|
||||||
|
ASSERT_EQ(input_frame_.width(), qs_.GetScaledResolution(input_frame_).width)
|
||||||
|
<< "Unexpected scale on half framedrop.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QualityScalerTest, DoesNotDownscaleAfterHalfFramedrop) {
|
||||||
|
for (int i = 0; i < kFramerate * kNumSeconds / 2; ++i) {
|
||||||
|
qs_.ReportEncodedFrame(kNormalQp);
|
||||||
|
ASSERT_EQ(input_frame_.width(), qs_.GetScaledResolution(input_frame_).width)
|
||||||
|
<< "Unexpected scale on half framedrop.";
|
||||||
|
|
||||||
|
qs_.ReportDroppedFrame();
|
||||||
|
ASSERT_EQ(input_frame_.width(), qs_.GetScaledResolution(input_frame_).width)
|
||||||
|
<< "Unexpected scale on half framedrop.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void QualityScalerTest::ContinuouslyDownscalesByHalfDimensionsAndBackUp() {
|
||||||
|
const int initial_min_dimension = input_frame_.width() < input_frame_.height()
|
||||||
|
? input_frame_.width()
|
||||||
|
: input_frame_.height();
|
||||||
|
int min_dimension = initial_min_dimension;
|
||||||
|
int current_shift = 0;
|
||||||
|
// Drop all frames to force-trigger downscaling.
|
||||||
|
while (min_dimension > 16) {
|
||||||
|
TriggerScale(kScaleDown);
|
||||||
|
QualityScaler::Resolution res = qs_.GetScaledResolution(input_frame_);
|
||||||
|
min_dimension = res.width < res.height ? res.width : res.height;
|
||||||
|
++current_shift;
|
||||||
|
ASSERT_EQ(input_frame_.width() >> current_shift, res.width);
|
||||||
|
ASSERT_EQ(input_frame_.height() >> current_shift, res.height);
|
||||||
|
ExpectScaleUsingReportedResolution();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we can scale back with good-quality frames.
|
||||||
|
while (min_dimension < initial_min_dimension) {
|
||||||
|
TriggerScale(kScaleUp);
|
||||||
|
QualityScaler::Resolution res = qs_.GetScaledResolution(input_frame_);
|
||||||
|
min_dimension = res.width < res.height ? res.width : res.height;
|
||||||
|
--current_shift;
|
||||||
|
ASSERT_EQ(input_frame_.width() >> current_shift, res.width);
|
||||||
|
ASSERT_EQ(input_frame_.height() >> current_shift, res.height);
|
||||||
|
ExpectScaleUsingReportedResolution();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify we don't start upscaling after further low use.
|
||||||
|
for (int i = 0; i < kFramerate * kNumSeconds; ++i) {
|
||||||
|
qs_.ReportEncodedFrame(kLowQp);
|
||||||
|
ExpectOriginalFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QualityScalerTest, ContinuouslyDownscalesByHalfDimensionsAndBackUp) {
|
||||||
|
ContinuouslyDownscalesByHalfDimensionsAndBackUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QualityScalerTest,
|
||||||
|
ContinuouslyDownscalesOddResolutionsByHalfDimensionsAndBackUp) {
|
||||||
|
const int kOddWidth = 517;
|
||||||
|
const int kHalfOddWidth = (kOddWidth + 1) / 2;
|
||||||
|
const int kOddHeight = 1239;
|
||||||
|
input_frame_.CreateEmptyFrame(
|
||||||
|
kOddWidth, kOddHeight, kOddWidth, kHalfOddWidth, kHalfOddWidth);
|
||||||
|
ContinuouslyDownscalesByHalfDimensionsAndBackUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
void QualityScalerTest::DoesNotDownscaleFrameDimensions(int width, int height) {
|
||||||
|
input_frame_.CreateEmptyFrame(
|
||||||
|
width, height, width, (width + 1) / 2, (width + 1) / 2);
|
||||||
|
|
||||||
|
for (int i = 0; i < kFramerate * kNumSeconds; ++i) {
|
||||||
|
qs_.ReportDroppedFrame();
|
||||||
|
ASSERT_EQ(input_frame_.width(), qs_.GetScaledResolution(input_frame_).width)
|
||||||
|
<< "Unexpected scale of minimal-size frame.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QualityScalerTest, DoesNotDownscaleFrom1PxWidth) {
|
||||||
|
DoesNotDownscaleFrameDimensions(1, kHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QualityScalerTest, DoesNotDownscaleFrom1PxHeight) {
|
||||||
|
DoesNotDownscaleFrameDimensions(kWidth, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(QualityScalerTest, DoesNotDownscaleFrom1Px) {
|
||||||
|
DoesNotDownscaleFrameDimensions(1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace webrtc
|
@ -18,8 +18,10 @@
|
|||||||
'<(webrtc_root)/system_wrappers/source/system_wrappers.gyp:system_wrappers',
|
'<(webrtc_root)/system_wrappers/source/system_wrappers.gyp:system_wrappers',
|
||||||
],
|
],
|
||||||
'sources': [
|
'sources': [
|
||||||
'include/frame_dropper.h',
|
|
||||||
'frame_dropper.cc',
|
'frame_dropper.cc',
|
||||||
|
'include/frame_dropper.h',
|
||||||
|
'quality_scaler.cc',
|
||||||
|
'quality_scaler.h',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
], # targets
|
], # targets
|
||||||
|
Loading…
Reference in New Issue
Block a user