From 3ae10dbad0eb613225e2fb2d22b27369c9b77dd7 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Fri, 23 Oct 2026 17:32:43 +0200 Subject: [PATCH] fix: register a sender once when replaceTrack is given its current track --- .../onvoid/webrtc/client/LocalTrackImpl.java | 9 ++++++++- .../dev/onvoid/webrtc/client/SenderImpl.java | 6 +++++- .../dev/onvoid/webrtc/client/MediaTests.java | 19 +++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/client/LocalTrackImpl.java b/webrtc/src/main/java/dev/onvoid/webrtc/client/LocalTrackImpl.java index 552d529e..30e73f5b 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/client/LocalTrackImpl.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/client/LocalTrackImpl.java @@ -19,7 +19,9 @@ import static java.util.Objects.requireNonNull; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.CompletableFuture; import dev.onvoid.webrtc.media.MediaStreamTrack; @@ -54,7 +56,8 @@ abstract class LocalTrackImpl implements LocalTrack { // Guarded by this. - private final List senders = new ArrayList<>(); + /** A sender registers once, however often it is given this track. */ + private final Set senders = new LinkedHashSet<>(); private final List endedHandlers = new ArrayList<>(); @@ -177,6 +180,10 @@ synchronized void unregister(SenderImpl sender) { senders.remove(sender); } + synchronized List registeredSenders() { + return new ArrayList<>(senders); + } + /** * @return true while the native track may be used, which callers check * under this object's lock. diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/client/SenderImpl.java b/webrtc/src/main/java/dev/onvoid/webrtc/client/SenderImpl.java index b634389c..1461c08e 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/client/SenderImpl.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/client/SenderImpl.java @@ -113,6 +113,10 @@ public CompletableFuture replaceTrack(LocalTrack replacement) { if (next != null && !next.register(this)) { throw new IllegalStateException("The track " + next.id() + " is closed"); } + if (next == track) { + // Already sent, and registered only once. + return; + } try { sender.replaceTrack(next == null ? null : next.nativeTrack()); @@ -127,7 +131,7 @@ public CompletableFuture replaceTrack(LocalTrack replacement) { LocalTrackImpl previous = track; track = next; - if (previous != null && previous != next) { + if (previous != null) { previous.unregister(this); } }); diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/client/MediaTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/client/MediaTests.java index f786b277..70125e8c 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/client/MediaTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/client/MediaTests.java @@ -219,6 +219,25 @@ void closedTrackLeavesEveryPeer() throws Exception { assertEquals(other, one.track()); } + @Test + void sameTrackRegisteredOnce() throws Exception { + Peer[] peers = pair(); + LocalTrackImpl voice = (LocalTrackImpl) rtc.media().customAudio("voice"); + Sender sender = peers[0].addTrack(voice); + + for (int i = 0; i < 3; i++) { + await(sender.replaceTrack(voice)); + } + + assertEquals(voice, sender.track()); + assertEquals(Collections.singletonList(sender), voice.registeredSenders()); + + await(sender.remove()); + + // A removed sender, and its peer, are not kept by the track. + assertTrue(voice.registeredSenders().isEmpty()); + } + @Test void senderConfigured() throws Exception { Peer[] peers = pair();