721 lines
25 KiB
C++
721 lines
25 KiB
C++
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "VP8TrackEncoder.h"
|
|
|
|
#include <vpx/vp8cx.h>
|
|
#include <vpx/vpx_encoder.h>
|
|
|
|
#include "DriftCompensation.h"
|
|
#include "ImageConversion.h"
|
|
#include "mozilla/gfx/2D.h"
|
|
#include "prsystem.h"
|
|
#include "VideoSegment.h"
|
|
#include "VideoUtils.h"
|
|
#include "WebMWriter.h"
|
|
#include "mozilla/media/MediaUtils.h"
|
|
#include "mozilla/dom/ImageUtils.h"
|
|
#include "mozilla/dom/ImageBitmapBinding.h"
|
|
#include "mozilla/ProfilerLabels.h"
|
|
|
|
namespace mozilla {
|
|
|
|
LazyLogModule gVP8TrackEncoderLog("VP8TrackEncoder");
|
|
#define VP8LOG(level, msg, ...) \
|
|
MOZ_LOG(gVP8TrackEncoderLog, level, (msg, ##__VA_ARGS__))
|
|
|
|
constexpr int DEFAULT_BITRATE_BPS = 2500000;
|
|
constexpr int DEFAULT_KEYFRAME_INTERVAL_MS = 10000;
|
|
constexpr int DYNAMIC_MAXKFDIST_CHECK_INTERVAL = 5;
|
|
constexpr float DYNAMIC_MAXKFDIST_DIFFACTOR = 0.4;
|
|
constexpr float DYNAMIC_MAXKFDIST_KFINTERVAL_FACTOR = 0.75;
|
|
constexpr int I420_STRIDE_ALIGN = 16;
|
|
|
|
using namespace mozilla::gfx;
|
|
using namespace mozilla::layers;
|
|
using namespace mozilla::media;
|
|
using namespace mozilla::dom;
|
|
|
|
namespace {
|
|
|
|
template <int N>
|
|
static int Aligned(int aValue) {
|
|
if (aValue < N) {
|
|
return N;
|
|
}
|
|
|
|
// The `- 1` avoids overreaching when `aValue % N == 0`.
|
|
return (((aValue - 1) / N) + 1) * N;
|
|
}
|
|
|
|
template <int Alignment>
|
|
size_t I420Size(int aWidth, int aHeight) {
|
|
int yStride = Aligned<Alignment>(aWidth);
|
|
int yHeight = aHeight;
|
|
size_t yPlaneSize = yStride * yHeight;
|
|
|
|
int uvStride = Aligned<Alignment>((aWidth + 1) / 2);
|
|
int uvHeight = (aHeight + 1) / 2;
|
|
size_t uvPlaneSize = uvStride * uvHeight;
|
|
|
|
return yPlaneSize + uvPlaneSize * 2;
|
|
}
|
|
|
|
nsresult CreateEncoderConfig(int32_t aWidth, int32_t aHeight,
|
|
uint32_t aVideoBitrate, TrackRate aTrackRate,
|
|
int32_t aMaxKeyFrameDistance,
|
|
vpx_codec_enc_cfg_t* config) {
|
|
// Encoder configuration structure.
|
|
memset(config, 0, sizeof(vpx_codec_enc_cfg_t));
|
|
if (vpx_codec_enc_config_default(vpx_codec_vp8_cx(), config, 0)) {
|
|
VP8LOG(LogLevel::Error, "Failed to get default configuration");
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
config->g_w = aWidth;
|
|
config->g_h = aHeight;
|
|
// TODO: Maybe we should have various aFrameRate bitrate pair for each
|
|
// devices? or for different platform
|
|
|
|
// rc_target_bitrate needs kbit/s
|
|
config->rc_target_bitrate = std::max(
|
|
1U, (aVideoBitrate != 0 ? aVideoBitrate : DEFAULT_BITRATE_BPS) / 1000);
|
|
|
|
// Setting the time base of the codec
|
|
config->g_timebase.num = 1;
|
|
config->g_timebase.den = aTrackRate;
|
|
|
|
// No error resilience as this is not intended for UDP transports
|
|
config->g_error_resilient = 0;
|
|
|
|
// Allow some frame lagging for large timeslices (when low latency is not
|
|
// needed)
|
|
/*std::min(10U, mKeyFrameInterval / 200)*/
|
|
config->g_lag_in_frames = 0;
|
|
|
|
int32_t number_of_cores = PR_GetNumberOfProcessors();
|
|
if (aWidth * aHeight > 1920 * 1080 && number_of_cores >= 8) {
|
|
config->g_threads = 4; // 4 threads for > 1080p.
|
|
} else if (aWidth * aHeight > 1280 * 960 && number_of_cores >= 6) {
|
|
config->g_threads = 3; // 3 threads for 1080p.
|
|
} else if (aWidth * aHeight > 640 * 480 && number_of_cores >= 3) {
|
|
config->g_threads = 2; // 2 threads for qHD/HD.
|
|
} else {
|
|
config->g_threads = 1; // 1 thread for VGA or less
|
|
}
|
|
|
|
// rate control settings
|
|
|
|
// No frame dropping
|
|
config->rc_dropframe_thresh = 0;
|
|
// Variable bitrate
|
|
config->rc_end_usage = VPX_VBR;
|
|
// Single pass encoding
|
|
config->g_pass = VPX_RC_ONE_PASS;
|
|
// ffmpeg doesn't currently support streams that use resize.
|
|
// Therefore, for safety, we should turn it off until it does.
|
|
config->rc_resize_allowed = 0;
|
|
// Allows 100% under target bitrate to compensate for prior overshoot
|
|
config->rc_undershoot_pct = 100;
|
|
// Allows 15% over target bitrate to compensate for prior undershoot
|
|
config->rc_overshoot_pct = 15;
|
|
// Tells the decoding application to buffer 500ms before beginning playback
|
|
config->rc_buf_initial_sz = 500;
|
|
// The decoding application will try to keep 600ms of buffer during playback
|
|
config->rc_buf_optimal_sz = 600;
|
|
// The decoding application may buffer 1000ms worth of encoded data
|
|
config->rc_buf_sz = 1000;
|
|
|
|
// We set key frame interval to automatic and try to set kf_max_dist so that
|
|
// the encoder chooses to put keyframes slightly more often than
|
|
// mKeyFrameInterval (which will encode with VPX_EFLAG_FORCE_KF when reached).
|
|
config->kf_mode = VPX_KF_AUTO;
|
|
config->kf_max_dist = aMaxKeyFrameDistance;
|
|
|
|
return NS_OK;
|
|
}
|
|
} // namespace
|
|
|
|
VP8TrackEncoder::VP8TrackEncoder(RefPtr<DriftCompensator> aDriftCompensator,
|
|
TrackRate aTrackRate,
|
|
MediaQueue<EncodedFrame>& aEncodedDataQueue,
|
|
FrameDroppingMode aFrameDroppingMode,
|
|
Maybe<float> aKeyFrameIntervalFactor)
|
|
: VideoTrackEncoder(std::move(aDriftCompensator), aTrackRate,
|
|
aEncodedDataQueue, aFrameDroppingMode),
|
|
mKeyFrameInterval(
|
|
TimeDuration::FromMilliseconds(DEFAULT_KEYFRAME_INTERVAL_MS)),
|
|
mKeyFrameIntervalFactor(aKeyFrameIntervalFactor.valueOr(
|
|
DYNAMIC_MAXKFDIST_KFINTERVAL_FACTOR)) {
|
|
MOZ_COUNT_CTOR(VP8TrackEncoder);
|
|
CalculateMaxKeyFrameDistance().apply(
|
|
[&](auto aKfd) { SetMaxKeyFrameDistance(aKfd); });
|
|
}
|
|
|
|
VP8TrackEncoder::~VP8TrackEncoder() {
|
|
Destroy();
|
|
MOZ_COUNT_DTOR(VP8TrackEncoder);
|
|
}
|
|
|
|
void VP8TrackEncoder::Destroy() {
|
|
if (mInitialized) {
|
|
vpx_codec_destroy(&mVPXContext);
|
|
}
|
|
|
|
mInitialized = false;
|
|
}
|
|
|
|
Maybe<int32_t> VP8TrackEncoder::CalculateMaxKeyFrameDistance(
|
|
Maybe<float> aEstimatedFrameRate /* = Nothing() */) const {
|
|
if (!aEstimatedFrameRate && mMeanFrameDuration.empty()) {
|
|
// Not enough data to make a new calculation.
|
|
return Nothing();
|
|
}
|
|
|
|
// Calculate an estimation of our current framerate
|
|
const float estimatedFrameRate = aEstimatedFrameRate.valueOrFrom(
|
|
[&] { return 1.0f / mMeanFrameDuration.mean().ToSeconds(); });
|
|
// Set a kf_max_dist that should avoid triggering the VPX_EFLAG_FORCE_KF flag
|
|
return Some(std::max(
|
|
1, static_cast<int32_t>(estimatedFrameRate * mKeyFrameIntervalFactor *
|
|
mKeyFrameInterval.ToSeconds())));
|
|
}
|
|
|
|
void VP8TrackEncoder::SetMaxKeyFrameDistance(int32_t aMaxKeyFrameDistance) {
|
|
if (mInitialized) {
|
|
VP8LOG(
|
|
LogLevel::Debug,
|
|
"%p SetMaxKeyFrameDistance() set kf_max_dist to %d based on estimated "
|
|
"framerate %.2ffps keyframe-factor %.2f and keyframe-interval %.2fs",
|
|
this, aMaxKeyFrameDistance, 1 / mMeanFrameDuration.mean().ToSeconds(),
|
|
mKeyFrameIntervalFactor, mKeyFrameInterval.ToSeconds());
|
|
DebugOnly<nsresult> rv =
|
|
Reconfigure(mFrameWidth, mFrameHeight, aMaxKeyFrameDistance);
|
|
MOZ_ASSERT(
|
|
NS_SUCCEEDED(rv),
|
|
"Reconfig for new key frame distance with proven size should succeed");
|
|
} else {
|
|
VP8LOG(LogLevel::Debug, "%p SetMaxKeyFrameDistance() distance=%d", this,
|
|
aMaxKeyFrameDistance);
|
|
mMaxKeyFrameDistance = Some(aMaxKeyFrameDistance);
|
|
}
|
|
}
|
|
|
|
nsresult VP8TrackEncoder::Init(int32_t aWidth, int32_t aHeight,
|
|
int32_t aDisplayWidth, int32_t aDisplayHeight,
|
|
float aEstimatedFrameRate) {
|
|
if (aDisplayWidth < 1 || aDisplayHeight < 1) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
if (aEstimatedFrameRate <= 0) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
int32_t maxKeyFrameDistance =
|
|
*CalculateMaxKeyFrameDistance(Some(aEstimatedFrameRate));
|
|
|
|
nsresult rv = InitInternal(aWidth, aHeight, maxKeyFrameDistance);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
MOZ_ASSERT(!mI420Frame);
|
|
MOZ_ASSERT(mI420FrameSize == 0);
|
|
const size_t neededSize = I420Size<I420_STRIDE_ALIGN>(aWidth, aHeight);
|
|
mI420Frame.reset(new (fallible) uint8_t[neededSize]);
|
|
mI420FrameSize = mI420Frame ? neededSize : 0;
|
|
if (!mI420Frame) {
|
|
VP8LOG(LogLevel::Warning, "Allocating I420 frame of size %zu failed",
|
|
neededSize);
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
vpx_img_wrap(&mVPXImageWrapper, VPX_IMG_FMT_I420, aWidth, aHeight,
|
|
I420_STRIDE_ALIGN, mI420Frame.get());
|
|
|
|
if (!mMetadata) {
|
|
mMetadata = MakeAndAddRef<VP8Metadata>();
|
|
mMetadata->mWidth = aWidth;
|
|
mMetadata->mHeight = aHeight;
|
|
mMetadata->mDisplayWidth = aDisplayWidth;
|
|
mMetadata->mDisplayHeight = aDisplayHeight;
|
|
|
|
VP8LOG(LogLevel::Info,
|
|
"%p Init() created metadata. width=%d, height=%d, displayWidth=%d, "
|
|
"displayHeight=%d, framerate=%.2f",
|
|
this, mMetadata->mWidth, mMetadata->mHeight,
|
|
mMetadata->mDisplayWidth, mMetadata->mDisplayHeight,
|
|
aEstimatedFrameRate);
|
|
|
|
SetInitialized();
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult VP8TrackEncoder::InitInternal(int32_t aWidth, int32_t aHeight,
|
|
int32_t aMaxKeyFrameDistance) {
|
|
if (aWidth < 1 || aHeight < 1) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
if (mInitialized) {
|
|
MOZ_ASSERT(false);
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
VP8LOG(LogLevel::Debug,
|
|
"%p InitInternal(). width=%d, height=%d, kf_max_dist=%d", this, aWidth,
|
|
aHeight, aMaxKeyFrameDistance);
|
|
|
|
// Encoder configuration structure.
|
|
vpx_codec_enc_cfg_t config;
|
|
nsresult rv = CreateEncoderConfig(aWidth, aHeight, mVideoBitrate, mTrackRate,
|
|
aMaxKeyFrameDistance, &config);
|
|
NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
|
|
|
|
vpx_codec_flags_t flags = 0;
|
|
flags |= VPX_CODEC_USE_OUTPUT_PARTITION;
|
|
if (vpx_codec_enc_init(&mVPXContext, vpx_codec_vp8_cx(), &config, flags)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
vpx_codec_control(&mVPXContext, VP8E_SET_STATIC_THRESHOLD, 1);
|
|
vpx_codec_control(&mVPXContext, VP8E_SET_CPUUSED, 15);
|
|
vpx_codec_control(&mVPXContext, VP8E_SET_TOKEN_PARTITIONS,
|
|
VP8_TWO_TOKENPARTITION);
|
|
|
|
mFrameWidth = aWidth;
|
|
mFrameHeight = aHeight;
|
|
mMaxKeyFrameDistance = Some(aMaxKeyFrameDistance);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult VP8TrackEncoder::Reconfigure(int32_t aWidth, int32_t aHeight,
|
|
int32_t aMaxKeyFrameDistance) {
|
|
if (aWidth <= 0 || aHeight <= 0) {
|
|
MOZ_ASSERT(false);
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
if (!mInitialized) {
|
|
MOZ_ASSERT(false);
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
bool needsReInit = aMaxKeyFrameDistance != *mMaxKeyFrameDistance;
|
|
|
|
if (aWidth != mFrameWidth || aHeight != mFrameHeight) {
|
|
VP8LOG(LogLevel::Info, "Dynamic resolution change (%dx%d -> %dx%d).",
|
|
mFrameWidth, mFrameHeight, aWidth, aHeight);
|
|
const size_t neededSize = I420Size<I420_STRIDE_ALIGN>(aWidth, aHeight);
|
|
if (neededSize > mI420FrameSize) {
|
|
needsReInit = true;
|
|
mI420Frame.reset(new (fallible) uint8_t[neededSize]);
|
|
mI420FrameSize = mI420Frame ? neededSize : 0;
|
|
}
|
|
if (!mI420Frame) {
|
|
VP8LOG(LogLevel::Warning, "Allocating I420 frame of size %zu failed",
|
|
neededSize);
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
vpx_img_wrap(&mVPXImageWrapper, VPX_IMG_FMT_I420, aWidth, aHeight,
|
|
I420_STRIDE_ALIGN, mI420Frame.get());
|
|
}
|
|
|
|
if (needsReInit) {
|
|
Destroy();
|
|
mMaxKeyFrameDistance = Some(aMaxKeyFrameDistance);
|
|
nsresult rv = InitInternal(aWidth, aHeight, aMaxKeyFrameDistance);
|
|
NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
|
|
mInitialized = true;
|
|
return NS_OK;
|
|
}
|
|
|
|
// Encoder configuration structure.
|
|
vpx_codec_enc_cfg_t config;
|
|
nsresult rv = CreateEncoderConfig(aWidth, aHeight, mVideoBitrate, mTrackRate,
|
|
aMaxKeyFrameDistance, &config);
|
|
NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
|
|
// Set new configuration
|
|
if (vpx_codec_enc_config_set(&mVPXContext, &config) != VPX_CODEC_OK) {
|
|
VP8LOG(LogLevel::Error, "Failed to set new configuration");
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
mFrameWidth = aWidth;
|
|
mFrameHeight = aHeight;
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
already_AddRefed<TrackMetadataBase> VP8TrackEncoder::GetMetadata() {
|
|
AUTO_PROFILER_LABEL("VP8TrackEncoder::GetMetadata", OTHER);
|
|
|
|
MOZ_ASSERT(mInitialized);
|
|
|
|
if (!mInitialized) {
|
|
return nullptr;
|
|
}
|
|
|
|
MOZ_ASSERT(mMetadata);
|
|
return do_AddRef(mMetadata);
|
|
}
|
|
|
|
Result<RefPtr<EncodedFrame>, nsresult> VP8TrackEncoder::ExtractEncodedData() {
|
|
vpx_codec_iter_t iter = nullptr;
|
|
EncodedFrame::FrameType frameType = EncodedFrame::VP8_P_FRAME;
|
|
auto frameData = MakeRefPtr<EncodedFrame::FrameData>();
|
|
const vpx_codec_cx_pkt_t* pkt = nullptr;
|
|
while ((pkt = vpx_codec_get_cx_data(&mVPXContext, &iter)) != nullptr) {
|
|
switch (pkt->kind) {
|
|
case VPX_CODEC_CX_FRAME_PKT: {
|
|
// Copy the encoded data from libvpx to frameData
|
|
frameData->AppendElements((uint8_t*)pkt->data.frame.buf,
|
|
pkt->data.frame.sz);
|
|
break;
|
|
}
|
|
default: {
|
|
break;
|
|
}
|
|
}
|
|
// End of frame
|
|
if ((pkt->data.frame.flags & VPX_FRAME_IS_FRAGMENT) == 0) {
|
|
if (pkt->data.frame.flags & VPX_FRAME_IS_KEY) {
|
|
frameType = EncodedFrame::VP8_I_FRAME;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (frameData->IsEmpty()) {
|
|
return RefPtr<EncodedFrame>(nullptr);
|
|
}
|
|
|
|
if (!pkt) {
|
|
// This check silences a coverity warning about accessing a null pkt below.
|
|
return RefPtr<EncodedFrame>(nullptr);
|
|
}
|
|
|
|
if (pkt->data.frame.flags & VPX_FRAME_IS_KEY) {
|
|
// Update the since-last-keyframe counter, and account for this frame's
|
|
// time.
|
|
TrackTime frameTime = pkt->data.frame.pts;
|
|
DebugOnly<TrackTime> frameDuration = pkt->data.frame.duration;
|
|
MOZ_ASSERT(frameTime + frameDuration <= mEncodedTimestamp);
|
|
mDurationSinceLastKeyframe =
|
|
std::min(mDurationSinceLastKeyframe, mEncodedTimestamp - frameTime);
|
|
}
|
|
|
|
// Convert the timestamp and duration to Usecs.
|
|
media::TimeUnit timestamp = media::TimeUnit(pkt->data.frame.pts, mTrackRate);
|
|
if (!timestamp.IsValid()) {
|
|
NS_ERROR("Microsecond timestamp overflow");
|
|
return Err(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR);
|
|
}
|
|
|
|
mExtractedDuration += pkt->data.frame.duration;
|
|
if (!mExtractedDuration.isValid()) {
|
|
NS_ERROR("Duration overflow");
|
|
return Err(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR);
|
|
}
|
|
|
|
media::TimeUnit totalDuration =
|
|
media::TimeUnit(mExtractedDuration.value(), mTrackRate);
|
|
if (!totalDuration.IsValid()) {
|
|
NS_ERROR("Duration overflow");
|
|
return Err(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR);
|
|
}
|
|
|
|
media::TimeUnit duration = totalDuration - mExtractedDurationUs;
|
|
if (!duration.IsValid()) {
|
|
NS_ERROR("Duration overflow");
|
|
return Err(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR);
|
|
}
|
|
|
|
mExtractedDurationUs = totalDuration;
|
|
|
|
VP8LOG(LogLevel::Verbose,
|
|
"ExtractEncodedData TimeStamp %.2f, Duration %.2f, FrameType %d",
|
|
timestamp.ToSeconds(), duration.ToSeconds(), frameType);
|
|
|
|
if (static_cast<int>(totalDuration.ToSeconds()) /
|
|
DYNAMIC_MAXKFDIST_CHECK_INTERVAL >
|
|
static_cast<int>(mLastKeyFrameDistanceUpdate.ToSeconds()) /
|
|
DYNAMIC_MAXKFDIST_CHECK_INTERVAL) {
|
|
// The interval has passed since the last keyframe update. Update again.
|
|
mLastKeyFrameDistanceUpdate = totalDuration;
|
|
const int32_t maxKfDistance =
|
|
CalculateMaxKeyFrameDistance().valueOr(*mMaxKeyFrameDistance);
|
|
const float diffFactor =
|
|
static_cast<float>(maxKfDistance) / *mMaxKeyFrameDistance;
|
|
VP8LOG(LogLevel::Debug, "maxKfDistance: %d, factor: %.2f", maxKfDistance,
|
|
diffFactor);
|
|
if (std::abs(1.0 - diffFactor) > DYNAMIC_MAXKFDIST_DIFFACTOR) {
|
|
SetMaxKeyFrameDistance(maxKfDistance);
|
|
}
|
|
}
|
|
|
|
return MakeRefPtr<EncodedFrame>(timestamp, duration.ToMicroseconds(),
|
|
PR_USEC_PER_SEC, frameType,
|
|
std::move(frameData));
|
|
}
|
|
|
|
/**
|
|
* Encoding flow in Encode():
|
|
* 1: Assert valid state.
|
|
* 2: Encode the video chunks in mSourceSegment in a for-loop.
|
|
* 2.1: The duration is taken straight from the video chunk's duration.
|
|
* 2.2: Setup the video chunk with mVPXImageWrapper by PrepareRawFrame().
|
|
* 2.3: Pass frame to vp8 encoder by vpx_codec_encode().
|
|
* 2.4: Extract the encoded frame from encoder by ExtractEncodedData().
|
|
* 2.5: Set the nextEncodeOperation for the next frame.
|
|
* 2.6: If we are not skipping the next frame, add the encoded frame to
|
|
* mEncodedDataQueue. If we are skipping the next frame, extend the encoded
|
|
* frame's duration in the next run of the loop.
|
|
* 3. Clear aSegment.
|
|
*/
|
|
nsresult VP8TrackEncoder::Encode(VideoSegment* aSegment) {
|
|
MOZ_ASSERT(mInitialized);
|
|
MOZ_ASSERT(!IsEncodingComplete());
|
|
|
|
AUTO_PROFILER_LABEL("VP8TrackEncoder::Encode", OTHER);
|
|
|
|
EncodeOperation nextEncodeOperation = ENCODE_NORMAL_FRAME;
|
|
|
|
RefPtr<EncodedFrame> encodedFrame;
|
|
for (VideoSegment::ChunkIterator iter(*aSegment); !iter.IsEnded();
|
|
iter.Next()) {
|
|
VideoChunk& chunk = *iter;
|
|
|
|
VP8LOG(LogLevel::Verbose,
|
|
"nextEncodeOperation is %d for frame of duration %" PRId64,
|
|
nextEncodeOperation, chunk.GetDuration());
|
|
|
|
TimeStamp timebase = TimeStamp::Now();
|
|
|
|
// Encode frame.
|
|
if (nextEncodeOperation != SKIP_FRAME) {
|
|
MOZ_ASSERT(!encodedFrame);
|
|
nsresult rv = PrepareRawFrame(chunk);
|
|
NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
|
|
|
|
// Encode the data with VP8 encoder
|
|
int flags = 0;
|
|
if (nextEncodeOperation == ENCODE_I_FRAME) {
|
|
VP8LOG(LogLevel::Warning,
|
|
"MediaRecorder lagging behind. Encoding keyframe.");
|
|
flags |= VPX_EFLAG_FORCE_KF;
|
|
}
|
|
|
|
// Sum duration of non-key frames and force keyframe if exceeded the
|
|
// given keyframe interval
|
|
if (mKeyFrameInterval > TimeDuration::FromSeconds(0)) {
|
|
if (media::TimeUnit(mDurationSinceLastKeyframe, mTrackRate)
|
|
.ToTimeDuration() >= mKeyFrameInterval) {
|
|
VP8LOG(LogLevel::Warning,
|
|
"Reached mKeyFrameInterval without seeing a keyframe. Forcing "
|
|
"one. time: %.2f, interval: %.2f",
|
|
media::TimeUnit(mDurationSinceLastKeyframe, mTrackRate)
|
|
.ToSeconds(),
|
|
mKeyFrameInterval.ToSeconds());
|
|
mDurationSinceLastKeyframe = 0;
|
|
flags |= VPX_EFLAG_FORCE_KF;
|
|
}
|
|
mDurationSinceLastKeyframe += chunk.GetDuration();
|
|
}
|
|
|
|
if (vpx_codec_encode(&mVPXContext, &mVPXImageWrapper, mEncodedTimestamp,
|
|
(unsigned long)chunk.GetDuration(), flags,
|
|
VPX_DL_REALTIME)) {
|
|
VP8LOG(LogLevel::Error, "vpx_codec_encode failed to encode the frame.");
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
// Move forward the mEncodedTimestamp.
|
|
mEncodedTimestamp += chunk.GetDuration();
|
|
|
|
// Extract the encoded data from the underlying encoder and push it to
|
|
// mEncodedDataQueue.
|
|
auto result = ExtractEncodedData();
|
|
if (result.isErr()) {
|
|
VP8LOG(LogLevel::Error, "ExtractEncodedData failed.");
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
MOZ_ASSERT(result.inspect(),
|
|
"We expected a frame here. EOS is handled explicitly later");
|
|
encodedFrame = result.unwrap();
|
|
} else {
|
|
// SKIP_FRAME
|
|
|
|
MOZ_DIAGNOSTIC_ASSERT(encodedFrame);
|
|
|
|
if (mKeyFrameInterval > TimeDuration::FromSeconds(0)) {
|
|
mDurationSinceLastKeyframe += chunk.GetDuration();
|
|
}
|
|
|
|
// Move forward the mEncodedTimestamp.
|
|
mEncodedTimestamp += chunk.GetDuration();
|
|
|
|
// Extend the duration of the last encoded frame in mEncodedDataQueue
|
|
// because this frame will be skipped.
|
|
VP8LOG(LogLevel::Warning,
|
|
"MediaRecorder lagging behind. Skipping a frame.");
|
|
|
|
mExtractedDuration += chunk.mDuration;
|
|
if (!mExtractedDuration.isValid()) {
|
|
NS_ERROR("skipped duration overflow");
|
|
return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR;
|
|
}
|
|
|
|
media::TimeUnit totalDuration =
|
|
media::TimeUnit(mExtractedDuration.value(), mTrackRate);
|
|
media::TimeUnit skippedDuration = totalDuration - mExtractedDurationUs;
|
|
mExtractedDurationUs = totalDuration;
|
|
if (!skippedDuration.IsValid()) {
|
|
NS_ERROR("skipped duration overflow");
|
|
return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR;
|
|
}
|
|
|
|
encodedFrame = MakeRefPtr<EncodedFrame>(
|
|
encodedFrame->mTime,
|
|
encodedFrame->mDuration + skippedDuration.ToMicroseconds(),
|
|
encodedFrame->mDurationBase, encodedFrame->mFrameType,
|
|
encodedFrame->mFrameData);
|
|
}
|
|
|
|
mMeanFrameEncodeDuration.insert(TimeStamp::Now() - timebase);
|
|
mMeanFrameDuration.insert(
|
|
media::TimeUnit(chunk.GetDuration(), mTrackRate).ToTimeDuration());
|
|
nextEncodeOperation = GetNextEncodeOperation(
|
|
mMeanFrameEncodeDuration.mean(), mMeanFrameDuration.mean());
|
|
|
|
if (nextEncodeOperation != SKIP_FRAME) {
|
|
// Note that the next operation might be SKIP_FRAME even if there is no
|
|
// next frame.
|
|
mEncodedDataQueue.Push(encodedFrame.forget());
|
|
}
|
|
}
|
|
|
|
if (encodedFrame) {
|
|
// Push now if we ended on a SKIP_FRAME before.
|
|
mEncodedDataQueue.Push(encodedFrame.forget());
|
|
}
|
|
|
|
// Remove the chunks we have processed.
|
|
aSegment->Clear();
|
|
|
|
if (mEndOfStream) {
|
|
// EOS: Extract the remaining frames from the underlying encoder.
|
|
VP8LOG(LogLevel::Debug, "mEndOfStream is true");
|
|
// No more frames will be encoded. Clearing temporary frames saves some
|
|
// memory.
|
|
if (mI420Frame) {
|
|
mI420Frame = nullptr;
|
|
mI420FrameSize = 0;
|
|
}
|
|
// mMuteFrame must be released before gfx shutdown. We do it now since it
|
|
// may be too late when this VP8TrackEncoder gets destroyed.
|
|
mMuteFrame = nullptr;
|
|
// Bug 1243611, keep calling vpx_codec_encode and vpx_codec_get_cx_data
|
|
// until vpx_codec_get_cx_data return null.
|
|
while (true) {
|
|
if (vpx_codec_encode(&mVPXContext, nullptr, mEncodedTimestamp, 0, 0,
|
|
VPX_DL_REALTIME)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
auto result = ExtractEncodedData();
|
|
if (result.isErr()) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
if (!result.inspect()) {
|
|
// Null means end-of-stream.
|
|
break;
|
|
}
|
|
mEncodedDataQueue.Push(result.unwrap().forget());
|
|
}
|
|
mEncodedDataQueue.Finish();
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult VP8TrackEncoder::PrepareRawFrame(VideoChunk& aChunk) {
|
|
gfx::IntSize intrinsicSize = aChunk.mFrame.GetIntrinsicSize();
|
|
RefPtr<Image> img;
|
|
if (aChunk.mFrame.GetForceBlack() || aChunk.IsNull()) {
|
|
if (!mMuteFrame || mMuteFrame->GetSize() != intrinsicSize) {
|
|
mMuteFrame = mozilla::VideoFrame::CreateBlackImage(intrinsicSize);
|
|
}
|
|
if (!mMuteFrame) {
|
|
VP8LOG(LogLevel::Warning, "Failed to allocate black image of size %dx%d",
|
|
intrinsicSize.width, intrinsicSize.height);
|
|
return NS_OK;
|
|
}
|
|
img = mMuteFrame;
|
|
} else {
|
|
img = aChunk.mFrame.GetImage();
|
|
}
|
|
|
|
gfx::IntSize imgSize = img->GetSize();
|
|
if (imgSize != IntSize(mFrameWidth, mFrameHeight)) {
|
|
nsresult rv =
|
|
Reconfigure(imgSize.width, imgSize.height, *mMaxKeyFrameDistance);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
}
|
|
|
|
MOZ_ASSERT(mFrameWidth == imgSize.width);
|
|
MOZ_ASSERT(mFrameHeight == imgSize.height);
|
|
|
|
nsresult rv = ConvertToI420(img, mVPXImageWrapper.planes[VPX_PLANE_Y],
|
|
mVPXImageWrapper.stride[VPX_PLANE_Y],
|
|
mVPXImageWrapper.planes[VPX_PLANE_U],
|
|
mVPXImageWrapper.stride[VPX_PLANE_U],
|
|
mVPXImageWrapper.planes[VPX_PLANE_V],
|
|
mVPXImageWrapper.stride[VPX_PLANE_V], imgSize);
|
|
if (NS_FAILED(rv)) {
|
|
VP8LOG(LogLevel::Error, "Converting to I420 failed");
|
|
return rv;
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
// These two define value used in GetNextEncodeOperation to determine the
|
|
// EncodeOperation for next target frame.
|
|
#define I_FRAME_RATIO (0.85) // Effectively disabled, because perceived quality
|
|
#define SKIP_FRAME_RATIO (0.85)
|
|
|
|
/**
|
|
* Compares the elapsed time from the beginning of GetEncodedTrack and
|
|
* the processed frame duration in mSourceSegment
|
|
* in order to set the nextEncodeOperation for next target frame.
|
|
*/
|
|
VP8TrackEncoder::EncodeOperation VP8TrackEncoder::GetNextEncodeOperation(
|
|
TimeDuration aTimeElapsed, TimeDuration aProcessedDuration) {
|
|
if (mFrameDroppingMode == FrameDroppingMode::DISALLOW) {
|
|
return ENCODE_NORMAL_FRAME;
|
|
}
|
|
|
|
if (aTimeElapsed.ToSeconds() >
|
|
aProcessedDuration.ToSeconds() * SKIP_FRAME_RATIO) {
|
|
// The encoder is too slow.
|
|
// We should skip next frame to consume the mSourceSegment.
|
|
return SKIP_FRAME;
|
|
}
|
|
|
|
if (aTimeElapsed.ToSeconds() >
|
|
aProcessedDuration.ToSeconds() * I_FRAME_RATIO) {
|
|
// The encoder is a little slow.
|
|
// We force the encoder to encode an I-frame to accelerate.
|
|
return ENCODE_I_FRAME;
|
|
}
|
|
|
|
return ENCODE_NORMAL_FRAME;
|
|
}
|
|
|
|
} // namespace mozilla
|
|
|
|
#undef VP8LOG
|