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
2 changes: 2 additions & 0 deletions docs/guide/client-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ WebRtc rtc = WebRtc.builder()
| `closeTimeout(Duration)` | 10 seconds | How long `close()` waits. |
| `configureFactory(Consumer<PeerConnectionFactory.Builder>)` | none | Anything else of the factory, such as codec factories of your own. |

Every timeout of the client API, these two as well as the gathering timeout and the `Whip` and `Whep` timeouts below, must be positive; none of them can be switched off. A timeout counts in milliseconds, rounded up, so one under a millisecond waits a millisecond. One too long for milliseconds to count waits as long as they can count, about 292 million years.

### Audio Modes

WebRTC feeds the audio a factory captures into every audio sender of that factory, so captured audio and audio you push cannot be mixed in one runtime. The runtime's `AudioMode` picks one of them up front:
Expand Down
2 changes: 2 additions & 0 deletions docs/guide/media/media-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ import java.time.Duration;
MediaFileSource source = new MediaFileSource("rtsp://camera.local/stream1", Duration.ofSeconds(5));
```

`Duration.ZERO` waits as long as it takes, and is the only limit that does: a positive one waits at least a microsecond, and one too long for microseconds to count waits as long as they can count. A negative one is rejected.

An unreachable server makes the constructor throw an `IOException` once that time is up. A stream that stops sending during playback is reported to the listener's `onError`, and the player is left `PAUSED`. Closing a player never waits for the time limit: it breaks off whatever the player is waiting for at once.

A live stream has no end and no length: its `MediaInfo` reports a duration of 0, looping does not apply, and seeking is reported as an error. Pausing stops reading from the server, but the server keeps sending; resuming carries on from where playback paused, which leaves it behind real time by as long as it was paused. To get back to live, close the source and open it again.
Expand Down
12 changes: 11 additions & 1 deletion webrtc-media/src/main/cpp/src/media/MediaReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,17 @@ namespace ffmpeg
void MediaReader::BeginBlocking()
{
timed_out_.store(false);
deadline_us_.store(timeout_us_ > 0 ? av_gettime_relative() + timeout_us_ : 0);

if (timeout_us_ <= 0) {
deadline_us_.store(0);
return;
}

// Saturated: a timeout too long to count arrives as INT64_MAX, and the
// sum would wrap around into a deadline that has passed already.
const int64_t now = av_gettime_relative();

deadline_us_.store(timeout_us_ > INT64_MAX - now ? INT64_MAX : now + timeout_us_);
}

void MediaReader::EndBlocking()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
import java.io.IOException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Objects;

import dev.onvoid.webrtc.internal.Timeouts;

/**
* An opened media source, and what it contains.
Expand Down Expand Up @@ -86,21 +87,20 @@ public MediaReader(String source) throws IOException {
* @param source The path or {@code rtsp://} URL of the source to open.
* @param timeout How long opening, and later any single read, seek or
* close, may wait on the source. {@link Duration#ZERO}
* waits as long as it takes.
* waits as long as it takes; a positive timeout waits at
* least a microsecond.
*
* @throws IOException if the source cannot be opened in
* time, or holds nothing that can be
* played.
* @throws IllegalArgumentException if the timeout is negative.
*/
public MediaReader(String source, Duration timeout) throws IOException {
Objects.requireNonNull(timeout, "Timeout is null");

if (timeout.isNegative()) {
throw new IllegalArgumentException("Timeout is negative: " + timeout);
}
// Zero is the one timeout without limit: a positive one rounds up to a
// microsecond, and one too long to count is as long as can be counted.
Timeouts.requireNonNegative(timeout, "timeout");

handle = open(source, toMicros(timeout));
handle = open(source, Timeouts.toMicros(timeout));
}

/**
Expand Down Expand Up @@ -149,16 +149,6 @@ synchronized long detach() {
return detaching;
}

private static long toMicros(Duration duration) {
try {
return duration.toNanos() / 1000;
}
catch (ArithmeticException e) {
// Longer than nanoseconds can count, which is as good as never.
return 0;
}
}

private static native long open(String source, long timeoutUs) throws IOException;

private static native MediaInfo info(long handle);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -90,6 +91,17 @@ void readsDuration() throws Exception {
}
}

@Test
void opensWithLongestTimeout() throws Exception {
// Longer than microseconds can count. The deadline saturates, rather
// than wrap around into one that has passed already.
String source = asset().toAbsolutePath().toString();

try (MediaReader reader = new MediaReader(source, Duration.ofSeconds(Long.MAX_VALUE))) {
assertTrue(reader.getInfo().hasVideo());
}
}

@Test
void readsAviInfo() throws Exception {
try (MediaReader reader = new MediaReader(asset(AVI_ASSET))) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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.assertTimeoutPreemptively;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
Expand Down Expand Up @@ -126,6 +127,18 @@ void openTimesOut() throws Exception {
}
}

@Test
void shortTimeoutTimesOut() throws Exception {
// Under a microsecond, which rounded down would be zero, the timeout
// that waits as long as it takes.
try (TestRtspServer server = new TestRtspServer(TestRtspServer.Behavior.SILENT, 0)) {
IOException e = assertTimeoutPreemptively(Duration.ofSeconds(5), () -> assertThrows(IOException.class,
() -> new MediaReader(server.url(), Duration.ofNanos(1))));

assertTrue(e.getMessage().toLowerCase().contains("timed out"), e.getMessage());
}
}

@Test
void errorHidesCredentials() throws Exception {
try (TestRtspServer server = new TestRtspServer(TestRtspServer.Behavior.SILENT, 0)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

import dev.onvoid.webrtc.RTCSdpType;
import dev.onvoid.webrtc.RTCSessionDescription;
import dev.onvoid.webrtc.internal.Timeouts;

/**
* A WHIP or WHEP session: a peer whose offer and answer went over HTTP, and the
Expand Down Expand Up @@ -190,7 +191,7 @@ private static CompletableFuture<URI> negotiate(WebRtc rtc, Peer peer, URI endpo

ScheduledFuture<?> deadline = rtc.timer.schedule(() -> fail.accept(new WebRtcException(
"The session with " + endpoint + " was not connected within " + timeout)),
timeout.toMillis(), TimeUnit.MILLISECONDS);
Timeouts.toMillis(timeout), TimeUnit.MILLISECONDS);

abandoned.thenRun(() -> {
deadline.cancel(false);
Expand Down Expand Up @@ -356,8 +357,10 @@ private static HttpURLConnection open(URI uri, String method, String bearerToken
throws IOException {
HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
connection.setRequestMethod(method);
connection.setConnectTimeout((int) Math.min(Integer.MAX_VALUE, timeout.toMillis()));
connection.setReadTimeout((int) Math.min(Integer.MAX_VALUE, timeout.toMillis()));
// Never 0, which the connection takes as no timeout at all.
int millis = (int) Math.min(Integer.MAX_VALUE, Timeouts.toMillis(timeout));
connection.setConnectTimeout(millis);
connection.setReadTimeout(millis);
// A redirect would take the bearer token to wherever it points, so it
// fails the request instead.
connection.setInstanceFollowRedirects(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import dev.onvoid.webrtc.internal.Timeouts;
import dev.onvoid.webrtc.media.video.VideoDevice;
import dev.onvoid.webrtc.media.video.VideoTrack;

Expand Down Expand Up @@ -337,7 +338,7 @@ private CompletableFuture<Void> send(List<SenderImpl> senders) {
*/
private WebRtcException await(CompletableFuture<Void> sent, String failure) {
try {
sent.get(rtc.operationTimeout.toMillis(), TimeUnit.MILLISECONDS);
sent.get(Timeouts.toMillis(rtc.operationTimeout), TimeUnit.MILLISECONDS);

return null;
}
Expand Down
5 changes: 3 additions & 2 deletions webrtc/src/main/java/dev/onvoid/webrtc/client/PeerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import dev.onvoid.webrtc.RTCSignalingState;
import dev.onvoid.webrtc.SetSessionDescriptionObserver;
import dev.onvoid.webrtc.client.internal.EncodedFrames;
import dev.onvoid.webrtc.internal.Timeouts;
import dev.onvoid.webrtc.media.MediaStream;
import dev.onvoid.webrtc.media.MediaStreamTrack;
import dev.onvoid.webrtc.media.MediaType;
Expand Down Expand Up @@ -784,7 +785,7 @@ private ScheduledFuture<?> timeOut(CompletableFuture<?> operation) {
return rtc.timer.schedule(() -> control.execute(() ->
operation.completeExceptionally(new TimeoutException("WebRTC did not finish an operation of "
+ name + " within " + rtc.operationTimeout))),
rtc.operationTimeout.toMillis(), TimeUnit.MILLISECONDS);
Timeouts.toMillis(rtc.operationTimeout), TimeUnit.MILLISECONDS);
}
catch (RejectedExecutionException e) {
// The runtime is closing: control tasks queued before still run,
Expand Down Expand Up @@ -932,7 +933,7 @@ private CompletableFuture<Void> waitForGathering() {
if (gatheringWaiters.remove(future)) {
future.complete(null);
}
}), options.gatheringTimeout.toMillis(), TimeUnit.MILLISECONDS);
}), Timeouts.toMillis(options.gatheringTimeout), TimeUnit.MILLISECONDS);
}
catch (RejectedExecutionException e) {
// The runtime is closing and has no timer left to wait with.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import dev.onvoid.webrtc.RTCConfiguration;
import dev.onvoid.webrtc.RTCIceServer;
import dev.onvoid.webrtc.RTCIceTransportPolicy;
import dev.onvoid.webrtc.internal.Timeouts;

/**
* How {@link WebRtc#createPeer} sets up a peer.
Expand Down Expand Up @@ -254,13 +255,7 @@ public Builder receive(MediaKind... kinds) {
* @return this builder.
*/
public Builder gatheringTimeout(Duration timeout) {
requireNonNull(timeout, "timeout");

if (timeout.isNegative() || timeout.isZero()) {
throw new IllegalArgumentException("The timeout must be positive, got " + timeout);
}

this.gatheringTimeout = timeout;
this.gatheringTimeout = Timeouts.requirePositive(timeout, "gathering timeout");
return this;
}

Expand Down
21 changes: 6 additions & 15 deletions webrtc/src/main/java/dev/onvoid/webrtc/client/WebRtc.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import java.util.function.Supplier;

import dev.onvoid.webrtc.PeerConnectionFactory;
import dev.onvoid.webrtc.internal.Timeouts;
import dev.onvoid.webrtc.media.audio.AudioDeviceModule;
import dev.onvoid.webrtc.media.audio.AudioDeviceModuleBase;
import dev.onvoid.webrtc.media.audio.AudioProcessing;
Expand Down Expand Up @@ -300,7 +301,7 @@ public void close() {
CompletableFuture<Void> future = closeAsync();

try {
future.get(closeTimeout.toMillis(), TimeUnit.MILLISECONDS);
future.get(Timeouts.toMillis(closeTimeout), TimeUnit.MILLISECONDS);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
Expand Down Expand Up @@ -462,7 +463,7 @@ private void shutdown() {
*/
private void failPending() {
try {
controlPool.awaitTermination(closeTimeout.toMillis(), TimeUnit.MILLISECONDS);
controlPool.awaitTermination(Timeouts.toMillis(closeTimeout), TimeUnit.MILLISECONDS);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
Expand Down Expand Up @@ -492,7 +493,7 @@ private WebRtcException awaitAll(Supplier<List<CompletableFuture<Void>>> closes,

try {
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.get(closeTimeout.toMillis(), TimeUnit.MILLISECONDS);
.get(Timeouts.toMillis(closeTimeout), TimeUnit.MILLISECONDS);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
Expand Down Expand Up @@ -650,7 +651,7 @@ public Builder errorHandler(Consumer<Throwable> handler) {
* @return this builder.
*/
public Builder operationTimeout(Duration timeout) {
this.operationTimeout = positive(timeout);
this.operationTimeout = Timeouts.requirePositive(timeout, "operation timeout");
return this;
}

Expand All @@ -661,7 +662,7 @@ public Builder operationTimeout(Duration timeout) {
* @return this builder.
*/
public Builder closeTimeout(Duration timeout) {
this.closeTimeout = positive(timeout);
this.closeTimeout = Timeouts.requirePositive(timeout, "close timeout");
return this;
}

Expand Down Expand Up @@ -705,15 +706,5 @@ Builder timer(ScheduledExecutorService timer) {
public WebRtc build() {
return new WebRtc(this);
}

private static Duration positive(Duration duration) {
requireNonNull(duration, "duration");

if (duration.isNegative() || duration.isZero()) {
throw new IllegalArgumentException("The duration must be positive, got " + duration);
}

return duration;
}
}
}
10 changes: 3 additions & 7 deletions webrtc/src/main/java/dev/onvoid/webrtc/client/Whep.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import java.util.Set;
import java.util.concurrent.CompletableFuture;

import dev.onvoid.webrtc.internal.Timeouts;

/**
* Receives a stream from a server with the WebRTC HTTP Egress Protocol (WHEP),
* as media servers such as MediaMTX, Janus and Cloudflare Stream offer it:
Expand Down Expand Up @@ -136,13 +138,7 @@ public Builder receive(MediaKind... kinds) {
* @return this builder.
*/
public Builder timeout(Duration timeout) {
requireNonNull(timeout, "timeout");

if (timeout.isNegative() || timeout.isZero()) {
throw new IllegalArgumentException("The timeout must be positive, got " + timeout);
}

this.timeout = timeout;
this.timeout = Timeouts.requirePositive(timeout, "timeout");
return this;
}

Expand Down
10 changes: 3 additions & 7 deletions webrtc/src/main/java/dev/onvoid/webrtc/client/Whip.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import java.util.List;
import java.util.concurrent.CompletableFuture;

import dev.onvoid.webrtc.internal.Timeouts;

/**
* Publishes tracks to a server with the WebRTC HTTP Ingestion Protocol (WHIP,
* RFC 9725), as media servers and streaming services take it:
Expand Down Expand Up @@ -127,13 +129,7 @@ public Builder iceServer(String... urls) {
* @return this builder.
*/
public Builder timeout(Duration timeout) {
requireNonNull(timeout, "timeout");

if (timeout.isNegative() || timeout.isZero()) {
throw new IllegalArgumentException("The timeout must be positive, got " + timeout);
}

this.timeout = timeout;
this.timeout = Timeouts.requirePositive(timeout, "timeout");
return this;
}

Expand Down
Loading
Loading