Skip to content
Closed
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
794 changes: 794 additions & 0 deletions design/client-api.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/.vitepress/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
items: [
{ text: 'What is webrtc-java?', link: '/introduction' },
{ text: 'Getting Started', link: '/get-started' },
{ text: 'Client API', link: '/client-api' },
],
},
{
Expand Down
522 changes: 522 additions & 0 deletions docs/guide/client-api.md

Large diffs are not rendered by default.

32 changes: 26 additions & 6 deletions docs/guide/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

This section provides an overview of the example applications included in the `webrtc-examples` Maven module. These examples demonstrate various features and capabilities of the webrtc-java library.

## Client API

The [`ClientApiExample`](https://github.com/devopvoid/webrtc-java/blob/master/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/ClientApiExample.java) connects two peers in one process with the [Client API](/guide/client-api), and sends video, audio and chat messages from one to the other. It needs no audio or video hardware and no server.

**Key features demonstrated:**
- Creating a headless `WebRtc` runtime, which needs no microphone or speakers
- Connecting two peers through a signaling that carries JSON text, as a WebSocket would
- Letting perfect negotiation run the offer and answer exchange, with one polite peer
- Pushing video frames, and audio in pieces of any length
- Receiving remote tracks, their frames and their audio
- Exchanging messages on a data channel
- Reading the connection's statistics once a second
- Closing the peers, tracks and factory by closing the runtime

This example is the shortest way to see a complete call working, and a starting point for applications built on the client API.

```bash
mvn exec:java -D"exec.mainClass=dev.onvoid.webrtc.examples.ClientApiExample" -D"exec.args=10"
```

## PeerConnection

The [`PeerConnectionExample`](https://github.com/devopvoid/webrtc-java/blob/master/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/PeerConnectionExample.java) demonstrates how to set up a peer connection with audio and video tracks to be able to send and receive media.
Expand Down Expand Up @@ -116,15 +136,15 @@ mvn exec:java -D"exec.mainClass=dev.onvoid.webrtc.examples.EncryptedRecordingExa

## Web Client

The [`WebClientExample`](https://github.com/devopvoid/webrtc-java/blob/master/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/web/WebClientExample.java) demonstrates how to combine WebSocket signaling with WebRTC peer connections for real-time communication between web and Java clients.
The [`WebClientExample`](https://github.com/devopvoid/webrtc-java/blob/master/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/web/WebClientExample.java) calls a browser from Java over WebSocket signaling, with the [Client API](/guide/client-api). The browser client is served by the example's `WebServer`.

**Key features demonstrated:**
- Connecting to a signaling server using WebSockets
- Setting up audio and video media tracks
- Creating and managing WebRTC peer connections
- Handling signaling messages (offers, answers, ICE candidates)
- Joining and leaving rooms for multi-user communication
- Using custom audio sources for audio streaming
- Implementing a `Signaling` over the application's own message format, which also names the sender and receiver of each message
- Sending generated audio and video, pushed into tracks of the runtime
- Letting the peer negotiate by itself, and starting a new peer whenever the browser joins the room
- Joining and leaving rooms
- Logging the audio and video the browser sends

This example is valuable for applications that need to establish WebRTC connections between Java clients and web browsers, enabling cross-platform real-time communication with audio and video.

Expand Down
4 changes: 4 additions & 0 deletions docs/guide/get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ implementation group: "dev.onvoid.webrtc", name: "webrtc-java", version: "{{ VER

## Basic Setup

::: tip
This guide uses the core API, which mirrors WebRTC's own. The [Client API](/guide/client-api) builds on it: it negotiates by itself, keeps your handlers off WebRTC's threads, and closes everything it created with one call. Most applications are simpler with it.
:::

### Initialize the WebRTC Library

First, create a `PeerConnectionFactory` which is the entry point for most WebRTC operations:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
/*
* 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.examples;

import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import dev.onvoid.webrtc.client.AudioMode;
import dev.onvoid.webrtc.client.CustomAudio;
import dev.onvoid.webrtc.client.CustomVideo;
import dev.onvoid.webrtc.client.DataChannel;
import dev.onvoid.webrtc.client.PcmFormat;
import dev.onvoid.webrtc.client.Peer;
import dev.onvoid.webrtc.client.PeerOptions;
import dev.onvoid.webrtc.client.SignalMessage;
import dev.onvoid.webrtc.client.VideoStats;
import dev.onvoid.webrtc.client.WebRtc;
import dev.onvoid.webrtc.media.video.NativeI420Buffer;
import dev.onvoid.webrtc.media.video.VideoFrame;

/**
* Example of the high-level client API ({@code dev.onvoid.webrtc.client}): two peers in one
* process send each other video, audio and chat messages.
* <p>
* This example shows how to:
* <ul>
* <li>Create a {@link WebRtc} runtime that needs no audio hardware</li>
* <li>Connect two peers through a signaling that carries JSON text, as a WebSocket would</li>
* <li>Let perfect negotiation run the offer and answer exchange, with one polite peer</li>
* <li>Send pushed video frames and pushed audio of any length</li>
* <li>Receive remote tracks, their frames and their audio</li>
* <li>Exchange messages on a data channel</li>
* <li>Read the connection's statistics once a second</li>
* </ul>
* <p>
* No observer, no {@code dispose()} and no thread of its own is needed: handlers run on the event
* thread of their peer, and closing the runtime closes everything it created.
* <p>
* Run it for a number of seconds, five by default:
* <pre>
* mvn exec:java -D"exec.mainClass=dev.onvoid.webrtc.examples.ClientApiExample" -D"exec.args=10"
* </pre>
*
* @author Alex Andres
*/
public class ClientApiExample {

private static final int WIDTH = 640;

private static final int HEIGHT = 360;

private static final PcmFormat AUDIO_FORMAT = PcmFormat.of(48000, 1);


public static void main(String[] args) throws Exception {
int seconds = args.length > 0 ? Integer.parseInt(args[0]) : 5;

// A headless runtime sends pushed audio and needs no microphone or speakers.
try (WebRtc rtc = WebRtc.builder().audio(AudioMode.HEADLESS).build()) {
AtomicReference<Peer> callerRef = new AtomicReference<>();
AtomicReference<Peer> calleeRef = new AtomicReference<>();

// Each signaling turns messages into JSON text and hands that text to the other peer.
// A real application sends it through its WebSocket instead.
Peer caller = rtc.createPeer(PeerOptions.builder()
.name("caller")
.signaling(message -> transmit("caller", calleeRef.get(), message.toJson()))
.build());
Peer callee = rtc.createPeer(PeerOptions.builder()
.name("callee")
.polite(true)
.signaling(message -> transmit("callee", callerRef.get(), message.toJson()))
.build());

callerRef.set(caller);
calleeRef.set(callee);

receive(callee);
caller.onStateChange(state -> System.out.println("Caller is " + state));

// Pushed media. The same tracks could be sent on any number of peers.
CustomVideo video = rtc.media().customVideo("pattern");
CustomAudio audio = rtc.media().customAudio("tone");
caller.addTrack(video);
caller.addTrack(audio);

// Sent once the channel opens.
DataChannel chat = caller.createChannel("chat");
chat.onMessage(message -> System.out.println("Caller got \"" + message.text() + "\""));
chat.send("Hello from the caller");

caller.onStats(Duration.ofSeconds(1), stats -> {
if (stats.candidateType() == null) {
// Not connected yet.
return;
}

String size = stats.outboundVideo().isEmpty() ? "-" : describe(stats.outboundVideo().get(0));

System.out.printf("Caller sends %d kbit/s over a %s candidate, video %s%n",
Math.max(0, stats.outgoingBitrate()) / 1000, stats.candidateType(), size);
});

ScheduledExecutorService feeder = Executors.newSingleThreadScheduledExecutor();

try {
feed(feeder, video, audio);

// The main thread may wait; a handler must not.
caller.whenConnected().get(10, TimeUnit.SECONDS);

Thread.sleep(seconds * 1000L);
}
finally {
feeder.shutdownNow();
}
}
// The runtime closed the peers, the tracks and the factory, in that order.

System.out.println("Closed");
}

/**
* Prints what the callee receives.
*/
private static void receive(Peer callee) {
AtomicInteger frames = new AtomicInteger();
AtomicLong audioMillis = new AtomicLong();

callee.onTrack(track -> {
String kind = track.kind().name().toLowerCase();

System.out.println("Callee receives the " + kind + " track in stream " + track.streamIds());

if (track.isVideo()) {
// A slow handler gets the newest frame next; frames in between are dropped.
track.asVideo().onFrame(frame -> {
if (frames.incrementAndGet() % 30 == 1) {
System.out.printf("Callee got video frame %d, %dx%d%n", frames.get(),
frame.buffer.getWidth(), frame.buffer.getHeight());
}
});
}
else {
track.asAudio().onAudio(chunk -> {
long millis = audioMillis.addAndGet(chunk.frames() * 1000L / chunk.sampleRate());

if (millis % 1000 == 0) {
System.out.printf("Callee got %d ms of audio%n", millis);
}
});
}

track.onEnded(() -> System.out.println("Callee's " + kind + " track ended"));
});

// Channels the caller opens. Messages that arrive before onMessage are kept for it.
callee.onChannel(channel -> channel.onMessage(message -> {
System.out.println("Callee got \"" + message.text() + "\"");
channel.send("Echo: " + message.text());
}));
}

/**
* Stands in for a WebSocket: carries a message as text and reads it on the other side.
*/
private static void transmit(String from, Peer to, String json) {
SignalMessage message = SignalMessage.fromJson(json);

if (message.type() != SignalMessage.Type.CANDIDATE) {
System.out.printf("%s sends an %s of %d characters%n", from,
message.type().name().toLowerCase(), json.length());
}

to.signal(message);
}

/**
* Pushes a moving gradient at 30 frames per second, and a 440 Hz tone a second at a time.
*/
private static void feed(ScheduledExecutorService feeder, CustomVideo video, CustomAudio audio) {
AtomicInteger shade = new AtomicInteger();

feeder.scheduleAtFixedRate(() -> {
NativeI420Buffer buffer = NativeI420Buffer.allocate(WIDTH, HEIGHT);
ByteBuffer y = buffer.getDataY();
int offset = shade.addAndGet(2);

for (int i = 0; i < y.capacity(); i++) {
y.put(i, (byte) (i % WIDTH + offset));
}

VideoFrame frame = new VideoFrame(buffer, System.nanoTime());
video.push(frame);
frame.release();
}, 0, 33, TimeUnit.MILLISECONDS);

// Audio may be pushed in pieces of any length; the track sends it in 10 ms chunks.
byte[] second = tone(AUDIO_FORMAT.sampleRate());

feeder.scheduleAtFixedRate(() -> audio.push(second, AUDIO_FORMAT), 0, 1, TimeUnit.SECONDS);
}

private static byte[] tone(int sampleRate) {
byte[] pcm = new byte[sampleRate * 2];

for (int i = 0; i < sampleRate; i++) {
short sample = (short) (Math.sin(2 * Math.PI * 440 * i / sampleRate) * 6000);

pcm[2 * i] = (byte) sample;
pcm[2 * i + 1] = (byte) (sample >> 8);
}

return pcm;
}

private static String describe(VideoStats video) {
return String.format("%s %dx%d at %.0f fps", video.codec(), video.width(), video.height(),
video.framesPerSecond());
}
}
Loading
Loading