diff --git a/docs/guide/get-started.md b/docs/guide/get-started.md index b69172e2f..b7ef0cc8b 100644 --- a/docs/guide/get-started.md +++ b/docs/guide/get-started.md @@ -331,6 +331,28 @@ RTCRtpSender videoSender = peerConnection.addTrack(videoTrack, streamIds); RTCRtpSender audioSender = peerConnection.addTrack(audioTrack, streamIds); ``` +### Receiving Tracks + +A remote track arrives in `onTrack` with the transceiver that carries it. To learn when its media actually starts flowing, for example to show a video only once there is something to show, set an `RTCRtpReceiverObserver`; an `RTCRtpSenderObserver` does the same for the first packet a sender sends. Either is called right away when that has already happened: + +```java +@Override +public void onTrack(RTCRtpTransceiver transceiver) { + RTCRtpReceiver receiver = transceiver.getReceiver(); + + receiver.setObserver(mediaType -> System.out.println("First " + mediaType + " packet received")); + + MediaStreamTrack track = receiver.getTrack(); + + if (track instanceof AudioTrack) { + // Playout volume of this remote track, 0 to 10; 1 plays it as received. + ((AudioTrack) track).setVolume(0.5); + } +} +``` + +An observer belongs to the `RTCRtpReceiver` or `RTCRtpSender` object it was set through, and is removed when that object is disposed. `getId()` identifies a sender or receiver, and `RTCRtpSender.getStreams()` returns the stream IDs its track was added with. + ## Cleanup Always properly dispose of resources when you're done: diff --git a/webrtc-jni/src/main/cpp/include/JNI_AudioTrack.h b/webrtc-jni/src/main/cpp/include/JNI_AudioTrack.h index 8261a1fe5..6ef6bae46 100644 --- a/webrtc-jni/src/main/cpp/include/JNI_AudioTrack.h +++ b/webrtc-jni/src/main/cpp/include/JNI_AudioTrack.h @@ -7,6 +7,14 @@ #ifdef __cplusplus extern "C" { #endif + /* + * Class: dev_onvoid_webrtc_media_audio_AudioTrack + * Method: setVolumeInternal + * Signature: (D)V + */ + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_audio_AudioTrack_setVolumeInternal + (JNIEnv *, jobject, jdouble); + /* * Class: dev_onvoid_webrtc_media_audio_AudioTrack * Method: getSignalLevel diff --git a/webrtc-jni/src/main/cpp/include/JNI_RTCRtpReceiver.h b/webrtc-jni/src/main/cpp/include/JNI_RTCRtpReceiver.h index 8a9c8b2ef..633d9954a 100644 --- a/webrtc-jni/src/main/cpp/include/JNI_RTCRtpReceiver.h +++ b/webrtc-jni/src/main/cpp/include/JNI_RTCRtpReceiver.h @@ -63,6 +63,22 @@ extern "C" { JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCRtpReceiver_requestKeyFrame (JNIEnv *, jobject); + /* + * Class: dev_onvoid_webrtc_RTCRtpReceiver + * Method: getId + * Signature: ()Ljava/lang/String; + */ + JNIEXPORT jstring JNICALL Java_dev_onvoid_webrtc_RTCRtpReceiver_getId + (JNIEnv *, jobject); + + /* + * Class: dev_onvoid_webrtc_RTCRtpReceiver + * Method: setObserver + * Signature: (Ldev/onvoid/webrtc/RTCRtpReceiverObserver;)V + */ + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCRtpReceiver_setObserver + (JNIEnv *, jobject, jobject); + /* * Class: dev_onvoid_webrtc_RTCRtpReceiver * Method: dispose diff --git a/webrtc-jni/src/main/cpp/include/JNI_RTCRtpSender.h b/webrtc-jni/src/main/cpp/include/JNI_RTCRtpSender.h index eced0d4df..26a0437e6 100644 --- a/webrtc-jni/src/main/cpp/include/JNI_RTCRtpSender.h +++ b/webrtc-jni/src/main/cpp/include/JNI_RTCRtpSender.h @@ -79,6 +79,30 @@ extern "C" { JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCRtpSender_generateKeyFrame (JNIEnv *, jobject); + /* + * Class: dev_onvoid_webrtc_RTCRtpSender + * Method: getId + * Signature: ()Ljava/lang/String; + */ + JNIEXPORT jstring JNICALL Java_dev_onvoid_webrtc_RTCRtpSender_getId + (JNIEnv *, jobject); + + /* + * Class: dev_onvoid_webrtc_RTCRtpSender + * Method: setObserver + * Signature: (Ldev/onvoid/webrtc/RTCRtpSenderObserver;)V + */ + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCRtpSender_setObserver + (JNIEnv *, jobject, jobject); + + /* + * Class: dev_onvoid_webrtc_RTCRtpSender + * Method: getStreams + * Signature: ()Ljava/util/List; + */ + JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCRtpSender_getStreams + (JNIEnv *, jobject); + /* * Class: dev_onvoid_webrtc_RTCRtpSender * Method: dispose diff --git a/webrtc-jni/src/main/cpp/include/api/PeerConnectionObserver.h b/webrtc-jni/src/main/cpp/include/api/PeerConnectionObserver.h index 82ca0833e..a4d05127f 100644 --- a/webrtc-jni/src/main/cpp/include/api/PeerConnectionObserver.h +++ b/webrtc-jni/src/main/cpp/include/api/PeerConnectionObserver.h @@ -72,10 +72,6 @@ namespace jni }; private: - // Reports a Java exception thrown by a callback without letting it - // unwind through WebRTC. - void ReportException(JNIEnv * env) noexcept; - JavaGlobalRef observer; const std::shared_ptr javaClass; diff --git a/webrtc-jni/src/main/cpp/include/api/RTCRtpReceiverObserver.h b/webrtc-jni/src/main/cpp/include/api/RTCRtpReceiverObserver.h new file mode 100644 index 000000000..fb1dfe655 --- /dev/null +++ b/webrtc-jni/src/main/cpp/include/api/RTCRtpReceiverObserver.h @@ -0,0 +1,55 @@ +/* + * 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_RECEIVER_OBSERVER_H_ +#define JNI_WEBRTC_API_RTC_RTP_RECEIVER_OBSERVER_H_ + +#include "JavaClass.h" +#include "JavaRef.h" + +#include "api/rtp_receiver_interface.h" + +#include +#include + +namespace jni +{ + class RTCRtpReceiverObserver : public webrtc::RtpReceiverObserverInterface + { + public: + RTCRtpReceiverObserver(JNIEnv * env, const JavaGlobalRef & observer); + ~RTCRtpReceiverObserver() = default; + + // RtpReceiverObserverInterface implementation. + void OnFirstPacketReceived(webrtc::MediaType media_type) override; + + private: + class JavaRTCRtpReceiverObserverClass : public JavaClass + { + public: + explicit JavaRTCRtpReceiverObserverClass(JNIEnv * env); + + jmethodID onFirstPacketReceived; + }; + + private: + JavaGlobalRef observer; + + const std::shared_ptr javaClass; + }; +} + +#endif diff --git a/webrtc-jni/src/main/cpp/include/api/RTCRtpSenderObserver.h b/webrtc-jni/src/main/cpp/include/api/RTCRtpSenderObserver.h new file mode 100644 index 000000000..0e8af67d0 --- /dev/null +++ b/webrtc-jni/src/main/cpp/include/api/RTCRtpSenderObserver.h @@ -0,0 +1,55 @@ +/* + * 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_SENDER_OBSERVER_H_ +#define JNI_WEBRTC_API_RTC_RTP_SENDER_OBSERVER_H_ + +#include "JavaClass.h" +#include "JavaRef.h" + +#include "api/rtp_sender_interface.h" + +#include +#include + +namespace jni +{ + class RTCRtpSenderObserver : public webrtc::RtpSenderObserverInterface + { + public: + RTCRtpSenderObserver(JNIEnv * env, const JavaGlobalRef & observer); + ~RTCRtpSenderObserver() = default; + + // RtpSenderObserverInterface implementation. + void OnFirstPacketSent(webrtc::MediaType media_type) override; + + private: + class JavaRTCRtpSenderObserverClass : public JavaClass + { + public: + explicit JavaRTCRtpSenderObserverClass(JNIEnv * env); + + jmethodID onFirstPacketSent; + }; + + private: + JavaGlobalRef observer; + + const std::shared_ptr javaClass; + }; +} + +#endif diff --git a/webrtc-jni/src/main/cpp/include/api/WebRTCUtils.h b/webrtc-jni/src/main/cpp/include/api/WebRTCUtils.h index f45fe07d0..9735a8bf9 100644 --- a/webrtc-jni/src/main/cpp/include/api/WebRTCUtils.h +++ b/webrtc-jni/src/main/cpp/include/api/WebRTCUtils.h @@ -82,7 +82,13 @@ namespace jni // uncaught exception handler, as the JVM does for an exception escaping a // Java thread. Callbacks that WebRTC invokes use it, because a C++ // exception must not unwind through WebRTC's frames. - void ReportUncaughtException(JNIEnv * env, jthrowable exception); + void ReportUncaughtException(JNIEnv * env, jthrowable exception) noexcept; + + // Clears the Java exception pending on the calling thread, if any, and + // reports it as uncaught. A callback that WebRTC invokes ends with this + // instead of ExceptionCheck(), which throws, after turning any C++ + // exception of its own into a Java one with ThrowCxxJavaException(). + void ReportPendingException(JNIEnv * env) noexcept; } #endif \ No newline at end of file diff --git a/webrtc-jni/src/main/cpp/src/JNI_AudioTrack.cpp b/webrtc-jni/src/main/cpp/src/JNI_AudioTrack.cpp index d738064d6..4469bb098 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_AudioTrack.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_AudioTrack.cpp @@ -54,6 +54,21 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_audio_AudioTrack_removeSinkI } } +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_audio_AudioTrack_setVolumeInternal +(JNIEnv * env, jobject caller, jdouble volume) +{ + webrtc::AudioTrackInterface * track = GetHandle(env, caller); + CHECK_HANDLE(track); + + // Only the source of a remote track, a RemoteAudioSource, applies the + // volume; the source of a local track ignores it. + webrtc::AudioSourceInterface * source = track->GetSource(); + + if (source != nullptr) { + source->SetVolume(static_cast(volume)); + } +} + JNIEXPORT jint JNICALL Java_dev_onvoid_webrtc_media_audio_AudioTrack_getSignalLevel (JNIEnv * env, jobject caller) { diff --git a/webrtc-jni/src/main/cpp/src/JNI_CustomVideoSource.cpp b/webrtc-jni/src/main/cpp/src/JNI_CustomVideoSource.cpp index a00e7f2f6..b23d11137 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_CustomVideoSource.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_CustomVideoSource.cpp @@ -67,10 +67,15 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_video_CustomVideoSource_push CHECK_HANDLE(source); if (javaFrame != nullptr) { - auto frame = jni::JavaLocalRef(env, javaFrame); - webrtc::VideoFrame nativeFrame = jni::VideoFrame::toNative(env, frame); - - source->PushFrame(nativeFrame); + try { + auto frame = jni::JavaLocalRef(env, javaFrame); + webrtc::VideoFrame nativeFrame = jni::VideoFrame::toNative(env, frame); + + source->PushFrame(nativeFrame); + } + catch (...) { + ThrowCxxJavaException(env); + } } } @@ -81,9 +86,14 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_video_CustomVideoSource_push CHECK_HANDLE(source); if (javaFrame != nullptr) { - auto frame = jni::JavaLocalRef(env, javaFrame); - webrtc::VideoFrame nativeFrame = jni::VideoFrame::toNative(env, frame); - - source->PushFrame(nativeFrame, timestampUs); + try { + auto frame = jni::JavaLocalRef(env, javaFrame); + webrtc::VideoFrame nativeFrame = jni::VideoFrame::toNative(env, frame); + + source->PushFrame(nativeFrame, timestampUs); + } + catch (...) { + ThrowCxxJavaException(env); + } } } diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCRtpReceiver.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCRtpReceiver.cpp index 154da0b47..9b5e3948d 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCRtpReceiver.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCRtpReceiver.cpp @@ -16,10 +16,12 @@ #include "JNI_RTCRtpReceiver.h" #include "api/RTCRtpParameters.h" +#include "api/RTCRtpReceiverObserver.h" #include "api/RTCRtpContributingSource.h" #include "api/RTCRtpSynchronizationSource.h" #include "JavaFactories.h" #include "JavaList.h" +#include "JavaString.h" #include "JavaUtils.h" #include "api/EncodedFrameTransformer.h" @@ -149,12 +151,53 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCRtpReceiver_requestKeyFrame } } +JNIEXPORT jstring JNICALL Java_dev_onvoid_webrtc_RTCRtpReceiver_getId +(JNIEnv * env, jobject caller) +{ + webrtc::RtpReceiverInterface * receiver = GetHandle(env, caller); + CHECK_HANDLEV(receiver, nullptr); + + return jni::JavaString::toJava(env, receiver->id()).release(); +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCRtpReceiver_setObserver +(JNIEnv * env, jobject caller, jobject jObserver) +{ + webrtc::RtpReceiverInterface * receiver = GetHandle(env, caller); + CHECK_HANDLE(receiver); + + try { + jni::RTCRtpReceiverObserver * observer = nullptr; + + if (jObserver != nullptr) { + observer = new jni::RTCRtpReceiverObserver(env, jni::JavaGlobalRef(env, jObserver)); + } + + // Runs on the signaling thread, which is also the thread that calls + // the observer, so the previous one is out of use once this returns. + receiver->SetObserver(observer); + + ReplaceNativeObserver(env, caller, "observerHandle", observer); + } + catch (...) { + ThrowCxxJavaException(env); + } +} + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCRtpReceiver_dispose (JNIEnv * env, jobject caller) { webrtc::RtpReceiverInterface * receiver = GetHandle(env, caller); CHECK_HANDLE(receiver); + // WebRTC must let go of an observer set through this object before it is + // deleted. + if (GetHandle(env, caller, "observerHandle") != nullptr) { + receiver->SetObserver(nullptr); + + ClearNativeObserver(env, caller, "observerHandle"); + } + // Unlike e.g. MediaStreamTrack, an RTCRtpReceiver is not exclusively // owned by one Java wrapper: the owning RtpTransceiver keeps its own // reference, and other Java wrappers may have been obtained via separate diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCRtpSender.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCRtpSender.cpp index a70435f1b..ba2bb4e09 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCRtpSender.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCRtpSender.cpp @@ -15,12 +15,15 @@ */ #include "JNI_RTCRtpSender.h" +#include "api/RTCRtpSenderObserver.h" #include "api/RTCRtpSendParameters.h" #include "api/WebRTCUtils.h" #include "JavaFactories.h" +#include "JavaArrayList.h" #include "JavaList.h" #include "JavaRef.h" #include "JavaRuntimeException.h" +#include "JavaString.h" #include "JavaUtils.h" #include "api/EncodedFrameTransformer.h" @@ -162,12 +165,76 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCRtpSender_generateKeyFrame } } +JNIEXPORT jstring JNICALL Java_dev_onvoid_webrtc_RTCRtpSender_getId +(JNIEnv * env, jobject caller) +{ + webrtc::RtpSenderInterface * sender = GetHandle(env, caller); + CHECK_HANDLEV(sender, nullptr); + + return jni::JavaString::toJava(env, sender->id()).release(); +} + +JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCRtpSender_getStreams +(JNIEnv * env, jobject caller) +{ + webrtc::RtpSenderInterface * sender = GetHandle(env, caller); + CHECK_HANDLEV(sender, nullptr); + + try { + const std::vector streamIds = sender->stream_ids(); + + jni::JavaArrayList list(env, streamIds.size()); + + for (const auto & id : streamIds) { + list.add(jni::JavaString::toJava(env, id)); + } + + return list.listObject().release(); + } + catch (...) { + ThrowCxxJavaException(env); + return nullptr; + } +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCRtpSender_setObserver +(JNIEnv * env, jobject caller, jobject jObserver) +{ + webrtc::RtpSenderInterface * sender = GetHandle(env, caller); + CHECK_HANDLE(sender); + + try { + jni::RTCRtpSenderObserver * observer = nullptr; + + if (jObserver != nullptr) { + observer = new jni::RTCRtpSenderObserver(env, jni::JavaGlobalRef(env, jObserver)); + } + + // Runs on the signaling thread, which is also the thread that calls + // the observer, so the previous one is out of use once this returns. + sender->SetObserver(observer); + + ReplaceNativeObserver(env, caller, "observerHandle", observer); + } + catch (...) { + ThrowCxxJavaException(env); + } +} + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCRtpSender_dispose (JNIEnv * env, jobject caller) { webrtc::RtpSenderInterface * sender = GetHandle(env, caller); CHECK_HANDLE(sender); + // WebRTC must let go of an observer set through this object before it is + // deleted. + if (GetHandle(env, caller, "observerHandle") != nullptr) { + sender->SetObserver(nullptr); + + ClearNativeObserver(env, caller, "observerHandle"); + } + // Unlike e.g. MediaStreamTrack, an RTCRtpSender is not exclusively owned // by one Java wrapper: the owning RtpTransceiver keeps its own reference, // and other Java wrappers may have been obtained via separate diff --git a/webrtc-jni/src/main/cpp/src/api/AddIceCandidateObserver.cpp b/webrtc-jni/src/main/cpp/src/api/AddIceCandidateObserver.cpp index fd257d783..79a24fcbc 100644 --- a/webrtc-jni/src/main/cpp/src/api/AddIceCandidateObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/api/AddIceCandidateObserver.cpp @@ -79,11 +79,7 @@ namespace jni } // A Java exception must not escape into WebRTC's signaling thread. - if (env->ExceptionCheck()) { - JavaLocalRef exception(env, env->ExceptionOccurred()); - env->ExceptionClear(); - ReportUncaughtException(env, exception.get()); - } + ReportPendingException(env); } AddIceCandidateObserver::JavaAddIceCandidateObserverClass::JavaAddIceCandidateObserverClass(JNIEnv * env) diff --git a/webrtc-jni/src/main/cpp/src/api/AudioTrackSink.cpp b/webrtc-jni/src/main/cpp/src/api/AudioTrackSink.cpp index 957668f99..f9451565e 100644 --- a/webrtc-jni/src/main/cpp/src/api/AudioTrackSink.cpp +++ b/webrtc-jni/src/main/cpp/src/api/AudioTrackSink.cpp @@ -15,7 +15,9 @@ */ #include "api/AudioTrackSink.h" +#include "api/WebRTCUtils.h" #include "JavaClasses.h" +#include "JavaUtils.h" #include "JNI_WebRTC.h" namespace jni @@ -37,12 +39,15 @@ namespace jni const jbyte * buffer = static_cast(data); jsize dataSize = static_cast(frames * channels * (bitsPerSample / 8)); - jbyteArray dataArray = env->NewByteArray(dataSize); - env->SetByteArrayRegion(dataArray, 0, dataSize, buffer); + JavaLocalRef dataArray(env, env->NewByteArray(dataSize)); - env->CallVoidMethod(sink, javaClass->onData, dataArray, bitsPerSample, sampleRate, channels, frames); - ExceptionCheck(env); - env->DeleteLocalRef(dataArray); + if (dataArray.get() != nullptr) { + env->SetByteArrayRegion(dataArray.get(), 0, dataSize, buffer); + env->CallVoidMethod(sink, javaClass->onData, dataArray.get(), static_cast(bitsPerSample), + static_cast(sampleRate), static_cast(channels), static_cast(frames)); + } + + ReportPendingException(env); } AudioTrackSink::JavaAudioTrackSinkClass::JavaAudioTrackSinkClass(JNIEnv * env) diff --git a/webrtc-jni/src/main/cpp/src/api/CreateSessionDescriptionObserver.cpp b/webrtc-jni/src/main/cpp/src/api/CreateSessionDescriptionObserver.cpp index 52d897552..61478d3ca 100644 --- a/webrtc-jni/src/main/cpp/src/api/CreateSessionDescriptionObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/api/CreateSessionDescriptionObserver.cpp @@ -18,6 +18,7 @@ #include "api/RTCSessionDescription.h" #include "api/WebRTCUtils.h" #include "JavaString.h" +#include "JavaUtils.h" #include "JNI_WebRTC.h" namespace jni @@ -36,11 +37,16 @@ namespace jni return; } - JavaLocalRef javaDesc = jni::RTCSessionDescription::toJava(env, desc); + try { + JavaLocalRef javaDesc = jni::RTCSessionDescription::toJava(env, desc); - env->CallVoidMethod(observer, javaClass->onSuccess, javaDesc.get()); + env->CallVoidMethod(observer, javaClass->onSuccess, javaDesc.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } - ExceptionCheck(env); + ReportPendingException(env); } void CreateSessionDescriptionObserver::OnFailure(webrtc::RTCError error) @@ -51,11 +57,16 @@ namespace jni return; } - JavaLocalRef errorMessage = JavaString::toJava(env, RTCErrorToString(error)); + try { + JavaLocalRef errorMessage = JavaString::toJava(env, RTCErrorToString(error)); - env->CallVoidMethod(observer, javaClass->onFailure, errorMessage.get()); + env->CallVoidMethod(observer, javaClass->onFailure, errorMessage.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } - ExceptionCheck(env); + ReportPendingException(env); } CreateSessionDescriptionObserver::JavaCreateSessionDescObserverClass::JavaCreateSessionDescObserverClass(JNIEnv * env) diff --git a/webrtc-jni/src/main/cpp/src/api/PeerConnectionObserver.cpp b/webrtc-jni/src/main/cpp/src/api/PeerConnectionObserver.cpp index 48b2430b0..1d093e995 100644 --- a/webrtc-jni/src/main/cpp/src/api/PeerConnectionObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/api/PeerConnectionObserver.cpp @@ -45,11 +45,16 @@ namespace jni return; } - auto jState = JavaEnums::toJava(env, state); + try { + auto jState = JavaEnums::toJava(env, state); - env->CallVoidMethod(observer, javaClass->onConnectionChange, jState.get()); + env->CallVoidMethod(observer, javaClass->onConnectionChange, jState.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } - ExceptionCheck(env); + ReportPendingException(env); } void PeerConnectionObserver::OnSignalingChange(webrtc::PeerConnectionInterface::SignalingState state) @@ -60,11 +65,16 @@ namespace jni return; } - auto jState = JavaEnums::toJava(env, state); + try { + auto jState = JavaEnums::toJava(env, state); - env->CallVoidMethod(observer, javaClass->onSignalingChange, jState.get()); + env->CallVoidMethod(observer, javaClass->onSignalingChange, jState.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } - ExceptionCheck(env); + ReportPendingException(env); } void PeerConnectionObserver::OnTrack(webrtc::scoped_refptr transceiver) @@ -75,15 +85,20 @@ namespace jni return; } - // transceiver is passed by value, so this call owns one reference on - // it; transfer that reference into the Java wrapper, which disposes - // it, instead of releasing it right back when transceiver goes out - // of scope and leaving the wrapper's pointer unowned. - auto jTransceiver = JavaFactories::create(env, transceiver.release()); + try { + // transceiver is passed by value, so this call owns one reference on + // it; transfer that reference into the Java wrapper, which disposes + // it, instead of releasing it right back when transceiver goes out + // of scope and leaving the wrapper's pointer unowned. + auto jTransceiver = JavaFactories::create(env, transceiver.release()); - env->CallVoidMethod(observer, javaClass->onTrack, jTransceiver.get()); + env->CallVoidMethod(observer, javaClass->onTrack, jTransceiver.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } - ExceptionCheck(env); + ReportPendingException(env); } void PeerConnectionObserver::OnAddTrack(webrtc::scoped_refptr receiver, const std::vector> & streams) @@ -111,7 +126,7 @@ namespace jni ThrowCxxJavaException(env); } - ExceptionCheck(env); + ReportPendingException(env); } void PeerConnectionObserver::OnRemoveTrack(webrtc::scoped_refptr receiver) @@ -122,15 +137,20 @@ namespace jni return; } - // receiver is passed by value, so this call owns one reference on - // it; transfer that reference into the Java wrapper, which disposes - // it, instead of releasing it right back when receiver goes out of - // scope and leaving the wrapper's pointer unowned. - auto jReceiver = JavaFactories::create(env, receiver.release()); + try { + // receiver is passed by value, so this call owns one reference on + // it; transfer that reference into the Java wrapper, which disposes + // it, instead of releasing it right back when receiver goes out of + // scope and leaving the wrapper's pointer unowned. + auto jReceiver = JavaFactories::create(env, receiver.release()); - env->CallVoidMethod(observer, javaClass->onRemoveTrack, jReceiver.get()); + env->CallVoidMethod(observer, javaClass->onRemoveTrack, jReceiver.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } - ExceptionCheck(env); + ReportPendingException(env); } void PeerConnectionObserver::OnDataChannel(webrtc::scoped_refptr channel) @@ -141,11 +161,16 @@ namespace jni return; } - auto jDataChannel = JavaFactories::create(env, channel.release()); + try { + auto jDataChannel = JavaFactories::create(env, channel.release()); - env->CallVoidMethod(observer, javaClass->onDataChannel, jDataChannel.get()); + env->CallVoidMethod(observer, javaClass->onDataChannel, jDataChannel.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } - ExceptionCheck(env); + ReportPendingException(env); } void PeerConnectionObserver::OnRenegotiationNeeded() @@ -158,7 +183,7 @@ namespace jni env->CallVoidMethod(observer, javaClass->onRenegotiationNeeded); - ExceptionCheck(env); + ReportPendingException(env); } void PeerConnectionObserver::OnIceConnectionChange(webrtc::PeerConnectionInterface::IceConnectionState state) @@ -169,11 +194,16 @@ namespace jni return; } - auto jState = JavaEnums::toJava(env, state); + try { + auto jState = JavaEnums::toJava(env, state); - env->CallVoidMethod(observer, javaClass->onIceConnectionChange, jState.get()); + env->CallVoidMethod(observer, javaClass->onIceConnectionChange, jState.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } - ExceptionCheck(env); + ReportPendingException(env); } void PeerConnectionObserver::OnIceGatheringChange(webrtc::PeerConnectionInterface::IceGatheringState state) @@ -184,11 +214,16 @@ namespace jni return; } - auto jState = JavaEnums::toJava(env, state); + try { + auto jState = JavaEnums::toJava(env, state); - env->CallVoidMethod(observer, javaClass->onIceGatheringChange, jState.get()); + env->CallVoidMethod(observer, javaClass->onIceGatheringChange, jState.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } - ExceptionCheck(env); + ReportPendingException(env); } void PeerConnectionObserver::OnIceCandidate(const webrtc::IceCandidateInterface * candidate) @@ -199,11 +234,16 @@ namespace jni return; } - JavaLocalRef jCandidate = RTCIceCandidate::toJava(env, candidate); + try { + JavaLocalRef jCandidate = RTCIceCandidate::toJava(env, candidate); - env->CallVoidMethod(observer, javaClass->onIceCandidate, jCandidate.get()); + env->CallVoidMethod(observer, javaClass->onIceCandidate, jCandidate.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } - ExceptionCheck(env); + ReportPendingException(env); } void PeerConnectionObserver::OnIceCandidateError(const std::string & address, int port, const std::string & url, int error_code, const std::string & error_text) @@ -214,11 +254,16 @@ namespace jni return; } - JavaLocalRef event = RTCPeerConnectionIceErrorEvent::toJava(env, address, port, url, error_code, error_text); + try { + JavaLocalRef event = RTCPeerConnectionIceErrorEvent::toJava(env, address, port, url, error_code, error_text); - env->CallVoidMethod(observer, javaClass->onIceCandidateError, event.get()); + env->CallVoidMethod(observer, javaClass->onIceCandidateError, event.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } - ExceptionCheck(env); + ReportPendingException(env); } void PeerConnectionObserver::OnIceCandidateRemoved(const webrtc::IceCandidate * candidate) @@ -240,7 +285,7 @@ namespace jni ThrowCxxJavaException(env); } - ReportException(env); + ReportPendingException(env); } void PeerConnectionObserver::OnIceConnectionReceivingChange(bool receiving) @@ -253,7 +298,7 @@ namespace jni env->CallVoidMethod(observer, javaClass->onIceConnectionReceivingChange, receiving); - ExceptionCheck(env); + ReportPendingException(env); } void PeerConnectionObserver::OnIceSelectedCandidatePairChanged(const webrtc::CandidatePairChangeEvent & event) @@ -275,16 +320,7 @@ namespace jni ThrowCxxJavaException(env); } - ReportException(env); - } - - void PeerConnectionObserver::ReportException(JNIEnv * env) noexcept - { - if (env->ExceptionCheck()) { - JavaLocalRef exception(env, env->ExceptionOccurred()); - env->ExceptionClear(); - ReportUncaughtException(env, exception.get()); - } + ReportPendingException(env); } PeerConnectionObserver::JavaPeerConnectionObserverClass::JavaPeerConnectionObserverClass(JNIEnv * env) diff --git a/webrtc-jni/src/main/cpp/src/api/RTCDataChannelObserver.cpp b/webrtc-jni/src/main/cpp/src/api/RTCDataChannelObserver.cpp index 6333bd06d..8e82b8187 100644 --- a/webrtc-jni/src/main/cpp/src/api/RTCDataChannelObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/api/RTCDataChannelObserver.cpp @@ -15,6 +15,7 @@ */ #include "api/RTCDataChannelObserver.h" +#include "api/WebRTCUtils.h" #include "JavaFactories.h" #include "JavaUtils.h" #include "JNI_WebRTC.h" @@ -38,7 +39,7 @@ namespace jni env->CallVoidMethod(observer, javaClass->onStateChange); - ExceptionCheck(env); + ReportPendingException(env); } void RTCDataChannelObserver::OnMessage(const webrtc::DataBuffer & buffer) @@ -49,11 +50,16 @@ namespace jni return; } - JavaLocalRef jBuffer = bufferFactory->create(env, &buffer); + try { + JavaLocalRef jBuffer = bufferFactory->create(env, &buffer); - env->CallVoidMethod(observer, javaClass->onMessage, jBuffer.get()); + env->CallVoidMethod(observer, javaClass->onMessage, jBuffer.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } - ExceptionCheck(env); + ReportPendingException(env); } void RTCDataChannelObserver::OnBufferedAmountChange(uint64_t sent_data_size) @@ -66,7 +72,7 @@ namespace jni env->CallVoidMethod(observer, javaClass->onBufferedAmountChange, static_cast(sent_data_size)); - ExceptionCheck(env); + ReportPendingException(env); } RTCDataChannelObserver::JavaRTCDataChannelObserverClass::JavaRTCDataChannelObserverClass(JNIEnv * env) diff --git a/webrtc-jni/src/main/cpp/src/api/RTCDataChannelSendObserver.cpp b/webrtc-jni/src/main/cpp/src/api/RTCDataChannelSendObserver.cpp index 023e54e1c..df1999e90 100644 --- a/webrtc-jni/src/main/cpp/src/api/RTCDataChannelSendObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/api/RTCDataChannelSendObserver.cpp @@ -57,11 +57,7 @@ namespace jni } } // A Java exception must not escape into WebRTC's network task or a destructor. - if (env->ExceptionCheck()) { - JavaLocalRef exception(env, env->ExceptionOccurred()); - env->ExceptionClear(); - ReportUncaughtException(env, exception.get()); - } + ReportPendingException(env); } RTCDataChannelSendObserver::JavaSendObserverClass::JavaSendObserverClass(JNIEnv * env) diff --git a/webrtc-jni/src/main/cpp/src/api/RTCDtlsTransportObserver.cpp b/webrtc-jni/src/main/cpp/src/api/RTCDtlsTransportObserver.cpp index f72d350f0..69227b98e 100644 --- a/webrtc-jni/src/main/cpp/src/api/RTCDtlsTransportObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/api/RTCDtlsTransportObserver.cpp @@ -38,11 +38,16 @@ namespace jni return; } - auto state = JavaEnums::toJava(env, info.state()); + try { + auto state = JavaEnums::toJava(env, info.state()); - env->CallVoidMethod(observer, javaClass->onStateChange, state.get()); + env->CallVoidMethod(observer, javaClass->onStateChange, state.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } - ExceptionCheck(env); + ReportPendingException(env); } void RTCDtlsTransportObserver::OnError(webrtc::RTCError error) @@ -53,11 +58,16 @@ namespace jni return; } - JavaLocalRef errorMessage = JavaString::toJava(env, RTCErrorToString(error)); + try { + JavaLocalRef errorMessage = JavaString::toJava(env, RTCErrorToString(error)); - env->CallVoidMethod(observer, javaClass->onError, errorMessage.get()); + env->CallVoidMethod(observer, javaClass->onError, errorMessage.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } - ExceptionCheck(env); + ReportPendingException(env); } RTCDtlsTransportObserver::JavaRTCDtlsTransportObserverClass::JavaRTCDtlsTransportObserverClass(JNIEnv * env) diff --git a/webrtc-jni/src/main/cpp/src/api/RTCDtmfSenderObserver.cpp b/webrtc-jni/src/main/cpp/src/api/RTCDtmfSenderObserver.cpp index a0a549608..db6f05a2d 100644 --- a/webrtc-jni/src/main/cpp/src/api/RTCDtmfSenderObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/api/RTCDtmfSenderObserver.cpp @@ -15,6 +15,7 @@ */ #include "api/RTCDtmfSenderObserver.h" +#include "api/WebRTCUtils.h" #include "JavaClasses.h" #include "JavaString.h" #include "JavaUtils.h" @@ -36,12 +37,17 @@ namespace jni return; } - JavaLocalRef jTone = JavaString::toJava(env, tone); - JavaLocalRef jToneBuffer = JavaString::toJava(env, tone_buffer); + try { + JavaLocalRef jTone = JavaString::toJava(env, tone); + JavaLocalRef jToneBuffer = JavaString::toJava(env, tone_buffer); - env->CallVoidMethod(observer, javaClass->onToneChange, jTone.get(), jToneBuffer.get()); + env->CallVoidMethod(observer, javaClass->onToneChange, jTone.get(), jToneBuffer.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } - ExceptionCheck(env); + ReportPendingException(env); } RTCDtmfSenderObserver::JavaRTCDtmfSenderObserverClass::JavaRTCDtmfSenderObserverClass(JNIEnv * env) diff --git a/webrtc-jni/src/main/cpp/src/api/RTCRtpReceiverObserver.cpp b/webrtc-jni/src/main/cpp/src/api/RTCRtpReceiverObserver.cpp new file mode 100644 index 000000000..d44b8e6a8 --- /dev/null +++ b/webrtc-jni/src/main/cpp/src/api/RTCRtpReceiverObserver.cpp @@ -0,0 +1,58 @@ +/* + * 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/RTCRtpReceiverObserver.h" +#include "api/WebRTCUtils.h" +#include "JavaClasses.h" +#include "JavaEnums.h" +#include "JavaUtils.h" +#include "JNI_WebRTC.h" + +namespace jni +{ + RTCRtpReceiverObserver::RTCRtpReceiverObserver(JNIEnv * env, const JavaGlobalRef & observer) : + observer(observer), + javaClass(JavaClasses::get(env)) + { + } + + void RTCRtpReceiverObserver::OnFirstPacketReceived(webrtc::MediaType media_type) + { + JNIEnv * env = AttachCurrentThread(); + + if (env == nullptr) { + return; + } + + try { + auto jMediaType = JavaEnums::toJava(env, media_type); + + env->CallVoidMethod(observer, javaClass->onFirstPacketReceived, jMediaType.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } + + ReportPendingException(env); + } + + RTCRtpReceiverObserver::JavaRTCRtpReceiverObserverClass::JavaRTCRtpReceiverObserverClass(JNIEnv * env) + { + jclass cls = FindClass(env, PKG"RTCRtpReceiverObserver"); + + onFirstPacketReceived = GetMethod(env, cls, "onFirstPacketReceived", "(L" PKG_MEDIA "MediaType;)V"); + } +} diff --git a/webrtc-jni/src/main/cpp/src/api/RTCRtpSenderObserver.cpp b/webrtc-jni/src/main/cpp/src/api/RTCRtpSenderObserver.cpp new file mode 100644 index 000000000..666de5660 --- /dev/null +++ b/webrtc-jni/src/main/cpp/src/api/RTCRtpSenderObserver.cpp @@ -0,0 +1,58 @@ +/* + * 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/RTCRtpSenderObserver.h" +#include "api/WebRTCUtils.h" +#include "JavaClasses.h" +#include "JavaEnums.h" +#include "JavaUtils.h" +#include "JNI_WebRTC.h" + +namespace jni +{ + RTCRtpSenderObserver::RTCRtpSenderObserver(JNIEnv * env, const JavaGlobalRef & observer) : + observer(observer), + javaClass(JavaClasses::get(env)) + { + } + + void RTCRtpSenderObserver::OnFirstPacketSent(webrtc::MediaType media_type) + { + JNIEnv * env = AttachCurrentThread(); + + if (env == nullptr) { + return; + } + + try { + auto jMediaType = JavaEnums::toJava(env, media_type); + + env->CallVoidMethod(observer, javaClass->onFirstPacketSent, jMediaType.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } + + ReportPendingException(env); + } + + RTCRtpSenderObserver::JavaRTCRtpSenderObserverClass::JavaRTCRtpSenderObserverClass(JNIEnv * env) + { + jclass cls = FindClass(env, PKG"RTCRtpSenderObserver"); + + onFirstPacketSent = GetMethod(env, cls, "onFirstPacketSent", "(L" PKG_MEDIA "MediaType;)V"); + } +} diff --git a/webrtc-jni/src/main/cpp/src/api/RTCStatsCollectorCallback.cpp b/webrtc-jni/src/main/cpp/src/api/RTCStatsCollectorCallback.cpp index 5bfe61c04..408d4b9a8 100644 --- a/webrtc-jni/src/main/cpp/src/api/RTCStatsCollectorCallback.cpp +++ b/webrtc-jni/src/main/cpp/src/api/RTCStatsCollectorCallback.cpp @@ -17,6 +17,7 @@ #include "api/RTCStatsCollectorCallback.h" #include "api/RTCStatsReport.h" #include "api/WebRTCUtils.h" +#include "JavaUtils.h" #include "JNI_WebRTC.h" namespace jni @@ -35,11 +36,16 @@ namespace jni return; } - JavaLocalRef javaReport = jni::RTCStatsReport::toJava(env, report); + try { + JavaLocalRef javaReport = jni::RTCStatsReport::toJava(env, report); - env->CallVoidMethod(callback, javaClass->onStatsDelivered, javaReport.get()); + env->CallVoidMethod(callback, javaClass->onStatsDelivered, javaReport.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } - ExceptionCheck(env); + ReportPendingException(env); } RTCStatsCollectorCallback::JavaRTCStatsCollectorCallbackClass::JavaRTCStatsCollectorCallbackClass(JNIEnv * env) diff --git a/webrtc-jni/src/main/cpp/src/api/SetSessionDescriptionObserver.cpp b/webrtc-jni/src/main/cpp/src/api/SetSessionDescriptionObserver.cpp index 1ea9ac008..28adbb8d0 100644 --- a/webrtc-jni/src/main/cpp/src/api/SetSessionDescriptionObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/api/SetSessionDescriptionObserver.cpp @@ -38,7 +38,7 @@ namespace jni env->CallVoidMethod(observer, javaClass->onSuccess); - ExceptionCheck(env); + ReportPendingException(env); } void SetSessionDescriptionObserver::OnFailure(webrtc::RTCError error) @@ -49,11 +49,16 @@ namespace jni return; } - JavaLocalRef errorMessage = JavaString::toJava(env, RTCErrorToString(error)); + try { + JavaLocalRef errorMessage = JavaString::toJava(env, RTCErrorToString(error)); - env->CallVoidMethod(observer, javaClass->onFailure, errorMessage.get()); + env->CallVoidMethod(observer, javaClass->onFailure, errorMessage.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } - ExceptionCheck(env); + ReportPendingException(env); } SetSessionDescriptionObserver::JavaSetSessionDescObserverClass::JavaSetSessionDescObserverClass(JNIEnv * env) diff --git a/webrtc-jni/src/main/cpp/src/api/VideoTrackSink.cpp b/webrtc-jni/src/main/cpp/src/api/VideoTrackSink.cpp index 0c343d04b..c6b41ce68 100644 --- a/webrtc-jni/src/main/cpp/src/api/VideoTrackSink.cpp +++ b/webrtc-jni/src/main/cpp/src/api/VideoTrackSink.cpp @@ -15,7 +15,9 @@ */ #include "api/VideoTrackSink.h" +#include "api/WebRTCUtils.h" #include "JavaClasses.h" +#include "JavaUtils.h" #include "JNI_WebRTC.h" #include "api/video/i420_buffer.h" @@ -52,14 +54,19 @@ namespace jni jint rotation = static_cast(frame.rotation()); jlong timestamp = frame.timestamp_us() * webrtc::kNumNanosecsPerMicrosec; - JavaLocalRef jBuffer = I420Buffer::toJava(env, i420BufferCopy); - jobject jFrame = env->NewObject(javaFrameClass->cls, javaFrameClass->ctor, jBuffer.get(), rotation, timestamp); + try { + JavaLocalRef jBuffer = I420Buffer::toJava(env, i420BufferCopy); + JavaLocalRef jFrame(env, env->NewObject(javaFrameClass->cls, javaFrameClass->ctor, jBuffer.get(), rotation, timestamp)); - env->CallVoidMethod(sink, javaClass->onFrame, jFrame); - ExceptionCheck(env); - // jBuffer is a JavaLocalRef and deletes its own local reference on - // scope exit; deleting it again here would be a double free. - env->DeleteLocalRef(jFrame); + if (jFrame.get() != nullptr) { + env->CallVoidMethod(sink, javaClass->onFrame, jFrame.get()); + } + } + catch (...) { + ThrowCxxJavaException(env); + } + + ReportPendingException(env); } VideoTrackSink::JavaVideoTrackSinkClass::JavaVideoTrackSinkClass(JNIEnv * env) diff --git a/webrtc-jni/src/main/cpp/src/api/WebRTCUtils.cpp b/webrtc-jni/src/main/cpp/src/api/WebRTCUtils.cpp index 2d3b4a250..420e7640f 100644 --- a/webrtc-jni/src/main/cpp/src/api/WebRTCUtils.cpp +++ b/webrtc-jni/src/main/cpp/src/api/WebRTCUtils.cpp @@ -30,7 +30,7 @@ namespace jni // ExceptionDescribe would print the "Exception in thread" prefix straight to // native stderr and the stack trace to System.err, splitting the report // across two streams and leaving the prefix on an unterminated line. - void ReportUncaughtException(JNIEnv * env, jthrowable exception) + void ReportUncaughtException(JNIEnv * env, jthrowable exception) noexcept { JavaLocalRef threadClass(env, env->FindClass("java/lang/Thread")); JavaLocalRef handlerClass(env, env->FindClass("java/lang/Thread$UncaughtExceptionHandler")); @@ -59,4 +59,16 @@ namespace jni env->ExceptionClear(); } } + + void ReportPendingException(JNIEnv * env) noexcept + { + if (!env->ExceptionCheck()) { + return; + } + + JavaLocalRef exception(env, env->ExceptionOccurred()); + env->ExceptionClear(); + + ReportUncaughtException(env, exception.get()); + } } diff --git a/webrtc-jni/src/main/cpp/src/media/MediaStreamTrackObserver.cpp b/webrtc-jni/src/main/cpp/src/media/MediaStreamTrackObserver.cpp index faba1f5dc..dcf36809b 100644 --- a/webrtc-jni/src/main/cpp/src/media/MediaStreamTrackObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/media/MediaStreamTrackObserver.cpp @@ -15,6 +15,7 @@ */ #include "media/MediaStreamTrackObserver.h" +#include "api/WebRTCUtils.h" #include "JavaFactories.h" #include "JavaUtils.h" #include "JNI_WebRTC.h" @@ -41,19 +42,26 @@ namespace jni // Check state changes. - if (eventType == MediaStreamTrackEvent::mute) { - if (track->enabled() != trackEnabled) { - // The condition "muted" is managed using the "enabled" property. - env->CallVoidMethod(javaTrack, javaClass->onTrackMute, createJavaTrack(env).release(), !track->enabled()); - ExceptionCheck(env); + try { + if (eventType == MediaStreamTrackEvent::mute) { + if (track->enabled() != trackEnabled) { + // The condition "muted" is managed using the "enabled" property. + JavaLocalRef jTrack = createJavaTrack(env); + env->CallVoidMethod(javaTrack, javaClass->onTrackMute, jTrack.get(), !track->enabled()); + } } - } - else if (eventType == MediaStreamTrackEvent::ended) { - if (track->state() != trackState && track->state() == webrtc::MediaStreamTrackInterface::TrackState::kEnded) { - env->CallVoidMethod(javaTrack, javaClass->onTrackEnd, createJavaTrack(env).release()); - ExceptionCheck(env); + else if (eventType == MediaStreamTrackEvent::ended) { + if (track->state() != trackState && track->state() == webrtc::MediaStreamTrackInterface::TrackState::kEnded) { + JavaLocalRef jTrack = createJavaTrack(env); + env->CallVoidMethod(javaTrack, javaClass->onTrackEnd, jTrack.get()); + } } } + catch (...) { + ThrowCxxJavaException(env); + } + + ReportPendingException(env); // Save current state. trackEnabled = track->enabled(); diff --git a/webrtc-jni/src/main/cpp/src/media/audio/AudioTransportSink.cpp b/webrtc-jni/src/main/cpp/src/media/audio/AudioTransportSink.cpp index 48fb61709..fffd8c5fc 100644 --- a/webrtc-jni/src/main/cpp/src/media/audio/AudioTransportSink.cpp +++ b/webrtc-jni/src/main/cpp/src/media/audio/AudioTransportSink.cpp @@ -15,7 +15,9 @@ */ #include "media/audio/AudioTransportSink.h" +#include "api/WebRTCUtils.h" #include "JavaClasses.h" +#include "JavaUtils.h" #include "JNI_WebRTC.h" namespace jni @@ -46,12 +48,17 @@ namespace jni const jbyte * buffer = static_cast(audioSamples); jsize dataSize = static_cast(nSamples * nBytesPerSample); - jbyteArray dataArray = env->NewByteArray(dataSize); - env->SetByteArrayRegion(dataArray, 0, dataSize, buffer); - env->CallVoidMethod(sink, javaClass->onRecordedData, dataArray, nSamples, nBytesPerSample, nChannels, samplesPerSec, totalDelayMS, clockDrift); + JavaLocalRef dataArray(env, env->NewByteArray(dataSize)); - ExceptionCheck(env); - env->DeleteLocalRef(dataArray); + if (dataArray.get() != nullptr) { + env->SetByteArrayRegion(dataArray.get(), 0, dataSize, buffer); + env->CallVoidMethod(sink, javaClass->onRecordedData, dataArray.get(), static_cast(nSamples), + static_cast(nBytesPerSample), static_cast(nChannels), static_cast(samplesPerSec), + static_cast(totalDelayMS), static_cast(clockDrift)); + } + + // The audio device's thread must not see a C++ exception. + ReportPendingException(env); return 0; } diff --git a/webrtc-jni/src/main/cpp/src/media/video/desktop/DesktopCaptureCallback.cpp b/webrtc-jni/src/main/cpp/src/media/video/desktop/DesktopCaptureCallback.cpp index 2abebde34..ea0931c31 100644 --- a/webrtc-jni/src/main/cpp/src/media/video/desktop/DesktopCaptureCallback.cpp +++ b/webrtc-jni/src/main/cpp/src/media/video/desktop/DesktopCaptureCallback.cpp @@ -16,8 +16,10 @@ #include "media/video/desktop/DesktopCaptureCallback.h" #include "media/video/desktop/DesktopFrame.h" +#include "api/WebRTCUtils.h" #include "JavaClasses.h" #include "JavaEnums.h" +#include "JavaUtils.h" #include "JNI_WebRTC.h" #include "libyuv/convert.h" @@ -50,14 +52,18 @@ namespace jni if (result != webrtc::DesktopCapturer::Result::SUCCESS) { // Propagate the failure to Java instead of silently dropping it — // callers waiting for a frame otherwise have to rely on timeouts. - auto jerror = JavaEnums::toJava(env, result); - env->CallVoidMethod(callback, javaClass->onCaptureResult, jerror.get(), nullptr); - ExceptionCheck(env); + try { + auto jerror = JavaEnums::toJava(env, result); + env->CallVoidMethod(callback, javaClass->onCaptureResult, jerror.get(), nullptr); + } + catch (...) { + ThrowCxxJavaException(env); + } + + ReportPendingException(env); return; } - auto jresult = JavaEnums::toJava(env, result); - int width = frame->size().width(); int height = frame->size().height(); @@ -113,15 +119,21 @@ namespace jni webrtc::scoped_refptr i420BufferCopy = webrtc::I420Buffer::Copy(*i420Buffer); i420BufferCopy->AddRef(); - JavaLocalRef jBuffer = I420Buffer::toJava(env, i420BufferCopy); - jobject jFrame = env->NewObject(javaFrameClass->cls, javaFrameClass->ctor, jBuffer.get(), rotation, timestamp); + try { + auto jresult = JavaEnums::toJava(env, result); + JavaLocalRef jBuffer = I420Buffer::toJava(env, i420BufferCopy); + JavaLocalRef jFrame(env, env->NewObject(javaFrameClass->cls, javaFrameClass->ctor, jBuffer.get(), rotation, timestamp)); - env->CallVoidMethod(callback, javaClass->onCaptureResult, jresult.get(), jFrame); + if (jFrame.get() != nullptr) { + env->CallVoidMethod(callback, javaClass->onCaptureResult, jresult.get(), jFrame.get()); + } + } + catch (...) { + ThrowCxxJavaException(env); + } - ExceptionCheck(env); - // jBuffer is a JavaLocalRef and deletes its own local reference on - // scope exit; deleting it again here would be a double free. - env->DeleteLocalRef(jFrame); + // The capturer is WebRTC code, whichever thread it calls back on. + ReportPendingException(env); } DesktopCaptureCallback::JavaDesktopCaptureCallbackClass::JavaDesktopCaptureCallbackClass(JNIEnv * env) diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiver.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiver.java index 7e120f76d..acab5bce7 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiver.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiver.java @@ -29,6 +29,14 @@ */ public class RTCRtpReceiver extends DisposableNativeObject { + /* + * The native observer set through this object, owned by it and freed when + * replaced, removed or disposed. + */ + @SuppressWarnings("unused") + private long observerHandle; + + /** * Constructor to be used by the native api. */ @@ -36,6 +44,30 @@ private RTCRtpReceiver() { } + /** + * Returns the ID of this receiver, which is unique within its peer + * connection. It is the same for all RTCRtpReceiver objects of the same + * receiver. + * + * @return The ID of this receiver. + */ + public native String getId(); + + /** + * Sets the observer that is told when this receiver receives its first + * RTP packet, replacing any observer set before. {@code null} removes the + * observer. + *

+ * A receiver has one observer. It belongs to the RTCRtpReceiver object it + * was set through: disposing that object removes it, also when it was set + * through another object of the same receiver in the meantime, so set it + * through one object, such as the one {@link + * PeerConnectionObserver#onTrack} provides. + * + * @param observer The observer, or {@code null}. + */ + public native void setObserver(RTCRtpReceiverObserver observer); + /** * The track that is associated with this RTCRtpReceiver object. Note that * track.stop() is final, although clones are not affected. Since diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiverObserver.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiverObserver.java new file mode 100644 index 000000000..6f42cdff0 --- /dev/null +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiverObserver.java @@ -0,0 +1,42 @@ +/* + * 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. + */ + +package dev.onvoid.webrtc; + +import dev.onvoid.webrtc.media.MediaType; + +/** + * Callback interface to learn when an {@link RTCRtpReceiver} receives its + * first RTP packet, for example to show media only once it actually flows. + *

+ * The callback runs on the peer connection's signaling thread. An exception + * thrown by it is reported to the thread's uncaught exception handler. + * + * @author Alex Andres + * + * @see RTCRtpReceiver#setObserver(RTCRtpReceiverObserver) + */ +public interface RTCRtpReceiverObserver { + + /** + * The receiver has received its first RTP packet. When the observer is + * set after that has happened, this is called right away. + * + * @param mediaType The media type of the receiver. + */ + void onFirstPacketReceived(MediaType mediaType); + +} diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSender.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSender.java index b30c010ee..486b15e5f 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSender.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSender.java @@ -31,6 +31,14 @@ */ public class RTCRtpSender extends DisposableNativeObject { + /* + * The native observer set through this object, owned by it and freed when + * replaced, removed or disposed. + */ + @SuppressWarnings("unused") + private long observerHandle; + + /** * Constructor to be used by the native api. */ @@ -38,6 +46,29 @@ private RTCRtpSender() { } + /** + * Returns the ID of this sender, which is unique within its peer + * connection. It is the same for all RTCRtpSender objects of the same + * sender. + * + * @return The ID of this sender. + */ + public native String getId(); + + /** + * Sets the observer that is told when this sender sends its first RTP + * packet, replacing any observer set before. {@code null} removes the + * observer. + *

+ * A sender has one observer. It belongs to the RTCRtpSender object it was + * set through: disposing that object removes it, also when it was set + * through another object of the same sender in the meantime, so set it + * through one object. + * + * @param observer The observer, or {@code null}. + */ + public native void setObserver(RTCRtpSenderObserver observer); + /** * Returns the track that is associated with this RTCRtpSender. If track is * ended, or if the track's output is disabled, i.e. the track is disabled @@ -102,6 +133,15 @@ private RTCRtpSender() { */ public native void setStreams(List streamIds); + /** + * Returns the IDs of the media streams associated with this sender's + * track, as given to {@link RTCPeerConnection#addTrack addTrack}, the + * {@link RTCRtpTransceiverInit} or {@link #setStreams}. + * + * @return The IDs of the media streams. + */ + public native List getStreams(); + /** * Returns the RTCDtmfSender associated with this RTCRtpSender. The RTCDtmfSender * enables the transmission of DTMF (Dual-Tone Multi-Frequency) tones over the diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSenderObserver.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSenderObserver.java new file mode 100644 index 000000000..da93591a0 --- /dev/null +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSenderObserver.java @@ -0,0 +1,42 @@ +/* + * 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. + */ + +package dev.onvoid.webrtc; + +import dev.onvoid.webrtc.media.MediaType; + +/** + * Callback interface to learn when an {@link RTCRtpSender} sends its first + * RTP packet. + *

+ * The callback runs on the peer connection's signaling thread. An exception + * thrown by it is reported to the thread's uncaught exception handler. + * + * @author Alex Andres + * + * @see RTCRtpSender#setObserver(RTCRtpSenderObserver) + */ +public interface RTCRtpSenderObserver { + + /** + * The sender has sent its first RTP packet. When the observer is set + * after that has happened, this is called right away. + * + * @param mediaType The media type of the sender. + */ + void onFirstPacketSent(MediaType mediaType); + +} diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/AudioTrack.java b/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/AudioTrack.java index 78397cc08..0fa70feeb 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/AudioTrack.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/AudioTrack.java @@ -85,6 +85,27 @@ public void removeSink(AudioTrackSink sink) { */ public native int getSignalLevel(); + /** + * Sets the playout volume of a remote audio track, the one of an {@link + * dev.onvoid.webrtc.RTCRtpReceiver}, from 0 (silent) to 10, where 1 plays + * the audio as received. Values above 1 amplify it. It applies to what the + * audio device plays, not to the audio passed to an {@link AudioTrackSink}. + * A local track ignores it. + * + * @param volume The volume, from 0 to 10. + * + * @throws IllegalArgumentException If the volume is outside 0 to 10. + */ + public void setVolume(double volume) { + if (!(volume >= 0 && volume <= 10)) { + throw new IllegalArgumentException("Volume must be between 0 and 10: " + volume); + } + + setVolumeInternal(volume); + } + + private native void setVolumeInternal(double volume); + private native long addSinkInternal(AudioTrackSink sink); private native void removeSinkInternal(long sinkHandle); diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/ObserverExceptionTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/ObserverExceptionTests.java new file mode 100644 index 000000000..5480b5925 --- /dev/null +++ b/webrtc/src/test/java/dev/onvoid/webrtc/ObserverExceptionTests.java @@ -0,0 +1,235 @@ +/* + * 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. + */ + +package dev.onvoid.webrtc; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * An exception thrown by an application callback that WebRTC invokes used to + * be rethrown as a C++ exception through WebRTC's frames, which leaks WebRTC's + * locks or ends the process. It now goes to the thread's uncaught exception + * handler, and WebRTC carries on. + */ +class ObserverExceptionTests extends TestBase { + + private final List uncaught = new CopyOnWriteArrayList<>(); + + private Thread.UncaughtExceptionHandler previousHandler; + + + @BeforeEach + void captureUncaught() { + uncaught.clear(); + previousHandler = Thread.getDefaultUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler((thread, error) -> uncaught.add(error)); + } + + @AfterEach + void restoreHandler() { + Thread.setDefaultUncaughtExceptionHandler(previousHandler); + } + + @Test + void statsCallbackThrows() throws Exception { + RTCPeerConnection connection = factory.createPeerConnection(new RTCConfiguration(), candidate -> { }); + + try { + connection.getStats(report -> { + throw new CallbackFailure("stats"); + }); + + awaitUncaught("stats"); + + // The connection still delivers stats. + CountDownLatch delivered = new CountDownLatch(1); + connection.getStats(report -> delivered.countDown()); + + assertTrue(delivered.await(5, TimeUnit.SECONDS)); + } + finally { + closeWithin(connection); + } + } + + @Test + void createOfferObserverThrows() throws Exception { + RTCPeerConnection connection = factory.createPeerConnection(new RTCConfiguration(), candidate -> { }); + + try { + connection.createDataChannel("data", new RTCDataChannelInit()); + connection.createOffer(new RTCOfferOptions(), new CreateSessionDescriptionObserver() { + @Override + public void onSuccess(RTCSessionDescription description) { + throw new CallbackFailure("offer"); + } + + @Override + public void onFailure(String error) { + } + }); + + awaitUncaught("offer"); + + // The signaling thread still runs the next operation. + TestCreateDescObserver observer = new TestCreateDescObserver(); + connection.createOffer(new RTCOfferOptions(), observer); + + assertNotNull(observer.get(5, TimeUnit.SECONDS)); + } + finally { + closeWithin(connection); + } + } + + @Test + void peerConnectionObserverThrows() throws Exception { + RTCPeerConnection[] peers = new RTCPeerConnection[2]; + CountDownLatch connected = new CountDownLatch(2); + + for (int i = 0; i < 2; i++) { + int remote = 1 - i; + + peers[i] = factory.createPeerConnection(new RTCConfiguration(), new PeerConnectionObserver() { + @Override + public void onIceCandidate(RTCIceCandidate candidate) { + peers[remote].addIceCandidate(candidate); + + throw new CallbackFailure("candidate"); + } + + @Override + public void onConnectionChange(RTCPeerConnectionState state) { + if (state == RTCPeerConnectionState.CONNECTED) { + connected.countDown(); + } + + throw new CallbackFailure("connection"); + } + }); + } + + try { + peers[0].createDataChannel("data", new RTCDataChannelInit()); + + TestSetDescObserver setOffer = new TestSetDescObserver(); + peers[0].setLocalDescription(setOffer); + setOffer.get(5, TimeUnit.SECONDS); + + TestSetDescObserver setRemoteOffer = new TestSetDescObserver(); + peers[1].setRemoteDescription(peers[0].getLocalDescription(), setRemoteOffer); + setRemoteOffer.get(5, TimeUnit.SECONDS); + + TestSetDescObserver setAnswer = new TestSetDescObserver(); + peers[1].setLocalDescription(setAnswer); + setAnswer.get(5, TimeUnit.SECONDS); + + TestSetDescObserver setRemoteAnswer = new TestSetDescObserver(); + peers[0].setRemoteDescription(peers[1].getLocalDescription(), setRemoteAnswer); + setRemoteAnswer.get(5, TimeUnit.SECONDS); + + assertTrue(connected.await(10, TimeUnit.SECONDS), "Peers did not connect"); + + awaitUncaught("candidate"); + awaitUncaught("connection"); + } + finally { + // Used to hang on a lock that a thrown callback left held. + closeWithin(peers[0]); + closeWithin(peers[1]); + } + } + + @Test + void dataChannelObserverThrows() throws Exception { + try (TestDataChannelPair pair = new TestDataChannelPair(factory)) { + pair.connect(); + + CountDownLatch received = new CountDownLatch(2); + + pair.receiver.unregisterObserver(); + pair.receiver.registerObserver(new RTCDataChannelObserver() { + @Override + public void onBufferedAmountChange(long previousAmount) { + } + + @Override + public void onStateChange() { + } + + @Override + public void onMessage(RTCDataChannelBuffer buffer) { + received.countDown(); + + throw new CallbackFailure("message"); + } + }); + + // Every message arrives although the observer throws on each one. + for (String text : new String[] { "one", "two" }) { + ByteBuffer data = ByteBuffer.wrap(text.getBytes(StandardCharsets.UTF_8)); + pair.sender.send(new RTCDataChannelBuffer(data, false)); + } + + assertTrue(received.await(5, TimeUnit.SECONDS), "Messages were lost"); + + awaitUncaught("message"); + } + } + + private void awaitUncaught(String callback) throws InterruptedException { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); + + while (System.nanoTime() < deadline) { + for (Throwable error : uncaught) { + if (error instanceof CallbackFailure && callback.equals(error.getMessage())) { + return; + } + } + + Thread.sleep(10); + } + + throw new AssertionError("No uncaught exception from the " + callback + " callback, got " + uncaught); + } + + private static void closeWithin(RTCPeerConnection connection) { + assertTimeoutPreemptively(Duration.ofSeconds(10), connection::close); + } + + + + private static class CallbackFailure extends RuntimeException { + + CallbackFailure(String callback) { + super(callback); + } + } +} diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/RTCRtpSenderReceiverTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCRtpSenderReceiverTests.java new file mode 100644 index 000000000..5b41d963d --- /dev/null +++ b/webrtc/src/test/java/dev/onvoid/webrtc/RTCRtpSenderReceiverTests.java @@ -0,0 +1,210 @@ +/* + * 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. + */ + +package dev.onvoid.webrtc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.onvoid.webrtc.media.MediaStreamTrack; +import dev.onvoid.webrtc.media.MediaType; +import dev.onvoid.webrtc.media.audio.AudioTrack; +import dev.onvoid.webrtc.media.audio.CustomAudioSource; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; + +class RTCRtpSenderReceiverTests extends TestBase { + + @Test + void firstPacketEvents() throws Exception { + try (TestMediaCall call = new TestMediaCall(factory, true, true)) { + call.negotiate(); + + RTCRtpReceiver videoReceiver = call.getReceiver(MediaStreamTrack.VIDEO_TRACK_KIND); + RTCRtpReceiver audioReceiver = call.getReceiver(MediaStreamTrack.AUDIO_TRACK_KIND); + + CountDownLatch videoReceived = new CountDownLatch(1); + CountDownLatch audioReceived = new CountDownLatch(1); + CountDownLatch videoSent = new CountDownLatch(1); + AtomicReference videoType = new AtomicReference<>(); + AtomicReference sentType = new AtomicReference<>(); + + videoReceiver.setObserver(type -> { + videoType.set(type); + videoReceived.countDown(); + }); + audioReceiver.setObserver(type -> audioReceived.countDown()); + call.getVideoSender().setObserver(type -> { + sentType.set(type); + videoSent.countDown(); + }); + + try { + call.awaitConnected(); + call.startMedia(); + + assertTrue(videoSent.await(10, TimeUnit.SECONDS), "No first video packet sent"); + assertTrue(videoReceived.await(10, TimeUnit.SECONDS), "No first video packet received"); + assertTrue(audioReceived.await(10, TimeUnit.SECONDS), "No first audio packet received"); + + assertEquals(MediaType.VIDEO, sentType.get()); + assertEquals(MediaType.VIDEO, videoType.get()); + + // An observer set late is told right away, before setObserver + // returns. + CountDownLatch late = new CountDownLatch(1); + videoReceiver.setObserver(type -> late.countDown()); + + assertEquals(0, late.getCount()); + + CountDownLatch lateSent = new CountDownLatch(1); + call.getVideoSender().setObserver(type -> lateSent.countDown()); + + assertEquals(0, lateSent.getCount()); + + videoReceiver.setObserver(null); + call.getVideoSender().setObserver(null); + } + finally { + // Disposing removes the observers set through these objects. + videoReceiver.dispose(); + audioReceiver.dispose(); + } + } + } + + @Test + void senderIdAndStreams() { + RTCPeerConnection connection = factory.createPeerConnection(new RTCConfiguration(), candidate -> { }); + // Custom audio, like the calls of the other tests: a factory sends + // either device or custom audio. + AudioTrack track = factory.createAudioTrack("audioTrack", new CustomAudioSource()); + + try { + RTCRtpSender sender = connection.addTrack(track, Arrays.asList("stream-a", "stream-b")); + + assertFalse(sender.getId().isEmpty()); + assertEquals(Arrays.asList("stream-a", "stream-b"), sender.getStreams()); + + sender.setStreams(Collections.singletonList("stream-c")); + + assertEquals(Collections.singletonList("stream-c"), sender.getStreams()); + + // Every object of the same sender has its ID. + RTCRtpSender[] senders = connection.getSenders(); + + assertEquals(sender.getId(), senders[0].getId()); + + for (RTCRtpSender other : senders) { + other.dispose(); + } + } + finally { + connection.close(); + } + } + + @Test + void receiverId() { + RTCPeerConnection connection = factory.createPeerConnection(new RTCConfiguration(), candidate -> { }); + + try { + RTCRtpTransceiver transceiver = connection.addTransceiver( + factory.createAudioTrack("audio", new CustomAudioSource()), + new RTCRtpTransceiverInit()); + + RTCRtpReceiver receiver = transceiver.getReceiver(); + RTCRtpReceiver[] receivers = connection.getReceivers(); + + assertFalse(receiver.getId().isEmpty()); + assertEquals(receiver.getId(), receivers[0].getId()); + + // Setting and removing an observer before any media flows. + receiver.setObserver(type -> { }); + receiver.setObserver(null); + + for (RTCRtpReceiver other : receivers) { + other.dispose(); + } + receiver.dispose(); + transceiver.dispose(); + } + finally { + connection.close(); + } + } + + @Test + void remoteTrackVolume() throws Exception { + try (TestMediaCall call = new TestMediaCall(factory, false, true)) { + call.negotiate(); + + RTCRtpReceiver receiver = call.getReceiver(MediaStreamTrack.AUDIO_TRACK_KIND); + + try { + AudioTrack track = (AudioTrack) receiver.getTrack(); + + track.setVolume(0); + track.setVolume(0.5); + track.setVolume(10); + + for (double invalid : new double[] { -0.1, 10.1, Double.NaN }) { + assertThrows(IllegalArgumentException.class, () -> track.setVolume(invalid)); + } + } + finally { + receiver.dispose(); + } + } + } + + @Test + void localTrackIgnoresVolume() { + AudioTrack track = factory.createAudioTrack("audioTrack", new CustomAudioSource()); + + try { + track.setVolume(2); + } + finally { + track.dispose(); + } + } + + @Test + void streamsOfSenderWithoutStreams() { + RTCPeerConnection connection = factory.createPeerConnection(new RTCConfiguration(), candidate -> { }); + AudioTrack track = factory.createAudioTrack("audioTrack", new CustomAudioSource()); + + try { + RTCRtpSender sender = connection.addTrack(track, Collections.emptyList()); + List streams = sender.getStreams(); + + assertTrue(streams.isEmpty(), streams.toString()); + } + finally { + connection.close(); + } + } +}