blob: aa00ff5da95e0b9f381511a468ca01a0b5a0dae3 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "media/video/vpx_video_encoder.h"
#include <algorithm>
#include <memory>
#include <optional>
#include "base/containers/heap_array.h"
#include "base/logging.h"
#include "base/numerics/checked_math.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "media/base/media_switches.h"
#include "media/base/svc_scalability_mode.h"
#include "media/base/timestamp_constants.h"
#include "media/base/video_codecs.h"
#include "media/base/video_frame.h"
#include "media/base/video_util.h"
#include "media/video/video_encoder_info.h"
#include "third_party/libvpx/source/libvpx/vpx/vp8cx.h"
#include "third_party/libyuv/include/libyuv/convert.h"
namespace media {
namespace {
constexpr vpx_enc_frame_flags_t VP8_UPDATE_NOTHING =
VP8_EFLAG_NO_UPD_ARF | VP8_EFLAG_NO_UPD_GF | VP8_EFLAG_NO_UPD_LAST;
// Frame Pattern:
// Layer Index 0: |0| |2| |4| |6| |8|
// Layer Index 1: | |1| |3| |5| |7| |
vpx_enc_frame_flags_t vp8_2layers_temporal_flags[] = {
// Layer 0 : update and reference only last frame
VP8_EFLAG_NO_REF_GF | VP8_EFLAG_NO_REF_ARF | VP8_EFLAG_NO_UPD_GF |
VP8_EFLAG_NO_UPD_ARF,
// Layer 1: only reference last frame, no updates
VP8_UPDATE_NOTHING | VP8_EFLAG_NO_REF_ARF | VP8_EFLAG_NO_REF_GF};
// Frame Pattern:
// Layer Index 0: |0| | | |4| | | |8| | | |12|
// Layer Index 1: | | |2| | | |6| | | |10| | |
// Layer Index 2: | |1| |3| |5| |7| |9| |11| |
vpx_enc_frame_flags_t vp8_3layers_temporal_flags[] = {
// Layer 0 : update and reference only last frame
// It only depends on layer 0
VP8_EFLAG_NO_REF_GF | VP8_EFLAG_NO_REF_ARF | VP8_EFLAG_NO_UPD_GF |
VP8_EFLAG_NO_UPD_ARF,
// Layer 2: only reference last frame, no updates
// It only depends on layer 0
VP8_UPDATE_NOTHING | VP8_EFLAG_NO_REF_ARF | VP8_EFLAG_NO_REF_GF,
// Layer 1: only reference last frame, update gold frame
// It only depends on layer 0
VP8_EFLAG_NO_REF_GF | VP8_EFLAG_NO_REF_ARF | VP8_EFLAG_NO_UPD_ARF |
VP8_EFLAG_NO_UPD_LAST,
// Layer 2: reference last frame and gold frame, no updates
// It depends on layer 0 and layer 1
VP8_UPDATE_NOTHING | VP8_EFLAG_NO_REF_ARF,
};
EncoderStatus SetUpVpxConfig(const VideoEncoder::Options& opts,
VideoCodecProfile profile,
vpx_codec_enc_cfg_t* config) {
if (opts.frame_size.width() <= 0 || opts.frame_size.height() <= 0)
return EncoderStatus(EncoderStatus::Codes::kEncoderUnsupportedConfig,
"Negative width or height values.");
if (!opts.frame_size.GetCheckedArea().IsValid())
return EncoderStatus(EncoderStatus::Codes::kEncoderUnsupportedConfig,
"Frame is too large.");
config->g_pass = VPX_RC_ONE_PASS;
// libvpx encoding is performed synchronously.
config->g_lag_in_frames = 0;
config->rc_max_quantizer = 58;
// Increase min QP to 12 for vp8 screen sharing; It reduces the encoding
// bitrate on static content and thus helps reduce big overshoot on slide
// change. This optimization is cited from libwebRTC.
// third_party/webrtc/modules/video_coding/codecs/vp8/libvpx_vp8_encoder.cc.
// TODO(bugs.webrtc.org/15785): Set min quantizer for screen content in VP9.
config->rc_min_quantizer = 2;
if (profile == VP8PROFILE_ANY &&
opts.content_hint == VideoEncoder::ContentHint::Screen) {
config->rc_min_quantizer = 12;
}
config->rc_resize_allowed = 0;
// Only if latency_mode is real time, a frame might be dropped.
config->rc_dropframe_thresh =
opts.latency_mode == VideoEncoder::LatencyMode::Realtime
? GetDefaultVideoEncoderDropFrameThreshold()
: 0;
config->g_timebase.num = 1;
config->g_timebase.den = base::Time::kMicrosecondsPerSecond;
// Set the number of threads based on the image width and num of cores.
config->g_threads = GetNumberOfThreadsForSoftwareEncoding(opts.frame_size);
// Insert keyframes at will with a given max interval
if (opts.keyframe_interval.has_value()) {
config->kf_mode = VPX_KF_AUTO;
config->kf_min_dist = 0;
config->kf_max_dist = opts.keyframe_interval.value();
}
uint32_t default_bitrate = GetDefaultVideoEncodeBitrate(
opts.frame_size, opts.framerate.value_or(30));
config->rc_end_usage = VPX_VBR;
// The unit of rc_target_bitrate is kilobits per second.
config->rc_target_bitrate = default_bitrate / 1000;
if (opts.bitrate.has_value()) {
const auto& bitrate = opts.bitrate.value();
switch (bitrate.mode()) {
case Bitrate::Mode::kVariable:
config->rc_end_usage = VPX_VBR;
break;
case Bitrate::Mode::kConstant:
config->rc_end_usage = VPX_CBR;
break;
case Bitrate::Mode::kExternal:
// libvpx doesn't have a special rate control mode for per-frame
// quantizer. Instead we just set CBR and set
// VP9E_SET_QUANTIZER_ONE_PASS before each frame.
config->rc_end_usage = VPX_CBR;
// Let the whole AV1 quantizer range to be used.
config->rc_max_quantizer = 63;
config->rc_min_quantizer = 0;
break;
}
if (bitrate.target_bps() != 0) {
config->rc_target_bitrate = bitrate.target_bps() / 1000;
}
}
config->g_w = opts.frame_size.width();
config->g_h = opts.frame_size.height();
if (!opts.scalability_mode)
return EncoderStatus::Codes::kOk;
switch (opts.scalability_mode.value()) {
case SVCScalabilityMode::kL1T1:
// Nothing to do
break;
case SVCScalabilityMode::kL1T2:
// Frame Pattern:
// Layer Index 0: |0| |2| |4| |6| |8|
// Layer Index 1: | |1| |3| |5| |7| |
config->ts_number_layers = 2;
config->ts_periodicity = 2;
DCHECK_EQ(config->ts_periodicity,
sizeof(vp8_2layers_temporal_flags) /
sizeof(vp8_2layers_temporal_flags[0]));
config->ts_layer_id[0] = 0;
config->ts_layer_id[1] = 1;
config->ts_rate_decimator[0] = 2;
config->ts_rate_decimator[1] = 1;
// Bitrate allocation L0: 60% L1: 40%
config->layer_target_bitrate[0] = config->ts_target_bitrate[0] =
60 * config->rc_target_bitrate / 100;
config->layer_target_bitrate[1] = config->ts_target_bitrate[1] =
config->rc_target_bitrate;
config->temporal_layering_mode = VP9E_TEMPORAL_LAYERING_MODE_0101;
config->g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT;
break;
case SVCScalabilityMode::kL1T3:
// Frame Pattern:
// Layer Index 0: |0| | | |4| | | |8| | | |12|
// Layer Index 1: | | |2| | | |6| | | |10| | |
// Layer Index 2: | |1| |3| |5| |7| |9| |11| |
config->ts_number_layers = 3;
config->ts_periodicity = 4;
DCHECK_EQ(config->ts_periodicity,
sizeof(vp8_3layers_temporal_flags) /
sizeof(vp8_3layers_temporal_flags[0]));
config->ts_layer_id[0] = 0;
config->ts_layer_id[1] = 2;
config->ts_layer_id[2] = 1;
config->ts_layer_id[3] = 2;
config->ts_rate_decimator[0] = 4;
config->ts_rate_decimator[1] = 2;
config->ts_rate_decimator[2] = 1;
// Bitrate allocation L0: 50% L1: 20% L2: 30%
config->layer_target_bitrate[0] = config->ts_target_bitrate[0] =
50 * config->rc_target_bitrate / 100;
config->layer_target_bitrate[1] = config->ts_target_bitrate[1] =
70 * config->rc_target_bitrate / 100;
config->layer_target_bitrate[2] = config->ts_target_bitrate[2] =
config->rc_target_bitrate;
config->temporal_layering_mode = VP9E_TEMPORAL_LAYERING_MODE_0212;
config->g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT;
break;
default: {
return EncoderStatus(EncoderStatus::Codes::kEncoderUnsupportedConfig,
"Unsupported number of temporal layers.");
}
}
return EncoderStatus::Codes::kOk;
}
vpx_svc_extra_cfg_t MakeSvcExtraConfig(const vpx_codec_enc_cfg_t& config) {
vpx_svc_extra_cfg_t result = {};
result.temporal_layering_mode = config.temporal_layering_mode;
for (size_t i = 0; i < config.ts_number_layers; ++i) {
result.scaling_factor_num[i] = 1;
result.scaling_factor_den[i] = 1;
result.max_quantizers[i] = config.rc_max_quantizer;
result.min_quantizers[i] = config.rc_min_quantizer;
}
return result;
}
void FreeCodecCtx(vpx_codec_ctx_t* codec_ctx) {
if (codec_ctx->name) {
// Codec has been initialized, we need to destroy it.
auto error = vpx_codec_destroy(codec_ctx);
DCHECK_EQ(error, VPX_CODEC_OK);
}
delete codec_ctx;
}
std::string LogVpxErrorMessage(vpx_codec_ctx_t* context,
const char* message,
vpx_codec_err_t status) {
auto formatted_msg = base::StringPrintf("%s: %s (%s)", message,
vpx_codec_err_to_string(status),
vpx_codec_error_detail(context));
DLOG(ERROR) << formatted_msg;
return formatted_msg;
}
// If conversion is needed for given profile and frame, returns the destination
// pixel format. If no conversion is needed returns nullopt.
std::optional<VideoPixelFormat> GetConversionFormat(VideoCodecProfile profile,
VideoPixelFormat format,
bool needs_resize) {
switch (profile) {
case VP8PROFILE_ANY:
case VP9PROFILE_PROFILE0:
if ((format != PIXEL_FORMAT_NV12 && format != PIXEL_FORMAT_I420) ||
needs_resize) {
return PIXEL_FORMAT_I420;
}
break;
case VP9PROFILE_PROFILE1:
if (format != PIXEL_FORMAT_I444 || needs_resize) {
return PIXEL_FORMAT_I444;
}
break;
case VP9PROFILE_PROFILE2:
if (format != PIXEL_FORMAT_YUV420P10 || needs_resize) {
// VideoFrameConverter doesn't support 10bit yet, so output I420 then
// convert to I010.
return PIXEL_FORMAT_I420;
}
break;
case VP9PROFILE_PROFILE3:
if (format != PIXEL_FORMAT_YUV444P10 || needs_resize) {
// VideoFrameConverter doesn't support 10bit yet, so output I444 then
// convert to I410.
return PIXEL_FORMAT_I444;
}
break;
default:
NOTREACHED(); // Checked during Initialize().
}
return std::nullopt;
}
// Sets up a standard 3-plane vpx_image_t from `frame`.
void SetupStandardYuvPlanes(const VideoFrame& frame, vpx_image_t* vpx_image) {
DCHECK_EQ(VideoFrame::NumPlanes(frame.format()), 3u);
vpx_image->planes[VPX_PLANE_Y] =
const_cast<uint8_t*>(frame.visible_data(VideoFrame::kYPlane));
vpx_image->planes[VPX_PLANE_U] =
const_cast<uint8_t*>(frame.visible_data(VideoFrame::kUPlane));
vpx_image->planes[VPX_PLANE_V] =
const_cast<uint8_t*>(frame.visible_data(VideoFrame::kVPlane));
vpx_image->stride[VPX_PLANE_Y] = frame.stride(VideoFrame::kYPlane);
vpx_image->stride[VPX_PLANE_U] = frame.stride(VideoFrame::kUPlane);
vpx_image->stride[VPX_PLANE_V] = frame.stride(VideoFrame::kVPlane);
}
void I444ToI410(const VideoFrame& frame, vpx_image_t* vpx_image) {
DCHECK_EQ(frame.format(), PIXEL_FORMAT_I444);
for (size_t i = 0; i < VideoFrame::NumPlanes(frame.format()); ++i) {
libyuv::Convert8To16Plane(
frame.visible_data(i), frame.stride(i),
reinterpret_cast<uint16_t*>(vpx_image->planes[i]),
vpx_image->stride[i] / 2, 1024,
VideoFrame::Columns(i, frame.format(),
frame.visible_rect().size().width()),
VideoFrame::Rows(i, frame.format(),
frame.visible_rect().size().height()));
}
}
} // namespace
VpxVideoEncoder::VpxVideoEncoder() : codec_(nullptr, FreeCodecCtx) {}
void VpxVideoEncoder::Initialize(VideoCodecProfile profile,
const Options& options,
EncoderInfoCB info_cb,
OutputCB output_cb,
EncoderStatusCB done_cb) {
done_cb = BindCallbackToCurrentLoopIfNeeded(std::move(done_cb));
if (codec_) {
std::move(done_cb).Run(EncoderStatus::Codes::kEncoderInitializeTwice);
return;
}
profile_ = profile;
bool is_vp9 = false;
vpx_codec_iface_t* iface = nullptr;
if (profile == VP8PROFILE_ANY) {
iface = vpx_codec_vp8_cx();
} else if (profile == VP9PROFILE_PROFILE0 || profile == VP9PROFILE_PROFILE1 ||
((profile == VP9PROFILE_PROFILE2 ||
profile == VP9PROFILE_PROFILE3) &&
// High bit depth encoding is not enabled on all platforms.
(vpx_codec_get_caps(vpx_codec_vp9_cx()) &
VPX_CODEC_CAP_HIGHBITDEPTH))) {
is_vp9 = true;
iface = vpx_codec_vp9_cx();
} else {
auto status =
EncoderStatus(EncoderStatus::Codes::kEncoderUnsupportedProfile)
.WithData("profile", profile);
std::move(done_cb).Run(status);
return;
}
if (options.bitrate.has_value() &&
options.bitrate->mode() == Bitrate::Mode::kExternal && !is_vp9) {
std::move(done_cb).Run(
EncoderStatus(EncoderStatus::Codes::kEncoderUnsupportedConfig,
"VP8 doesn't support per-frame quantizer"));
return;
}
auto vpx_error = vpx_codec_enc_config_default(iface, &codec_config_, 0);
if (vpx_error != VPX_CODEC_OK) {
auto status =
EncoderStatus(EncoderStatus::Codes::kEncoderInitializationError,
"Failed to get default VPX config.")
.WithData("vpx_error", vpx_error);
std::move(done_cb).Run(status);
return;
}
if (profile == VP9PROFILE_PROFILE0 || profile == VP9PROFILE_PROFILE2) {
if (options.subsampling.value_or(VideoChromaSampling::k420) !=
VideoChromaSampling::k420) {
std::move(done_cb).Run(EncoderStatus(
EncoderStatus::Codes::kEncoderUnsupportedConfig,
"Only 4:2:0 subsampling is supported with VP9 profiles 0 and 2."));
return;
}
} else if (profile == VP9PROFILE_PROFILE1 || profile == VP9PROFILE_PROFILE3) {
// TODO(crbug.com/40144811): Support 4:2:2 subsampling.
if (options.subsampling != VideoChromaSampling::k444) {
std::move(done_cb).Run(EncoderStatus(
EncoderStatus::Codes::kEncoderUnsupportedConfig,
"Only 4:4:4 subsampling is supported with VP9 profiles 1 and 3."));
return;
}
}
if ((profile == VP9PROFILE_PROFILE0 || profile == VP9PROFILE_PROFILE1) &&
options.bit_depth.value_or(8) != 8) {
std::move(done_cb).Run(EncoderStatus(
EncoderStatus::Codes::kEncoderUnsupportedConfig,
"Only 8-bit depth is supported with VP9 profiles 0 and 1."));
return;
}
if ((profile == VP9PROFILE_PROFILE2 || profile == VP9PROFILE_PROFILE3) &&
options.bit_depth.value_or(10) != 10) {
std::move(done_cb).Run(EncoderStatus(
EncoderStatus::Codes::kEncoderUnsupportedConfig,
"Only 10-bit depth is supported with VP9 profiles 2 and 3."));
return;
}
switch (profile) {
case VP8PROFILE_ANY:
case VP9PROFILE_PROFILE0:
codec_config_.g_profile = 0;
codec_config_.g_bit_depth = VPX_BITS_8;
codec_config_.g_input_bit_depth = 8;
break;
case VP9PROFILE_PROFILE1:
codec_config_.g_profile = 1;
codec_config_.g_bit_depth = VPX_BITS_8;
codec_config_.g_input_bit_depth = 8;
break;
case VP9PROFILE_PROFILE2:
codec_config_.g_profile = 2;
codec_config_.g_bit_depth = VPX_BITS_10;
codec_config_.g_input_bit_depth = 10;
break;
case VP9PROFILE_PROFILE3:
codec_config_.g_profile = 3;
codec_config_.g_bit_depth = VPX_BITS_10;
codec_config_.g_input_bit_depth = 10;
break;
default:
NOTREACHED(); // Enforced via a profile check above.
}
auto status = SetUpVpxConfig(options, profile_, &codec_config_);
if (!status.is_ok()) {
std::move(done_cb).Run(status);
return;
}
vpx_codec_unique_ptr codec(new vpx_codec_ctx_t, FreeCodecCtx);
codec->name = nullptr; // We are allowed to use vpx_codec_ctx_t.name
vpx_error = vpx_codec_enc_init(
codec.get(), iface, &codec_config_,
codec_config_.g_bit_depth == VPX_BITS_8 ? 0 : VPX_CODEC_USE_HIGHBITDEPTH);
if (vpx_error != VPX_CODEC_OK) {
auto msg = LogVpxErrorMessage(
codec.get(), "VPX encoder initialization error", vpx_error);
std::move(done_cb).Run(
EncoderStatus(EncoderStatus::Codes::kEncoderInitializationError, msg));
return;
}
// For VP9 the values used for real-time encoding mode are 5, 6, 7,
// 8, 9. Higher means faster encoding, but lower quality.
// For VP8 typical values used for real-time encoding are -4, -6, -8,
// -10, -12. Again larger magnitude means faster encoding but lower
// quality.
int cpu_used = is_vp9 ? 7 : -6;
vpx_error = vpx_codec_control(codec.get(), VP8E_SET_CPUUSED, cpu_used);
if (vpx_error != VPX_CODEC_OK) {
auto msg = LogVpxErrorMessage(
codec.get(), "VPX encoder VP8E_SET_CPUUSED error", vpx_error);
std::move(done_cb).Run(
EncoderStatus(EncoderStatus::Codes::kEncoderInitializationError, msg));
return;
}
if (is_vp9) {
// Set the number of column tiles in encoding an input frame, with number of
// tile columns (in Log2 unit) as the parameter.
// The minimum width of a tile column is 256 pixels, the maximum is 4096.
int log2_tile_columns =
static_cast<int>(std::log2(codec_config_.g_w / 256));
vpx_codec_control(codec.get(), VP9E_SET_TILE_COLUMNS, log2_tile_columns);
// Turn on row level multi-threading.
vpx_codec_control(codec.get(), VP9E_SET_ROW_MT, 1);
if (codec_config_.ts_number_layers > 1) {
vpx_svc_extra_cfg_t svc_conf = MakeSvcExtraConfig(codec_config_);
// VP9 needs SVC to be turned on explicitly
vpx_codec_control(codec.get(), VP9E_SET_SVC_PARAMETERS, &svc_conf);
vpx_error = vpx_codec_control(codec.get(), VP9E_SET_SVC, 1);
if (vpx_error != VPX_CODEC_OK) {
auto msg = LogVpxErrorMessage(codec.get(),
"Can't activate SVC encoding", vpx_error);
status = EncoderStatus(
EncoderStatus::Codes::kEncoderInitializationError, msg);
std::move(done_cb).Run(status);
return;
}
}
// In CBR mode aq-mode=3 (cyclic refresh) is enabled for quality
// improvement. Note: For VP8, cyclic refresh is internally done as
// default.
if (codec_config_.rc_end_usage == VPX_CBR) {
vpx_codec_control(codec.get(), VP9E_SET_AQ_MODE, 3);
}
}
// Tune configs for screen sharing. The values are the same as libwebrtc.
// third_party/webrtc/modules/video_coding/codecs/vp8/libvpx_vp8_encoder.cc.
// third_party/webrtc/modules/video_coding/codecs/vp9/libvpx_vp9_encoder.cc.
unsigned int static_thresh = 1;
if (options.content_hint == ContentHint::Screen) {
if (is_vp9) {
// TODO(bugs.webrtc.org/15785): Set static threshold for screen content
// in VP9 too.
vpx_codec_control(codec.get(), VP9E_SET_TUNE_CONTENT,
VP9E_CONTENT_SCREEN);
} else {
static_thresh = 100;
// Tune configs for screen sharing. The values are the same as WebRTC
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/modules/video_coding/codecs/vp8/libvpx_vp8_encoder.cc
// 0: camera (default)
// 1: screen
// 2: screen with allowing drop frame.
unsigned int screen_content_mode = 1;
if (options.latency_mode == LatencyMode::Realtime &&
base::FeatureList::IsEnabled(kWebCodecsVideoEncoderFrameDrop)) {
screen_content_mode = 2;
}
vpx_codec_control(codec.get(), VP8E_SET_SCREEN_CONTENT_MODE,
screen_content_mode);
}
}
vpx_codec_control(codec.get(), VP8E_SET_STATIC_THRESHOLD, static_thresh);
options_ = options;
originally_configured_size_ = options.frame_size;
output_cb_ = BindCallbackToCurrentLoopIfNeeded(std::move(output_cb));
codec_ = std::move(codec);
if (info_cb) {
VideoEncoderInfo info;
info.implementation_name = "VpxVideoEncoder";
info.is_hardware_accelerated = false;
BindCallbackToCurrentLoopIfNeeded(std::move(info_cb)).Run(info);
}
std::move(done_cb).Run(EncoderStatus::Codes::kOk);
}
void VpxVideoEncoder::Encode(scoped_refptr<VideoFrame> frame,
const EncodeOptions& encode_options,
EncoderStatusCB done_cb) {
done_cb = BindCallbackToCurrentLoopIfNeeded(std::move(done_cb));
if (!codec_) {
std::move(done_cb).Run(
EncoderStatus::Codes::kEncoderInitializeNeverCompleted);
return;
}
bool key_frame = encode_options.key_frame;
if (!frame) {
std::move(done_cb).Run(
EncoderStatus(EncoderStatus::Codes::kEncoderFailedEncode,
"No frame provided for encoding."));
return;
}
if (frame->format() == PIXEL_FORMAT_NV12 && frame->HasGpuMemoryBuffer()) {
frame = ConvertToMemoryMappedFrame(frame);
if (!frame) {
std::move(done_cb).Run(
EncoderStatus(EncoderStatus::Codes::kEncoderFailedEncode,
"Convert GMB frame to MemoryMappedFrame failed."));
return;
}
}
if (!frame->IsMappable()) {
std::move(done_cb).Run(
EncoderStatus(EncoderStatus::Codes::kEncoderFailedEncode,
"Unexpected frame format.")
.WithData("IsMappable", frame->IsMappable())
.WithData("format", frame->format()));
return;
}
// Format conversion or resizing may be necessary to get the frame into the
// form needed by libvpx for encoding.
if (auto conversion_format =
GetConversionFormat(profile_, frame->format(),
/*needs_resize=*/frame->visible_rect().size() !=
options_.frame_size)) {
auto temp_frame = frame_pool_.CreateFrame(
*conversion_format, options_.frame_size, gfx::Rect(options_.frame_size),
options_.frame_size, frame->timestamp());
if (!temp_frame) {
std::move(done_cb).Run(
EncoderStatus(EncoderStatus::Codes::kEncoderFailedEncode,
"Can't allocate a temporary frame for conversion"));
return;
}
// If `frame->format()` is unsupported ConvertAndScale() will fail.
auto convert_status = frame_converter_.ConvertAndScale(*frame, *temp_frame);
if (!convert_status.is_ok()) {
std::move(done_cb).Run(
EncoderStatus(EncoderStatus::Codes::kEncoderFailedEncode)
.AddCause(std::move(convert_status)));
return;
}
frame = std::move(temp_frame);
}
// Resizing should have been taken care of above.
DCHECK_EQ(frame->visible_rect().size(), options_.frame_size);
switch (profile_) {
case VP8PROFILE_ANY:
case VP9PROFILE_PROFILE0: {
DCHECK(frame->format() == PIXEL_FORMAT_NV12 ||
frame->format() == PIXEL_FORMAT_I420);
if (frame->format() == PIXEL_FORMAT_NV12) {
RecreateVpxImageIfNeeded(VPX_IMG_FMT_NV12, /*needs_memory=*/false);
vpx_image_.planes[VPX_PLANE_Y] =
const_cast<uint8_t*>(frame->visible_data(VideoFrame::kYPlane));
vpx_image_.planes[VPX_PLANE_U] =
const_cast<uint8_t*>(frame->visible_data(VideoFrame::kUVPlane));
// In NV12 U and V samples are combined in one plane (bytes go UVUVUV),
// but libvpx treats them as two planes with the same stride but shifted
// by one byte.
vpx_image_.planes[VPX_PLANE_V] = vpx_image_.planes[VPX_PLANE_U] + 1;
vpx_image_.stride[VPX_PLANE_Y] = frame->stride(VideoFrame::kYPlane);
vpx_image_.stride[VPX_PLANE_U] = frame->stride(VideoFrame::kUVPlane);
vpx_image_.stride[VPX_PLANE_V] = frame->stride(VideoFrame::kUVPlane);
} else {
RecreateVpxImageIfNeeded(VPX_IMG_FMT_I420, /*needs_memory=*/false);
SetupStandardYuvPlanes(*frame, &vpx_image_);
}
break;
}
case VP9PROFILE_PROFILE2:
DCHECK(frame->format() == PIXEL_FORMAT_YUV420P10 ||
frame->format() == PIXEL_FORMAT_I420);
if (frame->format() == PIXEL_FORMAT_YUV420P10) {
RecreateVpxImageIfNeeded(VPX_IMG_FMT_I42016, /*needs_memory=*/false);
SetupStandardYuvPlanes(*frame, &vpx_image_);
break;
}
RecreateVpxImageIfNeeded(VPX_IMG_FMT_I42016, /*needs_memory=*/true);
libyuv::I420ToI010(
frame->visible_data(VideoFrame::kYPlane),
frame->stride(VideoFrame::kYPlane),
frame->visible_data(VideoFrame::kUPlane),
frame->stride(VideoFrame::kUPlane),
frame->visible_data(VideoFrame::kVPlane),
frame->stride(VideoFrame::kVPlane),
reinterpret_cast<uint16_t*>(vpx_image_.planes[VPX_PLANE_Y]),
vpx_image_.stride[VPX_PLANE_Y] / 2,
reinterpret_cast<uint16_t*>(vpx_image_.planes[VPX_PLANE_U]),
vpx_image_.stride[VPX_PLANE_U] / 2,
reinterpret_cast<uint16_t*>(vpx_image_.planes[VPX_PLANE_V]),
vpx_image_.stride[VPX_PLANE_V] / 2, frame->visible_rect().width(),
frame->visible_rect().height());
break;
case VP9PROFILE_PROFILE1:
DCHECK_EQ(frame->format(), PIXEL_FORMAT_I444);
RecreateVpxImageIfNeeded(VPX_IMG_FMT_I444, /*needs_memory=*/false);
SetupStandardYuvPlanes(*frame, &vpx_image_);
break;
case VP9PROFILE_PROFILE3:
DCHECK(frame->format() == PIXEL_FORMAT_YUV444P10 ||
frame->format() == PIXEL_FORMAT_I444);
if (frame->format() == PIXEL_FORMAT_YUV444P10) {
RecreateVpxImageIfNeeded(VPX_IMG_FMT_I44416, /*needs_memory=*/false);
SetupStandardYuvPlanes(*frame, &vpx_image_);
break;
}
RecreateVpxImageIfNeeded(VPX_IMG_FMT_I44416, /*needs_memory=*/true);
I444ToI410(*frame, &vpx_image_);
break;
default:
NOTREACHED(); // Checked during Initialize().
}
// Use zero as a timestamp, so encoder will not use it for rate control.
// In absence of timestamp libvpx uses duration.
constexpr auto timestamp_us = 0;
auto duration_us = GetFrameDuration(*frame).InMicroseconds();
last_frame_timestamp_ = frame->timestamp();
if (last_frame_color_space_ != frame->ColorSpace()) {
last_frame_color_space_ = frame->ColorSpace();
key_frame = true;
UpdateEncoderColorSpace();
}
const unsigned long deadline = VPX_DL_REALTIME;
vpx_codec_flags_t flags = key_frame ? VPX_EFLAG_FORCE_KF : 0;
int temporal_id = 0;
const bool is_layer_encoding = codec_config_.ts_number_layers > 1;
if (is_layer_encoding) {
if (key_frame)
temporal_svc_frame_index_ = 0;
unsigned int index_in_temp_cycle =
temporal_svc_frame_index_ % codec_config_.ts_periodicity;
temporal_id = codec_config_.ts_layer_id[index_in_temp_cycle];
if (profile_ == VP8PROFILE_ANY) {
auto* vp8_layers_flags = codec_config_.ts_number_layers == 2
? vp8_2layers_temporal_flags
: vp8_3layers_temporal_flags;
flags |= vp8_layers_flags[index_in_temp_cycle];
vpx_codec_control(codec_.get(), VP8E_SET_TEMPORAL_LAYER_ID, temporal_id);
}
}
if (encode_options.quantizer.has_value()) {
DCHECK_EQ(options_.bitrate->mode(), Bitrate::Mode::kExternal);
// Convert double quantizer to an integer within codec's supported range.
int qp = static_cast<int>(std::lround(encode_options.quantizer.value()));
qp = std::clamp(qp, static_cast<int>(codec_config_.rc_min_quantizer),
static_cast<int>(codec_config_.rc_max_quantizer));
vpx_codec_control(codec_.get(), VP9E_SET_QUANTIZER_ONE_PASS, qp);
}
TRACE_EVENT1("media", "vpx_codec_encode", "timestamp", frame->timestamp());
auto vpx_error = vpx_codec_encode(codec_.get(), &vpx_image_, timestamp_us,
duration_us, flags, deadline);
if (vpx_error != VPX_CODEC_OK) {
auto msg =
LogVpxErrorMessage(codec_.get(), "VPX encoding error", vpx_error);
std::move(done_cb).Run(
EncoderStatus(EncoderStatus::Codes::kEncoderFailedEncode, msg)
.WithData("vpx_error", vpx_error));
return;
}
auto output =
GetEncoderOutput(temporal_id, frame->timestamp(), frame->ColorSpace());
if (is_layer_encoding) {
// If we got an unexpected key frame, |temporal_svc_frame_index_| needs to
// be adjusted, because the next frame should have index 1.
if (output.key_frame) {
temporal_svc_frame_index_ = 0;
}
if (output.size != 0) {
temporal_svc_frame_index_++;
}
}
output_cb_.Run(std::move(output), {});
std::move(done_cb).Run(EncoderStatus::Codes::kOk);
}
void VpxVideoEncoder::ChangeOptions(const Options& options,
OutputCB output_cb,
EncoderStatusCB done_cb) {
done_cb = BindCallbackToCurrentLoopIfNeeded(std::move(done_cb));
if (!codec_) {
std::move(done_cb).Run(
EncoderStatus::Codes::kEncoderInitializeNeverCompleted);
return;
}
// Libvpx is very peculiar about encoded frame size changes,
// - VP8: vpx_codec_enc_config_set() returns VPX_CODEC_INVALID_PARAM if we
// try to increase encoded width or height larger than their initial
// values.
// - VP9: The codec may crash if we try to increase encoded width or height
// larger than their initial values.
//
// Mind the difference between old frame sizes:
// - |originally_configured_size_| is set only once when the vpx_codec_ctx_t
// is created.
// - |options_.frame_size| changes every time ChangeOptions() is called.
// More info can be found here:
// https://bugs.chromium.org/p/webm/issues/detail?id=1642
// https://bugs.chromium.org/p/webm/issues/detail?id=912
if (profile_ != VP8PROFILE_ANY) {
// VP9 resize restrictions
if (options.frame_size.width() > originally_configured_size_.width() ||
options.frame_size.height() > originally_configured_size_.height()) {
auto status = EncoderStatus(
EncoderStatus::Codes::kEncoderUnsupportedConfig,
"libvpx/VP9 doesn't support dynamically increasing frame dimensions");
std::move(done_cb).Run(std::move(status));
return;
}
}
vpx_codec_enc_cfg_t new_config = codec_config_;
auto status = SetUpVpxConfig(options, profile_, &new_config);
if (!status.is_ok()) {
std::move(done_cb).Run(status);
return;
}
auto error = vpx_codec_enc_config_set(codec_.get(), &new_config);
const bool is_vp9 = (profile_ != VP8PROFILE_ANY);
if (is_vp9 && error == VPX_CODEC_OK && new_config.ts_number_layers > 1) {
vpx_svc_extra_cfg_t svc_conf = MakeSvcExtraConfig(new_config);
vpx_codec_control(codec_.get(), VP9E_SET_SVC_PARAMETERS, &svc_conf);
error = vpx_codec_control(codec_.get(), VP9E_SET_SVC, 1);
}
if (error == VPX_CODEC_OK) {
codec_config_ = new_config;
options_ = options;
if (!output_cb.is_null())
output_cb_ = BindCallbackToCurrentLoopIfNeeded(std::move(output_cb));
} else {
status = EncoderStatus(EncoderStatus::Codes::kEncoderUnsupportedConfig,
"Failed to set new VPX config")
.WithData("vpx_error", error);
}
std::move(done_cb).Run(std::move(status));
}
base::TimeDelta VpxVideoEncoder::GetFrameDuration(const VideoFrame& frame) {
// Frame has duration in metadata, use it.
if (frame.metadata().frame_duration.has_value())
return frame.metadata().frame_duration.value();
// Options have framerate specified, use it.
if (options_.framerate.has_value())
return base::Seconds(1.0 / options_.framerate.value());
// No real way to figure out duration, use time passed since the last frame
// as an educated guess, but clamp it within a reasonable limits.
constexpr auto min_duration = base::Seconds(1.0 / 60.0);
constexpr auto max_duration = base::Seconds(1.0 / 24.0);
auto duration = frame.timestamp() - last_frame_timestamp_;
return std::clamp(duration, min_duration, max_duration);
}
VpxVideoEncoder::~VpxVideoEncoder() {
if (!codec_)
return;
// It's safe to call vpx_img_free, even if vpx_image_ has never been
// initialized. vpx_img_free is not going to deallocate the vpx_image_
// itself, only internal buffers.
vpx_img_free(&vpx_image_);
}
void VpxVideoEncoder::Flush(EncoderStatusCB done_cb) {
done_cb = BindCallbackToCurrentLoopIfNeeded(std::move(done_cb));
if (!codec_) {
std::move(done_cb).Run(
EncoderStatus::Codes::kEncoderInitializeNeverCompleted);
return;
}
// The libvpx encoder is operating synchronously and thus doesn't have to
// flush if and only if |g_lag_in_frames| is set to 0.
CHECK_EQ(codec_config_.g_lag_in_frames, 0u);
std::move(done_cb).Run(EncoderStatus::Codes::kOk);
}
VideoEncoderOutput VpxVideoEncoder::GetEncoderOutput(
int temporal_id,
base::TimeDelta timestamp,
gfx::ColorSpace color_space) const {
vpx_codec_iter_t iter = nullptr;
const vpx_codec_cx_pkt_t* pkt = nullptr;
VideoEncoderOutput output;
// We don't given timestamps to vpx_codec_encode() that's why
// pkt->data.frame.pts can't be used here.
output.timestamp = timestamp;
while ((pkt = vpx_codec_get_cx_data(codec_.get(), &iter))) {
if (pkt->kind == VPX_CODEC_CX_FRAME_PKT) {
// The encoder is operating synchronously. There should be exactly one
// encoded packet, or the frame is dropped.
CHECK_EQ(output.size, 0u);
output.size = pkt->data.frame.sz;
output.data = base::HeapArray<uint8_t>::Uninit(output.size);
memcpy(output.data.data(), pkt->data.frame.buf, output.size);
output.key_frame = (pkt->data.frame.flags & VPX_FRAME_IS_KEY) != 0;
output.temporal_id = output.key_frame ? 0 : temporal_id;
output.color_space = color_space;
}
}
return output;
}
void VpxVideoEncoder::RecreateVpxImageIfNeeded(vpx_img_fmt fmt,
bool needs_memory) {
const bool has_changed = vpx_image_.fmt != fmt ||
vpx_image_.d_w != codec_config_.g_w ||
vpx_image_.d_h != codec_config_.g_h;
if (!has_changed) {
return;
}
vpx_img_free(&vpx_image_);
if (needs_memory) {
CHECK(vpx_img_alloc(&vpx_image_, fmt, codec_config_.g_w, codec_config_.g_h,
/*align=*/1));
} else {
// Encoding will write the actual plane pointers, but we have to pass a
// value to vpx_img_wrap() to avoid an unnecessary allocation.
static const uint8_t unused = 0;
CHECK(vpx_img_wrap(&vpx_image_, fmt, codec_config_.g_w, codec_config_.g_h,
/*stride_align=*/1, const_cast<uint8_t*>(&unused)));
}
vpx_image_.bit_depth = (fmt & VPX_IMG_FMT_HIGHBITDEPTH) ? 16 : 8;
}
void VpxVideoEncoder::UpdateEncoderColorSpace() {
auto vpx_cs = VPX_CS_UNKNOWN;
switch (last_frame_color_space_.GetPrimaryID()) {
case gfx::ColorSpace::PrimaryID::BT709: {
const auto matrix_id = last_frame_color_space_.GetMatrixID();
if (matrix_id == gfx::ColorSpace::MatrixID::GBR ||
matrix_id == gfx::ColorSpace::MatrixID::RGB) {
vpx_cs = VPX_CS_SRGB;
} else {
vpx_cs = VPX_CS_BT_709;
}
break;
}
case gfx::ColorSpace::PrimaryID::BT2020:
vpx_cs = VPX_CS_BT_2020;
break;
case gfx::ColorSpace::PrimaryID::SMPTE170M:
vpx_cs = VPX_CS_SMPTE_170;
break;
case gfx::ColorSpace::PrimaryID::SMPTE240M:
vpx_cs = VPX_CS_SMPTE_240;
break;
case gfx::ColorSpace::PrimaryID::BT470BG:
vpx_cs = VPX_CS_BT_601;
break;
default:
break;
};
if (vpx_cs != VPX_CS_UNKNOWN) {
vpx_image_.cs = vpx_cs;
if (profile_ != VP8PROFILE_ANY) {
auto vpx_error =
vpx_codec_control(codec_.get(), VP9E_SET_COLOR_SPACE, vpx_cs);
if (vpx_error != VPX_CODEC_OK) {
LogVpxErrorMessage(codec_.get(), "Failed to set color space",
vpx_error);
}
}
}
if (last_frame_color_space_.GetRangeID() == gfx::ColorSpace::RangeID::FULL ||
last_frame_color_space_.GetRangeID() ==
gfx::ColorSpace::RangeID::LIMITED) {
const auto vpx_range =
last_frame_color_space_.GetRangeID() == gfx::ColorSpace::RangeID::FULL
? VPX_CR_FULL_RANGE
: VPX_CR_STUDIO_RANGE;
vpx_image_.range = vpx_range;
if (profile_ != VP8PROFILE_ANY) {
auto vpx_error =
vpx_codec_control(codec_.get(), VP9E_SET_COLOR_RANGE, vpx_range);
if (vpx_error != VPX_CODEC_OK) {
LogVpxErrorMessage(codec_.get(), "Failed to set color range",
vpx_error);
}
}
}
}
} // namespace media