Hello, thank you for open-sourcing WebRTC-Java.
I encountered some issues during use. My scenario requires starting and stopping audio transmission frequently based on external conditions, necessitating frequent startup and shutdown. During stress testing, I used vmmap to monitor the heap and found that after multiple startups and shutdowns, the heap and blocks kept increasing without stabilizing, leading me to suspect a memory leak.
Environment:
OS: Windows 11 23h2
JDK version: Zulu JDK 25.0.2
Springboot: 3.5.13
Webrtc-java: 0.14.0
Below is my test code:
importcom.lsl.core.webrtc.model.WebrtcPushPeerObserver;
importdev.onvoid.webrtc.*;
importdev.onvoid.webrtc.media.audio.*;
importdev.onvoid.webrtc.media.video.CustomVideoSource;
importdev.onvoid.webrtc.media.video.NativeI420Buffer;
importdev.onvoid.webrtc.media.video.VideoFrame;
importdev.onvoid.webrtc.media.video.VideoTrack;
importjakarta.annotation.PreDestroy;
importlombok.extern.slf4j.Slf4j;
importjava.nio.ByteBuffer;
importjava.util.List;
importjava.util.concurrent.*;
importjava.util.concurrent.locks.ReentrantLock;
@Slf4jpublicclassWebrtcPushVoiceService {
privatefinalReentrantLockPUSH_VOICE_LOCK = newReentrantLock();
privatevolatilebooleanpushVoiceFlag = false;
privatefinalSignalingClientsignalingClient;
privatefinalAudioDeviceaudioDevice;
privateAudioDeviceModuleaudioModule;
privatePeerConnectionFactoryfactory;
privateRTCPeerConnectionpeerConnection;
privateAudioTrackaudioTrack;
privateVideoTrackvideoTrack;
privateCustomVideoSourcememoryVideoSource;
privateScheduledExecutorServicedummyVideoExecutor;
privateNativeI420BufferdummyBlackBuffer;
privatefinalExecutorServicewebrtcPushAsyncExecutor = Executors.newSingleThreadExecutor(
Thread.ofPlatform().name("webrtc-push-async-thread").factory());
publicWebrtcPushVoiceService(SignalingClientsignalingClient, AudioDeviceaudioDevice) {
this.signalingClient = signalingClient;
this.audioDevice = audioDevice;
}
publicvoidstartPushVoiceStream(StringpushUrl) {
PUSH_VOICE_LOCK.lock();
try {
if (pushVoiceFlag) {
return;
}
stopPushInternal();
pushVoiceFlag = true;
audioModule = newAudioDeviceModule();
audioModule.setRecordingDevice(audioDevice);
audioModule.initRecording();
factory = newPeerConnectionFactory(audioModule);
RTCConfigurationconfig = newRTCConfiguration();
WebrtcPushPeerObserverobserver = newWebrtcPushPeerObserver(
pushUrl,
signalingClient,
() -> webrtcPushAsyncExecutor.submit(this::stopPushVoiceStream));
peerConnection = factory.createPeerConnection(config, observer);
observer.setPeerConnection(peerConnection);
AudioTrackSourceaudioSource = factory.createAudioSource(newAudioOptions());
audioTrack = factory.createAudioTrack("audio", audioSource);
RTCRtpTransceiverInitaudioInit = newRTCRtpTransceiverInit();
audioInit.direction = RTCRtpTransceiverDirection.SEND_ONLY;
audioInit.streamIds = List.of("rts");
peerConnection.addTransceiver(audioTrack, audioInit);
injectMemoryVideoTrackAndOffer();
} catch (Exceptione) {
stopPushInternal();
} finally {
PUSH_VOICE_LOCK.unlock();
}
}
privatevoidinjectMemoryVideoTrackAndOffer() {
try {
memoryVideoSource = newCustomVideoSource();
videoTrack = factory.createVideoTrack("dummy_video", memoryVideoSource);
RTCRtpTransceiverInitvideoInit = newRTCRtpTransceiverInit();
videoInit.direction = RTCRtpTransceiverDirection.SEND_ONLY;
videoInit.streamIds = List.of("rts");
peerConnection.addTransceiver(videoTrack, videoInit);
startDummyVideoLoop();
createAndSendOffer();
} catch (Exceptione) {
createAndSendOffer();
}
}
privatevoidstartDummyVideoLoop() {
dummyVideoExecutor = newScheduledThreadPoolExecutor(1,
Thread.ofPlatform().name("dummy-video-loop").factory());
intwidth = 16;
intheight = 16;
dummyBlackBuffer = NativeI420Buffer.allocate(width, height);
ByteBufferdataY = dummyBlackBuffer.getDataY();
ByteBufferdataU = dummyBlackBuffer.getDataU();
ByteBufferdataV = dummyBlackBuffer.getDataV();
byteblackY = 0;
byteblackUV = (byte) 128;
while (dataY.hasRemaining())
dataY.put(blackY);
while (dataU.hasRemaining())
dataU.put(blackUV);
while (dataV.hasRemaining())
dataV.put(blackUV);
dummyVideoExecutor.scheduleAtFixedRate(() -> {
if (!pushVoiceFlag || memoryVideoSource == null || dummyBlackBuffer == null)
return;
try {
dummyBlackBuffer.retain();
longtimestampNs = System.nanoTime();
VideoFrameframe = newVideoFrame(dummyBlackBuffer, timestampNs);
memoryVideoSource.pushFrame(frame);
frame.release();
} catch (Exceptione) {
}
}, 0, 33, TimeUnit.MILLISECONDS); // 30 FPS
}
privatevoidcreateAndSendOffer() {
RTCOfferOptionsofferOptions = newRTCOfferOptions();
peerConnection.createOffer(offerOptions, newCreateSessionDescriptionObserver() {
@OverridepublicvoidonSuccess(RTCSessionDescriptionofferDescription) {
if (!pushVoiceFlag) {
return;
}
RTCPeerConnectioncurrentPc = peerConnection;
if (currentPc == null)
return;
currentPc.setLocalDescription(offerDescription, newSetSessionDescriptionObserver() {
@OverridepublicvoidonSuccess() {
}
@OverridepublicvoidonFailure(Stringerror) {
webrtcPushAsyncExecutor.submit(WebrtcPushVoiceService.this::stopPushVoiceStream);
}
});
}
@OverridepublicvoidonFailure(Stringerror) {
webrtcPushAsyncExecutor.submit(() -> stopPushVoiceStream());
}
});
}
publicvoidstopPushVoiceStream() {
PUSH_VOICE_LOCK.lock();
try {
stopPushInternal();
} finally {
PUSH_VOICE_LOCK.unlock();
}
}
privatevoidstopPushInternal() {
this.pushVoiceFlag = false;
if (dummyVideoExecutor != null) {
dummyVideoExecutor.shutdown();
try {
if (!dummyVideoExecutor.awaitTermination(100, TimeUnit.MILLISECONDS)) {
dummyVideoExecutor.shutdownNow();
}
} catch (InterruptedExceptione) {
dummyVideoExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
dummyVideoExecutor = null;
}
if (peerConnection != null) {
RTCPeerConnectiontempPc = peerConnection;
peerConnection = null;
try {
tempPc.close();
} catch (Exceptione) {
log.error("error", e);
}
}
videoTrack = null;
audioTrack = null;
if (memoryVideoSource != null) {
CustomVideoSourcetempSource = memoryVideoSource;
memoryVideoSource = null;
try {
tempSource.dispose();
} catch (Throwablee) {
}
}
if (dummyBlackBuffer != null) {
try {
dummyBlackBuffer.release();
dummyBlackBuffer = null;
} catch (Exceptione) {
}
}
if (factory != null) {
PeerConnectionFactorytempFactory = factory;
factory = null;
try {
tempFactory.dispose();
} catch (Throwablet) {
}
}
if (audioModule != null) {
audioModule.stopRecording();
AudioDeviceModuletempModule = audioModule;
audioModule = null;
try {
tempModule.dispose();
} catch (Exceptione) {
}
}
}
@PreDestroypublicvoiddestroy() {
stopPushVoiceStream();
if (!webrtcPushAsyncExecutor.isShutdown()) {
webrtcPushAsyncExecutor.shutdown();
try {
if (!webrtcPushAsyncExecutor.awaitTermination(2, TimeUnit.SECONDS)) {
webrtcPushAsyncExecutor.shutdownNow();
}
} catch (InterruptedExceptione) {
webrtcPushAsyncExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
}Below is my simple stress test code:
publicclassTest2 {
staticvoidmain() {
StringstartUrl = "http://localhost:6666/test/webrtc/push/start?url=?";
StringstopUrl = "http://localhost:6666/test/webrtc/push/stop";
for (inti = 0; i < 10000; i++) {
StringstartResult = HttpUtil.get(startUrl);
ThreadUtil.sleep(5000);
StringstopResult = HttpUtil.get(stopUrl);
}
}
}Initially, I closed videoTrack audioTrack CustomVideoSource in stopPushInternal, but encountered the exception: "Native object was not deleted. A reference is still around somewhere."
I consulted an AI coding tool, which told me that RTCPeerConnection needed to be closed before closing videoTrack audioTrack CustomVideoSource. Even after changing the order, the exception still occurred during closure: "Native object was not deleted. A reference is still around somewhere."
The AI then told me that I didn't need to explicitly close videoTrack audioTrack; it would automatically close when the factory closed. Therefore, in my test code, I set both videoTrack audioTrack and videoTrack to null.
Hello, thank you for open-sourcing WebRTC-Java.
I encountered some issues during use. My scenario requires starting and stopping audio transmission frequently based on external conditions, necessitating frequent startup and shutdown. During stress testing, I used vmmap to monitor the heap and found that after multiple startups and shutdowns, the heap and blocks kept increasing without stabilizing, leading me to suspect a memory leak.
Environment:
OS: Windows 11 23h2
JDK version: Zulu JDK 25.0.2
Springboot: 3.5.13
Webrtc-java: 0.14.0
Below is my test code:
Below is my simple stress test code:
Initially, I closed
videoTrack audioTrack CustomVideoSourceinstopPushInternal, but encountered the exception: "Native object was not deleted. A reference is still around somewhere."I consulted an AI coding tool, which told me that
RTCPeerConnectionneeded to be closed before closingvideoTrack audioTrack CustomVideoSource. Even after changing the order, the exception still occurred during closure: "Native object was not deleted. A reference is still around somewhere."The AI then told me that I didn't need to explicitly close
videoTrack audioTrack; it would automatically close when the factory closed. Therefore, in my test code, I set bothvideoTrack audioTrackandvideoTrackto null.