entry : sdpFmtp.entrySet()) {
+ if (!first) {
+ sb.append(", ");
+ }
+ sb.append(entry.getKey()).append("=").append(entry.getValue());
+ first = false;
+ }
+ sb.append("}");
+ }
+
+ System.out.println(sb);
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..88fddc6d
--- /dev/null
+++ b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/DesktopVideoExample.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2025 WebRTC Java Contributors
+ *
+ * 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.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.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 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.
+ *
+ * This example shows how to:
+ *
+ * - Create a PeerConnectionFactory
+ * - Get available desktop sources (screens and windows)
+ * - Create a VideoDesktopSource for capturing screen or window content
+ * - Configure the VideoDesktopSource properties
+ * - Create a video track with the desktop source
+ * - Set up a peer connection
+ *
+ *
+ * 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).
+ *
+ * @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;
+
+
+ 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 screens = screenCapturer.getDesktopSources();
+ System.out.println("\nAvailable screens:");
+ for (DesktopSource screen : screens) {
+ System.out.printf(" Screen: %s (ID: %d)%n", screen.title, screen.id);
+ }
+
+ // Get available windows.
+ WindowCapturer windowCapturer = new WindowCapturer();
+ List windows = windowCapturer.getDesktopSources();
+ System.out.println("\nAvailable windows:");
+ for (DesktopSource window : windows) {
+ System.out.printf(" Window: %s (ID: %d)%n", window.title, window.id);
+ }
+
+ // Clean up the capturers as we only needed them to get the sources.
+ screenCapturer.dispose();
+ windowCapturer.dispose();
+
+ // Create a desktop video source.
+ videoSource = new VideoDesktopSource();
+
+ // Configure the desktop video source.
+ // Set frame rate (e.g., 30 fps).
+ videoSource.setFrameRate(30);
+
+ // Set maximum frame size (e.g., 1920x1080).
+ videoSource.setMaxFrameSize(1920, 1080);
+
+ // Select a source to capture.
+ // For this example; we'll use the first available screen if there is one.
+ if (!screens.isEmpty()) {
+ DesktopSource selectedScreen = screens.get(0);
+ System.out.printf("%nSelected screen for capture: %s (ID: %d)%n",
+ selectedScreen.title, selectedScreen.id);
+ videoSource.setSourceId(selectedScreen.id, false);
+ }
+ // Otherwise, use the first available window if there is one.
+ else if (!windows.isEmpty()) {
+ DesktopSource selectedWindow = windows.get(0);
+ System.out.printf("%nSelected window for capture: %s (ID: %d)%n",
+ selectedWindow.title, selectedWindow.id);
+ videoSource.setSourceId(selectedWindow.id, true);
+ }
+ // If no sources are available, fall back to a default (primary screen).
+ else {
+ System.out.println("\nNo desktop sources found. Using default (primary screen).");
+ videoSource.setSourceId(0, false);
+ }
+
+ // Start capturing.
+ videoSource.start();
+
+ // Create a video track with the desktop source.
+ VideoTrack videoTrack = factory.createVideoTrack("video0", videoSource);
+
+ // Add the tracks to the peer connection.
+ List streamIds = new ArrayList<>();
+ streamIds.add("stream1");
+ peerConnection.addTrack(videoTrack, streamIds);
+
+ System.out.println("LocalPeer: Created with a desktop video track");
+ }
+
+ /**
+ * Closes the peer connection and releases resources.
+ */
+ public void dispose() {
+ if (videoSource != null) {
+ // Stop capturing before disposing.
+ videoSource.stop();
+ videoSource.dispose();
+ }
+ if (peerConnection != null) {
+ peerConnection.close();
+ }
+ }
+
+ // 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());
+ }
+
+ @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.
+ }
+
+ @Override
+ public void onAddTrack(RTCRtpReceiver receiver, MediaStream[] mediaStreams) {
+ System.out.println("LocalPeer: Track added: " + receiver.getTrack().getKind());
+ }
+
+ @Override
+ public void onRemoveTrack(RTCRtpReceiver receiver) {
+ System.out.println("LocalPeer: Track removed: " + receiver.getTrack().getKind());
+ }
+
+ @Override
+ public void onTrack(RTCRtpTransceiver transceiver) {
+ MediaStreamTrack track = transceiver.getReceiver().getTrack();
+
+ System.out.println("LocalPeer: Transceiver track added: " + track.getKind());
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..27cc7eb2
--- /dev/null
+++ b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/PeerConnectionExample.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright 2025 WebRTC Java Contributors
+ *
+ * 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.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.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;
+
+/**
+ * Example demonstrating how to set up a peer connection with audio and video tracks
+ * to be able to send and receive media.
+ *
+ * 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
+ *
+ *
+ * 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).
+ *
+ * @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 AudioTrack audioTrack;
+ private final VideoTrack videoTrack;
+ 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;
+
+ AudioTrackSource 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");
+ peerConnection.addTrack(audioTrack, streamIds);
+ 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);
+ }
+ if (peerConnection != null) {
+ peerConnection.close();
+ }
+ }
+
+ // 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());
+ }
+
+ @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.
+ }
+
+ @Override
+ public void onAddTrack(RTCRtpReceiver receiver, MediaStream[] mediaStreams) {
+ System.out.println("LocalPeer: Track added: " + receiver.getTrack().getKind());
+ }
+
+ @Override
+ public void onRemoveTrack(RTCRtpReceiver receiver) {
+ System.out.println("LocalPeer: Track removed: " + receiver.getTrack().getKind());
+ }
+
+ @Override
+ public void onTrack(RTCRtpTransceiver transceiver) {
+ MediaStreamTrack track = transceiver.getReceiver().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);
+ }
+ }
+
+
+
+ /**
+ * A simple implementation of VideoTrackSink that logs information about received frames.
+ */
+ private static class VideoFrameLogger implements VideoTrackSink {
+
+ private static final long LOG_INTERVAL_MS = 1000; // Log every second
+ private int frameCount = 0;
+ private long lastLogTime = System.currentTimeMillis();
+
+
+ @Override
+ public void onVideoFrame(VideoFrame frame) {
+ frameCount++;
+
+ 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);
+
+ frameCount = 0;
+ lastLogTime = now;
+ }
+
+ // Release the native resources associated with this frame to prevent memory leaks.
+ frame.release();
+ }
+ }
+
+
+
+ /**
+ * 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;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/WhepExample.java b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/WhepExample.java
new file mode 100644
index 00000000..3280d194
--- /dev/null
+++ b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/WhepExample.java
@@ -0,0 +1,305 @@
+/*
+ * Example application that demonstrates how to set up a WebRTC peer connection,
+ * create an offer, and accept a remote answer.
+ */
+
+package dev.onvoid.webrtc.examples;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import dev.onvoid.webrtc.*;
+import dev.onvoid.webrtc.media.MediaStream;
+import dev.onvoid.webrtc.media.MediaStreamTrack;
+import dev.onvoid.webrtc.media.video.VideoDeviceSource;
+import dev.onvoid.webrtc.media.video.VideoTrack;
+
+/**
+ * Example implementation of WebRTC HTTP Egress Protocol (WHEP) client.
+ *
+ * This class demonstrates:
+ *
+ * - Setting up a WebRTC peer connection
+ * - Creating and sending an SDP offer to a WHEP endpoint
+ * - Receiving and processing an SDP answer
+ * - Establishing media streaming over WebRTC
+ *
+ *
+ * The example creates a receive-only peer connection that can accept
+ * incoming video streams from a WHEP-compatible server.
+ *
+ * @see WHEP Specification
+ *
+ * @author Alex Andres
+ */
+public class WhepExample {
+
+ private static final String WHEP_ENDPOINT_URL = "http://localhost:8889/mystream/whep";
+
+ /** Factory for creating peer connections and media objects. */
+ private PeerConnectionFactory factory;
+
+ /** The WebRTC peer connection that handles media communication. */
+ private RTCPeerConnection peerConnection;
+
+ /** The local SDP offer to be sent to the remote endpoint. */
+ private RTCSessionDescription localOffer;
+
+ // Synchronization objects for async operations.
+ private final CountDownLatch offerCreatedLatch = new CountDownLatch(1);
+ private final CountDownLatch localDescriptionSetLatch = new CountDownLatch(1);
+ private final CountDownLatch remoteDescriptionSetLatch = new CountDownLatch(1);
+
+
+ public static void main(String[] args) {
+ WhepExample example = new WhepExample();
+
+ try {
+ example.run();
+ }
+ catch (Exception e) {
+ Logger.getLogger("WHEPExample").log(Level.SEVERE, "Error running WHEP example", e);
+ }
+ finally {
+ example.cleanup();
+ }
+ }
+
+ public void run() throws Exception {
+ System.out.println("Starting WebRTC Peer Connection Example");
+
+ initializePeerConnectionFactory();
+ createPeerConnection();
+ createOffer();
+
+ // Wait for the offer to be created.
+ if (!offerCreatedLatch.await(5, TimeUnit.SECONDS)) {
+ throw new IllegalStateException("Timeout waiting for offer creation.");
+ }
+
+ // Set the local description (the offer).
+ setLocalDescription(localOffer);
+
+ // Wait for the local description to be set.
+ if (!localDescriptionSetLatch.await(5, TimeUnit.SECONDS)) {
+ throw new IllegalStateException("Timeout waiting for local description to be set.");
+ }
+
+ System.out.println("Local offer created and set.");
+ //System.out.println("SDP Offer: " + localOffer.sdp);
+ System.out.println("Sending local offer to the remote endpoint.");
+
+ String answerSdp = sendOfferEndpoint(localOffer.sdp);
+
+ //System.out.println("SDP Answer: " + answerSdp);
+
+ // Set the remote description (the answer).
+ setRemoteDescription(new RTCSessionDescription(RTCSdpType.ANSWER, answerSdp));
+
+ // Wait for the remote description to be set.
+ if (!remoteDescriptionSetLatch.await(5, TimeUnit.SECONDS)) {
+ throw new IllegalStateException("Timeout waiting for remote description to be set.");
+ }
+
+ System.out.println("Remote answer set. Peer connection established!");
+ System.out.println("Media should now be exchanged between peers.");
+
+ // Wait a bit to see connection state changes.
+ Thread.sleep(10000);
+
+ System.out.println("WebRTC Peer Connection Example completed.");
+ }
+
+ private void initializePeerConnectionFactory() {
+ System.out.println("Initializing PeerConnectionFactory.");
+ factory = new PeerConnectionFactory();
+ }
+
+ private void createPeerConnection() {
+ System.out.println("Creating peer connection.");
+
+ // Create ICE servers configuration.
+ RTCConfiguration config = new RTCConfiguration();
+
+ // Add Google's public STUN server.
+ RTCIceServer iceServer = new RTCIceServer();
+ iceServer.urls.add("stun:stun.l.google.com:19302");
+ config.iceServers.add(iceServer);
+
+ // Create the peer connection with our observer.
+ peerConnection = factory.createPeerConnection(config, new PeerConnectionObserverImpl());
+
+ // Create a video track from a video device source (e.g., webcam).
+ // Since we are only receiving video in this example, the source will be a dummy video source.
+ VideoDeviceSource videoSource = new VideoDeviceSource();
+ VideoTrack videoTrack = factory.createVideoTrack("videoTrack", videoSource);
+ videoTrack.addSink(videoFrame -> System.out.println("Received video frame: " + videoFrame));
+
+ // Only interested in receiving video, so we set up a transceiver for that.
+ RTCRtpTransceiverInit transceiverInit = new RTCRtpTransceiverInit();
+ transceiverInit.direction = RTCRtpTransceiverDirection.RECV_ONLY;
+
+ // Add the transceiver to the peer connection with the video track.
+ RTCRtpTransceiver transceiver = peerConnection.addTransceiver(videoTrack, transceiverInit);
+
+ // Set up a sink to handle incoming video frames.
+ MediaStreamTrack track = transceiver.getReceiver().getTrack();
+ if (track instanceof VideoTrack vTrack) {
+ vTrack.addSink(videoFrame -> {
+ System.out.println("Received video frame: " + videoFrame);
+ });
+ }
+ }
+
+ private void createOffer() {
+ System.out.println("Creating offer.");
+
+ // Create offer options (use default options).
+ RTCOfferOptions options = new RTCOfferOptions();
+
+ // Create the offer.
+ peerConnection.createOffer(options, new CreateSessionDescriptionObserver() {
+ @Override
+ public void onSuccess(RTCSessionDescription description) {
+ System.out.println("Offer created successfully.");
+ localOffer = description;
+ offerCreatedLatch.countDown();
+ }
+
+ @Override
+ public void onFailure(String error) {
+ System.err.println("Failed to create offer: " + error);
+ offerCreatedLatch.countDown();
+ }
+ });
+ }
+
+ private void setLocalDescription(RTCSessionDescription description) {
+ System.out.println("Setting local description.");
+
+ peerConnection.setLocalDescription(description, new SetSessionDescriptionObserver() {
+ @Override
+ public void onSuccess() {
+ System.out.println("Local description set successfully.");
+ localDescriptionSetLatch.countDown();
+ }
+
+ @Override
+ public void onFailure(String error) {
+ System.err.println("Failed to set local description: " + error);
+ localDescriptionSetLatch.countDown();
+ }
+ });
+ }
+
+ private String sendOfferEndpoint(String sdpOffer) throws Exception {
+ HttpClient client = HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(10))
+ .build();
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(WHEP_ENDPOINT_URL))
+ .header("Content-Type", "application/sdp")
+ .POST(HttpRequest.BodyPublishers.ofString(sdpOffer))
+ .timeout(Duration.ofSeconds(30))
+ .build();
+
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+ if (response.statusCode() == 200 || response.statusCode() == 201) {
+ System.out.println("WHEP request successful");
+ return response.body();
+ }
+ else {
+ throw new RuntimeException("WHEP request failed with status: " + response.statusCode());
+ }
+ }
+
+ private void setRemoteDescription(RTCSessionDescription description) {
+ System.out.println("Setting remote description.");
+
+ peerConnection.setRemoteDescription(description, new SetSessionDescriptionObserver() {
+ @Override
+ public void onSuccess() {
+ System.out.println("Remote description set successfully.");
+ remoteDescriptionSetLatch.countDown();
+ }
+
+ @Override
+ public void onFailure(String error) {
+ System.err.println("Failed to set remote description: " + error);
+ remoteDescriptionSetLatch.countDown();
+ }
+ });
+ }
+
+ private void cleanup() {
+ System.out.println("Cleaning up resources.");
+
+ if (peerConnection != null) {
+ peerConnection.close();
+ peerConnection = null;
+ }
+
+ if (factory != null) {
+ factory.dispose();
+ factory = null;
+ }
+ }
+
+
+
+ /**
+ * Implementation of PeerConnectionObserver to handle events from the peer connection.
+ */
+ private static class PeerConnectionObserverImpl implements PeerConnectionObserver {
+
+ @Override
+ public void onIceCandidate(RTCIceCandidate candidate) {
+ System.out.println("ICE candidate: " + candidate.sdp);
+ // In a real application, we would send this candidate to the remote peer
+ }
+
+ @Override
+ public void onConnectionChange(RTCPeerConnectionState state) {
+ System.out.println("Connection state changed: " + state);
+ }
+
+ @Override
+ public void onIceConnectionChange(RTCIceConnectionState state) {
+ System.out.println("ICE connection state changed: " + state);
+ }
+
+ @Override
+ public void onIceGatheringChange(RTCIceGatheringState state) {
+ System.out.println("ICE gathering state changed: " + state);
+ }
+
+ @Override
+ public void onSignalingChange(RTCSignalingState state) {
+ System.out.println("Signaling state changed: " + state);
+ }
+
+ @Override
+ public void onDataChannel(RTCDataChannel dataChannel) {
+ System.out.println("Data channel created: " + dataChannel.getLabel());
+ }
+
+ @Override
+ public void onRenegotiationNeeded() {
+ System.out.println("Renegotiation needed.");
+ }
+
+ @Override
+ public void onAddTrack(RTCRtpReceiver receiver, MediaStream[] mediaStreams) {
+ System.out.println("Track added.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/webrtc-examples/src/main/java/module-info.java b/webrtc-examples/src/main/java/module-info.java
new file mode 100644
index 00000000..e051b4cd
--- /dev/null
+++ b/webrtc-examples/src/main/java/module-info.java
@@ -0,0 +1,7 @@
+module webrtc.java.examples {
+
+ requires java.logging;
+ requires java.net.http;
+ requires webrtc.java;
+
+}
\ No newline at end of file