Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/guide/advanced/video-codecs.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ PeerConnectionFactory factory = PeerConnectionFactory.builder()

Hardware encoders produce a single layer. A stream that asks for SVC, such as an AV1 stream with a scalability mode like `L1T3`, is encoded in software.

VP9 can be encoded in hardware too, on Windows with the Media Foundation VP9 encoder of the GPU driver, which Intel GPUs have. It is off by default, since it has not been tested on such a GPU yet; turn it on with a system property before the factory is created:

```java
System.setProperty("webrtc.hardware.vp9Encoder", "true");
```

Where the GPU has no VP9 encoder, or the stream asks for SVC, VP9 is still encoded with libvpx.

NVENC needs an NVIDIA driver of version 522 or newer on Windows, 520 or newer on Linux. VA-API needs libva 2 and a driver that encodes H.264, such as Intel's `intel-media-va-driver` (iHD) or Mesa's `mesa-va-drivers` for AMD, and access to a render node in `/dev/dri`. All of them are loaded at run time, so nothing needs to be installed on machines without them.

The hardware encoders take over H.264 Constrained Baseline and Baseline with packetization mode 1, and AV1 profile 0, formats the software encoders offer too, so encoding these in hardware never changes what is negotiated. H.265 (Main profile) has no software encoder: it is offered after the other codecs, only where the hardware encodes it, so the codec a peer connection prefers stays the same, and a call uses H.265 only if it is preferred (e.g. with `RTCRtpTransceiver.setCodecPreferences()`) and the other end decodes it. On macOS, VideoToolbox is required to encode H.265 in hardware, and never falls back to its software encoder; a Mac without a hardware HEVC encoder does not offer H.265. When a hardware encoder fails to start, for example because the GPU has no encoder sessions left, or fails while encoding, the stream switches to the next encoder in line (e.g. from NVENC to Media Foundation on Windows), and finally to the software encoder, and continues with a key frame. On a machine without a hardware encoder, the factory encodes like `DefaultVideoEncoderFactory`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "common_video/h264/h264_bitstream_parser.h"
#include "common_video/h265/h265_bitstream_parser.h"
#include "modules/video_coding/codecs/h264/include/h264_globals.h"
#include "modules/video_coding/codecs/vp9/include/vp9_globals.h"
#include "modules/video_coding/include/video_codec_interface.h"
#include "modules/video_coding/svc/scalable_video_controller_no_layering.h"

Expand All @@ -43,7 +44,8 @@ namespace jni
// key frame, not necessarily in front of every one, so they are kept and
// put back where missing. The frame dependencies WebRTC needs for AV1 and
// H.265 are those of a stream without layers, which is all hardware
// encoders produce here.
// encoders produce here. VP9 frames go out as they come, with the VP9
// information of a single layer, as the Java encoders get it.
class EncoderOutputProcessor
{
public:
Expand All @@ -64,8 +66,9 @@ namespace jni
webrtc::CodecSpecificInfo & info);

// The quality scaling settings that suit the codec: the thresholds
// of WebRTC's H.264 encoder, for H.265 as well, and none for AV1,
// whose QP is not parsed from the bitstream.
// of WebRTC's H.264 encoder, for H.265 as well, those for VP9 of
// the Java encoders, and none for AV1, whose QP is not parsed from
// the bitstream.
webrtc::VideoEncoder::ScalingSettings GetScalingSettings() const;

private:
Expand All @@ -87,6 +90,11 @@ namespace jni
webrtc::H264BitstreamParser h264Parser;
webrtc::H265BitstreamParser h265Parser;
webrtc::ScalableVideoControllerNoLayering svcController;

// The group of frames VP9 describes: one layer, every frame
// referring to the one before.
webrtc::GofInfoVP9 gof;
size_t gofIndex = 0;
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2026 Alex Andres
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#ifndef JNI_WEBRTC_MEDIA_VIDEO_CODEC_HARDWARE_CODEC_OPTIONS_H_
#define JNI_WEBRTC_MEDIA_VIDEO_CODEC_HARDWARE_CODEC_OPTIONS_H_

