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
30 changes: 30 additions & 0 deletions webrtc/src/main/java/dev/onvoid/webrtc/client/LocalCameraImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,25 @@ public CompletableFuture<Void> switchCamera(VideoDevice device) {
requireNonNull(device, "device");

CompletableFuture<Void> switched = new CompletableFuture<>();

if (isEnded()) {
switched.completeExceptionally(new MediaException("The camera track is closed"));
return switched;
}

// Registered like the creation of a track, as the switch opens a
// camera and uses the factory: a runtime that begins to close either
// waits for it before it frees the factory, or is seen closing here.
LocalMediaImpl.Creation operation;

try {
operation = media.begin();
}
catch (PeerClosedException e) {
switched.completeExceptionally(e);
return switched;
}

media.rtc.track(switched, null);

// Opening a camera can take a moment, so it happens off the caller's thread.
Expand All @@ -73,6 +92,12 @@ public CompletableFuture<Void> switchCamera(VideoDevice device) {
switched.completeExceptionally(e instanceof WebRtcException
? e : new MediaException("Switching to the camera " + device.getName() + " failed", e));
}
finally {
operation.close();
}
}, () -> {
operation.close();
switched.completeExceptionally(new PeerClosedException("The runtime is closed"));
});

// Completed on the event executor, so that the application's
Expand All @@ -84,6 +109,11 @@ private void switchTo(VideoDevice device) {
LocalMediaImpl.SharedCamera current;

synchronized (this) {
// Closed while the switch was queued, so no camera is opened.
if (!isLive()) {
throw new MediaException("The camera track is closed");
}

current = camera;
}

Expand Down
100 changes: 100 additions & 0 deletions webrtc/src/test/java/dev/onvoid/webrtc/client/RuntimeCloseTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import dev.onvoid.webrtc.RTCSessionDescription;
import dev.onvoid.webrtc.media.audio.AudioDevice;
import dev.onvoid.webrtc.media.video.CustomVideoSource;
import dev.onvoid.webrtc.media.video.VideoDevice;

import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -441,6 +442,73 @@ void switchDeviceAfterClose() throws Exception {
await(closed);
}

@Test
void closeWhileSwitchingCamera() throws Exception {
// A camera switch is queued when the runtime begins to close. The
// runtime waits for it before it frees the factory, and the switch,
// which finds its track closed, opens no camera.
ManualExecutor control = new ManualExecutor();
WebRtc runtime = runtime(control);
LocalMediaImpl media = (LocalMediaImpl) runtime.media();
LocalCameraImpl camera = camera(runtime);

CompletableFuture<Void> switched = camera.switchCamera(camera("Other camera"));

// Named among what the runtime waits for, beside the track.
assertEquals(2, media.closeAll().size());

CompletableFuture<Void> closed = runtime.closeAsync();
control.runUntil(closed::isDone, TIMEOUT_SECONDS);
await(closed);

// The switch ran while the runtime still had its threads, rather than
// being failed by the runtime's close.
assertTrackClosed(switched);
}

@Test
void switchCameraAfterClose() throws Exception {
LocalCameraImpl camera = camera(rtc);
await(camera.closeAsync());

// Failed at once, without opening a camera over the factory.
CompletableFuture<Void> switched = camera.switchCamera(camera("Other camera"));

assertTrue(switched.isDone());
assertTrackClosed(switched);
}

@Test
void switchCameraRejected() throws Exception {
// The executor refuses the switch, which fails and lets go of its
// registration, so that the runtime does not wait for it.
ManualExecutor control = new ManualExecutor();
WebRtc runtime = runtime(control);
LocalMediaImpl media = (LocalMediaImpl) runtime.media();
LocalCameraImpl camera = camera(runtime);

try {
control.rejecting(true);
CompletableFuture<Void> switched = camera.switchCamera(camera("Other camera"));

ExecutionException e = assertThrows(ExecutionException.class, () -> await(switched));
assertInstanceOf(PeerClosedException.class, e.getCause());
assertEquals(1, errors.size(), () -> "Errors: " + errors);
assertInstanceOf(RejectedExecutionException.class, errors.get(0).getCause());
}
finally {
control.rejecting(false);
errors.clear();
}

// Only the track is left.
assertEquals(1, media.closeAll().size());

CompletableFuture<Void> closed = runtime.closeAsync();
control.runUntil(closed::isDone, TIMEOUT_SECONDS);
await(closed);
}

@Test
void closedPeerNeedsNoTimer() throws Exception {
// An operation reaches a peer the closing runtime has closed, after it
Expand Down Expand Up @@ -511,6 +579,38 @@ private WebRtc runtime(ManualExecutor control) {
.build();
}

/**
* A camera track over a custom source, so that no camera is needed. Its
* camera has another user, so that the track's release lets go of its
* share only, and touches no capture device.
*/
private static LocalCameraImpl camera(WebRtc runtime) {
LocalMediaImpl media = (LocalMediaImpl) runtime.media();
VideoDevice device = camera("Test camera");
LocalMediaImpl.SharedCamera shared = new LocalMediaImpl.SharedCamera(device.getDescriptor(),
CameraOptions.builder().device(device).build(), null);
shared.users = 2;

try (LocalMediaImpl.Creation creation = media.begin()) {
CustomVideoSource source = new CustomVideoSource();
LocalCameraImpl camera = media.register(new LocalCameraImpl(media,
runtime.factory.createVideoTrack("camera", source), shared));
camera.onEnded(source::dispose);

return camera;
}
}

private static VideoDevice camera(String name) {
return new VideoDevice(name, name.toLowerCase().replace(' ', '-')) { };
}

private static void assertTrackClosed(CompletableFuture<Void> switched) {
ExecutionException e = assertThrows(ExecutionException.class, () -> await(switched));
assertInstanceOf(MediaException.class, e.getCause());
assertEquals("The camera track is closed", e.getCause().getMessage());
}

/**
* @return what waiting for the future threw, or null.
*/
Expand Down
Loading