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
31 changes: 31 additions & 0 deletions docs/guide/advanced/encoded-transforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This guide explains how to read and change the encoded frames of a call as they
- Asking for key frames
- End-to-end encryption as a worked example
- Threading and performance
- Sending frames that are already encoded through an injector

A sender's transform sees every frame after the encoder and before the packetizer; a receiver's sees every frame after the depacketizer and before the decoder. Everything between the two, whether that is the network, a TURN server or an SFU, only ever carries what the transform produced. That is what makes end-to-end encryption possible, and also frame metadata, watermarks, or analysis of the encoded stream.

Expand Down Expand Up @@ -104,3 +105,33 @@ The transform never runs on a thread that carries media. Each sender and receive
- a transform that falls several seconds behind has frames dropped until it catches up, rather than having them queue up in memory.

Per frame, the cost is one small Java object and, only if the payload is read, one copy into a buffer that is reused from frame to frame. Senders and receivers without a transform are not touched at all.

## Injecting Encoded Frames

A sender can also send frames that are already encoded, such as a camera's H.264 stream or a recording, without WebRTC encoding anything. An injector takes the place of the sender's track, so it is created for a sender without one, for example from a transceiver added by media type:

```java
RTCRtpTransceiverInit init = new RTCRtpTransceiverInit();
init.direction = RTCRtpTransceiverDirection.SEND_ONLY;

RTCRtpTransceiver transceiver = peerConnection.addTransceiver(MediaType.VIDEO, init);
RTCEncodedVideoFrameInjector injector = transceiver.getSender()
.createEncodedVideoFrameInjector(new RTCEncodedVideoFrameInjectorObserver() {
@Override
public void onKeyFrameRequest() {
// Inject a key frame next.
}

@Override
public void onBitrateChange(int allocatedBitrate, int availableOutgoingBitrate) {
// Adapt the source's bitrate.
}
});

// After negotiation, for every frame:
injector.injectFrame(frameData, rtpTimestamp, keyFrame, width, height);
```

The frames must be in the codec the sender negotiated, which `setCodecPreferences` on the transceiver can make sure of, and the RTP timestamp runs on the 90 kHz video clock. An audio sender takes an `RTCEncodedAudioFrameInjector` the same way, whose frames carry the payload type of their codec, from the sender's `getParameters().codecs`. Do not inject audio through a factory that sends audio captured by its audio device module, as WebRTC feeds captured audio to every audio sender.

