From 603245e171d6db554917f4571dc46e7fde4f20c5 Mon Sep 17 00:00:00 2001 From: floriankirmaier Date: Mon, 4 Dec 2023 16:59:18 +0100 Subject: [PATCH] Added webrtc module --- gradle.properties | 2 +- jpro-webrtc/build.gradle | 3 + jpro-webrtc/example/build.gradle | 22 ++ .../example/src/main/java/module-info.java | 9 + .../webrtc/example/simple/WebRTCSimple.java | 74 ++++++ .../example/videoroom/VideoRoomApp.java | 40 ++++ .../example/videoroom/model/VideoRoom.java | 148 ++++++++++++ .../example/videoroom/page/OverviewPage.java | 22 ++ .../example/videoroom/page/VideoRoomPage.java | 110 +++++++++ .../src/main/resources/jpro/html/defaultpage | 22 ++ .../webrtc/example/videoroom/videoroom.css | 68 ++++++ jpro-webrtc/src/main/java/module-info.java | 6 + .../one/jpro/platform/webrtc/MediaStream.java | 26 +++ .../one/jpro/platform/webrtc/RTCDetails.java | 52 +++++ .../platform/webrtc/RTCPeerConnection.java | 213 ++++++++++++++++++ .../one/jpro/platform/webrtc/VideoFrame.java | 35 +++ settings.gradle | 2 + 17 files changed, 853 insertions(+), 1 deletion(-) create mode 100644 jpro-webrtc/build.gradle create mode 100644 jpro-webrtc/example/build.gradle create mode 100644 jpro-webrtc/example/src/main/java/module-info.java create mode 100644 jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/simple/WebRTCSimple.java create mode 100644 jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/videoroom/VideoRoomApp.java create mode 100644 jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/videoroom/model/VideoRoom.java create mode 100644 jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/videoroom/page/OverviewPage.java create mode 100644 jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/videoroom/page/VideoRoomPage.java create mode 100644 jpro-webrtc/example/src/main/resources/jpro/html/defaultpage create mode 100644 jpro-webrtc/example/src/main/resources/one/jpro/platform/webrtc/example/videoroom/videoroom.css create mode 100644 jpro-webrtc/src/main/java/module-info.java create mode 100644 jpro-webrtc/src/main/java/one/jpro/platform/webrtc/MediaStream.java create mode 100644 jpro-webrtc/src/main/java/one/jpro/platform/webrtc/RTCDetails.java create mode 100644 jpro-webrtc/src/main/java/one/jpro/platform/webrtc/RTCPeerConnection.java create mode 100644 jpro-webrtc/src/main/java/one/jpro/platform/webrtc/VideoFrame.java diff --git a/gradle.properties b/gradle.properties index 9e844573..c25bb3e0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ JPRO_PLATFORM_VERSION = 0.2.7-SNAPSHOT -JPRO_VERSION = 2023.3.1 +JPRO_VERSION = 2023.3.2-SNAPSHOT JAVAFX_VERSION = 17.0.9 SIMPLEFX_VERSION = 3.2.27 JMEMORYBUDDY_VERSION = 0.5.1 diff --git a/jpro-webrtc/build.gradle b/jpro-webrtc/build.gradle new file mode 100644 index 00000000..4db0a5b9 --- /dev/null +++ b/jpro-webrtc/build.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation "com.sandec.jpro:jpro-webapi:$JPRO_VERSION" +} \ No newline at end of file diff --git a/jpro-webrtc/example/build.gradle b/jpro-webrtc/example/build.gradle new file mode 100644 index 00000000..5c584b0c --- /dev/null +++ b/jpro-webrtc/example/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'org.openjfx.javafxplugin' + id 'jpro-gradle-plugin' +} + +//mainClassName = "one.jpro.platform.webrtc.example.simple.WebRTCSimple" +mainClassName = "one.jpro.platform.webrtc.example.videoroom.VideoRoomApp" + +application { + mainClass = "$mainClassName" + mainModule = moduleName +} + +dependencies { + implementation project(':jpro-webrtc') + implementation project(':jpro-routing:core') +} + +javafx { + version = "$JAVAFX_VERSION" + modules = ['javafx.graphics', 'javafx.controls', 'javafx.swing', 'javafx.fxml', 'javafx.media', 'javafx.web'] +} \ No newline at end of file diff --git a/jpro-webrtc/example/src/main/java/module-info.java b/jpro-webrtc/example/src/main/java/module-info.java new file mode 100644 index 00000000..a14b041f --- /dev/null +++ b/jpro-webrtc/example/src/main/java/module-info.java @@ -0,0 +1,9 @@ +module one.jpro.platform.webrtc.example { + requires one.jpro.platform.webrtc; + requires javafx.controls; + requires jpro.webapi; + requires one.jpro.platform.routing.core; + + exports one.jpro.platform.webrtc.example.simple; + exports one.jpro.platform.webrtc.example.videoroom; +} \ No newline at end of file diff --git a/jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/simple/WebRTCSimple.java b/jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/simple/WebRTCSimple.java new file mode 100644 index 00000000..b4ffd4a1 --- /dev/null +++ b/jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/simple/WebRTCSimple.java @@ -0,0 +1,74 @@ +package one.jpro.platform.webrtc.example.simple; + +import com.jpro.webapi.JProApplication; +import com.jpro.webapi.JSVariable; +import javafx.collections.ListChangeListener; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; +import one.jpro.platform.webrtc.MediaStream; +import one.jpro.platform.webrtc.RTCDetails; +import one.jpro.platform.webrtc.RTCPeerConnection; +import one.jpro.platform.webrtc.VideoFrame; + +public class WebRTCSimple extends JProApplication { + + @Override + public void start(Stage primaryStage) throws Exception { + + var pin = new VBox(); + + //var rtc = new RTCPeerConnection(getWebAPI()); + var rtc1 = new RTCPeerConnection(getWebAPI()); + var rtc2 = new RTCPeerConnection(getWebAPI()); + + var video1 = new VideoFrame(getWebAPI()); + var video2 = new VideoFrame(getWebAPI()); + + rtc1.tracks.addListener((ListChangeListener) change -> { + while(change.next()) { + if(change.wasAdded()) { + video1.setStream(change.getAddedSubList().get(0)); + } + } + }); + rtc2.tracks.addListener((ListChangeListener) change -> { + while(change.next()) { + if(change.wasAdded()) { + video2.setStream(change.getAddedSubList().get(0)); + } + } + }); + + var f1 = MediaStream.getCameraStream(rtc1.getWebAPI()).js.thenAccept(stream -> { + rtc1.addStream(stream); + }); + var f2 = MediaStream.getCameraStream(rtc2.getWebAPI()).js.thenAccept(stream -> { + rtc2.addStream(stream); + }); + + (f1.thenCompose(a -> f2)).thenAccept( r -> + RTCPeerConnection.connectConnections(rtc1, rtc2) + ); + + var WebRTCLabel = new Label("WebRTC"); + WebRTCLabel.setStyle("-fx-font-size: 20px;"); + pin.getChildren().add(WebRTCLabel); + pin.getChildren().add(new RTCDetails(rtc1)); + pin.getChildren().add(video1); + pin.getChildren().add(new Label(" ---------------- ")); + + pin.getChildren().add(new RTCDetails(rtc2)); + pin.getChildren().add(video2); + + //pin.getChildren().add(createLabel("signalingState", rtc.tracks)); + + primaryStage.setScene(new Scene(pin)); + + primaryStage.show(); + } + + + +} diff --git a/jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/videoroom/VideoRoomApp.java b/jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/videoroom/VideoRoomApp.java new file mode 100644 index 00000000..05f8bc13 --- /dev/null +++ b/jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/videoroom/VideoRoomApp.java @@ -0,0 +1,40 @@ +package one.jpro.platform.webrtc.example.videoroom; + +import one.jpro.platform.routing.Filters; +import one.jpro.platform.routing.Route; +import one.jpro.platform.routing.RouteApp; +import one.jpro.platform.routing.RouteUtils; +import one.jpro.platform.webrtc.example.videoroom.page.OverviewPage; +import one.jpro.platform.webrtc.example.videoroom.page.VideoRoomPage; +import simplefx.experimental.parts.FXFuture; + +import java.util.regex.Pattern; + +import static one.jpro.platform.routing.RouteUtils.viewFromNode; + +public class VideoRoomApp extends RouteApp { + + static Pattern roomPattern = Pattern.compile("/room/([0-9a-fA-F]*)"); + + @Override + public Route createRoute() { + + getScene().getStylesheets().add("/one/jpro/platform/webrtc/example/videoroom/videoroom.css"); + + // / -> overview + // /room/id -> room + return Route.empty() + .and(RouteUtils.getNode("/", (r) -> new OverviewPage())) + .and(r -> { + System.out.println("path: " + r.path()); + var matcher = roomPattern.matcher(r.path()); + if(matcher.matches()) { + var roomID = matcher.group(1); + return FXFuture.unit(viewFromNode(new VideoRoomPage(roomID, getWebAPI()))); + } else { + return FXFuture.unit(null); + } + }) + .filter(Filters.FullscreenFilter(true)); + } +} diff --git a/jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/videoroom/model/VideoRoom.java b/jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/videoroom/model/VideoRoom.java new file mode 100644 index 00000000..1ecc281d --- /dev/null +++ b/jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/videoroom/model/VideoRoom.java @@ -0,0 +1,148 @@ +package one.jpro.platform.webrtc.example.videoroom.model; + +import com.jpro.webapi.JSVariable; +import com.jpro.webapi.WebAPI; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import one.jpro.platform.webrtc.MediaStream; +import one.jpro.platform.webrtc.RTCPeerConnection; +import one.jpro.platform.webrtc.VideoFrame; + +public class VideoRoom { + static ObservableList rooms = FXCollections.observableArrayList(); + + public String id; + ObservableList users = FXCollections.observableArrayList(); + + public VideoRoom(String id) { + this.id = id; + } + + + public static VideoRoom getOrCreateRoom(String id) { + for(VideoRoom room : rooms) { + if(room.id.equals(id)) { + return room; + } + } + VideoRoom room = new VideoRoom(id); + rooms.add(room); + return room; + } + + public void addUserAndCreateConnections(User user) { + for(User otherUser : users) { + createWebRTCUserVideo(user, otherUser); + } + users.add(user); + } + + public static void createWebRTCUserVideo(User user1, User user2) { + var webAPI1 = user1.webAPI; + var webAPI2 = user2.webAPI; + + var rtc1 = new RTCPeerConnection(webAPI1); + var rtc2 = new RTCPeerConnection(webAPI2); + + var video1 = new VideoFrame(webAPI1); + var video2 = new VideoFrame(webAPI2); + + rtc1.tracks.addListener((ListChangeListener) change -> { + System.out.println("tracks size for user 1: " + rtc1.tracks.size()); + while(change.next()) { + if(change.wasAdded()) { + video1.setStream(change.getAddedSubList().get(0)); + } + } + }); + rtc2.tracks.addListener((ListChangeListener) change -> { + System.out.println("tracks size for user 2: " + rtc2.tracks.size()); + while(change.next()) { + if(change.wasAdded()) { + video2.setStream(change.getAddedSubList().get(0)); + } + } + }); + + user1.mediaStream.addListener((observable, oldValue, newValue) -> { + try { + //rtc1.removeAllTracks(); + rtc1.removeStream(oldValue); + newValue.js.thenAccept(stream -> { + rtc1.addStream(stream); + }); + } catch (Exception e) { + e.printStackTrace(); + } + }); + user2.mediaStream.addListener((observable, oldValue, newValue) -> { + try { + //rtc2.removeAllTracks(); + rtc2.removeStream(oldValue); + newValue.js.thenAccept(stream -> { + rtc2.addStream(stream); + }); + } catch (Exception e) { + e.printStackTrace(); + } + }); + var f1 = user1.mediaStream.get().js.thenAccept(stream -> { + rtc1.addStream(stream); + }); + var f2 = user2.mediaStream.get().js.thenAccept(stream -> { + rtc2.addStream(stream); + }); + + (f1.thenCompose(a -> f2)).thenAccept( r -> + RTCPeerConnection.connectConnections(rtc1, rtc2) + ); + + user1.userVideos.add(new UserVideo(user2, video1, rtc1)); + + user2.userVideos.add(new UserVideo(user1, video2, rtc2)); + } + + public static ObservableList getRooms() { + return rooms; + } + + + /** + * A user in a room + */ + public static class User { + + public User(String name, MediaStream mediaStream, WebAPI webAPI) { + this.name.set(name); + this.mediaStream = new SimpleObjectProperty<>(mediaStream); + this.webAPI = webAPI; + } + + public WebAPI webAPI; + public StringProperty name = new SimpleStringProperty("User"); + + public SimpleObjectProperty mediaStream; + + public ObservableList userVideos = FXCollections.observableArrayList(); + } + + /** + * The video of a user in a room accessed by a specific user + */ + public static class UserVideo { + + UserVideo (User user, VideoFrame videoFrame, RTCPeerConnection connection) { + this.user = user; + this.videoFrame = videoFrame; + this.connection = connection; + } + + public User user; + public VideoFrame videoFrame; + public RTCPeerConnection connection; + } +} diff --git a/jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/videoroom/page/OverviewPage.java b/jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/videoroom/page/OverviewPage.java new file mode 100644 index 00000000..3d068ff9 --- /dev/null +++ b/jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/videoroom/page/OverviewPage.java @@ -0,0 +1,22 @@ +package one.jpro.platform.webrtc.example.videoroom.page; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; +import one.jpro.platform.routing.LinkUtil; + +public class OverviewPage extends VBox { + + public OverviewPage() { + getStyleClass().add("page"); + getStyleClass().add("overview-page"); + var overview = new Label("Overview"); + overview.getStyleClass().add("title"); + getChildren().add(overview); + + int randomId = (int) (Math.random() * 1000); + var button = new Button("Create Room"); + LinkUtil.setLink(button, "/room/" + randomId); + getChildren().add(button); + } +} diff --git a/jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/videoroom/page/VideoRoomPage.java b/jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/videoroom/page/VideoRoomPage.java new file mode 100644 index 00000000..3e4defa1 --- /dev/null +++ b/jpro-webrtc/example/src/main/java/one/jpro/platform/webrtc/example/videoroom/page/VideoRoomPage.java @@ -0,0 +1,110 @@ +package one.jpro.platform.webrtc.example.videoroom.page; + +import com.jpro.webapi.WebAPI; +import javafx.collections.ListChangeListener; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.shape.Rectangle; +import one.jpro.platform.webrtc.MediaStream; +import one.jpro.platform.webrtc.RTCPeerConnection; +import one.jpro.platform.webrtc.VideoFrame; +import one.jpro.platform.webrtc.example.videoroom.model.VideoRoom; + +public class VideoRoomPage extends VBox { + + VideoRoom videoRoom; + + static int i = 0; + + public VideoRoomPage(String id, WebAPI webAPI) { + getStyleClass().add("page"); + getStyleClass().add("video-room-page"); + + videoRoom = VideoRoom.getOrCreateRoom(id); + + i += 1; + + var me = new VideoRoom.User("Me_"+i, MediaStream.getCameraStream(webAPI), webAPI); + + var roomName = new Label("Room: " + id); + roomName.getStyleClass().add("room-name"); + + var usrNameLabel = new Label("Name: "); + usrNameLabel.getStyleClass().add("user-name-label"); + var usrName = new TextField(); + usrName.getStyleClass().add("user-name-field"); + usrName.setMaxWidth(100); + usrName.textProperty().bindBidirectional(me.name); + var usrNameBox = new HBox(); + usrNameBox.getStyleClass().add("user-name-box"); + usrNameBox.getChildren().addAll(usrNameLabel, usrName); + + var otherViews = new HBox(); + + otherViews.getStyleClass().add("other-views"); + + me.userVideos.addListener((ListChangeListener) change -> { + while(change.next()) { + if(change.wasAdded()) { + var frames = change.getAddedSubList().stream() + .map(userVideo -> videoFrameContainer(userVideo.videoFrame, userVideo.user, userVideo.connection)) + .toArray(Node[]::new); + otherViews.getChildren().addAll(frames); + } + } + }); + + videoRoom.addUserAndCreateConnections(me); + + var userVideo = new VideoFrame(webAPI); + me.mediaStream.addListener((observable, oldValue, newValue) -> { + userVideo.setStream(newValue); + }); + userVideo.setStream(me.mediaStream.get()); + + var buttons = new HBox(); + buttons.getStyleClass().add("buttons"); + var shareScreen = new Button("Share Screen"); + buttons.getChildren().add(shareScreen); + + shareScreen.setOnAction(e -> { + me.mediaStream.set(MediaStream.getScreenStream(webAPI)); + }); + + getChildren().add(roomName); + getChildren().add(usrNameBox); + getChildren().add(otherViews); + getChildren().add(videoFrameContainer(userVideo, me, null)); + getChildren().add(buttons); + } + + public Node videoFrameContainer(VideoFrame frame, VideoRoom.User user, RTCPeerConnection connection) { + StackPane wrapper = new StackPane(); + var clip = new Rectangle(); + clip.widthProperty().bind(wrapper.widthProperty()); + clip.heightProperty().bind(wrapper.heightProperty()); + clip.setArcWidth(20); + clip.setArcHeight(20); + wrapper.setClip(clip); + wrapper.getStyleClass().add("video-frame-wrapper"); + wrapper.getChildren().add(frame); + + VBox box = new VBox(); + box.getStyleClass().add("video-frame-box"); + var name = new Label(); + name.getStyleClass().add("video-frame-name"); + name.textProperty().bind(user.name); + box.getChildren().add(name); + // For debugging + //if(connection != null) { + // box.getChildren().add(new RTCDetails(connection)); + //} + box.getChildren().add(wrapper); + return box; + } +} diff --git a/jpro-webrtc/example/src/main/resources/jpro/html/defaultpage b/jpro-webrtc/example/src/main/resources/jpro/html/defaultpage new file mode 100644 index 00000000..923bebf2 --- /dev/null +++ b/jpro-webrtc/example/src/main/resources/jpro/html/defaultpage @@ -0,0 +1,22 @@ + + + + + jpro Application: Hello JPro + + + + + + + + + + + + + + + + + diff --git a/jpro-webrtc/example/src/main/resources/one/jpro/platform/webrtc/example/videoroom/videoroom.css b/jpro-webrtc/example/src/main/resources/one/jpro/platform/webrtc/example/videoroom/videoroom.css new file mode 100644 index 00000000..fa21cdeb --- /dev/null +++ b/jpro-webrtc/example/src/main/resources/one/jpro/platform/webrtc/example/videoroom/videoroom.css @@ -0,0 +1,68 @@ +.page { + -fx-background-color: #ffffff; + -fx-alignment: center; +} + +.title { + -fx-font-size: 20px; +} + +.video-room-page { + -fx-alignment: center; + -fx-spacing: 16; +} + +.room-name { + -fx-font-size: 20px; + -fx-font-weight: bold; +} + +.user-name-label { + -fx-font-size: 20px; + -fx-font-weight: bold; +} +.user-name-field { + -fx-font-size: 20px; + -fx-font-weight: bold; + -fx-pref-width: 200; + // Cool javafx border + -fx-border-color: #000000; + -fx-border-width: 2; + -fx-border-radius: 4; + -fx-background-color: #ffffff; + -fx-background-radius: 4; +} +.user-name-box { + -fx-alignment: center; + -fx-spacing: 16; +} + +.other-views { + -fx-alignment: center; + -fx-spacing: 16; +} + +.video-frame-wrapper { + -fx-background-color: #000000; + -fx-pref-width: 400; + -fx-max-width: 400; + -fx-pref-height: 300; +} + +.video-frame-box { + -fx-max-width: 400; + -fx-alignment: center; + -fx-background-color: #ddddff; + -fx-background-radius: 16; + -fx-padding: 24; + -fx-spacing: 16; +} +.video-frame-name { + -fx-font-size: 20px; + -fx-font-weight: bold; +} + +.buttons { + -fx-alignment: center; + -fx-spacing: 16; +} \ No newline at end of file diff --git a/jpro-webrtc/src/main/java/module-info.java b/jpro-webrtc/src/main/java/module-info.java new file mode 100644 index 00000000..395d7b54 --- /dev/null +++ b/jpro-webrtc/src/main/java/module-info.java @@ -0,0 +1,6 @@ +module one.jpro.platform.webrtc { + requires javafx.controls; + requires jpro.webapi; + + exports one.jpro.platform.webrtc; +} \ No newline at end of file diff --git a/jpro-webrtc/src/main/java/one/jpro/platform/webrtc/MediaStream.java b/jpro-webrtc/src/main/java/one/jpro/platform/webrtc/MediaStream.java new file mode 100644 index 00000000..60343621 --- /dev/null +++ b/jpro-webrtc/src/main/java/one/jpro/platform/webrtc/MediaStream.java @@ -0,0 +1,26 @@ +package one.jpro.platform.webrtc; + +import com.jpro.webapi.JSVariable; +import com.jpro.webapi.WebAPI; + +import java.util.concurrent.CompletableFuture; + +public class MediaStream { + + public CompletableFuture js; + MediaStream(WebAPI webAPI, CompletableFuture js) { + this.js = js; + } + + public static MediaStream getCameraStream(WebAPI webAPI) { + var js = webAPI.executeJSAsync("return await navigator.mediaDevices.getUserMedia({video: true, audio: false});"); + return new MediaStream(webAPI, js); + } + + public static MediaStream getScreenStream(WebAPI webAPI) { + var js = webAPI.executeJSAsync("return await navigator.mediaDevices.getDisplayMedia({video: {\n" + + " displaySurface: \"window\",\n" + + " }, audio: false});"); + return new MediaStream(webAPI, js); + } +} diff --git a/jpro-webrtc/src/main/java/one/jpro/platform/webrtc/RTCDetails.java b/jpro-webrtc/src/main/java/one/jpro/platform/webrtc/RTCDetails.java new file mode 100644 index 00000000..d719dc44 --- /dev/null +++ b/jpro-webrtc/src/main/java/one/jpro/platform/webrtc/RTCDetails.java @@ -0,0 +1,52 @@ +package one.jpro.platform.webrtc; + +import javafx.beans.binding.Bindings; +import javafx.beans.property.StringProperty; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; + +/** + * This class shows various details of an RTCPeerConnection. + */ +public class RTCDetails extends VBox { + + private RTCPeerConnection rtc; + public RTCDetails(RTCPeerConnection rtc) { + this.rtc = rtc; + + + getChildren().add(createLabel("connectionState", rtc.connectionState)); + getChildren().add(createLabel("iceConnectionState", rtc.iceConnectionState)); + getChildren().add(createLabel("iceGatheringState", rtc.iceGatheringState)); + getChildren().add(createLabel("signalingState", rtc.signalingState)); + + getChildren().add(createSizeLabel("iceCandidates", rtc.iceCandidates)); + getChildren().add(createSizeLabel("trackCount", rtc.tracks)); + } + + private Label createLabel(String name, StringProperty prop) { + var label = new Label(); + label.textProperty().bind(Bindings.concat(name, ": ", prop)); + return label; + } + + private Label createSizeLabel(String name, ObservableList list) { + var label = new Label(); + //label.textProperty().bind(Bindings.concat(name, ": ", Bindings.size(list))); + // put in the whole list + //label.textProperty().bind(Bindings.concat(name, ": ", list)); + // but add listener for List + list.addListener((ListChangeListener) change -> { + while(change.next()) { + if(change.wasAdded()) { + label.setText(name + ": " + list.size()); + } + } + }); + label.wrapTextProperty().setValue(true); + return label; + } + +} diff --git a/jpro-webrtc/src/main/java/one/jpro/platform/webrtc/RTCPeerConnection.java b/jpro-webrtc/src/main/java/one/jpro/platform/webrtc/RTCPeerConnection.java new file mode 100644 index 00000000..9a299dfc --- /dev/null +++ b/jpro-webrtc/src/main/java/one/jpro/platform/webrtc/RTCPeerConnection.java @@ -0,0 +1,213 @@ +package one.jpro.platform.webrtc; + +import com.jpro.webapi.JSVariable; +import com.jpro.webapi.WebAPI; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public class RTCPeerConnection { + + private static String defaultConf = "{ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }"; + + public StringProperty connectionState = new SimpleStringProperty("new"); + + public StringProperty iceConnectionState = new SimpleStringProperty("new"); + + public StringProperty iceGatheringState = new SimpleStringProperty("new"); + + public StringProperty signalingState = new SimpleStringProperty("stable"); + + private WebAPI webAPI; + private Set hardReferences = new HashSet<>(); + + public ObservableList tracks = FXCollections.observableArrayList(); + public ObservableList iceCandidates = FXCollections.observableArrayList(); + + public Consumer onNewIceCandidate = str -> {}; + + public Runnable onnegotiationneeded = () -> {}; + + JSVariable js; + + public RTCPeerConnection(WebAPI webAPI) { + this(webAPI, defaultConf); + } + + public RTCPeerConnection(WebAPI webAPI, String conf) { + this.webAPI = webAPI; + js = webAPI.executeScriptWithVariable("new RTCPeerConnection(" + conf + ");"); + + listenToProperty("connectionState", connectionState::set); + listenToProperty("iceConnectionState", iceConnectionState::set); + listenToProperty("iceGatheringState", iceGatheringState::set); + listenToProperty("signalingState", signalingState::set); + + listenToJS("track", event -> { + JSVariable track = webAPI.executeScriptWithVariable(event.getName() + ".streams[0];"); + tracks.add(track); + }); + + listenToJS("icecandidate", iceCandidate -> { + //System.out.println("TO EXECUTE: " + "" + iceCandidate.getName() + ".candidate"); + webAPI.executeScriptWithFuture("" + iceCandidate.getName() + ".candidate").thenAccept(str -> { + iceCandidates.add(str); + onNewIceCandidate.accept(str); + }); + }); + + listenToJS("negotiationneeded", e -> { + System.out.println("negotiationneeded: " + "" + e.getName()); + onnegotiationneeded.run(); + }); + } + + public WebAPI getWebAPI() { + return webAPI; + } + + public void listenToJS(String propName, Consumer setter) { + var jsFun = webAPI.registerJavaFunctionWithVariable(setter); + webAPI.executeScript(js.getName() + ".on" + propName + " = function(str){ console.log('str: ' + str); " + jsFun.getName() + "(str);};"); + hardReferences.add(jsFun); + } + + public void listenToProperty(String propName, Consumer setter) { + var jsFun = webAPI.registerJavaFunction(str -> { + // remove first and last character + str = str.substring(1, str.length() - 1); + setter.accept(str); + }); + webAPI.executeScript(js.getName() + ".on" + propName + "change = function(str){" + jsFun.getName() + "(str);};" + + jsFun.getName() + "(" + js.getName() + "." + propName + ");"); + hardReferences.add(jsFun); + } + + public CompletableFuture createOfferAndSetLocal() { + return webAPI.executeJSAsync("const offer = await "+js.getName()+".createOffer();" + + "await "+js.getName()+".setLocalDescription(offer);" + + "return offer;").thenCompose(js -> { + return webAPI.executeScriptWithFuture(js.getName());}); + } + + public CompletableFuture createAnswerAndSetLocal() { + return webAPI.executeJSAsync("const answer = await "+js.getName()+".createAnswer();" + + "await "+js.getName()+".setLocalDescription(answer);" + + "return answer;").thenCompose(js -> { + return webAPI.executeScriptWithFuture(js.getName());}); + } + + public CompletableFuture setLocalDescription(String sdp) { + return webAPI.executeJSAsync("return await " + js.getName() + ".setLocalDescription(" + sdp + ");"); + } + + public CompletableFuture setRemoteDescription(String sdp) { + return webAPI.executeJSAsync("return await " + js.getName() + ".setRemoteDescription(new RTCSessionDescription(" + sdp + "));"); + } + + public CompletableFuture addStream(JSVariable stream) { + return webAPI.executeScriptWithFuture(stream.getName() + ".getTracks().forEach(function(track) {" + + js.getName() + ".addTrack(track, " + stream.getName() + ");" + + "}); undefined;"); + } + + public CompletableFuture removeStream(MediaStream stream) { + return stream.js.thenApply(s -> { + return webAPI.executeScriptWithVariable(js.getName() + ".getSenders().forEach(sender => {" + + "if (sender.track) {" + + "sender.track.stop();" + + "}" + + js.getName() + ".removeTrack(sender);" + + "});"); + }); + } + + //public void removeAllTracks() { + // webAPI.executeScript(js.getName() + ".getSenders().forEach(sender => {" + + // "if (sender.track) {" + + // "sender.track.stop();" + + // "}" + + // js.getName() + ".removeTrack(sender);" + + // "});"); + //} + + public void addTrack(JSVariable track) { + webAPI.executeScript(js.getName() + ".addTrack(" + track.getName() + ");"); + } + + public void removeTracks(JSVariable stream) { + webAPI.executeScript(js.getName() + ".removeTrack(" + stream.getName() + ");"); + } + + /** + * Sets the remote ICE candidates. + */ + public void addIceCandidate(String iceCandidate) { + if(iceCandidate == null) { + throw new IllegalArgumentException("iceCandidate must not be null"); + } + if(iceCandidate.equals("null")) { + throw new IllegalArgumentException("iceCandidate was String 'null'"); + } + webAPI.executeScript(js.getName() + ".addIceCandidate(new RTCIceCandidate(" + iceCandidate + "));"); + } + + public static void connectConnections(RTCPeerConnection rtc1, RTCPeerConnection rtc2) { + + // Link ice candidates + rtc1.iceCandidates.stream().forEach(rtc2::addIceCandidate); + rtc2.iceCandidates.stream().forEach(rtc1::addIceCandidate); + rtc1.onNewIceCandidate = (rtc2::addIceCandidate); + rtc2.onNewIceCandidate = (rtc1::addIceCandidate); + + AtomicBoolean isNegotiating = new AtomicBoolean(false); + + //rtc1.onnegotiationneeded = () -> { + // negotiate(rtc1, rtc2); + //}; + //rtc2.onnegotiationneeded = () -> { + // negotiate(rtc2, rtc1); + //}; + + rtc1.onnegotiationneeded = () -> { + if(!isNegotiating.get()) { + isNegotiating.set(true); + negotiate(rtc1, rtc2).thenRun(() -> { + isNegotiating.set(false); + }); + } + }; + rtc2.onnegotiationneeded = () -> { + if(!isNegotiating.get()) { + isNegotiating.set(true); + negotiate(rtc2, rtc1).thenRun(() -> { + isNegotiating.set(false); + }); + } + }; + isNegotiating.set(true); + negotiate(rtc1, rtc2).thenRun(() -> { + isNegotiating.set(false); + }); + } + + public static CompletableFuture negotiate(RTCPeerConnection rtc1, RTCPeerConnection rtc2) { + + return rtc1.createOfferAndSetLocal().thenCompose(sdp -> { + //System.out.println("OFFER: " + sdp); + return rtc2.setRemoteDescription(sdp).thenCompose(s -> { + return rtc2.createAnswerAndSetLocal().thenCompose(sdp2 -> { + //System.out.println("ANSWER: " + sdp2); + return rtc1.setRemoteDescription(sdp2); + }); + }); + }); + } +} diff --git a/jpro-webrtc/src/main/java/one/jpro/platform/webrtc/VideoFrame.java b/jpro-webrtc/src/main/java/one/jpro/platform/webrtc/VideoFrame.java new file mode 100644 index 00000000..8977adad --- /dev/null +++ b/jpro-webrtc/src/main/java/one/jpro/platform/webrtc/VideoFrame.java @@ -0,0 +1,35 @@ +package one.jpro.platform.webrtc; + +import com.jpro.webapi.HTMLView; +import com.jpro.webapi.JSVariable; +import com.jpro.webapi.WebAPI; + +public class VideoFrame extends HTMLView { + + private WebAPI webAPI; + JSVariable elem; + public VideoFrame(WebAPI webAPI) { + super(""); + this.webAPI = webAPI; + elem = webAPI.getHTMLViewElement(this); + + setPrefSize(100,100); + + widthProperty().addListener((observable, oldValue, newValue) -> { + webAPI.executeScript(elem.getName()+".firstElementChild.width = "+newValue.intValue()+";"); + }); + heightProperty().addListener((observable, oldValue, newValue) -> { + webAPI.executeScript(elem.getName()+".firstElementChild.height = "+newValue.intValue()+";"); + }); + } + + public void setStream(MediaStream stream) { + stream.js.thenAccept(s -> { + webAPI.executeScript(elem.getName()+".firstElementChild.srcObject = "+s.getName()+";"); + }); + } + + public void setStream(JSVariable stream) { + webAPI.executeScript(elem.getName()+".firstElementChild.srcObject = "+stream.getName()+";"); + } +} diff --git a/settings.gradle b/settings.gradle index 63087738..eec37724 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,6 +17,8 @@ include "jpro-sessions" include "jpro-sessions:example" include "jpro-html-scrollpane" include "jpro-html-scrollpane:example" +include "jpro-webrtc" +include "jpro-webrtc:example" include "tree-showing" include "freeze-detector" include "example"