blob: b9be90716429aa6305ebd2a5165ff7952a632b32 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/renderer/media_recorder/vpx_encoder.h"
#include <algorithm>
#include <string>
#include "base/bind.h"
#include "base/system/sys_info.h"
#include "base/threading/thread.h"
#include "base/trace_event/trace_event.h"
#include "media/base/video_frame.h"
#include "ui/gfx/geometry/size.h"
using media::VideoFrame;
using media::VideoFrameMetadata;
namespace content {
void VpxEncoder::VpxCodecDeleter::operator()(vpx_codec_ctx_t* codec) {
if (!codec)
return;
vpx_codec_err_t ret = vpx_codec_destroy(codec);
CHECK_EQ(ret, VPX_CODEC_OK);
delete codec;
}
static int GetNumberOfThreadsForEncoding() {
// Do not saturate CPU utilization just for encoding. On a lower-end system
// with only 1 or 2 cores, use only one thread for encoding. On systems with
// more cores, allow half of the cores to be used for encoding.
return std::min(8, (base::SysInfo::NumberOfProcessors() + 1) / 2);
}
// static
void VpxEncoder::ShutdownEncoder(std::unique_ptr<base::Thread> encoding_thread,
ScopedVpxCodecCtxPtr encoder) {
DCHECK(encoding_thread->IsRunning());
encoding_thread->Stop();
// Both |encoding_thread| and |encoder| will be destroyed at end-of-scope.
}
VpxEncoder::VpxEncoder(
bool use_vp9,
const VideoTrackRecorder::OnEncodedVideoCB& on_encoded_video_callback,
int32_t bits_per_second,
scoped_refptr<base::SingleThreadTaskRunner> main_task_runner)
: VideoTrackRecorder::Encoder(on_encoded_video_callback,
bits_per_second,
std::move(main_task_runner)),
use_vp9_(use_vp9) {
codec_config_.g_timebase.den = 0; // Not initialized.
alpha_codec_config_.g_timebase.den = 0; // Not initialized.
DCHECK(encoding_thread_->IsRunning());
}
VpxEncoder::~VpxEncoder() {
main_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&VpxEncoder::ShutdownEncoder, std::move(encoding_thread_),
std::move(encoder_)));
}
bool VpxEncoder::CanEncodeAlphaChannel() {
return true;
}
void VpxEncoder::EncodeOnEncodingTaskRunner(scoped_refptr<VideoFrame> frame,
base::TimeTicks capture_timestamp) {
TRACE_EVENT0("media", "VpxEncoder::EncodeOnEncodingTaskRunner");
DCHECK(encoding_task_runner_->BelongsToCurrentThread());
const gfx::Size frame_size = frame->visible_rect().size();
base::TimeDelta duration = EstimateFrameDuration(frame);
const media::WebmMuxer::VideoParameters video_params(frame);
if (!IsInitialized(codec_config_) ||
gfx::Size(codec_config_.g_w, codec_config_.g_h) != frame_size) {
ConfigureEncoderOnEncodingTaskRunner(frame_size, &codec_config_, &encoder_);
}
const bool frame_has_alpha = frame->format() == media::PIXEL_FORMAT_I420A;
// Split the duration between two encoder instances if alpha is encoded.
duration = frame_has_alpha ? duration / 2 : duration;
if (frame_has_alpha && (!IsInitialized(alpha_codec_config_) ||
gfx::Size(alpha_codec_config_.g_w,
alpha_codec_config_.g_h) != frame_size)) {
ConfigureEncoderOnEncodingTaskRunner(frame_size, &alpha_codec_config_,
&alpha_encoder_);
u_plane_stride_ = media::VideoFrame::RowBytes(
VideoFrame::kUPlane, frame->format(), frame_size.width());
v_plane_stride_ = media::VideoFrame::RowBytes(
VideoFrame::kVPlane, frame->format(), frame_size.width());
v_plane_offset_ = media::VideoFrame::PlaneSize(
frame->format(), VideoFrame::kUPlane, frame_size)
.GetArea();
alpha_dummy_planes_.resize(
v_plane_offset_ + media::VideoFrame::PlaneSize(
frame->format(), VideoFrame::kVPlane, frame_size)
.GetArea());
// It is more expensive to encode 0x00, so use 0x80 instead.
std::fill(alpha_dummy_planes_.begin(), alpha_dummy_planes_.end(), 0x80);
}
// If we introduced a new alpha frame, force keyframe.
const bool force_keyframe = frame_has_alpha && !last_frame_had_alpha_;
last_frame_had_alpha_ = frame_has_alpha;
std::unique_ptr<std::string> data(new std::string);
bool keyframe = false;
DoEncode(encoder_.get(), frame_size, frame->data(VideoFrame::kYPlane),
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), duration, force_keyframe,
data.get(), &keyframe);
std::unique_ptr<std::string> alpha_data(new std::string);
if (frame_has_alpha) {
bool alpha_keyframe = false;
DoEncode(alpha_encoder_.get(), frame_size, frame->data(VideoFrame::kAPlane),
frame->visible_data(VideoFrame::kAPlane),
frame->stride(VideoFrame::kAPlane), alpha_dummy_planes_.data(),
u_plane_stride_, alpha_dummy_planes_.data() + v_plane_offset_,
v_plane_stride_, duration, keyframe, alpha_data.get(),
&alpha_keyframe);
DCHECK_EQ(keyframe, alpha_keyframe);
}
frame = nullptr;
origin_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(OnFrameEncodeCompleted, on_encoded_video_callback_,
video_params, std::move(data), std::move(alpha_data),
capture_timestamp, keyframe));
}
void VpxEncoder::DoEncode(vpx_codec_ctx_t* const encoder,
const gfx::Size& frame_size,
uint8_t* const data,
uint8_t* const y_plane,
int y_stride,
uint8_t* const u_plane,
int u_stride,
uint8_t* const v_plane,
int v_stride,
const base::TimeDelta& duration,
bool force_keyframe,
std::string* const output_data,
bool* const keyframe) {
DCHECK(encoding_task_runner_->BelongsToCurrentThread());
vpx_image_t vpx_image;
vpx_image_t* const result =
vpx_img_wrap(&vpx_image, VPX_IMG_FMT_I420, frame_size.width(),
frame_size.height(), 1 /* align */, data);
DCHECK_EQ(result, &vpx_image);
vpx_image.planes[VPX_PLANE_Y] = y_plane;
vpx_image.planes[VPX_PLANE_U] = u_plane;
vpx_image.planes[VPX_PLANE_V] = v_plane;
vpx_image.stride[VPX_PLANE_Y] = y_stride;
vpx_image.stride[VPX_PLANE_U] = u_stride;
vpx_image.stride[VPX_PLANE_V] = v_stride;
const vpx_codec_flags_t flags = force_keyframe ? VPX_EFLAG_FORCE_KF : 0;
// Encode the frame. The presentation time stamp argument here is fixed to
// zero to force the encoder to base its single-frame bandwidth calculations
// entirely on |predicted_frame_duration|.
const vpx_codec_err_t ret =
vpx_codec_encode(encoder, &vpx_image, 0 /* pts */,
duration.InMicroseconds(), flags, VPX_DL_REALTIME);
DCHECK_EQ(ret, VPX_CODEC_OK)
<< vpx_codec_err_to_string(ret) << ", #" << vpx_codec_error(encoder)
<< " -" << vpx_codec_error_detail(encoder);
*keyframe = false;
vpx_codec_iter_t iter = nullptr;
const vpx_codec_cx_pkt_t* pkt = nullptr;
while ((pkt = vpx_codec_get_cx_data(encoder, &iter)) != nullptr) {
if (pkt->kind != VPX_CODEC_CX_FRAME_PKT)
continue;
output_data->assign(static_cast<char*>(pkt->data.frame.buf),
pkt->data.frame.sz);
*keyframe = (pkt->data.frame.flags & VPX_FRAME_IS_KEY) != 0;
break;
}
}
void VpxEncoder::ConfigureEncoderOnEncodingTaskRunner(
const gfx::Size& size,
vpx_codec_enc_cfg_t* codec_config,
ScopedVpxCodecCtxPtr* encoder) {
DCHECK(encoding_task_runner_->BelongsToCurrentThread());
if (IsInitialized(*codec_config)) {
// TODO(mcasas) VP8 quirk/optimisation: If the new |size| is strictly less-
// than-or-equal than the old size, in terms of area, the existing encoder
// instance could be reused after changing |codec_config->{g_w,g_h}|.
DVLOG(1) << "Destroying/Re-Creating encoder for new frame size: "
<< gfx::Size(codec_config->g_w, codec_config->g_h).ToString()
<< " --> " << size.ToString() << (use_vp9_ ? " vp9" : " vp8");
encoder->reset();
}
const vpx_codec_iface_t* codec_interface =
use_vp9_ ? vpx_codec_vp9_cx() : vpx_codec_vp8_cx();
vpx_codec_err_t result = vpx_codec_enc_config_default(
codec_interface, codec_config, 0 /* reserved */);
DCHECK_EQ(VPX_CODEC_OK, result);
DCHECK_EQ(320u, codec_config->g_w);
DCHECK_EQ(240u, codec_config->g_h);
DCHECK_EQ(256u, codec_config->rc_target_bitrate);
// Use the selected bitrate or adjust default bit rate to account for the
// actual size. Note: |rc_target_bitrate| units are kbit per second.
if (bits_per_second_ > 0) {
codec_config->rc_target_bitrate = bits_per_second_ / 1000;
} else {
codec_config->rc_target_bitrate = size.GetArea() *
codec_config->rc_target_bitrate /
codec_config->g_w / codec_config->g_h;
}
// Both VP8/VP9 configuration should be Variable BitRate by default.
DCHECK_EQ(VPX_VBR, codec_config->rc_end_usage);
if (use_vp9_) {
// Number of frames to consume before producing output.
codec_config->g_lag_in_frames = 0;
// DCHECK that the profile selected by default is I420 (magic number 0).
DCHECK_EQ(0u, codec_config->g_profile);
} else {
// VP8 always produces frames instantaneously.
DCHECK_EQ(0u, codec_config->g_lag_in_frames);
}
DCHECK(size.width());
DCHECK(size.height());
codec_config->g_w = size.width();
codec_config->g_h = size.height();
codec_config->g_pass = VPX_RC_ONE_PASS;
// Timebase is the smallest interval used by the stream, can be set to the
// frame rate or to e.g. microseconds.
codec_config->g_timebase.num = 1;
codec_config->g_timebase.den = base::Time::kMicrosecondsPerSecond;
// Let the encoder decide where to place the Keyframes, between min and max.
// In VPX_KF_AUTO mode libvpx will sometimes emit keyframes regardless of min/
// max distance out of necessity.
// Note that due to http://crbug.com/440223, it might be necessary to force a
// key frame after 10,000frames since decoding fails after 30,000 non-key
// frames.
// Forcing a keyframe in regular intervals also allows seeking in the
// resulting recording with decent performance.
codec_config->kf_mode = VPX_KF_AUTO;
codec_config->kf_min_dist = 0;
codec_config->kf_max_dist = 100;
codec_config->g_threads = GetNumberOfThreadsForEncoding();
// Number of frames to consume before producing output.
codec_config->g_lag_in_frames = 0;
encoder->reset(new vpx_codec_ctx_t);
const vpx_codec_err_t ret = vpx_codec_enc_init(
encoder->get(), codec_interface, codec_config, 0 /* flags */);
DCHECK_EQ(VPX_CODEC_OK, ret);
if (use_vp9_) {
// Values of VP8E_SET_CPUUSED greater than 0 will increase encoder speed at
// the expense of quality up to a maximum value of 8 for VP9, by tuning the
// target time spent encoding the frame. Go from 8 to 5 (values for real
// time encoding) depending on the amount of cores available in the system.
const int kCpuUsed =
std::max(5, 8 - base::SysInfo::NumberOfProcessors() / 2);
result = vpx_codec_control(encoder->get(), VP8E_SET_CPUUSED, kCpuUsed);
DLOG_IF(WARNING, VPX_CODEC_OK != result) << "VP8E_SET_CPUUSED failed";
}
}
bool VpxEncoder::IsInitialized(const vpx_codec_enc_cfg_t& codec_config) const {
DCHECK(encoding_task_runner_->BelongsToCurrentThread());
return codec_config.g_timebase.den != 0;
}
base::TimeDelta VpxEncoder::EstimateFrameDuration(
const scoped_refptr<VideoFrame>& frame) {
DCHECK(encoding_task_runner_->BelongsToCurrentThread());
using base::TimeDelta;
TimeDelta predicted_frame_duration;
if (!frame->metadata()->GetTimeDelta(VideoFrameMetadata::FRAME_DURATION,
&predicted_frame_duration) ||
predicted_frame_duration <= TimeDelta()) {
// The source of the video frame did not provide the frame duration. Use
// the actual amount of time between the current and previous frame as a
// prediction for the next frame's duration.
// TODO(mcasas): This duration estimation could lead to artifacts if the
// cadence of the received stream is compromised (e.g. camera freeze, pause,
// remote packet loss). Investigate using GetFrameRate() in this case.
predicted_frame_duration = frame->timestamp() - last_frame_timestamp_;
}
last_frame_timestamp_ = frame->timestamp();
// Make sure |predicted_frame_duration| is in a safe range of values.
const TimeDelta kMaxFrameDuration = TimeDelta::FromSecondsD(1.0 / 8);
const TimeDelta kMinFrameDuration = TimeDelta::FromMilliseconds(1);
return std::min(kMaxFrameDuration,
std::max(predicted_frame_duration, kMinFrameDuration));
}
} // namespace content