Java demo (#81)
authorsvangasse <stevevangasse@gmail.com>
Tue, 26 Feb 2019 12:41:15 +0000 (12:41 +0000)
committerMathieu Duponchelle <mathieu.duponchelle@opencreed.com>
Tue, 26 Feb 2019 12:41:15 +0000 (13:41 +0100)
Added working demo using GStreamer Java bindings

webrtc/.gitignore
webrtc/README.md
webrtc/docker-compose.yml
webrtc/sendrecv/gst-java/Dockerfile [new file with mode: 0644]
webrtc/sendrecv/gst-java/build.gradle [new file with mode: 0644]
webrtc/sendrecv/gst-java/gradle/wrapper/gradle-wrapper.jar [new file with mode: 0644]
webrtc/sendrecv/gst-java/gradle/wrapper/gradle-wrapper.properties [new file with mode: 0644]
webrtc/sendrecv/gst-java/gradlew [new file with mode: 0755]
webrtc/sendrecv/gst-java/gradlew.bat [new file with mode: 0644]
webrtc/sendrecv/gst-java/src/main/java/WebrtcSendRecv.java [new file with mode: 0644]

index bab2990..2747cce 100644 (file)
 *.idb
 *.pdb
 
+# Java build files
+.idea/
+*.iml
+.gradle/
+build/
+out/
+
 # Our stuff
 *.pem
 webrtc-sendrecv
index 40a8f53..09000ba 100644 (file)
@@ -92,6 +92,16 @@ With all versions, you will see a bouncing ball + hear red noise in the browser,
 
 You can pass a --server argument to all versions, for example `--server=wss://127.0.0.1:8443`.
 
+#### Running the Java version
+
+`cd sendrecv/gst-java`\
+`./gradlew build`\
+`java -jar build/libs/gst-java.jar --peer-id=ID` with the `id` from the browser.
+
+You can optionally specify the server URL too (it defaults to wss://webrtc.nirbheek.in:8443):
+
+`java -jar build/libs/gst-java.jar --peer-id=1 --server=ws://localhost:8443`
+
 ### multiparty-sendrecv: Multiparty audio conference with N peers
 
 * Build the sources in the `gst/` directory on your machine
index dbdbb9e..b506955 100644 (file)
@@ -5,8 +5,10 @@ services:
   #
   # sendrecv-gst:
   #   build: ./sendrecv/gst
-  sendrecv-gst-rust:
-    build: ./sendrecv/gst-rust
+  sendrecv-gst-java:
+    build: ./sendrecv/gst-java
+  #sendrecv-gst-rust:
+  #  build: ./sendrecv/gst-rust
   sendrecv-js:
     build: ./sendrecv/js
     ports:
diff --git a/webrtc/sendrecv/gst-java/Dockerfile b/webrtc/sendrecv/gst-java/Dockerfile
new file mode 100644 (file)
index 0000000..ab6da8f
--- /dev/null
@@ -0,0 +1,36 @@
+# START BUILD PHASE
+FROM gradle:5.1.1-jdk11 as builder
+WORKDIR /home/gradle/work
+COPY . /home/gradle/work/
+USER root
+RUN chown -R gradle:gradle /home/gradle/work
+USER gradle
+RUN gradle build
+# END BUILD PHASE
+
+FROM openjdk:10
+
+# GStreamer dependencies
+USER root
+RUN apt-get update &&\
+  apt-get install -yq \
+  libgstreamer1.0-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
+  gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav \
+  gstreamer1.0-doc gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa \
+  gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-pulseaudio gstreamer1.0-nice
+
+# Seems to be a problem with GStreamer and lastest openssl in debian buster, so rolling back to working version
+# https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/issues/811
+RUN curl -SL http://security-cdn.debian.org/debian-security/pool/updates/main/o/openssl/openssl_1.1.0j-1~deb9u1_amd64.deb -o openssl.deb && \
+    dpkg -i openssl.deb
+
+COPY --from=builder /home/gradle/work/build/libs/work.jar /gst-java.jar
+
+CMD echo "Waiting a few seconds for you to open the browser at localhost:8080" \
+    && sleep 10 \
+    && java -jar /gst-java.jar \
+    --peer-id=1 \
+    --server=ws://signalling:8443
+
+
+
diff --git a/webrtc/sendrecv/gst-java/build.gradle b/webrtc/sendrecv/gst-java/build.gradle
new file mode 100644 (file)
index 0000000..dd04e16
--- /dev/null
@@ -0,0 +1,35 @@
+plugins {
+    id 'java'
+}
+
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+repositories {
+    mavenCentral()
+}
+
+dependencies {
+
+    // GStreamer
+    compile "net.java.dev.jna:jna:5.2.0"
+    compile "org.freedesktop.gstreamer:gst1-java-core:0.9.4"
+
+    // Websockets
+    compile 'org.asynchttpclient:async-http-client:2.7.0'
+    compile 'com.fasterxml.jackson.core:jackson-databind:2.9.8'
+
+    // Logging
+    compile 'org.slf4j:slf4j-simple:1.8.0-beta2'
+}
+
+
+// Build a "fat" executable jar file
+jar {
+    manifest {
+        attributes 'Main-Class': 'WebrtcSendRecv'
+    }
+    from {
+        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
+    }
+}
\ No newline at end of file
diff --git a/webrtc/sendrecv/gst-java/gradle/wrapper/gradle-wrapper.jar b/webrtc/sendrecv/gst-java/gradle/wrapper/gradle-wrapper.jar
new file mode 100644 (file)
index 0000000..f6b961f
Binary files /dev/null and b/webrtc/sendrecv/gst-java/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/webrtc/sendrecv/gst-java/gradle/wrapper/gradle-wrapper.properties b/webrtc/sendrecv/gst-java/gradle/wrapper/gradle-wrapper.properties
new file mode 100644 (file)
index 0000000..44e7c4d
--- /dev/null
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/webrtc/sendrecv/gst-java/gradlew b/webrtc/sendrecv/gst-java/gradlew
new file mode 100755 (executable)
index 0000000..cccdd3d
--- /dev/null
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/webrtc/sendrecv/gst-java/gradlew.bat b/webrtc/sendrecv/gst-java/gradlew.bat
new file mode 100644 (file)
index 0000000..e95643d
--- /dev/null
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off\r
+@rem ##########################################################################\r
+@rem\r
+@rem  Gradle startup script for Windows\r
+@rem\r
+@rem ##########################################################################\r
+\r
+@rem Set local scope for the variables with windows NT shell\r
+if "%OS%"=="Windows_NT" setlocal\r
+\r
+set DIRNAME=%~dp0\r
+if "%DIRNAME%" == "" set DIRNAME=.\r
+set APP_BASE_NAME=%~n0\r
+set APP_HOME=%DIRNAME%\r
+\r
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r
+set DEFAULT_JVM_OPTS=\r
+\r
+@rem Find java.exe\r
+if defined JAVA_HOME goto findJavaFromJavaHome\r
+\r
+set JAVA_EXE=java.exe\r
+%JAVA_EXE% -version >NUL 2>&1\r
+if "%ERRORLEVEL%" == "0" goto init\r
+\r
+echo.\r
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r
+echo.\r
+echo Please set the JAVA_HOME variable in your environment to match the\r
+echo location of your Java installation.\r
+\r
+goto fail\r
+\r
+:findJavaFromJavaHome\r
+set JAVA_HOME=%JAVA_HOME:"=%\r
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe\r
+\r
+if exist "%JAVA_EXE%" goto init\r
+\r
+echo.\r
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r
+echo.\r
+echo Please set the JAVA_HOME variable in your environment to match the\r
+echo location of your Java installation.\r
+\r
+goto fail\r
+\r
+:init\r
+@rem Get command-line arguments, handling Windows variants\r
+\r
+if not "%OS%" == "Windows_NT" goto win9xME_args\r
+\r
+:win9xME_args\r
+@rem Slurp the command line arguments.\r
+set CMD_LINE_ARGS=\r
+set _SKIP=2\r
+\r
+:win9xME_args_slurp\r
+if "x%~1" == "x" goto execute\r
+\r
+set CMD_LINE_ARGS=%*\r
+\r
+:execute\r
+@rem Setup the command line\r
+\r
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar\r
+\r
+@rem Execute Gradle\r
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r
+\r
+:end\r
+@rem End local scope for the variables with windows NT shell\r
+if "%ERRORLEVEL%"=="0" goto mainEnd\r
+\r
+:fail\r
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r
+rem the _cmd.exe /c_ return code!\r
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1\r
+exit /b 1\r
+\r
+:mainEnd\r
+if "%OS%"=="Windows_NT" endlocal\r
+\r
+:omega\r
diff --git a/webrtc/sendrecv/gst-java/src/main/java/WebrtcSendRecv.java b/webrtc/sendrecv/gst-java/src/main/java/WebrtcSendRecv.java
new file mode 100644 (file)
index 0000000..bc91a40
--- /dev/null
@@ -0,0 +1,266 @@
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.asynchttpclient.DefaultAsyncHttpClient;
+import org.asynchttpclient.DefaultAsyncHttpClientConfig;
+import org.asynchttpclient.ws.WebSocket;
+import org.asynchttpclient.ws.WebSocketListener;
+import org.asynchttpclient.ws.WebSocketUpgradeHandler;
+import org.freedesktop.gstreamer.*;
+import org.freedesktop.gstreamer.Element.PAD_ADDED;
+import org.freedesktop.gstreamer.elements.DecodeBin;
+import org.freedesktop.gstreamer.elements.WebRTCBin;
+import org.freedesktop.gstreamer.elements.WebRTCBin.CREATE_OFFER;
+import org.freedesktop.gstreamer.elements.WebRTCBin.ON_ICE_CANDIDATE;
+import org.freedesktop.gstreamer.elements.WebRTCBin.ON_NEGOTIATION_NEEDED;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/**
+ * Demo gstreamer app for negotiating and streaming a sendrecv webrtc stream
+ * with a browser JS app.
+ *
+ * @author stevevangasse
+ */
+public class WebrtcSendRecv {
+
+    private static final Logger logger = LoggerFactory.getLogger(WebrtcSendRecv.class);
+    private static final String REMOTE_SERVER_URL = "wss://webrtc.nirbheek.in:8443";
+    private static final String VIDEO_BIN_DESCRIPTION = "videotestsrc ! videoconvert ! queue ! vp8enc deadline=1 ! rtpvp8pay ! queue ! capsfilter caps=application/x-rtp,media=video,encoding-name=VP8,payload=97";
+    private static final String AUDIO_BIN_DESCRIPTION = "audiotestsrc ! audioconvert ! audioresample ! queue ! opusenc ! rtpopuspay ! queue ! capsfilter caps=application/x-rtp,media=audio,encoding-name=OPUS,payload=96";
+
+    private final String serverUrl;
+    private final String peerId;
+    private final ObjectMapper mapper = new ObjectMapper();
+    private WebSocket websocket;
+    private WebRTCBin webRTCBin;
+    private Pipeline pipe;
+
+    public static void main(String[] args) throws Exception {
+        if (args.length == 0) {
+            logger.error("Please pass at least the peer-id from the signalling server e.g java -jar build/libs/gst-java.jar --peer-id=1234 --server=wss://webrtc.nirbheek.in:8443");
+            return;
+        }
+        String serverUrl = REMOTE_SERVER_URL;
+        String peerId = null;
+        for (int i=0; i<args.length; i++) {
+            if (args[i].startsWith("--server=")) {
+                serverUrl = args[i].substring("--server=".length());
+            } else if (args[i].startsWith("--peer-id=")) {
+                peerId = args[i].substring("--peer-id=".length());
+            }
+        }
+        logger.info("Using peer id {}, on server: {}", peerId, serverUrl);
+        WebrtcSendRecv webrtcSendRecv = new WebrtcSendRecv(peerId, serverUrl);
+        webrtcSendRecv.startCall();
+    }
+
+    private WebrtcSendRecv(String peerId, String serverUrl) {
+        this.peerId = peerId;
+        this.serverUrl = serverUrl;
+        Gst.init();
+        webRTCBin = new WebRTCBin("sendrecv");
+
+        Bin video = Gst.parseBinFromDescription(VIDEO_BIN_DESCRIPTION, true);
+        Bin audio = Gst.parseBinFromDescription(AUDIO_BIN_DESCRIPTION, true);
+
+        pipe = new Pipeline();
+        pipe.addMany(webRTCBin, video, audio);
+        video.link(webRTCBin);
+        audio.link(webRTCBin);
+        setupPipeLogging(pipe);
+
+        // When the pipeline goes to PLAYING, the on_negotiation_needed() callback will be called, and we will ask webrtcbin to create an offer which will match the pipeline above.
+        webRTCBin.connect(onNegotiationNeeded);
+        webRTCBin.connect(onIceCandidate);
+        webRTCBin.connect(onIncomingStream);
+    }
+
+    private void startCall() throws Exception {
+        DefaultAsyncHttpClientConfig httpClientConfig =
+                new DefaultAsyncHttpClientConfig
+                        .Builder()
+                        .setUseInsecureTrustManager(true)
+                        .build();
+
+        websocket = new DefaultAsyncHttpClient(httpClientConfig)
+                .prepareGet(serverUrl)
+                .execute(
+                        new WebSocketUpgradeHandler
+                                .Builder()
+                                .addWebSocketListener(webSocketListener)
+                                .build())
+                .get();
+
+        Gst.main();
+    }
+
+    private WebSocketListener webSocketListener = new WebSocketListener() {
+
+        @Override
+        public void onOpen(WebSocket websocket) {
+            logger.info("websocket onOpen");
+            websocket.sendTextFrame("HELLO 564322");
+        }
+
+        @Override
+        public void onClose(WebSocket websocket, int code, String reason) {
+            logger.info("websocket onClose: " + code + " : " + reason);
+            Gst.quit();
+        }
+
+        @Override
+        public void onTextFrame(String payload, boolean finalFragment, int rsv) {
+            if (payload.equals("HELLO")) {
+                websocket.sendTextFrame("SESSION " + peerId);
+            } else if (payload.equals("SESSION_OK")) {
+                pipe.play();
+            } else if (payload.startsWith("ERROR")) {
+                logger.error(payload);
+                Gst.quit();
+            } else {
+                handleSdp(payload);
+            }
+        }
+
+        @Override
+        public void onError(Throwable t) {
+            logger.error("onError", t);
+        }
+    };
+
+    private void handleSdp(String payload) {
+        try {
+            JsonNode answer = mapper.readTree(payload);
+            if (answer.has("sdp")) {
+                String sdpStr = answer.get("sdp").get("sdp").textValue();
+                logger.info("answer SDP:\n{}", sdpStr);
+                SDPMessage sdpMessage = new SDPMessage();
+                sdpMessage.parseBuffer(sdpStr);
+                WebRTCSessionDescription description = new WebRTCSessionDescription(WebRTCSDPType.ANSWER, sdpMessage);
+                webRTCBin.setRemoteDescription(description);
+            }
+            else if (answer.has("ice")) {
+                String candidate = answer.get("ice").get("candidate").textValue();
+                int sdpMLineIndex = answer.get("ice").get("sdpMLineIndex").intValue();
+                logger.info("Adding ICE candidate: {}", candidate);
+                webRTCBin.addIceCandidate(sdpMLineIndex, candidate);
+            }
+        } catch (IOException e) {
+            logger.error("Problem reading payload", e);
+        }
+    }
+
+    private CREATE_OFFER onOfferCreated = offer -> {
+        webRTCBin.setLocalDescription(offer);
+        try {
+            JsonNode rootNode = mapper.createObjectNode();
+            JsonNode sdpNode = mapper.createObjectNode();
+            ((ObjectNode) sdpNode).put("type", "offer");
+            ((ObjectNode) sdpNode).put("sdp", offer.getSDPMessage().toString());
+            ((ObjectNode) rootNode).set("sdp", sdpNode);
+            String json = mapper.writeValueAsString(rootNode);
+            logger.info("Sending offer:\n{}", json);
+            websocket.sendTextFrame(json);
+        } catch (JsonProcessingException e) {
+            logger.error("Couldn't write JSON", e);
+        }
+    };
+
+    private ON_NEGOTIATION_NEEDED onNegotiationNeeded = elem -> {
+        logger.info("onNegotiationNeeded: " + elem.getName());
+
+        // When webrtcbin has created the offer, it will hit our callback and we send SDP offer over the websocket to signalling server
+        webRTCBin.createOffer(onOfferCreated);
+    };
+
+    private ON_ICE_CANDIDATE onIceCandidate = (sdpMLineIndex, candidate) -> {
+        JsonNode rootNode = mapper.createObjectNode();
+        JsonNode iceNode = mapper.createObjectNode();
+        ((ObjectNode) iceNode).put("candidate", candidate);
+        ((ObjectNode) iceNode).put("sdpMLineIndex", sdpMLineIndex);
+        ((ObjectNode) rootNode).set("ice", iceNode);
+
+        try {
+            String json = mapper.writeValueAsString(rootNode);
+            logger.info("ON_ICE_CANDIDATE: " + json);
+            websocket.sendTextFrame(json);
+        } catch (JsonProcessingException e) {
+            logger.error("Couldn't write JSON", e);
+        }
+    };
+
+    private PAD_ADDED onIncomingDecodebinStream = (element, pad) -> {
+        logger.info("onIncomingDecodebinStream");
+        if (!pad.hasCurrentCaps()) {
+            logger.info("Pad has no caps, ignoring: {}", pad.getName());
+            return;
+        }
+        Structure caps = pad.getCaps().getStructure(0);
+        String name = caps.getName();
+        if (name.startsWith("video")) {
+            logger.info("onIncomingDecodebinStream video");
+            Element queue = ElementFactory.make("queue", "my-videoqueue");
+            Element videoconvert = ElementFactory.make("videoconvert", "my-videoconvert");
+            Element autovideosink = ElementFactory.make("autovideosink", "my-autovideosink");
+            pipe.addMany(queue, videoconvert, autovideosink);
+            queue.syncStateWithParent();
+            videoconvert.syncStateWithParent();
+            autovideosink.syncStateWithParent();
+            pad.link(queue.getStaticPad("sink"));
+            queue.link(videoconvert);
+            videoconvert.link(autovideosink);
+        }
+        if (name.startsWith("audio")) {
+            logger.info("onIncomingDecodebinStream audio");
+            Element queue = ElementFactory.make("queue", "my-audioqueue");
+            Element audioconvert = ElementFactory.make("audioconvert", "my-audioconvert");
+            Element audioresample = ElementFactory.make("audioresample", "my-audioresample");
+            Element autoaudiosink = ElementFactory.make("autoaudiosink", "my-autoaudiosink");
+            pipe.addMany(queue, audioconvert, audioresample, autoaudiosink);
+            queue.syncStateWithParent();
+            audioconvert.syncStateWithParent();
+            audioresample.syncStateWithParent();
+            autoaudiosink.syncStateWithParent();
+            pad.link(queue.getStaticPad("sink"));
+            queue.link(audioconvert);
+            audioconvert.link(audioresample);
+            audioresample.link(autoaudiosink);
+        }
+    };
+
+    private PAD_ADDED onIncomingStream = (element, pad) -> {
+        if (pad.getDirection() != PadDirection.SRC) {
+            logger.info("Pad is not source, ignoring: {}", pad.getDirection());
+            return;
+        }
+        logger.info("Receiving stream! Element: {} Pad: {}", element.getName(), pad.getName());
+        DecodeBin decodebin = new DecodeBin("my-decoder-" + pad.getName());
+        decodebin.connect(onIncomingDecodebinStream);
+        pipe.add(decodebin);
+        decodebin.syncStateWithParent();
+        webRTCBin.link(decodebin);
+    };
+
+    private void setupPipeLogging(Pipeline pipe) {
+        Bus bus = pipe.getBus();
+        bus.connect((Bus.EOS) source -> {
+            logger.info("Reached end of stream: " + source.toString());
+            Gst.quit();
+        });
+
+        bus.connect((Bus.ERROR) (source, code, message) -> {
+            logger.error("Error from source: '{}', with code: {}, and message '{}'", source, code, message);
+        });
+
+        bus.connect((source, old, current, pending) -> {
+            if (source instanceof Pipeline) {
+                logger.info("Pipe state changed from {} to new {}", old, current);
+            }
+        });
+    }
+}
+