diff --git a/design/client-api.md b/design/client-api.md index 1665aa10..19130c64 100644 --- a/design/client-api.md +++ b/design/client-api.md @@ -815,4 +815,7 @@ Next: `Whep.from(...).play(rtc)` return sessions over `HttpURLConnection` (Java 8), and `VideoView` is in `dev.onvoid.webrtc.client.swing`; - porting the existing examples (phases 2 and 3); `MediaFilePlayerExample` is ported, with a - Record button, and `LocalTrack.closeAsync()` came with it. + Record button, and `LocalTrack.closeAsync()` came with it. `PeerConnectionExample`, + `DesktopVideoExample` and `MediaFileExample` are ported too, each over a loopback signaling. + `CodecListExample`, `EncryptedRecordingExample` and `VoiceActivityExample` stay on the core API, + which they demonstrate: the factory's codecs, encoded frame transforms and voice activity. diff --git a/docs/guide/examples.md b/docs/guide/examples.md index 37777876..2dacb935 100644 --- a/docs/guide/examples.md +++ b/docs/guide/examples.md @@ -24,16 +24,21 @@ mvn exec:java -D"exec.mainClass=dev.onvoid.webrtc.examples.ClientApiExample" -D" ## 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. +The [`PeerConnectionExample`](https://github.com/devopvoid/webrtc-java/blob/master/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/PeerConnectionExample.java) sends the camera and the microphone from one peer to another in the same process, with the [Client API](/guide/client-api), and logs the video frames and audio that arrive. **Key features demonstrated:** -- Creating a `PeerConnectionFactory` -- Creating audio and video tracks -- Setting up a peer connection -- Adding tracks to the peer connection for sending media -- Implementing callbacks to receive incoming audio and video frames +- Creating a `WebRtc` runtime that captures from and plays to the audio devices +- Listing the cameras and microphones +- Opening the camera and the microphone as local tracks +- Connecting two peers through a loopback signaling, in place of a server +- Receiving the remote tracks, their frames and their audio +- Muting a received audio track, while its audio still reaches a handler -This example provides a foundation for building WebRTC applications that need to handle audio and video communication. +This example provides a foundation for building WebRTC applications that need to handle audio and video communication. Press Enter to end the call: + +```bash +mvn exec:java -D"exec.mainClass=dev.onvoid.webrtc.examples.PeerConnectionExample" +``` ## WHEP @@ -64,17 +69,20 @@ This example is useful for understanding what codecs are available on the curren ## Desktop Video -The [`DesktopVideoExample`](https://github.com/devopvoid/webrtc-java/blob/master/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/DesktopVideoExample.java) demonstrates how to set up a peer connection with a desktop video source for screen or window capture. +The [`DesktopVideoExample`](https://github.com/devopvoid/webrtc-java/blob/master/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/DesktopVideoExample.java) shares a screen or a window from one peer to another in the same process, with the [Client API](/guide/client-api). **Key features demonstrated:** -- Creating a `PeerConnectionFactory` -- Getting available desktop sources (screens and windows) -- Creating a `VideoDesktopSource` for capturing screen or window content -- Configuring the `VideoDesktopSource` properties -- Creating a video track with the desktop source -- Setting up a peer connection +- Listing the screens and windows that can be captured +- Capturing one of them as a local video track, with a frame rate and a largest size +- Connecting two peers through a loopback signaling, in place of a server +- Reading the resolution and frame rate the sender encodes -This example is particularly useful for applications that need to implement screen sharing or remote desktop functionality. +This example is particularly useful for applications that need to implement screen sharing or remote desktop functionality. It shares the first screen, or the first window with `window`. Under Wayland, the system asks which one to share instead. + +```bash +mvn exec:java -D"exec.mainClass=dev.onvoid.webrtc.examples.DesktopVideoExample" +mvn exec:java -D"exec.mainClass=dev.onvoid.webrtc.examples.DesktopVideoExample" -D"exec.args=window" +``` ## Media File @@ -83,9 +91,10 @@ The [`MediaFileExample`](https://github.com/devopvoid/webrtc-java/blob/master/we **Key features demonstrated:** - Opening a media file, or a live RTSP stream, with a `MediaFileSource` - Reading what the source contains from its `MediaInfo` -- Creating audio and video tracks from the media sources it feeds -- Adding those tracks to a peer connection +- Sending the media sources it feeds as [Client API](/guide/client-api) tracks, with `videoTrack` and `audioTrack` +- Receiving them on a second peer, connected through a loopback signaling - Following playback through a `MediaPlayerListener` +- Closing the tracks before the source, which owns what they were made from This example is useful for applications that stream pre-recorded media, relay an IP camera into WebRTC, or need a dependable stand-in for a camera in testing. diff --git a/docs/guide/media/media-files.md b/docs/guide/media/media-files.md index c26946cf..68cc4d5c 100644 --- a/docs/guide/media/media-files.md +++ b/docs/guide/media/media-files.md @@ -288,7 +288,7 @@ Some of the formats FFmpeg decodes, such as H.264, H.265/HEVC and AAC, may be co ## Complete Example -See `MediaFileExample` in the `webrtc-examples` module, which opens a file, reports what it contains, creates tracks, adds them to a peer connection and follows playback to the end. +See `MediaFileExample` in the `webrtc-examples` module, which opens a file, reports what it contains, sends its tracks from one [Client API](/guide/client-api) peer to another and follows playback to the end. ```shell mvn -pl webrtc-examples compile diff --git a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/DesktopVideoExample.java b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/DesktopVideoExample.java index 0fad48c0..01e580a2 100644 --- a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/DesktopVideoExample.java +++ b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/DesktopVideoExample.java @@ -16,255 +16,138 @@ package dev.onvoid.webrtc.examples; -import java.util.ArrayList; +import java.time.Duration; import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; - -import dev.onvoid.webrtc.PeerConnectionFactory; -import dev.onvoid.webrtc.PeerConnectionObserver; -import dev.onvoid.webrtc.RTCConfiguration; -import dev.onvoid.webrtc.RTCDataChannel; -import dev.onvoid.webrtc.RTCIceCandidate; -import dev.onvoid.webrtc.RTCIceConnectionState; -import dev.onvoid.webrtc.RTCIceGatheringState; -import dev.onvoid.webrtc.RTCIceServer; -import dev.onvoid.webrtc.RTCPeerConnection; -import dev.onvoid.webrtc.RTCPeerConnectionState; -import dev.onvoid.webrtc.RTCRtpReceiver; -import dev.onvoid.webrtc.RTCRtpSender; -import dev.onvoid.webrtc.RTCRtpTransceiver; -import dev.onvoid.webrtc.RTCSignalingState; -import dev.onvoid.webrtc.media.MediaStream; -import dev.onvoid.webrtc.media.MediaStreamTrack; -import dev.onvoid.webrtc.media.video.VideoDesktopSource; -import dev.onvoid.webrtc.media.video.VideoTrack; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import dev.onvoid.webrtc.client.AudioMode; +import dev.onvoid.webrtc.client.Devices; +import dev.onvoid.webrtc.client.LocalVideo; +import dev.onvoid.webrtc.client.Peer; +import dev.onvoid.webrtc.client.PeerOptions; +import dev.onvoid.webrtc.client.ScreenOptions; +import dev.onvoid.webrtc.client.Signaling; +import dev.onvoid.webrtc.client.SignalingPair; +import dev.onvoid.webrtc.client.VideoStats; +import dev.onvoid.webrtc.client.WebRtc; import dev.onvoid.webrtc.media.video.desktop.DesktopSource; -import dev.onvoid.webrtc.media.video.desktop.ScreenCapturer; -import dev.onvoid.webrtc.media.video.desktop.WindowCapturer; /** - * Example demonstrating how to set up a peer connection with a desktop video source. + * Example demonstrating how to share a screen or a window over a peer connection, with the + * client API. *
* This example shows how to: *
- * Note: This example focuses only on setting up the local peer connection with - * a desktop video source for bidirectional media transfer. In a real application, - * you would need to establish a connection with a remote peer through a signaling - * channel (e.g., WebSocket). + * Both peers run in this process, so the call connects without a server. In a real + * application each peer is in another process, and its {@link Signaling} carries the messages + * over a WebSocket or the like. + *
+ * Under Wayland, the system asks which screen or window to share once the capture starts, and + * each list holds a single source without a title that stands for that choice. + *
+ * Run it to share the first screen, or with {@code window} to share the first window, and press + * Enter to stop: + *
+ * mvn exec:java -D"exec.mainClass=dev.onvoid.webrtc.examples.DesktopVideoExample" + * mvn exec:java -D"exec.mainClass=dev.onvoid.webrtc.examples.DesktopVideoExample" -D"exec.args=window" + ** * @author Alex Andres */ public class DesktopVideoExample { - public static void main(String[] args) { - // Create a PeerConnectionFactory, which is the main entry point for WebRTC. - PeerConnectionFactory factory = new PeerConnectionFactory(); - - try { - LocalPeer localPeer = new LocalPeer(factory); - - // Keep the application running to observe state changes. - System.out.println("Press Enter to exit..."); - System.in.read(); - - // Clean up. - localPeer.dispose(); - } - catch (Exception e) { - Logger.getLogger(DesktopVideoExample.class.getName()) - .log(Level.SEVERE, "Error in DesktopVideoExample", e); - } - finally { - // Dispose the factory when done. - factory.dispose(); - } - } - - /** - * Represents a peer connection with audio and desktop video tracks. - */ - private static class LocalPeer implements PeerConnectionObserver { - - private final RTCPeerConnection peerConnection; - private final VideoDesktopSource videoSource; - private final RTCRtpSender videoSender; - - - public LocalPeer(PeerConnectionFactory factory) { - // Create a basic configuration for the peer connection. - RTCConfiguration config = new RTCConfiguration(); - - // Add a STUN server to help with NAT traversal. - RTCIceServer iceServer = new RTCIceServer(); - iceServer.urls.add("stun:stun.l.google.com:19302"); - config.iceServers.add(iceServer); - - // Create the peer connection. - peerConnection = factory.createPeerConnection(config, this); - - // Get available desktop sources. - System.out.println("Getting available desktop sources..."); - - // Get available screens. - ScreenCapturer screenCapturer = new ScreenCapturer(); - List
* This example shows how to: *
* Decoding happens entirely in native code. Frames are paced in real time and * carry the presentation times of the file, so what a receiver gets keeps the * timing of the media rather than the timing of a Java thread. *
- * Note: this example only sets up the local side. A real application would - * reach a remote peer through a signaling channel, as - * {@link PeerConnectionExample} shows. + * Both peers run in this process. A real application reaches its remote peer + * through a {@link Signaling} that carries the messages over a WebSocket or + * the like. {@link MediaFilePlayerExample} also shows the received video and + * plays the received audio. *
* Run it with the media file or stream URL to send: *
@@ -73,7 +71,7 @@
*/
public class MediaFileExample {
- public static void main(String[] args) {
+ public static void main(String[] args) throws Exception {
if (args.length < 1) {
System.out.println("Usage: MediaFileExample ");
System.out.println(" for example: MediaFileExample movie.mp4");
@@ -81,66 +79,104 @@ public static void main(String[] args) {
return;
}
- // A factory that sends pushed audio must not also be capturing from a
- // microphone, so this one is given a dummy audio layer. An application
- // that needs both needs a second factory.
- AudioDeviceModule audioModule = new AudioDeviceModule(AudioLayer.kDummyAudio);
- PeerConnectionFactory factory = new PeerConnectionFactory(audioModule);
+ // The file's audio is pushed, so the runtime must not capture from a
+ // microphone, and a headless one needs no audio devices at all. The
+ // runtime goes first: it loads the native library the source needs.
+ try (WebRtc rtc = WebRtc.builder().audio(AudioMode.HEADLESS).build()) {
+ // Opening reads the container and picks the streams to play. A
+ // source with nothing playable in it fails here.
+ try (MediaFileSource source = new MediaFileSource(args[0])) {
+ printInfo(source.getInfo());
+
+ SignalingPair signaling = Signaling.loopback();
+
+ Peer sender = rtc.createPeer(PeerOptions.builder()
+ .name("sender")
+ .signaling(signaling.first())
+ .build());
+ Peer receiver = rtc.createPeer(PeerOptions.builder()
+ .name("receiver")
+ .polite(true)
+ .signaling(signaling.second())
+ .build());
+
+ AtomicInteger videoFrames = new AtomicInteger();
+ AtomicLong audioMillis = new AtomicLong();
+
+ receiver.onTrack(track -> {
+ System.out.println("Receiving " + track.kind());
+
+ if (track.isVideo()) {
+ track.asVideo().onFrame(frame -> videoFrames.incrementAndGet());
+ }
+ else {
+ track.asAudio().onAudio(chunk ->
+ audioMillis.addAndGet(chunk.frames() * 1000L / chunk.sampleRate()));
+ }
+ });
+
+ // A source with no video has no video source, and likewise
+ // for audio, so each track is only made if there is something
+ // to feed it. The sources stay the file source's.
+ List tracks = new ArrayList<>();
+
+ if (source.getVideoSource() != null) {
+ tracks.add(rtc.media().videoTrack("video", source.getVideoSource()));
+ }
+ if (source.getAudioSource() != null) {
+ tracks.add(rtc.media().audioTrack("audio", source.getAudioSource()));
+ }
- // Opening reads the container and picks the streams to play. A source
- // with nothing playable in it fails here.
- try (MediaFileSource source = new MediaFileSource(args[0])) {
- printInfo(source.getInfo());
+ // Both tracks go into one stream, so a receiver plays them in sync.
+ tracks.forEach(sender::addTrack);
- LocalPeer localPeer = new LocalPeer(factory, source);
+ // The main thread may wait; a handler must not.
+ sender.whenConnected().get(15, TimeUnit.SECONDS);
- CountDownLatch finished = new CountDownLatch(1);
+ CountDownLatch finished = new CountDownLatch(1);
- source.setListener(new MediaPlayerListener() {
+ source.setListener(new MediaPlayerListener() {
- @Override
- public void onStateChanged(MediaPlayerState state) {
- System.out.println("Player state: " + state);
- }
+ @Override
+ public void onStateChanged(MediaPlayerState state) {
+ System.out.println("Player state: " + state);
+ }
- @Override
- public void onEndOfStream() {
- System.out.println("Reached the end of the source.");
- finished.countDown();
- }
+ @Override
+ public void onEndOfStream() {
+ System.out.println("Reached the end of the source.");
+ finished.countDown();
+ }
- @Override
- public void onError(String message) {
- System.out.println("Playback failed: " + message);
- finished.countDown();
- }
- });
+ @Override
+ public void onError(String message) {
+ System.out.println("Playback failed: " + message);
+ finished.countDown();
+ }
+ });
- // Playing the source over and over is one line, and is what a
- // stand-in for a camera usually wants:
- // source.setLooping(true);
+ // Playing the source over and over is one line, and is what a
+ // stand-in for a camera usually wants:
+ // source.setLooping(true);
- source.play();
+ source.play();
- // Report where playback has got to while it runs.
- while (!finished.await(1, TimeUnit.SECONDS)) {
- System.out.printf(" at %.1f s, %d video frames and %d audio chunks sent%n",
- source.getPositionUs() / 1_000_000.0,
- localPeer.videoFrames.get(), localPeer.audioChunks.get());
- }
+ // Report where playback has got to while it runs.
+ while (!finished.await(1, TimeUnit.SECONDS)) {
+ System.out.printf(" at %.1f s, %d video frames and %.1f s of audio received%n",
+ source.getPositionUs() / 1_000_000.0, videoFrames.get(),
+ audioMillis.get() / 1000.0);
+ }
- // The tracks go before the source, which disposes of the media
- // sources they were made from.
- localPeer.dispose();
- }
- catch (Exception e) {
- Logger.getLogger(MediaFileExample.class.getName())
- .log(Level.SEVERE, "Error in MediaFileExample", e);
- }
- finally {
- factory.dispose();
- audioModule.dispose();
+ // The tracks go before the source, which disposes of the media
+ // sources they were made from. Waiting for them is what makes
+ // closing the source safe.
+ for (LocalTrack track : tracks) {
+ track.closeAsync().get(5, TimeUnit.SECONDS);
+ }
+ }
}
+ // Closing the runtime closed the peers.
}
private static void printInfo(MediaInfo info) {
@@ -168,79 +204,4 @@ private static void printInfo(MediaInfo info) {
System.out.println(" audio: none");
}
}
-
- /**
- * A peer connection carrying the tracks a media file feeds.
- */
- private static class LocalPeer implements PeerConnectionObserver {
-
- final AtomicInteger videoFrames = new AtomicInteger();
- final AtomicInteger audioChunks = new AtomicInteger();
-
- private final RTCPeerConnection peerConnection;
- private final VideoTrack videoTrack;
- private final AudioTrack audioTrack;
- private final RTCRtpSender videoSender;
- private final RTCRtpSender audioSender;
-
-
- LocalPeer(PeerConnectionFactory factory, MediaFileSource source) {
- peerConnection = factory.createPeerConnection(new RTCConfiguration(), this);
-
- // A source with no video has no video source, and likewise for
- // audio, so each track is only made if there is something to feed
- // it.
- if (source.getVideoSource() != null) {
- videoTrack = factory.createVideoTrack("video", source.getVideoSource());
-
- // Watching the track shows what the peer connection is being
- // given. A real application would not need this.
- videoTrack.addSink(frame -> videoFrames.incrementAndGet());
-
- videoSender = peerConnection.addTrack(videoTrack, List.of("stream"));
- }
- else {
- videoTrack = null;
- videoSender = null;
- }
-
- if (source.getAudioSource() != null) {
- audioTrack = factory.createAudioTrack("audio", source.getAudioSource());
- audioTrack.addSink((data, bits, rate, channels, frames) ->
- audioChunks.incrementAndGet());
-
- audioSender = peerConnection.addTrack(audioTrack, List.of("stream"));
- }
- else {
- audioTrack = null;
- audioSender = null;
- }
- }
-
- void dispose() {
- // An RTCRtpSender is not owned by the peer connection, so each one
- // handed out by addTrack has to be released here. Doing it before
- // the connection closes is what lets go of the tracks, and with
- // them the media sources the file source owns.
- if (videoSender != null) {
- videoSender.dispose();
- }
- if (audioSender != null) {
- audioSender.dispose();
- }
-
- peerConnection.close();
- }
-
- @Override
- public void onIceCandidate(RTCIceCandidate candidate) {
- // A real application sends this to the remote peer over its
- // signaling channel.
- }
-
- @Override
- public void onIceConnectionChange(RTCIceConnectionState state) {
- System.out.println("ICE connection state: " + state);
- }
- }
}
diff --git a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/PeerConnectionExample.java b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/PeerConnectionExample.java
index ee7679de..e9133f4e 100644
--- a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/PeerConnectionExample.java
+++ b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/PeerConnectionExample.java
@@ -16,298 +16,170 @@
package dev.onvoid.webrtc.examples;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import dev.onvoid.webrtc.PeerConnectionFactory;
-import dev.onvoid.webrtc.PeerConnectionObserver;
-import dev.onvoid.webrtc.RTCConfiguration;
-import dev.onvoid.webrtc.RTCDataChannel;
-import dev.onvoid.webrtc.RTCIceCandidate;
-import dev.onvoid.webrtc.RTCIceConnectionState;
-import dev.onvoid.webrtc.RTCIceGatheringState;
-import dev.onvoid.webrtc.RTCIceServer;
-import dev.onvoid.webrtc.RTCPeerConnection;
-import dev.onvoid.webrtc.RTCPeerConnectionState;
-import dev.onvoid.webrtc.RTCRtpReceiver;
-import dev.onvoid.webrtc.RTCRtpSender;
-import dev.onvoid.webrtc.RTCRtpTransceiver;
-import dev.onvoid.webrtc.RTCSignalingState;
-import dev.onvoid.webrtc.media.MediaStream;
-import dev.onvoid.webrtc.media.MediaStreamTrack;
-import dev.onvoid.webrtc.media.audio.AudioOptions;
-import dev.onvoid.webrtc.media.audio.AudioTrack;
-import dev.onvoid.webrtc.media.audio.AudioTrackSink;
-import dev.onvoid.webrtc.media.audio.AudioTrackSource;
-import dev.onvoid.webrtc.media.video.VideoDeviceSource;
-import dev.onvoid.webrtc.media.video.VideoFrame;
-import dev.onvoid.webrtc.media.video.VideoTrack;
-import dev.onvoid.webrtc.media.video.VideoTrackSink;
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+import dev.onvoid.webrtc.client.Devices;
+import dev.onvoid.webrtc.client.LocalAudio;
+import dev.onvoid.webrtc.client.LocalCamera;
+import dev.onvoid.webrtc.client.MediaException;
+import dev.onvoid.webrtc.client.Peer;
+import dev.onvoid.webrtc.client.PeerOptions;
+import dev.onvoid.webrtc.client.RemoteTrack;
+import dev.onvoid.webrtc.client.Signaling;
+import dev.onvoid.webrtc.client.SignalingPair;
+import dev.onvoid.webrtc.client.WebRtc;
/**
- * Example demonstrating how to set up a peer connection with audio and video tracks
- * to be able to send and receive media.
+ * Example demonstrating how to send a camera and a microphone over a peer connection, and how
+ * to receive the video frames and audio on the other side, with the client API.
*
* This example shows how to:
*
- * - Create a PeerConnectionFactory
- * - Create audio and video tracks
- * - Set up a peer connection
- * - Add tracks to the peer connection for sending media
- * - Implement callbacks to receive incoming audio and video frames
+ * - Create a {@link WebRtc} runtime that captures from and plays to the audio devices
+ * - List the cameras and microphones
+ * - Open the camera and the microphone as local tracks
+ * - Connect two peers through a loopback signaling, in place of a server
+ * - Send the tracks, and receive them as remote tracks with their frames and audio
+ * - Mute a received audio track, while its audio still reaches a handler
*
*
- * Note: This example focuses only on setting up the local peer connection with
- * audio and video tracks for bidirectional media transfer. In a real application,
- * you would need to establish a connection with a remote peer through a signaling
- * channel (e.g., WebSocket).
+ * Both peers run in this process, so the call connects without a server. In a real
+ * application each peer is in another process, and its {@link Signaling} carries the messages
+ * over a WebSocket or the like, as the web example does. Nothing else changes.
+ *
+ * Run it, and press Enter to end the call:
+ *
+ * mvn exec:java -D"exec.mainClass=dev.onvoid.webrtc.examples.PeerConnectionExample"
+ *
*
* @author Alex Andres
*/
public class PeerConnectionExample {
- public static void main(String[] args) {
- // Create a PeerConnectionFactory, which is the main entry point for WebRTC.
- PeerConnectionFactory factory = new PeerConnectionFactory();
-
- try {
- LocalPeer localPeer = new LocalPeer(factory);
-
- // Keep the application running to observe state changes.
- System.out.println("Press Enter to exit...");
- System.in.read();
-
- // Clean up.
- localPeer.dispose();
- }
- catch (Exception e) {
- Logger.getLogger(PeerConnectionExample.class.getName())
- .log(Level.SEVERE, "Error in PeerConnectionExample", e);
- }
- finally {
- // Dispose the factory when done
- factory.dispose();
- }
- }
-
- /**
- * Represents a peer connection with audio and video tracks.
- */
- private static class LocalPeer implements PeerConnectionObserver {
-
- private final RTCPeerConnection peerConnection;
- private final AudioTrackSource audioSource;
- private final AudioTrack audioTrack;
- private final VideoTrack videoTrack;
- private final RTCRtpSender audioSender;
- private final RTCRtpSender videoSender;
- private final AudioFrameLogger audioFrameLogger = new AudioFrameLogger();
- private final VideoFrameLogger videoFrameLogger = new VideoFrameLogger();
-
-
- public LocalPeer(PeerConnectionFactory factory) {
- // Create a basic configuration for the peer connection.
- RTCConfiguration config = new RTCConfiguration();
-
- // Add a STUN server to help with NAT traversal.
- RTCIceServer iceServer = new RTCIceServer();
- iceServer.urls.add("stun:stun.l.google.com:19302");
- config.iceServers.add(iceServer);
-
- // Create the peer connection.
- peerConnection = factory.createPeerConnection(config, this);
-
- // Create an audio source with options.
- AudioOptions audioOptions = new AudioOptions();
- audioOptions.echoCancellation = true;
- audioOptions.autoGainControl = true;
- audioOptions.noiseSuppression = true;
-
- audioSource = factory.createAudioSource(audioOptions);
- audioTrack = factory.createAudioTrack("audio0", audioSource);
-
- VideoDeviceSource videoSource = new VideoDeviceSource();
- videoTrack = factory.createVideoTrack("video0", videoSource);
-
- // Add the tracks to the peer connection.
- List streamIds = new ArrayList<>();
- streamIds.add("stream1");
- audioSender = peerConnection.addTrack(audioTrack, streamIds);
- videoSender = peerConnection.addTrack(videoTrack, streamIds);
-
- System.out.println("LocalPeer: Created with audio and video tracks");
- }
-
- /**
- * Closes the peer connection and releases resources.
- */
- public void dispose() {
- if (audioTrack != null) {
- audioTrack.removeSink(audioFrameLogger);
- }
- if (videoTrack != null) {
- videoTrack.removeSink(videoFrameLogger);
+ public static void main(String[] args) throws Exception {
+ // The runtime captures from the microphone and plays to the speakers selected in its
+ // devices.
+ try (WebRtc rtc = WebRtc.create()) {
+ Devices devices = rtc.media().devices();
+
+ devices.cameras().forEach(camera -> System.out.println("Camera: " + camera.getName()));
+ devices.microphones().forEach(mic -> System.out.println("Microphone: " + mic.getName()));
+
+ // The two ends of a loopback signaling hand each message to the other peer.
+ SignalingPair signaling = Signaling.loopback();
+
+ Peer caller = rtc.createPeer(PeerOptions.builder()
+ .name("caller")
+ .iceServer("stun:stun.l.google.com:19302")
+ .signaling(signaling.first())
+ .build());
+ Peer callee = rtc.createPeer(PeerOptions.builder()
+ .name("callee")
+ .iceServer("stun:stun.l.google.com:19302")
+ .polite(true)
+ .signaling(signaling.second())
+ .build());
+
+ caller.onStateChange(state -> System.out.println("Caller is " + state));
+ callee.onTrack(PeerConnectionExample::receive);
+
+ boolean sending = false;
+
+ if (devices.cameras().isEmpty()) {
+ System.out.println("No camera, sending no video");
}
- // RTCRtpSender instances are not owned by the peer connection; the
- // application must dispose them once they are no longer needed.
- if (audioSender != null) {
- audioSender.dispose();
+ else {
+ try {
+ LocalCamera camera = rtc.media().camera();
+ caller.addTrack(camera);
+ sending = true;
+
+ System.out.println("Sending the camera " + camera.device().getName());
+ }
+ catch (MediaException e) {
+ System.out.println("The camera could not be opened: " + e.getMessage());
+ }
}
- if (videoSender != null) {
- videoSender.dispose();
- }
- if (peerConnection != null) {
- peerConnection.close();
- }
- // AudioTrackSource is ref-counted and not owned by the audio
- // track; the application must dispose it once no longer needed.
- if (audioSource != null) {
- audioSource.dispose();
- }
- }
-
- // PeerConnectionObserver implementation.
-
- @Override
- public void onIceCandidate(RTCIceCandidate candidate) {
- System.out.println("LocalPeer: New ICE candidate: " + candidate.sdp);
- // In a real application, you would send this candidate to the remote peer
- // through your signaling channel.
- }
-
- @Override
- public void onConnectionChange(RTCPeerConnectionState state) {
- System.out.println("LocalPeer: Connection state changed to: " + state);
- }
-
- @Override
- public void onIceConnectionChange(RTCIceConnectionState state) {
- System.out.println("LocalPeer: ICE connection state changed to: " + state);
- }
-
- @Override
- public void onIceGatheringChange(RTCIceGatheringState state) {
- System.out.println("LocalPeer: ICE gathering state changed to: " + state);
- }
- @Override
- public void onSignalingChange(RTCSignalingState state) {
- System.out.println("LocalPeer: Signaling state changed to: " + state);
- }
-
- @Override
- public void onDataChannel(RTCDataChannel dataChannel) {
- System.out.println("LocalPeer: Data channel created: " + dataChannel.getLabel());
- }
+ if (devices.microphones().isEmpty()) {
+ System.out.println("No microphone, sending no audio");
+ }
+ else {
+ // Echo cancellation, noise suppression and gain control are on.
+ LocalAudio microphone = rtc.media().microphone();
+ caller.addTrack(microphone);
+ sending = true;
- @Override
- public void onRenegotiationNeeded() {
- System.out.println("LocalPeer: Renegotiation needed");
- // In a real application, you would create an offer and set it as the local description.
- }
+ System.out.println("Sending the microphone");
+ }
- @Override
- public void onAddTrack(RTCRtpReceiver receiver, MediaStream[] mediaStreams) {
- System.out.println("LocalPeer: Track added: " + receiver.getTrack().getKind());
+ if (!sending) {
+ System.out.println("There is nothing to send. The ClientApiExample needs no devices.");
+ return;
+ }
- // The receiver is a query result the application owns; dispose it
- // once its track has been retrieved.
- receiver.dispose();
- }
+ // Adding the tracks negotiated the call. The main thread may wait; a handler must not.
+ caller.whenConnected().get(15, TimeUnit.SECONDS);
- @Override
- public void onRemoveTrack(RTCRtpReceiver receiver) {
- System.out.println("LocalPeer: Track removed: " + receiver.getTrack().getKind());
+ caller.onStats(Duration.ofSeconds(5), stats -> System.out.printf(
+ "Caller sends %d kbit/s over a %s candidate%n",
+ Math.max(0, stats.outgoingBitrate()) / 1000, stats.candidateType()));
- receiver.dispose();
+ System.out.println("Press Enter to end the call...");
+ System.in.read();
}
+ // Closing the runtime closed the peers, the camera and the microphone.
- @Override
- public void onTrack(RTCRtpTransceiver transceiver) {
- RTCRtpReceiver receiver = transceiver.getReceiver();
- MediaStreamTrack track = receiver.getTrack();
- String kind = track.getKind();
-
- if (kind.equals(MediaStreamTrack.AUDIO_TRACK_KIND)) {
- AudioTrack audioTrack = (AudioTrack) track;
- audioTrack.addSink(audioFrameLogger);
- }
- if (kind.equals(MediaStreamTrack.VIDEO_TRACK_KIND)) {
- VideoTrack videoTrack = (VideoTrack) track;
- videoTrack.addSink(videoFrameLogger);
- }
-
- System.out.println("LocalPeer: Transceiver track added: " + kind);
-
- // The receiver and transceiver are query results the application
- // owns; dispose them once the track has been retrieved. The track
- // itself is unaffected and keeps delivering frames to its sink.
- receiver.dispose();
- transceiver.dispose();
- }
+ System.out.println("Closed");
}
-
-
/**
- * A simple implementation of VideoTrackSink that logs information about received frames.
+ * Logs what the callee receives, once a second.
*/
- private static class VideoFrameLogger implements VideoTrackSink {
+ private static void receive(RemoteTrack track) {
+ String kind = track.kind().name().toLowerCase();
- private static final long LOG_INTERVAL_MS = 1000; // Log every second
- private int frameCount = 0;
- private long lastLogTime = System.currentTimeMillis();
+ System.out.println("Callee receives the " + kind + " track");
+ if (track.isVideo()) {
+ AtomicInteger frames = new AtomicInteger();
+ AtomicLong lastLog = new AtomicLong(System.nanoTime());
- @Override
- public void onVideoFrame(VideoFrame frame) {
- frameCount++;
+ // The frame is released after the handler returns. A slow handler gets the newest
+ // frame next, and the frames in between are dropped.
+ track.asVideo().onFrame(frame -> {
+ int count = frames.incrementAndGet();
+ long now = System.nanoTime();
- long now = System.currentTimeMillis();
- if (now - lastLogTime >= LOG_INTERVAL_MS) {
- System.out.printf("Received %d video frames in the last %.1f seconds%n",
- frameCount, (now - lastLogTime) / 1000.0);
- System.out.printf("Last frame: %dx%d, rotation: %d, timestamp: %dms%n",
- frame.buffer.getWidth(), frame.buffer.getHeight(), frame.rotation,
- frame.timestampNs / 1000000);
+ if (now - lastLog.get() >= TimeUnit.SECONDS.toNanos(1)) {
+ System.out.printf("Received %d video frames, the last one %dx%d%n", count,
+ frame.buffer.getWidth(), frame.buffer.getHeight());
- frameCount = 0;
- lastLogTime = now;
- }
-
- // Release the native resources associated with this frame to prevent memory leaks.
- frame.release();
+ frames.set(0);
+ lastLog.set(now);
+ }
+ });
}
- }
+ else {
+ // The callee would play the microphone back to the speakers, and the microphone
+ // would pick it up again. Muting the track keeps its audio for the handler.
+ track.asAudio().setVolume(0);
+ AtomicLong millis = new AtomicLong();
+ track.asAudio().onAudio(chunk -> {
+ long total = millis.addAndGet(chunk.frames() * 1000L / chunk.sampleRate());
- /**
- * A simple implementation of AudioTrackSink that logs information about received audio data.
- */
- private static class AudioFrameLogger implements AudioTrackSink {
-
- private static final long LOG_INTERVAL_MS = 1000; // Log every second
- private int frameCount = 0;
- private long lastLogTime = System.currentTimeMillis();
-
-
- @Override
- public void onData(byte[] data, int bitsPerSample, int sampleRate, int channels, int frames) {
- frameCount++;
-
- long now = System.currentTimeMillis();
- if (now - lastLogTime >= LOG_INTERVAL_MS) {
- System.out.printf("Received %d audio frames in the last %.1f seconds%n",
- frameCount, (now - lastLogTime) / 1000.0);
- System.out.printf("Last audio data: %d bytes, %d bits/sample, %d Hz, %d channels, %d frames%n",
- data.length, bitsPerSample, sampleRate, channels, frames);
-
- frameCount = 0;
- lastLogTime = now;
- }
+ if (total % 1000 == 0) {
+ System.out.printf("Received %d s of audio, %d Hz, %d channel(s)%n", total / 1000,
+ chunk.sampleRate(), chunk.channels());
+ }
+ });
}
+
+ track.onEnded(() -> System.out.println("The " + kind + " track ended"));
}
-}
\ No newline at end of file
+}