diff --git a/docs/guide/advanced/video-codecs.md b/docs/guide/advanced/video-codecs.md index d80b970d..e185733e 100644 --- a/docs/guide/advanced/video-codecs.md +++ b/docs/guide/advanced/video-codecs.md @@ -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`. diff --git a/webrtc-jni/src/main/cpp/include/media/video/codec/EncoderOutputProcessor.h b/webrtc-jni/src/main/cpp/include/media/video/codec/EncoderOutputProcessor.h index 56720ca0..6b0f8ddf 100644 --- a/webrtc-jni/src/main/cpp/include/media/video/codec/EncoderOutputProcessor.h +++ b/webrtc-jni/src/main/cpp/include/media/video/codec/EncoderOutputProcessor.h @@ -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" @@ -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: @@ -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: @@ -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; }; } diff --git a/webrtc-jni/src/main/cpp/include/media/video/codec/HardwareCodecOptions.h b/webrtc-jni/src/main/cpp/include/media/video/codec/HardwareCodecOptions.h new file mode 100644 index 00000000..2b60dcd3 --- /dev/null +++ b/webrtc-jni/src/main/cpp/include/media/video/codec/HardwareCodecOptions.h @@ -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 diff --git a/webrtc-jni/src/main/cpp/include/media/video/codec/windows/MFVideoEncoderFactory.h b/webrtc-jni/src/main/cpp/include/media/video/codec/windows/MFVideoEncoderFactory.h index b107a25d..05e769ea 100644 --- a/webrtc-jni/src/main/cpp/include/media/video/codec/windows/MFVideoEncoderFactory.h +++ b/webrtc-jni/src/main/cpp/include/media/video/codec/windows/MFVideoEncoderFactory.h @@ -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 Create(); + // or AV1 encoder on this system, nor a VP9 one where that is + // enabled. + static std::unique_ptr Create(bool vp9Enabled); ~MFVideoEncoderFactory() override = default; @@ -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; }; } diff --git a/webrtc-jni/src/main/cpp/src/media/video/codec/EncoderOutputProcessor.cpp b/webrtc-jni/src/main/cpp/src/media/video/codec/EncoderOutputProcessor.cpp index e4306191..c541f6f0 100644 --- a/webrtc-jni/src/main/cpp/src/media/video/codec/EncoderOutputProcessor.cpp +++ b/webrtc-jni/src/main/cpp/src/media/video/codec/EncoderOutputProcessor.cpp @@ -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 @@ -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; @@ -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 bitstream, bool keyFrame, @@ -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; @@ -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; } @@ -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; @@ -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(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 @@ -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; } diff --git a/webrtc-jni/src/main/cpp/src/media/video/codec/HardwareCodecOptions.cpp b/webrtc-jni/src/main/cpp/src/media/video/codec/HardwareCodecOptions.cpp new file mode 100644 index 00000000..5e337059 --- /dev/null +++ b/webrtc-jni/src/main/cpp/src/media/video/codec/HardwareCodecOptions.cpp @@ -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 + +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; + } +} diff --git a/webrtc-jni/src/main/cpp/src/media/video/codec/windows/MFVideoEncoder.cpp b/webrtc-jni/src/main/cpp/src/media/video/codec/windows/MFVideoEncoder.cpp index 9d4420cc..5971d372 100644 --- a/webrtc-jni/src/main/cpp/src/media/video/codec/windows/MFVideoEncoder.cpp +++ b/webrtc-jni/src/main/cpp/src/media/video/codec/windows/MFVideoEncoder.cpp @@ -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; } @@ -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 diff --git a/webrtc-jni/src/main/cpp/src/media/video/codec/windows/MFVideoEncoderFactory.cpp b/webrtc-jni/src/main/cpp/src/media/video/codec/windows/MFVideoEncoderFactory.cpp index 7ef1dd53..2b4519c6 100644 --- a/webrtc-jni/src/main/cpp/src/media/video/codec/windows/MFVideoEncoderFactory.cpp +++ b/webrtc-jni/src/main/cpp/src/media/video/codec/windows/MFVideoEncoderFactory.cpp @@ -47,11 +47,12 @@ namespace jni } } - std::unique_ptr MFVideoEncoderFactory::Create() + std::unique_ptr MFVideoEncoderFactory::Create(bool vp9Enabled) { bool h264 = false; bool h265 = false; bool av1 = false; + bool vp9 = false; try { ComInitializer comInitializer; @@ -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(new MFVideoEncoderFactory(h264, h265, av1)); + return std::unique_ptr(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) { } @@ -96,6 +103,9 @@ namespace jni if (av1) { formats.push_back(webrtc::SdpVideoFormat::AV1Profile0()); } + if (vp9) { + formats.push_back(webrtc::SdpVideoFormat::VP9Profile0()); + } return formats; } diff --git a/webrtc-jni/src/main/cpp/src/media/video/codec/windows/WindowsHardwareVideoCodecFactories.cpp b/webrtc-jni/src/main/cpp/src/media/video/codec/windows/WindowsHardwareVideoCodecFactories.cpp index d651eb55..8800a1ad 100644 --- a/webrtc-jni/src/main/cpp/src/media/video/codec/windows/WindowsHardwareVideoCodecFactories.cpp +++ b/webrtc-jni/src/main/cpp/src/media/video/codec/windows/WindowsHardwareVideoCodecFactories.cpp @@ -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" @@ -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)); } diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/media/video/codec/HardwareVideoEncoderFactory.java b/webrtc/src/main/java/dev/onvoid/webrtc/media/video/codec/HardwareVideoEncoderFactory.java index 7694822a..f181aef1 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/media/video/codec/HardwareVideoEncoderFactory.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/media/video/codec/HardwareVideoEncoderFactory.java @@ -40,6 +40,11 @@ * have an AV1 encoder, with NVENC or Media Foundation, as a single layer; a * stream that asks for SVC is encoded in software. H.265 (Main profile) is * encoded with NVENC or Media Foundation on GPUs that have an H.265 encoder. + * VP9 (profile 0) is encoded with Media Foundation on GPUs that have a VP9 + * encoder, which on Windows are Intel's, but only with the system property + * {@code webrtc.hardware.vp9Encoder} set to {@code true} when the factory is + * created: the encoder has not been tested on such a GPU yet. Without it, VP9 + * is encoded with libvpx. * A hardware encoder that fails to start, for example because the GPU has no * encoder sessions left, or fails while encoding, is replaced by the next one * in line, and finally by the software encoder of the same codec, so a stream diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/HardwareVideoEncoderIntegrationTest.java b/webrtc/src/test/java/dev/onvoid/webrtc/HardwareVideoEncoderIntegrationTest.java index d6a42229..48ea15c8 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/HardwareVideoEncoderIntegrationTest.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/HardwareVideoEncoderIntegrationTest.java @@ -49,9 +49,11 @@ * A machine without a hardware encoder skips the tests that need one, as CI * runners do. Set the system property {@code webrtc.test.hardwareEncoder} to * {@code true} on a machine that has one, to make those tests fail instead, - * {@code webrtc.test.hardwareAv1Encoder} for a GPU that encodes AV1, and + * {@code webrtc.test.hardwareAv1Encoder} for a GPU that encodes AV1, * {@code webrtc.test.hardwareH265Encoder} for one that encodes and decodes - * H.265, which a call in H.265 needs at both ends, having no software codec. + * H.265, which a call in H.265 needs at both ends, having no software codec, + * and {@code webrtc.test.hardwareVp9Encoder} for one that encodes VP9 (Intel, + * on Windows), which is tested with hardware VP9 encoding turned on. */ @Execution(ExecutionMode.SAME_THREAD) class HardwareVideoEncoderIntegrationTest extends TestBase { @@ -64,6 +66,11 @@ class HardwareVideoEncoderIntegrationTest extends TestBase { private static final boolean HARDWARE_H265_REQUIRED = Boolean.getBoolean("webrtc.test.hardwareH265Encoder"); + private static final boolean HARDWARE_VP9_REQUIRED = Boolean.getBoolean("webrtc.test.hardwareVp9Encoder"); + + /** The property that turns on hardware VP9 encoding, which is off by default. */ + private static final String VP9_ENCODER_PROPERTY = "webrtc.hardware.vp9Encoder"; + private static final String OS = System.getProperty("os.name").toLowerCase(Locale.ROOT); /** @@ -79,6 +86,10 @@ class HardwareVideoEncoderIntegrationTest extends TestBase { private static final Predicate H265 = codec -> "H265".equalsIgnoreCase(codec.getName()); + private static final Predicate VP9 = codec -> + "VP9".equalsIgnoreCase(codec.getName()) + && "0".equals(codec.getSDPFmtp().getOrDefault("profile-id", "0")); + @Test void hardwareKeepsCodecs() { @@ -248,6 +259,53 @@ private PeerConnectionFactory hardwareCallFactory() { .build(); } + @Test + void vp9HardwareEncodingIsOptIn() throws Exception { + assumeTrue(OS.contains("win") || OS.contains("linux"), + "hardware encoders are implemented on Windows and Linux only"); + assumeTrue(System.getProperty(VP9_ENCODER_PROPERTY) == null, VP9_ENCODER_PROPERTY + " is set"); + + PeerConnectionFactory hardware = hardwareFactory(); + + try { + // Without the property, VP9 stays with libvpx on every GPU. + String implementation = encoderImplementation(hardware, VP9); + + assertTrue(implementation.contains("libvpx"), implementation); + } + finally { + hardware.dispose(); + } + } + + @Test + void hardwareEncodesVp9WhenEnabled() throws Exception { + assumeTrue(OS.contains("win"), "hardware VP9 encoding is implemented on Windows only"); + + String previous = System.setProperty(VP9_ENCODER_PROPERTY, "true"); + + try { + PeerConnectionFactory hardware = hardwareFactory(); + + try { + // Only Intel GPUs encode VP9; elsewhere libvpx does, and the + // test is skipped unless hardware is required. + assertHardware(encoderImplementation(hardware, VP9), HARDWARE_VP9_REQUIRED); + } + finally { + hardware.dispose(); + } + } + finally { + if (previous == null) { + System.clearProperty(VP9_ENCODER_PROPERTY); + } + else { + System.setProperty(VP9_ENCODER_PROPERTY, previous); + } + } + } + private static boolean offersH265(List codecs) { return codecs.stream().anyMatch(codec -> "H265".equalsIgnoreCase(codec.getName())); }