namespace jni
{
// The Java system property that turns on hardware VP9 encoding, which is
// off by default: only Intel GPUs encode VP9, and the encoder has not been
// tested on one.
constexpr const char * kHardwareVp9EncoderProperty = "webrtc.hardware.vp9Encoder";

// Whether hardware VP9 encoding is turned on: the Java system property
// above is "true", as Boolean.getBoolean has it. Callable from any thread;
// false where Java cannot be asked, for one with an exception pending.
bool IsHardwareVp9EncoderEnabled();
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ namespace jni
// packetization mode 1, since mode 0 needs each NAL unit to fit a packet,
// which hardware encoders cannot be relied on to keep to, and AV1 in
// profile 0. H.265, which WebRTC has no encoder of its own for, is offered
// in Main profile.
// in Main profile. VP9 (profile 0), which only Intel GPUs encode, only if
// asked for.
class MFVideoEncoderFactory : public webrtc::VideoEncoderFactory
{
public:
// Returns a factory, or null if there is no hardware H.264, H.265
// or AV1 encoder on this system.
static std::unique_ptr<MFVideoEncoderFactory> Create();
// or AV1 encoder on this system, nor a VP9 one where that is
// enabled.
static std::unique_ptr<MFVideoEncoderFactory> Create(bool vp9Enabled);

~MFVideoEncoderFactory() override = default;

Expand All @@ -48,12 +50,13 @@ namespace jni
const webrtc::SdpVideoFormat & format) override;

private:
MFVideoEncoderFactory(bool h264, bool h265, bool av1);
MFVideoEncoderFactory(bool h264, bool h265, bool av1, bool vp9);

private:
const bool h264;
const bool h265;
const bool av1;
const bool vp9;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "common_video/h264/h264_common.h"
#include "common_video/h265/h265_common.h"
#include "modules/video_coding/codecs/interface/common_constants.h"
#include "modules/video_coding/utility/vp9_uncompressed_header_parser.h"
#include "rtc_base/logging.h"

namespace jni
Expand All @@ -31,6 +32,10 @@ namespace jni
constexpr int kLowH264QpThreshold = 24;
constexpr int kHighH264QpThreshold = 37;

// Those of the Java VP9 encoders, for a QP of 0 to 255.
constexpr int kLowVp9QpThreshold = 96;
constexpr int kHighVp9QpThreshold = 185;

// AV1 OBU types, section 6.2.2 of the AV1 specification.
constexpr uint8_t kObuSequenceHeader = 1;
constexpr uint8_t kObuTemporalDelimiter = 2;
Expand Down Expand Up @@ -121,11 +126,14 @@ namespace jni
if (mode == format.parameters.end() || mode->second != "1") {
packetizationMode = webrtc::H264PacketizationMode::SingleNalUnit;
}

gof.SetGofInfoVP9(webrtc::TemporalStructureMode::kTemporalStructureMode1);
}

void EncoderOutputProcessor::Reset()
{
parameterSets.clear();
gofIndex = 0;
}

bool EncoderOutputProcessor::Process(std::span<const uint8_t> bitstream, bool keyFrame,
Expand All @@ -141,6 +149,11 @@ namespace jni
case webrtc::kVideoCodecH265:
processed = ProcessH265(bitstream, keyFrame, output);
break;
case webrtc::kVideoCodecVP9:
// No parameter sets: every key frame carries what it needs.
output.assign(bitstream.begin(), bitstream.end());
processed = !output.empty();
break;
default:
processed = ProcessH264(bitstream, keyFrame, output);
break;
Expand All @@ -165,6 +178,10 @@ namespace jni
h265Parser.ParseBitstream(encoded);
image.qp_ = h265Parser.GetLastSliceQp().value_or(-1);
}
else if (codec == webrtc::kVideoCodecVP9) {
int qp = -1;
image.qp_ = webrtc::vp9::GetQp(output.data(), output.size(), &qp) ? qp : -1;
}
else {
image.qp_ = -1;
}
Expand Down Expand Up @@ -328,7 +345,8 @@ namespace jni
}

// AV1 and H.265 have no codec specific information; WebRTC sends
// their frame dependencies from these, in the dependency descriptor.
// their frame dependencies, and those of VP9, from these, in the
// dependency descriptor.
auto layerFrames = svcController.NextFrameConfig(/*restart=*/keyFrame);
info.generic_frame_info = svcController.OnEncodeDone(layerFrames[0]);
info.scalability_mode = webrtc::ScalabilityMode::kL1T1;
Expand All @@ -339,6 +357,32 @@ namespace jni
webrtc::RenderResolution(image._encodedWidth, image._encodedHeight)
};
}

if (codec == webrtc::kVideoCodecVP9) {
// VP9 has its own RTP header, which WebRTC fills from these: one
// spatial and one temporal layer, the group of frames sent with
// every key frame.
if (keyFrame) {
gofIndex = 0;
}

info.codecSpecific.VP9.inter_pic_predicted = !keyFrame;
info.codecSpecific.VP9.flexible_mode = false;
info.codecSpecific.VP9.ss_data_available = keyFrame;
info.codecSpecific.VP9.temporal_idx = webrtc::kNoTemporalIdx;
info.codecSpecific.VP9.temporal_up_switch = true;
info.codecSpecific.VP9.inter_layer_predicted = false;
info.codecSpecific.VP9.gof_idx = static_cast<uint8_t>(gofIndex++ % gof.num_frames_in_gof);
info.codecSpecific.VP9.num_spatial_layers = 1;
info.codecSpecific.VP9.first_frame_in_picture = true;
info.codecSpecific.VP9.spatial_layer_resolution_present = keyFrame;

if (keyFrame) {
info.codecSpecific.VP9.width[0] = image._encodedWidth;
info.codecSpecific.VP9.height[0] = image._encodedHeight;
info.codecSpecific.VP9.gof.CopyGofInfoVP9(gof);
}
}
}

webrtc::VideoEncoder::ScalingSettings EncoderOutputProcessor::GetScalingSettings() const
Expand All @@ -348,6 +392,9 @@ namespace jni
if (codec == webrtc::kVideoCodecH264 || codec == webrtc::kVideoCodecH265) {
return webrtc::VideoEncoder::ScalingSettings(kLowH264QpThreshold, kHighH264QpThreshold);
}
if (codec == webrtc::kVideoCodecVP9) {
return webrtc::VideoEncoder::ScalingSettings(kLowVp9QpThreshold, kHighVp9QpThreshold);
}

return webrtc::VideoEncoder::ScalingSettings::kOff;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2026 Alex Andres
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include "media/video/codec/HardwareCodecOptions.h"
#include "JavaUtils.h"

#include <jni.h>

namespace jni
{
bool IsHardwareVp9EncoderEnabled()
{
JNIEnv * env = AttachCurrentThread();

// An exception pending is someone else's to handle, and no JNI call
// may be made meanwhile.
if (env == nullptr || env->ExceptionCheck()) {
return false;
}
if (env->PushLocalFrame(4) != JNI_OK) {
env->ExceptionClear();
return false;
}

bool enabled = false;

// java.lang.Boolean comes from the bootstrap class loader, which any
// thread reaches, one without Java frames as well.
jclass booleanClass = env->FindClass("java/lang/Boolean");

if (booleanClass != nullptr) {
jmethodID getBoolean = env->GetStaticMethodID(booleanClass, "getBoolean", "(Ljava/lang/String;)Z");
jstring name = getBoolean != nullptr ? env->NewStringUTF(kHardwareVp9EncoderProperty) : nullptr;

if (name != nullptr) {
enabled = env->CallStaticBooleanMethod(booleanClass, getBoolean, name) == JNI_TRUE;
}
}

// Raised here, so cleared here.
if (env->ExceptionCheck()) {
env->ExceptionClear();
enabled = false;
}

env->PopLocalFrame(nullptr);

return enabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ namespace jni
return MFVideoFormat_AV1;
case webrtc::kVideoCodecH265:
return MFVideoFormat_HEVC;
case webrtc::kVideoCodecVP9:
return MFVideoFormat_VP90;
default:
return MFVideoFormat_H264;
}
Expand Down Expand Up @@ -311,6 +313,12 @@ namespace jni

hr = transform->SetOutputType(outputStreamId, outputType.Get(), 0);
}
else if (codec == webrtc::kVideoCodecVP9) {
// 8-bit 4:2:0: VP9 profile 0, what WebRTC offers.
outputType->SetUINT32(MF_MT_VIDEO_PROFILE, eAVEncVP9VProfile_420_8);

hr = transform->SetOutputType(outputStreamId, outputType.Get(), 0);
}
else {
// Constrained Baseline is what peers expect, and is a subset of
// Baseline too. Older encoders only know Baseline, which peers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ namespace jni
}
}

