Skip to content

feat: add custom video codec factories - #303

Merged
devopvoid merged 2 commits into
mainfrom
feat/custom-video-codec-factories
Oct 1, 2026
Merged

devopvoid merged 2 commits into
mainfrom
feat/custom-video-codec-factories

Conversation

@devopvoid

Copy link
Copy Markdown
Owner

A PeerConnectionFactory can now be given its own video encoder and decoder factories, which decide what video codecs it sends and receives. Until now the codecs were hard-coded in JNI_PeerConnectionFactory.cpp.

PeerConnectionFactory factory = PeerConnectionFactory.builder()
        .setAudioDeviceModule(adm)
        .setVideoEncoderFactory(new MyEncoderFactory())
        .setVideoDecoderFactory(new MyDecoderFactory())
        .build();

API

  • PeerConnectionFactory.builder() sets the codec factories along with everything the constructors take (field trials, audio device module, audio processing). The constructors are unchanged.
  • New package dev.onvoid.webrtc.media.video.codec, modelled on the Android SDK:
    • VideoEncoder and VideoDecoder, for codecs implemented in Java
    • VideoEncoderFactory and VideoDecoderFactory
    • EncodedImage, VideoCodecInfo and VideoCodecStatus
    • DefaultVideoEncoderFactory and DefaultVideoDecoderFactory, which provide the built-in codecs. A factory can add its own codec and delegate the rest to them. The NativeVideoEncoder and NativeVideoDecoder they hand out make WebRTC create the built-in codec natively, with no detour through Java.
  • A codec name WebRTC does not know (e.g. X-MYCODEC) is negotiated and sent with the generic RTP packetization.

Native side

  • Wrappers ported from the Android SDK run the Java codecs.
  • Frames reach a Java encoder without a copy. Encoded images reach a Java decoder as a read-only view.
  • The callback a codec hands its output to reaches the wrapper only until the codec is released, so late output cannot touch a destroyed wrapper.
  • Whatever a Java codec or factory throws is logged and treated as an error, and never propagates into WebRTC.
  • Decoded frames may use any I420Buffer with direct buffers, not only NativeI420Buffer.
  • The built-in factory setup moved to media/video/codec/DefaultVideoCodecFactories.cpp, with the macOS VideoToolbox path unchanged.

Log sink fix (separate commit)

Found while testing: the Java log sink bridge let a C++ exception escape into WebRTC. This happened when a sink threw, or when a message was logged while a Java exception was pending. WebRTC calls sinks under its global logging lock and is built without unwinding, so the lock stayed held and the next thread to log blocked forever, typically in RTCPeerConnection.close(). Because a log sink cannot be removed, a sink installed by one test class made this appear in later ones. The bridge now sets a pending exception aside, restores it afterwards, and never throws. VideoCodecFactoryTests.failingEncoderFactoryDoesNotCrash hangs without the fix.

Testing

  • VideoCodecFactoryTests (10 tests):
    • a full call through a Java encoder and decoder, including a decoder that returns a plain Java buffer
    • delegation to the built-in codecs
    • factories that throw
    • codec advertisement
  • mvn -pl webrtc test: 194 tests pass.
  • mvn -pl webrtc test -Pjni-check: 194 tests pass, with no FATAL ERROR in native method.
  • Built and tested on Windows only. macOS and Linux will first be checked by CI.

Docs

New guide docs/guide/advanced/video-codecs.md, linked from the sidebar, the guide index, the README and the landing page. The field trials guide mentions the builder.

Not included

  • Hardware encoders for Windows and Linux (NVENC, Media Foundation). They can now plug in through this API, but none ship yet.
  • Android's getEncoderSelector().

WebRTC calls log sinks while it holds its global logging lock, and it is
built without unwinding. The Java log sink bridge ended with
ExceptionCheck(), which throws a C++ exception when the sink throws, and
also when the code that logged had a Java exception pending, since no JNI
call may be made then. That exception passed through WebRTC's frames
without releasing the lock, and the next thread to log blocked forever,
typically hanging RTCPeerConnection.close().

The bridge now sets a pending exception aside and restores it afterwards,
reports what the sink throws, and never throws itself.
A PeerConnectionFactory can now be given its own video encoder and
decoder factories, which decide what video codecs it sends and receives.
PeerConnectionFactory.builder() sets them, along with everything the
constructors take.

The new package dev.onvoid.webrtc.media.video.codec has VideoEncoder and
VideoDecoder, for codecs implemented in Java, their factories, and
EncodedImage and VideoCodecInfo. DefaultVideoEncoderFactory and
DefaultVideoDecoderFactory provide the built-in codecs, so a factory can
add its own codec while keeping them; the NativeVideoEncoder and
NativeVideoDecoder they hand out make WebRTC create the built-in codec
natively. A codec name WebRTC does not know is negotiated and sent with
the generic packetization.

Native wrappers, ported from the Android SDK, run the Java codecs. Frames
reach a Java encoder without a copy, encoded images a Java decoder as a
read-only view; the callbacks a codec hands its output to reach the
wrapper only until the codec is released. Whatever a Java codec or
factory throws is logged and treated as an error, and never reaches
WebRTC. Decoded frames may use any I420Buffer with direct buffers.
@devopvoid
devopvoid added this pull request to stack #311 September 30, 2026 00:20
@devopvoid
devopvoid merged commit 7023c51 into main Oct 1, 2026
17 checks passed
@devopvoid
devopvoid deleted the feat/custom-video-codec-factories branch October 1, 2026 22:34
SendableMetatype added a commit to EduGeyser/webrtc-java that referenced this pull request Oct 10, 2026
Brings in upstream 0.19.0 and the work after it: the FFmpeg-based
webrtc-java-media module with its native extension API (devopvoid#291, devopvoid#292,
devopvoid#299, devopvoid#300), encoded frame transforms (devopvoid#300), the remaining sender
encoding parameters and RTCConfiguration fields (devopvoid#316, devopvoid#317), custom
and hardware video codec factories (devopvoid#303, devopvoid#310 to devopvoid#315, devopvoid#318 to devopvoid#330),
the log sink and 32-bit ARM stats fixes, and upstream's CI rework
(devopvoid#294 to devopvoid#309).

Fork-side adaptations:
- The media module builds with Gradle, and only in the full variant:
  it needs audio and video. On Linux it links like the JNI library,
  since the Chromium sysroots no longer ship the static libgcc_eh.a.
  The sysroots carry stubs of glibc that no executable links against,
  so FFmpeg's configure links with lld and allows undefined symbols in
  shared libraries.
- The data channels variant leaves out the new media sources, the codec
  factory setup and the hardware codec headers, and skips the new tests
  that need audio or video.
- The module tests run on the module path with the test resources
  patched in, and the core tests open the internal package that
  NativeApi and NativeLoader are tested in.
- The WebRTC checkout keeps this fork's sync, with upstream's
  checkout_instrumented_libraries setting.
- CI builds all platforms from one reusable workflow, as upstream now
  does. The full variant also builds FFmpeg and tests the media module.
  The WebRTC and FFmpeg caches are saved as soon as their builds finish,
  and a release is prepared locally and pushed in one atomic push.
- The Maven poms and upstream's Dependabot setup stay out, and the
  README is the fork's own; the rename script now leaves it alone.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant