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
23 changes: 23 additions & 0 deletions docs/guide/audio/headless-audio.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,29 @@ finally {
- If you only need the audio pipeline to be ready when remote audio arrives, you may delay playout initialization until after creating your RTCPeerConnection.
:::

### Who sent the received audio

A sink can also learn which RTP packets the audio was assembled from: their SSRC, contributing sources (CSRCs) and audio level. It implements the second `onData` method of `AudioTrackSink`, and is then called with it instead of the first; the packets are collected only for such a sink:

```java
AudioTrackSink sink = new AudioTrackSink() {
@Override
public void onData(byte[] data, int bitsPerSample, int sampleRate, int channels, int frames) {
// Not called for this sink.
}

@Override
public void onData(byte[] data, int bitsPerSample, int sampleRate, int channels, int frames,
RTCRtpPacketInfo[] packetInfos) {
for (RTCRtpPacketInfo info : packetInfos) {
System.out.println(info.ssrc + " level " + info.audioLevel);
}
}
};
```

A receiver learns its sources as playout pulls the audio too, so `RTCRtpReceiver.getSynchronizationSources()`, `getContributingSources()` and `RTCRtpReceiverObserver.onSourceChanged` stay empty for audio while playout is not running.


## Sending audio

Expand Down
4 changes: 2 additions & 2 deletions webrtc-jni/src/main/cpp/include/JNI_AudioTrack.h

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

20 changes: 18 additions & 2 deletions webrtc-jni/src/main/cpp/include/api/AudioTrackSink.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,48 @@
#include "JavaRef.h"

#include "api/media_stream_interface.h"
#include "api/rtp_packet_infos.h"

#include <jni.h>
#include <memory>
#include <optional>

namespace jni
{
class AudioTrackSink : public webrtc::AudioTrackSinkInterface
{
public:
AudioTrackSink(JNIEnv * env, const JavaGlobalRef<jobject> & sink);
// With packetInfos, the Java sink is called with the RTP packets
// the audio was assembled from.
AudioTrackSink(JNIEnv * env, const JavaGlobalRef<jobject> & sink, bool packetInfos);
~AudioTrackSink() = default;

// AudioTrackSinkInterface implementation.
// AudioTrackSinkInterface implementation. WebRTC calls the variant
// its source knows the most for.
void OnData(const void * data, int bitsPerSample, int sampleRate, size_t channels, size_t frames) override;
void OnData(const void * data, int bitsPerSample, int sampleRate, size_t channels, size_t frames,
std::optional<int64_t> absoluteCaptureTimestampMs) override;
void OnData(const void * data, int bitsPerSample, int sampleRate, size_t channels, size_t frames,
std::optional<int64_t> absoluteCaptureTimestampMs, const webrtc::RtpPacketInfos & packetInfos) override;

private:
void Deliver(const void * data, int bitsPerSample, int sampleRate, size_t channels, size_t frames,
const webrtc::RtpPacketInfos * infos);

class JavaAudioTrackSinkClass : public JavaClass
{
public:
explicit JavaAudioTrackSinkClass(JNIEnv * env);

jmethodID onData;
jmethodID onDataWithPacketInfos;
};

private:
JavaGlobalRef<jobject> sink;

const bool packetInfos;

const std::shared_ptr<JavaAudioTrackSinkClass> javaClass;
};
}
Expand Down
1 change: 1 addition & 0 deletions webrtc-jni/src/main/cpp/include/api/RTCOfferOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ namespace jni
jmethodID ctor;
jfieldID vad;
jfieldID iceRestart;
jfieldID restrictToStableOrHaveLocalOffer;
};

JavaLocalRef<jobject> toJava(JNIEnv * env, const webrtc::PeerConnectionInterface::RTCOfferAnswerOptions & nativeType);
Expand Down
46 changes: 46 additions & 0 deletions webrtc-jni/src/main/cpp/include/api/RTCRtpPacketInfo.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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_API_RTC_RTP_PACKET_INFO_H_
#define JNI_WEBRTC_API_RTC_RTP_PACKET_INFO_H_

#include "JavaClass.h"
#include "JavaRef.h"

#include "api/rtp_packet_info.h"
#include "api/rtp_packet_infos.h"

#include <jni.h>

namespace jni
{
namespace RTCRtpPacketInfo
{
class JavaRTCRtpPacketInfoClass : public JavaClass
{
public:
explicit JavaRTCRtpPacketInfoClass(JNIEnv * env);

jclass cls;
jmethodID ctor;
};

JavaLocalRef<jobject> toJava(JNIEnv * env, const webrtc::RtpPacketInfo & info);
JavaLocalRef<jobjectArray> toJavaArray(JNIEnv * env, const webrtc::RtpPacketInfos & infos);
}
}

#endif
2 changes: 2 additions & 0 deletions webrtc-jni/src/main/cpp/include/api/RTCRtpReceiverObserver.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ namespace jni

// RtpReceiverObserverInterface implementation.
void OnFirstPacketReceived(webrtc::MediaType media_type) override;
void OnSourceChanged(bool ssrc_changed, bool csrc_changed) override;

private:
class JavaRTCRtpReceiverObserverClass : public JavaClass
Expand All @@ -43,6 +44,7 @@ namespace jni
explicit JavaRTCRtpReceiverObserverClass(JNIEnv * env);

jmethodID onFirstPacketReceived;
jmethodID onSourceChanged;
};

private:
Expand Down
4 changes: 2 additions & 2 deletions webrtc-jni/src/main/cpp/src/JNI_AudioTrack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
#include "api/media_stream_interface.h"

JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_audio_AudioTrack_addSinkInternal
(JNIEnv * env, jobject caller, jobject jsink)
(JNIEnv * env, jobject caller, jobject jsink, jboolean packetInfos)
{
if (jsink == nullptr) {
env->Throw(jni::JavaNullPointerException(env, "AudioTrackSink must not be null"));
Expand All @@ -32,7 +32,7 @@ JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_audio_AudioTrack_addSinkInt
webrtc::AudioTrackInterface * track = GetHandle<webrtc::AudioTrackInterface>(env, caller);
CHECK_HANDLEV(track, 0);

auto sink = new jni::AudioTrackSink(env, jni::JavaGlobalRef<jobject>(env, jsink));
auto sink = new jni::AudioTrackSink(env, jni::JavaGlobalRef<jobject>(env, jsink), packetInfos);

track->AddSink(sink);

Expand Down
44 changes: 41 additions & 3 deletions webrtc-jni/src/main/cpp/src/api/AudioTrackSink.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,40 @@
*/

#include "api/AudioTrackSink.h"
#include "api/RTCRtpPacketInfo.h"
#include "api/WebRTCUtils.h"
#include "JavaClasses.h"
#include "JavaUtils.h"
#include "JNI_WebRTC.h"

namespace jni
{
AudioTrackSink::AudioTrackSink(JNIEnv * env, const JavaGlobalRef<jobject> & sink) :
AudioTrackSink::AudioTrackSink(JNIEnv * env, const JavaGlobalRef<jobject> & sink, bool packetInfos) :
sink(sink),
packetInfos(packetInfos),
javaClass(JavaClasses::get<JavaAudioTrackSinkClass>(env))
{
}

void AudioTrackSink::OnData(const void * data, int bitsPerSample, int sampleRate, size_t channels, size_t frames)
{
Deliver(data, bitsPerSample, sampleRate, channels, frames, nullptr);
}

void AudioTrackSink::OnData(const void * data, int bitsPerSample, int sampleRate, size_t channels, size_t frames,
std::optional<int64_t> absoluteCaptureTimestampMs)
{
Deliver(data, bitsPerSample, sampleRate, channels, frames, nullptr);
}

void AudioTrackSink::OnData(const void * data, int bitsPerSample, int sampleRate, size_t channels, size_t frames,
std::optional<int64_t> absoluteCaptureTimestampMs, const webrtc::RtpPacketInfos & packetInfos)
{
Deliver(data, bitsPerSample, sampleRate, channels, frames, &packetInfos);
}

void AudioTrackSink::Deliver(const void * data, int bitsPerSample, int sampleRate, size_t channels, size_t frames,
const webrtc::RtpPacketInfos * infos)
{
JNIEnv * env = AttachCurrentThread();

Expand All @@ -43,8 +63,25 @@ namespace jni

if (dataArray.get() != nullptr) {
env->SetByteArrayRegion(dataArray.get(), 0, dataSize, buffer);
env->CallVoidMethod(sink, javaClass->onData, dataArray.get(), static_cast<jint>(bitsPerSample),
static_cast<jint>(sampleRate), static_cast<jint>(channels), static_cast<jint>(frames));

if (packetInfos) {
try {
JavaLocalRef<jobjectArray> infoArray = (infos != nullptr)
? RTCRtpPacketInfo::toJavaArray(env, *infos)
: RTCRtpPacketInfo::toJavaArray(env, webrtc::RtpPacketInfos());

env->CallVoidMethod(sink, javaClass->onDataWithPacketInfos, dataArray.get(),
static_cast<jint>(bitsPerSample), static_cast<jint>(sampleRate),
static_cast<jint>(channels), static_cast<jint>(frames), infoArray.get());
}
catch (...) {
ThrowCxxJavaException(env);
}
}
else {
env->CallVoidMethod(sink, javaClass->onData, dataArray.get(), static_cast<jint>(bitsPerSample),
static_cast<jint>(sampleRate), static_cast<jint>(channels), static_cast<jint>(frames));
}
}

ReportPendingException(env);
Expand All @@ -55,5 +92,6 @@ namespace jni
jclass cls = FindClass(env, PKG_AUDIO"AudioTrackSink");

onData = GetMethod(env, cls, "onData", "([BIIII)V");
onDataWithPacketInfos = GetMethod(env, cls, "onData", "([BIIII[L" PKG "RTCRtpPacketInfo;)V");
}
}
5 changes: 5 additions & 0 deletions webrtc-jni/src/main/cpp/src/api/RTCOfferOptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ namespace jni

env->SetBooleanField(obj, javaClass->vad, nativeType.voice_activity_detection);
env->SetBooleanField(obj, javaClass->iceRestart, nativeType.ice_restart);
env->SetBooleanField(obj, javaClass->restrictToStableOrHaveLocalOffer,
nativeType.restrict_offer_to_stable_or_have_local_offer);

return JavaLocalRef<jobject>(env, obj);
}
Expand All @@ -47,6 +49,8 @@ namespace jni
webrtc::PeerConnectionInterface::RTCOfferAnswerOptions options;
options.voice_activity_detection = obj.getBoolean(javaClass->vad);
options.ice_restart = obj.getBoolean(javaClass->iceRestart);
options.restrict_offer_to_stable_or_have_local_offer =
obj.getBoolean(javaClass->restrictToStableOrHaveLocalOffer);

return options;
}
Expand All @@ -59,6 +63,7 @@ namespace jni

vad = GetFieldID(env, cls, "voiceActivityDetection", "Z");
iceRestart = GetFieldID(env, cls, "iceRestart", "Z");
restrictToStableOrHaveLocalOffer = GetFieldID(env, cls, "restrictToStableOrHaveLocalOffer", "Z");
}
}
}
82 changes: 82 additions & 0 deletions webrtc-jni/src/main/cpp/src/api/RTCRtpPacketInfo.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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 "api/RTCRtpPacketInfo.h"
#include "JavaClasses.h"
#include "JavaPrimitive.h"
#include "JavaUtils.h"
#include "JNI_WebRTC.h"

#include <vector>

namespace jni
{
namespace RTCRtpPacketInfo
{
JavaLocalRef<jobject> toJava(JNIEnv * env, const webrtc::RtpPacketInfo & info)
{
const auto javaClass = JavaClasses::get<JavaRTCRtpPacketInfoClass>(env);

const std::vector<uint32_t> & csrcs = info.csrcs();
std::vector<jlong> sources(csrcs.begin(), csrcs.end());

JavaLocalRef<jlongArray> jSources(env, env->NewLongArray(static_cast<jsize>(sources.size())));
ExceptionCheck(env);

env->SetLongArrayRegion(jSources.get(), 0, static_cast<jsize>(sources.size()), sources.data());

JavaLocalRef<jobject> jAudioLevel(env, nullptr);

if (info.audio_level().has_value()) {
jAudioLevel = Integer::create(env, static_cast<jint>(*info.audio_level()));
}

jlong receiveTimeMs = info.receive_time().IsFinite() ? info.receive_time().ms() : 0;

jobject object = env->NewObject(javaClass->cls, javaClass->ctor,
static_cast<jlong>(info.ssrc()), jSources.get(), static_cast<jlong>(info.rtp_timestamp()),
receiveTimeMs, jAudioLevel.get());
ExceptionCheck(env);

return JavaLocalRef<jobject>(env, object);
}

JavaLocalRef<jobjectArray> toJavaArray(JNIEnv * env, const webrtc::RtpPacketInfos & infos)
{
const auto javaClass = JavaClasses::get<JavaRTCRtpPacketInfoClass>(env);

JavaLocalRef<jobjectArray> array(env, env->NewObjectArray(static_cast<jsize>(infos.size()), javaClass->cls, nullptr));
ExceptionCheck(env);

jsize index = 0;

for (const webrtc::RtpPacketInfo & info : infos) {
JavaLocalRef<jobject> jInfo = toJava(env, info);

env->SetObjectArrayElement(array.get(), index++, jInfo.get());
}

return array;
}

JavaRTCRtpPacketInfoClass::JavaRTCRtpPacketInfoClass(JNIEnv * env)
{
cls = FindClass(env, PKG"RTCRtpPacketInfo");

ctor = GetMethod(env, cls, "<init>", "(J[JJJLjava/lang/Integer;)V");
}
}
}
15 changes: 15 additions & 0 deletions webrtc-jni/src/main/cpp/src/api/RTCRtpReceiverObserver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,25 @@ namespace jni
ReportPendingException(env);
}

void RTCRtpReceiverObserver::OnSourceChanged(bool ssrc_changed, bool csrc_changed)
{
JNIEnv * env = AttachCurrentThread();

if (env == nullptr) {
return;
}

env->CallVoidMethod(observer, javaClass->onSourceChanged,
static_cast<jboolean>(ssrc_changed), static_cast<jboolean>(csrc_changed));

ReportPendingException(env);
}

RTCRtpReceiverObserver::JavaRTCRtpReceiverObserverClass::JavaRTCRtpReceiverObserverClass(JNIEnv * env)
{
jclass cls = FindClass(env, PKG"RTCRtpReceiverObserver");

onFirstPacketReceived = GetMethod(env, cls, "onFirstPacketReceived", "(L" PKG_MEDIA "MediaType;)V");
onSourceChanged = GetMethod(env, cls, "onSourceChanged", "(ZZ)V");
}
}
Loading
Loading