Giving the sender a track through `replaceTrack` ends the injection, and `dispose()` releases the injector. The observer runs on a callback thread shared with other WebRTC observers, so it may call back into WebRTC but should not block.
2 changes: 1 addition & 1 deletion docs/guide/data/data-channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ dataChannel.sendAsync(binaryChannelBuffer, new RTCDataChannelSendObserver() {
});
```

The observer is called exactly once, with a failure if the operation is discarded while the channel shuts down. Success means the message was queued locally, not that the peer received it. Callbacks run on a native callback thread shared with other send and DTLS transport observers; they may call other WebRTC methods, but should not block, so dispatch long work to an executor of your own. Without an observer, `sendAsync` logs failures and reports nothing to the caller.
The observer is called exactly once, with a failure if the operation is discarded while the channel shuts down. Success means the message was queued locally, not that the peer received it. Callbacks run on a native callback thread shared with other WebRTC observers; they may call other WebRTC methods, but should not block, so dispatch long work to an executor of your own. Without an observer, `sendAsync` logs failures and reports nothing to the caller.

### Receiving Data

Expand Down
106 changes: 62 additions & 44 deletions webrtc-jni/src/main/cpp/dependencies/webrtc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -171,21 +171,37 @@ elseif(LINUX)
endif()
endif()

# Targets outside the default build graph whose objects the JNI code needs,
# built and linked explicitly: the api/video utility classes
# (AdaptedVideoTrackSource, VideoAdapter, VideoBroadcaster), which fell out of
# it when they moved out of media/base, and the frame factory that encoded
# frame injectors are fed through. On Linux and Windows the shared library link
# would leave their symbols undefined and loading would fail at runtime. The
# Apple archive is assembled from an object glob and only needs them built.
set(WEBRTC_EXTRA_TARGETS
api/video:adapted_video_track_source
api/video:video_adapter
api/video:video_broadcaster
api:frame_transformer_factory
)

# Written next to a compiled WebRTC, in the build tree and in the install
# directory, naming the GN args it was built with. A build without it, or with
# other args, is not used and WebRTC is built again: it would lack what the
# args enable (H.265 for one), or have defines, and with them struct layouts,
# that differ from those the JNI code is compiled with below. Changing an arg
# here thus rebuilds WebRTC on its own; CI's cache key covers this file.
# directory, naming the GN args and the extra targets it was built with. A
# build without it, or with other args or targets, is not used and WebRTC is
# built again: it would lack what the args enable (H.265 for one) or a target's
# library, or have defines, and with them struct layouts, that differ from those
# the JNI code is compiled with below. Changing an arg or a target here thus
# rebuilds WebRTC on its own; CI's cache key covers this file.
set(WEBRTC_ARGS_FILE "webrtc-java-gn-args.txt")
set(WEBRTC_BUILD_RECORD "${COMPILE_ARGS} extra_targets=\"${WEBRTC_EXTRA_TARGETS}\"")

function(webrtc_args_match DIRECTORY RESULT)
set(MATCHES FALSE)

if(EXISTS "${DIRECTORY}/${WEBRTC_ARGS_FILE}")
file(READ "${DIRECTORY}/${WEBRTC_ARGS_FILE}" RECORDED_ARGS)

if(RECORDED_ARGS STREQUAL COMPILE_ARGS)
if(RECORDED_ARGS STREQUAL WEBRTC_BUILD_RECORD)
set(MATCHES TRUE)
endif()
endif()
Expand All @@ -199,23 +215,22 @@ webrtc_args_match("${WEBRTC_SRC}/${WEBRTC_BUILD}" WEBRTC_BUILT_ARGS_MATCH)
if(EXISTS "${WEBRTC_LIB_PATH_INSTALLED}" AND NOT WEBRTC_INSTALLED_ARGS_MATCH)
# Out of the way, so that nothing links it before the new build replaces
# it on install.
message(STATUS "WebRTC: the installed build has other GN args, building again")
message(STATUS "WebRTC: the installed build has other GN args or targets, building again")
file(REMOVE "${WEBRTC_LIB_PATH_INSTALLED}")
endif()

# The libraries of the extra targets, re-archived flat under obj/ by the build
# below.
set(EXTRA_LIB_NAMES "")
foreach(EXTRA_TARGET ${WEBRTC_EXTRA_TARGETS})
string(REGEX REPLACE "^.*:" "" EXTRA_TARGET_NAME "${EXTRA_TARGET}")

# The api/video utility classes (AdaptedVideoTrackSource, VideoAdapter,
# VideoBroadcaster) fell out of the default build graph when they moved out
# of media/base, so their objects no longer reach the monolithic archive.
# On Linux and Windows the shared library link leaves their symbols
# undefined and loading fails at runtime; they are built and linked
# explicitly. The Apple archive is assembled from an object glob and only
# needs them built.
if(WIN32)
set(VIDEO_UTIL_LIB_NAMES adapted_video_track_source.lib video_adapter.lib video_broadcaster.lib)
else()
set(VIDEO_UTIL_LIB_NAMES libadapted_video_track_source.a libvideo_adapter.a libvideo_broadcaster.a)
endif()
if(WIN32)
list(APPEND EXTRA_LIB_NAMES "${EXTRA_TARGET_NAME}.lib")
else()
list(APPEND EXTRA_LIB_NAMES "lib${EXTRA_TARGET_NAME}.a")
endif()
endforeach()

if(EXISTS "${WEBRTC_LIB_PATH_INSTALLED}")
set(TARGET_INC_DIR ${WEBRTC_INSTALL_DIR}/include)
Expand All @@ -224,7 +239,7 @@ if(EXISTS "${WEBRTC_LIB_PATH_INSTALLED}")
set(TARGET_LIBCPP_BUILDTOOLS_INC_DIR ${TARGET_INC_DIR}/third_party/libc++/)
set(TARGET_LIBCPP_INC_DIR ${TARGET_INC_DIR}/third_party/libc++/include)
set(TARGET_LIBCPP_ABI_INC_DIR ${TARGET_INC_DIR}/third_party/libc++abi/include)
set(VIDEO_UTIL_LIB_DIR ${WEBRTC_INSTALL_DIR}/lib)
set(EXTRA_LIB_DIR ${WEBRTC_INSTALL_DIR}/lib)
else()
set(TARGET_INC_DIR ${WEBRTC_SRC})
set(TARGET_LINK_LIB ${WEBRTC_LIB_PATH})
Expand All @@ -234,13 +249,13 @@ else()
set(TARGET_LIBCPP_ABI_INC_DIR ${WEBRTC_SRC}/third_party/libc++abi/src/include)
# The flat re-archives; gn's originals are thin archives that break
# when copied away from their object files.
set(VIDEO_UTIL_LIB_DIR ${WEBRTC_SRC}/${WEBRTC_BUILD}/obj)
set(EXTRA_LIB_DIR ${WEBRTC_SRC}/${WEBRTC_BUILD}/obj)
endif()

set(VIDEO_UTIL_LINK_LIBS "")
set(EXTRA_LINK_LIBS "")
if(NOT APPLE)
foreach(VIDEO_UTIL_LIB ${VIDEO_UTIL_LIB_NAMES})
list(APPEND VIDEO_UTIL_LINK_LIBS "${VIDEO_UTIL_LIB_DIR}/${VIDEO_UTIL_LIB}")
foreach(EXTRA_LIB ${EXTRA_LIB_NAMES})
list(APPEND EXTRA_LINK_LIBS "${EXTRA_LIB_DIR}/${EXTRA_LIB}")
endforeach()
endif()

Expand Down Expand Up @@ -268,7 +283,7 @@ target_include_directories(${PROJECT_NAME}
${TARGET_INC_DIR}/third_party/abseil-cpp
${TARGET_INC_DIR}/third_party/libyuv/include
)
target_link_libraries(${PROJECT_NAME} ${TARGET_LINK_LIB} ${VIDEO_UTIL_LINK_LIBS})
target_link_libraries(${PROJECT_NAME} ${TARGET_LINK_LIB} ${EXTRA_LINK_LIBS})

# rtc_use_h265=true makes gn define RTC_ENABLE_H265 for WebRTC; the JNI code
# needs it as well, for the H.265 parts of WebRTC's headers it uses (the H.265
Expand Down Expand Up @@ -460,7 +475,7 @@ message(STATUS "WebRTC: compile")
# output directory that Siso built.
if(APPLE)
execute_command(
COMMAND autoninja -C "${WEBRTC_BUILD}" :default api/audio_codecs:builtin_audio_decoder_factory api/task_queue:default_task_queue_factory sdk:native_api sdk:default_codec_factory_objc pc:peer_connection sdk:videocapture_objc api/video:adapted_video_track_source api/video:video_adapter api/video:video_broadcaster
COMMAND autoninja -C "${WEBRTC_BUILD}" :default api/audio_codecs:builtin_audio_decoder_factory api/task_queue:default_task_queue_factory sdk:native_api sdk:default_codec_factory_objc pc:peer_connection sdk:videocapture_objc ${WEBRTC_EXTRA_TARGETS}
WORKING_DIRECTORY "${WEBRTC_SRC}"
RESULT_VARIABLE NINJA_RESULT
)
Expand All @@ -478,11 +493,10 @@ else()
RESULT_VARIABLE NINJA_RESULT
)

# The api/video utility targets are not reachable from the default
# graph since their move out of media/base; build them explicitly so
# their symbols exist for the shared library link.
# The extra targets are not reachable from the default graph; build them
# explicitly so their symbols exist for the shared library link.
execute_command(
COMMAND autoninja -C "${WEBRTC_BUILD}" api/video:adapted_video_track_source api/video:video_adapter api/video:video_broadcaster
COMMAND autoninja -C "${WEBRTC_BUILD}" ${WEBRTC_EXTRA_TARGETS}
WORKING_DIRECTORY "${WEBRTC_SRC}"
RESULT_VARIABLE NINJA_UTIL_RESULT
)
Expand All @@ -494,12 +508,14 @@ if(WIN32)
# without the object tree; a cold build never notices because the
# objects still sit beside the archives. Re-archive them in regular
# form with the toolchain librarian, mirroring the Linux path below.
foreach(VIDEO_UTIL_TARGET adapted_video_track_source video_adapter video_broadcaster)
file(GLOB_RECURSE VIDEO_UTIL_OBJS
${WEBRTC_SRC}/${WEBRTC_BUILD}/obj/api/video/${VIDEO_UTIL_TARGET}/*.obj
foreach(EXTRA_TARGET ${WEBRTC_EXTRA_TARGETS})
string(REPLACE ":" "/" EXTRA_TARGET_DIR "${EXTRA_TARGET}")
string(REGEX REPLACE "^.*:" "" EXTRA_TARGET_NAME "${EXTRA_TARGET}")
file(GLOB_RECURSE EXTRA_TARGET_OBJS
${WEBRTC_SRC}/${WEBRTC_BUILD}/obj/${EXTRA_TARGET_DIR}/*.obj
)
execute_command(
COMMAND ${WEBRTC_SRC}/third_party/llvm-build/Release+Asserts/bin/lld-link.exe /lib /OUT:${WEBRTC_SRC}/${WEBRTC_BUILD}/obj/${VIDEO_UTIL_TARGET}.lib ${VIDEO_UTIL_OBJS}
COMMAND ${WEBRTC_SRC}/third_party/llvm-build/Release+Asserts/bin/lld-link.exe /lib /OUT:${WEBRTC_SRC}/${WEBRTC_BUILD}/obj/${EXTRA_TARGET_NAME}.lib ${EXTRA_TARGET_OBJS}
)
endforeach()
endif()
Expand All @@ -516,13 +532,15 @@ if(LINUX)

# gn emits thin archives whose members reference objects by relative
# path, so they break once copied to the install tree. Re-archive the
# api/video utility objects into regular archives, like libc++ below.
foreach(VIDEO_UTIL_TARGET adapted_video_track_source video_adapter video_broadcaster)
file(GLOB_RECURSE VIDEO_UTIL_OBJS
${WEBRTC_SRC}/${WEBRTC_BUILD}/obj/api/video/${VIDEO_UTIL_TARGET}/*.o
# extra targets' objects into regular archives, like libc++ below.
foreach(EXTRA_TARGET ${WEBRTC_EXTRA_TARGETS})
string(REPLACE ":" "/" EXTRA_TARGET_DIR "${EXTRA_TARGET}")
string(REGEX REPLACE "^.*:" "" EXTRA_TARGET_NAME "${EXTRA_TARGET}")
file(GLOB_RECURSE EXTRA_TARGET_OBJS
${WEBRTC_SRC}/${WEBRTC_BUILD}/obj/${EXTRA_TARGET_DIR}/*.o
)
execute_command(
COMMAND ${CMAKE_AR} rcs ${WEBRTC_SRC}/${WEBRTC_BUILD}/obj/lib${VIDEO_UTIL_TARGET}.a ${VIDEO_UTIL_OBJS}
COMMAND ${CMAKE_AR} rcs ${WEBRTC_SRC}/${WEBRTC_BUILD}/obj/lib${EXTRA_TARGET_NAME}.a ${EXTRA_TARGET_OBJS}
)
endforeach()

Expand Down Expand Up @@ -564,16 +582,16 @@ if(LINUX)
endif()

if(LINUX)
foreach(VIDEO_UTIL_LIB ${VIDEO_UTIL_LIB_NAMES})
foreach(EXTRA_LIB ${EXTRA_LIB_NAMES})
install(
FILES "${WEBRTC_SRC}/${WEBRTC_BUILD}/obj/${VIDEO_UTIL_LIB}"
FILES "${WEBRTC_SRC}/${WEBRTC_BUILD}/obj/${EXTRA_LIB}"
DESTINATION "${WEBRTC_INSTALL_DIR}/lib"
)
endforeach()
elseif(WIN32)
foreach(VIDEO_UTIL_LIB ${VIDEO_UTIL_LIB_NAMES})
foreach(EXTRA_LIB ${EXTRA_LIB_NAMES})
install(
FILES "${WEBRTC_SRC}/${WEBRTC_BUILD}/obj/${VIDEO_UTIL_LIB}"
FILES "${WEBRTC_SRC}/${WEBRTC_BUILD}/obj/${EXTRA_LIB}"
DESTINATION "${WEBRTC_INSTALL_DIR}/lib"
)
endforeach()
Expand All @@ -582,7 +600,7 @@ endif()
# The record of the GN args, once the build has succeeded, in the build tree
# and, with the library, in the install directory.
if(GN_RESULT EQUAL 0 AND NINJA_RESULT EQUAL 0 AND NINJA_UTIL_RESULT EQUAL 0 AND EXISTS "${WEBRTC_LIB_PATH}")
file(WRITE "${WEBRTC_SRC}/${WEBRTC_BUILD}/${WEBRTC_ARGS_FILE}" "${COMPILE_ARGS}")
file(WRITE "${WEBRTC_SRC}/${WEBRTC_BUILD}/${WEBRTC_ARGS_FILE}" "${WEBRTC_BUILD_RECORD}")

install(FILES "${WEBRTC_SRC}/${WEBRTC_BUILD}/${WEBRTC_ARGS_FILE}" DESTINATION "${WEBRTC_INSTALL_DIR}")
else()
Expand Down
36 changes: 36 additions & 0 deletions webrtc-jni/src/main/cpp/include/JNI_RTCEncodedAudioFrameInjector.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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 <jni.h>

#ifndef _Included_dev_onvoid_webrtc_RTCEncodedAudioFrameInjector
#define _Included_dev_onvoid_webrtc_RTCEncodedAudioFrameInjector
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCEncodedAudioFrameInjector_injectDirectBuffer
(JNIEnv *, jobject, jobject, jint, jint, jint, jlong, jint, jboolean, jlong, jboolean);

JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCEncodedAudioFrameInjector_injectByteArray
(JNIEnv *, jobject, jbyteArray, jint, jlong, jint, jboolean, jlong, jboolean);

JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCEncodedAudioFrameInjector_dispose
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif
36 changes: 36 additions & 0 deletions webrtc-jni/src/main/cpp/include/JNI_RTCEncodedVideoFrameInjector.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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 <jni.h>

#ifndef _Included_dev_onvoid_webrtc_RTCEncodedVideoFrameInjector
#define _Included_dev_onvoid_webrtc_RTCEncodedVideoFrameInjector
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCEncodedVideoFrameInjector_injectDirectBuffer
(JNIEnv *, jobject, jobject, jint, jint, jlong, jboolean, jint, jint, jlong, jboolean);

JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCEncodedVideoFrameInjector_injectByteArray
(JNIEnv *, jobject, jbyteArray, jlong, jboolean, jint, jint, jlong, jboolean);

JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCEncodedVideoFrameInjector_dispose
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif
8 changes: 8 additions & 0 deletions webrtc-jni/src/main/cpp/include/JNI_RTCPeerConnection.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions webrtc-jni/src/main/cpp/include/JNI_RTCRtpSender.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading