Skip to content

Commit d2aa2df

Browse files
authored
feat: decode VP9 with VideoToolbox on macOS (#318)
* feat: decode VP9 with VideoToolbox on macOS HardwareVideoDecoderFactory now decodes VP9 profile 0 in hardware on macOS, through the VP9 decoder VideoToolbox offers once it is registered. H.264 stays with the default decoders, which use VideoToolbox already. The decoder creates its session on the first key frame, from the header parsed with WebRTC's VP9 header parser: the format description carries the vpcC box and the colour properties, and the output pixel format follows the range of the stream. The whole encoded image goes in as one sample, so a hidden frame is not decoded on its own. A key frame of another size or range replaces the session. Frames come out as the CVPixelBuffer VideoToolbox made, without a copy. Anything it cannot decode goes to libvpx through FallbackVideoDecoder: other profiles, sizes outside 64x64 to 4096x4096, frames with spatial layers, a session that cannot be created or is not in hardware, and a decoder that keeps failing. The macOS shortcuts in DefaultVideoCodecFactories are gone; macOS now defines the platform hooks like Windows and Linux do. Tests: VP9 through VideoToolbox, VP9 in software by default, and a resolution change in the middle of a call. A new check makes sure decoded frames carry a picture. * fix: keep the VP9 decoder from failing inter frames the parser has no size for WebRTC's VP9 header parser gives a header only for a frame that states its size: key frames, and inter frames that do not take the size from a reference. Most inter frames take it, and the parser returns nothing for them. The decoder treated that as a failure, so in a stream without temporal layers every inter frame failed, the receiver asked for a key frame, and only that key frame decoded: a few frames a second, and a key frame request for nearly every frame. A frame without a header is decoded now. A key frame without one is handed to the software decoder, since there is nothing to create a session from. The tests did not notice because they waited for ten frames and checked the picture and the size. New ones read the statistics of the receiver, that a stream asks for no more than one key frame, and cover temporal layers and spatial layers, which a real libwebrtc stream now makes: L2T2 at 640x480 ends up in libvpx. The test call can send another size than 320x240 for that, since WebRTC encodes no spatial layers below a size.
1 parent 3b4fbf0 commit d2aa2df

10 files changed

Lines changed: 1122 additions & 18 deletions

File tree

‎docs/guide/advanced/video-codecs.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,13 @@ PeerConnectionFactory factory = PeerConnectionFactory.builder()
5353
|---|---|
5454
| Windows | H.264 and AV1, on GPUs that decode them, through the Media Foundation decoders of Windows on Direct3D 11 (DXVA) |
5555
| Linux | Not yet; decoding is in software |
56-
| macOS | VideoToolbox, as with `DefaultVideoDecoderFactory` |
56+
| macOS | H.264 through VideoToolbox, as with `DefaultVideoDecoderFactory`, and VP9 (profile 0) through VideoToolbox's VP9 decoder, on Macs that have one |
5757

5858
AV1 on Windows needs the *AV1 Video Extension*, which Windows 11 includes. Decoded frames are copied from GPU memory back to system memory, where WebRTC's frames are, so hardware decoding pays off mostly at high resolutions and with many streams; at low resolutions WebRTC's software decoders are about as cheap. A hardware decoder that fails, or turns out to decode in software, is replaced by the software decoder, which starts with the next key frame.
5959

60-
Which decoder a stream uses shows in the `decoderImplementation` statistic of its `inbound-rtp` stats, e.g. `MediaFoundation (Microsoft H264 Video Decoder MFT)` or `MediaFoundation (AV1VideoExtension)`.
60+
On macOS, VP9 decoding in hardware saves CPU: in a measurement on an Apple M2 it took a fraction of the processor time libvpx needs, but multi-threaded libvpx was about as fast on the clock. VideoToolbox decodes VP9 in hardware only where the Mac has a decoder for it, which Apple silicon does; a Mac without one decodes VP9 with libvpx as before. Profile 2 (10 bit), sizes below 64x64 or above 4096x4096, and streams with spatial layers (SVC) are decoded with libvpx too.
61+
62+
Which decoder a stream uses shows in the `decoderImplementation` statistic of its `inbound-rtp` stats, e.g. `MediaFoundation (Microsoft H264 Video Decoder MFT)`, `MediaFoundation (AV1VideoExtension)` or `VideoToolbox (VP9)`.
6163

6264
### Native Codecs
6365

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2026 Alex Andres
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#ifndef JNI_WEBRTC_MEDIA_VIDEO_CODEC_VT_VIDEO_DECODER_FACTORY_H_
18+
#define JNI_WEBRTC_MEDIA_VIDEO_CODEC_VT_VIDEO_DECODER_FACTORY_H_
19+
20+
#include "api/environment/environment.h"
21+
#include "api/video_codecs/sdp_video_format.h"
22+
#include "api/video_codecs/video_decoder.h"
23+
#include "api/video_codecs/video_decoder_factory.h"
24+
25+
#include <memory>
26+
#include <vector>
27+
28+
namespace jni
29+
{
30+
// Creates the VideoToolbox decoders that WebRTC's own decoders for macOS
31+
// do not offer: VP9, in profile 0, the format the software factory
32+
// offers first. H.264 is decoded through VideoToolbox by the default
33+
// decoders already.
34+
class VTVideoDecoderFactory : public webrtc::VideoDecoderFactory
35+
{
36+
public:
37+
// Returns a factory, or null if this Mac has no hardware decoder
38+
// for VP9. The decoder VideoToolbox has for VP9 has to be
39+
// registered first; this does it, once for the process.
40+
static std::unique_ptr<VTVideoDecoderFactory> Create();
41+
42+
~VTVideoDecoderFactory() override = default;
43+
44+
std::vector<webrtc::SdpVideoFormat> GetSupportedFormats() const override;
45+
std::unique_ptr<webrtc::VideoDecoder> Create(const webrtc::Environment & env,
46+
const webrtc::SdpVideoFormat & format) override;
47+
48+
private:
49+
VTVideoDecoderFactory() = default;
50+
};
51+
}
52+
53+
#endif
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright 2026 Alex Andres
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#ifndef JNI_WEBRTC_MEDIA_VIDEO_CODEC_VT_VP9_DECODER_H_
18+
#define JNI_WEBRTC_MEDIA_VIDEO_CODEC_VT_VP9_DECODER_H_
19+
20+
#include "api/video/encoded_image.h"
21+
#include "api/video_codecs/video_decoder.h"
22+
#include "modules/video_coding/utility/vp9_uncompressed_header_parser.h"
23+
24+
#include <CoreMedia/CoreMedia.h>
25+
#include <CoreVideo/CoreVideo.h>
26+
#include <VideoToolbox/VideoToolbox.h>
27+
28+
#include <cstdint>
29+
30+
namespace jni
31+
{
32+
// Decodes VP9 profile 0 on the media engine or GPU of a Mac, through the
33+
// VP9 decoder VideoToolbox offers once it is registered.
34+
//
35+
// The decompression session needs the properties of the stream, so it is
36+
// created on the first key frame, from the header the key frame carries,
37+
// and again whenever a key frame changes the size or the range. Decoding
38+
// is synchronous: each frame comes out of VideoToolbox before Decode
39+
// returns. A frame is handed on as the CVPixelBuffer VideoToolbox made,
40+
// without a copy.
41+
//
42+
// Whatever VideoToolbox cannot decode in place goes to the software
43+
// decoder, by returning WEBRTC_VIDEO_CODEC_FALLBACK_SOFTWARE: a profile
44+
// other than 0, a size outside what the hardware decodes, frames with
45+
// spatial layers, and a decoder that keeps failing.
46+
class VTVp9Decoder : public webrtc::VideoDecoder
47+
{
48+
public:
49+
VTVp9Decoder();
50+
~VTVp9Decoder() override;
51+
52+
using webrtc::VideoDecoder::Decode;
53+
54+
bool Configure(const Settings & settings) override;
55+
int32_t Decode(const webrtc::EncodedImage & image, int64_t renderTimeMs) override;
56+
int32_t RegisterDecodeCompleteCallback(webrtc::DecodedImageCallback * callback) override;
57+
int32_t Release() override;
58+
DecoderInfo GetDecoderInfo() const override;
59+
const char * ImplementationName() const override;
60+
61+
private:
62+
// What a session is created for. A key frame that differs from it
63+
// needs a new format description, and often a new session.
64+
struct StreamConfig
65+
{
66+
int width = 0;
67+
int height = 0;
68+
bool fullRange = false;
69+
webrtc::Vp9ColorSpace colorSpace = webrtc::Vp9ColorSpace::CS_UNKNOWN;
70+
71+
bool operator==(const StreamConfig & other) const;
72+
};
73+
74+
// The result of decoding one frame, filled in by the callback of
75+
// the session.
76+
struct Output
77+
{
78+
OSStatus status = noErr;
79+
CVImageBufferRef image = nullptr;
80+
};
81+
82+
static void OnOutput(void * decoder, void * frame, OSStatus status, VTDecodeInfoFlags flags,
83+
CVImageBufferRef image, CMTime presentationTime, CMTime duration);
84+
85+
// Reads the stream properties from a key frame header. Returns
86+
// false for a stream the hardware decoder does not take.
87+
static bool ReadConfig(const webrtc::Vp9UncompressedHeader & header, StreamConfig & config);
88+
89+
// Makes sure there is a session for the stream, creating or
90+
// replacing it where the stream changed.
91+
bool EnsureSession(const StreamConfig & config);
92+
bool CreateSession(const StreamConfig & config, CMVideoFormatDescriptionRef format);
93+
void DestroySession();
94+
95+
// Decodes one encoded frame as a single sample.
96+
OSStatus DecodeSample(const webrtc::EncodedImage & image, Output & output);
97+
98+
// Counts a failure and tells WebRTC what to do about it.
99+
int32_t Fail(OSStatus status);
100+
101+
void Deliver(const webrtc::EncodedImage & image, int64_t renderTimeMs, CVImageBufferRef pixels);
102+
103+
private:
104+
webrtc::DecodedImageCallback * callback;
105+
106+
VTDecompressionSessionRef session;
107+
CMVideoFormatDescriptionRef format;
108+
StreamConfig active;
109+
110+
// Set after a failure, until the next key frame.
111+
bool requireKeyFrame;
112+
int consecutiveErrors;
113+
int64_t sampleCount;
114+
};
115+
}
116+
117+
#endif

‎webrtc-jni/src/main/cpp/src/media/video/codec/DefaultVideoCodecFactories.cpp‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,13 @@ namespace jni
5454

5555
std::unique_ptr<webrtc::VideoEncoderFactory> CreateHardwareVideoEncoderFactory()
5656
{
57-
#ifdef __APPLE__
58-
return CreateDefaultVideoEncoderFactory();
59-
#else
6057
std::vector<std::unique_ptr<webrtc::VideoEncoderFactory>> hardware = CreatePlatformHardwareVideoEncoderFactories();
6158

6259
if (hardware.empty()) {
6360
return CreateDefaultVideoEncoderFactory();
6461
}
6562

6663
return std::make_unique<HardwareVideoEncoderFactory>(std::move(hardware), CreateDefaultVideoEncoderFactory());
67-
#endif
6864
}
6965

7066
std::unique_ptr<webrtc::VideoDecoderFactory> CreateDefaultVideoDecoderFactory()
@@ -82,16 +78,12 @@ namespace jni
8278

8379
std::unique_ptr<webrtc::VideoDecoderFactory> CreateHardwareVideoDecoderFactory()
8480
{
85-
#ifdef __APPLE__
86-
return CreateDefaultVideoDecoderFactory();
87-
#else
8881
std::vector<std::unique_ptr<webrtc::VideoDecoderFactory>> hardware = CreatePlatformHardwareVideoDecoderFactories();
8982

9083
if (hardware.empty()) {
9184
return CreateDefaultVideoDecoderFactory();
9285
}
9386

9487
return std::make_unique<HardwareVideoDecoderFactory>(std::move(hardware), CreateDefaultVideoDecoderFactory());
95-
#endif
9688
}
9789
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2026 Alex Andres
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "media/video/codec/HardwareVideoDecoderFactory.h"
18+
#include "media/video/codec/HardwareVideoEncoderFactory.h"
19+
#include "media/video/codec/macos/VTVideoDecoderFactory.h"
20+
21+
namespace jni
22+
{
23+
std::vector<std::unique_ptr<webrtc::VideoEncoderFactory>> CreatePlatformHardwareVideoEncoderFactories()
24+
{
25+
// H.264 is encoded through VideoToolbox by the default encoders, and
26+
// VideoToolbox has no encoder for the other codecs.
27+
return {};
28+
}
29+
30+
std::vector<std::unique_ptr<webrtc::VideoDecoderFactory>> CreatePlatformHardwareVideoDecoderFactories()
31+
{
32+
std::vector<std::unique_ptr<webrtc::VideoDecoderFactory>> factories;
33+
34+
// H.264 is decoded through VideoToolbox by the default decoders;
35+
// VP9 is the codec they decode in software.
36+
if (auto videoToolbox = VTVideoDecoderFactory::Create()) {
37+
factories.push_back(std::move(videoToolbox));
38+
}
39+
40+
return factories;
41+
}
42+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2026 Alex Andres
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "media/video/codec/macos/VTVideoDecoderFactory.h"
18+
#include "media/video/codec/macos/VTVp9Decoder.h"
19+
20+
#include "rtc_base/logging.h"
21+
22+
#include <VideoToolbox/VideoToolbox.h>
23+
24+
namespace jni
25+
{
26+
namespace
27+
{
28+
// Whether this Mac decodes VP9 in hardware. VideoToolbox has the VP9
29+
// decoder only after it is registered, so that comes first. The
30+
// answer does not change while the process runs, and is asked once.
31+
bool HasHardwareVp9Decoder()
32+
{
33+
static const bool available = [] {
34+
VTRegisterSupplementalVideoDecoderIfAvailable(kCMVideoCodecType_VP9);
35+
36+
const bool supported = VTIsHardwareDecodeSupported(kCMVideoCodecType_VP9);
37+
38+
RTC_LOG(LS_INFO) << "VideoToolbox hardware decoder for VP9: " << supported;
39+
40+
return supported;
41+
}();
42+
43+
return available;
44+
}
45+
}
46+
47+
std::unique_ptr<VTVideoDecoderFactory> VTVideoDecoderFactory::Create()
48+
{
49+
if (!HasHardwareVp9Decoder()) {
50+
return nullptr;
51+
}
52+
53+
return std::unique_ptr<VTVideoDecoderFactory>(new VTVideoDecoderFactory());
54+
}
55+
56+
std::vector<webrtc::SdpVideoFormat> VTVideoDecoderFactory::GetSupportedFormats() const
57+
{
58+
return { webrtc::SdpVideoFormat::VP9Profile0() };
59+
}
60+
61+
std::unique_ptr<webrtc::VideoDecoder> VTVideoDecoderFactory::Create(const webrtc::Environment & env,
62+
const webrtc::SdpVideoFormat & format)
63+
{
64+
return std::make_unique<VTVp9Decoder>();
65+
}
66+
}

0 commit comments

Comments
 (0)