std::unique_ptr<MFVideoEncoderFactory> MFVideoEncoderFactory::Create()
std::unique_ptr<MFVideoEncoderFactory> MFVideoEncoderFactory::Create(bool vp9Enabled)
{
bool h264 = false;
bool h265 = false;
bool av1 = false;
bool vp9 = false;

try {
ComInitializer comInitializer;
Expand All @@ -60,23 +61,29 @@ namespace jni
h264 = HasHardwareEncoder(MFVideoFormat_H264);
h265 = HasHardwareEncoder(MFVideoFormat_HEVC);
av1 = HasHardwareEncoder(MFVideoFormat_AV1);
vp9 = vp9Enabled && HasHardwareEncoder(MFVideoFormat_VP90);
}
catch (...) {
return nullptr;
}

if (!h264 && !h265 && !av1) {
if (vp9Enabled && !vp9) {
RTC_LOG(LS_INFO) << "Hardware VP9 encoding is enabled, but there is no Media Foundation VP9 encoder";
}

if (!h264 && !h265 && !av1 && !vp9) {
RTC_LOG(LS_INFO) << "No Media Foundation hardware encoder found";
return nullptr;
}

return std::unique_ptr<MFVideoEncoderFactory>(new MFVideoEncoderFactory(h264, h265, av1));
return std::unique_ptr<MFVideoEncoderFactory>(new MFVideoEncoderFactory(h264, h265, av1, vp9));
}

MFVideoEncoderFactory::MFVideoEncoderFactory(bool h264, bool h265, bool av1) :
MFVideoEncoderFactory::MFVideoEncoderFactory(bool h264, bool h265, bool av1, bool vp9) :
h264(h264),
h265(h265),
av1(av1)
av1(av1),
vp9(vp9)
{
}

Expand All @@ -96,6 +103,9 @@ namespace jni
if (av1) {
formats.push_back(webrtc::SdpVideoFormat::AV1Profile0());
}
if (vp9) {
formats.push_back(webrtc::SdpVideoFormat::VP9Profile0());
}

return formats;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

#include "media/video/codec/HardwareCodecOptions.h"
#include "media/video/codec/HardwareVideoDecoderFactory.h"
#include "media/video/codec/HardwareVideoEncoderFactory.h"
#include "media/video/codec/nvenc/NvencVideoEncoderFactory.h"
Expand All @@ -32,7 +33,9 @@ namespace jni
if (auto nvenc = NvencVideoEncoderFactory::Create()) {
factories.push_back(std::move(nvenc));
}
if (auto mediaFoundation = MFVideoEncoderFactory::Create()) {
// VP9 only if asked for: only Intel GPUs encode it, and the encoder
// has not been tested on one.
if (auto mediaFoundation = MFVideoEncoderFactory::Create(IsHardwareVp9EncoderEnabled())) {
factories.push_back(std::move(mediaFoundation));
}

Expand Down
Loading
Loading