diff --git a/webrtc/.gitignore b/webrtc/.gitignore index bab2990ef5..2747cceb90 100644 --- a/webrtc/.gitignore +++ b/webrtc/.gitignore @@ -42,6 +42,13 @@ *.idb *.pdb +# Java build files +.idea/ +*.iml +.gradle/ +build/ +out/ + # Our stuff *.pem webrtc-sendrecv diff --git a/webrtc/README.md b/webrtc/README.md index 40a8f53829..09000ba0f3 100644 --- a/webrtc/README.md +++ b/webrtc/README.md @@ -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 diff --git a/webrtc/docker-compose.yml b/webrtc/docker-compose.yml index dbdbb9ed04..b5069551f4 100644 --- a/webrtc/docker-compose.yml +++ b/webrtc/docker-compose.yml @@ -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 index 0000000000..ab6da8fbbc --- /dev/null +++ b/webrtc/sendrecv/gst-java/Dockerfile @@ -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 index 0000000000..dd04e168d5 --- /dev/null +++ b/webrtc/sendrecv/gst-java/build.gradle @@ -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 index 0000000000..f6b961fd5a 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 index 0000000000..44e7c4d1d7 --- /dev/null +++ b/webrtc/sendrecv/gst-java/gradle/wrapper/gradle-wrapper.properties @@ -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 index 0000000000..cccdd3d517 --- /dev/null +++ b/webrtc/sendrecv/gst-java/gradlew @@ -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 index 0000000000..e95643d6a2 --- /dev/null +++ b/webrtc/sendrecv/gst-java/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega 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 index 0000000000..bc91a40510 --- /dev/null +++ b/webrtc/sendrecv/gst-java/src/main/java/WebrtcSendRecv.java @@ -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 { + 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); + } + }); + } +} +