mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-04 23:46:43 +00:00
Move gstwebrtc-demos into gst-examples
Original repository location: https://github.com/centricular/gstwebrtc-demos
This commit is contained in:
commit
a88e90fa9e
110 changed files with 15305 additions and 0 deletions
55
webrtc/.gitignore
vendored
Normal file
55
webrtc/.gitignore
vendored
Normal file
|
@ -0,0 +1,55 @@
|
|||
# Prerequisites
|
||||
*.d
|
||||
|
||||
# Object files
|
||||
*.o
|
||||
*.ko
|
||||
*.obj
|
||||
*.elf
|
||||
|
||||
# Linker output
|
||||
*.ilk
|
||||
*.map
|
||||
*.exp
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Libraries
|
||||
*.lib
|
||||
*.a
|
||||
*.la
|
||||
*.lo
|
||||
|
||||
# Shared objects (inc. Windows DLLs)
|
||||
*.dll
|
||||
*.so
|
||||
*.so.*
|
||||
*.dylib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
*.i*86
|
||||
*.x86_64
|
||||
*.hex
|
||||
|
||||
# Debug files
|
||||
*.dSYM/
|
||||
*.su
|
||||
*.idb
|
||||
*.pdb
|
||||
|
||||
# Java build files
|
||||
.idea/
|
||||
*.iml
|
||||
.gradle/
|
||||
build/
|
||||
out/
|
||||
|
||||
# Our stuff
|
||||
*.pem
|
||||
webrtc-sendrecv
|
||||
__pycache__
|
25
webrtc/LICENSE
Normal file
25
webrtc/LICENSE
Normal file
|
@ -0,0 +1,25 @@
|
|||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2017, Centricular
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
127
webrtc/README.md
Normal file
127
webrtc/README.md
Normal file
|
@ -0,0 +1,127 @@
|
|||
# GStreamer WebRTC demos
|
||||
|
||||
All demos use the same signalling server in the `signalling/` directory
|
||||
|
||||
## Downloading GStreamer
|
||||
|
||||
The GStreamer WebRTC implementation has now been merged upstream, and is in the GStreamer 1.14 release. Binaries can be found here:
|
||||
|
||||
https://gstreamer.freedesktop.org/download/
|
||||
|
||||
## Building GStreamer from source
|
||||
|
||||
If you don't want to use the binaries provided by GStreamer or on your Linux distro, you can build GStreamer from source.
|
||||
|
||||
The easiest way to build the webrtc plugin and all the plugins it needs, is to [use Cerbero](https://gstreamer.freedesktop.org/documentation/installing/building-from-source-using-cerbero.html). These instructions should work out of the box for all platforms, including cross-compiling for iOS and Android.
|
||||
|
||||
One thing to note is that it's written in Python 2, so you may need to replace all instances of `./cerbero-uninstalled` (or `cerbero`) with `python2 cerbero-uninstalled` or whatever Python 2 is called on your platform.
|
||||
|
||||
## Building GStreamer manually from source
|
||||
|
||||
Here are the commands for Ubuntu 18.04.
|
||||
|
||||
```
|
||||
sudo apt-get install -y gstreamer1.0-tools gstreamer1.0-nice gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-plugins-good libgstreamer1.0-dev git libglib2.0-dev libgstreamer-plugins-bad1.0-dev libsoup2.4-dev libjson-glib-dev
|
||||
```
|
||||
|
||||
For hacking on the webrtc plugin, you may want to build manually using the git repositories:
|
||||
|
||||
- http://cgit.freedesktop.org/gstreamer/gstreamer
|
||||
- http://cgit.freedesktop.org/gstreamer/gst-plugins-base
|
||||
- http://cgit.freedesktop.org/gstreamer/gst-plugins-good
|
||||
- http://cgit.freedesktop.org/gstreamer/gst-plugins-bad
|
||||
- http://cgit.freedesktop.org/libnice/libnice
|
||||
|
||||
You can build these with either Autotools gst-uninstalled:
|
||||
|
||||
https://arunraghavan.net/2014/07/quick-start-guide-to-gst-uninstalled-1-x/
|
||||
|
||||
Or with Meson gst-build:
|
||||
|
||||
https://cgit.freedesktop.org/gstreamer/gst-build/
|
||||
|
||||
You may need to install the following packages using your package manager:
|
||||
|
||||
json-glib, libsoup, libnice, libnice-gstreamer1 (the gstreamer plugin for libnice, called gstreamer1.0-nice Debian)
|
||||
|
||||
## Filing bugs
|
||||
|
||||
Please only file bugs about the demos here. Bugs about GStreamer's WebRTC implementation should be filed on the [GStreamer bugzilla](https://bugzilla.gnome.org/enter_bug.cgi?product=GStreamer&component=gst-plugins-bad).
|
||||
|
||||
You can also find us on IRC by joining #gstreamer @ FreeNode.
|
||||
|
||||
## Documentation
|
||||
|
||||
Currently, the best way to understand the API is to read the examples. This post breaking down the API should help with that:
|
||||
|
||||
http://blog.nirbheek.in/2018/02/gstreamer-webrtc.html
|
||||
|
||||
## Examples
|
||||
|
||||
### sendrecv: Send and receive audio and video
|
||||
|
||||
* Serve the `js/` directory on the root of your website, or open https://webrtc.nirbheek.in
|
||||
- The JS code assumes the signalling server is on port 8443 of the same server serving the HTML
|
||||
|
||||
* Open the website in a browser and ensure that the status is "Registered with server, waiting for call", and note the `id` too.
|
||||
|
||||
#### Running the C version
|
||||
|
||||
* Build the sources in the `gst/` directory on your machine. Use `make` or
|
||||
|
||||
```console
|
||||
$ gcc webrtc-sendrecv.c $(pkg-config --cflags --libs gstreamer-webrtc-1.0 gstreamer-sdp-1.0 libsoup-2.4 json-glib-1.0) -o webrtc-sendrecv
|
||||
```
|
||||
|
||||
* Run `webrtc-sendrecv --peer-id=ID` with the `id` from the browser. You will see state changes and an SDP exchange.
|
||||
|
||||
#### Running the Python version
|
||||
|
||||
* python3 -m pip install --user websockets
|
||||
* run `python3 sendrecv/gst/webrtc_sendrecv.py ID` with the `id` from the browser. You will see state changes and an SDP exchange.
|
||||
|
||||
> The python version requires at least version 1.14.2 of gstreamer and its plugins.
|
||||
|
||||
#### Running the Rust version
|
||||
|
||||
* Install a recent Rust toolchain, e.g. via [rustup](https://rustup.rs/).
|
||||
* Run `cargo build` for building the executable.
|
||||
* Run `cargo run -- --peer-id=ID` with the `id` from the browser. You will see state changes and an SDP exchange.
|
||||
|
||||
With all versions, you will see a bouncing ball + hear red noise in the browser, and your browser's webcam + mic in the gst app.
|
||||
|
||||
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
|
||||
|
||||
```console
|
||||
$ gcc mp-webrtc-sendrecv.c $(pkg-config --cflags --libs gstreamer-webrtc-1.0 gstreamer-sdp-1.0 libsoup-2.4 json-glib-1.0) -o mp-webrtc-sendrecv
|
||||
```
|
||||
|
||||
* Run `mp-webrtc-sendrecv --room-id=ID` with `ID` as a room name. The peer will connect to the signalling server and setup a conference room.
|
||||
* Run this as many times as you like, each will spawn a peer that sends red noise and outputs the red noise it receives from other peers.
|
||||
- To change what a peer sends, find the `audiotestsrc` element in the source and change the `wave` property.
|
||||
- You can, of course, also replace `audiotestsrc` itself with `autoaudiosrc` (any platform) or `pulsesink` (on linux).
|
||||
* TODO: implement JS to do the same, derived from the JS for the `sendrecv` example.
|
||||
|
||||
### TODO: Selective Forwarding Unit (SFU) example
|
||||
|
||||
* Server routes media between peers
|
||||
* Participant sends 1 stream, receives n-1 streams
|
||||
|
||||
### TODO: Multipoint Control Unit (MCU) example
|
||||
|
||||
* Server mixes media from all participants
|
||||
* Participant sends 1 stream, receives 1 stream
|
5
webrtc/android/app/.gitignore
vendored
Normal file
5
webrtc/android/app/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
.externalNativeBuild/
|
||||
assets/
|
||||
gst-build-*/
|
||||
src/main/java/org/freedesktop/gstreamer/GStreamer.java
|
||||
src/main/java/org/freedesktop/gstreamer/androidmedia/
|
66
webrtc/android/app/build.gradle
Normal file
66
webrtc/android/app/build.gradle
Normal file
|
@ -0,0 +1,66 @@
|
|||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.freedesktop.gstreamer.webrtc"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 15
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
def gstRoot
|
||||
|
||||
if (project.hasProperty('gstAndroidRoot'))
|
||||
gstRoot = project.gstAndroidRoot
|
||||
else
|
||||
gstRoot = System.env.GSTREAMER_ROOT_ANDROID
|
||||
|
||||
if (gstRoot == null)
|
||||
throw new GradleException('GSTREAMER_ROOT_ANDROID must be set, or "gstAndroidRoot" must be defined in your gradle.properties in the top level directory of the unpacked universal GStreamer Android binaries')
|
||||
|
||||
arguments "NDK_APPLICATION_MK=src/main/jni/Application.mk", "GSTREAMER_JAVA_SRC_DIR=src/main/java", "GSTREAMER_ROOT_ANDROID=$gstRoot", "GSTREAMER_ASSETS_DIR=src/main/assets", "V=1"
|
||||
|
||||
targets "gstwebrtc"
|
||||
|
||||
// All archs except MIPS and MIPS64 are supported
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
path 'src/main/jni/Android.mk'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
if (project.hasProperty('compileDebugJavaWithJavac'))
|
||||
compileDebugJavaWithJavac.dependsOn 'externalNativeBuildDebug'
|
||||
if (project.hasProperty('compileReleaseJavaWithJavac'))
|
||||
compileReleaseJavaWithJavac.dependsOn 'externalNativeBuildRelease'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
testImplementation 'junit:junit:4.12'
|
||||
implementation 'com.android.support:appcompat-v7:23.1.1'
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
|
||||
}
|
19
webrtc/android/app/gradle.properties
Normal file
19
webrtc/android/app/gradle.properties
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
#org.gradle.jvmargs=-Xmx1536m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
#gstAndroidRoot=/home/matt/Projects/cerbero/build/dist/android_universal
|
||||
|
160
webrtc/android/app/gradlew
vendored
Normal file
160
webrtc/android/app/gradlew
vendored
Normal file
|
@ -0,0 +1,160 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# 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
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
esac
|
||||
|
||||
# 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
|
||||
|
||||
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" ] ; 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
|
||||
|
||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||
function splitJvmOpts() {
|
||||
JVM_OPTS=("$@")
|
||||
}
|
||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||
|
||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
90
webrtc/android/app/gradlew.bat
vendored
Normal file
90
webrtc/android/app/gradlew.bat
vendored
Normal file
|
@ -0,0 +1,90 @@
|
|||
@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
|
||||
|
||||
@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=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@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 Windowz variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_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=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
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
|
17
webrtc/android/app/proguard-rules.pro
vendored
Normal file
17
webrtc/android/app/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /home/arun/code/android/sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
27
webrtc/android/app/src/main/AndroidManifest.xml
Normal file
27
webrtc/android/app/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.freedesktop.gstreamer.webrtc">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:glEsVersion="0x00020000"/>
|
||||
|
||||
<application android:label="@string/app_name">
|
||||
<activity android:name=".WebRTC"
|
||||
android:label="@string/app_name">
|
||||
<!-- Files whose MIME type is known to Android -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
|
@ -0,0 +1,92 @@
|
|||
/* GStreamer
|
||||
*
|
||||
* Copyright (C) 2014-2015 Matthew Waters <matthew@centricular.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.freedesktop.gstreamer;
|
||||
|
||||
import java.io.Closeable;
|
||||
import android.view.Surface;
|
||||
import android.content.Context;
|
||||
import org.freedesktop.gstreamer.GStreamer;
|
||||
|
||||
public class WebRTC implements Closeable {
|
||||
private static native void nativeClassInit();
|
||||
public static void init(Context context) throws Exception {
|
||||
System.loadLibrary("gstreamer_android");
|
||||
GStreamer.init(context);
|
||||
|
||||
System.loadLibrary("gstwebrtc");
|
||||
nativeClassInit();
|
||||
}
|
||||
|
||||
private long native_webrtc;
|
||||
private native void nativeNew();
|
||||
public WebRTC() {
|
||||
nativeNew();
|
||||
}
|
||||
|
||||
private native void nativeFree();
|
||||
@Override
|
||||
public void close() {
|
||||
nativeFree();
|
||||
}
|
||||
|
||||
private Surface surface;
|
||||
private native void nativeSetSurface(Surface surface);
|
||||
public void setSurface(Surface surface) {
|
||||
this.surface = surface;
|
||||
nativeSetSurface(surface);
|
||||
}
|
||||
|
||||
public Surface getSurface() {
|
||||
return surface;
|
||||
}
|
||||
|
||||
private String signallingServer;
|
||||
private native void nativeSetSignallingServer(String server);
|
||||
public void setSignallingServer(String server) {
|
||||
this.signallingServer = server;
|
||||
nativeSetSignallingServer(server);
|
||||
}
|
||||
|
||||
public String getSignallingServer() {
|
||||
return this.signallingServer;
|
||||
}
|
||||
|
||||
private String callID;
|
||||
private native void nativeSetCallID(String ID);
|
||||
public void setCallID(String ID) {
|
||||
this.callID = ID;
|
||||
nativeSetCallID(ID);
|
||||
}
|
||||
|
||||
public String getCallID() {
|
||||
return this.callID;
|
||||
}
|
||||
|
||||
private native void nativeCallOtherParty();
|
||||
public void callOtherParty() {
|
||||
nativeCallOtherParty();
|
||||
}
|
||||
|
||||
private native void nativeEndCall();
|
||||
public void endCall() {
|
||||
nativeEndCall();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
/* GStreamer
|
||||
*
|
||||
* Copyright (C) 2014 Sebastian Dröge <sebastian@centricular.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.freedesktop.gstreamer.webrtc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
|
||||
// A simple SurfaceView whose width and height can be set from the outside
|
||||
public class GStreamerSurfaceView extends SurfaceView {
|
||||
public int media_width = 320;
|
||||
public int media_height = 240;
|
||||
|
||||
// Mandatory constructors, they do not do much
|
||||
public GStreamerSurfaceView(Context context, AttributeSet attrs,
|
||||
int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
public GStreamerSurfaceView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public GStreamerSurfaceView (Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
// Called by the layout manager to find out our size and give us some rules.
|
||||
// We will try to maximize our size, and preserve the media's aspect ratio if
|
||||
// we are given the freedom to do so.
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
if (media_width == 0 || media_height == 0) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
return;
|
||||
}
|
||||
|
||||
int width = 0, height = 0;
|
||||
int wmode = View.MeasureSpec.getMode(widthMeasureSpec);
|
||||
int hmode = View.MeasureSpec.getMode(heightMeasureSpec);
|
||||
int wsize = View.MeasureSpec.getSize(widthMeasureSpec);
|
||||
int hsize = View.MeasureSpec.getSize(heightMeasureSpec);
|
||||
|
||||
Log.i ("GStreamer", "onMeasure called with " + media_width + "x" + media_height);
|
||||
// Obey width rules
|
||||
switch (wmode) {
|
||||
case View.MeasureSpec.AT_MOST:
|
||||
if (hmode == View.MeasureSpec.EXACTLY) {
|
||||
width = Math.min(hsize * media_width / media_height, wsize);
|
||||
break;
|
||||
}
|
||||
case View.MeasureSpec.EXACTLY:
|
||||
width = wsize;
|
||||
break;
|
||||
case View.MeasureSpec.UNSPECIFIED:
|
||||
width = media_width;
|
||||
}
|
||||
|
||||
// Obey height rules
|
||||
switch (hmode) {
|
||||
case View.MeasureSpec.AT_MOST:
|
||||
if (wmode == View.MeasureSpec.EXACTLY) {
|
||||
height = Math.min(wsize * media_height / media_width, hsize);
|
||||
break;
|
||||
}
|
||||
case View.MeasureSpec.EXACTLY:
|
||||
height = hsize;
|
||||
break;
|
||||
case View.MeasureSpec.UNSPECIFIED:
|
||||
height = media_height;
|
||||
}
|
||||
|
||||
// Finally, calculate best size when both axis are free
|
||||
if (hmode == View.MeasureSpec.AT_MOST && wmode == View.MeasureSpec.AT_MOST) {
|
||||
int correct_height = width * media_height / media_width;
|
||||
int correct_width = height * media_width / media_height;
|
||||
|
||||
if (correct_height < height)
|
||||
height = correct_height;
|
||||
else
|
||||
width = correct_width;
|
||||
}
|
||||
|
||||
// Obey minimum size
|
||||
width = Math.max (getSuggestedMinimumWidth(), width);
|
||||
height = Math.max (getSuggestedMinimumHeight(), height);
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
/* GStreamer
|
||||
*
|
||||
* Copyright (C) 2014 Sebastian Dröge <sebastian@centricular.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.freedesktop.gstreamer.webrtc;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.PowerManager;
|
||||
import android.util.Log;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
public class WebRTC extends Activity implements SurfaceHolder.Callback {
|
||||
private PowerManager.WakeLock wake_lock;
|
||||
private org.freedesktop.gstreamer.WebRTC webRTC;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState)
|
||||
{
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
try {
|
||||
org.freedesktop.gstreamer.WebRTC.init(this);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
setContentView(R.layout.main);
|
||||
|
||||
webRTC = new org.freedesktop.gstreamer.WebRTC();
|
||||
|
||||
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
||||
wake_lock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "GStreamer WebRTC");
|
||||
wake_lock.setReferenceCounted(false);
|
||||
|
||||
final TextView URLText = (TextView) this.findViewById(R.id.URLText);
|
||||
final TextView IDText = (TextView) this.findViewById(R.id.IDText);
|
||||
final GStreamerSurfaceView gsv = (GStreamerSurfaceView) this.findViewById(R.id.surface_video);
|
||||
|
||||
ImageButton play = (ImageButton) this.findViewById(R.id.button_play);
|
||||
play.setOnClickListener(new OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
webRTC.setSignallingServer(URLText.getText().toString());
|
||||
webRTC.setCallID(IDText.getText().toString());
|
||||
webRTC.setSurface(gsv.getHolder().getSurface());
|
||||
webRTC.callOtherParty();
|
||||
wake_lock.acquire();
|
||||
}
|
||||
});
|
||||
|
||||
ImageButton pause= (ImageButton) this.findViewById(R.id.button_pause);
|
||||
pause.setOnClickListener(new OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
webRTC.endCall();
|
||||
wake_lock.release();
|
||||
}
|
||||
});
|
||||
|
||||
/* webRTC.setVideoDimensionsChangedListener(new org.freedesktop.gstreamer.WebRTC.VideoDimensionsChangedListener() {
|
||||
public void videoDimensionsChanged(org.freedesktop.gstreamer.WebRTC webRTC, final int width, final int height) {
|
||||
runOnUiThread (new Runnable() {
|
||||
public void run() {
|
||||
Log.i ("GStreamer", "Media size changed to " + width + "x" + height);
|
||||
gsv.media_width = width;
|
||||
gsv.media_height = height;
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
gsv.requestLayout();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});*/
|
||||
|
||||
SurfaceHolder sh = gsv.getHolder();
|
||||
sh.addCallback(this);
|
||||
}
|
||||
|
||||
protected void onDestroy() {
|
||||
webRTC.close();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width,
|
||||
int height) {
|
||||
Log.d("GStreamer", "Surface changed to format " + format + " width "
|
||||
+ width + " height " + height);
|
||||
webRTC.setSurface(holder.getSurface());
|
||||
}
|
||||
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
Log.d("GStreamer", "Surface created: " + holder.getSurface());
|
||||
}
|
||||
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
Log.d("GStreamer", "Surface destroyed");
|
||||
webRTC.setSurface(null);
|
||||
}
|
||||
}
|
45
webrtc/android/app/src/main/jni/Android.mk
Normal file
45
webrtc/android/app/src/main/jni/Android.mk
Normal file
|
@ -0,0 +1,45 @@
|
|||
LOCAL_PATH := $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_MODULE := gstwebrtc
|
||||
LOCAL_SRC_FILES := webrtc.c dummy.cpp
|
||||
|
||||
LOCAL_SHARED_LIBRARIES := gstreamer_android
|
||||
LOCAL_LDLIBS := -llog -landroid
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
|
||||
ifndef GSTREAMER_ROOT_ANDROID
|
||||
$(error GSTREAMER_ROOT_ANDROID is not defined!)
|
||||
endif
|
||||
|
||||
ifeq ($(TARGET_ARCH_ABI),armeabi)
|
||||
GSTREAMER_ROOT := $(GSTREAMER_ROOT_ANDROID)/arm
|
||||
else ifeq ($(TARGET_ARCH_ABI),armeabi-v7a)
|
||||
GSTREAMER_ROOT := $(GSTREAMER_ROOT_ANDROID)/armv7
|
||||
else ifeq ($(TARGET_ARCH_ABI),arm64-v8a)
|
||||
GSTREAMER_ROOT := $(GSTREAMER_ROOT_ANDROID)/arm64
|
||||
else ifeq ($(TARGET_ARCH_ABI),x86)
|
||||
GSTREAMER_ROOT := $(GSTREAMER_ROOT_ANDROID)/x86
|
||||
else ifeq ($(TARGET_ARCH_ABI),x86_64)
|
||||
GSTREAMER_ROOT := $(GSTREAMER_ROOT_ANDROID)/x86_64
|
||||
else
|
||||
$(error Target arch ABI not supported: $(TARGET_ARCH_ABI))
|
||||
endif
|
||||
|
||||
GSTREAMER_NDK_BUILD_PATH := $(GSTREAMER_ROOT)/share/gst-android/ndk-build/
|
||||
|
||||
include $(GSTREAMER_NDK_BUILD_PATH)/plugins.mk
|
||||
|
||||
GSTREAMER_PLUGINS_CORE_CUSTOM := coreelements app audioconvert audiorate audioresample videoconvert videorate videoscale videotestsrc volume
|
||||
GSTREAMER_PLUGINS_CODECS_CUSTOM := videoparsersbad vpx opus audioparsers opusparse androidmedia
|
||||
GSTREAMER_PLUGINS_NET_CUSTOM := tcp rtsp rtp rtpmanager udp srtp webrtc dtls nice
|
||||
GSTREAMER_PLUGINS := $(GSTREAMER_PLUGINS_CORE_CUSTOM) $(GSTREAMER_PLUGINS_CODECS_CUSTOM) $(GSTREAMER_PLUGINS_NET_CUSTOM) \
|
||||
$(GSTREAMER_PLUGINS_ENCODING) \
|
||||
$(GSTREAMER_PLUGINS_SYS)
|
||||
|
||||
GSTREAMER_EXTRA_DEPS := gstreamer-webrtc-1.0 gstreamer-sdp-1.0 gstreamer-video-1.0 libsoup-2.4 json-glib-1.0 glib-2.0
|
||||
|
||||
G_IO_MODULES = gnutls
|
||||
|
||||
include $(GSTREAMER_NDK_BUILD_PATH)/gstreamer-1.0.mk
|
4
webrtc/android/app/src/main/jni/Application.mk
Normal file
4
webrtc/android/app/src/main/jni/Application.mk
Normal file
|
@ -0,0 +1,4 @@
|
|||
APP_PLATFORM = 15
|
||||
APP_ABI = armeabi-v7a arm64-v8a x86 x86_64
|
||||
APP_STL = c++_shared
|
||||
#APP_ABI = armeabi-v7a arm64-v8a x86_64
|
1
webrtc/android/app/src/main/jni/dummy.cpp
Normal file
1
webrtc/android/app/src/main/jni/dummy.cpp
Normal file
|
@ -0,0 +1 @@
|
|||
/* This is needed purely to force linking libc++_shared */
|
918
webrtc/android/app/src/main/jni/webrtc.c
Normal file
918
webrtc/android/app/src/main/jni/webrtc.c
Normal file
|
@ -0,0 +1,918 @@
|
|||
/* GStreamer
|
||||
*
|
||||
* Copyright (C) 2014 Sebastian Dröge <sebastian@centricular.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
#include <android/native_window.h>
|
||||
#include <android/native_window_jni.h>
|
||||
|
||||
#include <gst/video/videooverlay.h>
|
||||
|
||||
/* helper library for webrtc things */
|
||||
#define GST_USE_UNSTABLE_API
|
||||
#include <gst/webrtc/webrtc.h>
|
||||
|
||||
/* For signalling */
|
||||
#include <libsoup/soup.h>
|
||||
#include <json-glib/json-glib.h>
|
||||
|
||||
GST_DEBUG_CATEGORY_STATIC (debug_category);
|
||||
#define GST_CAT_DEFAULT debug_category
|
||||
|
||||
#define DEFAULT_SIGNALLING_SERVER "wss://webrtc.nirbheek.in:8443"
|
||||
|
||||
#define GET_CUSTOM_DATA(env, thiz, fieldID) (WebRTC *)(gintptr)(*env)->GetLongField (env, thiz, fieldID)
|
||||
#define SET_CUSTOM_DATA(env, thiz, fieldID, data) (*env)->SetLongField (env, thiz, fieldID, (jlong)(gintptr)data)
|
||||
|
||||
enum AppState {
|
||||
APP_STATE_UNKNOWN = 0,
|
||||
APP_STATE_ERROR = 1, /* generic error */
|
||||
SERVER_CONNECTING = 1000,
|
||||
SERVER_CONNECTION_ERROR,
|
||||
SERVER_CONNECTED, /* Ready to register */
|
||||
SERVER_REGISTERING = 2000,
|
||||
SERVER_REGISTRATION_ERROR,
|
||||
SERVER_REGISTERED, /* Ready to call a peer */
|
||||
SERVER_CLOSED, /* server connection closed by us or the server */
|
||||
PEER_CONNECTING = 3000,
|
||||
PEER_CONNECTION_ERROR,
|
||||
PEER_CONNECTED,
|
||||
PEER_CALL_NEGOTIATING = 4000,
|
||||
PEER_CALL_STARTED,
|
||||
PEER_CALL_STOPPING,
|
||||
PEER_CALL_STOPPED,
|
||||
PEER_CALL_ERROR,
|
||||
};
|
||||
|
||||
typedef struct _WebRTC
|
||||
{
|
||||
jobject java_webrtc;
|
||||
GstElement *pipe;
|
||||
GThread *thread;
|
||||
GMainLoop *loop;
|
||||
GMutex lock;
|
||||
GCond cond;
|
||||
ANativeWindow *native_window;
|
||||
SoupWebsocketConnection *ws_conn;
|
||||
gchar *signalling_server;
|
||||
gchar *peer_id;
|
||||
enum AppState app_state;
|
||||
GstElement *webrtcbin, *video_sink;
|
||||
} WebRTC;
|
||||
|
||||
static pthread_key_t current_jni_env;
|
||||
static JavaVM *java_vm;
|
||||
static jfieldID native_webrtc_field_id;
|
||||
|
||||
static gboolean
|
||||
cleanup_and_quit_loop (WebRTC * webrtc, const gchar * msg, enum AppState state)
|
||||
{
|
||||
if (msg)
|
||||
g_printerr ("%s\n", msg);
|
||||
if (state > 0)
|
||||
webrtc->app_state = state;
|
||||
|
||||
if (webrtc->ws_conn) {
|
||||
if (soup_websocket_connection_get_state (webrtc->ws_conn) ==
|
||||
SOUP_WEBSOCKET_STATE_OPEN)
|
||||
/* This will call us again */
|
||||
soup_websocket_connection_close (webrtc->ws_conn, 1000, "");
|
||||
else
|
||||
g_object_unref (webrtc->ws_conn);
|
||||
}
|
||||
|
||||
if (webrtc->loop) {
|
||||
g_main_loop_quit (webrtc->loop);
|
||||
webrtc->loop = NULL;
|
||||
}
|
||||
|
||||
if (webrtc->pipe) {
|
||||
gst_element_set_state (webrtc->pipe, GST_STATE_NULL);
|
||||
gst_object_unref (webrtc->pipe);
|
||||
webrtc->pipe = NULL;
|
||||
}
|
||||
|
||||
/* To allow usage as a GSourceFunc */
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static gchar*
|
||||
get_string_from_json_object (JsonObject * object)
|
||||
{
|
||||
JsonNode *root;
|
||||
JsonGenerator *generator;
|
||||
gchar *text;
|
||||
|
||||
/* Make it the root node */
|
||||
root = json_node_init_object (json_node_alloc (), object);
|
||||
generator = json_generator_new ();
|
||||
json_generator_set_root (generator, root);
|
||||
text = json_generator_to_data (generator, NULL);
|
||||
|
||||
/* Release everything */
|
||||
g_object_unref (generator);
|
||||
json_node_free (root);
|
||||
return text;
|
||||
}
|
||||
|
||||
static GstElement *
|
||||
handle_media_stream (GstPad * pad, GstElement * pipe, const char * convert_name,
|
||||
const char * sink_name)
|
||||
{
|
||||
GstPad *qpad;
|
||||
GstElement *q, *conv, *sink;
|
||||
GstPadLinkReturn ret;
|
||||
|
||||
q = gst_element_factory_make ("queue", NULL);
|
||||
g_assert (q);
|
||||
conv = gst_element_factory_make (convert_name, NULL);
|
||||
g_assert (conv);
|
||||
sink = gst_element_factory_make (sink_name, NULL);
|
||||
g_assert (sink);
|
||||
if (g_strcmp0 (convert_name, "audioconvert") == 0) {
|
||||
GstElement *resample = gst_element_factory_make ("audioresample", NULL);
|
||||
g_assert_nonnull (resample);
|
||||
gst_bin_add_many (GST_BIN (pipe), q, conv, resample, sink, NULL);
|
||||
gst_element_sync_state_with_parent (q);
|
||||
gst_element_sync_state_with_parent (conv);
|
||||
gst_element_sync_state_with_parent (resample);
|
||||
gst_element_sync_state_with_parent (sink);
|
||||
gst_element_link_many (q, conv, resample, sink, NULL);
|
||||
} else {
|
||||
gst_bin_add_many (GST_BIN (pipe), q, conv, sink, NULL);
|
||||
gst_element_sync_state_with_parent (q);
|
||||
gst_element_sync_state_with_parent (conv);
|
||||
gst_element_sync_state_with_parent (sink);
|
||||
gst_element_link_many (q, conv, sink, NULL);
|
||||
}
|
||||
|
||||
qpad = gst_element_get_static_pad (q, "sink");
|
||||
|
||||
ret = gst_pad_link (pad, qpad);
|
||||
g_assert (ret == GST_PAD_LINK_OK);
|
||||
gst_object_unref (qpad);
|
||||
|
||||
return sink;
|
||||
}
|
||||
|
||||
static void
|
||||
on_incoming_decodebin_stream (GstElement * decodebin, GstPad * pad,
|
||||
WebRTC * webrtc)
|
||||
{
|
||||
GstCaps *caps;
|
||||
const gchar *name;
|
||||
|
||||
if (!gst_pad_has_current_caps (pad)) {
|
||||
g_printerr ("Pad '%s' has no caps, can't do anything, ignoring\n",
|
||||
GST_PAD_NAME (pad));
|
||||
return;
|
||||
}
|
||||
|
||||
caps = gst_pad_get_current_caps (pad);
|
||||
name = gst_structure_get_name (gst_caps_get_structure (caps, 0));
|
||||
|
||||
if (g_str_has_prefix (name, "video")) {
|
||||
GstElement *sink = handle_media_stream (pad, webrtc->pipe, "videoconvert", "glimagesink");
|
||||
if (webrtc->video_sink == NULL) {
|
||||
webrtc->video_sink = sink;
|
||||
if (webrtc->native_window)
|
||||
gst_video_overlay_set_window_handle (GST_VIDEO_OVERLAY (sink), (gpointer) webrtc->native_window);
|
||||
}
|
||||
} else if (g_str_has_prefix (name, "audio")) {
|
||||
handle_media_stream (pad, webrtc->pipe, "audioconvert", "autoaudiosink");
|
||||
} else {
|
||||
g_printerr ("Unknown pad %s, ignoring", GST_PAD_NAME (pad));
|
||||
}
|
||||
|
||||
gst_caps_unref (caps);
|
||||
}
|
||||
|
||||
static void
|
||||
on_incoming_stream (GstElement * webrtcbin, GstPad * pad, WebRTC * webrtc)
|
||||
{
|
||||
GstElement *decodebin;
|
||||
|
||||
if (GST_PAD_DIRECTION (pad) != GST_PAD_SRC)
|
||||
return;
|
||||
|
||||
decodebin = gst_element_factory_make ("decodebin", NULL);
|
||||
g_signal_connect (decodebin, "pad-added",
|
||||
G_CALLBACK (on_incoming_decodebin_stream), webrtc);
|
||||
gst_bin_add (GST_BIN (webrtc->pipe), decodebin);
|
||||
gst_element_sync_state_with_parent (decodebin);
|
||||
gst_element_link (webrtcbin, decodebin);
|
||||
}
|
||||
|
||||
static void
|
||||
send_ice_candidate_message (GstElement * webrtcbin G_GNUC_UNUSED, guint mlineindex,
|
||||
gchar * candidate, WebRTC * webrtc)
|
||||
{
|
||||
gchar *text;
|
||||
JsonObject *ice, *msg;
|
||||
|
||||
if (webrtc->app_state < PEER_CALL_NEGOTIATING) {
|
||||
cleanup_and_quit_loop (webrtc, "Can't send ICE, not in call", APP_STATE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
ice = json_object_new ();
|
||||
json_object_set_string_member (ice, "candidate", candidate);
|
||||
json_object_set_int_member (ice, "sdpMLineIndex", mlineindex);
|
||||
msg = json_object_new ();
|
||||
json_object_set_object_member (msg, "ice", ice);
|
||||
text = get_string_from_json_object (msg);
|
||||
json_object_unref (msg);
|
||||
|
||||
soup_websocket_connection_send_text (webrtc->ws_conn, text);
|
||||
g_free (text);
|
||||
}
|
||||
|
||||
static void
|
||||
send_sdp_offer (WebRTC * webrtc, GstWebRTCSessionDescription * offer)
|
||||
{
|
||||
gchar *text;
|
||||
JsonObject *msg, *sdp;
|
||||
|
||||
if (webrtc->app_state < PEER_CALL_NEGOTIATING) {
|
||||
cleanup_and_quit_loop (webrtc, "Can't send offer, not in call", APP_STATE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
text = gst_sdp_message_as_text (offer->sdp);
|
||||
g_print ("Sending offer:\n%s\n", text);
|
||||
|
||||
sdp = json_object_new ();
|
||||
json_object_set_string_member (sdp, "type", "offer");
|
||||
json_object_set_string_member (sdp, "sdp", text);
|
||||
g_free (text);
|
||||
|
||||
msg = json_object_new ();
|
||||
json_object_set_object_member (msg, "sdp", sdp);
|
||||
text = get_string_from_json_object (msg);
|
||||
json_object_unref (msg);
|
||||
|
||||
soup_websocket_connection_send_text (webrtc->ws_conn, text);
|
||||
g_free (text);
|
||||
}
|
||||
|
||||
/* Offer created by our pipeline, to be sent to the peer */
|
||||
static void
|
||||
on_offer_created (GstPromise * promise, WebRTC * webrtc)
|
||||
{
|
||||
GstWebRTCSessionDescription *offer = NULL;
|
||||
const GstStructure *reply;
|
||||
|
||||
g_assert (webrtc->app_state == PEER_CALL_NEGOTIATING);
|
||||
|
||||
g_assert (gst_promise_wait(promise) == GST_PROMISE_RESULT_REPLIED);
|
||||
reply = gst_promise_get_reply (promise);
|
||||
gst_structure_get (reply, "offer",
|
||||
GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &offer, NULL);
|
||||
gst_promise_unref (promise);
|
||||
|
||||
promise = gst_promise_new ();
|
||||
g_signal_emit_by_name (webrtc->webrtcbin, "set-local-description", offer, promise);
|
||||
gst_promise_interrupt (promise);
|
||||
gst_promise_unref (promise);
|
||||
|
||||
/* Send offer to peer */
|
||||
send_sdp_offer (webrtc, offer);
|
||||
gst_webrtc_session_description_free (offer);
|
||||
}
|
||||
|
||||
static void
|
||||
on_negotiation_needed (GstElement * element, WebRTC * webrtc)
|
||||
{
|
||||
GstPromise *promise;
|
||||
|
||||
webrtc->app_state = PEER_CALL_NEGOTIATING;
|
||||
promise = gst_promise_new_with_change_func (on_offer_created, webrtc, NULL);;
|
||||
g_signal_emit_by_name (webrtc->webrtcbin, "create-offer", NULL, promise);
|
||||
}
|
||||
|
||||
static void
|
||||
add_fec_to_offer (GstElement * webrtc)
|
||||
{
|
||||
GstWebRTCRTPTransceiver *trans = NULL;
|
||||
|
||||
/* A transceiver has already been created when a sink pad was
|
||||
* requested on the sending webrtcbin */
|
||||
g_signal_emit_by_name (webrtc, "get-transceiver", 0, &trans);
|
||||
|
||||
g_object_set (trans, "fec-type", GST_WEBRTC_FEC_TYPE_ULP_RED,
|
||||
"fec-percentage", 25, "do-nack", FALSE, NULL);
|
||||
}
|
||||
|
||||
#define RTP_CAPS_OPUS "application/x-rtp,media=audio,encoding-name=OPUS,payload=100"
|
||||
#define RTP_CAPS_VP8 "application/x-rtp,media=video,encoding-name=VP8,payload=101"
|
||||
|
||||
static gboolean
|
||||
start_pipeline (WebRTC * webrtc)
|
||||
{
|
||||
GstStateChangeReturn ret;
|
||||
GError *error = NULL;
|
||||
GstPad *pad;
|
||||
|
||||
webrtc->pipe =
|
||||
gst_parse_launch ("webrtcbin name=sendrecv "
|
||||
"ahcsrc device-facing=front ! video/x-raw,width=[320,1280] ! queue max-size-buffers=1 ! videoconvert ! "
|
||||
"vp8enc keyframe-max-dist=30 deadline=1 error-resilient=default ! rtpvp8pay picture-id-mode=15-bit mtu=1300 ! "
|
||||
"queue max-size-time=300000000 ! " RTP_CAPS_VP8 " ! sendrecv.sink_0 "
|
||||
"openslessrc ! queue ! audioconvert ! audioresample ! audiorate ! queue ! opusenc ! rtpopuspay ! "
|
||||
"queue ! " RTP_CAPS_OPUS " ! sendrecv.sink_1 ",
|
||||
&error);
|
||||
|
||||
if (error) {
|
||||
g_printerr ("Failed to parse launch: %s\n", error->message);
|
||||
g_error_free (error);
|
||||
goto err;
|
||||
}
|
||||
|
||||
webrtc->webrtcbin = gst_bin_get_by_name (GST_BIN (webrtc->pipe), "sendrecv");
|
||||
g_assert (webrtc->webrtcbin != NULL);
|
||||
add_fec_to_offer (webrtc->webrtcbin);
|
||||
|
||||
/* This is the gstwebrtc entry point where we create the offer and so on. It
|
||||
* will be called when the pipeline goes to PLAYING. */
|
||||
g_signal_connect (webrtc->webrtcbin, "on-negotiation-needed",
|
||||
G_CALLBACK (on_negotiation_needed), webrtc);
|
||||
/* We need to transmit this ICE candidate to the browser via the websockets
|
||||
* signalling server. Incoming ice candidates from the browser need to be
|
||||
* added by us too, see on_server_message() */
|
||||
g_signal_connect (webrtc->webrtcbin, "on-ice-candidate",
|
||||
G_CALLBACK (send_ice_candidate_message), webrtc);
|
||||
/* Incoming streams will be exposed via this signal */
|
||||
g_signal_connect (webrtc->webrtcbin, "pad-added", G_CALLBACK (on_incoming_stream),
|
||||
webrtc);
|
||||
/* Lifetime is the same as the pipeline itself */
|
||||
gst_object_unref (webrtc->webrtcbin);
|
||||
|
||||
g_print ("Starting pipeline\n");
|
||||
ret = gst_element_set_state (GST_ELEMENT (webrtc->pipe), GST_STATE_PLAYING);
|
||||
if (ret == GST_STATE_CHANGE_FAILURE)
|
||||
goto err;
|
||||
|
||||
return TRUE;
|
||||
|
||||
err:
|
||||
if (webrtc->pipe)
|
||||
g_clear_object (&webrtc->pipe);
|
||||
if (webrtc->webrtcbin)
|
||||
webrtc->webrtcbin = NULL;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
setup_call (WebRTC * webrtc)
|
||||
{
|
||||
gchar *msg;
|
||||
|
||||
if (soup_websocket_connection_get_state (webrtc->ws_conn) !=
|
||||
SOUP_WEBSOCKET_STATE_OPEN)
|
||||
return FALSE;
|
||||
|
||||
if (!webrtc->peer_id)
|
||||
return FALSE;
|
||||
|
||||
g_print ("Setting up signalling server call with %s\n", webrtc->peer_id);
|
||||
webrtc->app_state = PEER_CONNECTING;
|
||||
msg = g_strdup_printf ("SESSION %s", webrtc->peer_id);
|
||||
soup_websocket_connection_send_text (webrtc->ws_conn, msg);
|
||||
g_free (msg);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
register_with_server (WebRTC * webrtc)
|
||||
{
|
||||
gchar *hello;
|
||||
gint32 our_id;
|
||||
|
||||
if (soup_websocket_connection_get_state (webrtc->ws_conn) !=
|
||||
SOUP_WEBSOCKET_STATE_OPEN)
|
||||
return FALSE;
|
||||
|
||||
our_id = g_random_int_range (10, 10000);
|
||||
g_print ("Registering id %i with server\n", our_id);
|
||||
webrtc->app_state = SERVER_REGISTERING;
|
||||
|
||||
/* Register with the server with a random integer id. Reply will be received
|
||||
* by on_server_message() */
|
||||
hello = g_strdup_printf ("HELLO %i", our_id);
|
||||
soup_websocket_connection_send_text (webrtc->ws_conn, hello);
|
||||
g_free (hello);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
on_server_closed (SoupWebsocketConnection * conn G_GNUC_UNUSED,
|
||||
WebRTC * webrtc)
|
||||
{
|
||||
webrtc->app_state = SERVER_CLOSED;
|
||||
cleanup_and_quit_loop (webrtc, "Server connection closed", 0);
|
||||
}
|
||||
|
||||
/* One mega message handler for our asynchronous calling mechanism */
|
||||
static void
|
||||
on_server_message (SoupWebsocketConnection * conn, SoupWebsocketDataType type,
|
||||
GBytes * message, WebRTC * webrtc)
|
||||
{
|
||||
gsize size;
|
||||
gchar *text, *data;
|
||||
|
||||
switch (type) {
|
||||
case SOUP_WEBSOCKET_DATA_BINARY:
|
||||
g_printerr ("Received unknown binary message, ignoring\n");
|
||||
g_bytes_unref (message);
|
||||
return;
|
||||
case SOUP_WEBSOCKET_DATA_TEXT:
|
||||
data = g_bytes_unref_to_data (message, &size);
|
||||
/* Convert to NULL-terminated string */
|
||||
text = g_strndup (data, size);
|
||||
g_free (data);
|
||||
break;
|
||||
default:
|
||||
g_assert_not_reached ();
|
||||
}
|
||||
|
||||
/* Server has accepted our registration, we are ready to send commands */
|
||||
if (g_strcmp0 (text, "HELLO") == 0) {
|
||||
if (webrtc->app_state != SERVER_REGISTERING) {
|
||||
cleanup_and_quit_loop (webrtc, "ERROR: Received HELLO when not registering",
|
||||
APP_STATE_ERROR);
|
||||
goto out;
|
||||
}
|
||||
webrtc->app_state = SERVER_REGISTERED;
|
||||
g_print ("Registered with server\n");
|
||||
/* Ask signalling server to connect us with a specific peer */
|
||||
if (!setup_call (webrtc)) {
|
||||
cleanup_and_quit_loop (webrtc, "ERROR: Failed to setup call", PEER_CALL_ERROR);
|
||||
goto out;
|
||||
}
|
||||
/* Call has been setup by the server, now we can start negotiation */
|
||||
} else if (g_strcmp0 (text, "SESSION_OK") == 0) {
|
||||
if (webrtc->app_state != PEER_CONNECTING) {
|
||||
cleanup_and_quit_loop (webrtc, "ERROR: Received SESSION_OK when not calling",
|
||||
PEER_CONNECTION_ERROR);
|
||||
goto out;
|
||||
}
|
||||
|
||||
webrtc->app_state = PEER_CONNECTED;
|
||||
/* Start negotiation (exchange SDP and ICE candidates) */
|
||||
if (!start_pipeline (webrtc))
|
||||
cleanup_and_quit_loop (webrtc, "ERROR: failed to start pipeline",
|
||||
PEER_CALL_ERROR);
|
||||
/* Handle errors */
|
||||
} else if (g_str_has_prefix (text, "ERROR")) {
|
||||
switch (webrtc->app_state) {
|
||||
case SERVER_CONNECTING:
|
||||
webrtc->app_state = SERVER_CONNECTION_ERROR;
|
||||
break;
|
||||
case SERVER_REGISTERING:
|
||||
webrtc->app_state = SERVER_REGISTRATION_ERROR;
|
||||
break;
|
||||
case PEER_CONNECTING:
|
||||
webrtc->app_state = PEER_CONNECTION_ERROR;
|
||||
break;
|
||||
case PEER_CONNECTED:
|
||||
case PEER_CALL_NEGOTIATING:
|
||||
webrtc->app_state = PEER_CALL_ERROR;
|
||||
break;
|
||||
default:
|
||||
webrtc->app_state = APP_STATE_ERROR;
|
||||
}
|
||||
cleanup_and_quit_loop (webrtc, text, 0);
|
||||
/* Look for JSON messages containing SDP and ICE candidates */
|
||||
} else {
|
||||
JsonNode *root;
|
||||
JsonObject *object;
|
||||
JsonParser *parser = json_parser_new ();
|
||||
|
||||
g_print ("Got server message %s", text);
|
||||
|
||||
if (!json_parser_load_from_data (parser, text, -1, NULL)) {
|
||||
g_printerr ("Unknown message '%s', ignoring", text);
|
||||
g_object_unref (parser);
|
||||
goto out;
|
||||
}
|
||||
|
||||
root = json_parser_get_root (parser);
|
||||
if (!JSON_NODE_HOLDS_OBJECT (root)) {
|
||||
g_printerr ("Unknown json message '%s', ignoring", text);
|
||||
g_object_unref (parser);
|
||||
goto out;
|
||||
}
|
||||
|
||||
object = json_node_get_object (root);
|
||||
/* Check type of JSON message */
|
||||
if (json_object_has_member (object, "sdp")) {
|
||||
int ret;
|
||||
const gchar *text;
|
||||
GstSDPMessage *sdp;
|
||||
GstWebRTCSessionDescription *answer;
|
||||
|
||||
g_assert (webrtc->app_state == PEER_CALL_NEGOTIATING);
|
||||
|
||||
object = json_object_get_object_member (object, "sdp");
|
||||
|
||||
g_assert (json_object_has_member (object, "type"));
|
||||
|
||||
/* In this example, we always create the offer and receive one answer.
|
||||
* See tests/examples/webrtcbidirectional.c in gst-plugins-bad for how to
|
||||
* handle offers from peers and reply with answers using webrtcbin. */
|
||||
g_assert_cmpstr (json_object_get_string_member (object, "type"), ==,
|
||||
"answer");
|
||||
|
||||
text = json_object_get_string_member (object, "sdp");
|
||||
|
||||
g_print ("Received answer:\n%s\n", text);
|
||||
|
||||
ret = gst_sdp_message_new (&sdp);
|
||||
g_assert (ret == GST_SDP_OK);
|
||||
|
||||
ret = gst_sdp_message_parse_buffer (text, strlen (text), sdp);
|
||||
g_assert (ret == GST_SDP_OK);
|
||||
|
||||
answer = gst_webrtc_session_description_new (GST_WEBRTC_SDP_TYPE_ANSWER,
|
||||
sdp);
|
||||
g_assert (answer);
|
||||
|
||||
/* Set remote description on our pipeline */
|
||||
{
|
||||
GstPromise *promise = gst_promise_new ();
|
||||
g_signal_emit_by_name (webrtc->webrtcbin, "set-remote-description", answer,
|
||||
promise);
|
||||
gst_promise_interrupt (promise);
|
||||
gst_promise_unref (promise);
|
||||
}
|
||||
|
||||
webrtc->app_state = PEER_CALL_STARTED;
|
||||
} else if (json_object_has_member (object, "ice")) {
|
||||
JsonObject *ice;
|
||||
const gchar *candidate;
|
||||
gint sdpmlineindex;
|
||||
|
||||
ice = json_object_get_object_member (object, "ice");
|
||||
candidate = json_object_get_string_member (ice, "candidate");
|
||||
sdpmlineindex = json_object_get_int_member (ice, "sdpMLineIndex");
|
||||
|
||||
/* Add ice candidate sent by remote peer */
|
||||
g_signal_emit_by_name (webrtc->webrtcbin, "add-ice-candidate", sdpmlineindex,
|
||||
candidate);
|
||||
} else {
|
||||
g_printerr ("Ignoring unknown JSON message:\n%s\n", text);
|
||||
}
|
||||
g_object_unref (parser);
|
||||
}
|
||||
|
||||
out:
|
||||
g_free (text);
|
||||
}
|
||||
|
||||
static void
|
||||
on_server_connected (SoupSession * session, GAsyncResult * res,
|
||||
WebRTC * webrtc)
|
||||
{
|
||||
GError *error = NULL;
|
||||
|
||||
webrtc->ws_conn = soup_session_websocket_connect_finish (session, res, &error);
|
||||
if (error) {
|
||||
cleanup_and_quit_loop (webrtc, error->message, SERVER_CONNECTION_ERROR);
|
||||
g_error_free (error);
|
||||
return;
|
||||
}
|
||||
|
||||
g_assert (webrtc->ws_conn != NULL);
|
||||
|
||||
webrtc->app_state = SERVER_CONNECTED;
|
||||
g_print ("Connected to signalling server\n");
|
||||
|
||||
g_signal_connect (webrtc->ws_conn, "closed", G_CALLBACK (on_server_closed), webrtc);
|
||||
g_signal_connect (webrtc->ws_conn, "message", G_CALLBACK (on_server_message), webrtc);
|
||||
|
||||
/* Register with the server so it knows about us and can accept commands */
|
||||
register_with_server (webrtc);
|
||||
}
|
||||
|
||||
/*
|
||||
* Connect to the signalling server. This is the entrypoint for everything else.
|
||||
*/
|
||||
static gboolean
|
||||
connect_to_websocket_server_async (WebRTC * webrtc)
|
||||
{
|
||||
SoupLogger *logger;
|
||||
SoupMessage *message;
|
||||
SoupSession *session;
|
||||
const char *https_aliases[] = {"wss", NULL};
|
||||
const gchar *ca_certs;
|
||||
|
||||
ca_certs = g_getenv("CA_CERTIFICATES");
|
||||
g_assert (ca_certs != NULL);
|
||||
g_print ("ca-certificates %s", ca_certs);
|
||||
session = soup_session_new_with_options (SOUP_SESSION_SSL_STRICT, FALSE,
|
||||
// SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE,
|
||||
SOUP_SESSION_SSL_CA_FILE, ca_certs,
|
||||
SOUP_SESSION_HTTPS_ALIASES, https_aliases, NULL);
|
||||
|
||||
logger = soup_logger_new (SOUP_LOGGER_LOG_BODY, -1);
|
||||
soup_session_add_feature (session, SOUP_SESSION_FEATURE (logger));
|
||||
g_object_unref (logger);
|
||||
|
||||
message = soup_message_new (SOUP_METHOD_GET, webrtc->signalling_server);
|
||||
|
||||
g_print ("Connecting to server...\n");
|
||||
|
||||
/* Once connected, we will register */
|
||||
soup_session_websocket_connect_async (session, message, NULL, NULL, NULL,
|
||||
(GAsyncReadyCallback) on_server_connected, webrtc);
|
||||
webrtc->app_state = SERVER_CONNECTING;
|
||||
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/* Register this thread with the VM */
|
||||
static JNIEnv *
|
||||
attach_current_thread (void)
|
||||
{
|
||||
JNIEnv *env;
|
||||
JavaVMAttachArgs args;
|
||||
|
||||
GST_DEBUG ("Attaching thread %p", g_thread_self ());
|
||||
args.version = JNI_VERSION_1_4;
|
||||
args.name = NULL;
|
||||
args.group = NULL;
|
||||
|
||||
if ((*java_vm)->AttachCurrentThread (java_vm, &env, &args) < 0) {
|
||||
GST_ERROR ("Failed to attach current thread");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
/* Unregister this thread from the VM */
|
||||
static void
|
||||
detach_current_thread (void *env)
|
||||
{
|
||||
GST_DEBUG ("Detaching thread %p", g_thread_self ());
|
||||
(*java_vm)->DetachCurrentThread (java_vm);
|
||||
}
|
||||
|
||||
/* Retrieve the JNI environment for this thread */
|
||||
static JNIEnv *
|
||||
get_jni_env (void)
|
||||
{
|
||||
JNIEnv *env;
|
||||
|
||||
if ((env = pthread_getspecific (current_jni_env)) == NULL) {
|
||||
env = attach_current_thread ();
|
||||
pthread_setspecific (current_jni_env, env);
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
/*
|
||||
* Java Bindings
|
||||
*/
|
||||
|
||||
static void
|
||||
native_end_call (JNIEnv * env, jobject thiz)
|
||||
{
|
||||
WebRTC *webrtc = GET_CUSTOM_DATA (env, thiz, native_webrtc_field_id);
|
||||
|
||||
if (!webrtc)
|
||||
return;
|
||||
|
||||
g_mutex_lock (&webrtc->lock);
|
||||
if (webrtc->loop) {
|
||||
GThread *thread = webrtc->thread;
|
||||
|
||||
GST_INFO("Ending current call");
|
||||
cleanup_and_quit_loop (webrtc, NULL, 0);
|
||||
webrtc->thread = NULL;
|
||||
g_mutex_unlock (&webrtc->lock);
|
||||
g_thread_join (thread);
|
||||
} else {
|
||||
g_mutex_unlock (&webrtc->lock);
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
_unlock_mutex (GMutex * m)
|
||||
{
|
||||
g_mutex_unlock (m);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static gpointer
|
||||
_call_thread (WebRTC * webrtc)
|
||||
{
|
||||
GMainContext *context = NULL;
|
||||
JNIEnv *env = attach_current_thread();
|
||||
|
||||
g_mutex_lock (&webrtc->lock);
|
||||
|
||||
context = g_main_context_new ();
|
||||
webrtc->loop = g_main_loop_new (context, FALSE);
|
||||
g_main_context_invoke (context, (GSourceFunc) _unlock_mutex, &webrtc->lock);
|
||||
g_main_context_invoke (context, (GSourceFunc) connect_to_websocket_server_async, webrtc);
|
||||
g_main_context_push_thread_default (context);
|
||||
g_cond_broadcast (&webrtc->cond);
|
||||
g_main_loop_run (webrtc->loop);
|
||||
g_main_context_pop_thread_default (context);
|
||||
|
||||
detach_current_thread (env);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
native_call_other_party(JNIEnv * env, jobject thiz)
|
||||
{
|
||||
WebRTC *webrtc = GET_CUSTOM_DATA (env, thiz, native_webrtc_field_id);
|
||||
|
||||
if (!webrtc)
|
||||
return;
|
||||
|
||||
if (webrtc->thread)
|
||||
native_end_call (env, thiz);
|
||||
|
||||
GST_INFO("calling other party");
|
||||
|
||||
webrtc->thread = g_thread_new("webrtc", (GThreadFunc) _call_thread, webrtc);
|
||||
g_mutex_lock (&webrtc->lock);
|
||||
while (!webrtc->loop)
|
||||
g_cond_wait (&webrtc->cond, &webrtc->lock);
|
||||
g_mutex_unlock (&webrtc->lock);
|
||||
}
|
||||
|
||||
static void
|
||||
native_new (JNIEnv * env, jobject thiz)
|
||||
{
|
||||
WebRTC *webrtc = g_new0 (WebRTC, 1);
|
||||
|
||||
SET_CUSTOM_DATA (env, thiz, native_webrtc_field_id, webrtc);
|
||||
webrtc->java_webrtc = (*env)->NewGlobalRef (env, thiz);
|
||||
|
||||
webrtc->signalling_server = g_strdup (DEFAULT_SIGNALLING_SERVER);
|
||||
|
||||
g_mutex_init (&webrtc->lock);
|
||||
g_cond_init (&webrtc->cond);
|
||||
}
|
||||
|
||||
static void
|
||||
native_free (JNIEnv * env, jobject thiz)
|
||||
{
|
||||
WebRTC *webrtc = GET_CUSTOM_DATA (env, thiz, native_webrtc_field_id);
|
||||
|
||||
if (!webrtc)
|
||||
return;
|
||||
|
||||
(*env)->DeleteGlobalRef (env, webrtc->java_webrtc);
|
||||
|
||||
native_end_call (env, thiz);
|
||||
|
||||
g_cond_clear (&webrtc->cond);
|
||||
g_mutex_clear (&webrtc->lock);
|
||||
g_free (webrtc->peer_id);
|
||||
g_free (webrtc->signalling_server);
|
||||
g_free (webrtc);
|
||||
SET_CUSTOM_DATA (env, thiz, native_webrtc_field_id, NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
native_class_init (JNIEnv * env, jclass klass)
|
||||
{
|
||||
native_webrtc_field_id =
|
||||
(*env)->GetFieldID (env, klass, "native_webrtc", "J");
|
||||
|
||||
if (!native_webrtc_field_id) {
|
||||
static const gchar *message =
|
||||
"The calling class does not implement all necessary interface methods";
|
||||
jclass exception_class = (*env)->FindClass (env, "java/lang/Exception");
|
||||
__android_log_print (ANDROID_LOG_ERROR, "GstPlayer", "%s", message);
|
||||
(*env)->ThrowNew (env, exception_class, message);
|
||||
}
|
||||
|
||||
//gst_debug_set_threshold_from_string ("gl*:7", FALSE);
|
||||
}
|
||||
|
||||
static void
|
||||
native_set_surface (JNIEnv * env, jobject thiz, jobject surface)
|
||||
{
|
||||
WebRTC *webrtc= GET_CUSTOM_DATA (env, thiz, native_webrtc_field_id);
|
||||
ANativeWindow *new_native_window;
|
||||
|
||||
if (!webrtc)
|
||||
return;
|
||||
|
||||
new_native_window = surface ? ANativeWindow_fromSurface (env, surface) : NULL;
|
||||
GST_DEBUG ("Received surface %p (native window %p)", surface,
|
||||
new_native_window);
|
||||
|
||||
if (webrtc->native_window) {
|
||||
ANativeWindow_release (webrtc->native_window);
|
||||
}
|
||||
|
||||
webrtc->native_window = new_native_window;
|
||||
if (webrtc->video_sink)
|
||||
gst_video_overlay_set_window_handle (GST_VIDEO_OVERLAY (webrtc->video_sink), (guintptr) new_native_window);
|
||||
}
|
||||
|
||||
static void
|
||||
native_set_signalling_server (JNIEnv * env, jobject thiz, jstring server) {
|
||||
WebRTC *webrtc= GET_CUSTOM_DATA (env, thiz, native_webrtc_field_id);
|
||||
const gchar *s;
|
||||
|
||||
if (!webrtc)
|
||||
return;
|
||||
|
||||
s = (*env)->GetStringUTFChars(env, server, NULL);
|
||||
if (webrtc->signalling_server)
|
||||
g_free (webrtc->signalling_server);
|
||||
webrtc->signalling_server = g_strdup (s);
|
||||
(*env)->ReleaseStringUTFChars(env, server, s);
|
||||
}
|
||||
|
||||
static void
|
||||
native_set_call_id(JNIEnv * env, jobject thiz, jstring peer_id) {
|
||||
WebRTC *webrtc = GET_CUSTOM_DATA (env, thiz, native_webrtc_field_id);
|
||||
const gchar *s;
|
||||
|
||||
if (!webrtc)
|
||||
return;
|
||||
|
||||
s = (*env)->GetStringUTFChars(env, peer_id, NULL);
|
||||
g_free (webrtc->peer_id);
|
||||
webrtc->peer_id = g_strdup (s);
|
||||
(*env)->ReleaseStringUTFChars(env, peer_id, s);
|
||||
}
|
||||
|
||||
/* List of implemented native methods */
|
||||
static JNINativeMethod native_methods[] = {
|
||||
{"nativeClassInit", "()V", (void *) native_class_init},
|
||||
{"nativeNew", "()V", (void *) native_new},
|
||||
{"nativeFree", "()V", (void *) native_free},
|
||||
{"nativeSetSurface", "(Landroid/view/Surface;)V",
|
||||
(void *) native_set_surface},
|
||||
{"nativeSetSignallingServer", "(Ljava/lang/String;)V",
|
||||
(void *) native_set_signalling_server},
|
||||
{"nativeSetCallID", "(Ljava/lang/String;)V",
|
||||
(void *) native_set_call_id},
|
||||
{"nativeCallOtherParty", "()V",
|
||||
(void *) native_call_other_party},
|
||||
{"nativeEndCall", "()V",
|
||||
(void *) native_end_call}
|
||||
};
|
||||
|
||||
/* Library initializer */
|
||||
jint
|
||||
JNI_OnLoad (JavaVM * vm, void *reserved)
|
||||
{
|
||||
JNIEnv *env = NULL;
|
||||
|
||||
java_vm = vm;
|
||||
|
||||
if ((*vm)->GetEnv (vm, (void **) &env, JNI_VERSION_1_4) != JNI_OK) {
|
||||
__android_log_print (ANDROID_LOG_ERROR, "GstPlayer",
|
||||
"Could not retrieve JNIEnv");
|
||||
return 0;
|
||||
}
|
||||
jclass klass = (*env)->FindClass (env, "org/freedesktop/gstreamer/WebRTC");
|
||||
if (!klass) {
|
||||
__android_log_print (ANDROID_LOG_ERROR, "GstWebRTC",
|
||||
"Could not retrieve class org.freedesktop.gstreamer.WebRTC");
|
||||
return 0;
|
||||
}
|
||||
if ((*env)->RegisterNatives (env, klass, native_methods,
|
||||
G_N_ELEMENTS (native_methods))) {
|
||||
__android_log_print (ANDROID_LOG_ERROR, "GstWebRTC",
|
||||
"Could not register native methods for org.freedesktop.gstreamer.WebRTC");
|
||||
return 0;
|
||||
}
|
||||
|
||||
pthread_key_create (¤t_jni_env, detach_current_thread);
|
||||
|
||||
return JNI_VERSION_1_4;
|
||||
}
|
124
webrtc/android/app/src/main/res/layout/main.xml
Normal file
124
webrtc/android/app/src/main/res/layout/main.xml
Normal file
|
@ -0,0 +1,124 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
|
||||
<TableLayout
|
||||
android:id="@+id/input"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:stretchColumns="1"
|
||||
app:layout_constraintBottom_toTopOf="@+id/controls"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="1.0"
|
||||
app:layout_constraintVertical_chainStyle="spread_inside">
|
||||
|
||||
<TableRow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/URL"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="right|center_vertical"
|
||||
android:padding="8dp"
|
||||
android:text="URL" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/URLText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:inputType="textUri"
|
||||
android:text="wss://webrtc.nirbheek.in:8443" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ID"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="right|center_vertical"
|
||||
android:padding="8dp"
|
||||
android:text="ID" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/IDText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:inputType="number"
|
||||
android:text="ID" />
|
||||
</TableRow>
|
||||
|
||||
</TableLayout>
|
||||
|
||||
|
||||
<android.support.constraint.ConstraintLayout
|
||||
android:id="@+id/controls"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toTopOf="@id/surface_video"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/input">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_play"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:contentDescription="@string/button_play"
|
||||
android:src="@android:drawable/ic_media_play"
|
||||
android:text="@string/button_play"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/button_pause"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_pause"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:contentDescription="@string/button_pause"
|
||||
android:src="@android:drawable/ic_media_pause"
|
||||
android:text="@string/button_pause"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/button_play"
|
||||
app:layout_constraintTop_toTopOf="@+id/button_play" />
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
|
||||
|
||||
<org.freedesktop.gstreamer.webrtc.GStreamerSurfaceView
|
||||
android:id="@+id/surface_video"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/controls"
|
||||
app:layout_constraintVertical_bias="1.0" />
|
||||
|
||||
<android.support.constraint.Guideline
|
||||
android:id="@+id/guideline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_begin="50dp" />
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
6
webrtc/android/app/src/main/res/values/strings.xml
Normal file
6
webrtc/android/app/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">GStreamer WebRTC</string>
|
||||
<string name="button_play">Play</string>
|
||||
<string name="button_pause">Pause</string>
|
||||
</resources>
|
33
webrtc/android/build.gradle
Normal file
33
webrtc/android/build.gradle
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
ext {
|
||||
supportLibVersion = '27.1.1' // variable that can be referenced to keep support libs consistent
|
||||
commonLibVersion= '2.12.4'
|
||||
versionBuildTool = '28.0.3'
|
||||
versionCompiler = 28
|
||||
versionTarget = 27
|
||||
versionNameString = '1.0.0'
|
||||
javaSourceCompatibility = JavaVersion.VERSION_1_8
|
||||
javaTargetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
23
webrtc/android/gradle.properties
Normal file
23
webrtc/android/gradle.properties
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx10248m -XX:MaxPermSize=256m
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# gstAndroidRoot can be set to point to the unpacked GStreamer android top-level directory
|
||||
# containing each architecture in subdirectories, or else set the GSTREAMER_ROOT_ANDROID
|
||||
# environment variable to that location
|
||||
# gstAndroidRoot=/home/matt/Projects/cerbero/build/dist/android_universal
|
172
webrtc/android/gradlew
vendored
Executable file
172
webrtc/android/gradlew
vendored
Executable file
|
@ -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" "$@"
|
84
webrtc/android/gradlew.bat
vendored
Normal file
84
webrtc/android/gradlew.bat
vendored
Normal file
|
@ -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
|
1
webrtc/android/settings.gradle
Normal file
1
webrtc/android/settings.gradle
Normal file
|
@ -0,0 +1 @@
|
|||
include ':app'
|
144
webrtc/check/basic.py
Normal file
144
webrtc/check/basic.py
Normal file
|
@ -0,0 +1,144 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.support.wait import WebDriverWait
|
||||
from selenium.webdriver.firefox.firefox_profile import FirefoxProfile
|
||||
from selenium.webdriver.chrome.options import Options as COptions
|
||||
import webrtc_sendrecv as webrtc
|
||||
import simple_server as sserver
|
||||
import asyncio
|
||||
import threading
|
||||
import signal
|
||||
|
||||
import gi
|
||||
gi.require_version('Gst', '1.0')
|
||||
from gi.repository import Gst
|
||||
|
||||
thread = None
|
||||
stop = None
|
||||
server = None
|
||||
|
||||
class AsyncIOThread(threading.Thread):
|
||||
def __init__ (self, loop):
|
||||
threading.Thread.__init__(self)
|
||||
self.loop = loop
|
||||
|
||||
def run(self):
|
||||
asyncio.set_event_loop(self.loop)
|
||||
self.loop.run_forever()
|
||||
self.loop.close()
|
||||
print ("closed loop")
|
||||
|
||||
def stop_thread(self):
|
||||
self.loop.call_soon_threadsafe(self.loop.stop)
|
||||
|
||||
async def run_until(server, stop_token):
|
||||
async with server:
|
||||
await stop_token
|
||||
print ("run_until done")
|
||||
|
||||
def setUpModule():
|
||||
global thread, server
|
||||
Gst.init(None)
|
||||
cacerts_path = os.environ.get('TEST_CA_CERT_PATH')
|
||||
loop = asyncio.new_event_loop()
|
||||
|
||||
thread = AsyncIOThread(loop)
|
||||
thread.start()
|
||||
server = sserver.WebRTCSimpleServer('127.0.0.1', 8443, 20, False, cacerts_path)
|
||||
def f():
|
||||
global stop
|
||||
stop = asyncio.ensure_future(server.run())
|
||||
loop.call_soon_threadsafe(f)
|
||||
|
||||
def tearDownModule():
|
||||
global thread, stop
|
||||
stop.cancel()
|
||||
thread.stop_thread()
|
||||
thread.join()
|
||||
print("thread joined")
|
||||
|
||||
def valid_int(n):
|
||||
if isinstance(n, int):
|
||||
return True
|
||||
if isinstance(n, str):
|
||||
try:
|
||||
num = int(n)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
return False
|
||||
|
||||
def create_firefox_driver():
|
||||
capabilities = webdriver.DesiredCapabilities().FIREFOX.copy()
|
||||
capabilities['acceptSslCerts'] = True
|
||||
capabilities['acceptInsecureCerts'] = True
|
||||
profile = FirefoxProfile()
|
||||
profile.set_preference ('media.navigator.streams.fake', True)
|
||||
profile.set_preference ('media.navigator.permission.disabled', True)
|
||||
|
||||
return webdriver.Firefox(firefox_profile=profile, capabilities=capabilities)
|
||||
|
||||
def create_chrome_driver():
|
||||
capabilities = webdriver.DesiredCapabilities().CHROME.copy()
|
||||
capabilities['acceptSslCerts'] = True
|
||||
capabilities['acceptInsecureCerts'] = True
|
||||
copts = COptions()
|
||||
copts.add_argument('--allow-file-access-from-files')
|
||||
copts.add_argument('--use-fake-ui-for-media-stream')
|
||||
copts.add_argument('--use-fake-device-for-media-stream')
|
||||
copts.add_argument('--enable-blink-features=RTCUnifiedPlanByDefault')
|
||||
|
||||
return webdriver.Chrome(options=copts, desired_capabilities=capabilities)
|
||||
|
||||
class ServerConnectionTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.browser = create_firefox_driver()
|
||||
# self.browser = create_chrome_driver()
|
||||
self.addCleanup(self.browser.quit)
|
||||
self.html_source = os.environ.get('TEST_HTML_SOURCE')
|
||||
self.assertIsNot(self.html_source, None)
|
||||
self.assertNotEqual(self.html_source, '')
|
||||
self.html_source = 'file://' + self.html_source + '/index.html'
|
||||
|
||||
def get_peer_id(self):
|
||||
self.browser.get(self.html_source)
|
||||
peer_id = WebDriverWait(self.browser, 5).until(
|
||||
lambda x: x.find_element_by_id('peer-id'),
|
||||
message='Peer-id element was never seen'
|
||||
)
|
||||
WebDriverWait (self.browser, 5).until(
|
||||
lambda x: valid_int(peer_id.text),
|
||||
message='Peer-id never became a number'
|
||||
)
|
||||
return int(peer_id.text)
|
||||
|
||||
def testPeerID(self):
|
||||
self.get_peer_id()
|
||||
|
||||
def testPerformCall(self):
|
||||
loop = asyncio.new_event_loop()
|
||||
thread = AsyncIOThread(loop)
|
||||
thread.start()
|
||||
peer_id = self.get_peer_id()
|
||||
client = webrtc.WebRTCClient(peer_id + 1, peer_id, 'wss://127.0.0.1:8443')
|
||||
|
||||
async def do_things():
|
||||
await client.connect()
|
||||
async def stop_after(client, delay):
|
||||
await asyncio.sleep(delay)
|
||||
await client.stop()
|
||||
future = asyncio.ensure_future (stop_after (client, 5))
|
||||
res = await client.loop()
|
||||
thread.stop_thread()
|
||||
return res
|
||||
|
||||
res = asyncio.run_coroutine_threadsafe(do_things(), loop).result()
|
||||
thread.join()
|
||||
print ("client thread joined")
|
||||
self.assertEqual(res, 0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2)
|
15
webrtc/check/meson.build
Normal file
15
webrtc/check/meson.build
Normal file
|
@ -0,0 +1,15 @@
|
|||
tests = [
|
||||
['basic', 'basic.py'],
|
||||
]
|
||||
|
||||
test_deps = [certs]
|
||||
|
||||
foreach elem : tests
|
||||
test(elem.get(0),
|
||||
py3,
|
||||
depends: test_deps,
|
||||
args : files(elem.get(1)),
|
||||
env : ['PYTHONPATH=' + join_paths(meson.source_root(), 'sendrecv', 'gst') + ':' + join_paths(meson.source_root(), 'signalling'),
|
||||
'TEST_HTML_SOURCE=' + join_paths(meson.source_root(), 'sendrecv', 'js'),
|
||||
'TEST_CA_CERT_PATH=' + join_paths(meson.build_root(), 'signalling')])
|
||||
endforeach
|
17
webrtc/check/validate/README.md
Normal file
17
webrtc/check/validate/README.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
# What is this?
|
||||
|
||||
The entire contents of this folder perform testing of GStreamer's webrtc
|
||||
implementation against browser implementations using the selenium webdriver
|
||||
testing framework.
|
||||
|
||||
# Dependencies:
|
||||
|
||||
- gst-validate: https://gitlab.freedesktop.org/gstreamer/gst-devtools/tree/master/validate
|
||||
- gst-python: https://gitlab.freedesktop.org/gstreamer/gst-python/
|
||||
- selenium: https://www.seleniumhq.org/projects/webdriver/
|
||||
- selenium python bindings
|
||||
- chrome and firefox with webdriver support
|
||||
|
||||
# Run the tests
|
||||
|
||||
`GST_VALIDATE_APPS_DIR=/path/to/gstwebrtc-demos/check/validate/apps/ GST_VALIDATE_SCENARIOS_PATH=/path/to/gstwebrtc-demos/check/validate/scenarios/ gst-validate-launcher --testsuites-dir /path/to/gstwebrtc-demos/check/validate/testsuites/ webrtc`
|
106
webrtc/check/validate/actions.py
Normal file
106
webrtc/check/validate/actions.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
# Copyright (c) 2020, Matthew Waters <matthew@centricular.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this program; if not, write to the
|
||||
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
import gi
|
||||
gi.require_version("GstValidate", "1.0")
|
||||
from gi.repository import GstValidate
|
||||
|
||||
from observer import Signal
|
||||
from enums import Actions
|
||||
|
||||
import logging
|
||||
|
||||
l = logging.getLogger(__name__)
|
||||
|
||||
class ActionObserver(object):
|
||||
def __init__(self):
|
||||
def _action_continue(val):
|
||||
return val not in [GstValidate.ActionReturn.ERROR, GstValidate.ActionReturn.ERROR_REPORTED]
|
||||
def _action_accum(previous, val):
|
||||
# we want to always keep any errors propagated
|
||||
if val in [GstValidate.ActionReturn.ERROR, GstValidate.ActionReturn.ERROR_REPORTED]:
|
||||
return val
|
||||
if previous in [GstValidate.ActionReturn.ERROR, GstValidate.ActionReturn.ERROR_REPORTED]:
|
||||
return previous
|
||||
|
||||
# we want to always prefer async returns
|
||||
if previous in [GstValidate.ActionReturn.ASYNC, GstValidate.ActionReturn.INTERLACED]:
|
||||
return previous
|
||||
if val in [GstValidate.ActionReturn.ASYNC, GstValidate.ActionReturn.INTERLACED]:
|
||||
return val
|
||||
|
||||
return val
|
||||
|
||||
self.action = Signal(_action_continue, _action_accum)
|
||||
|
||||
def _action(self, scenario, action):
|
||||
l.debug('executing action: ' + str(action.structure))
|
||||
return self.action.fire (Actions(action.structure.get_name()), action)
|
||||
|
||||
|
||||
def register_action_types(observer):
|
||||
if not isinstance(observer, ActionObserver):
|
||||
raise TypeError
|
||||
|
||||
GstValidate.register_action_type(Actions.CREATE_OFFER.value,
|
||||
"webrtc", observer._action, None,
|
||||
"Instruct a create-offer to commence",
|
||||
GstValidate.ActionTypeFlags.NONE)
|
||||
GstValidate.register_action_type(Actions.CREATE_ANSWER.value,
|
||||
"webrtc", observer._action, None,
|
||||
"Create answer",
|
||||
GstValidate.ActionTypeFlags.NONE)
|
||||
GstValidate.register_action_type(Actions.WAIT_FOR_NEGOTIATION_STATE.value,
|
||||
"webrtc", observer._action, None,
|
||||
"Wait for a specific negotiation state to be reached",
|
||||
GstValidate.ActionTypeFlags.NONE)
|
||||
GstValidate.register_action_type(Actions.ADD_STREAM.value,
|
||||
"webrtc", observer._action, None,
|
||||
"Add a stream to the webrtcbin",
|
||||
GstValidate.ActionTypeFlags.NONE)
|
||||
GstValidate.register_action_type(Actions.ADD_DATA_CHANNEL.value,
|
||||
"webrtc", observer._action, None,
|
||||
"Add a data channel to the webrtcbin",
|
||||
GstValidate.ActionTypeFlags.NONE)
|
||||
GstValidate.register_action_type(Actions.SEND_DATA_CHANNEL_STRING.value,
|
||||
"webrtc", observer._action, None,
|
||||
"Send a message using a data channel",
|
||||
GstValidate.ActionTypeFlags.NONE)
|
||||
GstValidate.register_action_type(Actions.WAIT_FOR_DATA_CHANNEL_STATE.value,
|
||||
"webrtc", observer._action, None,
|
||||
"Wait for data channel to reach state",
|
||||
GstValidate.ActionTypeFlags.NONE)
|
||||
GstValidate.register_action_type(Actions.CLOSE_DATA_CHANNEL.value,
|
||||
"webrtc", observer._action, None,
|
||||
"Close a data channel",
|
||||
GstValidate.ActionTypeFlags.NONE)
|
||||
GstValidate.register_action_type(Actions.WAIT_FOR_DATA_CHANNEL.value,
|
||||
"webrtc", observer._action, None,
|
||||
"Wait for a data channel to appear",
|
||||
GstValidate.ActionTypeFlags.NONE)
|
||||
GstValidate.register_action_type(Actions.WAIT_FOR_DATA_CHANNEL_STRING.value,
|
||||
"webrtc", observer._action, None,
|
||||
"Wait for a data channel to receive a message",
|
||||
GstValidate.ActionTypeFlags.NONE)
|
||||
GstValidate.register_action_type(Actions.WAIT_FOR_NEGOTIATION_NEEDED.value,
|
||||
"webrtc", observer._action, None,
|
||||
"Wait for a the on-negotiation-needed signal to fire",
|
||||
GstValidate.ActionTypeFlags.NONE)
|
||||
GstValidate.register_action_type(Actions.SET_WEBRTC_OPTIONS.value,
|
||||
"webrtc", observer._action, None,
|
||||
"Set some webrtc options",
|
||||
GstValidate.ActionTypeFlags.NONE)
|
0
webrtc/check/validate/apps/__init__.py
Normal file
0
webrtc/check/validate/apps/__init__.py
Normal file
289
webrtc/check/validate/apps/gstwebrtc.py
Normal file
289
webrtc/check/validate/apps/gstwebrtc.py
Normal file
|
@ -0,0 +1,289 @@
|
|||
# Copyright (c) 2020, Matthew Waters <matthew@centricular.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this program; if not, write to the
|
||||
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import itertools
|
||||
|
||||
import tempfile
|
||||
|
||||
from launcher.baseclasses import TestsManager, GstValidateTest, ScenarioManager
|
||||
from launcher.utils import DEFAULT_TIMEOUT
|
||||
|
||||
DEFAULT_BROWSERS = ['firefox', 'chrome']
|
||||
|
||||
# list of scenarios. These are the names of the actual scenario files stored
|
||||
# on disk.
|
||||
DEFAULT_SCENARIOS = [
|
||||
"offer_answer",
|
||||
"vp8_send_stream",
|
||||
"open_data_channel",
|
||||
"send_data_channel_string",
|
||||
]
|
||||
|
||||
# various configuration changes that are included from other scenarios.
|
||||
# key is the name of the override used in the name of the test
|
||||
# value is the subdirectory where the override is placed
|
||||
# changes some things about the test like:
|
||||
# - who initiates the negotiation
|
||||
# - bundle settings
|
||||
SCENARIO_OVERRIDES = {
|
||||
# name : directory
|
||||
|
||||
# who starts the negotiation
|
||||
'local' : 'local_initiates_negotiation',
|
||||
'remote' : 'remote_initiates_negotiation',
|
||||
|
||||
# bundle-policy configuration
|
||||
# XXX: webrtcbin's bundle-policy=none is not part of the spec
|
||||
'none_compat' : 'bundle_local_none_remote_max_compat',
|
||||
'none_balanced' : 'bundle_local_none_remote_balanced',
|
||||
'none_bundle' : 'bundle_local_none_remote_max_bundle',
|
||||
'compat_compat' : 'bundle_local_max_compat_remote_max_compat',
|
||||
'compat_balanced' : 'bundle_local_max_compat_remote_balanced',
|
||||
'compat_bundle' : 'bundle_local_max_compat_remote_max_bundle',
|
||||
'balanced_compat' : 'bundle_local_balanced_remote_max_compat',
|
||||
'balanced_balanced' : 'bundle_local_balanced_remote_balanced',
|
||||
'balanced_bundle' : 'bundle_local_balanced_remote_bundle',
|
||||
'bundle_compat' : 'bundle_local_max_bundle_remote_max_compat',
|
||||
'bundle_balanced' : 'bundle_local_max_bundle_remote_balanced',
|
||||
'bundle_bundle' : 'bundle_local_max_bundle_remote_max_bundle',
|
||||
}
|
||||
|
||||
bundle_options = ['compat', 'balanced', 'bundle']
|
||||
|
||||
# Given an override, these are the choices to choose from. Each choice is a
|
||||
# separate test
|
||||
OVERRIDE_CHOICES = {
|
||||
'initiator' : ['local', 'remote'],
|
||||
'bundle' : ['_'.join(opt) for opt in itertools.product(['none'] + bundle_options, bundle_options)],
|
||||
}
|
||||
|
||||
# Which scenarios support which override. All the overrides will be chosen
|
||||
SCENARIO_OVERRIDES_SUPPORTED = {
|
||||
"offer_answer" : ['initiator', 'bundle'],
|
||||
"vp8_send_stream" : ['initiator', 'bundle'],
|
||||
"open_data_channel" : ['initiator', 'bundle'],
|
||||
"send_data_channel_string" : ['initiator', 'bundle'],
|
||||
}
|
||||
|
||||
# Things that don't work for some reason or another.
|
||||
DEFAULT_BLACKLIST = [
|
||||
(r"webrtc\.firefox\.local\..*offer_answer",
|
||||
"Firefox doesn't like a SDP without any media"),
|
||||
(r"webrtc.*remote.*vp8_send_stream",
|
||||
"We can't match payload types with a remote offer and a sending stream"),
|
||||
(r"webrtc.*\.balanced_.*",
|
||||
"webrtcbin doesn't implement bundle-policy=balanced"),
|
||||
(r"webrtc.*\.none_bundle.*",
|
||||
"Browsers want a BUNDLE group if in max-bundle mode"),
|
||||
]
|
||||
|
||||
class MutableInt(object):
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
class GstWebRTCTest(GstValidateTest):
|
||||
__used_ports = set()
|
||||
__last_id = MutableInt(10)
|
||||
|
||||
@classmethod
|
||||
def __get_open_port(cls):
|
||||
while True:
|
||||
# hackish trick from
|
||||
# http://stackoverflow.com/questions/2838244/get-open-tcp-port-in-python?answertab=votes#tab-top
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.bind(("", 0))
|
||||
port = s.getsockname()[1]
|
||||
if port not in cls.__used_ports:
|
||||
cls.__used_ports.add(port)
|
||||
s.close()
|
||||
return port
|
||||
|
||||
s.close()
|
||||
|
||||
@classmethod
|
||||
def __get_available_peer_id(cls):
|
||||
# each connection uses two peer ids
|
||||
peerid = cls.__last_id.value
|
||||
cls.__last_id.value += 2
|
||||
return peerid
|
||||
|
||||
def __init__(self, classname, tests_manager, scenario, browser, scenario_override_includes=None, timeout=DEFAULT_TIMEOUT):
|
||||
super().__init__("python3",
|
||||
classname,
|
||||
tests_manager.options,
|
||||
tests_manager.reporter,
|
||||
timeout=timeout,
|
||||
scenario=scenario)
|
||||
self.webrtc_server = None
|
||||
filename = inspect.getframeinfo (inspect.currentframe ()).filename
|
||||
self.current_file_path = os.path.dirname (os.path.abspath (filename))
|
||||
self.certdir = None
|
||||
self.browser = browser
|
||||
self.scenario_override_includes = scenario_override_includes
|
||||
|
||||
def launch_server(self):
|
||||
if self.options.redirect_logs == 'stdout':
|
||||
self.webrtcserver_logs = sys.stdout
|
||||
elif self.options.redirect_logs == 'stderr':
|
||||
self.webrtcserver_logs = sys.stderr
|
||||
else:
|
||||
self.webrtcserver_logs = open(self.logfile + '_webrtcserver.log', 'w+')
|
||||
self.extra_logfiles.add(self.webrtcserver_logs.name)
|
||||
|
||||
generate_certs_location = os.path.join(self.current_file_path, "..", "..", "..", "signalling", "generate_cert.sh")
|
||||
self.certdir = tempfile.mkdtemp()
|
||||
command = [generate_certs_location, self.certdir]
|
||||
|
||||
server_env = os.environ.copy()
|
||||
|
||||
subprocess.run(command,
|
||||
stderr=self.webrtcserver_logs,
|
||||
stdout=self.webrtcserver_logs,
|
||||
env=server_env)
|
||||
|
||||
self.server_port = self.__get_open_port()
|
||||
|
||||
server_location = os.path.join(self.current_file_path, "..", "..", "..", "signalling", "simple_server.py")
|
||||
command = [server_location, "--cert-path", self.certdir, "--addr", "127.0.0.1", "--port", str(self.server_port)]
|
||||
|
||||
self.webrtc_server = subprocess.Popen(command,
|
||||
stderr=self.webrtcserver_logs,
|
||||
stdout=self.webrtcserver_logs,
|
||||
env=server_env)
|
||||
while True:
|
||||
s = socket.socket()
|
||||
try:
|
||||
s.connect((("127.0.0.1", self.server_port)))
|
||||
break
|
||||
except ConnectionRefusedError:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
return ' '.join(command)
|
||||
|
||||
def build_arguments(self):
|
||||
gst_id = self.__get_available_peer_id()
|
||||
web_id = gst_id + 1
|
||||
|
||||
self.add_arguments(os.path.join(self.current_file_path, '..', 'webrtc_validate.py'))
|
||||
self.add_arguments('--server')
|
||||
self.add_arguments("wss://127.0.0.1:%s" % (self.server_port,))
|
||||
self.add_arguments('--browser')
|
||||
self.add_arguments(self.browser)
|
||||
self.add_arguments("--html-source")
|
||||
html_page = os.path.join(self.current_file_path, '..', 'web', 'single_stream.html')
|
||||
html_params = '?server=127.0.0.1&port=' + str(self.server_port) + '&id=' + str(web_id)
|
||||
self.add_arguments("file://" + html_page + html_params)
|
||||
self.add_arguments("--name")
|
||||
self.add_arguments(self.classname)
|
||||
self.add_arguments('--peer-id')
|
||||
self.add_arguments(str(web_id))
|
||||
self.add_arguments(str(gst_id))
|
||||
|
||||
def close_logfile(self):
|
||||
super().close_logfile()
|
||||
if not self.options.redirect_logs:
|
||||
self.webrtcserver_logs.close()
|
||||
|
||||
def process_update(self):
|
||||
res = super().process_update()
|
||||
if res:
|
||||
kill_subprocess(self, self.webrtc_server, DEFAULT_TIMEOUT)
|
||||
self.__used_ports.remove(self.server_port)
|
||||
if self.certdir:
|
||||
shutil.rmtree(self.certdir, ignore_errors=True)
|
||||
|
||||
return res
|
||||
|
||||
def get_subproc_env(self):
|
||||
env = super().get_subproc_env()
|
||||
if not self.scenario_override_includes:
|
||||
return env
|
||||
|
||||
# this feels gross...
|
||||
paths = env.get('GST_VALIDATE_SCENARIOS_PATH', '').split(os.pathsep)
|
||||
new_paths = []
|
||||
for p in paths:
|
||||
new_paths.append(p)
|
||||
for override_path in self.scenario_override_includes:
|
||||
new_p = os.path.join(p, override_path)
|
||||
if os.path.exists (new_p):
|
||||
new_paths.append(new_p)
|
||||
env['GST_VALIDATE_SCENARIOS_PATH'] = os.pathsep.join(new_paths)
|
||||
|
||||
return env
|
||||
|
||||
class GstWebRTCTestsManager(TestsManager):
|
||||
scenarios_manager = ScenarioManager()
|
||||
name = "webrtc"
|
||||
|
||||
def __init__(self):
|
||||
super(GstWebRTCTestsManager, self).__init__()
|
||||
self.loading_testsuite = self.name
|
||||
self._scenarios = []
|
||||
|
||||
def add_scenarios(self, scenarios):
|
||||
if isinstance(scenarios, list):
|
||||
self._scenarios.extend(scenarios)
|
||||
else:
|
||||
self._scenarios.append(scenarios)
|
||||
|
||||
self._scenarios = list(set(self._scenarios))
|
||||
|
||||
def set_scenarios(self, scenarios):
|
||||
self._scenarios = []
|
||||
self.add_scenarios(scenarios)
|
||||
|
||||
def get_scenarios(self):
|
||||
return self._scenarios
|
||||
|
||||
def populate_testsuite(self):
|
||||
self.add_scenarios (DEFAULT_SCENARIOS)
|
||||
self.set_default_blacklist(DEFAULT_BLACKLIST)
|
||||
|
||||
def list_tests(self):
|
||||
if self.tests:
|
||||
return self.tests
|
||||
|
||||
scenarios = [(scenario_name, self.scenarios_manager.get_scenario(scenario_name))
|
||||
for scenario_name in self.get_scenarios()]
|
||||
|
||||
for browser in DEFAULT_BROWSERS:
|
||||
for name, scenario in scenarios:
|
||||
if not scenario:
|
||||
self.warning("Could not find scenario %s" % name)
|
||||
continue
|
||||
if not SCENARIO_OVERRIDES_SUPPORTED[name]:
|
||||
# no override choices supported
|
||||
classname = browser + '.' + name
|
||||
print ("adding", classname)
|
||||
self.add_test(GstWebRTCTest(classname, self, scenario, browser))
|
||||
else:
|
||||
for overrides in itertools.product(*[OVERRIDE_CHOICES[c] for c in SCENARIO_OVERRIDES_SUPPORTED[name]]):
|
||||
oname = '.'.join (overrides)
|
||||
opaths = [SCENARIO_OVERRIDES[p] for p in overrides]
|
||||
classname = browser + '.' + oname + '.' + name
|
||||
print ("adding", classname)
|
||||
self.add_test(GstWebRTCTest(classname, self, scenario, browser, opaths))
|
||||
|
||||
return self.tests
|
88
webrtc/check/validate/browser.py
Normal file
88
webrtc/check/validate/browser.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
# Copyright (c) 2020, Matthew Waters <matthew@centricular.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this program; if not, write to the
|
||||
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
import logging
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.support.wait import WebDriverWait
|
||||
from selenium.webdriver.firefox.firefox_profile import FirefoxProfile
|
||||
from selenium.webdriver.chrome.options import Options as COptions
|
||||
|
||||
l = logging.getLogger(__name__)
|
||||
|
||||
def create_firefox_driver():
|
||||
capabilities = webdriver.DesiredCapabilities().FIREFOX.copy()
|
||||
capabilities['acceptSslCerts'] = True
|
||||
profile = FirefoxProfile()
|
||||
profile.set_preference ('media.navigator.streams.fake', True)
|
||||
profile.set_preference ('media.navigator.permission.disabled', True)
|
||||
|
||||
return webdriver.Firefox(firefox_profile=profile, capabilities=capabilities)
|
||||
|
||||
def create_chrome_driver():
|
||||
capabilities = webdriver.DesiredCapabilities().CHROME.copy()
|
||||
capabilities['acceptSslCerts'] = True
|
||||
copts = COptions()
|
||||
copts.add_argument('--allow-file-access-from-files')
|
||||
copts.add_argument('--use-fake-ui-for-media-stream')
|
||||
copts.add_argument('--use-fake-device-for-media-stream')
|
||||
copts.add_argument('--enable-blink-features=RTCUnifiedPlanByDefault')
|
||||
# XXX: until libnice can deal with mdns candidates
|
||||
local_state = {"enabled_labs_experiments" : ["enable-webrtc-hide-local-ips-with-mdns@2"] }
|
||||
copts.add_experimental_option("localState", {"browser" : local_state})
|
||||
|
||||
return webdriver.Chrome(options=copts, desired_capabilities=capabilities)
|
||||
|
||||
def create_driver(name):
|
||||
if name == 'firefox':
|
||||
return create_firefox_driver()
|
||||
elif name == 'chrome':
|
||||
return create_chrome_driver()
|
||||
else:
|
||||
raise ValueError("Unknown browser name \'" + name + "\'")
|
||||
|
||||
def valid_int(n):
|
||||
if isinstance(n, int):
|
||||
return True
|
||||
if isinstance(n, str):
|
||||
try:
|
||||
num = int(n)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
return False
|
||||
|
||||
class Browser(object):
|
||||
"""
|
||||
A browser as connected through selenium.
|
||||
"""
|
||||
def __init__(self, driver):
|
||||
l.info('Using driver \'' + driver.name + '\' with capabilities ' + str(driver.capabilities))
|
||||
self.driver = driver
|
||||
|
||||
def open(self, html_source):
|
||||
self.driver.get(html_source)
|
||||
|
||||
def get_peer_id(self):
|
||||
peer_id = WebDriverWait(self.driver, 5).until(
|
||||
lambda x: x.find_element_by_id('peer-id'),
|
||||
message='Peer-id element was never seen'
|
||||
)
|
||||
WebDriverWait (self.driver, 5).until(
|
||||
lambda x: valid_int(peer_id.text),
|
||||
message='Peer-id never became a number'
|
||||
)
|
||||
return int(peer_id.text)
|
249
webrtc/check/validate/client.py
Normal file
249
webrtc/check/validate/client.py
Normal file
|
@ -0,0 +1,249 @@
|
|||
# Copyright (c) 2020, Matthew Waters <matthew@centricular.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this program; if not, write to the
|
||||
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
import threading
|
||||
import copy
|
||||
|
||||
from observer import Signal, WebRTCObserver, DataChannelObserver, StateObserver
|
||||
from enums import NegotiationState, DataChannelState
|
||||
|
||||
import gi
|
||||
gi.require_version("Gst", "1.0")
|
||||
from gi.repository import Gst
|
||||
gi.require_version("GstWebRTC", "1.0")
|
||||
from gi.repository import GstWebRTC
|
||||
gi.require_version("GstSdp", "1.0")
|
||||
from gi.repository import GstSdp
|
||||
gi.require_version("GstValidate", "1.0")
|
||||
from gi.repository import GstValidate
|
||||
|
||||
|
||||
class WebRTCBinObserver(WebRTCObserver):
|
||||
"""
|
||||
Observe a webrtcbin element.
|
||||
"""
|
||||
def __init__(self, element):
|
||||
WebRTCObserver.__init__(self)
|
||||
self.element = element
|
||||
self.signal_handlers = []
|
||||
self.signal_handlers.append(element.connect("on-negotiation-needed", self._on_negotiation_needed))
|
||||
self.signal_handlers.append(element.connect("on-ice-candidate", self._on_ice_candidate))
|
||||
self.signal_handlers.append(element.connect("pad-added", self._on_pad_added))
|
||||
self.signal_handlers.append(element.connect("on-new-transceiver", self._on_new_transceiver))
|
||||
self.signal_handlers.append(element.connect("on-data-channel", self._on_data_channel))
|
||||
self.negotiation_needed = 0
|
||||
self._negotiation_needed_observer = StateObserver(self, "negotiation_needed", threading.Condition())
|
||||
self.on_negotiation_needed = Signal()
|
||||
self.on_ice_candidate = Signal()
|
||||
self.on_pad_added = Signal()
|
||||
self.on_new_transceiver = Signal()
|
||||
|
||||
def _on_negotiation_needed(self, element):
|
||||
self.negotiation_needed += 1
|
||||
self._negotiation_needed_observer.update(self.negotiation_needed)
|
||||
self.on_negotiation_needed.fire()
|
||||
|
||||
def _on_ice_candidate(self, element, mline, candidate):
|
||||
self.on_ice_candidate.fire(mline, candidate)
|
||||
|
||||
def _on_pad_added(self, element, pad):
|
||||
self.on_pad_added.fire(pad)
|
||||
|
||||
def _on_description_set(self, promise, desc):
|
||||
new_state = self._update_negotiation_from_description_state(desc)
|
||||
if new_state == NegotiationState.OFFER_SET:
|
||||
self.on_offer_set.fire (desc)
|
||||
elif new_state == NegotiationState.ANSWER_SET:
|
||||
self.on_answer_set.fire (desc)
|
||||
|
||||
def _on_new_transceiver(self, element, transceiver):
|
||||
self.on_new_transceiver.fire(transceiver)
|
||||
|
||||
def _on_data_channel(self, element, channel):
|
||||
observer = WebRTCBinDataChannelObserver(channel, channel.props.label, 'remote')
|
||||
self.add_channel(observer)
|
||||
|
||||
def _update_negotiation_from_description_state(self, desc):
|
||||
new_state = None
|
||||
if desc.type == GstWebRTC.WebRTCSDPType.OFFER:
|
||||
new_state = NegotiationState.OFFER_SET
|
||||
elif desc.type == GstWebRTC.WebRTCSDPType.ANSWER:
|
||||
new_state = NegotiationState.ANSWER_SET
|
||||
assert new_state is not None
|
||||
self._update_negotiation_state(new_state)
|
||||
return new_state
|
||||
|
||||
def _deepcopy_session_description(self, desc):
|
||||
# XXX: passing 'offer' to both a promise and an action signal without
|
||||
# a deepcopy will segfault...
|
||||
new_sdp = GstSdp.SDPMessage.new()[1]
|
||||
GstSdp.sdp_message_parse_buffer(bytes(desc.sdp.as_text().encode()), new_sdp)
|
||||
return GstWebRTC.WebRTCSessionDescription.new(desc.type, new_sdp)
|
||||
|
||||
def _on_offer_created(self, promise, element):
|
||||
self._update_negotiation_state(NegotiationState.OFFER_CREATED)
|
||||
reply = promise.get_reply()
|
||||
offer = reply['offer']
|
||||
|
||||
new_offer = self._deepcopy_session_description(offer)
|
||||
promise = Gst.Promise.new_with_change_func(self._on_description_set, new_offer)
|
||||
|
||||
new_offer = self._deepcopy_session_description(offer)
|
||||
self.element.emit('set-local-description', new_offer, promise)
|
||||
self.on_offer_created.fire(offer)
|
||||
|
||||
def _on_answer_created(self, promise, element):
|
||||
self._update_negotiation_state(NegotiationState.ANSWER_CREATED)
|
||||
reply = promise.get_reply()
|
||||
offer = reply['answer']
|
||||
|
||||
new_offer = self._deepcopy_session_description(offer)
|
||||
promise = Gst.Promise.new_with_change_func(self._on_description_set, new_offer)
|
||||
|
||||
new_offer = self._deepcopy_session_description(offer)
|
||||
self.element.emit('set-local-description', new_offer, promise)
|
||||
self.on_answer_created.fire(offer)
|
||||
|
||||
def create_offer(self, options=None):
|
||||
promise = Gst.Promise.new_with_change_func(self._on_offer_created, self.element)
|
||||
self.element.emit('create-offer', options, promise)
|
||||
|
||||
def create_answer(self, options=None):
|
||||
promise = Gst.Promise.new_with_change_func(self._on_answer_created, self.element)
|
||||
self.element.emit('create-answer', options, promise)
|
||||
|
||||
def set_remote_description(self, desc):
|
||||
promise = Gst.Promise.new_with_change_func(self._on_description_set, desc)
|
||||
self.element.emit('set-remote-description', desc, promise)
|
||||
|
||||
def add_ice_candidate(self, mline, candidate):
|
||||
self.element.emit('add-ice-candidate', mline, candidate)
|
||||
|
||||
def add_data_channel(self, ident):
|
||||
channel = self.element.emit('create-data-channel', ident, None)
|
||||
observer = WebRTCBinDataChannelObserver(channel, ident, 'local')
|
||||
self.add_channel(observer)
|
||||
|
||||
def wait_for_negotiation_needed(self, generation):
|
||||
self._negotiation_needed_observer.wait_for ((generation,))
|
||||
|
||||
class WebRTCStream(object):
|
||||
"""
|
||||
An stream attached to a webrtcbin element
|
||||
"""
|
||||
def __init__(self):
|
||||
self.bin = None
|
||||
|
||||
def set_description(self, desc):
|
||||
assert self.bin is None
|
||||
self.bin = Gst.parse_bin_from_description(desc, True)
|
||||
|
||||
def add_and_link(self, parent, link):
|
||||
assert self.bin is not None
|
||||
self.bin.set_locked_state(True)
|
||||
parent.add(self.bin)
|
||||
src = self.bin.get_static_pad("src")
|
||||
sink = self.bin.get_static_pad("sink")
|
||||
assert src is None or sink is None
|
||||
if src:
|
||||
self.bin.link(link)
|
||||
if sink:
|
||||
link.link(self.bin)
|
||||
self.bin.set_locked_state(False)
|
||||
self.bin.sync_state_with_parent()
|
||||
|
||||
def add_and_link_to(self, parent, link, pad):
|
||||
assert self.bin is not None
|
||||
self.bin.set_locked_state(True)
|
||||
parent.add(self.bin)
|
||||
src = self.bin.get_static_pad("src")
|
||||
sink = self.bin.get_static_pad("sink")
|
||||
assert src is None or sink is None
|
||||
if pad.get_direction() == Gst.PadDirection.SRC:
|
||||
assert sink is not None
|
||||
pad.link(sink)
|
||||
if pad.get_direction() == Gst.PadDirection.SINK:
|
||||
assert src is not None
|
||||
src.link(pad)
|
||||
self.bin.set_locked_state(False)
|
||||
self.bin.sync_state_with_parent()
|
||||
|
||||
class WebRTCClient(WebRTCBinObserver):
|
||||
"""
|
||||
Client for performing webrtc operations. Controls the pipeline that
|
||||
contains a webrtcbin element.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.pipeline = Gst.Pipeline(None)
|
||||
self.webrtcbin = Gst.ElementFactory.make("webrtcbin")
|
||||
super().__init__(self.webrtcbin)
|
||||
self.pipeline.add(self.webrtcbin)
|
||||
self._streams = []
|
||||
|
||||
def stop(self):
|
||||
self.pipeline.set_state (Gst.State.NULL)
|
||||
|
||||
def add_stream(self, desc):
|
||||
stream = WebRTCStream()
|
||||
stream.set_description(desc)
|
||||
stream.add_and_link (self.pipeline, self.webrtcbin)
|
||||
self._streams.append(stream)
|
||||
|
||||
def add_stream_with_pad(self, desc, pad):
|
||||
stream = WebRTCStream()
|
||||
stream.set_description(desc)
|
||||
stream.add_and_link_to (self.pipeline, self.webrtcbin, pad)
|
||||
self._streams.append(stream)
|
||||
|
||||
def set_options (self, opts):
|
||||
if opts.has_field("local-bundle-policy"):
|
||||
self.webrtcbin.props.bundle_policy = opts["local-bundle-policy"]
|
||||
|
||||
|
||||
class WebRTCBinDataChannelObserver(DataChannelObserver):
|
||||
"""
|
||||
Data channel observer for a webrtcbin data channel.
|
||||
"""
|
||||
def __init__(self, target, ident, location):
|
||||
super().__init__(ident, location)
|
||||
self.target = target
|
||||
self.signal_handlers = []
|
||||
self.signal_handlers.append(target.connect("on-open", self._on_open))
|
||||
self.signal_handlers.append(target.connect("on-close", self._on_close))
|
||||
self.signal_handlers.append(target.connect("on-error", self._on_error))
|
||||
self.signal_handlers.append(target.connect("on-message-data", self._on_message_data))
|
||||
self.signal_handlers.append(target.connect("on-message-string", self._on_message_string))
|
||||
self.signal_handlers.append(target.connect("on-buffered-amount-low", self._on_buffered_amount_low))
|
||||
|
||||
def _on_open(self, channel):
|
||||
self._update_state (DataChannelState.OPEN)
|
||||
def _on_close(self, channel):
|
||||
self._update_state (DataChannelState.CLOSED)
|
||||
def _on_error(self, channel):
|
||||
self._update_state (DataChannelState.ERROR)
|
||||
def _on_message_data(self, channel, data):
|
||||
self.data.append(msg)
|
||||
def _on_message_string(self, channel, msg):
|
||||
self.got_message (msg)
|
||||
def _on_buffered_amount_low(self, channel):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
self.target.emit('close')
|
||||
|
||||
def send_string (self, msg):
|
||||
self.target.emit('send-string', msg)
|
71
webrtc/check/validate/enums.py
Normal file
71
webrtc/check/validate/enums.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
# Copyright (c) 2020, Matthew Waters <matthew@centricular.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this program; if not, write to the
|
||||
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
from enum import Enum, unique
|
||||
|
||||
@unique
|
||||
class SignallingState(Enum):
|
||||
"""
|
||||
State of the signalling protocol.
|
||||
"""
|
||||
NEW = "new" # no connection has been made
|
||||
ERROR = "error" # an error was thrown. overrides all others
|
||||
OPEN = "open" # websocket connection is open
|
||||
HELLO = "hello" # hello was sent and received
|
||||
SESSION = "session" # session setup was sent and received
|
||||
|
||||
@unique
|
||||
class NegotiationState(Enum):
|
||||
"""
|
||||
State of the webrtc negotiation. Both peers have separate states and are
|
||||
tracked separately.
|
||||
"""
|
||||
NEW = "new" # No negotiation has been performed
|
||||
ERROR = "error" # an error occured
|
||||
OFFER_CREATED = "offer-created" # offer was created
|
||||
ANSWER_CREATED = "answer-created" # answer was created
|
||||
OFFER_SET = "offer-set" # offer has been set
|
||||
ANSWER_SET = "answer-set" # answer has been set
|
||||
|
||||
@unique
|
||||
class DataChannelState(Enum):
|
||||
"""
|
||||
State of a data channel. Each data channel is tracked individually
|
||||
"""
|
||||
NEW = "new" # data channel created but not connected
|
||||
OPEN = "open" # data channel is open, data can flow
|
||||
CLOSED = "closed" # data channel is closed, sending data will fail
|
||||
ERROR = "error" # data channel encountered an error
|
||||
|
||||
@unique
|
||||
class Actions(Enum):
|
||||
"""
|
||||
Action names that we implement. Each name is the structure name for each
|
||||
action as stored in the scenario file.
|
||||
"""
|
||||
CREATE_OFFER = "create-offer" # create an offer and send it to the peer
|
||||
CREATE_ANSWER = "create-answer" # create an answer and send it to the peer
|
||||
WAIT_FOR_NEGOTIATION_STATE = "wait-for-negotiation-state" # wait for the @NegotiationState to reach a certain value
|
||||
ADD_STREAM = "add-stream" # add a stream to send to the peer. local only
|
||||
ADD_DATA_CHANNEL = "add-data-channel" # add a stream to send to the peer. local only
|
||||
WAIT_FOR_DATA_CHANNEL = "wait-for-data-channel" # wait for a data channel to appear
|
||||
WAIT_FOR_DATA_CHANNEL_STATE = "wait-for-data-channel-state" # wait for a data channel to have a certain state
|
||||
SEND_DATA_CHANNEL_STRING = "send-data-channel-string" # send a string over the data channel
|
||||
WAIT_FOR_DATA_CHANNEL_STRING = "wait-for-data-channel-string" # wait for a string on the data channel
|
||||
CLOSE_DATA_CHANNEL = "close-data-channel" # close a data channel
|
||||
WAIT_FOR_NEGOTIATION_NEEDED = "wait-for-negotiation-needed" # wait for negotiation needed to fire
|
||||
SET_WEBRTC_OPTIONS = "set-webrtc-options" # set some options
|
169
webrtc/check/validate/observer.py
Normal file
169
webrtc/check/validate/observer.py
Normal file
|
@ -0,0 +1,169 @@
|
|||
# Copyright (c) 2020, Matthew Waters <matthew@centricular.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this program; if not, write to the
|
||||
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from enums import NegotiationState, DataChannelState
|
||||
|
||||
l = logging.getLogger(__name__)
|
||||
|
||||
class Signal(object):
|
||||
"""
|
||||
A class for callback-based signal handlers.
|
||||
"""
|
||||
def __init__(self, cont_func=None, accum_func=None):
|
||||
self._handlers = []
|
||||
if not cont_func:
|
||||
# by default continue when None/no return value is provided or
|
||||
# True is returned
|
||||
cont_func = lambda x: x is None or x
|
||||
self.cont_func = cont_func
|
||||
# default to accumulating truths
|
||||
if not accum_func:
|
||||
accum_func = lambda prev, v: prev and v
|
||||
self.accum_func = accum_func
|
||||
|
||||
def connect(self, handler):
|
||||
self._handlers.append(handler)
|
||||
|
||||
def disconnect(self, handler):
|
||||
self._handlers.remove(handler)
|
||||
|
||||
def fire(self, *args):
|
||||
ret = None
|
||||
for handler in self._handlers:
|
||||
ret = self.accum_func(ret, handler(*args))
|
||||
if not self.cont_func(ret):
|
||||
break
|
||||
return ret
|
||||
|
||||
|
||||
class StateObserver(object):
|
||||
"""
|
||||
Observe some state. Allows waiting for specific states to occur and
|
||||
notifying waiters of updated values. Will hold previous states to ensure
|
||||
@update cannot change the state before @wait_for can look at the state.
|
||||
"""
|
||||
def __init__(self, target, attr_name, cond):
|
||||
self.target = target
|
||||
self.attr_name = attr_name
|
||||
self.cond = cond
|
||||
# track previous states of the value so that the notification still
|
||||
# occurs even if the field has moved on to another state
|
||||
self.previous_states = []
|
||||
|
||||
def wait_for(self, states):
|
||||
ret = None
|
||||
with self.cond:
|
||||
state = getattr (self.target, self.attr_name)
|
||||
l.debug (str(self.target) + " \'" + self.attr_name +
|
||||
"\' waiting for " + str(states))
|
||||
while True:
|
||||
l.debug(str(self.target) + " \'" + self.attr_name +
|
||||
"\' previous states: " + str(self.previous_states))
|
||||
for i, s in enumerate (self.previous_states):
|
||||
if s in states:
|
||||
l.debug(str(self.target) + " \'" + self.attr_name +
|
||||
"\' " + str(s) + " found at position " +
|
||||
str(i) + " of " + str(self.previous_states))
|
||||
self.previous_states = self.previous_states[i:]
|
||||
return s
|
||||
self.cond.wait()
|
||||
|
||||
def update (self, new_state):
|
||||
with self.cond:
|
||||
self.previous_states += [new_state]
|
||||
setattr(self.target, self.attr_name, new_state)
|
||||
self.cond.notify_all()
|
||||
l.debug (str(self.target) + " updated \'" + self.attr_name + "\' to " + str(new_state))
|
||||
|
||||
|
||||
class WebRTCObserver(object):
|
||||
"""
|
||||
Base webrtc observer class. Stores a lot of duplicated functionality
|
||||
between the local and remove peer implementations.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.state = NegotiationState.NEW
|
||||
self._state_observer = StateObserver(self, "state", threading.Condition())
|
||||
self.on_offer_created = Signal()
|
||||
self.on_answer_created = Signal()
|
||||
self.on_offer_set = Signal()
|
||||
self.on_answer_set = Signal()
|
||||
self.on_data_channel = Signal()
|
||||
self.data_channels = []
|
||||
self._xxxxxxxdata_channel_ids = None
|
||||
self._data_channels_observer = StateObserver(self, "_xxxxxxxdata_channel_ids", threading.Condition())
|
||||
|
||||
def _update_negotiation_state(self, new_state):
|
||||
self._state_observer.update (new_state)
|
||||
|
||||
def wait_for_negotiation_states(self, states):
|
||||
return self._state_observer.wait_for (states)
|
||||
|
||||
def find_channel (self, ident):
|
||||
for c in self.data_channels:
|
||||
if c.ident == ident:
|
||||
return c
|
||||
|
||||
def add_channel (self, channel):
|
||||
l.debug('adding channel ' + str (channel) + ' with name ' + str(channel.ident))
|
||||
self.data_channels.append (channel)
|
||||
self._data_channels_observer.update (channel.ident)
|
||||
self.on_data_channel.fire(channel)
|
||||
|
||||
def wait_for_data_channel(self, ident):
|
||||
return self._data_channels_observer.wait_for (ident)
|
||||
|
||||
def create_offer(self, options):
|
||||
raise NotImplementedError()
|
||||
|
||||
def add_data_channel(self, ident):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class DataChannelObserver(object):
|
||||
"""
|
||||
Base webrtc data channelobserver class. Stores a lot of duplicated
|
||||
functionality between the local and remove peer implementations.
|
||||
"""
|
||||
def __init__(self, ident, location):
|
||||
self.state = DataChannelState.NEW
|
||||
self._state_observer = StateObserver(self, "state", threading.Condition())
|
||||
self.ident = ident
|
||||
self.location = location
|
||||
self.message = None
|
||||
self._message_observer = StateObserver(self, "message", threading.Condition())
|
||||
|
||||
def _update_state(self, new_state):
|
||||
self._state_observer.update (new_state)
|
||||
|
||||
def wait_for_states(self, states):
|
||||
return self._state_observer.wait_for (states)
|
||||
|
||||
def wait_for_message (self, msg):
|
||||
return self._message_observer.wait_for (msg)
|
||||
|
||||
def got_message(self, msg):
|
||||
self._message_observer.update (msg)
|
||||
|
||||
def close (self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def send_string (self, msg):
|
||||
raise NotImplementedError()
|
|
@ -0,0 +1 @@
|
|||
set-vars, local_bundle_policy=balanced, remote_bundle_policy=balanced
|
|
@ -0,0 +1 @@
|
|||
set-vars, local_bundle_policy=balanced, remote_bundle_policy=max-bundle
|
|
@ -0,0 +1 @@
|
|||
set-vars, local_bundle_policy=balanced, remote_bundle_policy=max-compat
|
|
@ -0,0 +1 @@
|
|||
set-vars, local_bundle_policy=max-bundle, remote_bundle_policy=balanced
|
|
@ -0,0 +1 @@
|
|||
set-vars, local_bundle_policy=max-bundle, remote_bundle_policy=max-bundle
|
|
@ -0,0 +1 @@
|
|||
set-vars, local_bundle_policy=max-bundle, remote_bundle_policy=max-compat
|
|
@ -0,0 +1 @@
|
|||
set-vars, local_bundle_policy=max-compat, remote_bundle_policy=balanced
|
|
@ -0,0 +1 @@
|
|||
set-vars, local_bundle_policy=max-compat, remote_bundle_policy=max-bundle
|
|
@ -0,0 +1 @@
|
|||
set-vars, local_bundle_policy=max-compat, remote_bundle_policy=max-compat
|
|
@ -0,0 +1 @@
|
|||
set-vars, local_bundle_policy=none, remote_bundle_policy=balanced
|
|
@ -0,0 +1 @@
|
|||
set-vars, local_bundle_policy=none, remote_bundle_policy=max-bundle
|
|
@ -0,0 +1 @@
|
|||
set-vars, local_bundle_policy=none, remote_bundle_policy=max-compat
|
|
@ -0,0 +1 @@
|
|||
set-vars, negotiation_initiator=local, negotiation_responder=remote
|
15
webrtc/check/validate/scenarios/offer_answer.scenario
Normal file
15
webrtc/check/validate/scenarios/offer_answer.scenario
Normal file
|
@ -0,0 +1,15 @@
|
|||
description, summary="Produce an offer and negotiate it with the peer"
|
||||
include,location=negotiation_initiator.scenario
|
||||
include,location=bundle_policy.scenario
|
||||
|
||||
set-webrtc-options, local-bundle-policy="$(local_bundle_policy)", remote-bundle-policy="$(remote_bundle_policy)"
|
||||
|
||||
create-offer, which="$(negotiation_initiator)";
|
||||
# all of these waits are technically unnecessary and only the last is needed
|
||||
wait-for-negotiation-state, which="$(negotiation_initiator)", state="offer-created"
|
||||
wait-for-negotiation-state, which="$(negotiation_initiator)", state="offer-set"
|
||||
wait-for-negotiation-state, which="$(negotiation_responder)", state="offer-set"
|
||||
create-answer, which="$(negotiation_responder)";
|
||||
wait-for-negotiation-state, which="$(negotiation_responder)", state="answer-created"
|
||||
wait-for-negotiation-state, which="$(negotiation_responder)", state="answer-set"
|
||||
wait-for-negotiation-state, which="$(negotiation_initiator)", state="answer-set"
|
23
webrtc/check/validate/scenarios/open_data_channel.scenario
Normal file
23
webrtc/check/validate/scenarios/open_data_channel.scenario
Normal file
|
@ -0,0 +1,23 @@
|
|||
description, summary="Open a data channel"
|
||||
include,location=negotiation_initiator.scenario
|
||||
include,location=bundle_policy.scenario
|
||||
|
||||
set-webrtc-options, local-bundle-policy="$(local_bundle_policy)", remote-bundle-policy="$(remote_bundle_policy)"
|
||||
|
||||
# add the channel on the initiator so that datachannel is added to the sdp
|
||||
add-data-channel, which="$(negotiation_initiator)", id="gstreamer";
|
||||
|
||||
# negotiate
|
||||
create-offer, which="$(negotiation_initiator)";
|
||||
wait-for-negotiation-state, which="$(negotiation_responder)", state="offer-set"
|
||||
create-answer, which="$(negotiation_responder)";
|
||||
wait-for-negotiation-state, which="$(negotiation_initiator)", state="answer-set"
|
||||
|
||||
# ensure data channel is created
|
||||
wait-for-data-channel, which="$(negotiation_responder)", id="gstreamer";
|
||||
wait-for-data-channel, which="$(negotiation_initiator)", id="gstreamer";
|
||||
wait-for-data-channel-state, which="$(negotiation_initiator)", id="gstreamer", state="open";
|
||||
|
||||
# only the browser closing works at the moment
|
||||
close-data-channel, which="remote", id="gstreamer"
|
||||
wait-for-data-channel-state, which="local", id="gstreamer", state="closed";
|
|
@ -0,0 +1 @@
|
|||
set-vars, negotiation_initiator=remote, negotiation_responder=local
|
|
@ -0,0 +1,21 @@
|
|||
description, summary="Send data over a data channel"
|
||||
include,location=negotiation_initiator.scenario
|
||||
include,location=bundle_policy.scenario
|
||||
|
||||
set-webrtc-options, local-bundle-policy="$(local_bundle_policy)", remote-bundle-policy="$(remote_bundle_policy)"
|
||||
|
||||
add-data-channel, which="$(negotiation_initiator)", id="gstreamer";
|
||||
|
||||
create-offer, which="$(negotiation_initiator)";
|
||||
wait-for-negotiation-state, which="$(negotiation_responder)", state="offer-set"
|
||||
create-answer, which="$(negotiation_responder)";
|
||||
wait-for-negotiation-state, which="$(negotiation_initiator)", state="answer-set"
|
||||
|
||||
# wait for the data channel to appear
|
||||
wait-for-data-channel, which="$(negotiation_initiator)", id="gstreamer";
|
||||
wait-for-data-channel, which="$(negotiation_responder)", id="gstreamer";
|
||||
wait-for-data-channel-state, which="$(negotiation_initiator)", id="gstreamer", state="open";
|
||||
|
||||
# send something
|
||||
send-data-channel-string, which="local", id="gstreamer", msg="some data";
|
||||
wait-for-data-channel-string, which="remote", id="gstreamer", msg="some data";
|
15
webrtc/check/validate/scenarios/vp8_send_stream.scenario
Normal file
15
webrtc/check/validate/scenarios/vp8_send_stream.scenario
Normal file
|
@ -0,0 +1,15 @@
|
|||
description, summary="Send a VP8 stream", handles-state=true
|
||||
include,location=negotiation_initiator.scenario
|
||||
include,location=bundle_policy.scenario
|
||||
|
||||
set-webrtc-options, local-bundle-policy="$(local_bundle_policy)", remote-bundle-policy="$(remote_bundle_policy)"
|
||||
|
||||
add-stream, pipeline="videotestsrc is-live=1 ! vp8enc ! rtpvp8pay ! queue"
|
||||
set-state, state="playing";
|
||||
wait-for-negotiation-needed, generation=1;
|
||||
|
||||
# negotiate
|
||||
create-offer, which="$(negotiation_initiator)";
|
||||
wait-for-negotiation-state, which="$(negotiation_responder)", state="offer-set"
|
||||
create-answer, which="$(negotiation_responder)";
|
||||
wait-for-negotiation-state, which="$(negotiation_initiator)", state="answer-set"
|
266
webrtc/check/validate/signalling.py
Normal file
266
webrtc/check/validate/signalling.py
Normal file
|
@ -0,0 +1,266 @@
|
|||
# Copyright (c) 2020, Matthew Waters <matthew@centricular.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this program; if not, write to the
|
||||
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
import websockets
|
||||
import asyncio
|
||||
import ssl
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import json
|
||||
import logging
|
||||
|
||||
from observer import Signal, StateObserver, WebRTCObserver, DataChannelObserver
|
||||
from enums import SignallingState, NegotiationState, DataChannelState
|
||||
|
||||
l = logging.getLogger(__name__)
|
||||
|
||||
class AsyncIOThread(threading.Thread):
|
||||
"""
|
||||
Run an asyncio loop in another thread.
|
||||
"""
|
||||
def __init__ (self, loop):
|
||||
threading.Thread.__init__(self)
|
||||
self.loop = loop
|
||||
|
||||
def run(self):
|
||||
asyncio.set_event_loop(self.loop)
|
||||
self.loop.run_forever()
|
||||
|
||||
def stop_thread(self):
|
||||
self.loop.call_soon_threadsafe(self.loop.stop)
|
||||
|
||||
|
||||
class SignallingClientThread(object):
|
||||
"""
|
||||
Connect to a signalling server
|
||||
"""
|
||||
def __init__(self, server):
|
||||
# server string to connect to. Passed directly to websockets.connect()
|
||||
self.server = server
|
||||
|
||||
# fired after we have connected to the signalling server
|
||||
self.wss_connected = Signal()
|
||||
# fired every time we receive a message from the signalling server
|
||||
self.message = Signal()
|
||||
|
||||
self._init_async()
|
||||
|
||||
def _init_async(self):
|
||||
self._running = False
|
||||
self.conn = None
|
||||
self._loop = asyncio.new_event_loop()
|
||||
|
||||
self._thread = AsyncIOThread(self._loop)
|
||||
self._thread.start()
|
||||
|
||||
self._loop.call_soon_threadsafe(lambda: asyncio.ensure_future(self._a_loop()))
|
||||
|
||||
async def _a_connect(self):
|
||||
# connect to the signalling server
|
||||
assert not self.conn
|
||||
sslctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
||||
self.conn = await websockets.connect(self.server, ssl=sslctx)
|
||||
|
||||
async def _a_loop(self):
|
||||
self._running = True
|
||||
l.info('loop started')
|
||||
await self._a_connect()
|
||||
self.wss_connected.fire()
|
||||
assert self.conn
|
||||
async for message in self.conn:
|
||||
self.message.fire(message)
|
||||
l.info('loop exited')
|
||||
|
||||
def send(self, data):
|
||||
# send some information to the peer
|
||||
async def _a_send():
|
||||
await self.conn.send(data)
|
||||
self._loop.call_soon_threadsafe(lambda: asyncio.ensure_future(_a_send()))
|
||||
|
||||
def stop(self):
|
||||
if self._running == False:
|
||||
return
|
||||
|
||||
cond = threading.Condition()
|
||||
|
||||
# asyncio, why you so complicated to stop ?
|
||||
tasks = asyncio.all_tasks(self._loop)
|
||||
async def _a_stop():
|
||||
if self.conn:
|
||||
await self.conn.close()
|
||||
self.conn = None
|
||||
|
||||
to_wait = [t for t in tasks if not t.done()]
|
||||
if to_wait:
|
||||
l.info('waiting for ' + str(to_wait))
|
||||
done, pending = await asyncio.wait(to_wait)
|
||||
with cond:
|
||||
l.error('notifying cond')
|
||||
cond.notify()
|
||||
self._running = False
|
||||
|
||||
with cond:
|
||||
self._loop.call_soon_threadsafe(lambda: asyncio.ensure_future(_a_stop()))
|
||||
l.error('cond waiting')
|
||||
cond.wait()
|
||||
l.error('cond waited')
|
||||
self._thread.stop_thread()
|
||||
self._thread.join()
|
||||
l.error('thread joined')
|
||||
|
||||
|
||||
class WebRTCSignallingClient(SignallingClientThread):
|
||||
"""
|
||||
Signalling client implementation. Deals wit session management over the
|
||||
signalling protocol. Sends and receives from a peer.
|
||||
"""
|
||||
def __init__(self, server, id_):
|
||||
super().__init__(server)
|
||||
|
||||
self.wss_connected.connect(self._on_connection)
|
||||
self.message.connect(self._on_message)
|
||||
self.state = SignallingState.NEW
|
||||
self._state_observer = StateObserver(self, "state", threading.Condition())
|
||||
|
||||
self.id = id_
|
||||
self._peerid = None
|
||||
|
||||
# fired when the hello has been received
|
||||
self.connected = Signal()
|
||||
# fired when the signalling server responds that the session creation is ok
|
||||
self.session_created = Signal()
|
||||
# fired on an error
|
||||
self.error = Signal()
|
||||
# fired when the peer receives some json data
|
||||
self.have_json = Signal()
|
||||
|
||||
def _update_state(self, new_state):
|
||||
self._state_observer.update (new_state)
|
||||
|
||||
def wait_for_states(self, states):
|
||||
return self._state_observer.wait_for (states)
|
||||
|
||||
def hello(self):
|
||||
self.send('HELLO ' + str(self.id))
|
||||
l.info("sent HELLO")
|
||||
self.wait_for_states([SignallingState.HELLO])
|
||||
|
||||
def create_session(self, peerid):
|
||||
self._peerid = peerid
|
||||
self.send('SESSION {}'.format(self._peerid))
|
||||
l.info("sent SESSION")
|
||||
self.wait_for_states([SignallingState.SESSION])
|
||||
|
||||
def _on_connection(self):
|
||||
self._update_state (SignallingState.OPEN)
|
||||
|
||||
def _on_message(self, message):
|
||||
l.debug("received: " + message)
|
||||
if message == 'HELLO':
|
||||
self._update_state (SignallingState.HELLO)
|
||||
self.connected.fire()
|
||||
elif message == 'SESSION_OK':
|
||||
self._update_state (SignallingState.SESSION)
|
||||
self.session_created.fire()
|
||||
elif message.startswith('ERROR'):
|
||||
self._update_state (SignallingState.ERROR)
|
||||
self.error.fire(message)
|
||||
else:
|
||||
msg = json.loads(message)
|
||||
self.have_json.fire(msg)
|
||||
return False
|
||||
|
||||
|
||||
class RemoteWebRTCObserver(WebRTCObserver):
|
||||
"""
|
||||
Use information sent over the signalling channel to construct the current
|
||||
state of a remote peer. Allow performing actions by sending requests over
|
||||
the signalling channel.
|
||||
"""
|
||||
def __init__(self, signalling):
|
||||
super().__init__()
|
||||
self.signalling = signalling
|
||||
|
||||
def on_json(msg):
|
||||
if 'STATE' in msg:
|
||||
state = NegotiationState (msg['STATE'])
|
||||
self._update_negotiation_state(state)
|
||||
if state == NegotiationState.OFFER_CREATED:
|
||||
self.on_offer_created.fire(msg['description'])
|
||||
elif state == NegotiationState.ANSWER_CREATED:
|
||||
self.on_answer_created.fire(msg['description'])
|
||||
elif state == NegotiationState.OFFER_SET:
|
||||
self.on_offer_set.fire (msg['description'])
|
||||
elif state == NegotiationState.ANSWER_SET:
|
||||
self.on_answer_set.fire (msg['description'])
|
||||
elif 'DATA-NEW' in msg:
|
||||
new = msg['DATA-NEW']
|
||||
observer = RemoteDataChannelObserver(new['id'], new['location'], self)
|
||||
self.add_channel (observer)
|
||||
elif 'DATA-STATE' in msg:
|
||||
ident = msg['id']
|
||||
channel = self.find_channel(ident)
|
||||
channel._update_state (DataChannelState(msg['DATA-STATE']))
|
||||
elif 'DATA-MSG' in msg:
|
||||
ident = msg['id']
|
||||
channel = self.find_channel(ident)
|
||||
channel.got_message(msg['DATA-MSG'])
|
||||
self.signalling.have_json.connect (on_json)
|
||||
|
||||
def add_data_channel (self, ident):
|
||||
msg = json.dumps({'DATA_CREATE': {'id': ident}})
|
||||
self.signalling.send (msg)
|
||||
|
||||
def create_offer (self):
|
||||
msg = json.dumps({'CREATE_OFFER': ""})
|
||||
self.signalling.send (msg)
|
||||
|
||||
def create_answer (self):
|
||||
msg = json.dumps({'CREATE_ANSWER': ""})
|
||||
self.signalling.send (msg)
|
||||
|
||||
def set_title (self, title):
|
||||
# entirely for debugging purposes
|
||||
msg = json.dumps({'SET_TITLE': title})
|
||||
self.signalling.send (msg)
|
||||
|
||||
def set_options (self, opts):
|
||||
options = {}
|
||||
if opts.has_field("remote-bundle-policy"):
|
||||
options["bundlePolicy"] = opts["remote-bundle-policy"]
|
||||
msg = json.dumps({'OPTIONS' : options})
|
||||
self.signalling.send (msg)
|
||||
|
||||
|
||||
class RemoteDataChannelObserver(DataChannelObserver):
|
||||
"""
|
||||
Use information sent over the signalling channel to construct the current
|
||||
state of a remote peer's data channel. Allow performing actions by sending
|
||||
requests over the signalling channel.
|
||||
"""
|
||||
def __init__(self, ident, location, webrtc):
|
||||
super().__init__(ident, location)
|
||||
self.webrtc = webrtc
|
||||
|
||||
def send_string(self, msg):
|
||||
msg = json.dumps({'DATA_SEND_MSG': {'msg' : msg, 'id': self.ident}})
|
||||
self.webrtc.signalling.send (msg)
|
||||
|
||||
def close (self):
|
||||
msg = json.dumps({'DATA_CLOSE': {'id': self.ident}})
|
||||
self.webrtc.signalling.send (msg)
|
0
webrtc/check/validate/testsuites/__init__.py
Normal file
0
webrtc/check/validate/testsuites/__init__.py
Normal file
31
webrtc/check/validate/testsuites/webrtc.py
Normal file
31
webrtc/check/validate/testsuites/webrtc.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Copyright (c) 2020 Matthew Waters <matthew@centricular.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this program; if not, write to the
|
||||
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
"""
|
||||
The GstValidate webrtc streams testsuite
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
TEST_MANAGER = "webrtc"
|
||||
|
||||
|
||||
def setup_tests(test_manager, options):
|
||||
print("Setting up webrtc tests")
|
||||
|
||||
return True
|
||||
|
31
webrtc/check/validate/web/single_stream.html
Normal file
31
webrtc/check/validate/web/single_stream.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<!--
|
||||
vim: set sts=2 sw=2 et :
|
||||
|
||||
|
||||
Demo Javascript app for negotiating and streaming a sendrecv webrtc stream
|
||||
with a GStreamer app. Runs only in passive mode, i.e., responds to offers
|
||||
with answers, exchanges ICE candidates, and streams.
|
||||
|
||||
Author: Nirbheek Chauhan <nirbheek@centricular.com>
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
.error { color: red; }
|
||||
</style>
|
||||
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
|
||||
<script src="webrtc.js"></script>
|
||||
<script>
|
||||
window.onload = websocketServerConnect;
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div><video id="stream" autoplay>Your browser doesn't support video</video></div>
|
||||
<div>Status: <span id="status">unknown</span></div>
|
||||
<div><textarea id="text" cols=40 rows=4></textarea></div>
|
||||
<div>Our id is <b id="peer-id">unknown</b></div>
|
||||
<br/>
|
||||
</body>
|
||||
</html>
|
387
webrtc/check/validate/web/webrtc.js
Normal file
387
webrtc/check/validate/web/webrtc.js
Normal file
|
@ -0,0 +1,387 @@
|
|||
/* vim: set sts=4 sw=4 et :
|
||||
*
|
||||
* Demo Javascript app for negotiating and streaming a sendrecv webrtc stream
|
||||
* with a GStreamer app. Runs only in passive mode, i.e., responds to offers
|
||||
* with answers, exchanges ICE candidates, and streams.
|
||||
*
|
||||
* Author: Nirbheek Chauhan <nirbheek@centricular.com>
|
||||
*/
|
||||
|
||||
// Set this to override the automatic detection in websocketServerConnect()
|
||||
var ws_server;
|
||||
var ws_port;
|
||||
// Set this to use a specific peer id instead of a random one
|
||||
var default_peer_id;
|
||||
// Override with your own STUN servers if you want
|
||||
var rtc_configuration = {iceServers: [{urls: "stun:stun.services.mozilla.com"},
|
||||
{urls: "stun:stun.l.google.com:19302"},]};
|
||||
var default_constraints = {video: true, audio: false};
|
||||
|
||||
var connect_attempts = 0;
|
||||
var peer_connection;
|
||||
var channels = []
|
||||
var ws_conn;
|
||||
// Promise for local stream after constraints are approved by the user
|
||||
var local_stream_promise;
|
||||
|
||||
function getOurId() {
|
||||
return Math.floor(Math.random() * (9000 - 10) + 10).toString();
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
// This will call onServerClose()
|
||||
ws_conn.close();
|
||||
}
|
||||
|
||||
function handleIncomingError(error) {
|
||||
setError("ERROR: " + error);
|
||||
resetState();
|
||||
}
|
||||
|
||||
function getVideoElement() {
|
||||
return document.getElementById("stream");
|
||||
}
|
||||
|
||||
function setStatus(text) {
|
||||
console.log(text);
|
||||
var span = document.getElementById("status")
|
||||
// Don't set the status if it already contains an error
|
||||
if (!span.classList.contains('error'))
|
||||
span.textContent = text;
|
||||
}
|
||||
|
||||
function setError(text) {
|
||||
console.error(text);
|
||||
var span = document.getElementById("status")
|
||||
span.textContent = text;
|
||||
span.classList.add('error');
|
||||
ws_conn.send(JSON.stringify({'STATE': 'error', 'msg' : text}))
|
||||
}
|
||||
|
||||
function resetVideo() {
|
||||
// Release the webcam and mic
|
||||
if (local_stream_promise)
|
||||
local_stream_promise.then(stream => {
|
||||
if (stream) {
|
||||
stream.getTracks().forEach(function (track) { track.stop(); });
|
||||
}
|
||||
});
|
||||
|
||||
// Reset the video element and stop showing the last received frame
|
||||
var videoElement = getVideoElement();
|
||||
videoElement.pause();
|
||||
videoElement.src = "";
|
||||
videoElement.load();
|
||||
}
|
||||
|
||||
function updateRemoteStateFromSetSDPJson(sdp) {
|
||||
if (sdp.type == "offer")
|
||||
ws_conn.send(JSON.stringify({'STATE': 'offer-set', 'description' : sdp}))
|
||||
else if (sdp.type == "answer")
|
||||
ws_conn.send(JSON.stringify({'STATE': 'answer-set', 'description' : sdp}))
|
||||
else
|
||||
throw new Error ("Unknown SDP type!");
|
||||
}
|
||||
|
||||
function updateRemoteStateFromGeneratedSDPJson(sdp) {
|
||||
if (sdp.type == "offer")
|
||||
ws_conn.send(JSON.stringify({'STATE': 'offer-created', 'description' : sdp}))
|
||||
else if (sdp.type == "answer")
|
||||
ws_conn.send(JSON.stringify({'STATE': 'answer-created', 'description' : sdp}))
|
||||
else
|
||||
throw new Error ("Unknown SDP type!");
|
||||
}
|
||||
|
||||
// SDP offer received from peer, set remote description and create an answer
|
||||
function onIncomingSDP(sdp) {
|
||||
peer_connection.setRemoteDescription(sdp).then(() => {
|
||||
updateRemoteStateFromSetSDPJson(sdp)
|
||||
setStatus("Set remote SDP", sdp.type);
|
||||
}).catch(setError);
|
||||
}
|
||||
|
||||
// Local description was set, send it to peer
|
||||
function onLocalDescription(desc) {
|
||||
updateRemoteStateFromGeneratedSDPJson(desc)
|
||||
console.log("Got local description: " + JSON.stringify(desc));
|
||||
peer_connection.setLocalDescription(desc).then(function() {
|
||||
updateRemoteStateFromSetSDPJson(desc)
|
||||
sdp = {'sdp': desc}
|
||||
setStatus("Sending SDP", sdp.type);
|
||||
ws_conn.send(JSON.stringify(sdp));
|
||||
});
|
||||
}
|
||||
|
||||
// ICE candidate received from peer, add it to the peer connection
|
||||
function onIncomingICE(ice) {
|
||||
var candidate = new RTCIceCandidate(ice);
|
||||
console.log("adding candidate", candidate)
|
||||
peer_connection.addIceCandidate(candidate).catch(setError);
|
||||
}
|
||||
|
||||
function createOffer(offer) {
|
||||
local_stream_promise.then((stream) => {
|
||||
setStatus("Got local stream, creating offer");
|
||||
peer_connection.createOffer()
|
||||
.then(onLocalDescription).catch(setError);
|
||||
}).catch(setError)
|
||||
}
|
||||
|
||||
function createAnswer(offer) {
|
||||
local_stream_promise.then((stream) => {
|
||||
setStatus("Got local stream, creating answer");
|
||||
peer_connection.createAnswer()
|
||||
.then(onLocalDescription).catch(setError);
|
||||
}).catch(setError)
|
||||
}
|
||||
|
||||
function handleOptions(options) {
|
||||
console.log ('received options', options);
|
||||
if (options.bundlePolicy != null) {
|
||||
rtc_configuration['bundlePolicy'] = options.bundlePolicy;
|
||||
}
|
||||
}
|
||||
|
||||
function onServerMessage(event) {
|
||||
console.log("Received " + event.data);
|
||||
switch (event.data) {
|
||||
case "HELLO":
|
||||
setStatus("Registered with server, waiting for call");
|
||||
return;
|
||||
default:
|
||||
if (event.data.startsWith("ERROR")) {
|
||||
handleIncomingError(event.data);
|
||||
return;
|
||||
}
|
||||
// Handle incoming JSON SDP and ICE messages
|
||||
try {
|
||||
msg = JSON.parse(event.data);
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
handleIncomingError("Error parsing incoming JSON: " + event.data);
|
||||
} else {
|
||||
handleIncomingError("Unknown error parsing response: " + event.data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.SET_TITLE != null) {
|
||||
// some debugging for tests that hang around
|
||||
document.title = msg['SET_TITLE']
|
||||
return;
|
||||
} else if (msg.OPTIONS != null) {
|
||||
handleOptions(msg.OPTIONS);
|
||||
return;
|
||||
}
|
||||
|
||||
// Incoming JSON signals the beginning of a call
|
||||
if (!peer_connection)
|
||||
createCall();
|
||||
|
||||
if (msg.sdp != null) {
|
||||
onIncomingSDP(msg.sdp);
|
||||
} else if (msg.ice != null) {
|
||||
onIncomingICE(msg.ice);
|
||||
} else if (msg.CREATE_OFFER != null) {
|
||||
createOffer(msg.CREATE_OFFER)
|
||||
} else if (msg.CREATE_ANSWER != null) {
|
||||
createAnswer(msg.CREATE_ANSWER)
|
||||
} else if (msg.DATA_CREATE != null) {
|
||||
addDataChannel(msg.DATA_CREATE.id)
|
||||
} else if (msg.DATA_CLOSE != null) {
|
||||
closeDataChannel(msg.DATA_CLOSE.id)
|
||||
} else if (msg.DATA_SEND_MSG != null) {
|
||||
sendDataChannelMessage(msg.DATA_SEND_MSG)
|
||||
} else {
|
||||
handleIncomingError("Unknown incoming JSON: " + msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onServerClose(event) {
|
||||
setStatus('Disconnected from server');
|
||||
resetVideo();
|
||||
|
||||
if (peer_connection) {
|
||||
peer_connection.close();
|
||||
peer_connection = null;
|
||||
}
|
||||
channels = []
|
||||
|
||||
// Reset after a second
|
||||
window.setTimeout(websocketServerConnect, 1000);
|
||||
}
|
||||
|
||||
function onServerError(event) {
|
||||
setError("Unable to connect to server, did you add an exception for the certificate?")
|
||||
// Retry after 3 seconds
|
||||
window.setTimeout(websocketServerConnect, 3000);
|
||||
}
|
||||
|
||||
function getLocalStream() {
|
||||
var constraints;
|
||||
constraints = default_constraints;
|
||||
console.log(JSON.stringify(constraints));
|
||||
|
||||
// Add local stream
|
||||
if (navigator.mediaDevices.getUserMedia) {
|
||||
return navigator.mediaDevices.getUserMedia(constraints);
|
||||
} else {
|
||||
errorUserMediaHandler();
|
||||
}
|
||||
}
|
||||
|
||||
function websocketServerConnect() {
|
||||
connect_attempts++;
|
||||
if (connect_attempts > 3) {
|
||||
setError("Too many connection attempts, aborting. Refresh page to try again");
|
||||
return;
|
||||
}
|
||||
// Clear errors in the status span
|
||||
var span = document.getElementById("status");
|
||||
span.classList.remove('error');
|
||||
span.textContent = '';
|
||||
// Fetch the peer id to use
|
||||
var url = new URL(window.location.href);
|
||||
|
||||
peer_id = url.searchParams.get("id");
|
||||
peer_id = peer_id || default_peer_id || getOurId();
|
||||
|
||||
ws_port = ws_port || url.searchParams.get("port");
|
||||
ws_port = ws_port || '8443';
|
||||
|
||||
ws_server = ws_server || url.searchParams.get("server");
|
||||
if (window.location.protocol.startsWith ("file")) {
|
||||
ws_server = ws_server || "127.0.0.1";
|
||||
} else if (window.location.protocol.startsWith ("http")) {
|
||||
ws_server = ws_server || window.location.hostname;
|
||||
} else {
|
||||
throw new Error ("Don't know how to connect to the signalling server with uri" + window.location);
|
||||
}
|
||||
|
||||
var ws_url = 'wss://' + ws_server + ':' + ws_port
|
||||
setStatus("Connecting to server " + ws_url);
|
||||
ws_conn = new WebSocket(ws_url);
|
||||
/* When connected, immediately register with the server */
|
||||
ws_conn.addEventListener('open', (event) => {
|
||||
document.getElementById("peer-id").textContent = peer_id;
|
||||
ws_conn.send('HELLO ' + peer_id);
|
||||
setStatus("Registering with server");
|
||||
});
|
||||
ws_conn.addEventListener('error', onServerError);
|
||||
ws_conn.addEventListener('message', onServerMessage);
|
||||
ws_conn.addEventListener('close', onServerClose);
|
||||
}
|
||||
|
||||
function onRemoteStreamAdded(event) {
|
||||
videoTracks = event.stream.getVideoTracks();
|
||||
audioTracks = event.stream.getAudioTracks();
|
||||
|
||||
if (videoTracks.length > 0) {
|
||||
console.log('Incoming stream: ' + videoTracks.length + ' video tracks and ' + audioTracks.length + ' audio tracks');
|
||||
getVideoElement().srcObject = event.stream;
|
||||
} else {
|
||||
handleIncomingError('Stream with unknown tracks added, resetting');
|
||||
}
|
||||
}
|
||||
|
||||
function errorUserMediaHandler() {
|
||||
setError("Browser doesn't support getUserMedia!");
|
||||
}
|
||||
|
||||
const handleDataChannelMessageReceived = (event) =>{
|
||||
console.log("dataChannel.OnMessage:", event, event.data.type);
|
||||
setStatus("Received data channel message");
|
||||
ws_conn.send(JSON.stringify({'DATA-MSG' : event.data, 'id' : event.target.label}));
|
||||
};
|
||||
|
||||
const handleDataChannelOpen = (event) =>{
|
||||
console.log("dataChannel.OnOpen", event);
|
||||
ws_conn.send(JSON.stringify({'DATA-STATE' : 'open', 'id' : event.target.label}));
|
||||
};
|
||||
|
||||
const handleDataChannelError = (error) =>{
|
||||
console.log("dataChannel.OnError:", error);
|
||||
ws_conn.send(JSON.stringify({'DATA-STATE' : error, 'id' : event.target.label}));
|
||||
};
|
||||
|
||||
const handleDataChannelClose = (event) =>{
|
||||
console.log("dataChannel.OnClose", event);
|
||||
ws_conn.send(JSON.stringify({'DATA-STATE' : 'closed', 'id' : event.target.label}));
|
||||
};
|
||||
|
||||
function onDataChannel(event) {
|
||||
setStatus("Data channel created");
|
||||
let channel = event.channel;
|
||||
console.log('adding remote data channel with label', channel.label)
|
||||
ws_conn.send(JSON.stringify({'DATA-NEW' : {'id' : channel.label, 'location' : 'remote'}}));
|
||||
channel.onopen = handleDataChannelOpen;
|
||||
channel.onmessage = handleDataChannelMessageReceived;
|
||||
channel.onerror = handleDataChannelError;
|
||||
channel.onclose = handleDataChannelClose;
|
||||
channels.push(channel)
|
||||
}
|
||||
|
||||
function addDataChannel(label) {
|
||||
channel = peer_connection.createDataChannel(label, null);
|
||||
console.log('adding local data channel with label', label)
|
||||
ws_conn.send(JSON.stringify({'DATA-NEW' : {'id' : label, 'location' : 'local'}}));
|
||||
channel.onopen = handleDataChannelOpen;
|
||||
channel.onmessage = handleDataChannelMessageReceived;
|
||||
channel.onerror = handleDataChannelError;
|
||||
channel.onclose = handleDataChannelClose;
|
||||
channels.push(channel)
|
||||
}
|
||||
|
||||
function find_channel(label) {
|
||||
console.log('find', label, 'in', channels)
|
||||
for (var c in channels) {
|
||||
if (channels[c].label === label) {
|
||||
console.log('found', label, c)
|
||||
return channels[c];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function closeDataChannel(label) {
|
||||
channel = find_channel (label)
|
||||
console.log('closing data channel with label', label)
|
||||
channel.close()
|
||||
}
|
||||
|
||||
function sendDataChannelMessage(msg) {
|
||||
channel = find_channel (msg.id)
|
||||
console.log('sending on data channel', msg.id, 'message', msg.msg)
|
||||
channel.send(msg.msg)
|
||||
}
|
||||
|
||||
function createCall() {
|
||||
// Reset connection attempts because we connected successfully
|
||||
connect_attempts = 0;
|
||||
|
||||
console.log('Creating RTCPeerConnection with configuration', rtc_configuration);
|
||||
|
||||
peer_connection = new RTCPeerConnection(rtc_configuration);
|
||||
peer_connection.ondatachannel = onDataChannel;
|
||||
peer_connection.onaddstream = onRemoteStreamAdded;
|
||||
/* Send our video/audio to the other peer */
|
||||
local_stream_promise = getLocalStream().then((stream) => {
|
||||
console.log('Adding local stream');
|
||||
peer_connection.addStream(stream);
|
||||
return stream;
|
||||
}).catch(setError);
|
||||
|
||||
peer_connection.onicecandidate = (event) => {
|
||||
// We have a candidate, send it to the remote party with the
|
||||
// same uuid
|
||||
if (event.candidate == null) {
|
||||
console.log("ICE Candidate was null, done");
|
||||
return;
|
||||
}
|
||||
console.log("generated ICE Candidate", event.candidate);
|
||||
ws_conn.send(JSON.stringify({'ice': event.candidate}));
|
||||
};
|
||||
|
||||
setStatus("Created peer connection for call, waiting for SDP");
|
||||
}
|
286
webrtc/check/validate/webrtc_validate.py
Normal file
286
webrtc/check/validate/webrtc_validate.py
Normal file
|
@ -0,0 +1,286 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2018, Matthew Waters <matthew@centricular.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this program; if not, write to the
|
||||
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
|
||||
from signalling import WebRTCSignallingClient, RemoteWebRTCObserver
|
||||
from actions import ActionObserver
|
||||
from client import WebRTCClient
|
||||
from browser import Browser, create_driver
|
||||
from enums import SignallingState, NegotiationState, DataChannelState, Actions
|
||||
|
||||
import gi
|
||||
gi.require_version("GLib", "2.0")
|
||||
from gi.repository import GLib
|
||||
gi.require_version("Gst", "1.0")
|
||||
from gi.repository import Gst
|
||||
gi.require_version("GstWebRTC", "1.0")
|
||||
from gi.repository import GstWebRTC
|
||||
gi.require_version("GstSdp", "1.0")
|
||||
from gi.repository import GstSdp
|
||||
gi.require_version("GstValidate", "1.0")
|
||||
from gi.repository import GstValidate
|
||||
|
||||
FORMAT = '%(asctime)-23s %(levelname)-7s %(thread)d %(name)-24s\t%(funcName)-24s %(message)s'
|
||||
LEVEL = os.environ.get("LOGLEVEL", "DEBUG")
|
||||
logging.basicConfig(level=LEVEL, format=FORMAT)
|
||||
l = logging.getLogger(__name__)
|
||||
|
||||
class WebRTCApplication(object):
|
||||
def __init__(self, server, id_, peerid, scenario_name, browser_name, html_source, test_name=None):
|
||||
self.server = server
|
||||
self.peerid = peerid
|
||||
self.html_source = html_source
|
||||
self.id = id_
|
||||
self.scenario_name = scenario_name
|
||||
self.browser_name = browser_name
|
||||
self.test_name = test_name
|
||||
|
||||
def _init_validate(self, scenario_file):
|
||||
self.runner = GstValidate.Runner.new()
|
||||
self.monitor = GstValidate.Monitor.factory_create(
|
||||
self.client.pipeline, self.runner, None)
|
||||
self._scenario = GstValidate.Scenario.factory_create(
|
||||
self.runner, self.client.pipeline, self.scenario_name)
|
||||
self._scenario.connect("done", self._on_scenario_done)
|
||||
self._scenario.props.execute_on_idle = True
|
||||
if not self._scenario.props.handles_states:
|
||||
self.client.pipeline.set_state(Gst.State.PLAYING)
|
||||
|
||||
def _on_scenario_done(self, scenario):
|
||||
l.error ('scenario done')
|
||||
GLib.idle_add(self.quit)
|
||||
|
||||
def _connect_actions(self, actions):
|
||||
def on_action(atype, action):
|
||||
"""
|
||||
From a validate action, perform the action as required
|
||||
"""
|
||||
if atype == Actions.CREATE_OFFER:
|
||||
assert action.structure["which"] in ("local", "remote")
|
||||
c = self.client if action.structure["which"] == "local" else self.remote_client
|
||||
c.create_offer()
|
||||
return GstValidate.ActionReturn.OK
|
||||
elif atype == Actions.CREATE_ANSWER:
|
||||
assert action.structure["which"] in ("local", "remote")
|
||||
c = self.client if action.structure["which"] == "local" else self.remote_client
|
||||
c.create_answer()
|
||||
return GstValidate.ActionReturn.OK
|
||||
elif atype == Actions.WAIT_FOR_NEGOTIATION_STATE:
|
||||
states = [NegotiationState(action.structure["state"]), NegotiationState.ERROR]
|
||||
assert action.structure["which"] in ("local", "remote")
|
||||
c = self.client if action.structure["which"] == "local" else self.remote_client
|
||||
state = c.wait_for_negotiation_states(states)
|
||||
return GstValidate.ActionReturn.OK if state != NegotiationState.ERROR else GstValidate.ActionReturn.ERROR
|
||||
elif atype == Actions.ADD_STREAM:
|
||||
self.client.add_stream(action.structure["pipeline"])
|
||||
return GstValidate.ActionReturn.OK
|
||||
elif atype == Actions.ADD_DATA_CHANNEL:
|
||||
assert action.structure["which"] in ("local", "remote")
|
||||
c = self.client if action.structure["which"] == "local" else self.remote_client
|
||||
c.add_data_channel(action.structure["id"])
|
||||
return GstValidate.ActionReturn.OK
|
||||
elif atype == Actions.SEND_DATA_CHANNEL_STRING:
|
||||
assert action.structure["which"] in ("local", "remote")
|
||||
c = self.client if action.structure["which"] == "local" else self.remote_client
|
||||
channel = c.find_channel (action.structure["id"])
|
||||
channel.send_string (action.structure["msg"])
|
||||
return GstValidate.ActionReturn.OK
|
||||
elif atype == Actions.WAIT_FOR_DATA_CHANNEL_STATE:
|
||||
assert action.structure["which"] in ("local", "remote")
|
||||
c = self.client if action.structure["which"] == "local" else self.remote_client
|
||||
states = [DataChannelState(action.structure["state"]), DataChannelState.ERROR]
|
||||
channel = c.find_channel (action.structure["id"])
|
||||
state = channel.wait_for_states(states)
|
||||
return GstValidate.ActionReturn.OK if state != DataChannelState.ERROR else GstValidate.ActionReturn.ERROR
|
||||
elif atype == Actions.CLOSE_DATA_CHANNEL:
|
||||
assert action.structure["which"] in ("local", "remote")
|
||||
c = self.client if action.structure["which"] == "local" else self.remote_client
|
||||
channel = c.find_channel (action.structure["id"])
|
||||
channel.close()
|
||||
return GstValidate.ActionReturn.OK
|
||||
elif atype == Actions.WAIT_FOR_DATA_CHANNEL:
|
||||
assert action.structure["which"] in ("local", "remote")
|
||||
c = self.client if action.structure["which"] == "local" else self.remote_client
|
||||
state = c.wait_for_data_channel(action.structure["id"])
|
||||
return GstValidate.ActionReturn.OK
|
||||
elif atype == Actions.WAIT_FOR_DATA_CHANNEL_STRING:
|
||||
assert action.structure["which"] in ("local", "remote")
|
||||
c = self.client if action.structure["which"] == "local" else self.remote_client
|
||||
channel = c.find_channel (action.structure["id"])
|
||||
channel.wait_for_message(action.structure["msg"])
|
||||
return GstValidate.ActionReturn.OK
|
||||
elif atype == Actions.WAIT_FOR_NEGOTIATION_NEEDED:
|
||||
self.client.wait_for_negotiation_needed(action.structure["generation"])
|
||||
return GstValidate.ActionReturn.OK
|
||||
elif atype == Actions.SET_WEBRTC_OPTIONS:
|
||||
self.client.set_options (action.structure)
|
||||
self.remote_client.set_options (action.structure)
|
||||
return GstValidate.ActionReturn.OK
|
||||
else:
|
||||
assert "Not reached" == ""
|
||||
|
||||
actions.action.connect (on_action)
|
||||
|
||||
def _connect_client_observer(self):
|
||||
def on_offer_created(offer):
|
||||
text = offer.sdp.as_text()
|
||||
msg = json.dumps({'sdp': {'type': 'offer', 'sdp': text}})
|
||||
self.signalling.send(msg)
|
||||
self.client.on_offer_created.connect(on_offer_created)
|
||||
|
||||
def on_answer_created(answer):
|
||||
text = answer.sdp.as_text()
|
||||
msg = json.dumps({'sdp': {'type': 'answer', 'sdp': text}})
|
||||
self.signalling.send(msg)
|
||||
self.client.on_answer_created.connect(on_answer_created)
|
||||
|
||||
def on_ice_candidate(mline, candidate):
|
||||
msg = json.dumps({'ice': {'sdpMLineIndex': str(mline), 'candidate' : candidate}})
|
||||
self.signalling.send(msg)
|
||||
self.client.on_ice_candidate.connect(on_ice_candidate)
|
||||
|
||||
def on_pad_added(pad):
|
||||
if pad.get_direction() != Gst.PadDirection.SRC:
|
||||
return
|
||||
self.client.add_stream_with_pad('fakesink', pad)
|
||||
self.client.on_pad_added.connect(on_pad_added)
|
||||
|
||||
def _connect_signalling_observer(self):
|
||||
def have_json(msg):
|
||||
if 'sdp' in msg:
|
||||
sdp = msg['sdp']
|
||||
res, sdpmsg = GstSdp.SDPMessage.new()
|
||||
GstSdp.sdp_message_parse_buffer(bytes(sdp['sdp'].encode()), sdpmsg)
|
||||
sdptype = GstWebRTC.WebRTCSDPType.ANSWER if sdp['type'] == 'answer' else GstWebRTC.WebRTCSDPType.OFFER
|
||||
desc = GstWebRTC.WebRTCSessionDescription.new(sdptype, sdpmsg)
|
||||
self.client.set_remote_description(desc)
|
||||
elif 'ice' in msg:
|
||||
ice = msg['ice']
|
||||
candidate = ice['candidate']
|
||||
sdpmlineindex = ice['sdpMLineIndex']
|
||||
self.client.add_ice_candidate(sdpmlineindex, candidate)
|
||||
self.signalling.have_json.connect(have_json)
|
||||
|
||||
def error(msg):
|
||||
# errors are unexpected
|
||||
l.error ('Unexpected error: ' + msg)
|
||||
GLib.idle_add(self.quit)
|
||||
GLib.idle_add(sys.exit, -20)
|
||||
self.signalling.error.connect(error)
|
||||
|
||||
def _init(self):
|
||||
self.main_loop = GLib.MainLoop()
|
||||
|
||||
self.client = WebRTCClient()
|
||||
self._connect_client_observer()
|
||||
|
||||
self.signalling = WebRTCSignallingClient(self.server, self.id)
|
||||
self.remote_client = RemoteWebRTCObserver (self.signalling)
|
||||
self._connect_signalling_observer()
|
||||
|
||||
actions = ActionObserver()
|
||||
actions.register_action_types()
|
||||
self._connect_actions(actions)
|
||||
|
||||
# wait for the signalling server to start up before creating the browser
|
||||
self.signalling.wait_for_states([SignallingState.OPEN])
|
||||
self.signalling.hello()
|
||||
|
||||
self.browser = Browser(create_driver(self.browser_name))
|
||||
self.browser.open(self.html_source)
|
||||
|
||||
browser_id = self.browser.get_peer_id ()
|
||||
assert browser_id == self.peerid
|
||||
|
||||
self.signalling.create_session(self.peerid)
|
||||
test_name = self.test_name if self.test_name else self.scenario_name
|
||||
self.remote_client.set_title (test_name)
|
||||
|
||||
self._init_validate(self.scenario_name)
|
||||
|
||||
def quit(self):
|
||||
# Stop signalling first so asyncio doesn't keep us alive on weird failures
|
||||
l.info('quiting')
|
||||
self.signalling.stop()
|
||||
l.info('signalling stopped')
|
||||
self.main_loop.quit()
|
||||
l.info('main loop stopped')
|
||||
self.client.stop()
|
||||
l.info('client stopped')
|
||||
self.browser.driver.quit()
|
||||
l.info('browser exitted')
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self._init()
|
||||
l.info("app initialized")
|
||||
self.main_loop.run()
|
||||
l.info("loop exited")
|
||||
except:
|
||||
l.exception("Fatal error")
|
||||
self.quit()
|
||||
raise
|
||||
|
||||
def parse_options():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('id', help='ID of this client', type=int)
|
||||
parser.add_argument('--peer-id', help='ID of the peer to connect to', type=int)
|
||||
parser.add_argument('--server', help='Signalling server to connect to, eg "wss://127.0.0.1:8443"')
|
||||
parser.add_argument('--html-source', help='HTML page to open in the browser', default=None)
|
||||
parser.add_argument('--scenario', help='Scenario file to execute', default=None)
|
||||
parser.add_argument('--browser', help='Browser name to use', default=None)
|
||||
parser.add_argument('--name', help='Name of the test', default=None)
|
||||
return parser.parse_args()
|
||||
|
||||
def init():
|
||||
Gst.init(None)
|
||||
GstValidate.init()
|
||||
|
||||
args = parse_options()
|
||||
if not args.scenario:
|
||||
args.scenario = os.environ.get('GST_VALIDATE_SCENARIO', None)
|
||||
# if we have both manual scenario creation and env, then the scenario
|
||||
# is executed twice...
|
||||
if 'GST_VALIDATE_SCENARIO' in os.environ:
|
||||
del os.environ['GST_VALIDATE_SCENARIO']
|
||||
if not args.scenario:
|
||||
raise ValueError("No scenario file provided")
|
||||
if not args.server:
|
||||
raise ValueError("No server location provided")
|
||||
if not args.peer_id:
|
||||
raise ValueError("No peer id provided")
|
||||
if not args.html_source:
|
||||
raise ValueError("No HTML page provided")
|
||||
if not args.browser:
|
||||
raise ValueError("No Browser provided")
|
||||
|
||||
return args
|
||||
|
||||
def run():
|
||||
args = init()
|
||||
w = WebRTCApplication (args.server, args.id, args.peer_id, args.scenario, args.browser, args.html_source, test_name=args.name)
|
||||
return w.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run())
|
21
webrtc/docker-compose.yml
Normal file
21
webrtc/docker-compose.yml
Normal file
|
@ -0,0 +1,21 @@
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
# uncomment the sendrecv you would like to use
|
||||
#
|
||||
# sendrecv-gst:
|
||||
# build: ./sendrecv/gst
|
||||
sendrecv-gst-java:
|
||||
build: ./sendrecv/gst-java
|
||||
#sendrecv-gst-rust:
|
||||
# build: ./sendrecv/gst-rust
|
||||
sendrecv-js:
|
||||
build: ./sendrecv/js
|
||||
ports:
|
||||
- 8080:80
|
||||
depends_on:
|
||||
- signalling
|
||||
signalling:
|
||||
build: ./signalling
|
||||
ports:
|
||||
- 8443:8443
|
453
webrtc/janus/janusvideoroom.py
Normal file
453
webrtc/janus/janusvideoroom.py
Normal file
|
@ -0,0 +1,453 @@
|
|||
# Janus Videoroom example
|
||||
# Copyright @tobiasfriden and @saket424 on github
|
||||
# See https://github.com/centricular/gstwebrtc-demos/issues/66
|
||||
# Copyright Jan Schmidt <jan@centricular.com> 2020
|
||||
import random
|
||||
import ssl
|
||||
import websockets
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import string
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
|
||||
import attr
|
||||
|
||||
# Set to False to send H.264
|
||||
DO_VP8 = True
|
||||
# Set to False to disable RTX (lost packet retransmission)
|
||||
DO_RTX = True
|
||||
# Choose the video source:
|
||||
VIDEO_SRC="videotestsrc pattern=ball"
|
||||
# VIDEO_SRC="v4l2src"
|
||||
|
||||
|
||||
@attr.s
|
||||
class JanusEvent:
|
||||
sender = attr.ib(validator=attr.validators.instance_of(int))
|
||||
|
||||
@attr.s
|
||||
class PluginData(JanusEvent):
|
||||
plugin = attr.ib(validator=attr.validators.instance_of(str))
|
||||
data = attr.ib()
|
||||
jsep = attr.ib()
|
||||
|
||||
@attr.s
|
||||
class WebrtcUp(JanusEvent):
|
||||
pass
|
||||
|
||||
@attr.s
|
||||
class Media(JanusEvent):
|
||||
receiving = attr.ib(validator=attr.validators.instance_of(bool))
|
||||
kind = attr.ib(validator=attr.validators.in_(["audio", "video"]))
|
||||
|
||||
@kind.validator
|
||||
def validate_kind(self, attribute, kind):
|
||||
if kind not in ["video", "audio"]:
|
||||
raise ValueError("kind must equal video or audio")
|
||||
|
||||
@attr.s
|
||||
class SlowLink(JanusEvent):
|
||||
uplink = attr.ib(validator=attr.validators.instance_of(bool))
|
||||
lost = attr.ib(validator=attr.validators.instance_of(int))
|
||||
|
||||
@attr.s
|
||||
class HangUp(JanusEvent):
|
||||
reason = attr.ib(validator=attr.validators.instance_of(str))
|
||||
|
||||
@attr.s(cmp=False)
|
||||
class Ack:
|
||||
transaction = attr.ib(validator=attr.validators.instance_of(str))
|
||||
|
||||
@attr.s
|
||||
class Jsep:
|
||||
sdp = attr.ib()
|
||||
type = attr.ib(validator=attr.validators.in_(["offer", "pranswer", "answer", "rollback"]))
|
||||
|
||||
|
||||
import gi
|
||||
gi.require_version('Gst', '1.0')
|
||||
from gi.repository import Gst
|
||||
gi.require_version('GstWebRTC', '1.0')
|
||||
from gi.repository import GstWebRTC
|
||||
gi.require_version('GstSdp', '1.0')
|
||||
from gi.repository import GstSdp
|
||||
|
||||
if DO_VP8:
|
||||
( encoder, payloader, rtp_encoding) = ( "vp8enc target-bitrate=100000 overshoot=25 undershoot=100 deadline=33000 keyframe-max-dist=1", "rtpvp8pay picture-id-mode=2", "VP8" )
|
||||
else:
|
||||
( encoder, payloader, rtp_encoding) = ( "x264enc", "rtph264pay", "H264" )
|
||||
|
||||
PIPELINE_DESC = '''
|
||||
webrtcbin name=sendrecv stun-server=stun://stun.l.google.com:19302
|
||||
{} ! video/x-raw,width=640,height=480 ! videoconvert ! queue !
|
||||
{} ! {} ! queue ! application/x-rtp,media=video,encoding-name={},payload=96 ! sendrecv.
|
||||
'''.format(VIDEO_SRC, encoder, payloader, rtp_encoding)
|
||||
|
||||
def transaction_id():
|
||||
return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8))
|
||||
|
||||
@attr.s
|
||||
class JanusGateway:
|
||||
server = attr.ib(validator=attr.validators.instance_of(str))
|
||||
#secure = attr.ib(default=True)
|
||||
_messages = attr.ib(factory=set)
|
||||
conn = None
|
||||
|
||||
async def connect(self):
|
||||
sslCon=None
|
||||
if self.server.startswith("wss"):
|
||||
sslCon=ssl.SSLContext()
|
||||
self.conn = await websockets.connect(self.server, subprotocols=['janus-protocol'], ssl=sslCon)
|
||||
transaction = transaction_id()
|
||||
await self.conn.send(json.dumps({
|
||||
"janus": "create",
|
||||
"transaction": transaction
|
||||
}))
|
||||
resp = await self.conn.recv()
|
||||
print (resp)
|
||||
parsed = json.loads(resp)
|
||||
assert parsed["janus"] == "success", "Failed creating session"
|
||||
assert parsed["transaction"] == transaction, "Incorrect transaction"
|
||||
self.session = parsed["data"]["id"]
|
||||
|
||||
async def close(self):
|
||||
if self.conn:
|
||||
await self.conn.close()
|
||||
|
||||
async def attach(self, plugin):
|
||||
assert hasattr(self, "session"), "Must connect before attaching to plugin"
|
||||
transaction = transaction_id()
|
||||
await self.conn.send(json.dumps({
|
||||
"janus": "attach",
|
||||
"session_id": self.session,
|
||||
"plugin": plugin,
|
||||
"transaction": transaction
|
||||
}))
|
||||
resp = await self.conn.recv()
|
||||
parsed = json.loads(resp)
|
||||
assert parsed["janus"] == "success", "Failed attaching to {}".format(plugin)
|
||||
assert parsed["transaction"] == transaction, "Incorrect transaction"
|
||||
self.handle = parsed["data"]["id"]
|
||||
|
||||
async def sendtrickle(self, candidate):
|
||||
assert hasattr(self, "session"), "Must connect before sending messages"
|
||||
assert hasattr(self, "handle"), "Must attach before sending messages"
|
||||
|
||||
transaction = transaction_id()
|
||||
janus_message = {
|
||||
"janus": "trickle",
|
||||
"session_id": self.session,
|
||||
"handle_id": self.handle,
|
||||
"transaction": transaction,
|
||||
"candidate": candidate
|
||||
}
|
||||
|
||||
await self.conn.send(json.dumps(janus_message))
|
||||
|
||||
#while True:
|
||||
# resp = await self._recv_and_parse()
|
||||
# if isinstance(resp, PluginData):
|
||||
# return resp
|
||||
# else:
|
||||
# self._messages.add(resp)
|
||||
#
|
||||
async def sendmessage(self, body, jsep=None):
|
||||
assert hasattr(self, "session"), "Must connect before sending messages"
|
||||
assert hasattr(self, "handle"), "Must attach before sending messages"
|
||||
|
||||
transaction = transaction_id()
|
||||
janus_message = {
|
||||
"janus": "message",
|
||||
"session_id": self.session,
|
||||
"handle_id": self.handle,
|
||||
"transaction": transaction,
|
||||
"body": body
|
||||
}
|
||||
if jsep is not None:
|
||||
janus_message["jsep"] = jsep
|
||||
|
||||
await self.conn.send(json.dumps(janus_message))
|
||||
|
||||
#while True:
|
||||
# resp = await self._recv_and_parse()
|
||||
# if isinstance(resp, PluginData):
|
||||
# if jsep is not None:
|
||||
# await client.handle_sdp(resp.jsep)
|
||||
# return resp
|
||||
# else:
|
||||
# self._messages.add(resp)
|
||||
|
||||
async def keepalive(self):
|
||||
assert hasattr(self, "session"), "Must connect before sending messages"
|
||||
assert hasattr(self, "handle"), "Must attach before sending messages"
|
||||
|
||||
while True:
|
||||
try:
|
||||
await asyncio.sleep(10)
|
||||
transaction = transaction_id()
|
||||
await self.conn.send(json.dumps({
|
||||
"janus": "keepalive",
|
||||
"session_id": self.session,
|
||||
"handle_id": self.handle,
|
||||
"transaction": transaction
|
||||
}))
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
|
||||
async def recv(self):
|
||||
if len(self._messages) > 0:
|
||||
return self._messages.pop()
|
||||
else:
|
||||
return await self._recv_and_parse()
|
||||
|
||||
async def _recv_and_parse(self):
|
||||
raw = json.loads(await self.conn.recv())
|
||||
janus = raw["janus"]
|
||||
|
||||
if janus == "event":
|
||||
return PluginData(
|
||||
sender=raw["sender"],
|
||||
plugin=raw["plugindata"]["plugin"],
|
||||
data=raw["plugindata"]["data"],
|
||||
jsep=raw["jsep"] if "jsep" in raw else None
|
||||
)
|
||||
elif janus == "webrtcup":
|
||||
return WebrtcUp(
|
||||
sender=raw["sender"]
|
||||
)
|
||||
elif janus == "media":
|
||||
return Media(
|
||||
sender=raw["sender"],
|
||||
receiving=raw["receiving"],
|
||||
kind=raw["type"]
|
||||
)
|
||||
elif janus == "slowlink":
|
||||
return SlowLink(
|
||||
sender=raw["sender"],
|
||||
uplink=raw["uplink"],
|
||||
lost=raw["lost"]
|
||||
)
|
||||
elif janus == "hangup":
|
||||
return HangUp(
|
||||
sender=raw["sender"],
|
||||
reason=raw["reason"]
|
||||
)
|
||||
elif janus == "ack":
|
||||
return Ack(
|
||||
transaction=raw["transaction"]
|
||||
)
|
||||
else:
|
||||
return raw
|
||||
|
||||
class WebRTCClient:
|
||||
def __init__(self, peer_id, server):
|
||||
self.conn = None
|
||||
self.pipe = None
|
||||
self.webrtc = None
|
||||
self.peer_id = peer_id
|
||||
self.signaling = JanusGateway(server)
|
||||
self.request = None
|
||||
self.offermsg = None
|
||||
|
||||
def send_sdp_offer(self, offer):
|
||||
text = offer.sdp.as_text()
|
||||
print ('Sending offer:\n%s' % text)
|
||||
# configure media
|
||||
media = {'audio': True, 'video': True}
|
||||
request = {'request': 'publish'}
|
||||
request.update(media)
|
||||
self.request = request
|
||||
self.offermsg = { 'sdp': text, 'trickle': True, 'type': 'offer' }
|
||||
print (self.offermsg)
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.run_until_complete(self.signaling.sendmessage(self.request, self.offermsg))
|
||||
|
||||
def on_offer_created(self, promise, _, __):
|
||||
promise.wait()
|
||||
reply = promise.get_reply()
|
||||
offer = reply.get_value('offer')
|
||||
promise = Gst.Promise.new()
|
||||
self.webrtc.emit('set-local-description', offer, promise)
|
||||
promise.interrupt()
|
||||
self.send_sdp_offer(offer)
|
||||
|
||||
def on_negotiation_needed(self, element):
|
||||
promise = Gst.Promise.new_with_change_func(self.on_offer_created, element, None)
|
||||
element.emit('create-offer', None, promise)
|
||||
|
||||
def send_ice_candidate_message(self, _, mlineindex, candidate):
|
||||
icemsg = {'candidate': candidate, 'sdpMLineIndex': mlineindex}
|
||||
print ("Sending ICE", icemsg)
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.run_until_complete(self.signaling.sendtrickle(icemsg))
|
||||
|
||||
def on_incoming_decodebin_stream(self, _, pad):
|
||||
if not pad.has_current_caps():
|
||||
print (pad, 'has no caps, ignoring')
|
||||
return
|
||||
|
||||
caps = pad.get_current_caps()
|
||||
name = caps.to_string()
|
||||
if name.startswith('video'):
|
||||
q = Gst.ElementFactory.make('queue')
|
||||
conv = Gst.ElementFactory.make('videoconvert')
|
||||
sink = Gst.ElementFactory.make('autovideosink')
|
||||
self.pipe.add(q)
|
||||
self.pipe.add(conv)
|
||||
self.pipe.add(sink)
|
||||
self.pipe.sync_children_states()
|
||||
pad.link(q.get_static_pad('sink'))
|
||||
q.link(conv)
|
||||
conv.link(sink)
|
||||
elif name.startswith('audio'):
|
||||
q = Gst.ElementFactory.make('queue')
|
||||
conv = Gst.ElementFactory.make('audioconvert')
|
||||
resample = Gst.ElementFactory.make('audioresample')
|
||||
sink = Gst.ElementFactory.make('autoaudiosink')
|
||||
self.pipe.add(q)
|
||||
self.pipe.add(conv)
|
||||
self.pipe.add(resample)
|
||||
self.pipe.add(sink)
|
||||
self.pipe.sync_children_states()
|
||||
pad.link(q.get_static_pad('sink'))
|
||||
q.link(conv)
|
||||
conv.link(resample)
|
||||
resample.link(sink)
|
||||
|
||||
def on_incoming_stream(self, _, pad):
|
||||
if pad.direction != Gst.PadDirection.SRC:
|
||||
return
|
||||
|
||||
decodebin = Gst.ElementFactory.make('decodebin')
|
||||
decodebin.connect('pad-added', self.on_incoming_decodebin_stream)
|
||||
self.pipe.add(decodebin)
|
||||
decodebin.sync_state_with_parent()
|
||||
self.webrtc.link(decodebin)
|
||||
|
||||
def start_pipeline(self):
|
||||
self.pipe = Gst.parse_launch(PIPELINE_DESC)
|
||||
self.webrtc = self.pipe.get_by_name('sendrecv')
|
||||
self.webrtc.connect('on-negotiation-needed', self.on_negotiation_needed)
|
||||
self.webrtc.connect('on-ice-candidate', self.send_ice_candidate_message)
|
||||
self.webrtc.connect('pad-added', self.on_incoming_stream)
|
||||
|
||||
trans = self.webrtc.emit('get-transceiver', 0)
|
||||
if DO_RTX:
|
||||
trans.set_property ('do-nack', True)
|
||||
self.pipe.set_state(Gst.State.PLAYING)
|
||||
|
||||
def extract_ice_from_sdp(self, sdp):
|
||||
mlineindex = -1
|
||||
for line in sdp.splitlines():
|
||||
if line.startswith("a=candidate"):
|
||||
candidate = line[2:]
|
||||
if mlineindex < 0:
|
||||
print("Received ice candidate in SDP before any m= line")
|
||||
continue
|
||||
print ('Received remote ice-candidate mlineindex {}: {}'.format(mlineindex, candidate))
|
||||
self.webrtc.emit('add-ice-candidate', mlineindex, candidate)
|
||||
elif line.startswith("m="):
|
||||
mlineindex += 1
|
||||
|
||||
async def handle_sdp(self, msg):
|
||||
print (msg)
|
||||
if 'sdp' in msg:
|
||||
sdp = msg['sdp']
|
||||
assert(msg['type'] == 'answer')
|
||||
print ('Received answer:\n%s' % sdp)
|
||||
res, sdpmsg = GstSdp.SDPMessage.new()
|
||||
GstSdp.sdp_message_parse_buffer(bytes(sdp.encode()), sdpmsg)
|
||||
|
||||
answer = GstWebRTC.WebRTCSessionDescription.new(GstWebRTC.WebRTCSDPType.ANSWER, sdpmsg)
|
||||
promise = Gst.Promise.new()
|
||||
self.webrtc.emit('set-remote-description', answer, promise)
|
||||
promise.interrupt()
|
||||
|
||||
# Extract ICE candidates from the SDP to work around a GStreamer
|
||||
# limitation in (at least) 1.16.2 and below
|
||||
self.extract_ice_from_sdp (sdp)
|
||||
|
||||
elif 'ice' in msg:
|
||||
ice = msg['ice']
|
||||
candidate = ice['candidate']
|
||||
sdpmlineindex = ice['sdpMLineIndex']
|
||||
self.webrtc.emit('add-ice-candidate', sdpmlineindex, candidate)
|
||||
|
||||
async def loop(self):
|
||||
signaling = self.signaling
|
||||
await signaling.connect()
|
||||
await signaling.attach("janus.plugin.videoroom")
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(signaling.keepalive())
|
||||
#asyncio.create_task(self.keepalive())
|
||||
|
||||
joinmessage = { "request": "join", "ptype": "publisher", "room": 1234, "display": self.peer_id }
|
||||
await signaling.sendmessage(joinmessage)
|
||||
|
||||
assert signaling.conn
|
||||
self.start_pipeline()
|
||||
|
||||
while True:
|
||||
try:
|
||||
msg = await signaling.recv()
|
||||
if isinstance(msg, PluginData):
|
||||
if msg.jsep is not None:
|
||||
await self.handle_sdp(msg.jsep)
|
||||
elif isinstance(msg, Media):
|
||||
print (msg)
|
||||
elif isinstance(msg, WebrtcUp):
|
||||
print (msg)
|
||||
elif isinstance(msg, SlowLink):
|
||||
print (msg)
|
||||
elif isinstance(msg, HangUp):
|
||||
print (msg)
|
||||
elif not isinstance(msg, Ack):
|
||||
if 'candidate' in msg:
|
||||
ice = msg['candidate']
|
||||
print (ice)
|
||||
if 'candidate' in ice:
|
||||
candidate = ice['candidate']
|
||||
sdpmlineindex = ice['sdpMLineIndex']
|
||||
self.webrtc.emit('add-ice-candidate', sdpmlineindex, candidate)
|
||||
print(msg)
|
||||
except (KeyboardInterrupt, ConnectionClosed):
|
||||
return
|
||||
|
||||
return 0
|
||||
|
||||
async def close(self):
|
||||
return await self.signaling.close()
|
||||
|
||||
def check_plugins():
|
||||
needed = ["opus", "vpx", "nice", "webrtc", "dtls", "srtp", "rtp",
|
||||
"rtpmanager", "videotestsrc", "audiotestsrc"]
|
||||
missing = list(filter(lambda p: Gst.Registry.get().find_plugin(p) is None, needed))
|
||||
if len(missing):
|
||||
print('Missing gstreamer plugins:', missing)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
Gst.init(None)
|
||||
if not check_plugins():
|
||||
sys.exit(1)
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('label', help='videoroom label')
|
||||
parser.add_argument('--server', help='Signalling server to connect to, eg "wss://127.0.0.1:8989"')
|
||||
args = parser.parse_args()
|
||||
c = WebRTCClient(args.label, args.server)
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
loop.run_until_complete(
|
||||
c.loop()
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
print("Interrupted, cleaning up")
|
||||
loop.run_until_complete(c.close())
|
41
webrtc/meson.build
Normal file
41
webrtc/meson.build
Normal file
|
@ -0,0 +1,41 @@
|
|||
project('gstwebrtc-demo', 'c',
|
||||
meson_version : '>= 0.48',
|
||||
license: 'BSD-2-Clause',
|
||||
default_options : [ 'warning_level=1',
|
||||
'buildtype=debug' ])
|
||||
|
||||
cc = meson.get_compiler('c')
|
||||
|
||||
if cc.get_id() == 'msvc'
|
||||
add_project_arguments(
|
||||
cc.get_supported_arguments(['/utf-8']), # set the input encoding to utf-8
|
||||
language : 'c')
|
||||
endif
|
||||
|
||||
gst_req = '>= 1.14.0'
|
||||
gst_dep = dependency('gstreamer-1.0', version : gst_req,
|
||||
fallback : ['gstreamer', 'gst_dep'])
|
||||
gstsdp_dep = dependency('gstreamer-sdp-1.0', version : gst_req,
|
||||
fallback : ['gst-plugins-base', 'sdp_dep'])
|
||||
gstwebrtc_dep = dependency('gstreamer-webrtc-1.0', version : gst_req,
|
||||
fallback : ['gst-plugins-bad', 'gstwebrtc_dep'])
|
||||
|
||||
libsoup_dep = dependency('libsoup-2.4', version : '>=2.48',
|
||||
fallback : ['libsoup', 'libsoup_dep'])
|
||||
json_glib_dep = dependency('json-glib-1.0',
|
||||
fallback : ['json-glib', 'json_glib_dep'])
|
||||
|
||||
|
||||
py3_mod = import('python3')
|
||||
py3 = py3_mod.find_python()
|
||||
|
||||
py3_version = py3_mod.language_version()
|
||||
if py3_version.version_compare('< 3.6')
|
||||
error('Could not find a sufficient python version required: 3.6, found {}'.format(py3_version))
|
||||
endif
|
||||
|
||||
subdir('multiparty-sendrecv')
|
||||
subdir('signalling')
|
||||
subdir('sendrecv')
|
||||
|
||||
subdir('check')
|
1344
webrtc/multiparty-sendrecv/gst-rust/Cargo.lock
generated
Normal file
1344
webrtc/multiparty-sendrecv/gst-rust/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
19
webrtc/multiparty-sendrecv/gst-rust/Cargo.toml
Normal file
19
webrtc/multiparty-sendrecv/gst-rust/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "webrtc-app"
|
||||
version = "0.1.0"
|
||||
authors = ["Sebastian Dröge <sebastian@centricular.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3"
|
||||
async-std = "1"
|
||||
structopt = { version = "0.3", default-features = false }
|
||||
anyhow = "1"
|
||||
rand = "0.7"
|
||||
async-tungstenite = { version = "0.5", features = ["async-std-runtime", "async-native-tls"] }
|
||||
gst = { package = "gstreamer", version = "0.15", features = ["v1_14"] }
|
||||
gst-webrtc = { package = "gstreamer-webrtc", version = "0.15" }
|
||||
gst-sdp = { package = "gstreamer-sdp", version = "0.15", features = ["v1_14"] }
|
||||
serde = "1"
|
||||
serde_derive = "1"
|
||||
serde_json = "1"
|
67
webrtc/multiparty-sendrecv/gst-rust/src/macos_workaround.rs
Normal file
67
webrtc/multiparty-sendrecv/gst-rust/src/macos_workaround.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
/// macOS has a specific requirement that there must be a run loop running
|
||||
/// on the main thread in order to open windows and use OpenGL.
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod runloop {
|
||||
use std::os::raw::c_void;
|
||||
#[repr(C)]
|
||||
pub struct CFRunLoop(*mut c_void);
|
||||
|
||||
#[link(name = "foundation", kind = "framework")]
|
||||
extern "C" {
|
||||
fn CFRunLoopRun();
|
||||
fn CFRunLoopGetMain() -> *mut c_void;
|
||||
fn CFRunLoopStop(l: *mut c_void);
|
||||
}
|
||||
|
||||
impl CFRunLoop {
|
||||
pub fn run() {
|
||||
unsafe {
|
||||
CFRunLoopRun();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_main() -> CFRunLoop {
|
||||
unsafe {
|
||||
let r = CFRunLoopGetMain();
|
||||
assert!(!r.is_null());
|
||||
CFRunLoop(r)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
unsafe { CFRunLoopStop(self.0) }
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for CFRunLoop {}
|
||||
}
|
||||
|
||||
/// On macOS this launches the callback function on a thread.
|
||||
/// On other platforms it's just executed immediately.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn run<T, F: FnOnce() -> T + Send + 'static>(main: F) -> T
|
||||
where
|
||||
T: Send + 'static,
|
||||
{
|
||||
main()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn run<T, F: FnOnce() -> T + Send + 'static>(main: F) -> T
|
||||
where
|
||||
T: Send + 'static,
|
||||
{
|
||||
use std::thread;
|
||||
|
||||
let l = runloop::CFRunLoop::get_main();
|
||||
let t = thread::spawn(move || {
|
||||
let res = main();
|
||||
l.stop();
|
||||
res
|
||||
});
|
||||
|
||||
runloop::CFRunLoop::run();
|
||||
|
||||
t.join().unwrap()
|
||||
}
|
1074
webrtc/multiparty-sendrecv/gst-rust/src/main.rs
Normal file
1074
webrtc/multiparty-sendrecv/gst-rust/src/main.rs
Normal file
File diff suppressed because it is too large
Load diff
1
webrtc/multiparty-sendrecv/gst/.gitignore
vendored
Normal file
1
webrtc/multiparty-sendrecv/gst/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
mp-webrtc-sendrecv
|
6
webrtc/multiparty-sendrecv/gst/Makefile
Normal file
6
webrtc/multiparty-sendrecv/gst/Makefile
Normal file
|
@ -0,0 +1,6 @@
|
|||
CC := gcc
|
||||
LIBS := $(shell pkg-config --libs --cflags gstreamer-webrtc-1.0 gstreamer-sdp-1.0 libsoup-2.4 json-glib-1.0)
|
||||
CFLAGS := -O0 -ggdb -Wall -fno-omit-frame-pointer
|
||||
|
||||
mp-webrtc-sendrecv: mp-webrtc-sendrecv.c
|
||||
"$(CC)" $(CFLAGS) $^ $(LIBS) -o $@
|
3
webrtc/multiparty-sendrecv/gst/meson.build
Normal file
3
webrtc/multiparty-sendrecv/gst/meson.build
Normal file
|
@ -0,0 +1,3 @@
|
|||
executable('mp-webrtc-sendrecv',
|
||||
'mp-webrtc-sendrecv.c',
|
||||
dependencies : [gst_dep, gstsdp_dep, gstwebrtc_dep, libsoup_dep, json_glib_dep ])
|
982
webrtc/multiparty-sendrecv/gst/mp-webrtc-sendrecv.c
Normal file
982
webrtc/multiparty-sendrecv/gst/mp-webrtc-sendrecv.c
Normal file
|
@ -0,0 +1,982 @@
|
|||
/*
|
||||
* Demo gstreamer app for negotiating and streaming a sendrecv audio-only webrtc
|
||||
* stream to all the peers in a multiparty room.
|
||||
*
|
||||
* gcc mp-webrtc-sendrecv.c $(pkg-config --cflags --libs gstreamer-webrtc-1.0 gstreamer-sdp-1.0 libsoup-2.4 json-glib-1.0) -o mp-webrtc-sendrecv
|
||||
*
|
||||
* Author: Nirbheek Chauhan <nirbheek@centricular.com>
|
||||
*/
|
||||
#include <gst/gst.h>
|
||||
#include <gst/sdp/sdp.h>
|
||||
#define GST_USE_UNSTABLE_API
|
||||
#include <gst/webrtc/webrtc.h>
|
||||
|
||||
/* For signalling */
|
||||
#include <libsoup/soup.h>
|
||||
#include <json-glib/json-glib.h>
|
||||
|
||||
#include <string.h>
|
||||
|
||||
enum AppState
|
||||
{
|
||||
APP_STATE_UNKNOWN = 0,
|
||||
APP_STATE_ERROR = 1, /* generic error */
|
||||
SERVER_CONNECTING = 1000,
|
||||
SERVER_CONNECTION_ERROR,
|
||||
SERVER_CONNECTED, /* Ready to register */
|
||||
SERVER_REGISTERING = 2000,
|
||||
SERVER_REGISTRATION_ERROR,
|
||||
SERVER_REGISTERED, /* Ready to call a peer */
|
||||
SERVER_CLOSED, /* server connection closed by us or the server */
|
||||
ROOM_JOINING = 3000,
|
||||
ROOM_JOIN_ERROR,
|
||||
ROOM_JOINED,
|
||||
ROOM_CALL_NEGOTIATING = 4000, /* negotiating with some or all peers */
|
||||
ROOM_CALL_OFFERING, /* when we're the one sending the offer */
|
||||
ROOM_CALL_ANSWERING, /* when we're the one answering an offer */
|
||||
ROOM_CALL_STARTED, /* in a call with some or all peers */
|
||||
ROOM_CALL_STOPPING,
|
||||
ROOM_CALL_STOPPED,
|
||||
ROOM_CALL_ERROR,
|
||||
};
|
||||
|
||||
static GMainLoop *loop;
|
||||
static GstElement *pipeline;
|
||||
static GList *peers;
|
||||
|
||||
static SoupWebsocketConnection *ws_conn = NULL;
|
||||
static enum AppState app_state = 0;
|
||||
static const gchar *default_server_url = "wss://webrtc.nirbheek.in:8443";
|
||||
static gchar *server_url = NULL;
|
||||
static gchar *local_id = NULL;
|
||||
static gchar *room_id = NULL;
|
||||
static gboolean strict_ssl = TRUE;
|
||||
|
||||
static GOptionEntry entries[] = {
|
||||
{"name", 0, 0, G_OPTION_ARG_STRING, &local_id,
|
||||
"Name we will send to the server", "ID"},
|
||||
{"room-id", 0, 0, G_OPTION_ARG_STRING, &room_id,
|
||||
"Room name to join or create", "ID"},
|
||||
{"server", 0, 0, G_OPTION_ARG_STRING, &server_url,
|
||||
"Signalling server to connect to", "URL"},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
static gint
|
||||
compare_str_glist (gconstpointer a, gconstpointer b)
|
||||
{
|
||||
return g_strcmp0 (a, b);
|
||||
}
|
||||
|
||||
static const gchar *
|
||||
find_peer_from_list (const gchar * peer_id)
|
||||
{
|
||||
return (g_list_find_custom (peers, peer_id, compare_str_glist))->data;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
cleanup_and_quit_loop (const gchar * msg, enum AppState state)
|
||||
{
|
||||
if (msg)
|
||||
g_printerr ("%s\n", msg);
|
||||
if (state > 0)
|
||||
app_state = state;
|
||||
|
||||
if (ws_conn) {
|
||||
if (soup_websocket_connection_get_state (ws_conn) ==
|
||||
SOUP_WEBSOCKET_STATE_OPEN)
|
||||
/* This will call us again */
|
||||
soup_websocket_connection_close (ws_conn, 1000, "");
|
||||
else
|
||||
g_object_unref (ws_conn);
|
||||
}
|
||||
|
||||
if (loop) {
|
||||
g_main_loop_quit (loop);
|
||||
loop = NULL;
|
||||
}
|
||||
|
||||
/* To allow usage as a GSourceFunc */
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static gchar *
|
||||
get_string_from_json_object (JsonObject * object)
|
||||
{
|
||||
JsonNode *root;
|
||||
JsonGenerator *generator;
|
||||
gchar *text;
|
||||
|
||||
/* Make it the root node */
|
||||
root = json_node_init_object (json_node_alloc (), object);
|
||||
generator = json_generator_new ();
|
||||
json_generator_set_root (generator, root);
|
||||
text = json_generator_to_data (generator, NULL);
|
||||
|
||||
/* Release everything */
|
||||
g_object_unref (generator);
|
||||
json_node_free (root);
|
||||
return text;
|
||||
}
|
||||
|
||||
static void
|
||||
handle_media_stream (GstPad * pad, GstElement * pipe, const char *convert_name,
|
||||
const char *sink_name)
|
||||
{
|
||||
GstPad *qpad;
|
||||
GstElement *q, *conv, *sink;
|
||||
GstPadLinkReturn ret;
|
||||
|
||||
q = gst_element_factory_make ("queue", NULL);
|
||||
g_assert_nonnull (q);
|
||||
conv = gst_element_factory_make (convert_name, NULL);
|
||||
g_assert_nonnull (conv);
|
||||
sink = gst_element_factory_make (sink_name, NULL);
|
||||
g_assert_nonnull (sink);
|
||||
gst_bin_add_many (GST_BIN (pipe), q, conv, sink, NULL);
|
||||
gst_element_sync_state_with_parent (q);
|
||||
gst_element_sync_state_with_parent (conv);
|
||||
gst_element_sync_state_with_parent (sink);
|
||||
gst_element_link_many (q, conv, sink, NULL);
|
||||
|
||||
qpad = gst_element_get_static_pad (q, "sink");
|
||||
|
||||
ret = gst_pad_link (pad, qpad);
|
||||
g_assert_cmpint (ret, ==, GST_PAD_LINK_OK);
|
||||
}
|
||||
|
||||
static void
|
||||
on_incoming_decodebin_stream (GstElement * decodebin, GstPad * pad,
|
||||
GstElement * pipe)
|
||||
{
|
||||
GstCaps *caps;
|
||||
const gchar *name;
|
||||
|
||||
if (!gst_pad_has_current_caps (pad)) {
|
||||
g_printerr ("Pad '%s' has no caps, can't do anything, ignoring\n",
|
||||
GST_PAD_NAME (pad));
|
||||
return;
|
||||
}
|
||||
|
||||
caps = gst_pad_get_current_caps (pad);
|
||||
name = gst_structure_get_name (gst_caps_get_structure (caps, 0));
|
||||
|
||||
if (g_str_has_prefix (name, "video")) {
|
||||
handle_media_stream (pad, pipe, "videoconvert", "autovideosink");
|
||||
} else if (g_str_has_prefix (name, "audio")) {
|
||||
handle_media_stream (pad, pipe, "audioconvert", "autoaudiosink");
|
||||
} else {
|
||||
g_printerr ("Unknown pad %s, ignoring", GST_PAD_NAME (pad));
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
on_incoming_stream (GstElement * webrtc, GstPad * pad, GstElement * pipe)
|
||||
{
|
||||
GstElement *decodebin;
|
||||
GstPad *sinkpad;
|
||||
|
||||
if (GST_PAD_DIRECTION (pad) != GST_PAD_SRC)
|
||||
return;
|
||||
|
||||
decodebin = gst_element_factory_make ("decodebin", NULL);
|
||||
g_signal_connect (decodebin, "pad-added",
|
||||
G_CALLBACK (on_incoming_decodebin_stream), pipe);
|
||||
gst_bin_add (GST_BIN (pipe), decodebin);
|
||||
gst_element_sync_state_with_parent (decodebin);
|
||||
|
||||
sinkpad = gst_element_get_static_pad (decodebin, "sink");
|
||||
gst_pad_link (pad, sinkpad);
|
||||
gst_object_unref (sinkpad);
|
||||
}
|
||||
|
||||
static void
|
||||
send_room_peer_msg (const gchar * text, const gchar * peer_id)
|
||||
{
|
||||
gchar *msg;
|
||||
|
||||
msg = g_strdup_printf ("ROOM_PEER_MSG %s %s", peer_id, text);
|
||||
soup_websocket_connection_send_text (ws_conn, msg);
|
||||
g_free (msg);
|
||||
}
|
||||
|
||||
static void
|
||||
send_ice_candidate_message (GstElement * webrtc G_GNUC_UNUSED, guint mlineindex,
|
||||
gchar * candidate, const gchar * peer_id)
|
||||
{
|
||||
gchar *text;
|
||||
JsonObject *ice, *msg;
|
||||
|
||||
if (app_state < ROOM_CALL_OFFERING) {
|
||||
cleanup_and_quit_loop ("Can't send ICE, not in call", APP_STATE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
ice = json_object_new ();
|
||||
json_object_set_string_member (ice, "candidate", candidate);
|
||||
json_object_set_int_member (ice, "sdpMLineIndex", mlineindex);
|
||||
msg = json_object_new ();
|
||||
json_object_set_object_member (msg, "ice", ice);
|
||||
text = get_string_from_json_object (msg);
|
||||
json_object_unref (msg);
|
||||
|
||||
send_room_peer_msg (text, peer_id);
|
||||
g_free (text);
|
||||
}
|
||||
|
||||
static void
|
||||
send_room_peer_sdp (GstWebRTCSessionDescription * desc, const gchar * peer_id)
|
||||
{
|
||||
JsonObject *msg, *sdp;
|
||||
gchar *text, *sdptype, *sdptext;
|
||||
|
||||
g_assert_cmpint (app_state, >=, ROOM_CALL_OFFERING);
|
||||
|
||||
if (desc->type == GST_WEBRTC_SDP_TYPE_OFFER)
|
||||
sdptype = "offer";
|
||||
else if (desc->type == GST_WEBRTC_SDP_TYPE_ANSWER)
|
||||
sdptype = "answer";
|
||||
else
|
||||
g_assert_not_reached ();
|
||||
|
||||
text = gst_sdp_message_as_text (desc->sdp);
|
||||
g_print ("Sending sdp %s to %s:\n%s\n", sdptype, peer_id, text);
|
||||
|
||||
sdp = json_object_new ();
|
||||
json_object_set_string_member (sdp, "type", sdptype);
|
||||
json_object_set_string_member (sdp, "sdp", text);
|
||||
g_free (text);
|
||||
|
||||
msg = json_object_new ();
|
||||
json_object_set_object_member (msg, "sdp", sdp);
|
||||
sdptext = get_string_from_json_object (msg);
|
||||
json_object_unref (msg);
|
||||
|
||||
send_room_peer_msg (sdptext, peer_id);
|
||||
g_free (sdptext);
|
||||
}
|
||||
|
||||
/* Offer created by our pipeline, to be sent to the peer */
|
||||
static void
|
||||
on_offer_created (GstPromise * promise, const gchar * peer_id)
|
||||
{
|
||||
GstElement *webrtc;
|
||||
GstWebRTCSessionDescription *offer;
|
||||
const GstStructure *reply;
|
||||
|
||||
g_assert_cmpint (app_state, ==, ROOM_CALL_OFFERING);
|
||||
|
||||
g_assert_cmpint (gst_promise_wait (promise), ==, GST_PROMISE_RESULT_REPLIED);
|
||||
reply = gst_promise_get_reply (promise);
|
||||
gst_structure_get (reply, "offer",
|
||||
GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &offer, NULL);
|
||||
gst_promise_unref (promise);
|
||||
|
||||
promise = gst_promise_new ();
|
||||
webrtc = gst_bin_get_by_name (GST_BIN (pipeline), peer_id);
|
||||
g_assert_nonnull (webrtc);
|
||||
g_signal_emit_by_name (webrtc, "set-local-description", offer, promise);
|
||||
gst_promise_interrupt (promise);
|
||||
gst_promise_unref (promise);
|
||||
|
||||
/* Send offer to peer */
|
||||
send_room_peer_sdp (offer, peer_id);
|
||||
gst_webrtc_session_description_free (offer);
|
||||
}
|
||||
|
||||
static void
|
||||
on_negotiation_needed (GstElement * webrtc, const gchar * peer_id)
|
||||
{
|
||||
GstPromise *promise;
|
||||
|
||||
app_state = ROOM_CALL_OFFERING;
|
||||
promise = gst_promise_new_with_change_func (
|
||||
(GstPromiseChangeFunc) on_offer_created, (gpointer) peer_id, NULL);
|
||||
g_signal_emit_by_name (webrtc, "create-offer", NULL, promise);
|
||||
}
|
||||
|
||||
static void
|
||||
remove_peer_from_pipeline (const gchar * peer_id)
|
||||
{
|
||||
gchar *qname;
|
||||
GstPad *srcpad, *sinkpad;
|
||||
GstElement *webrtc, *q, *tee;
|
||||
|
||||
webrtc = gst_bin_get_by_name (GST_BIN (pipeline), peer_id);
|
||||
if (!webrtc)
|
||||
return;
|
||||
|
||||
gst_bin_remove (GST_BIN (pipeline), webrtc);
|
||||
gst_object_unref (webrtc);
|
||||
|
||||
qname = g_strdup_printf ("queue-%s", peer_id);
|
||||
q = gst_bin_get_by_name (GST_BIN (pipeline), qname);
|
||||
g_free (qname);
|
||||
|
||||
sinkpad = gst_element_get_static_pad (q, "sink");
|
||||
g_assert_nonnull (sinkpad);
|
||||
srcpad = gst_pad_get_peer (sinkpad);
|
||||
g_assert_nonnull (srcpad);
|
||||
gst_object_unref (sinkpad);
|
||||
|
||||
gst_bin_remove (GST_BIN (pipeline), q);
|
||||
gst_object_unref (q);
|
||||
|
||||
tee = gst_bin_get_by_name (GST_BIN (pipeline), "audiotee");
|
||||
g_assert_nonnull (tee);
|
||||
gst_element_release_request_pad (tee, srcpad);
|
||||
gst_object_unref (srcpad);
|
||||
gst_object_unref (tee);
|
||||
}
|
||||
|
||||
static void
|
||||
add_peer_to_pipeline (const gchar * peer_id, gboolean offer)
|
||||
{
|
||||
int ret;
|
||||
gchar *tmp;
|
||||
GstElement *tee, *webrtc, *q;
|
||||
GstPad *srcpad, *sinkpad;
|
||||
|
||||
tmp = g_strdup_printf ("queue-%s", peer_id);
|
||||
q = gst_element_factory_make ("queue", tmp);
|
||||
g_free (tmp);
|
||||
webrtc = gst_element_factory_make ("webrtcbin", peer_id);
|
||||
|
||||
gst_bin_add_many (GST_BIN (pipeline), q, webrtc, NULL);
|
||||
|
||||
srcpad = gst_element_get_static_pad (q, "src");
|
||||
g_assert_nonnull (srcpad);
|
||||
sinkpad = gst_element_get_request_pad (webrtc, "sink_%u");
|
||||
g_assert_nonnull (sinkpad);
|
||||
ret = gst_pad_link (srcpad, sinkpad);
|
||||
g_assert_cmpint (ret, ==, GST_PAD_LINK_OK);
|
||||
gst_object_unref (srcpad);
|
||||
gst_object_unref (sinkpad);
|
||||
|
||||
tee = gst_bin_get_by_name (GST_BIN (pipeline), "audiotee");
|
||||
g_assert_nonnull (tee);
|
||||
srcpad = gst_element_get_request_pad (tee, "src_%u");
|
||||
g_assert_nonnull (srcpad);
|
||||
gst_object_unref (tee);
|
||||
sinkpad = gst_element_get_static_pad (q, "sink");
|
||||
g_assert_nonnull (sinkpad);
|
||||
ret = gst_pad_link (srcpad, sinkpad);
|
||||
g_assert_cmpint (ret, ==, GST_PAD_LINK_OK);
|
||||
gst_object_unref (srcpad);
|
||||
gst_object_unref (sinkpad);
|
||||
|
||||
/* This is the gstwebrtc entry point where we create the offer and so on. It
|
||||
* will be called when the pipeline goes to PLAYING.
|
||||
* XXX: We must connect this after webrtcbin has been linked to a source via
|
||||
* get_request_pad() and before we go from NULL->READY otherwise webrtcbin
|
||||
* will create an SDP offer with no media lines in it. */
|
||||
if (offer)
|
||||
g_signal_connect (webrtc, "on-negotiation-needed",
|
||||
G_CALLBACK (on_negotiation_needed), (gpointer) peer_id);
|
||||
|
||||
/* We need to transmit this ICE candidate to the browser via the websockets
|
||||
* signalling server. Incoming ice candidates from the browser need to be
|
||||
* added by us too, see on_server_message() */
|
||||
g_signal_connect (webrtc, "on-ice-candidate",
|
||||
G_CALLBACK (send_ice_candidate_message), (gpointer) peer_id);
|
||||
/* Incoming streams will be exposed via this signal */
|
||||
g_signal_connect (webrtc, "pad-added", G_CALLBACK (on_incoming_stream),
|
||||
pipeline);
|
||||
|
||||
/* Set to pipeline branch to PLAYING */
|
||||
ret = gst_element_sync_state_with_parent (q);
|
||||
g_assert_true (ret);
|
||||
ret = gst_element_sync_state_with_parent (webrtc);
|
||||
g_assert_true (ret);
|
||||
}
|
||||
|
||||
static void
|
||||
call_peer (const gchar * peer_id)
|
||||
{
|
||||
add_peer_to_pipeline (peer_id, TRUE);
|
||||
}
|
||||
|
||||
static void
|
||||
incoming_call_from_peer (const gchar * peer_id)
|
||||
{
|
||||
add_peer_to_pipeline (peer_id, FALSE);
|
||||
}
|
||||
|
||||
#define STR(x) #x
|
||||
#define RTP_CAPS_OPUS(x) "application/x-rtp,media=audio,encoding-name=OPUS,payload=" STR(x)
|
||||
|
||||
static gboolean
|
||||
start_pipeline (void)
|
||||
{
|
||||
GstStateChangeReturn ret;
|
||||
GError *error = NULL;
|
||||
|
||||
/* NOTE: webrtcbin currently does not support dynamic addition/removal of
|
||||
* streams, so we use a separate webrtcbin for each peer, but all of them are
|
||||
* inside the same pipeline. We start by connecting it to a fakesink so that
|
||||
* we can preroll early. */
|
||||
pipeline = gst_parse_launch ("tee name=audiotee ! queue ! fakesink "
|
||||
"audiotestsrc is-live=true wave=red-noise ! queue ! opusenc ! rtpopuspay ! "
|
||||
"queue ! " RTP_CAPS_OPUS (96) " ! audiotee. ", &error);
|
||||
|
||||
if (error) {
|
||||
g_printerr ("Failed to parse launch: %s\n", error->message);
|
||||
g_error_free (error);
|
||||
goto err;
|
||||
}
|
||||
|
||||
g_print ("Starting pipeline, not transmitting yet\n");
|
||||
ret = gst_element_set_state (GST_ELEMENT (pipeline), GST_STATE_PLAYING);
|
||||
if (ret == GST_STATE_CHANGE_FAILURE)
|
||||
goto err;
|
||||
|
||||
return TRUE;
|
||||
|
||||
err:
|
||||
g_print ("State change failure\n");
|
||||
if (pipeline)
|
||||
g_clear_object (&pipeline);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
join_room_on_server (void)
|
||||
{
|
||||
gchar *msg;
|
||||
|
||||
if (soup_websocket_connection_get_state (ws_conn) !=
|
||||
SOUP_WEBSOCKET_STATE_OPEN)
|
||||
return FALSE;
|
||||
|
||||
if (!room_id)
|
||||
return FALSE;
|
||||
|
||||
g_print ("Joining room %s\n", room_id);
|
||||
app_state = ROOM_JOINING;
|
||||
msg = g_strdup_printf ("ROOM %s", room_id);
|
||||
soup_websocket_connection_send_text (ws_conn, msg);
|
||||
g_free (msg);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
register_with_server (void)
|
||||
{
|
||||
gchar *hello;
|
||||
|
||||
if (soup_websocket_connection_get_state (ws_conn) !=
|
||||
SOUP_WEBSOCKET_STATE_OPEN)
|
||||
return FALSE;
|
||||
|
||||
g_print ("Registering id %s with server\n", local_id);
|
||||
app_state = SERVER_REGISTERING;
|
||||
|
||||
/* Register with the server with a random integer id. Reply will be received
|
||||
* by on_server_message() */
|
||||
hello = g_strdup_printf ("HELLO %s", local_id);
|
||||
soup_websocket_connection_send_text (ws_conn, hello);
|
||||
g_free (hello);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
on_server_closed (SoupWebsocketConnection * conn G_GNUC_UNUSED,
|
||||
gpointer user_data G_GNUC_UNUSED)
|
||||
{
|
||||
app_state = SERVER_CLOSED;
|
||||
cleanup_and_quit_loop ("Server connection closed", 0);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
do_registration (void)
|
||||
{
|
||||
if (app_state != SERVER_REGISTERING) {
|
||||
cleanup_and_quit_loop ("ERROR: Received HELLO when not registering",
|
||||
APP_STATE_ERROR);
|
||||
return FALSE;
|
||||
}
|
||||
app_state = SERVER_REGISTERED;
|
||||
g_print ("Registered with server\n");
|
||||
/* Ask signalling server that we want to join a room */
|
||||
if (!join_room_on_server ()) {
|
||||
cleanup_and_quit_loop ("ERROR: Failed to join room", ROOM_CALL_ERROR);
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*
|
||||
* When we join a room, we are responsible for calling by starting negotiation
|
||||
* with each peer in it by sending an SDP offer and ICE candidates.
|
||||
*/
|
||||
static void
|
||||
do_join_room (const gchar * text)
|
||||
{
|
||||
gint ii, len;
|
||||
gchar **peer_ids;
|
||||
|
||||
if (app_state != ROOM_JOINING) {
|
||||
cleanup_and_quit_loop ("ERROR: Received ROOM_OK when not calling",
|
||||
ROOM_JOIN_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
app_state = ROOM_JOINED;
|
||||
g_print ("Room joined\n");
|
||||
/* Start recording, but not transmitting */
|
||||
if (!start_pipeline ()) {
|
||||
cleanup_and_quit_loop ("ERROR: Failed to start pipeline", ROOM_CALL_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
peer_ids = g_strsplit (text, " ", -1);
|
||||
g_assert_cmpstr (peer_ids[0], ==, "ROOM_OK");
|
||||
len = g_strv_length (peer_ids);
|
||||
/* There are peers in the room already. We need to start negotiation
|
||||
* (exchange SDP and ICE candidates) and transmission of media. */
|
||||
if (len > 1 && strlen (peer_ids[1]) > 0) {
|
||||
g_print ("Found %i peers already in room\n", len - 1);
|
||||
app_state = ROOM_CALL_OFFERING;
|
||||
for (ii = 1; ii < len; ii++) {
|
||||
gchar *peer_id = g_strdup (peer_ids[ii]);
|
||||
g_print ("Negotiating with peer %s\n", peer_id);
|
||||
/* This might fail asynchronously */
|
||||
call_peer (peer_id);
|
||||
peers = g_list_prepend (peers, peer_id);
|
||||
}
|
||||
}
|
||||
|
||||
g_strfreev (peer_ids);
|
||||
return;
|
||||
}
|
||||
|
||||
static void
|
||||
handle_error_message (const gchar * msg)
|
||||
{
|
||||
switch (app_state) {
|
||||
case SERVER_CONNECTING:
|
||||
app_state = SERVER_CONNECTION_ERROR;
|
||||
break;
|
||||
case SERVER_REGISTERING:
|
||||
app_state = SERVER_REGISTRATION_ERROR;
|
||||
break;
|
||||
case ROOM_JOINING:
|
||||
app_state = ROOM_JOIN_ERROR;
|
||||
break;
|
||||
case ROOM_JOINED:
|
||||
case ROOM_CALL_NEGOTIATING:
|
||||
case ROOM_CALL_OFFERING:
|
||||
case ROOM_CALL_ANSWERING:
|
||||
app_state = ROOM_CALL_ERROR;
|
||||
break;
|
||||
case ROOM_CALL_STARTED:
|
||||
case ROOM_CALL_STOPPING:
|
||||
case ROOM_CALL_STOPPED:
|
||||
app_state = ROOM_CALL_ERROR;
|
||||
break;
|
||||
default:
|
||||
app_state = APP_STATE_ERROR;
|
||||
}
|
||||
cleanup_and_quit_loop (msg, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
on_answer_created (GstPromise * promise, const gchar * peer_id)
|
||||
{
|
||||
GstElement *webrtc;
|
||||
GstWebRTCSessionDescription *answer;
|
||||
const GstStructure *reply;
|
||||
|
||||
g_assert_cmpint (app_state, ==, ROOM_CALL_ANSWERING);
|
||||
|
||||
g_assert_cmpint (gst_promise_wait (promise), ==, GST_PROMISE_RESULT_REPLIED);
|
||||
reply = gst_promise_get_reply (promise);
|
||||
gst_structure_get (reply, "answer",
|
||||
GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &answer, NULL);
|
||||
gst_promise_unref (promise);
|
||||
|
||||
promise = gst_promise_new ();
|
||||
webrtc = gst_bin_get_by_name (GST_BIN (pipeline), peer_id);
|
||||
g_assert_nonnull (webrtc);
|
||||
g_signal_emit_by_name (webrtc, "set-local-description", answer, promise);
|
||||
gst_promise_interrupt (promise);
|
||||
gst_promise_unref (promise);
|
||||
|
||||
/* Send offer to peer */
|
||||
send_room_peer_sdp (answer, peer_id);
|
||||
gst_webrtc_session_description_free (answer);
|
||||
|
||||
app_state = ROOM_CALL_STARTED;
|
||||
}
|
||||
|
||||
static void
|
||||
handle_sdp_offer (const gchar * peer_id, const gchar * text)
|
||||
{
|
||||
int ret;
|
||||
GstPromise *promise;
|
||||
GstElement *webrtc;
|
||||
GstSDPMessage *sdp;
|
||||
GstWebRTCSessionDescription *offer;
|
||||
|
||||
g_assert_cmpint (app_state, ==, ROOM_CALL_ANSWERING);
|
||||
|
||||
g_print ("Received offer:\n%s\n", text);
|
||||
|
||||
ret = gst_sdp_message_new (&sdp);
|
||||
g_assert_cmpint (ret, ==, GST_SDP_OK);
|
||||
|
||||
ret = gst_sdp_message_parse_buffer ((guint8 *) text, strlen (text), sdp);
|
||||
g_assert_cmpint (ret, ==, GST_SDP_OK);
|
||||
|
||||
offer = gst_webrtc_session_description_new (GST_WEBRTC_SDP_TYPE_OFFER, sdp);
|
||||
g_assert_nonnull (offer);
|
||||
|
||||
/* Set remote description on our pipeline */
|
||||
promise = gst_promise_new ();
|
||||
webrtc = gst_bin_get_by_name (GST_BIN (pipeline), peer_id);
|
||||
g_assert_nonnull (webrtc);
|
||||
g_signal_emit_by_name (webrtc, "set-remote-description", offer, promise);
|
||||
/* We don't want to be notified when the action is done */
|
||||
gst_promise_interrupt (promise);
|
||||
gst_promise_unref (promise);
|
||||
|
||||
/* Create an answer that we will send back to the peer */
|
||||
promise = gst_promise_new_with_change_func (
|
||||
(GstPromiseChangeFunc) on_answer_created, (gpointer) peer_id, NULL);
|
||||
g_signal_emit_by_name (webrtc, "create-answer", NULL, promise);
|
||||
|
||||
gst_webrtc_session_description_free (offer);
|
||||
gst_object_unref (webrtc);
|
||||
}
|
||||
|
||||
static void
|
||||
handle_sdp_answer (const gchar * peer_id, const gchar * text)
|
||||
{
|
||||
int ret;
|
||||
GstPromise *promise;
|
||||
GstElement *webrtc;
|
||||
GstSDPMessage *sdp;
|
||||
GstWebRTCSessionDescription *answer;
|
||||
|
||||
g_assert_cmpint (app_state, >=, ROOM_CALL_OFFERING);
|
||||
|
||||
g_print ("Received answer:\n%s\n", text);
|
||||
|
||||
ret = gst_sdp_message_new (&sdp);
|
||||
g_assert_cmpint (ret, ==, GST_SDP_OK);
|
||||
|
||||
ret = gst_sdp_message_parse_buffer ((guint8 *) text, strlen (text), sdp);
|
||||
g_assert_cmpint (ret, ==, GST_SDP_OK);
|
||||
|
||||
answer = gst_webrtc_session_description_new (GST_WEBRTC_SDP_TYPE_ANSWER, sdp);
|
||||
g_assert_nonnull (answer);
|
||||
|
||||
/* Set remote description on our pipeline */
|
||||
promise = gst_promise_new ();
|
||||
webrtc = gst_bin_get_by_name (GST_BIN (pipeline), peer_id);
|
||||
g_assert_nonnull (webrtc);
|
||||
g_signal_emit_by_name (webrtc, "set-remote-description", answer, promise);
|
||||
gst_object_unref (webrtc);
|
||||
/* We don't want to be notified when the action is done */
|
||||
gst_promise_interrupt (promise);
|
||||
gst_promise_unref (promise);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
handle_peer_message (const gchar * peer_id, const gchar * msg)
|
||||
{
|
||||
JsonNode *root;
|
||||
JsonObject *object, *child;
|
||||
JsonParser *parser = json_parser_new ();
|
||||
if (!json_parser_load_from_data (parser, msg, -1, NULL)) {
|
||||
g_printerr ("Unknown message '%s' from '%s', ignoring", msg, peer_id);
|
||||
g_object_unref (parser);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
root = json_parser_get_root (parser);
|
||||
if (!JSON_NODE_HOLDS_OBJECT (root)) {
|
||||
g_printerr ("Unknown json message '%s' from '%s', ignoring", msg, peer_id);
|
||||
g_object_unref (parser);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
g_print ("Message from peer %s: %s\n", peer_id, msg);
|
||||
|
||||
object = json_node_get_object (root);
|
||||
/* Check type of JSON message */
|
||||
if (json_object_has_member (object, "sdp")) {
|
||||
const gchar *text, *sdp_type;
|
||||
|
||||
g_assert_cmpint (app_state, >=, ROOM_JOINED);
|
||||
|
||||
child = json_object_get_object_member (object, "sdp");
|
||||
|
||||
if (!json_object_has_member (child, "type")) {
|
||||
cleanup_and_quit_loop ("ERROR: received SDP without 'type'",
|
||||
ROOM_CALL_ERROR);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
sdp_type = json_object_get_string_member (child, "type");
|
||||
text = json_object_get_string_member (child, "sdp");
|
||||
|
||||
if (g_strcmp0 (sdp_type, "offer") == 0) {
|
||||
app_state = ROOM_CALL_ANSWERING;
|
||||
incoming_call_from_peer (peer_id);
|
||||
handle_sdp_offer (peer_id, text);
|
||||
} else if (g_strcmp0 (sdp_type, "answer") == 0) {
|
||||
g_assert_cmpint (app_state, >=, ROOM_CALL_OFFERING);
|
||||
handle_sdp_answer (peer_id, text);
|
||||
app_state = ROOM_CALL_STARTED;
|
||||
} else {
|
||||
cleanup_and_quit_loop ("ERROR: invalid sdp_type", ROOM_CALL_ERROR);
|
||||
return FALSE;
|
||||
}
|
||||
} else if (json_object_has_member (object, "ice")) {
|
||||
GstElement *webrtc;
|
||||
const gchar *candidate;
|
||||
gint sdpmlineindex;
|
||||
|
||||
child = json_object_get_object_member (object, "ice");
|
||||
candidate = json_object_get_string_member (child, "candidate");
|
||||
sdpmlineindex = json_object_get_int_member (child, "sdpMLineIndex");
|
||||
|
||||
/* Add ice candidate sent by remote peer */
|
||||
webrtc = gst_bin_get_by_name (GST_BIN (pipeline), peer_id);
|
||||
g_assert_nonnull (webrtc);
|
||||
g_signal_emit_by_name (webrtc, "add-ice-candidate", sdpmlineindex,
|
||||
candidate);
|
||||
gst_object_unref (webrtc);
|
||||
} else {
|
||||
g_printerr ("Ignoring unknown JSON message:\n%s\n", msg);
|
||||
}
|
||||
g_object_unref (parser);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* One mega message handler for our asynchronous calling mechanism */
|
||||
static void
|
||||
on_server_message (SoupWebsocketConnection * conn, SoupWebsocketDataType type,
|
||||
GBytes * message, gpointer user_data)
|
||||
{
|
||||
gchar *text;
|
||||
|
||||
switch (type) {
|
||||
case SOUP_WEBSOCKET_DATA_BINARY:
|
||||
g_printerr ("Received unknown binary message, ignoring\n");
|
||||
return;
|
||||
case SOUP_WEBSOCKET_DATA_TEXT:{
|
||||
gsize size;
|
||||
const gchar *data = g_bytes_get_data (message, &size);
|
||||
/* Convert to NULL-terminated string */
|
||||
text = g_strndup (data, size);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
g_assert_not_reached ();
|
||||
}
|
||||
|
||||
/* Server has accepted our registration, we are ready to send commands */
|
||||
if (g_strcmp0 (text, "HELLO") == 0) {
|
||||
/* May fail asynchronously */
|
||||
do_registration ();
|
||||
/* Room-related message */
|
||||
} else if (g_str_has_prefix (text, "ROOM_")) {
|
||||
/* Room joined, now we can start negotiation */
|
||||
if (g_str_has_prefix (text, "ROOM_OK ")) {
|
||||
/* May fail asynchronously */
|
||||
do_join_room (text);
|
||||
} else if (g_str_has_prefix (text, "ROOM_PEER")) {
|
||||
gchar **splitm = NULL;
|
||||
const gchar *peer_id;
|
||||
/* SDP and ICE, usually */
|
||||
if (g_str_has_prefix (text, "ROOM_PEER_MSG")) {
|
||||
splitm = g_strsplit (text, " ", 3);
|
||||
peer_id = find_peer_from_list (splitm[1]);
|
||||
g_assert_nonnull (peer_id);
|
||||
/* Could be an offer or an answer, or ICE, or an arbitrary message */
|
||||
handle_peer_message (peer_id, splitm[2]);
|
||||
} else if (g_str_has_prefix (text, "ROOM_PEER_JOINED")) {
|
||||
splitm = g_strsplit (text, " ", 2);
|
||||
peers = g_list_prepend (peers, g_strdup (splitm[1]));
|
||||
peer_id = find_peer_from_list (splitm[1]);
|
||||
g_assert_nonnull (peer_id);
|
||||
g_print ("Peer %s has joined the room\n", peer_id);
|
||||
} else if (g_str_has_prefix (text, "ROOM_PEER_LEFT")) {
|
||||
splitm = g_strsplit (text, " ", 2);
|
||||
peer_id = find_peer_from_list (splitm[1]);
|
||||
g_assert_nonnull (peer_id);
|
||||
peers = g_list_remove (peers, peer_id);
|
||||
g_print ("Peer %s has left the room\n", peer_id);
|
||||
remove_peer_from_pipeline (peer_id);
|
||||
g_free ((gchar *) peer_id);
|
||||
/* TODO: cleanup pipeline */
|
||||
} else {
|
||||
g_printerr ("WARNING: Ignoring unknown message %s\n", text);
|
||||
}
|
||||
g_strfreev (splitm);
|
||||
} else {
|
||||
goto err;
|
||||
}
|
||||
/* Handle errors */
|
||||
} else if (g_str_has_prefix (text, "ERROR")) {
|
||||
handle_error_message (text);
|
||||
} else {
|
||||
goto err;
|
||||
}
|
||||
|
||||
out:
|
||||
g_free (text);
|
||||
return;
|
||||
|
||||
err:
|
||||
{
|
||||
gchar *err_s = g_strdup_printf ("ERROR: unknown message %s", text);
|
||||
cleanup_and_quit_loop (err_s, 0);
|
||||
g_free (err_s);
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
on_server_connected (SoupSession * session, GAsyncResult * res,
|
||||
SoupMessage * msg)
|
||||
{
|
||||
GError *error = NULL;
|
||||
|
||||
ws_conn = soup_session_websocket_connect_finish (session, res, &error);
|
||||
if (error) {
|
||||
cleanup_and_quit_loop (error->message, SERVER_CONNECTION_ERROR);
|
||||
g_error_free (error);
|
||||
return;
|
||||
}
|
||||
|
||||
g_assert_nonnull (ws_conn);
|
||||
|
||||
app_state = SERVER_CONNECTED;
|
||||
g_print ("Connected to signalling server\n");
|
||||
|
||||
g_signal_connect (ws_conn, "closed", G_CALLBACK (on_server_closed), NULL);
|
||||
g_signal_connect (ws_conn, "message", G_CALLBACK (on_server_message), NULL);
|
||||
|
||||
/* Register with the server so it knows about us and can accept commands
|
||||
* responses from the server will be handled in on_server_message() above */
|
||||
register_with_server ();
|
||||
}
|
||||
|
||||
/*
|
||||
* Connect to the signalling server. This is the entrypoint for everything else.
|
||||
*/
|
||||
static void
|
||||
connect_to_websocket_server_async (void)
|
||||
{
|
||||
SoupLogger *logger;
|
||||
SoupMessage *message;
|
||||
SoupSession *session;
|
||||
const char *https_aliases[] = { "wss", NULL };
|
||||
|
||||
session = soup_session_new_with_options (SOUP_SESSION_SSL_STRICT, strict_ssl,
|
||||
SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE,
|
||||
//SOUP_SESSION_SSL_CA_FILE, "/etc/ssl/certs/ca-bundle.crt",
|
||||
SOUP_SESSION_HTTPS_ALIASES, https_aliases, NULL);
|
||||
|
||||
logger = soup_logger_new (SOUP_LOGGER_LOG_BODY, -1);
|
||||
soup_session_add_feature (session, SOUP_SESSION_FEATURE (logger));
|
||||
g_object_unref (logger);
|
||||
|
||||
message = soup_message_new (SOUP_METHOD_GET, server_url);
|
||||
|
||||
g_print ("Connecting to server...\n");
|
||||
|
||||
/* Once connected, we will register */
|
||||
soup_session_websocket_connect_async (session, message, NULL, NULL, NULL,
|
||||
(GAsyncReadyCallback) on_server_connected, message);
|
||||
app_state = SERVER_CONNECTING;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
check_plugins (void)
|
||||
{
|
||||
int i;
|
||||
gboolean ret;
|
||||
GstRegistry *registry;
|
||||
const gchar *needed[] = { "opus", "nice", "webrtc", "dtls", "srtp",
|
||||
"rtpmanager", "audiotestsrc", NULL
|
||||
};
|
||||
|
||||
registry = gst_registry_get ();
|
||||
ret = TRUE;
|
||||
for (i = 0; i < g_strv_length ((gchar **) needed); i++) {
|
||||
GstPlugin *plugin;
|
||||
plugin = gst_registry_find_plugin (registry, needed[i]);
|
||||
if (!plugin) {
|
||||
g_print ("Required gstreamer plugin '%s' not found\n", needed[i]);
|
||||
ret = FALSE;
|
||||
continue;
|
||||
}
|
||||
gst_object_unref (plugin);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
GOptionContext *context;
|
||||
GError *error = NULL;
|
||||
|
||||
context = g_option_context_new ("- gstreamer webrtc sendrecv demo");
|
||||
g_option_context_add_main_entries (context, entries, NULL);
|
||||
g_option_context_add_group (context, gst_init_get_option_group ());
|
||||
if (!g_option_context_parse (context, &argc, &argv, &error)) {
|
||||
g_printerr ("Error initializing: %s\n", error->message);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!check_plugins ())
|
||||
return -1;
|
||||
|
||||
if (!room_id) {
|
||||
g_printerr ("--room-id is a required argument\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!local_id)
|
||||
local_id = g_strdup_printf ("%s-%i", g_get_user_name (),
|
||||
g_random_int_range (10, 10000));
|
||||
/* Sanitize by removing whitespace, modifies string in-place */
|
||||
g_strdelimit (local_id, " \t\n\r", '-');
|
||||
|
||||
g_print ("Our local id is %s\n", local_id);
|
||||
|
||||
if (!server_url)
|
||||
server_url = g_strdup (default_server_url);
|
||||
|
||||
/* Don't use strict ssl when running a localhost server, because
|
||||
* it's probably a test server with a self-signed certificate */
|
||||
{
|
||||
GstUri *uri = gst_uri_from_string (server_url);
|
||||
if (g_strcmp0 ("localhost", gst_uri_get_host (uri)) == 0 ||
|
||||
g_strcmp0 ("127.0.0.1", gst_uri_get_host (uri)) == 0)
|
||||
strict_ssl = FALSE;
|
||||
gst_uri_unref (uri);
|
||||
}
|
||||
|
||||
loop = g_main_loop_new (NULL, FALSE);
|
||||
|
||||
connect_to_websocket_server_async ();
|
||||
|
||||
g_main_loop_run (loop);
|
||||
|
||||
gst_element_set_state (GST_ELEMENT (pipeline), GST_STATE_NULL);
|
||||
g_print ("Pipeline stopped\n");
|
||||
|
||||
gst_object_unref (pipeline);
|
||||
g_free (server_url);
|
||||
g_free (local_id);
|
||||
g_free (room_id);
|
||||
|
||||
return 0;
|
||||
}
|
1
webrtc/multiparty-sendrecv/meson.build
Normal file
1
webrtc/multiparty-sendrecv/meson.build
Normal file
|
@ -0,0 +1 @@
|
|||
subdir('gst')
|
11
webrtc/sendonly/Makefile
Normal file
11
webrtc/sendonly/Makefile
Normal file
|
@ -0,0 +1,11 @@
|
|||
CC := gcc
|
||||
LIBS := $(shell pkg-config --libs --cflags gstreamer-webrtc-1.0 gstreamer-sdp-1.0 libsoup-2.4 json-glib-1.0)
|
||||
CFLAGS := -O0 -ggdb -Wall -fno-omit-frame-pointer
|
||||
|
||||
all: webrtc-unidirectional-h264 webrtc-recvonly-h264
|
||||
|
||||
webrtc-unidirectional-h264: webrtc-unidirectional-h264.c
|
||||
"$(CC)" $(CFLAGS) $^ $(LIBS) -o $@
|
||||
|
||||
webrtc-recvonly-h264: webrtc-recvonly-h264.c
|
||||
"$(CC)" $(CFLAGS) $^ $(LIBS) -o $@
|
705
webrtc/sendonly/webrtc-recvonly-h264.c
Normal file
705
webrtc/sendonly/webrtc-recvonly-h264.c
Normal file
|
@ -0,0 +1,705 @@
|
|||
#include <locale.h>
|
||||
#include <glib.h>
|
||||
#include <gst/gst.h>
|
||||
#include <gst/sdp/sdp.h>
|
||||
|
||||
#ifdef G_OS_UNIX
|
||||
#include <glib-unix.h>
|
||||
#endif
|
||||
|
||||
#define GST_USE_UNSTABLE_API
|
||||
#include <gst/webrtc/webrtc.h>
|
||||
|
||||
#include <libsoup/soup.h>
|
||||
#include <json-glib/json-glib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* This example is a standalone app which serves a web page
|
||||
* and configures webrtcbin to receive an H.264 video feed, and to
|
||||
* send+recv an Opus audio stream */
|
||||
|
||||
#define RTP_PAYLOAD_TYPE "96"
|
||||
#define RTP_CAPS_OPUS "application/x-rtp,media=audio,encoding-name=OPUS,payload="
|
||||
|
||||
#define SOUP_HTTP_PORT 57778
|
||||
#define STUN_SERVER "stun.l.google.com:19302"
|
||||
|
||||
|
||||
|
||||
typedef struct _ReceiverEntry ReceiverEntry;
|
||||
|
||||
ReceiverEntry *create_receiver_entry (SoupWebsocketConnection * connection);
|
||||
void destroy_receiver_entry (gpointer receiver_entry_ptr);
|
||||
|
||||
GstPadProbeReturn payloader_caps_event_probe_cb (GstPad * pad,
|
||||
GstPadProbeInfo * info, gpointer user_data);
|
||||
|
||||
void on_offer_created_cb (GstPromise * promise, gpointer user_data);
|
||||
void on_negotiation_needed_cb (GstElement * webrtcbin, gpointer user_data);
|
||||
void on_ice_candidate_cb (GstElement * webrtcbin, guint mline_index,
|
||||
gchar * candidate, gpointer user_data);
|
||||
|
||||
void soup_websocket_message_cb (SoupWebsocketConnection * connection,
|
||||
SoupWebsocketDataType data_type, GBytes * message, gpointer user_data);
|
||||
void soup_websocket_closed_cb (SoupWebsocketConnection * connection,
|
||||
gpointer user_data);
|
||||
|
||||
void soup_http_handler (SoupServer * soup_server, SoupMessage * message,
|
||||
const char *path, GHashTable * query, SoupClientContext * client_context,
|
||||
gpointer user_data);
|
||||
void soup_websocket_handler (G_GNUC_UNUSED SoupServer * server,
|
||||
SoupWebsocketConnection * connection, const char *path,
|
||||
SoupClientContext * client_context, gpointer user_data);
|
||||
|
||||
static gchar *get_string_from_json_object (JsonObject * object);
|
||||
|
||||
|
||||
|
||||
|
||||
struct _ReceiverEntry
|
||||
{
|
||||
SoupWebsocketConnection *connection;
|
||||
|
||||
GstElement *pipeline;
|
||||
GstElement *webrtcbin;
|
||||
};
|
||||
|
||||
|
||||
|
||||
const gchar *html_source = " \n \
|
||||
<html> \n \
|
||||
<head> \n \
|
||||
<script type=\"text/javascript\" src=\"https://webrtc.github.io/adapter/adapter-latest.js\"></script> \n \
|
||||
<script type=\"text/javascript\"> \n \
|
||||
var html5VideoElement; \n \
|
||||
var websocketConnection; \n \
|
||||
var webrtcPeerConnection; \n \
|
||||
var webrtcConfiguration; \n \
|
||||
var reportError; \n \
|
||||
\n \
|
||||
function getLocalStream() { \n \
|
||||
var constraints = {\"video\":true,\"audio\":true}; \n \
|
||||
if (navigator.mediaDevices.getUserMedia) { \n \
|
||||
return navigator.mediaDevices.getUserMedia(constraints); \n \
|
||||
} \n \
|
||||
} \n \
|
||||
\n \
|
||||
function onLocalDescription(desc) { \n \
|
||||
console.log(\"Local description: \" + JSON.stringify(desc)); \n \
|
||||
webrtcPeerConnection.setLocalDescription(desc).then(function() { \n \
|
||||
websocketConnection.send(JSON.stringify({ type: \"sdp\", \"data\": webrtcPeerConnection.localDescription })); \n \
|
||||
}).catch(reportError); \n \
|
||||
} \n \
|
||||
\n \
|
||||
\n \
|
||||
function onIncomingSDP(sdp) { \n \
|
||||
console.log(\"Incoming SDP: \" + JSON.stringify(sdp)); \n \
|
||||
webrtcPeerConnection.setRemoteDescription(sdp).catch(reportError); \n \
|
||||
/* Send our video/audio to the other peer */ \n \
|
||||
local_stream_promise = getLocalStream().then((stream) => { \n \
|
||||
console.log('Adding local stream'); \n \
|
||||
webrtcPeerConnection.addStream(stream); \n \
|
||||
webrtcPeerConnection.createAnswer().then(onLocalDescription).catch(reportError); \n \
|
||||
}); \n \
|
||||
} \n \
|
||||
\n \
|
||||
\n \
|
||||
function onIncomingICE(ice) { \n \
|
||||
var candidate = new RTCIceCandidate(ice); \n \
|
||||
console.log(\"Incoming ICE: \" + JSON.stringify(ice)); \n \
|
||||
webrtcPeerConnection.addIceCandidate(candidate).catch(reportError); \n \
|
||||
} \n \
|
||||
\n \
|
||||
\n \
|
||||
function onAddRemoteStream(event) { \n \
|
||||
html5VideoElement.srcObject = event.streams[0]; \n \
|
||||
} \n \
|
||||
\n \
|
||||
\n \
|
||||
function onIceCandidate(event) { \n \
|
||||
if (event.candidate == null) \n \
|
||||
return; \n \
|
||||
\n \
|
||||
console.log(\"Sending ICE candidate out: \" + JSON.stringify(event.candidate)); \n \
|
||||
websocketConnection.send(JSON.stringify({ \"type\": \"ice\", \"data\": event.candidate })); \n \
|
||||
} \n \
|
||||
\n \
|
||||
\n \
|
||||
function onServerMessage(event) { \n \
|
||||
var msg; \n \
|
||||
\n \
|
||||
try { \n \
|
||||
msg = JSON.parse(event.data); \n \
|
||||
} catch (e) { \n \
|
||||
return; \n \
|
||||
} \n \
|
||||
\n \
|
||||
if (!webrtcPeerConnection) { \n \
|
||||
webrtcPeerConnection = new RTCPeerConnection(webrtcConfiguration); \n \
|
||||
webrtcPeerConnection.ontrack = onAddRemoteStream; \n \
|
||||
webrtcPeerConnection.onicecandidate = onIceCandidate; \n \
|
||||
} \n \
|
||||
\n \
|
||||
switch (msg.type) { \n \
|
||||
case \"sdp\": onIncomingSDP(msg.data); break; \n \
|
||||
case \"ice\": onIncomingICE(msg.data); break; \n \
|
||||
default: break; \n \
|
||||
} \n \
|
||||
} \n \
|
||||
\n \
|
||||
\n \
|
||||
function playStream(videoElement, hostname, port, path, configuration, reportErrorCB) { \n \
|
||||
var l = window.location;\n \
|
||||
var wsHost = (hostname != undefined) ? hostname : l.hostname; \n \
|
||||
var wsPort = (port != undefined) ? port : l.port; \n \
|
||||
var wsPath = (path != undefined) ? path : \"ws\"; \n \
|
||||
if (wsPort) \n\
|
||||
wsPort = \":\" + wsPort; \n\
|
||||
var wsUrl = \"ws://\" + wsHost + wsPort + \"/\" + wsPath; \n \
|
||||
\n \
|
||||
html5VideoElement = videoElement; \n \
|
||||
webrtcConfiguration = configuration; \n \
|
||||
reportError = (reportErrorCB != undefined) ? reportErrorCB : function(text) {}; \n \
|
||||
\n \
|
||||
websocketConnection = new WebSocket(wsUrl); \n \
|
||||
websocketConnection.addEventListener(\"message\", onServerMessage); \n \
|
||||
} \n \
|
||||
\n \
|
||||
window.onload = function() { \n \
|
||||
var vidstream = document.getElementById(\"stream\"); \n \
|
||||
var config = { 'iceServers': [{ 'urls': 'stun:" STUN_SERVER "' }] }; \n\
|
||||
playStream(vidstream, null, null, null, config, function (errmsg) { console.error(errmsg); }); \n \
|
||||
}; \n \
|
||||
\n \
|
||||
</script> \n \
|
||||
</head> \n \
|
||||
\n \
|
||||
<body> \n \
|
||||
<div> \n \
|
||||
<video id=\"stream\" autoplay playsinline>Your browser does not support video</video> \n \
|
||||
</div> \n \
|
||||
</body> \n \
|
||||
</html> \n \
|
||||
";
|
||||
|
||||
static void
|
||||
handle_media_stream (GstPad * pad, GstElement * pipe, const char * convert_name,
|
||||
const char * sink_name)
|
||||
{
|
||||
GstPad *qpad;
|
||||
GstElement *q, *conv, *resample, *sink;
|
||||
GstPadLinkReturn ret;
|
||||
|
||||
g_print ("Trying to handle stream with %s ! %s", convert_name, sink_name);
|
||||
|
||||
q = gst_element_factory_make ("queue", NULL);
|
||||
g_assert_nonnull (q);
|
||||
conv = gst_element_factory_make (convert_name, NULL);
|
||||
g_assert_nonnull (conv);
|
||||
sink = gst_element_factory_make (sink_name, NULL);
|
||||
g_assert_nonnull (sink);
|
||||
|
||||
if (g_strcmp0 (convert_name, "audioconvert") == 0) {
|
||||
/* Might also need to resample, so add it just in case.
|
||||
* Will be a no-op if it's not required. */
|
||||
resample = gst_element_factory_make ("audioresample", NULL);
|
||||
g_assert_nonnull (resample);
|
||||
gst_bin_add_many (GST_BIN (pipe), q, conv, resample, sink, NULL);
|
||||
gst_element_sync_state_with_parent (q);
|
||||
gst_element_sync_state_with_parent (conv);
|
||||
gst_element_sync_state_with_parent (resample);
|
||||
gst_element_sync_state_with_parent (sink);
|
||||
gst_element_link_many (q, conv, resample, sink, NULL);
|
||||
} else {
|
||||
gst_bin_add_many (GST_BIN (pipe), q, conv, sink, NULL);
|
||||
gst_element_sync_state_with_parent (q);
|
||||
gst_element_sync_state_with_parent (conv);
|
||||
gst_element_sync_state_with_parent (sink);
|
||||
gst_element_link_many (q, conv, sink, NULL);
|
||||
}
|
||||
|
||||
qpad = gst_element_get_static_pad (q, "sink");
|
||||
|
||||
ret = gst_pad_link (pad, qpad);
|
||||
g_assert_cmphex (ret, ==, GST_PAD_LINK_OK);
|
||||
}
|
||||
|
||||
static void
|
||||
on_incoming_decodebin_stream (GstElement * decodebin, GstPad * pad,
|
||||
GstElement * pipe)
|
||||
{
|
||||
GstCaps *caps;
|
||||
const gchar *name;
|
||||
|
||||
if (!gst_pad_has_current_caps (pad)) {
|
||||
g_printerr ("Pad '%s' has no caps, can't do anything, ignoring\n",
|
||||
GST_PAD_NAME (pad));
|
||||
return;
|
||||
}
|
||||
|
||||
caps = gst_pad_get_current_caps (pad);
|
||||
name = gst_structure_get_name (gst_caps_get_structure (caps, 0));
|
||||
|
||||
if (g_str_has_prefix (name, "video")) {
|
||||
handle_media_stream (pad, pipe, "videoconvert", "autovideosink");
|
||||
} else if (g_str_has_prefix (name, "audio")) {
|
||||
handle_media_stream (pad, pipe, "audioconvert", "autoaudiosink");
|
||||
} else {
|
||||
g_printerr ("Unknown pad %s, ignoring", GST_PAD_NAME (pad));
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
on_incoming_stream (GstElement * webrtc, GstPad * pad, ReceiverEntry *receiver_entry)
|
||||
{
|
||||
GstElement *decodebin;
|
||||
GstPad *sinkpad;
|
||||
|
||||
if (GST_PAD_DIRECTION (pad) != GST_PAD_SRC)
|
||||
return;
|
||||
|
||||
decodebin = gst_element_factory_make ("decodebin", NULL);
|
||||
g_signal_connect (decodebin, "pad-added",
|
||||
G_CALLBACK (on_incoming_decodebin_stream), receiver_entry->pipeline);
|
||||
gst_bin_add (GST_BIN (receiver_entry->pipeline), decodebin);
|
||||
gst_element_sync_state_with_parent (decodebin);
|
||||
|
||||
sinkpad = gst_element_get_static_pad (decodebin, "sink");
|
||||
gst_pad_link (pad, sinkpad);
|
||||
gst_object_unref (sinkpad);
|
||||
}
|
||||
|
||||
|
||||
ReceiverEntry *
|
||||
create_receiver_entry (SoupWebsocketConnection * connection)
|
||||
{
|
||||
GError *error;
|
||||
ReceiverEntry *receiver_entry;
|
||||
GstCaps *video_caps;
|
||||
GstWebRTCRTPTransceiver *trans = NULL;
|
||||
|
||||
receiver_entry = g_slice_alloc0 (sizeof (ReceiverEntry));
|
||||
receiver_entry->connection = connection;
|
||||
|
||||
g_object_ref (G_OBJECT (connection));
|
||||
|
||||
g_signal_connect (G_OBJECT (connection), "message",
|
||||
G_CALLBACK (soup_websocket_message_cb), (gpointer) receiver_entry);
|
||||
|
||||
error = NULL;
|
||||
receiver_entry->pipeline = gst_parse_launch ("webrtcbin name=webrtcbin stun-server=stun://" STUN_SERVER " "
|
||||
"audiotestsrc is-live=true wave=red-noise ! audioconvert ! audioresample ! queue ! opusenc ! rtpopuspay ! "
|
||||
"queue ! " RTP_CAPS_OPUS "97 ! webrtcbin. "
|
||||
, &error);
|
||||
if (error != NULL) {
|
||||
g_error ("Could not create WebRTC pipeline: %s\n", error->message);
|
||||
g_error_free (error);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
receiver_entry->webrtcbin =
|
||||
gst_bin_get_by_name (GST_BIN (receiver_entry->pipeline), "webrtcbin");
|
||||
g_assert (receiver_entry->webrtcbin != NULL);
|
||||
|
||||
/* Incoming streams will be exposed via this signal */
|
||||
g_signal_connect (receiver_entry->webrtcbin, "pad-added", G_CALLBACK (on_incoming_stream),
|
||||
receiver_entry);
|
||||
|
||||
#if 0
|
||||
GstElement *rtpbin = gst_bin_get_by_name (GST_BIN (receiver_entry->webrtcbin), "rtpbin");
|
||||
g_object_set (rtpbin, "latency", 40, NULL);
|
||||
gst_object_unref (rtpbin);
|
||||
#endif
|
||||
|
||||
// Create a 2nd transceiver for the receive only video stream
|
||||
video_caps = gst_caps_from_string ("application/x-rtp,media=video,encoding-name=H264,payload=" RTP_PAYLOAD_TYPE ",clock-rate=90000,packetization-mode=(string)1, profile-level-id=(string)42c016");
|
||||
g_signal_emit_by_name (receiver_entry->webrtcbin, "add-transceiver", GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_RECVONLY, video_caps, NULL, &trans);
|
||||
gst_caps_unref (video_caps);
|
||||
gst_object_unref (trans);
|
||||
|
||||
g_signal_connect (receiver_entry->webrtcbin, "on-negotiation-needed",
|
||||
G_CALLBACK (on_negotiation_needed_cb), (gpointer) receiver_entry);
|
||||
|
||||
g_signal_connect (receiver_entry->webrtcbin, "on-ice-candidate",
|
||||
G_CALLBACK (on_ice_candidate_cb), (gpointer) receiver_entry);
|
||||
|
||||
gst_element_set_state (receiver_entry->pipeline, GST_STATE_PLAYING);
|
||||
|
||||
return receiver_entry;
|
||||
|
||||
cleanup:
|
||||
destroy_receiver_entry ((gpointer) receiver_entry);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void
|
||||
destroy_receiver_entry (gpointer receiver_entry_ptr)
|
||||
{
|
||||
ReceiverEntry *receiver_entry = (ReceiverEntry *) receiver_entry_ptr;
|
||||
|
||||
g_assert (receiver_entry != NULL);
|
||||
|
||||
if (receiver_entry->pipeline != NULL) {
|
||||
gst_element_set_state (GST_ELEMENT (receiver_entry->pipeline),
|
||||
GST_STATE_NULL);
|
||||
|
||||
gst_object_unref (GST_OBJECT (receiver_entry->webrtcbin));
|
||||
gst_object_unref (GST_OBJECT (receiver_entry->pipeline));
|
||||
}
|
||||
|
||||
if (receiver_entry->connection != NULL)
|
||||
g_object_unref (G_OBJECT (receiver_entry->connection));
|
||||
|
||||
g_slice_free1 (sizeof (ReceiverEntry), receiver_entry);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
on_offer_created_cb (GstPromise * promise, gpointer user_data)
|
||||
{
|
||||
gchar *sdp_string;
|
||||
gchar *json_string;
|
||||
JsonObject *sdp_json;
|
||||
JsonObject *sdp_data_json;
|
||||
GstStructure const *reply;
|
||||
GstPromise *local_desc_promise;
|
||||
GstWebRTCSessionDescription *offer = NULL;
|
||||
ReceiverEntry *receiver_entry = (ReceiverEntry *) user_data;
|
||||
|
||||
reply = gst_promise_get_reply (promise);
|
||||
gst_structure_get (reply, "offer", GST_TYPE_WEBRTC_SESSION_DESCRIPTION,
|
||||
&offer, NULL);
|
||||
gst_promise_unref (promise);
|
||||
|
||||
local_desc_promise = gst_promise_new ();
|
||||
g_signal_emit_by_name (receiver_entry->webrtcbin, "set-local-description",
|
||||
offer, local_desc_promise);
|
||||
gst_promise_interrupt (local_desc_promise);
|
||||
gst_promise_unref (local_desc_promise);
|
||||
|
||||
sdp_string = gst_sdp_message_as_text (offer->sdp);
|
||||
g_print ("Negotiation offer created:\n%s\n", sdp_string);
|
||||
|
||||
sdp_json = json_object_new ();
|
||||
json_object_set_string_member (sdp_json, "type", "sdp");
|
||||
|
||||
sdp_data_json = json_object_new ();
|
||||
json_object_set_string_member (sdp_data_json, "type", "offer");
|
||||
json_object_set_string_member (sdp_data_json, "sdp", sdp_string);
|
||||
json_object_set_object_member (sdp_json, "data", sdp_data_json);
|
||||
|
||||
json_string = get_string_from_json_object (sdp_json);
|
||||
json_object_unref (sdp_json);
|
||||
|
||||
soup_websocket_connection_send_text (receiver_entry->connection, json_string);
|
||||
g_free (json_string);
|
||||
g_free (sdp_string);
|
||||
|
||||
gst_webrtc_session_description_free (offer);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
on_negotiation_needed_cb (GstElement * webrtcbin, gpointer user_data)
|
||||
{
|
||||
GstPromise *promise;
|
||||
ReceiverEntry *receiver_entry = (ReceiverEntry *) user_data;
|
||||
|
||||
g_print ("Creating negotiation offer\n");
|
||||
|
||||
promise = gst_promise_new_with_change_func (on_offer_created_cb,
|
||||
(gpointer) receiver_entry, NULL);
|
||||
g_signal_emit_by_name (G_OBJECT (webrtcbin), "create-offer", NULL, promise);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
on_ice_candidate_cb (G_GNUC_UNUSED GstElement * webrtcbin, guint mline_index,
|
||||
gchar * candidate, gpointer user_data)
|
||||
{
|
||||
JsonObject *ice_json;
|
||||
JsonObject *ice_data_json;
|
||||
gchar *json_string;
|
||||
ReceiverEntry *receiver_entry = (ReceiverEntry *) user_data;
|
||||
|
||||
ice_json = json_object_new ();
|
||||
json_object_set_string_member (ice_json, "type", "ice");
|
||||
|
||||
ice_data_json = json_object_new ();
|
||||
json_object_set_int_member (ice_data_json, "sdpMLineIndex", mline_index);
|
||||
json_object_set_string_member (ice_data_json, "candidate", candidate);
|
||||
json_object_set_object_member (ice_json, "data", ice_data_json);
|
||||
|
||||
json_string = get_string_from_json_object (ice_json);
|
||||
json_object_unref (ice_json);
|
||||
|
||||
soup_websocket_connection_send_text (receiver_entry->connection, json_string);
|
||||
g_free (json_string);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
soup_websocket_message_cb (G_GNUC_UNUSED SoupWebsocketConnection * connection,
|
||||
SoupWebsocketDataType data_type, GBytes * message, gpointer user_data)
|
||||
{
|
||||
gsize size;
|
||||
gchar *data;
|
||||
gchar *data_string;
|
||||
const gchar *type_string;
|
||||
JsonNode *root_json;
|
||||
JsonObject *root_json_object;
|
||||
JsonObject *data_json_object;
|
||||
JsonParser *json_parser = NULL;
|
||||
ReceiverEntry *receiver_entry = (ReceiverEntry *) user_data;
|
||||
|
||||
switch (data_type) {
|
||||
case SOUP_WEBSOCKET_DATA_BINARY:
|
||||
g_error ("Received unknown binary message, ignoring\n");
|
||||
g_bytes_unref (message);
|
||||
return;
|
||||
|
||||
case SOUP_WEBSOCKET_DATA_TEXT:
|
||||
data = g_bytes_unref_to_data (message, &size);
|
||||
/* Convert to NULL-terminated string */
|
||||
data_string = g_strndup (data, size);
|
||||
g_free (data);
|
||||
break;
|
||||
|
||||
default:
|
||||
g_assert_not_reached ();
|
||||
}
|
||||
|
||||
json_parser = json_parser_new ();
|
||||
if (!json_parser_load_from_data (json_parser, data_string, -1, NULL))
|
||||
goto unknown_message;
|
||||
|
||||
root_json = json_parser_get_root (json_parser);
|
||||
if (!JSON_NODE_HOLDS_OBJECT (root_json))
|
||||
goto unknown_message;
|
||||
|
||||
root_json_object = json_node_get_object (root_json);
|
||||
|
||||
if (!json_object_has_member (root_json_object, "type")) {
|
||||
g_error ("Received message without type field\n");
|
||||
goto cleanup;
|
||||
}
|
||||
type_string = json_object_get_string_member (root_json_object, "type");
|
||||
|
||||
if (!json_object_has_member (root_json_object, "data")) {
|
||||
g_error ("Received message without data field\n");
|
||||
goto cleanup;
|
||||
}
|
||||
data_json_object = json_object_get_object_member (root_json_object, "data");
|
||||
|
||||
if (g_strcmp0 (type_string, "sdp") == 0) {
|
||||
const gchar *sdp_type_string;
|
||||
const gchar *sdp_string;
|
||||
GstPromise *promise;
|
||||
GstSDPMessage *sdp;
|
||||
GstWebRTCSessionDescription *answer;
|
||||
int ret;
|
||||
|
||||
if (!json_object_has_member (data_json_object, "type")) {
|
||||
g_error ("Received SDP message without type field\n");
|
||||
goto cleanup;
|
||||
}
|
||||
sdp_type_string = json_object_get_string_member (data_json_object, "type");
|
||||
|
||||
if (g_strcmp0 (sdp_type_string, "answer") != 0) {
|
||||
g_error ("Expected SDP message type \"answer\", got \"%s\"\n",
|
||||
sdp_type_string);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (!json_object_has_member (data_json_object, "sdp")) {
|
||||
g_error ("Received SDP message without SDP string\n");
|
||||
goto cleanup;
|
||||
}
|
||||
sdp_string = json_object_get_string_member (data_json_object, "sdp");
|
||||
|
||||
g_print ("Received SDP:\n%s\n", sdp_string);
|
||||
|
||||
ret = gst_sdp_message_new (&sdp);
|
||||
g_assert_cmphex (ret, ==, GST_SDP_OK);
|
||||
|
||||
ret =
|
||||
gst_sdp_message_parse_buffer ((guint8 *) sdp_string,
|
||||
strlen (sdp_string), sdp);
|
||||
if (ret != GST_SDP_OK) {
|
||||
g_error ("Could not parse SDP string\n");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
answer = gst_webrtc_session_description_new (GST_WEBRTC_SDP_TYPE_ANSWER,
|
||||
sdp);
|
||||
g_assert_nonnull (answer);
|
||||
|
||||
promise = gst_promise_new ();
|
||||
g_signal_emit_by_name (receiver_entry->webrtcbin, "set-remote-description",
|
||||
answer, promise);
|
||||
gst_promise_interrupt (promise);
|
||||
gst_promise_unref (promise);
|
||||
gst_webrtc_session_description_free (answer);
|
||||
} else if (g_strcmp0 (type_string, "ice") == 0) {
|
||||
guint mline_index;
|
||||
const gchar *candidate_string;
|
||||
|
||||
if (!json_object_has_member (data_json_object, "sdpMLineIndex")) {
|
||||
g_error ("Received ICE message without mline index\n");
|
||||
goto cleanup;
|
||||
}
|
||||
mline_index =
|
||||
json_object_get_int_member (data_json_object, "sdpMLineIndex");
|
||||
|
||||
if (!json_object_has_member (data_json_object, "candidate")) {
|
||||
g_error ("Received ICE message without ICE candidate string\n");
|
||||
goto cleanup;
|
||||
}
|
||||
candidate_string = json_object_get_string_member (data_json_object,
|
||||
"candidate");
|
||||
|
||||
g_print ("Received ICE candidate with mline index %u; candidate: %s\n",
|
||||
mline_index, candidate_string);
|
||||
|
||||
g_signal_emit_by_name (receiver_entry->webrtcbin, "add-ice-candidate",
|
||||
mline_index, candidate_string);
|
||||
} else
|
||||
goto unknown_message;
|
||||
|
||||
cleanup:
|
||||
if (json_parser != NULL)
|
||||
g_object_unref (G_OBJECT (json_parser));
|
||||
g_free (data_string);
|
||||
return;
|
||||
|
||||
unknown_message:
|
||||
g_error ("Unknown message \"%s\", ignoring", data_string);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
soup_websocket_closed_cb (SoupWebsocketConnection * connection,
|
||||
gpointer user_data)
|
||||
{
|
||||
GHashTable *receiver_entry_table = (GHashTable *) user_data;
|
||||
g_hash_table_remove (receiver_entry_table, connection);
|
||||
g_print ("Closed websocket connection %p\n", (gpointer) connection);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
soup_http_handler (G_GNUC_UNUSED SoupServer * soup_server,
|
||||
SoupMessage * message, const char *path, G_GNUC_UNUSED GHashTable * query,
|
||||
G_GNUC_UNUSED SoupClientContext * client_context,
|
||||
G_GNUC_UNUSED gpointer user_data)
|
||||
{
|
||||
SoupBuffer *soup_buffer;
|
||||
|
||||
if ((g_strcmp0 (path, "/") != 0) && (g_strcmp0 (path, "/index.html") != 0)) {
|
||||
soup_message_set_status (message, SOUP_STATUS_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
soup_buffer =
|
||||
soup_buffer_new (SOUP_MEMORY_STATIC, html_source, strlen (html_source));
|
||||
|
||||
soup_message_headers_set_content_type (message->response_headers, "text/html",
|
||||
NULL);
|
||||
soup_message_body_append_buffer (message->response_body, soup_buffer);
|
||||
soup_buffer_free (soup_buffer);
|
||||
|
||||
soup_message_set_status (message, SOUP_STATUS_OK);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
soup_websocket_handler (G_GNUC_UNUSED SoupServer * server,
|
||||
SoupWebsocketConnection * connection, G_GNUC_UNUSED const char *path,
|
||||
G_GNUC_UNUSED SoupClientContext * client_context, gpointer user_data)
|
||||
{
|
||||
ReceiverEntry *receiver_entry;
|
||||
GHashTable *receiver_entry_table = (GHashTable *) user_data;
|
||||
|
||||
g_print ("Processing new websocket connection %p", (gpointer) connection);
|
||||
|
||||
g_signal_connect (G_OBJECT (connection), "closed",
|
||||
G_CALLBACK (soup_websocket_closed_cb), (gpointer) receiver_entry_table);
|
||||
|
||||
receiver_entry = create_receiver_entry (connection);
|
||||
g_hash_table_replace (receiver_entry_table, connection, receiver_entry);
|
||||
}
|
||||
|
||||
|
||||
static gchar *
|
||||
get_string_from_json_object (JsonObject * object)
|
||||
{
|
||||
JsonNode *root;
|
||||
JsonGenerator *generator;
|
||||
gchar *text;
|
||||
|
||||
/* Make it the root node */
|
||||
root = json_node_init_object (json_node_alloc (), object);
|
||||
generator = json_generator_new ();
|
||||
json_generator_set_root (generator, root);
|
||||
text = json_generator_to_data (generator, NULL);
|
||||
|
||||
/* Release everything */
|
||||
g_object_unref (generator);
|
||||
json_node_free (root);
|
||||
return text;
|
||||
}
|
||||
|
||||
#ifdef G_OS_UNIX
|
||||
gboolean
|
||||
exit_sighandler (gpointer user_data)
|
||||
{
|
||||
g_print ("Caught signal, stopping mainloop\n");
|
||||
GMainLoop *mainloop = (GMainLoop *) user_data;
|
||||
g_main_loop_quit (mainloop);
|
||||
return TRUE;
|
||||
}
|
||||
#endif
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
GMainLoop *mainloop;
|
||||
SoupServer *soup_server;
|
||||
GHashTable *receiver_entry_table;
|
||||
|
||||
setlocale (LC_ALL, "");
|
||||
gst_init (&argc, &argv);
|
||||
|
||||
receiver_entry_table =
|
||||
g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL,
|
||||
destroy_receiver_entry);
|
||||
|
||||
mainloop = g_main_loop_new (NULL, FALSE);
|
||||
g_assert (mainloop != NULL);
|
||||
|
||||
#ifdef G_OS_UNIX
|
||||
g_unix_signal_add (SIGINT, exit_sighandler, mainloop);
|
||||
g_unix_signal_add (SIGTERM, exit_sighandler, mainloop);
|
||||
#endif
|
||||
|
||||
soup_server =
|
||||
soup_server_new (SOUP_SERVER_SERVER_HEADER, "webrtc-soup-server", NULL);
|
||||
soup_server_add_handler (soup_server, "/", soup_http_handler, NULL, NULL);
|
||||
soup_server_add_websocket_handler (soup_server, "/ws", NULL, NULL,
|
||||
soup_websocket_handler, (gpointer) receiver_entry_table, NULL);
|
||||
soup_server_listen_all (soup_server, SOUP_HTTP_PORT,
|
||||
(SoupServerListenOptions) 0, NULL);
|
||||
|
||||
g_print ("WebRTC page link: http://127.0.0.1:%d/\n", (gint) SOUP_HTTP_PORT);
|
||||
|
||||
g_main_loop_run (mainloop);
|
||||
|
||||
g_object_unref (G_OBJECT (soup_server));
|
||||
g_hash_table_destroy (receiver_entry_table);
|
||||
g_main_loop_unref (mainloop);
|
||||
|
||||
gst_deinit ();
|
||||
|
||||
return 0;
|
||||
}
|
587
webrtc/sendonly/webrtc-unidirectional-h264.c
Normal file
587
webrtc/sendonly/webrtc-unidirectional-h264.c
Normal file
|
@ -0,0 +1,587 @@
|
|||
#include <locale.h>
|
||||
#include <glib.h>
|
||||
#include <gst/gst.h>
|
||||
#include <gst/sdp/sdp.h>
|
||||
|
||||
#ifdef G_OS_UNIX
|
||||
#include <glib-unix.h>
|
||||
#endif
|
||||
|
||||
#define GST_USE_UNSTABLE_API
|
||||
#include <gst/webrtc/webrtc.h>
|
||||
|
||||
#include <libsoup/soup.h>
|
||||
#include <json-glib/json-glib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define RTP_PAYLOAD_TYPE "96"
|
||||
#define SOUP_HTTP_PORT 57778
|
||||
#define STUN_SERVER "stun.l.google.com:19302"
|
||||
|
||||
typedef struct _ReceiverEntry ReceiverEntry;
|
||||
|
||||
ReceiverEntry *create_receiver_entry (SoupWebsocketConnection * connection);
|
||||
void destroy_receiver_entry (gpointer receiver_entry_ptr);
|
||||
|
||||
GstPadProbeReturn payloader_caps_event_probe_cb (GstPad * pad,
|
||||
GstPadProbeInfo * info, gpointer user_data);
|
||||
|
||||
void on_offer_created_cb (GstPromise * promise, gpointer user_data);
|
||||
void on_negotiation_needed_cb (GstElement * webrtcbin, gpointer user_data);
|
||||
void on_ice_candidate_cb (GstElement * webrtcbin, guint mline_index,
|
||||
gchar * candidate, gpointer user_data);
|
||||
|
||||
void soup_websocket_message_cb (SoupWebsocketConnection * connection,
|
||||
SoupWebsocketDataType data_type, GBytes * message, gpointer user_data);
|
||||
void soup_websocket_closed_cb (SoupWebsocketConnection * connection,
|
||||
gpointer user_data);
|
||||
|
||||
void soup_http_handler (SoupServer * soup_server, SoupMessage * message,
|
||||
const char *path, GHashTable * query, SoupClientContext * client_context,
|
||||
gpointer user_data);
|
||||
void soup_websocket_handler (G_GNUC_UNUSED SoupServer * server,
|
||||
SoupWebsocketConnection * connection, const char *path,
|
||||
SoupClientContext * client_context, gpointer user_data);
|
||||
|
||||
static gchar *get_string_from_json_object (JsonObject * object);
|
||||
|
||||
struct _ReceiverEntry
|
||||
{
|
||||
SoupWebsocketConnection *connection;
|
||||
|
||||
GstElement *pipeline;
|
||||
GstElement *webrtcbin;
|
||||
};
|
||||
|
||||
const gchar *html_source = " \n \
|
||||
<html> \n \
|
||||
<head> \n \
|
||||
<script type=\"text/javascript\" src=\"https://webrtc.github.io/adapter/adapter-latest.js\"></script> \n \
|
||||
<script type=\"text/javascript\"> \n \
|
||||
var html5VideoElement; \n \
|
||||
var websocketConnection; \n \
|
||||
var webrtcPeerConnection; \n \
|
||||
var webrtcConfiguration; \n \
|
||||
var reportError; \n \
|
||||
\n \
|
||||
\n \
|
||||
function onLocalDescription(desc) { \n \
|
||||
console.log(\"Local description: \" + JSON.stringify(desc)); \n \
|
||||
webrtcPeerConnection.setLocalDescription(desc).then(function() { \n \
|
||||
websocketConnection.send(JSON.stringify({ type: \"sdp\", \"data\": webrtcPeerConnection.localDescription })); \n \
|
||||
}).catch(reportError); \n \
|
||||
} \n \
|
||||
\n \
|
||||
\n \
|
||||
function onIncomingSDP(sdp) { \n \
|
||||
console.log(\"Incoming SDP: \" + JSON.stringify(sdp)); \n \
|
||||
webrtcPeerConnection.setRemoteDescription(sdp).catch(reportError); \n \
|
||||
webrtcPeerConnection.createAnswer().then(onLocalDescription).catch(reportError); \n \
|
||||
} \n \
|
||||
\n \
|
||||
\n \
|
||||
function onIncomingICE(ice) { \n \
|
||||
var candidate = new RTCIceCandidate(ice); \n \
|
||||
console.log(\"Incoming ICE: \" + JSON.stringify(ice)); \n \
|
||||
webrtcPeerConnection.addIceCandidate(candidate).catch(reportError); \n \
|
||||
} \n \
|
||||
\n \
|
||||
\n \
|
||||
function onAddRemoteStream(event) { \n \
|
||||
html5VideoElement.srcObject = event.streams[0]; \n \
|
||||
} \n \
|
||||
\n \
|
||||
\n \
|
||||
function onIceCandidate(event) { \n \
|
||||
if (event.candidate == null) \n \
|
||||
return; \n \
|
||||
\n \
|
||||
console.log(\"Sending ICE candidate out: \" + JSON.stringify(event.candidate)); \n \
|
||||
websocketConnection.send(JSON.stringify({ \"type\": \"ice\", \"data\": event.candidate })); \n \
|
||||
} \n \
|
||||
\n \
|
||||
\n \
|
||||
function onServerMessage(event) { \n \
|
||||
var msg; \n \
|
||||
\n \
|
||||
try { \n \
|
||||
msg = JSON.parse(event.data); \n \
|
||||
} catch (e) { \n \
|
||||
return; \n \
|
||||
} \n \
|
||||
\n \
|
||||
if (!webrtcPeerConnection) { \n \
|
||||
webrtcPeerConnection = new RTCPeerConnection(webrtcConfiguration); \n \
|
||||
webrtcPeerConnection.ontrack = onAddRemoteStream; \n \
|
||||
webrtcPeerConnection.onicecandidate = onIceCandidate; \n \
|
||||
} \n \
|
||||
\n \
|
||||
switch (msg.type) { \n \
|
||||
case \"sdp\": onIncomingSDP(msg.data); break; \n \
|
||||
case \"ice\": onIncomingICE(msg.data); break; \n \
|
||||
default: break; \n \
|
||||
} \n \
|
||||
} \n \
|
||||
\n \
|
||||
\n \
|
||||
function playStream(videoElement, hostname, port, path, configuration, reportErrorCB) { \n \
|
||||
var l = window.location;\n \
|
||||
var wsHost = (hostname != undefined) ? hostname : l.hostname; \n \
|
||||
var wsPort = (port != undefined) ? port : l.port; \n \
|
||||
var wsPath = (path != undefined) ? path : \"ws\"; \n \
|
||||
if (wsPort) \n\
|
||||
wsPort = \":\" + wsPort; \n\
|
||||
var wsUrl = \"ws://\" + wsHost + wsPort + \"/\" + wsPath; \n \
|
||||
\n \
|
||||
html5VideoElement = videoElement; \n \
|
||||
webrtcConfiguration = configuration; \n \
|
||||
reportError = (reportErrorCB != undefined) ? reportErrorCB : function(text) {}; \n \
|
||||
\n \
|
||||
websocketConnection = new WebSocket(wsUrl); \n \
|
||||
websocketConnection.addEventListener(\"message\", onServerMessage); \n \
|
||||
} \n \
|
||||
\n \
|
||||
window.onload = function() { \n \
|
||||
var vidstream = document.getElementById(\"stream\"); \n \
|
||||
var config = { 'iceServers': [{ 'urls': 'stun:" STUN_SERVER "' }] }; \n\
|
||||
playStream(vidstream, null, null, null, config, function (errmsg) { console.error(errmsg); }); \n \
|
||||
}; \n \
|
||||
\n \
|
||||
</script> \n \
|
||||
</head> \n \
|
||||
\n \
|
||||
<body> \n \
|
||||
<div> \n \
|
||||
<video id=\"stream\" autoplay playsinline>Your browser does not support video</video> \n \
|
||||
</div> \n \
|
||||
</body> \n \
|
||||
</html> \n \
|
||||
";
|
||||
|
||||
ReceiverEntry *
|
||||
create_receiver_entry (SoupWebsocketConnection * connection)
|
||||
{
|
||||
GError *error;
|
||||
ReceiverEntry *receiver_entry;
|
||||
GstWebRTCRTPTransceiver *trans;
|
||||
GArray *transceivers;
|
||||
|
||||
receiver_entry = g_slice_alloc0 (sizeof (ReceiverEntry));
|
||||
receiver_entry->connection = connection;
|
||||
|
||||
g_object_ref (G_OBJECT (connection));
|
||||
|
||||
g_signal_connect (G_OBJECT (connection), "message",
|
||||
G_CALLBACK (soup_websocket_message_cb), (gpointer) receiver_entry);
|
||||
|
||||
error = NULL;
|
||||
receiver_entry->pipeline =
|
||||
gst_parse_launch ("webrtcbin name=webrtcbin stun-server=stun://"
|
||||
STUN_SERVER " "
|
||||
"v4l2src ! videorate ! video/x-raw,width=640,height=360,framerate=15/1 ! videoconvert ! queue max-size-buffers=1 ! x264enc bitrate=600 speed-preset=ultrafast tune=zerolatency key-int-max=15 ! video/x-h264,profile=constrained-baseline ! queue max-size-time=100000000 ! h264parse ! "
|
||||
"rtph264pay config-interval=-1 name=payloader ! "
|
||||
"application/x-rtp,media=video,encoding-name=H264,payload="
|
||||
RTP_PAYLOAD_TYPE " ! webrtcbin. ", &error);
|
||||
if (error != NULL) {
|
||||
g_error ("Could not create WebRTC pipeline: %s\n", error->message);
|
||||
g_error_free (error);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
receiver_entry->webrtcbin =
|
||||
gst_bin_get_by_name (GST_BIN (receiver_entry->pipeline), "webrtcbin");
|
||||
g_assert (receiver_entry->webrtcbin != NULL);
|
||||
|
||||
g_signal_emit_by_name (receiver_entry->webrtcbin, "get-transceivers",
|
||||
&transceivers);
|
||||
g_assert (transceivers != NULL && transceivers->len > 0);
|
||||
trans = g_array_index (transceivers, GstWebRTCRTPTransceiver *, 0);
|
||||
trans->direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY;
|
||||
g_array_unref (transceivers);
|
||||
|
||||
g_signal_connect (receiver_entry->webrtcbin, "on-negotiation-needed",
|
||||
G_CALLBACK (on_negotiation_needed_cb), (gpointer) receiver_entry);
|
||||
|
||||
g_signal_connect (receiver_entry->webrtcbin, "on-ice-candidate",
|
||||
G_CALLBACK (on_ice_candidate_cb), (gpointer) receiver_entry);
|
||||
|
||||
gst_element_set_state (receiver_entry->pipeline, GST_STATE_PLAYING);
|
||||
|
||||
return receiver_entry;
|
||||
|
||||
cleanup:
|
||||
destroy_receiver_entry ((gpointer) receiver_entry);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void
|
||||
destroy_receiver_entry (gpointer receiver_entry_ptr)
|
||||
{
|
||||
ReceiverEntry *receiver_entry = (ReceiverEntry *) receiver_entry_ptr;
|
||||
|
||||
g_assert (receiver_entry != NULL);
|
||||
|
||||
if (receiver_entry->pipeline != NULL) {
|
||||
gst_element_set_state (GST_ELEMENT (receiver_entry->pipeline),
|
||||
GST_STATE_NULL);
|
||||
|
||||
gst_object_unref (GST_OBJECT (receiver_entry->webrtcbin));
|
||||
gst_object_unref (GST_OBJECT (receiver_entry->pipeline));
|
||||
}
|
||||
|
||||
if (receiver_entry->connection != NULL)
|
||||
g_object_unref (G_OBJECT (receiver_entry->connection));
|
||||
|
||||
g_slice_free1 (sizeof (ReceiverEntry), receiver_entry);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
on_offer_created_cb (GstPromise * promise, gpointer user_data)
|
||||
{
|
||||
gchar *sdp_string;
|
||||
gchar *json_string;
|
||||
JsonObject *sdp_json;
|
||||
JsonObject *sdp_data_json;
|
||||
GstStructure const *reply;
|
||||
GstPromise *local_desc_promise;
|
||||
GstWebRTCSessionDescription *offer = NULL;
|
||||
ReceiverEntry *receiver_entry = (ReceiverEntry *) user_data;
|
||||
|
||||
reply = gst_promise_get_reply (promise);
|
||||
gst_structure_get (reply, "offer", GST_TYPE_WEBRTC_SESSION_DESCRIPTION,
|
||||
&offer, NULL);
|
||||
gst_promise_unref (promise);
|
||||
|
||||
local_desc_promise = gst_promise_new ();
|
||||
g_signal_emit_by_name (receiver_entry->webrtcbin, "set-local-description",
|
||||
offer, local_desc_promise);
|
||||
gst_promise_interrupt (local_desc_promise);
|
||||
gst_promise_unref (local_desc_promise);
|
||||
|
||||
sdp_string = gst_sdp_message_as_text (offer->sdp);
|
||||
g_print ("Negotiation offer created:\n%s\n", sdp_string);
|
||||
|
||||
sdp_json = json_object_new ();
|
||||
json_object_set_string_member (sdp_json, "type", "sdp");
|
||||
|
||||
sdp_data_json = json_object_new ();
|
||||
json_object_set_string_member (sdp_data_json, "type", "offer");
|
||||
json_object_set_string_member (sdp_data_json, "sdp", sdp_string);
|
||||
json_object_set_object_member (sdp_json, "data", sdp_data_json);
|
||||
|
||||
json_string = get_string_from_json_object (sdp_json);
|
||||
json_object_unref (sdp_json);
|
||||
|
||||
soup_websocket_connection_send_text (receiver_entry->connection, json_string);
|
||||
g_free (json_string);
|
||||
g_free (sdp_string);
|
||||
|
||||
gst_webrtc_session_description_free (offer);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
on_negotiation_needed_cb (GstElement * webrtcbin, gpointer user_data)
|
||||
{
|
||||
GstPromise *promise;
|
||||
ReceiverEntry *receiver_entry = (ReceiverEntry *) user_data;
|
||||
|
||||
g_print ("Creating negotiation offer\n");
|
||||
|
||||
promise = gst_promise_new_with_change_func (on_offer_created_cb,
|
||||
(gpointer) receiver_entry, NULL);
|
||||
g_signal_emit_by_name (G_OBJECT (webrtcbin), "create-offer", NULL, promise);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
on_ice_candidate_cb (G_GNUC_UNUSED GstElement * webrtcbin, guint mline_index,
|
||||
gchar * candidate, gpointer user_data)
|
||||
{
|
||||
JsonObject *ice_json;
|
||||
JsonObject *ice_data_json;
|
||||
gchar *json_string;
|
||||
ReceiverEntry *receiver_entry = (ReceiverEntry *) user_data;
|
||||
|
||||
ice_json = json_object_new ();
|
||||
json_object_set_string_member (ice_json, "type", "ice");
|
||||
|
||||
ice_data_json = json_object_new ();
|
||||
json_object_set_int_member (ice_data_json, "sdpMLineIndex", mline_index);
|
||||
json_object_set_string_member (ice_data_json, "candidate", candidate);
|
||||
json_object_set_object_member (ice_json, "data", ice_data_json);
|
||||
|
||||
json_string = get_string_from_json_object (ice_json);
|
||||
json_object_unref (ice_json);
|
||||
|
||||
soup_websocket_connection_send_text (receiver_entry->connection, json_string);
|
||||
g_free (json_string);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
soup_websocket_message_cb (G_GNUC_UNUSED SoupWebsocketConnection * connection,
|
||||
SoupWebsocketDataType data_type, GBytes * message, gpointer user_data)
|
||||
{
|
||||
gsize size;
|
||||
gchar *data;
|
||||
gchar *data_string;
|
||||
const gchar *type_string;
|
||||
JsonNode *root_json;
|
||||
JsonObject *root_json_object;
|
||||
JsonObject *data_json_object;
|
||||
JsonParser *json_parser = NULL;
|
||||
ReceiverEntry *receiver_entry = (ReceiverEntry *) user_data;
|
||||
|
||||
switch (data_type) {
|
||||
case SOUP_WEBSOCKET_DATA_BINARY:
|
||||
g_error ("Received unknown binary message, ignoring\n");
|
||||
g_bytes_unref (message);
|
||||
return;
|
||||
|
||||
case SOUP_WEBSOCKET_DATA_TEXT:
|
||||
data = g_bytes_unref_to_data (message, &size);
|
||||
/* Convert to NULL-terminated string */
|
||||
data_string = g_strndup (data, size);
|
||||
g_free (data);
|
||||
break;
|
||||
|
||||
default:
|
||||
g_assert_not_reached ();
|
||||
}
|
||||
|
||||
json_parser = json_parser_new ();
|
||||
if (!json_parser_load_from_data (json_parser, data_string, -1, NULL))
|
||||
goto unknown_message;
|
||||
|
||||
root_json = json_parser_get_root (json_parser);
|
||||
if (!JSON_NODE_HOLDS_OBJECT (root_json))
|
||||
goto unknown_message;
|
||||
|
||||
root_json_object = json_node_get_object (root_json);
|
||||
|
||||
if (!json_object_has_member (root_json_object, "type")) {
|
||||
g_error ("Received message without type field\n");
|
||||
goto cleanup;
|
||||
}
|
||||
type_string = json_object_get_string_member (root_json_object, "type");
|
||||
|
||||
if (!json_object_has_member (root_json_object, "data")) {
|
||||
g_error ("Received message without data field\n");
|
||||
goto cleanup;
|
||||
}
|
||||
data_json_object = json_object_get_object_member (root_json_object, "data");
|
||||
|
||||
if (g_strcmp0 (type_string, "sdp") == 0) {
|
||||
const gchar *sdp_type_string;
|
||||
const gchar *sdp_string;
|
||||
GstPromise *promise;
|
||||
GstSDPMessage *sdp;
|
||||
GstWebRTCSessionDescription *answer;
|
||||
int ret;
|
||||
|
||||
if (!json_object_has_member (data_json_object, "type")) {
|
||||
g_error ("Received SDP message without type field\n");
|
||||
goto cleanup;
|
||||
}
|
||||
sdp_type_string = json_object_get_string_member (data_json_object, "type");
|
||||
|
||||
if (g_strcmp0 (sdp_type_string, "answer") != 0) {
|
||||
g_error ("Expected SDP message type \"answer\", got \"%s\"\n",
|
||||
sdp_type_string);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (!json_object_has_member (data_json_object, "sdp")) {
|
||||
g_error ("Received SDP message without SDP string\n");
|
||||
goto cleanup;
|
||||
}
|
||||
sdp_string = json_object_get_string_member (data_json_object, "sdp");
|
||||
|
||||
g_print ("Received SDP:\n%s\n", sdp_string);
|
||||
|
||||
ret = gst_sdp_message_new (&sdp);
|
||||
g_assert_cmphex (ret, ==, GST_SDP_OK);
|
||||
|
||||
ret =
|
||||
gst_sdp_message_parse_buffer ((guint8 *) sdp_string,
|
||||
strlen (sdp_string), sdp);
|
||||
if (ret != GST_SDP_OK) {
|
||||
g_error ("Could not parse SDP string\n");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
answer = gst_webrtc_session_description_new (GST_WEBRTC_SDP_TYPE_ANSWER,
|
||||
sdp);
|
||||
g_assert_nonnull (answer);
|
||||
|
||||
promise = gst_promise_new ();
|
||||
g_signal_emit_by_name (receiver_entry->webrtcbin, "set-remote-description",
|
||||
answer, promise);
|
||||
gst_promise_interrupt (promise);
|
||||
gst_promise_unref (promise);
|
||||
gst_webrtc_session_description_free (answer);
|
||||
} else if (g_strcmp0 (type_string, "ice") == 0) {
|
||||
guint mline_index;
|
||||
const gchar *candidate_string;
|
||||
|
||||
if (!json_object_has_member (data_json_object, "sdpMLineIndex")) {
|
||||
g_error ("Received ICE message without mline index\n");
|
||||
goto cleanup;
|
||||
}
|
||||
mline_index =
|
||||
json_object_get_int_member (data_json_object, "sdpMLineIndex");
|
||||
|
||||
if (!json_object_has_member (data_json_object, "candidate")) {
|
||||
g_error ("Received ICE message without ICE candidate string\n");
|
||||
goto cleanup;
|
||||
}
|
||||
candidate_string = json_object_get_string_member (data_json_object,
|
||||
"candidate");
|
||||
|
||||
g_print ("Received ICE candidate with mline index %u; candidate: %s\n",
|
||||
mline_index, candidate_string);
|
||||
|
||||
g_signal_emit_by_name (receiver_entry->webrtcbin, "add-ice-candidate",
|
||||
mline_index, candidate_string);
|
||||
} else
|
||||
goto unknown_message;
|
||||
|
||||
cleanup:
|
||||
if (json_parser != NULL)
|
||||
g_object_unref (G_OBJECT (json_parser));
|
||||
g_free (data_string);
|
||||
return;
|
||||
|
||||
unknown_message:
|
||||
g_error ("Unknown message \"%s\", ignoring", data_string);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
soup_websocket_closed_cb (SoupWebsocketConnection * connection,
|
||||
gpointer user_data)
|
||||
{
|
||||
GHashTable *receiver_entry_table = (GHashTable *) user_data;
|
||||
g_hash_table_remove (receiver_entry_table, connection);
|
||||
g_print ("Closed websocket connection %p\n", (gpointer) connection);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
soup_http_handler (G_GNUC_UNUSED SoupServer * soup_server,
|
||||
SoupMessage * message, const char *path, G_GNUC_UNUSED GHashTable * query,
|
||||
G_GNUC_UNUSED SoupClientContext * client_context,
|
||||
G_GNUC_UNUSED gpointer user_data)
|
||||
{
|
||||
SoupBuffer *soup_buffer;
|
||||
|
||||
if ((g_strcmp0 (path, "/") != 0) && (g_strcmp0 (path, "/index.html") != 0)) {
|
||||
soup_message_set_status (message, SOUP_STATUS_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
soup_buffer =
|
||||
soup_buffer_new (SOUP_MEMORY_STATIC, html_source, strlen (html_source));
|
||||
|
||||
soup_message_headers_set_content_type (message->response_headers, "text/html",
|
||||
NULL);
|
||||
soup_message_body_append_buffer (message->response_body, soup_buffer);
|
||||
soup_buffer_free (soup_buffer);
|
||||
|
||||
soup_message_set_status (message, SOUP_STATUS_OK);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
soup_websocket_handler (G_GNUC_UNUSED SoupServer * server,
|
||||
SoupWebsocketConnection * connection, G_GNUC_UNUSED const char *path,
|
||||
G_GNUC_UNUSED SoupClientContext * client_context, gpointer user_data)
|
||||
{
|
||||
ReceiverEntry *receiver_entry;
|
||||
GHashTable *receiver_entry_table = (GHashTable *) user_data;
|
||||
|
||||
g_print ("Processing new websocket connection %p", (gpointer) connection);
|
||||
|
||||
g_signal_connect (G_OBJECT (connection), "closed",
|
||||
G_CALLBACK (soup_websocket_closed_cb), (gpointer) receiver_entry_table);
|
||||
|
||||
receiver_entry = create_receiver_entry (connection);
|
||||
g_hash_table_replace (receiver_entry_table, connection, receiver_entry);
|
||||
}
|
||||
|
||||
|
||||
static gchar *
|
||||
get_string_from_json_object (JsonObject * object)
|
||||
{
|
||||
JsonNode *root;
|
||||
JsonGenerator *generator;
|
||||
gchar *text;
|
||||
|
||||
/* Make it the root node */
|
||||
root = json_node_init_object (json_node_alloc (), object);
|
||||
generator = json_generator_new ();
|
||||
json_generator_set_root (generator, root);
|
||||
text = json_generator_to_data (generator, NULL);
|
||||
|
||||
/* Release everything */
|
||||
g_object_unref (generator);
|
||||
json_node_free (root);
|
||||
return text;
|
||||
}
|
||||
|
||||
#ifdef G_OS_UNIX
|
||||
gboolean
|
||||
exit_sighandler (gpointer user_data)
|
||||
{
|
||||
g_print ("Caught signal, stopping mainloop\n");
|
||||
GMainLoop *mainloop = (GMainLoop *) user_data;
|
||||
g_main_loop_quit (mainloop);
|
||||
return TRUE;
|
||||
}
|
||||
#endif
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
GMainLoop *mainloop;
|
||||
SoupServer *soup_server;
|
||||
GHashTable *receiver_entry_table;
|
||||
|
||||
setlocale (LC_ALL, "");
|
||||
gst_init (&argc, &argv);
|
||||
|
||||
receiver_entry_table =
|
||||
g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL,
|
||||
destroy_receiver_entry);
|
||||
|
||||
mainloop = g_main_loop_new (NULL, FALSE);
|
||||
g_assert (mainloop != NULL);
|
||||
|
||||
#ifdef G_OS_UNIX
|
||||
g_unix_signal_add (SIGINT, exit_sighandler, mainloop);
|
||||
g_unix_signal_add (SIGTERM, exit_sighandler, mainloop);
|
||||
#endif
|
||||
|
||||
soup_server =
|
||||
soup_server_new (SOUP_SERVER_SERVER_HEADER, "webrtc-soup-server", NULL);
|
||||
soup_server_add_handler (soup_server, "/", soup_http_handler, NULL, NULL);
|
||||
soup_server_add_websocket_handler (soup_server, "/ws", NULL, NULL,
|
||||
soup_websocket_handler, (gpointer) receiver_entry_table, NULL);
|
||||
soup_server_listen_all (soup_server, SOUP_HTTP_PORT,
|
||||
(SoupServerListenOptions) 0, NULL);
|
||||
|
||||
g_print ("WebRTC page link: http://127.0.0.1:%d/\n", (gint) SOUP_HTTP_PORT);
|
||||
|
||||
g_main_loop_run (mainloop);
|
||||
|
||||
g_object_unref (G_OBJECT (soup_server));
|
||||
g_hash_table_destroy (receiver_entry_table);
|
||||
g_main_loop_unref (mainloop);
|
||||
|
||||
gst_deinit ();
|
||||
|
||||
return 0;
|
||||
}
|
36
webrtc/sendrecv/gst-java/Dockerfile
Normal file
36
webrtc/sendrecv/gst-java/Dockerfile
Normal file
|
@ -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
|
||||
|
||||
|
||||
|
35
webrtc/sendrecv/gst-java/build.gradle
Normal file
35
webrtc/sendrecv/gst-java/build.gradle
Normal file
|
@ -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) }
|
||||
}
|
||||
}
|
BIN
webrtc/sendrecv/gst-java/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
webrtc/sendrecv/gst-java/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
webrtc/sendrecv/gst-java/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
webrtc/sendrecv/gst-java/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -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
|
172
webrtc/sendrecv/gst-java/gradlew
vendored
Executable file
172
webrtc/sendrecv/gst-java/gradlew
vendored
Executable file
|
@ -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" "$@"
|
84
webrtc/sendrecv/gst-java/gradlew.bat
vendored
Normal file
84
webrtc/sendrecv/gst-java/gradlew.bat
vendored
Normal file
|
@ -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
|
266
webrtc/sendrecv/gst-java/src/main/java/WebrtcSendRecv.java
Normal file
266
webrtc/sendrecv/gst-java/src/main/java/WebrtcSendRecv.java
Normal file
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
1
webrtc/sendrecv/gst-rust/.dockerignore
Normal file
1
webrtc/sendrecv/gst-rust/.dockerignore
Normal file
|
@ -0,0 +1 @@
|
|||
/target/
|
6
webrtc/sendrecv/gst-rust/.gitignore
vendored
Normal file
6
webrtc/sendrecv/gst-rust/.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
1342
webrtc/sendrecv/gst-rust/Cargo.lock
generated
Normal file
1342
webrtc/sendrecv/gst-rust/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
19
webrtc/sendrecv/gst-rust/Cargo.toml
Normal file
19
webrtc/sendrecv/gst-rust/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "webrtc-app"
|
||||
version = "0.1.0"
|
||||
authors = ["Sebastian Dröge <sebastian@centricular.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3"
|
||||
async-std = "1"
|
||||
structopt = { version = "0.3", default-features = false }
|
||||
anyhow = "1"
|
||||
rand = "0.7"
|
||||
async-tungstenite = { version = "0.5", features = ["async-std-runtime", "async-native-tls"] }
|
||||
gst = { package = "gstreamer", version = "0.15", features = ["v1_14"] }
|
||||
gst-webrtc = { package = "gstreamer-webrtc", version = "0.15" }
|
||||
gst-sdp = { package = "gstreamer-sdp", version = "0.15", features = ["v1_14"] }
|
||||
serde = "1"
|
||||
serde_derive = "1"
|
||||
serde_json = "1"
|
13
webrtc/sendrecv/gst-rust/Dockerfile
Normal file
13
webrtc/sendrecv/gst-rust/Dockerfile
Normal file
|
@ -0,0 +1,13 @@
|
|||
FROM maxmcd/gstreamer:1.14-buster
|
||||
|
||||
RUN apt-get install -y curl
|
||||
RUN wget -O rustup.sh https://sh.rustup.rs && sh ./rustup.sh -y
|
||||
ENV PATH=$PATH:/root/.cargo/bin/
|
||||
|
||||
WORKDIR /opt/
|
||||
COPY . /opt/
|
||||
RUN cargo build
|
||||
|
||||
CMD echo "Waiting a few seconds for you to open the browser at localhost:8080" \
|
||||
&& sleep 10 \
|
||||
&& /opt/target/debug/gst-rust --peer-id=1 --server=ws://signalling:8443
|
67
webrtc/sendrecv/gst-rust/src/macos_workaround.rs
Normal file
67
webrtc/sendrecv/gst-rust/src/macos_workaround.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
/// macOS has a specific requirement that there must be a run loop running
|
||||
/// on the main thread in order to open windows and use OpenGL.
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod runloop {
|
||||
use std::os::raw::c_void;
|
||||
#[repr(C)]
|
||||
pub struct CFRunLoop(*mut c_void);
|
||||
|
||||
#[link(name = "foundation", kind = "framework")]
|
||||
extern "C" {
|
||||
fn CFRunLoopRun();
|
||||
fn CFRunLoopGetMain() -> *mut c_void;
|
||||
fn CFRunLoopStop(l: *mut c_void);
|
||||
}
|
||||
|
||||
impl CFRunLoop {
|
||||
pub fn run() {
|
||||
unsafe {
|
||||
CFRunLoopRun();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_main() -> CFRunLoop {
|
||||
unsafe {
|
||||
let r = CFRunLoopGetMain();
|
||||
assert!(!r.is_null());
|
||||
CFRunLoop(r)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
unsafe { CFRunLoopStop(self.0) }
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for CFRunLoop {}
|
||||
}
|
||||
|
||||
/// On macOS this launches the callback function on a thread.
|
||||
/// On other platforms it's just executed immediately.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn run<T, F: FnOnce() -> T + Send + 'static>(main: F) -> T
|
||||
where
|
||||
T: Send + 'static,
|
||||
{
|
||||
main()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn run<T, F: FnOnce() -> T + Send + 'static>(main: F) -> T
|
||||
where
|
||||
T: Send + 'static,
|
||||
{
|
||||
use std::thread;
|
||||
|
||||
let l = runloop::CFRunLoop::get_main();
|
||||
let t = thread::spawn(move || {
|
||||
let res = main();
|
||||
l.stop();
|
||||
res
|
||||
});
|
||||
|
||||
runloop::CFRunLoop::run();
|
||||
|
||||
t.join().unwrap()
|
||||
}
|
684
webrtc/sendrecv/gst-rust/src/main.rs
Normal file
684
webrtc/sendrecv/gst-rust/src/main.rs
Normal file
|
@ -0,0 +1,684 @@
|
|||
mod macos_workaround;
|
||||
|
||||
use std::sync::{Arc, Mutex, Weak};
|
||||
|
||||
use rand::prelude::*;
|
||||
|
||||
use structopt::StructOpt;
|
||||
|
||||
use async_std::prelude::*;
|
||||
use async_std::task;
|
||||
use futures::channel::mpsc;
|
||||
use futures::sink::{Sink, SinkExt};
|
||||
use futures::stream::StreamExt;
|
||||
|
||||
use async_tungstenite::tungstenite;
|
||||
use tungstenite::Error as WsError;
|
||||
use tungstenite::Message as WsMessage;
|
||||
|
||||
use gst::gst_element_error;
|
||||
use gst::prelude::*;
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
|
||||
const STUN_SERVER: &str = "stun://stun.l.google.com:19302";
|
||||
const TURN_SERVER: &str = "turn://foo:bar@webrtc.nirbheek.in:3478";
|
||||
|
||||
// upgrade weak reference or return
|
||||
#[macro_export]
|
||||
macro_rules! upgrade_weak {
|
||||
($x:ident, $r:expr) => {{
|
||||
match $x.upgrade() {
|
||||
Some(o) => o,
|
||||
None => return $r,
|
||||
}
|
||||
}};
|
||||
($x:ident) => {
|
||||
upgrade_weak!($x, ())
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
struct Args {
|
||||
#[structopt(short, long, default_value = "wss://webrtc.nirbheek.in:8443")]
|
||||
server: String,
|
||||
#[structopt(short, long)]
|
||||
peer_id: Option<u32>,
|
||||
}
|
||||
|
||||
// JSON messages we communicate with
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum JsonMsg {
|
||||
Ice {
|
||||
candidate: String,
|
||||
#[serde(rename = "sdpMLineIndex")]
|
||||
sdp_mline_index: u32,
|
||||
},
|
||||
Sdp {
|
||||
#[serde(rename = "type")]
|
||||
type_: String,
|
||||
sdp: String,
|
||||
},
|
||||
}
|
||||
|
||||
// Strong reference to our application state
|
||||
#[derive(Debug, Clone)]
|
||||
struct App(Arc<AppInner>);
|
||||
|
||||
// Weak reference to our application state
|
||||
#[derive(Debug, Clone)]
|
||||
struct AppWeak(Weak<AppInner>);
|
||||
|
||||
// Actual application state
|
||||
#[derive(Debug)]
|
||||
struct AppInner {
|
||||
args: Args,
|
||||
pipeline: gst::Pipeline,
|
||||
webrtcbin: gst::Element,
|
||||
send_msg_tx: Mutex<mpsc::UnboundedSender<WsMessage>>,
|
||||
}
|
||||
|
||||
// To be able to access the App's fields directly
|
||||
impl std::ops::Deref for App {
|
||||
type Target = AppInner;
|
||||
|
||||
fn deref(&self) -> &AppInner {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AppWeak {
|
||||
// Try upgrading a weak reference to a strong one
|
||||
fn upgrade(&self) -> Option<App> {
|
||||
self.0.upgrade().map(App)
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
// Downgrade the strong reference to a weak reference
|
||||
fn downgrade(&self) -> AppWeak {
|
||||
AppWeak(Arc::downgrade(&self.0))
|
||||
}
|
||||
|
||||
fn new(
|
||||
args: Args,
|
||||
) -> Result<
|
||||
(
|
||||
Self,
|
||||
impl Stream<Item = gst::Message>,
|
||||
impl Stream<Item = WsMessage>,
|
||||
),
|
||||
anyhow::Error,
|
||||
> {
|
||||
// Create the GStreamer pipeline
|
||||
let pipeline = gst::parse_launch(
|
||||
"videotestsrc pattern=ball is-live=true ! vp8enc deadline=1 ! rtpvp8pay pt=96 ! webrtcbin. \
|
||||
audiotestsrc is-live=true ! opusenc ! rtpopuspay pt=97 ! webrtcbin. \
|
||||
webrtcbin name=webrtcbin"
|
||||
)?;
|
||||
|
||||
// Downcast from gst::Element to gst::Pipeline
|
||||
let pipeline = pipeline
|
||||
.downcast::<gst::Pipeline>()
|
||||
.expect("not a pipeline");
|
||||
|
||||
// Get access to the webrtcbin by name
|
||||
let webrtcbin = pipeline
|
||||
.get_by_name("webrtcbin")
|
||||
.expect("can't find webrtcbin");
|
||||
|
||||
// Set some properties on webrtcbin
|
||||
webrtcbin.set_property_from_str("stun-server", STUN_SERVER);
|
||||
webrtcbin.set_property_from_str("turn-server", TURN_SERVER);
|
||||
webrtcbin.set_property_from_str("bundle-policy", "max-bundle");
|
||||
|
||||
// Create a stream for handling the GStreamer message asynchronously
|
||||
let bus = pipeline.get_bus().unwrap();
|
||||
let send_gst_msg_rx = gst::BusStream::new(&bus);
|
||||
|
||||
// Channel for outgoing WebSocket messages from other threads
|
||||
let (send_ws_msg_tx, send_ws_msg_rx) = mpsc::unbounded::<WsMessage>();
|
||||
|
||||
// Asynchronously set the pipeline to Playing
|
||||
pipeline.call_async(|pipeline| {
|
||||
pipeline
|
||||
.set_state(gst::State::Playing)
|
||||
.expect("Couldn't set pipeline to Playing");
|
||||
});
|
||||
|
||||
let app = App(Arc::new(AppInner {
|
||||
args,
|
||||
pipeline,
|
||||
webrtcbin,
|
||||
send_msg_tx: Mutex::new(send_ws_msg_tx),
|
||||
}));
|
||||
|
||||
// Connect to on-negotiation-needed to handle sending an Offer
|
||||
if app.args.peer_id.is_some() {
|
||||
let app_clone = app.downgrade();
|
||||
app.webrtcbin
|
||||
.connect("on-negotiation-needed", false, move |values| {
|
||||
let _webrtc = values[0].get::<gst::Element>().unwrap();
|
||||
|
||||
let app = upgrade_weak!(app_clone, None);
|
||||
if let Err(err) = app.on_negotiation_needed() {
|
||||
gst_element_error!(
|
||||
app.pipeline,
|
||||
gst::LibraryError::Failed,
|
||||
("Failed to negotiate: {:?}", err)
|
||||
);
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Whenever there is a new ICE candidate, send it to the peer
|
||||
let app_clone = app.downgrade();
|
||||
app.webrtcbin
|
||||
.connect("on-ice-candidate", false, move |values| {
|
||||
let _webrtc = values[0].get::<gst::Element>().expect("Invalid argument");
|
||||
let mlineindex = values[1].get_some::<u32>().expect("Invalid argument");
|
||||
let candidate = values[2]
|
||||
.get::<String>()
|
||||
.expect("Invalid argument")
|
||||
.unwrap();
|
||||
|
||||
let app = upgrade_weak!(app_clone, None);
|
||||
|
||||
if let Err(err) = app.on_ice_candidate(mlineindex, candidate) {
|
||||
gst_element_error!(
|
||||
app.pipeline,
|
||||
gst::LibraryError::Failed,
|
||||
("Failed to send ICE candidate: {:?}", err)
|
||||
);
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Whenever there is a new stream incoming from the peer, handle it
|
||||
let app_clone = app.downgrade();
|
||||
app.webrtcbin.connect_pad_added(move |_webrtc, pad| {
|
||||
let app = upgrade_weak!(app_clone);
|
||||
|
||||
if let Err(err) = app.on_incoming_stream(pad) {
|
||||
gst_element_error!(
|
||||
app.pipeline,
|
||||
gst::LibraryError::Failed,
|
||||
("Failed to handle incoming stream: {:?}", err)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Asynchronously set the pipeline to Playing
|
||||
app.pipeline.call_async(|pipeline| {
|
||||
// If this fails, post an error on the bus so we exit
|
||||
if pipeline.set_state(gst::State::Playing).is_err() {
|
||||
gst_element_error!(
|
||||
pipeline,
|
||||
gst::LibraryError::Failed,
|
||||
("Failed to set pipeline to Playing")
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Ok((app, send_gst_msg_rx, send_ws_msg_rx))
|
||||
}
|
||||
|
||||
// Handle WebSocket messages, both our own as well as WebSocket protocol messages
|
||||
fn handle_websocket_message(&self, msg: &str) -> Result<(), anyhow::Error> {
|
||||
if msg.starts_with("ERROR") {
|
||||
bail!("Got error message: {}", msg);
|
||||
}
|
||||
|
||||
let json_msg: JsonMsg = serde_json::from_str(msg)?;
|
||||
|
||||
match json_msg {
|
||||
JsonMsg::Sdp { type_, sdp } => self.handle_sdp(&type_, &sdp),
|
||||
JsonMsg::Ice {
|
||||
sdp_mline_index,
|
||||
candidate,
|
||||
} => self.handle_ice(sdp_mline_index, &candidate),
|
||||
}
|
||||
}
|
||||
|
||||
// Handle GStreamer messages coming from the pipeline
|
||||
fn handle_pipeline_message(&self, message: &gst::Message) -> Result<(), anyhow::Error> {
|
||||
use gst::message::MessageView;
|
||||
|
||||
match message.view() {
|
||||
MessageView::Error(err) => bail!(
|
||||
"Error from element {}: {} ({})",
|
||||
err.get_src()
|
||||
.map(|s| String::from(s.get_path_string()))
|
||||
.unwrap_or_else(|| String::from("None")),
|
||||
err.get_error(),
|
||||
err.get_debug().unwrap_or_else(|| String::from("None")),
|
||||
),
|
||||
MessageView::Warning(warning) => {
|
||||
println!("Warning: \"{}\"", warning.get_debug().unwrap());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Whenever webrtcbin tells us that (re-)negotiation is needed, simply ask
|
||||
// for a new offer SDP from webrtcbin without any customization and then
|
||||
// asynchronously send it to the peer via the WebSocket connection
|
||||
fn on_negotiation_needed(&self) -> Result<(), anyhow::Error> {
|
||||
println!("starting negotiation");
|
||||
|
||||
let app_clone = self.downgrade();
|
||||
let promise = gst::Promise::new_with_change_func(move |reply| {
|
||||
let app = upgrade_weak!(app_clone);
|
||||
|
||||
if let Err(err) = app.on_offer_created(reply) {
|
||||
gst_element_error!(
|
||||
app.pipeline,
|
||||
gst::LibraryError::Failed,
|
||||
("Failed to send SDP offer: {:?}", err)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
self.webrtcbin
|
||||
.emit("create-offer", &[&None::<gst::Structure>, &promise])
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Once webrtcbin has create the offer SDP for us, handle it by sending it to the peer via the
|
||||
// WebSocket connection
|
||||
fn on_offer_created(
|
||||
&self,
|
||||
reply: Result<&gst::StructureRef, gst::PromiseError>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let reply = match reply {
|
||||
Ok(reply) => reply,
|
||||
Err(err) => {
|
||||
bail!("Offer creation future got no reponse: {:?}", err);
|
||||
}
|
||||
};
|
||||
|
||||
let offer = reply
|
||||
.get_value("offer")
|
||||
.unwrap()
|
||||
.get::<gst_webrtc::WebRTCSessionDescription>()
|
||||
.expect("Invalid argument")
|
||||
.unwrap();
|
||||
self.webrtcbin
|
||||
.emit("set-local-description", &[&offer, &None::<gst::Promise>])
|
||||
.unwrap();
|
||||
|
||||
println!(
|
||||
"sending SDP offer to peer: {}",
|
||||
offer.get_sdp().as_text().unwrap()
|
||||
);
|
||||
|
||||
let message = serde_json::to_string(&JsonMsg::Sdp {
|
||||
type_: "offer".to_string(),
|
||||
sdp: offer.get_sdp().as_text().unwrap(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
self.send_msg_tx
|
||||
.lock()
|
||||
.unwrap()
|
||||
.unbounded_send(WsMessage::Text(message))
|
||||
.with_context(|| format!("Failed to send SDP offer"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Once webrtcbin has create the answer SDP for us, handle it by sending it to the peer via the
|
||||
// WebSocket connection
|
||||
fn on_answer_created(
|
||||
&self,
|
||||
reply: Result<&gst::StructureRef, gst::PromiseError>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let reply = match reply {
|
||||
Ok(reply) => reply,
|
||||
Err(err) => {
|
||||
bail!("Answer creation future got no reponse: {:?}", err);
|
||||
}
|
||||
};
|
||||
|
||||
let answer = reply
|
||||
.get_value("answer")
|
||||
.unwrap()
|
||||
.get::<gst_webrtc::WebRTCSessionDescription>()
|
||||
.expect("Invalid argument")
|
||||
.unwrap();
|
||||
self.webrtcbin
|
||||
.emit("set-local-description", &[&answer, &None::<gst::Promise>])
|
||||
.unwrap();
|
||||
|
||||
println!(
|
||||
"sending SDP answer to peer: {}",
|
||||
answer.get_sdp().as_text().unwrap()
|
||||
);
|
||||
|
||||
let message = serde_json::to_string(&JsonMsg::Sdp {
|
||||
type_: "answer".to_string(),
|
||||
sdp: answer.get_sdp().as_text().unwrap(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
self.send_msg_tx
|
||||
.lock()
|
||||
.unwrap()
|
||||
.unbounded_send(WsMessage::Text(message))
|
||||
.with_context(|| format!("Failed to send SDP answer"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Handle incoming SDP answers from the peer
|
||||
fn handle_sdp(&self, type_: &str, sdp: &str) -> Result<(), anyhow::Error> {
|
||||
if type_ == "answer" {
|
||||
print!("Received answer:\n{}\n", sdp);
|
||||
|
||||
let ret = gst_sdp::SDPMessage::parse_buffer(sdp.as_bytes())
|
||||
.map_err(|_| anyhow!("Failed to parse SDP answer"))?;
|
||||
let answer =
|
||||
gst_webrtc::WebRTCSessionDescription::new(gst_webrtc::WebRTCSDPType::Answer, ret);
|
||||
|
||||
self.webrtcbin
|
||||
.emit("set-remote-description", &[&answer, &None::<gst::Promise>])
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
} else if type_ == "offer" {
|
||||
print!("Received offer:\n{}\n", sdp);
|
||||
|
||||
let ret = gst_sdp::SDPMessage::parse_buffer(sdp.as_bytes())
|
||||
.map_err(|_| anyhow!("Failed to parse SDP offer"))?;
|
||||
|
||||
// And then asynchronously start our pipeline and do the next steps. The
|
||||
// pipeline needs to be started before we can create an answer
|
||||
let app_clone = self.downgrade();
|
||||
self.pipeline.call_async(move |_pipeline| {
|
||||
let app = upgrade_weak!(app_clone);
|
||||
|
||||
let offer = gst_webrtc::WebRTCSessionDescription::new(
|
||||
gst_webrtc::WebRTCSDPType::Offer,
|
||||
ret,
|
||||
);
|
||||
|
||||
app.0
|
||||
.webrtcbin
|
||||
.emit("set-remote-description", &[&offer, &None::<gst::Promise>])
|
||||
.unwrap();
|
||||
|
||||
let app_clone = app.downgrade();
|
||||
let promise = gst::Promise::new_with_change_func(move |reply| {
|
||||
let app = upgrade_weak!(app_clone);
|
||||
|
||||
if let Err(err) = app.on_answer_created(reply) {
|
||||
gst_element_error!(
|
||||
app.pipeline,
|
||||
gst::LibraryError::Failed,
|
||||
("Failed to send SDP answer: {:?}", err)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
app.0
|
||||
.webrtcbin
|
||||
.emit("create-answer", &[&None::<gst::Structure>, &promise])
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Sdp type is not \"answer\" but \"{}\"", type_)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle incoming ICE candidates from the peer by passing them to webrtcbin
|
||||
fn handle_ice(&self, sdp_mline_index: u32, candidate: &str) -> Result<(), anyhow::Error> {
|
||||
self.webrtcbin
|
||||
.emit("add-ice-candidate", &[&sdp_mline_index, &candidate])
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Asynchronously send ICE candidates to the peer via the WebSocket connection as a JSON
|
||||
// message
|
||||
fn on_ice_candidate(&self, mlineindex: u32, candidate: String) -> Result<(), anyhow::Error> {
|
||||
let message = serde_json::to_string(&JsonMsg::Ice {
|
||||
candidate,
|
||||
sdp_mline_index: mlineindex,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
self.send_msg_tx
|
||||
.lock()
|
||||
.unwrap()
|
||||
.unbounded_send(WsMessage::Text(message))
|
||||
.with_context(|| format!("Failed to send ICE candidate"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Whenever there's a new incoming, encoded stream from the peer create a new decodebin
|
||||
fn on_incoming_stream(&self, pad: &gst::Pad) -> Result<(), anyhow::Error> {
|
||||
// Early return for the source pads we're adding ourselves
|
||||
if pad.get_direction() != gst::PadDirection::Src {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let decodebin = gst::ElementFactory::make("decodebin", None).unwrap();
|
||||
let app_clone = self.downgrade();
|
||||
decodebin.connect_pad_added(move |_decodebin, pad| {
|
||||
let app = upgrade_weak!(app_clone);
|
||||
|
||||
if let Err(err) = app.on_incoming_decodebin_stream(pad) {
|
||||
gst_element_error!(
|
||||
app.pipeline,
|
||||
gst::LibraryError::Failed,
|
||||
("Failed to handle decoded stream: {:?}", err)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
self.pipeline.add(&decodebin).unwrap();
|
||||
decodebin.sync_state_with_parent().unwrap();
|
||||
|
||||
let sinkpad = decodebin.get_static_pad("sink").unwrap();
|
||||
pad.link(&sinkpad).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Handle a newly decoded decodebin stream and depending on its type, create the relevant
|
||||
// elements or simply ignore it
|
||||
fn on_incoming_decodebin_stream(&self, pad: &gst::Pad) -> Result<(), anyhow::Error> {
|
||||
let caps = pad.get_current_caps().unwrap();
|
||||
let name = caps.get_structure(0).unwrap().get_name();
|
||||
|
||||
let sink = if name.starts_with("video/") {
|
||||
gst::parse_bin_from_description(
|
||||
"queue ! videoconvert ! videoscale ! autovideosink",
|
||||
true,
|
||||
)?
|
||||
} else if name.starts_with("audio/") {
|
||||
gst::parse_bin_from_description(
|
||||
"queue ! audioconvert ! audioresample ! autoaudiosink",
|
||||
true,
|
||||
)?
|
||||
} else {
|
||||
println!("Unknown pad {:?}, ignoring", pad);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
self.pipeline.add(&sink).unwrap();
|
||||
sink.sync_state_with_parent()
|
||||
.with_context(|| format!("can't start sink for stream {:?}", caps))?;
|
||||
|
||||
let sinkpad = sink.get_static_pad("sink").unwrap();
|
||||
pad.link(&sinkpad)
|
||||
.with_context(|| format!("can't link sink for stream {:?}", caps))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure to shut down the pipeline when it goes out of scope
|
||||
// to release any system resources
|
||||
impl Drop for AppInner {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.pipeline.set_state(gst::State::Null);
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(
|
||||
args: Args,
|
||||
ws: impl Sink<WsMessage, Error = WsError> + Stream<Item = Result<WsMessage, WsError>>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
// Split the websocket into the Sink and Stream
|
||||
let (mut ws_sink, ws_stream) = ws.split();
|
||||
// Fuse the Stream, required for the select macro
|
||||
let mut ws_stream = ws_stream.fuse();
|
||||
|
||||
// Create our application state
|
||||
let (app, send_gst_msg_rx, send_ws_msg_rx) = App::new(args)?;
|
||||
|
||||
let mut send_gst_msg_rx = send_gst_msg_rx.fuse();
|
||||
let mut send_ws_msg_rx = send_ws_msg_rx.fuse();
|
||||
|
||||
// And now let's start our message loop
|
||||
loop {
|
||||
let ws_msg = futures::select! {
|
||||
// Handle the WebSocket messages here
|
||||
ws_msg = ws_stream.select_next_some() => {
|
||||
match ws_msg? {
|
||||
WsMessage::Close(_) => {
|
||||
println!("peer disconnected");
|
||||
break
|
||||
},
|
||||
WsMessage::Ping(data) => Some(WsMessage::Pong(data)),
|
||||
WsMessage::Pong(_) => None,
|
||||
WsMessage::Binary(_) => None,
|
||||
WsMessage::Text(text) => {
|
||||
app.handle_websocket_message(&text)?;
|
||||
None
|
||||
},
|
||||
}
|
||||
},
|
||||
// Pass the GStreamer messages to the application control logic
|
||||
gst_msg = send_gst_msg_rx.select_next_some() => {
|
||||
app.handle_pipeline_message(&gst_msg)?;
|
||||
None
|
||||
},
|
||||
// Handle WebSocket messages we created asynchronously
|
||||
// to send them out now
|
||||
ws_msg = send_ws_msg_rx.select_next_some() => Some(ws_msg),
|
||||
// Once we're done, break the loop and return
|
||||
complete => break,
|
||||
};
|
||||
|
||||
// If there's a message to send out, do so now
|
||||
if let Some(ws_msg) = ws_msg {
|
||||
ws_sink.send(ws_msg).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Check if all GStreamer plugins we require are available
|
||||
fn check_plugins() -> Result<(), anyhow::Error> {
|
||||
let needed = [
|
||||
"videotestsrc",
|
||||
"audiotestsrc",
|
||||
"videoconvert",
|
||||
"audioconvert",
|
||||
"autodetect",
|
||||
"opus",
|
||||
"vpx",
|
||||
"webrtc",
|
||||
"nice",
|
||||
"dtls",
|
||||
"srtp",
|
||||
"rtpmanager",
|
||||
"rtp",
|
||||
"playback",
|
||||
"videoscale",
|
||||
"audioresample",
|
||||
];
|
||||
|
||||
let registry = gst::Registry::get();
|
||||
let missing = needed
|
||||
.iter()
|
||||
.filter(|n| registry.find_plugin(n).is_none())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !missing.is_empty() {
|
||||
bail!("Missing plugins: {:?}", missing);
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn async_main() -> Result<(), anyhow::Error> {
|
||||
// Initialize GStreamer first
|
||||
gst::init()?;
|
||||
|
||||
check_plugins()?;
|
||||
|
||||
let args = Args::from_args();
|
||||
|
||||
// Connect to the given server
|
||||
let (mut ws, _) = async_tungstenite::async_std::connect_async(&args.server).await?;
|
||||
|
||||
println!("connected");
|
||||
|
||||
// Say HELLO to the server and see if it replies with HELLO
|
||||
let our_id = rand::thread_rng().gen_range(10, 10_000);
|
||||
println!("Registering id {} with server", our_id);
|
||||
ws.send(WsMessage::Text(format!("HELLO {}", our_id)))
|
||||
.await?;
|
||||
|
||||
let msg = ws
|
||||
.next()
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("didn't receive anything"))??;
|
||||
|
||||
if msg != WsMessage::Text("HELLO".into()) {
|
||||
bail!("server didn't say HELLO");
|
||||
}
|
||||
|
||||
if let Some(peer_id) = args.peer_id {
|
||||
// Join the given session
|
||||
ws.send(WsMessage::Text(format!("SESSION {}", peer_id)))
|
||||
.await?;
|
||||
|
||||
let msg = ws
|
||||
.next()
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("didn't receive anything"))??;
|
||||
|
||||
if msg != WsMessage::Text("SESSION_OK".into()) {
|
||||
bail!("server error: {:?}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
// All good, let's run our message loop
|
||||
run(args, ws).await
|
||||
}
|
||||
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
macos_workaround::run(|| task::block_on(async_main()))
|
||||
}
|
309
webrtc/sendrecv/gst-sharp/WebRTCSendRecv.cs
Normal file
309
webrtc/sendrecv/gst-sharp/WebRTCSendRecv.cs
Normal file
|
@ -0,0 +1,309 @@
|
|||
using System;
|
||||
using static System.Diagnostics.Debug;
|
||||
using Gst;
|
||||
using WebSocketSharp;
|
||||
using Gst.WebRTC;
|
||||
using Newtonsoft.Json;
|
||||
using System.Net.Security;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Gst.Sdp;
|
||||
using System.Text;
|
||||
using GLib;
|
||||
|
||||
namespace GstWebRTCDemo
|
||||
{
|
||||
class WebRtcClient : IDisposable
|
||||
{
|
||||
const string SERVER = "wss://127.0.0.1:8443";
|
||||
|
||||
const string PIPELINE_DESC = @"webrtcbin name=sendrecv bundle-policy=max-bundle
|
||||
videotestsrc is-live=true pattern=ball ! videoconvert ! queue ! vp8enc deadline=1 ! rtpvp8pay !
|
||||
queue ! application/x-rtp,media=video,encoding-name=VP8,payload=97 ! sendrecv.
|
||||
audiotestsrc is-live=true wave=red-noise ! audioconvert ! audioresample ! queue ! opusenc ! rtpopuspay !
|
||||
queue ! application/x-rtp,media=audio,encoding-name=OPUS,payload=96 ! sendrecv.";
|
||||
|
||||
readonly int _id;
|
||||
readonly int _peerId;
|
||||
readonly string _server;
|
||||
readonly WebSocket _conn;
|
||||
Pipeline pipe;
|
||||
Element webrtc;
|
||||
bool terminate;
|
||||
|
||||
public WebRtcClient(int id, int peerId, string server = SERVER)
|
||||
{
|
||||
_id = id;
|
||||
_peerId = peerId;
|
||||
_server = server;
|
||||
|
||||
_conn = new WebSocket(_server);
|
||||
_conn.SslConfiguration.ServerCertificateValidationCallback = validatCert;
|
||||
_conn.OnOpen += OnOpen;
|
||||
_conn.OnError += OnError;
|
||||
_conn.OnMessage += OnMessage;
|
||||
_conn.OnClose += OnClose;
|
||||
|
||||
pipe = (Pipeline)Parse.Launch(PIPELINE_DESC);
|
||||
}
|
||||
|
||||
bool validatCert(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Connect()
|
||||
{
|
||||
_conn.ConnectAsync();
|
||||
}
|
||||
|
||||
void SetupCall()
|
||||
{
|
||||
_conn.Send($"SESSION {_peerId}");
|
||||
}
|
||||
|
||||
void OnClose(object sender, CloseEventArgs e)
|
||||
{
|
||||
Console.WriteLine("Closed: " + e.Reason);
|
||||
|
||||
terminate = true;
|
||||
}
|
||||
|
||||
void OnError(object sender, ErrorEventArgs e)
|
||||
{
|
||||
Console.WriteLine("Error " + e.Message);
|
||||
|
||||
terminate = true;
|
||||
}
|
||||
|
||||
void OnOpen(object sender, System.EventArgs e)
|
||||
{
|
||||
var ws = sender as WebSocket;
|
||||
ws.SendAsync($"HELLO {_id}", (b) => Console.WriteLine($"Opened {b}"));
|
||||
}
|
||||
|
||||
void OnMessage(object sender, MessageEventArgs args)
|
||||
{
|
||||
var msg = args.Data;
|
||||
switch (msg)
|
||||
{
|
||||
case "HELLO":
|
||||
SetupCall();
|
||||
break;
|
||||
case "SESSION_OK":
|
||||
StartPipeline();
|
||||
break;
|
||||
default:
|
||||
if (msg.StartsWith("ERROR")) {
|
||||
Console.WriteLine(msg);
|
||||
terminate = true;
|
||||
} else {
|
||||
HandleSdp(msg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void StartPipeline()
|
||||
{
|
||||
webrtc = pipe.GetByName("sendrecv");
|
||||
Assert(webrtc != null);
|
||||
webrtc.Connect("on-negotiation-needed", OnNegotiationNeeded);
|
||||
webrtc.Connect("on-ice-candidate", OnIceCandidate);
|
||||
webrtc.Connect("pad-added", OnIncomingStream);
|
||||
pipe.SetState(State.Playing);
|
||||
Console.WriteLine("Playing");
|
||||
}
|
||||
|
||||
#region Webrtc signal handlers
|
||||
#region Incoming stream
|
||||
void OnIncomingStream(object o, GLib.SignalArgs args)
|
||||
{
|
||||
var pad = args.Args[0] as Pad;
|
||||
if (pad.Direction != PadDirection.Src)
|
||||
return;
|
||||
var decodebin = ElementFactory.Make("decodebin");
|
||||
decodebin.Connect("pad-added", OnIncomingDecodebinStream);
|
||||
pipe.Add(decodebin);
|
||||
decodebin.SyncStateWithParent();
|
||||
webrtc.Link(decodebin);
|
||||
}
|
||||
|
||||
void OnIncomingDecodebinStream(object o, SignalArgs args)
|
||||
{
|
||||
var pad = (Pad)args.Args[0];
|
||||
if (!pad.HasCurrentCaps)
|
||||
{
|
||||
Console.WriteLine($"{pad.Name} has no caps, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
var caps = pad.CurrentCaps;
|
||||
Assert(!caps.IsEmpty);
|
||||
Structure s = caps[0];
|
||||
var name = s.Name;
|
||||
if (name.StartsWith("video"))
|
||||
{
|
||||
var q = ElementFactory.Make("queue");
|
||||
var conv = ElementFactory.Make("videoconvert");
|
||||
var sink = ElementFactory.Make("autovideosink");
|
||||
pipe.Add(q, conv, sink);
|
||||
pipe.SyncChildrenStates();
|
||||
pad.Link(q.GetStaticPad("sink"));
|
||||
Element.Link(q, conv, sink);
|
||||
}
|
||||
else if (name.StartsWith("audio"))
|
||||
{
|
||||
var q = ElementFactory.Make("queue");
|
||||
var conv = ElementFactory.Make("audioconvert");
|
||||
var resample = ElementFactory.Make("audioresample");
|
||||
var sink = ElementFactory.Make("autoaudiosink");
|
||||
pipe.Add(q, conv, resample, sink);
|
||||
pipe.SyncChildrenStates();
|
||||
pad.Link(q.GetStaticPad("sink"));
|
||||
Element.Link(q, conv, resample, sink);
|
||||
}
|
||||
|
||||
}
|
||||
#endregion
|
||||
|
||||
void OnIceCandidate(object o, GLib.SignalArgs args)
|
||||
{
|
||||
var index = (uint)args.Args[0];
|
||||
var cand = (string)args.Args[1];
|
||||
var obj = new { ice = new { sdpMLineIndex = index, candidate = cand } };
|
||||
var iceMsg = JsonConvert.SerializeObject(obj);
|
||||
|
||||
_conn.SendAsync(iceMsg, (b) => { } );
|
||||
}
|
||||
|
||||
void OnNegotiationNeeded(object o, GLib.SignalArgs args)
|
||||
{
|
||||
var webRtc = o as Element;
|
||||
Assert(webRtc != null, "not a webrtc object");
|
||||
Promise promise = new Promise(OnOfferCreated, webrtc.Handle, null); // webRtc.Handle, null);
|
||||
Structure structure = new Structure("struct");
|
||||
webrtc.Emit("create-offer", structure, promise);
|
||||
}
|
||||
|
||||
void OnOfferCreated(Promise promise)
|
||||
{
|
||||
promise.Wait();
|
||||
var reply = promise.RetrieveReply();
|
||||
var gval = reply.GetValue("offer");
|
||||
WebRTCSessionDescription offer = (WebRTCSessionDescription)gval.Val;
|
||||
promise = new Promise();
|
||||
webrtc.Emit("set-local-description", offer, promise);
|
||||
promise.Interrupt();
|
||||
SendSdpOffer(offer) ;
|
||||
}
|
||||
#endregion
|
||||
|
||||
void SendSdpOffer(WebRTCSessionDescription offer)
|
||||
{
|
||||
var text = offer.Sdp.AsText();
|
||||
var obj = new { sdp = new { type = "offer", sdp = text } };
|
||||
var json = JsonConvert.SerializeObject(obj);
|
||||
Console.Write(json);
|
||||
|
||||
_conn.SendAsync(json, (b) => Console.WriteLine($"Send offer completed {b}"));
|
||||
}
|
||||
|
||||
void HandleSdp(string message)
|
||||
{
|
||||
var msg = JsonConvert.DeserializeObject<dynamic>(message);
|
||||
|
||||
if (msg.sdp != null)
|
||||
{
|
||||
var sdp = msg.sdp;
|
||||
if (sdp.type != null && sdp.type != "answer")
|
||||
{
|
||||
throw new Exception("Not an answer");
|
||||
}
|
||||
string sdpAns = sdp.sdp;
|
||||
Console.WriteLine($"received answer:\n{sdpAns}");
|
||||
SDPMessage.New(out SDPMessage sdpMsg);
|
||||
SDPMessage.ParseBuffer(ASCIIEncoding.Default.GetBytes(sdpAns), (uint)sdpAns.Length, sdpMsg);
|
||||
var answer = WebRTCSessionDescription.New(WebRTCSDPType.Answer, sdpMsg);
|
||||
var promise = new Promise();
|
||||
webrtc.Emit("set-remote-description", answer, promise);
|
||||
}
|
||||
else if (msg.ice != null)
|
||||
{
|
||||
var ice = msg.ice;
|
||||
string candidate = ice.candidate;
|
||||
uint sdpMLineIndex = ice.sdpMLineIndex;
|
||||
webrtc.Emit("add-ice-candidate", sdpMLineIndex, candidate);
|
||||
}
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
// Wait until error, EOS or State Change
|
||||
var bus = pipe.Bus;
|
||||
do {
|
||||
var msg = bus.TimedPopFiltered (Gst.Constants.SECOND, MessageType.Error | MessageType.Eos | MessageType.StateChanged);
|
||||
// Parse message
|
||||
if (msg != null) {
|
||||
switch (msg.Type) {
|
||||
case MessageType.Error:
|
||||
string debug;
|
||||
GLib.GException exc;
|
||||
msg.ParseError (out exc, out debug);
|
||||
Console.WriteLine ("Error received from element {0}: {1}", msg.Src.Name, exc.Message);
|
||||
Console.WriteLine ("Debugging information: {0}", debug != null ? debug : "none");
|
||||
terminate = true;
|
||||
break;
|
||||
case MessageType.Eos:
|
||||
Console.WriteLine ("End-Of-Stream reached.\n");
|
||||
terminate = true;
|
||||
break;
|
||||
case MessageType.StateChanged:
|
||||
// We are only interested in state-changed messages from the pipeline
|
||||
if (msg.Src == pipe) {
|
||||
State oldState, newState, pendingState;
|
||||
msg.ParseStateChanged (out oldState, out newState, out pendingState);
|
||||
Console.WriteLine ("Pipeline state changed from {0} to {1}:",
|
||||
Element.StateGetName (oldState), Element.StateGetName (newState));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// We should not reach here because we only asked for ERRORs, EOS and STATE_CHANGED
|
||||
Console.WriteLine ("Unexpected message received.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (!terminate);
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
((IDisposable)_conn).Dispose();
|
||||
pipe.SetState(State.Null);
|
||||
pipe.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
static class WebRtcSendRcv
|
||||
{
|
||||
const string SERVER = "wss://webrtc.nirbheek.in:8443";
|
||||
static Random random = new Random();
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
// Initialize GStreamer
|
||||
Gst.Application.Init (ref args);
|
||||
|
||||
if (args.Length == 0)
|
||||
throw new Exception("need peerId");
|
||||
int peerId = Int32.Parse(args[0]);
|
||||
var server = (args.Length > 1) ? args[1] : SERVER;
|
||||
|
||||
var ourId = random.Next(100, 10000);
|
||||
Console.WriteLine($"PeerId:{peerId} OurId:{ourId} ");
|
||||
var c = new WebRtcClient(ourId, peerId, server);
|
||||
c.Connect();
|
||||
c.Run();
|
||||
c.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
36
webrtc/sendrecv/gst-sharp/meson.build
Normal file
36
webrtc/sendrecv/gst-sharp/meson.build
Normal file
|
@ -0,0 +1,36 @@
|
|||
project('gstreamer-sharp', ['cs'], meson_version: '>=0.47.0', license: 'LGPL')
|
||||
gstreamer_version = '1.14.0'
|
||||
|
||||
|
||||
mono_path = ''
|
||||
nuget = find_program('nuget.py')
|
||||
|
||||
dependencies = []
|
||||
foreach dependency, version: { 'Newtonsoft.Json': '11.0.2', 'WebSocketSharp': '1.0.3-rc11'}
|
||||
message('Getting @0@:@1@'.format(dependency, version))
|
||||
get_dep= run_command(nuget, 'get',
|
||||
'--builddir', dependency,
|
||||
'--nuget-name', dependency,
|
||||
'--nuget-version', version,
|
||||
'--csharp-version=net45',
|
||||
'--current-builddir', meson.current_build_dir(),
|
||||
'--builddir', meson.build_root(),
|
||||
)
|
||||
|
||||
if get_dep.returncode() != 0
|
||||
error('Failed to get @0@-@1@: @2@'.format(dependency, version, get_dep.stderr()))
|
||||
endif
|
||||
|
||||
link_args = get_dep.stdout().split()
|
||||
dependencies += [declare_dependency(link_args: link_args, version: version)]
|
||||
foreach path: get_dep.stdout().split()
|
||||
mono_path += ':@0@'.format(join_paths(meson.build_root(), path.strip('-r:'), '..'))
|
||||
endforeach
|
||||
endforeach
|
||||
|
||||
# Use nugget once 1.16 is released.
|
||||
dependencies += [dependency('gstreamer-sharp-1.0', fallback: ['gstreamer-sharp', 'gst_sharp_dep'])]
|
||||
|
||||
message('Execute with MONO_PATH=@0@:$MONO_PATH @1@/WebRTCSendRecv.exe'.format(mono_path, meson.current_build_dir()))
|
||||
executable('WebRTCSendRecv', 'WebRTCSendRecv.cs',
|
||||
cs_args: ['-unsafe'], dependencies: dependencies)
|
211
webrtc/sendrecv/gst-sharp/nuget.py
Normal file
211
webrtc/sendrecv/gst-sharp/nuget.py
Normal file
|
@ -0,0 +1,211 @@
|
|||
#!/usr/bin/python3
|
||||
import argparse
|
||||
import getpass
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from urllib.request import urlretrieve
|
||||
from zipfile import ZipFile
|
||||
|
||||
NUSPEC_TEMPLATE = """<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>{package_name}</id>
|
||||
<authors>{author}</authors>
|
||||
<owners>{owner}</owners>
|
||||
<licenseUrl>{license_url}</licenseUrl>
|
||||
<projectUrl>{project_url}</projectUrl>
|
||||
<iconUrl>{icon_url}</iconUrl>
|
||||
<requireLicenseAcceptance>false</requireLicenseAcceptance>
|
||||
<description>{description}.</description>
|
||||
<copyright>{copyright}</copyright>
|
||||
<tags>{tags}</tags>
|
||||
<version>{version}</version>
|
||||
<dependencies>
|
||||
{dependencies} </dependencies>
|
||||
</metadata>
|
||||
<files>
|
||||
{files} </files>
|
||||
</package>
|
||||
"""
|
||||
|
||||
TARGETS_TEMPLATE = r"""<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Target Name="{package_name}CopyMapConfigs" AfterTargets="AfterBuild">
|
||||
<CreateItem Include="$(MSBuildThisFileDirectory)\{frameworkdir}\*.config">
|
||||
<Output TaskParameter="Include" ItemName="MapConfigs" />
|
||||
</CreateItem>
|
||||
|
||||
<Copy SourceFiles="@(MapConfigs)" DestinationFiles="@(MapConfigs->'$(OutDir)\%(RecursiveDir)%(Filename)%(Extension)')" />
|
||||
</Target>
|
||||
</Project>"""
|
||||
|
||||
|
||||
class Nugetifier:
|
||||
def cleanup_args(self):
|
||||
self.nugetdir = os.path.join(self.builddir,
|
||||
self.package_name + 'nupkg')
|
||||
self.frameworkdir = 'net45'
|
||||
self.nuget_build_dir = os.path.join(
|
||||
self.nugetdir, 'build', self.frameworkdir)
|
||||
self.nuget_lib_dir = os.path.join(
|
||||
self.nugetdir, 'lib', self.frameworkdir)
|
||||
self.nuspecfile = os.path.join(
|
||||
self.nugetdir, '%s.nuspec' % self.package_name)
|
||||
self.nugettargets = os.path.join(
|
||||
self.nuget_build_dir, "%s.targets" % self.package_name)
|
||||
self.nuget = shutil.which('nuget')
|
||||
if not self.nuget:
|
||||
print("Could not find the `nuget` tool, install it and retry!")
|
||||
return -1
|
||||
|
||||
for d in [self.nugetdir, self.nuget_lib_dir, self.nuget_build_dir]:
|
||||
os.makedirs(d, exist_ok=True)
|
||||
if not self.description:
|
||||
self.description = "%s c# bindings" % self.package_name
|
||||
if not self.copyright:
|
||||
self.copyright = "Copyright %s" % datetime.now().year
|
||||
if not self.tags:
|
||||
self.tags = self.package_name
|
||||
|
||||
return 0
|
||||
|
||||
def run(self):
|
||||
res = self.cleanup_args()
|
||||
if res:
|
||||
return res
|
||||
|
||||
self.files = ''
|
||||
|
||||
def add_file(path, target="lib"):
|
||||
f = ' <file src="%s" target="%s"/>\n' % (
|
||||
path, os.path.join(target, os.path.basename(path)))
|
||||
self.files += f
|
||||
|
||||
self.dependencies = ''
|
||||
for dependency in self.dependency:
|
||||
_id, version = dependency.split(":")
|
||||
self.dependencies += ' <dependency id="%s" version="%s" />\n' % (
|
||||
_id, version)
|
||||
|
||||
for assembly in self.assembly:
|
||||
add_file(assembly, os.path.join('lib', self.frameworkdir))
|
||||
|
||||
for f in [assembly + '.config', assembly[:-3] + 'pdb']:
|
||||
if os.path.exists(f):
|
||||
add_file(f, os.path.join('build', self.frameworkdir))
|
||||
|
||||
with open(self.nugettargets, 'w') as _:
|
||||
print(TARGETS_TEMPLATE.format(**self.__dict__), file=_)
|
||||
add_file(self.nugettargets, 'build')
|
||||
|
||||
with open(self.nuspecfile, 'w') as _:
|
||||
print(NUSPEC_TEMPLATE.format(**self.__dict__), file=_)
|
||||
|
||||
subprocess.check_call([self.nuget, 'pack', self.nuspecfile],
|
||||
cwd=self.builddir)
|
||||
|
||||
|
||||
class NugetDownloader:
|
||||
def reporthook(self, blocknum, blocksize, totalsize):
|
||||
readsofar = blocknum * blocksize
|
||||
if totalsize > 0:
|
||||
percent = readsofar * 1e2 / totalsize
|
||||
s = "\r%5.1f%% %*d / %d" % (
|
||||
percent, len(str(totalsize)), readsofar, totalsize)
|
||||
sys.stderr.write(s)
|
||||
if readsofar >= totalsize: # near the end
|
||||
sys.stderr.write("\n")
|
||||
else: # total size is unknown
|
||||
sys.stderr.write("read %d\n" % (readsofar,))
|
||||
|
||||
def run(self):
|
||||
url = "https://www.nuget.org/api/v2/package/{nuget_name}/{nuget_version}".format(
|
||||
**self.__dict__)
|
||||
workdir = os.path.join(self.current_builddir,
|
||||
self.nuget_name, self.nuget_version)
|
||||
os.makedirs(workdir, exist_ok=True)
|
||||
|
||||
try:
|
||||
with open(os.path.join(workdir, 'linkline'), 'r') as f:
|
||||
print(f.read())
|
||||
return
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
nugetpath = os.path.join(workdir, self.nuget_name) + '.zip'
|
||||
print("Downloading %s into %s" % (url, nugetpath), file=sys.stderr)
|
||||
urlretrieve(url, nugetpath, self.reporthook)
|
||||
|
||||
lib_paths = [os.path.join('lib', self.csharp_version), 'lib']
|
||||
build_path = os.path.join('build', self.csharp_version)
|
||||
dll_path = os.path.join(self.nuget_name, self.nuget_version)
|
||||
extract_dir = os.path.join(self.current_builddir, dll_path)
|
||||
os.makedirs(extract_dir, exist_ok=True)
|
||||
linkline = ''
|
||||
|
||||
print("%s - %s" % (self.builddir, extract_dir), file=sys.stderr)
|
||||
configs = []
|
||||
dlldir = None
|
||||
with ZipFile(nugetpath) as zip:
|
||||
for lib_path in lib_paths:
|
||||
for f in zip.infolist():
|
||||
if f.filename.startswith(lib_path) or f.filename.startswith(build_path):
|
||||
zip.extract(f, path=extract_dir)
|
||||
if f.filename.endswith('.dll'):
|
||||
fpath = os.path.relpath(os.path.join(extract_dir, f.filename), self.builddir)
|
||||
linkline += ' -r:' + fpath
|
||||
|
||||
dlldir = os.path.dirname(os.path.join(extract_dir, f.filename))
|
||||
elif f.filename.endswith('.dll.config'):
|
||||
configs.append(os.path.join(extract_dir, f.filename))
|
||||
|
||||
if dlldir:
|
||||
break
|
||||
|
||||
print(dlldir, file=sys.stderr)
|
||||
for config in configs:
|
||||
print(config, file=sys.stderr)
|
||||
print(os.path.join(dlldir, os.path.basename(config)), file=sys.stderr)
|
||||
os.rename(config, os.path.join(dlldir, os.path.basename(config)))
|
||||
|
||||
with open(os.path.join(workdir, 'linkline'), 'w') as f:
|
||||
print(linkline.strip(), file=f)
|
||||
|
||||
print(linkline.strip())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if "get" not in sys.argv:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--builddir')
|
||||
parser.add_argument('--package-name')
|
||||
parser.add_argument('--author', default=getpass.getuser())
|
||||
parser.add_argument('--owner', default=getpass.getuser())
|
||||
parser.add_argument('--native', action='append', default=[])
|
||||
parser.add_argument('--assembly', action='append', default=[])
|
||||
parser.add_argument('--out')
|
||||
parser.add_argument('--description')
|
||||
parser.add_argument('--copyright')
|
||||
parser.add_argument('--version')
|
||||
parser.add_argument('--icon-url', default='')
|
||||
parser.add_argument('--project-url', default='')
|
||||
parser.add_argument('--license-url', default='')
|
||||
parser.add_argument('--tags', default='')
|
||||
parser.add_argument('--dependency', default=[], action='append')
|
||||
|
||||
runner = Nugetifier()
|
||||
else:
|
||||
sys.argv.remove('get')
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--builddir')
|
||||
parser.add_argument('--current-builddir')
|
||||
parser.add_argument('--nuget-name')
|
||||
parser.add_argument('--nuget-version')
|
||||
parser.add_argument('--csharp-version')
|
||||
|
||||
runner = NugetDownloader()
|
||||
|
||||
options = parser.parse_args(namespace=runner)
|
||||
exit(runner.run())
|
4
webrtc/sendrecv/gst-sharp/subprojects/bindinator.wrap
Normal file
4
webrtc/sendrecv/gst-sharp/subprojects/bindinator.wrap
Normal file
|
@ -0,0 +1,4 @@
|
|||
[wrap-git]
|
||||
directory=bindinator
|
||||
url=https://github.com/GLibSharp/bindinator.git
|
||||
revision=master
|
|
@ -0,0 +1,5 @@
|
|||
[wrap-git]
|
||||
directory=gstreamer-sharp
|
||||
url=https://anongit.freedesktop.org/git/gstreamer/gstreamer-sharp.git
|
||||
push-url=ssh://git.freedesktop.org/git/gstreamer/gstreamer-sharp
|
||||
revision=master
|
4
webrtc/sendrecv/gst-sharp/subprojects/gtk-sharp.wrap
Normal file
4
webrtc/sendrecv/gst-sharp/subprojects/gtk-sharp.wrap
Normal file
|
@ -0,0 +1,4 @@
|
|||
[wrap-git]
|
||||
directory=gtk-sharp
|
||||
url=https://github.com/gtk-sharp/gtk-sharp.git
|
||||
revision=master
|
18
webrtc/sendrecv/gst/Dockerfile
Normal file
18
webrtc/sendrecv/gst/Dockerfile
Normal file
|
@ -0,0 +1,18 @@
|
|||
FROM maxmcd/gstreamer:1.14-buster
|
||||
|
||||
RUN apt-get install -y libjson-glib-dev
|
||||
# RUN apk update
|
||||
# RUN apk add json-glib-dev libsoup-dev
|
||||
|
||||
WORKDIR /opt/
|
||||
COPY . /opt/
|
||||
|
||||
RUN make
|
||||
|
||||
CMD echo "Waiting a few seconds for you to open the browser at localhost:8080" \
|
||||
&& sleep 10 \
|
||||
&& ./webrtc-sendrecv \
|
||||
--peer-id=1 \
|
||||
--server=ws://signalling:8443 \
|
||||
--disable-ssl
|
||||
|
6
webrtc/sendrecv/gst/Makefile
Normal file
6
webrtc/sendrecv/gst/Makefile
Normal file
|
@ -0,0 +1,6 @@
|
|||
CC := gcc
|
||||
LIBS := $(shell pkg-config --libs --cflags glib-2.0 gstreamer-1.0 gstreamer-sdp-1.0 gstreamer-webrtc-1.0 json-glib-1.0 libsoup-2.4)
|
||||
CFLAGS := -O0 -ggdb -Wall -fno-omit-frame-pointer \
|
||||
$(shell pkg-config --cflags glib-2.0 gstreamer-1.0 gstreamer-sdp-1.0 gstreamer-webrtc-1.0 json-glib-1.0 libsoup-2.4)
|
||||
webrtc-sendrecv: webrtc-sendrecv.c
|
||||
"$(CC)" $(CFLAGS) $^ $(LIBS) -o $@
|
5
webrtc/sendrecv/gst/meson.build
Normal file
5
webrtc/sendrecv/gst/meson.build
Normal file
|
@ -0,0 +1,5 @@
|
|||
executable('webrtc-sendrecv',
|
||||
'webrtc-sendrecv.c',
|
||||
dependencies : [gst_dep, gstsdp_dep, gstwebrtc_dep, libsoup_dep, json_glib_dep ])
|
||||
|
||||
webrtc_py = files('webrtc_sendrecv.py')
|
831
webrtc/sendrecv/gst/webrtc-sendrecv.c
Normal file
831
webrtc/sendrecv/gst/webrtc-sendrecv.c
Normal file
|
@ -0,0 +1,831 @@
|
|||
/*
|
||||
* Demo gstreamer app for negotiating and streaming a sendrecv webrtc stream
|
||||
* with a browser JS app.
|
||||
*
|
||||
* gcc webrtc-sendrecv.c $(pkg-config --cflags --libs gstreamer-webrtc-1.0 gstreamer-sdp-1.0 libsoup-2.4 json-glib-1.0) -o webrtc-sendrecv
|
||||
*
|
||||
* Author: Nirbheek Chauhan <nirbheek@centricular.com>
|
||||
*/
|
||||
#include <gst/gst.h>
|
||||
#include <gst/sdp/sdp.h>
|
||||
|
||||
#define GST_USE_UNSTABLE_API
|
||||
#include <gst/webrtc/webrtc.h>
|
||||
|
||||
/* For signalling */
|
||||
#include <libsoup/soup.h>
|
||||
#include <json-glib/json-glib.h>
|
||||
|
||||
#include <string.h>
|
||||
|
||||
enum AppState
|
||||
{
|
||||
APP_STATE_UNKNOWN = 0,
|
||||
APP_STATE_ERROR = 1, /* generic error */
|
||||
SERVER_CONNECTING = 1000,
|
||||
SERVER_CONNECTION_ERROR,
|
||||
SERVER_CONNECTED, /* Ready to register */
|
||||
SERVER_REGISTERING = 2000,
|
||||
SERVER_REGISTRATION_ERROR,
|
||||
SERVER_REGISTERED, /* Ready to call a peer */
|
||||
SERVER_CLOSED, /* server connection closed by us or the server */
|
||||
PEER_CONNECTING = 3000,
|
||||
PEER_CONNECTION_ERROR,
|
||||
PEER_CONNECTED,
|
||||
PEER_CALL_NEGOTIATING = 4000,
|
||||
PEER_CALL_STARTED,
|
||||
PEER_CALL_STOPPING,
|
||||
PEER_CALL_STOPPED,
|
||||
PEER_CALL_ERROR,
|
||||
};
|
||||
|
||||
static GMainLoop *loop;
|
||||
static GstElement *pipe1, *webrtc1;
|
||||
static GObject *send_channel, *receive_channel;
|
||||
|
||||
static SoupWebsocketConnection *ws_conn = NULL;
|
||||
static enum AppState app_state = 0;
|
||||
static const gchar *peer_id = NULL;
|
||||
static const gchar *server_url = "wss://webrtc.nirbheek.in:8443";
|
||||
static gboolean disable_ssl = FALSE;
|
||||
static gboolean remote_is_offerer = FALSE;
|
||||
|
||||
static GOptionEntry entries[] = {
|
||||
{"peer-id", 0, 0, G_OPTION_ARG_STRING, &peer_id,
|
||||
"String ID of the peer to connect to", "ID"},
|
||||
{"server", 0, 0, G_OPTION_ARG_STRING, &server_url,
|
||||
"Signalling server to connect to", "URL"},
|
||||
{"disable-ssl", 0, 0, G_OPTION_ARG_NONE, &disable_ssl, "Disable ssl", NULL},
|
||||
{"remote-offerer", 0, 0, G_OPTION_ARG_NONE, &remote_is_offerer,
|
||||
"Request that the peer generate the offer and we'll answer", NULL},
|
||||
{NULL},
|
||||
};
|
||||
|
||||
static gboolean
|
||||
cleanup_and_quit_loop (const gchar * msg, enum AppState state)
|
||||
{
|
||||
if (msg)
|
||||
g_printerr ("%s\n", msg);
|
||||
if (state > 0)
|
||||
app_state = state;
|
||||
|
||||
if (ws_conn) {
|
||||
if (soup_websocket_connection_get_state (ws_conn) ==
|
||||
SOUP_WEBSOCKET_STATE_OPEN)
|
||||
/* This will call us again */
|
||||
soup_websocket_connection_close (ws_conn, 1000, "");
|
||||
else
|
||||
g_object_unref (ws_conn);
|
||||
}
|
||||
|
||||
if (loop) {
|
||||
g_main_loop_quit (loop);
|
||||
loop = NULL;
|
||||
}
|
||||
|
||||
/* To allow usage as a GSourceFunc */
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static gchar *
|
||||
get_string_from_json_object (JsonObject * object)
|
||||
{
|
||||
JsonNode *root;
|
||||
JsonGenerator *generator;
|
||||
gchar *text;
|
||||
|
||||
/* Make it the root node */
|
||||
root = json_node_init_object (json_node_alloc (), object);
|
||||
generator = json_generator_new ();
|
||||
json_generator_set_root (generator, root);
|
||||
text = json_generator_to_data (generator, NULL);
|
||||
|
||||
/* Release everything */
|
||||
g_object_unref (generator);
|
||||
json_node_free (root);
|
||||
return text;
|
||||
}
|
||||
|
||||
static void
|
||||
handle_media_stream (GstPad * pad, GstElement * pipe, const char *convert_name,
|
||||
const char *sink_name)
|
||||
{
|
||||
GstPad *qpad;
|
||||
GstElement *q, *conv, *resample, *sink;
|
||||
GstPadLinkReturn ret;
|
||||
|
||||
g_print ("Trying to handle stream with %s ! %s", convert_name, sink_name);
|
||||
|
||||
q = gst_element_factory_make ("queue", NULL);
|
||||
g_assert_nonnull (q);
|
||||
conv = gst_element_factory_make (convert_name, NULL);
|
||||
g_assert_nonnull (conv);
|
||||
sink = gst_element_factory_make (sink_name, NULL);
|
||||
g_assert_nonnull (sink);
|
||||
|
||||
if (g_strcmp0 (convert_name, "audioconvert") == 0) {
|
||||
/* Might also need to resample, so add it just in case.
|
||||
* Will be a no-op if it's not required. */
|
||||
resample = gst_element_factory_make ("audioresample", NULL);
|
||||
g_assert_nonnull (resample);
|
||||
gst_bin_add_many (GST_BIN (pipe), q, conv, resample, sink, NULL);
|
||||
gst_element_sync_state_with_parent (q);
|
||||
gst_element_sync_state_with_parent (conv);
|
||||
gst_element_sync_state_with_parent (resample);
|
||||
gst_element_sync_state_with_parent (sink);
|
||||
gst_element_link_many (q, conv, resample, sink, NULL);
|
||||
} else {
|
||||
gst_bin_add_many (GST_BIN (pipe), q, conv, sink, NULL);
|
||||
gst_element_sync_state_with_parent (q);
|
||||
gst_element_sync_state_with_parent (conv);
|
||||
gst_element_sync_state_with_parent (sink);
|
||||
gst_element_link_many (q, conv, sink, NULL);
|
||||
}
|
||||
|
||||
qpad = gst_element_get_static_pad (q, "sink");
|
||||
|
||||
ret = gst_pad_link (pad, qpad);
|
||||
g_assert_cmphex (ret, ==, GST_PAD_LINK_OK);
|
||||
}
|
||||
|
||||
static void
|
||||
on_incoming_decodebin_stream (GstElement * decodebin, GstPad * pad,
|
||||
GstElement * pipe)
|
||||
{
|
||||
GstCaps *caps;
|
||||
const gchar *name;
|
||||
|
||||
if (!gst_pad_has_current_caps (pad)) {
|
||||
g_printerr ("Pad '%s' has no caps, can't do anything, ignoring\n",
|
||||
GST_PAD_NAME (pad));
|
||||
return;
|
||||
}
|
||||
|
||||
caps = gst_pad_get_current_caps (pad);
|
||||
name = gst_structure_get_name (gst_caps_get_structure (caps, 0));
|
||||
|
||||
if (g_str_has_prefix (name, "video")) {
|
||||
handle_media_stream (pad, pipe, "videoconvert", "autovideosink");
|
||||
} else if (g_str_has_prefix (name, "audio")) {
|
||||
handle_media_stream (pad, pipe, "audioconvert", "autoaudiosink");
|
||||
} else {
|
||||
g_printerr ("Unknown pad %s, ignoring", GST_PAD_NAME (pad));
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
on_incoming_stream (GstElement * webrtc, GstPad * pad, GstElement * pipe)
|
||||
{
|
||||
GstElement *decodebin;
|
||||
GstPad *sinkpad;
|
||||
|
||||
if (GST_PAD_DIRECTION (pad) != GST_PAD_SRC)
|
||||
return;
|
||||
|
||||
decodebin = gst_element_factory_make ("decodebin", NULL);
|
||||
g_signal_connect (decodebin, "pad-added",
|
||||
G_CALLBACK (on_incoming_decodebin_stream), pipe);
|
||||
gst_bin_add (GST_BIN (pipe), decodebin);
|
||||
gst_element_sync_state_with_parent (decodebin);
|
||||
|
||||
sinkpad = gst_element_get_static_pad (decodebin, "sink");
|
||||
gst_pad_link (pad, sinkpad);
|
||||
gst_object_unref (sinkpad);
|
||||
}
|
||||
|
||||
static void
|
||||
send_ice_candidate_message (GstElement * webrtc G_GNUC_UNUSED, guint mlineindex,
|
||||
gchar * candidate, gpointer user_data G_GNUC_UNUSED)
|
||||
{
|
||||
gchar *text;
|
||||
JsonObject *ice, *msg;
|
||||
|
||||
if (app_state < PEER_CALL_NEGOTIATING) {
|
||||
cleanup_and_quit_loop ("Can't send ICE, not in call", APP_STATE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
ice = json_object_new ();
|
||||
json_object_set_string_member (ice, "candidate", candidate);
|
||||
json_object_set_int_member (ice, "sdpMLineIndex", mlineindex);
|
||||
msg = json_object_new ();
|
||||
json_object_set_object_member (msg, "ice", ice);
|
||||
text = get_string_from_json_object (msg);
|
||||
json_object_unref (msg);
|
||||
|
||||
soup_websocket_connection_send_text (ws_conn, text);
|
||||
g_free (text);
|
||||
}
|
||||
|
||||
static void
|
||||
send_sdp_to_peer (GstWebRTCSessionDescription * desc)
|
||||
{
|
||||
gchar *text;
|
||||
JsonObject *msg, *sdp;
|
||||
|
||||
if (app_state < PEER_CALL_NEGOTIATING) {
|
||||
cleanup_and_quit_loop ("Can't send SDP to peer, not in call",
|
||||
APP_STATE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
text = gst_sdp_message_as_text (desc->sdp);
|
||||
sdp = json_object_new ();
|
||||
|
||||
if (desc->type == GST_WEBRTC_SDP_TYPE_OFFER) {
|
||||
g_print ("Sending offer:\n%s\n", text);
|
||||
json_object_set_string_member (sdp, "type", "offer");
|
||||
} else if (desc->type == GST_WEBRTC_SDP_TYPE_ANSWER) {
|
||||
g_print ("Sending answer:\n%s\n", text);
|
||||
json_object_set_string_member (sdp, "type", "answer");
|
||||
} else {
|
||||
g_assert_not_reached ();
|
||||
}
|
||||
|
||||
json_object_set_string_member (sdp, "sdp", text);
|
||||
g_free (text);
|
||||
|
||||
msg = json_object_new ();
|
||||
json_object_set_object_member (msg, "sdp", sdp);
|
||||
text = get_string_from_json_object (msg);
|
||||
json_object_unref (msg);
|
||||
|
||||
soup_websocket_connection_send_text (ws_conn, text);
|
||||
g_free (text);
|
||||
}
|
||||
|
||||
/* Offer created by our pipeline, to be sent to the peer */
|
||||
static void
|
||||
on_offer_created (GstPromise * promise, gpointer user_data)
|
||||
{
|
||||
GstWebRTCSessionDescription *offer = NULL;
|
||||
const GstStructure *reply;
|
||||
|
||||
g_assert_cmphex (app_state, ==, PEER_CALL_NEGOTIATING);
|
||||
|
||||
g_assert_cmphex (gst_promise_wait (promise), ==, GST_PROMISE_RESULT_REPLIED);
|
||||
reply = gst_promise_get_reply (promise);
|
||||
gst_structure_get (reply, "offer",
|
||||
GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &offer, NULL);
|
||||
gst_promise_unref (promise);
|
||||
|
||||
promise = gst_promise_new ();
|
||||
g_signal_emit_by_name (webrtc1, "set-local-description", offer, promise);
|
||||
gst_promise_interrupt (promise);
|
||||
gst_promise_unref (promise);
|
||||
|
||||
/* Send offer to peer */
|
||||
send_sdp_to_peer (offer);
|
||||
gst_webrtc_session_description_free (offer);
|
||||
}
|
||||
|
||||
static void
|
||||
on_negotiation_needed (GstElement * element, gpointer user_data)
|
||||
{
|
||||
app_state = PEER_CALL_NEGOTIATING;
|
||||
|
||||
if (remote_is_offerer) {
|
||||
gchar *msg = g_strdup_printf ("OFFER_REQUEST");
|
||||
soup_websocket_connection_send_text (ws_conn, msg);
|
||||
g_free (msg);
|
||||
} else {
|
||||
GstPromise *promise;
|
||||
promise =
|
||||
gst_promise_new_with_change_func (on_offer_created, user_data, NULL);;
|
||||
g_signal_emit_by_name (webrtc1, "create-offer", NULL, promise);
|
||||
}
|
||||
}
|
||||
|
||||
#define STUN_SERVER " stun-server=stun://stun.l.google.com:19302 "
|
||||
#define RTP_CAPS_OPUS "application/x-rtp,media=audio,encoding-name=OPUS,payload="
|
||||
#define RTP_CAPS_VP8 "application/x-rtp,media=video,encoding-name=VP8,payload="
|
||||
|
||||
static void
|
||||
data_channel_on_error (GObject * dc, gpointer user_data)
|
||||
{
|
||||
cleanup_and_quit_loop ("Data channel error", 0);
|
||||
}
|
||||
|
||||
static void
|
||||
data_channel_on_open (GObject * dc, gpointer user_data)
|
||||
{
|
||||
GBytes *bytes = g_bytes_new ("data", strlen ("data"));
|
||||
g_print ("data channel opened\n");
|
||||
g_signal_emit_by_name (dc, "send-string", "Hi! from GStreamer");
|
||||
g_signal_emit_by_name (dc, "send-data", bytes);
|
||||
g_bytes_unref (bytes);
|
||||
}
|
||||
|
||||
static void
|
||||
data_channel_on_close (GObject * dc, gpointer user_data)
|
||||
{
|
||||
cleanup_and_quit_loop ("Data channel closed", 0);
|
||||
}
|
||||
|
||||
static void
|
||||
data_channel_on_message_string (GObject * dc, gchar * str, gpointer user_data)
|
||||
{
|
||||
g_print ("Received data channel message: %s\n", str);
|
||||
}
|
||||
|
||||
static void
|
||||
connect_data_channel_signals (GObject * data_channel)
|
||||
{
|
||||
g_signal_connect (data_channel, "on-error",
|
||||
G_CALLBACK (data_channel_on_error), NULL);
|
||||
g_signal_connect (data_channel, "on-open", G_CALLBACK (data_channel_on_open),
|
||||
NULL);
|
||||
g_signal_connect (data_channel, "on-close",
|
||||
G_CALLBACK (data_channel_on_close), NULL);
|
||||
g_signal_connect (data_channel, "on-message-string",
|
||||
G_CALLBACK (data_channel_on_message_string), NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
on_data_channel (GstElement * webrtc, GObject * data_channel,
|
||||
gpointer user_data)
|
||||
{
|
||||
connect_data_channel_signals (data_channel);
|
||||
receive_channel = data_channel;
|
||||
}
|
||||
|
||||
static void
|
||||
on_ice_gathering_state_notify (GstElement * webrtcbin, GParamSpec * pspec,
|
||||
gpointer user_data)
|
||||
{
|
||||
GstWebRTCICEGatheringState ice_gather_state;
|
||||
const gchar *new_state = "unknown";
|
||||
|
||||
g_object_get (webrtcbin, "ice-gathering-state", &ice_gather_state, NULL);
|
||||
switch (ice_gather_state) {
|
||||
case GST_WEBRTC_ICE_GATHERING_STATE_NEW:
|
||||
new_state = "new";
|
||||
break;
|
||||
case GST_WEBRTC_ICE_GATHERING_STATE_GATHERING:
|
||||
new_state = "gathering";
|
||||
break;
|
||||
case GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE:
|
||||
new_state = "complete";
|
||||
break;
|
||||
}
|
||||
g_print ("ICE gathering state changed to %s\n", new_state);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
start_pipeline (void)
|
||||
{
|
||||
GstStateChangeReturn ret;
|
||||
GError *error = NULL;
|
||||
|
||||
pipe1 =
|
||||
gst_parse_launch ("webrtcbin bundle-policy=max-bundle name=sendrecv "
|
||||
STUN_SERVER
|
||||
"videotestsrc is-live=true pattern=ball ! videoconvert ! queue ! vp8enc deadline=1 ! rtpvp8pay ! "
|
||||
"queue ! " RTP_CAPS_VP8 "96 ! sendrecv. "
|
||||
"audiotestsrc is-live=true wave=red-noise ! audioconvert ! audioresample ! queue ! opusenc ! rtpopuspay ! "
|
||||
"queue ! " RTP_CAPS_OPUS "97 ! sendrecv. ", &error);
|
||||
|
||||
if (error) {
|
||||
g_printerr ("Failed to parse launch: %s\n", error->message);
|
||||
g_error_free (error);
|
||||
goto err;
|
||||
}
|
||||
|
||||
webrtc1 = gst_bin_get_by_name (GST_BIN (pipe1), "sendrecv");
|
||||
g_assert_nonnull (webrtc1);
|
||||
|
||||
/* This is the gstwebrtc entry point where we create the offer and so on. It
|
||||
* will be called when the pipeline goes to PLAYING. */
|
||||
g_signal_connect (webrtc1, "on-negotiation-needed",
|
||||
G_CALLBACK (on_negotiation_needed), NULL);
|
||||
/* We need to transmit this ICE candidate to the browser via the websockets
|
||||
* signalling server. Incoming ice candidates from the browser need to be
|
||||
* added by us too, see on_server_message() */
|
||||
g_signal_connect (webrtc1, "on-ice-candidate",
|
||||
G_CALLBACK (send_ice_candidate_message), NULL);
|
||||
g_signal_connect (webrtc1, "notify::ice-gathering-state",
|
||||
G_CALLBACK (on_ice_gathering_state_notify), NULL);
|
||||
|
||||
gst_element_set_state (pipe1, GST_STATE_READY);
|
||||
|
||||
g_signal_emit_by_name (webrtc1, "create-data-channel", "channel", NULL,
|
||||
&send_channel);
|
||||
if (send_channel) {
|
||||
g_print ("Created data channel\n");
|
||||
connect_data_channel_signals (send_channel);
|
||||
} else {
|
||||
g_print ("Could not create data channel, is usrsctp available?\n");
|
||||
}
|
||||
|
||||
g_signal_connect (webrtc1, "on-data-channel", G_CALLBACK (on_data_channel),
|
||||
NULL);
|
||||
/* Incoming streams will be exposed via this signal */
|
||||
g_signal_connect (webrtc1, "pad-added", G_CALLBACK (on_incoming_stream),
|
||||
pipe1);
|
||||
/* Lifetime is the same as the pipeline itself */
|
||||
gst_object_unref (webrtc1);
|
||||
|
||||
g_print ("Starting pipeline\n");
|
||||
ret = gst_element_set_state (GST_ELEMENT (pipe1), GST_STATE_PLAYING);
|
||||
if (ret == GST_STATE_CHANGE_FAILURE)
|
||||
goto err;
|
||||
|
||||
return TRUE;
|
||||
|
||||
err:
|
||||
if (pipe1)
|
||||
g_clear_object (&pipe1);
|
||||
if (webrtc1)
|
||||
webrtc1 = NULL;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
setup_call (void)
|
||||
{
|
||||
gchar *msg;
|
||||
|
||||
if (soup_websocket_connection_get_state (ws_conn) !=
|
||||
SOUP_WEBSOCKET_STATE_OPEN)
|
||||
return FALSE;
|
||||
|
||||
if (!peer_id)
|
||||
return FALSE;
|
||||
|
||||
g_print ("Setting up signalling server call with %s\n", peer_id);
|
||||
app_state = PEER_CONNECTING;
|
||||
msg = g_strdup_printf ("SESSION %s", peer_id);
|
||||
soup_websocket_connection_send_text (ws_conn, msg);
|
||||
g_free (msg);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
register_with_server (void)
|
||||
{
|
||||
gchar *hello;
|
||||
gint32 our_id;
|
||||
|
||||
if (soup_websocket_connection_get_state (ws_conn) !=
|
||||
SOUP_WEBSOCKET_STATE_OPEN)
|
||||
return FALSE;
|
||||
|
||||
our_id = g_random_int_range (10, 10000);
|
||||
g_print ("Registering id %i with server\n", our_id);
|
||||
app_state = SERVER_REGISTERING;
|
||||
|
||||
/* Register with the server with a random integer id. Reply will be received
|
||||
* by on_server_message() */
|
||||
hello = g_strdup_printf ("HELLO %i", our_id);
|
||||
soup_websocket_connection_send_text (ws_conn, hello);
|
||||
g_free (hello);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
on_server_closed (SoupWebsocketConnection * conn G_GNUC_UNUSED,
|
||||
gpointer user_data G_GNUC_UNUSED)
|
||||
{
|
||||
app_state = SERVER_CLOSED;
|
||||
cleanup_and_quit_loop ("Server connection closed", 0);
|
||||
}
|
||||
|
||||
/* Answer created by our pipeline, to be sent to the peer */
|
||||
static void
|
||||
on_answer_created (GstPromise * promise, gpointer user_data)
|
||||
{
|
||||
GstWebRTCSessionDescription *answer = NULL;
|
||||
const GstStructure *reply;
|
||||
|
||||
g_assert_cmphex (app_state, ==, PEER_CALL_NEGOTIATING);
|
||||
|
||||
g_assert_cmphex (gst_promise_wait (promise), ==, GST_PROMISE_RESULT_REPLIED);
|
||||
reply = gst_promise_get_reply (promise);
|
||||
gst_structure_get (reply, "answer",
|
||||
GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &answer, NULL);
|
||||
gst_promise_unref (promise);
|
||||
|
||||
promise = gst_promise_new ();
|
||||
g_signal_emit_by_name (webrtc1, "set-local-description", answer, promise);
|
||||
gst_promise_interrupt (promise);
|
||||
gst_promise_unref (promise);
|
||||
|
||||
/* Send answer to peer */
|
||||
send_sdp_to_peer (answer);
|
||||
gst_webrtc_session_description_free (answer);
|
||||
}
|
||||
|
||||
static void
|
||||
on_offer_set (GstPromise * promise, gpointer user_data)
|
||||
{
|
||||
gst_promise_unref (promise);
|
||||
promise = gst_promise_new_with_change_func (on_answer_created, NULL, NULL);
|
||||
g_signal_emit_by_name (webrtc1, "create-answer", NULL, promise);
|
||||
}
|
||||
|
||||
static void
|
||||
on_offer_received (GstSDPMessage *sdp)
|
||||
{
|
||||
GstWebRTCSessionDescription *offer = NULL;
|
||||
GstPromise *promise;
|
||||
|
||||
offer = gst_webrtc_session_description_new (GST_WEBRTC_SDP_TYPE_OFFER, sdp);
|
||||
g_assert_nonnull (offer);
|
||||
|
||||
/* Set remote description on our pipeline */
|
||||
{
|
||||
promise = gst_promise_new_with_change_func (on_offer_set, NULL, NULL);
|
||||
g_signal_emit_by_name (webrtc1, "set-remote-description", offer,
|
||||
promise);
|
||||
}
|
||||
gst_webrtc_session_description_free (offer);
|
||||
}
|
||||
|
||||
/* One mega message handler for our asynchronous calling mechanism */
|
||||
static void
|
||||
on_server_message (SoupWebsocketConnection * conn, SoupWebsocketDataType type,
|
||||
GBytes * message, gpointer user_data)
|
||||
{
|
||||
gchar *text;
|
||||
|
||||
switch (type) {
|
||||
case SOUP_WEBSOCKET_DATA_BINARY:
|
||||
g_printerr ("Received unknown binary message, ignoring\n");
|
||||
return;
|
||||
case SOUP_WEBSOCKET_DATA_TEXT:{
|
||||
gsize size;
|
||||
const gchar *data = g_bytes_get_data (message, &size);
|
||||
/* Convert to NULL-terminated string */
|
||||
text = g_strndup (data, size);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
g_assert_not_reached ();
|
||||
}
|
||||
|
||||
/* Server has accepted our registration, we are ready to send commands */
|
||||
if (g_strcmp0 (text, "HELLO") == 0) {
|
||||
if (app_state != SERVER_REGISTERING) {
|
||||
cleanup_and_quit_loop ("ERROR: Received HELLO when not registering",
|
||||
APP_STATE_ERROR);
|
||||
goto out;
|
||||
}
|
||||
app_state = SERVER_REGISTERED;
|
||||
g_print ("Registered with server\n");
|
||||
/* Ask signalling server to connect us with a specific peer */
|
||||
if (!setup_call ()) {
|
||||
cleanup_and_quit_loop ("ERROR: Failed to setup call", PEER_CALL_ERROR);
|
||||
goto out;
|
||||
}
|
||||
/* Call has been setup by the server, now we can start negotiation */
|
||||
} else if (g_strcmp0 (text, "SESSION_OK") == 0) {
|
||||
if (app_state != PEER_CONNECTING) {
|
||||
cleanup_and_quit_loop ("ERROR: Received SESSION_OK when not calling",
|
||||
PEER_CONNECTION_ERROR);
|
||||
goto out;
|
||||
}
|
||||
|
||||
app_state = PEER_CONNECTED;
|
||||
/* Start negotiation (exchange SDP and ICE candidates) */
|
||||
if (!start_pipeline ())
|
||||
cleanup_and_quit_loop ("ERROR: failed to start pipeline",
|
||||
PEER_CALL_ERROR);
|
||||
/* Handle errors */
|
||||
} else if (g_str_has_prefix (text, "ERROR")) {
|
||||
switch (app_state) {
|
||||
case SERVER_CONNECTING:
|
||||
app_state = SERVER_CONNECTION_ERROR;
|
||||
break;
|
||||
case SERVER_REGISTERING:
|
||||
app_state = SERVER_REGISTRATION_ERROR;
|
||||
break;
|
||||
case PEER_CONNECTING:
|
||||
app_state = PEER_CONNECTION_ERROR;
|
||||
break;
|
||||
case PEER_CONNECTED:
|
||||
case PEER_CALL_NEGOTIATING:
|
||||
app_state = PEER_CALL_ERROR;
|
||||
break;
|
||||
default:
|
||||
app_state = APP_STATE_ERROR;
|
||||
}
|
||||
cleanup_and_quit_loop (text, 0);
|
||||
/* Look for JSON messages containing SDP and ICE candidates */
|
||||
} else {
|
||||
JsonNode *root;
|
||||
JsonObject *object, *child;
|
||||
JsonParser *parser = json_parser_new ();
|
||||
if (!json_parser_load_from_data (parser, text, -1, NULL)) {
|
||||
g_printerr ("Unknown message '%s', ignoring", text);
|
||||
g_object_unref (parser);
|
||||
goto out;
|
||||
}
|
||||
|
||||
root = json_parser_get_root (parser);
|
||||
if (!JSON_NODE_HOLDS_OBJECT (root)) {
|
||||
g_printerr ("Unknown json message '%s', ignoring", text);
|
||||
g_object_unref (parser);
|
||||
goto out;
|
||||
}
|
||||
|
||||
object = json_node_get_object (root);
|
||||
/* Check type of JSON message */
|
||||
if (json_object_has_member (object, "sdp")) {
|
||||
int ret;
|
||||
GstSDPMessage *sdp;
|
||||
const gchar *text, *sdptype;
|
||||
GstWebRTCSessionDescription *answer;
|
||||
|
||||
g_assert_cmphex (app_state, ==, PEER_CALL_NEGOTIATING);
|
||||
|
||||
child = json_object_get_object_member (object, "sdp");
|
||||
|
||||
if (!json_object_has_member (child, "type")) {
|
||||
cleanup_and_quit_loop ("ERROR: received SDP without 'type'",
|
||||
PEER_CALL_ERROR);
|
||||
goto out;
|
||||
}
|
||||
|
||||
sdptype = json_object_get_string_member (child, "type");
|
||||
/* In this example, we create the offer and receive one answer by default,
|
||||
* but it's possible to comment out the offer creation and wait for an offer
|
||||
* instead, so we handle either here.
|
||||
*
|
||||
* See tests/examples/webrtcbidirectional.c in gst-plugins-bad for another
|
||||
* example how to handle offers from peers and reply with answers using webrtcbin. */
|
||||
text = json_object_get_string_member (child, "sdp");
|
||||
ret = gst_sdp_message_new (&sdp);
|
||||
g_assert_cmphex (ret, ==, GST_SDP_OK);
|
||||
ret = gst_sdp_message_parse_buffer ((guint8 *) text, strlen (text), sdp);
|
||||
g_assert_cmphex (ret, ==, GST_SDP_OK);
|
||||
|
||||
if (g_str_equal (sdptype, "answer")) {
|
||||
g_print ("Received answer:\n%s\n", text);
|
||||
answer = gst_webrtc_session_description_new (GST_WEBRTC_SDP_TYPE_ANSWER,
|
||||
sdp);
|
||||
g_assert_nonnull (answer);
|
||||
|
||||
/* Set remote description on our pipeline */
|
||||
{
|
||||
GstPromise *promise = gst_promise_new ();
|
||||
g_signal_emit_by_name (webrtc1, "set-remote-description", answer,
|
||||
promise);
|
||||
gst_promise_interrupt (promise);
|
||||
gst_promise_unref (promise);
|
||||
}
|
||||
app_state = PEER_CALL_STARTED;
|
||||
} else {
|
||||
g_print ("Received offer:\n%s\n", text);
|
||||
on_offer_received (sdp);
|
||||
}
|
||||
|
||||
} else if (json_object_has_member (object, "ice")) {
|
||||
const gchar *candidate;
|
||||
gint sdpmlineindex;
|
||||
|
||||
child = json_object_get_object_member (object, "ice");
|
||||
candidate = json_object_get_string_member (child, "candidate");
|
||||
sdpmlineindex = json_object_get_int_member (child, "sdpMLineIndex");
|
||||
|
||||
/* Add ice candidate sent by remote peer */
|
||||
g_signal_emit_by_name (webrtc1, "add-ice-candidate", sdpmlineindex,
|
||||
candidate);
|
||||
} else {
|
||||
g_printerr ("Ignoring unknown JSON message:\n%s\n", text);
|
||||
}
|
||||
g_object_unref (parser);
|
||||
}
|
||||
|
||||
out:
|
||||
g_free (text);
|
||||
}
|
||||
|
||||
static void
|
||||
on_server_connected (SoupSession * session, GAsyncResult * res,
|
||||
SoupMessage * msg)
|
||||
{
|
||||
GError *error = NULL;
|
||||
|
||||
ws_conn = soup_session_websocket_connect_finish (session, res, &error);
|
||||
if (error) {
|
||||
cleanup_and_quit_loop (error->message, SERVER_CONNECTION_ERROR);
|
||||
g_error_free (error);
|
||||
return;
|
||||
}
|
||||
|
||||
g_assert_nonnull (ws_conn);
|
||||
|
||||
app_state = SERVER_CONNECTED;
|
||||
g_print ("Connected to signalling server\n");
|
||||
|
||||
g_signal_connect (ws_conn, "closed", G_CALLBACK (on_server_closed), NULL);
|
||||
g_signal_connect (ws_conn, "message", G_CALLBACK (on_server_message), NULL);
|
||||
|
||||
/* Register with the server so it knows about us and can accept commands */
|
||||
register_with_server ();
|
||||
}
|
||||
|
||||
/*
|
||||
* Connect to the signalling server. This is the entrypoint for everything else.
|
||||
*/
|
||||
static void
|
||||
connect_to_websocket_server_async (void)
|
||||
{
|
||||
SoupLogger *logger;
|
||||
SoupMessage *message;
|
||||
SoupSession *session;
|
||||
const char *https_aliases[] = { "wss", NULL };
|
||||
|
||||
session =
|
||||
soup_session_new_with_options (SOUP_SESSION_SSL_STRICT, !disable_ssl,
|
||||
SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE,
|
||||
//SOUP_SESSION_SSL_CA_FILE, "/etc/ssl/certs/ca-bundle.crt",
|
||||
SOUP_SESSION_HTTPS_ALIASES, https_aliases, NULL);
|
||||
|
||||
logger = soup_logger_new (SOUP_LOGGER_LOG_BODY, -1);
|
||||
soup_session_add_feature (session, SOUP_SESSION_FEATURE (logger));
|
||||
g_object_unref (logger);
|
||||
|
||||
message = soup_message_new (SOUP_METHOD_GET, server_url);
|
||||
|
||||
g_print ("Connecting to server...\n");
|
||||
|
||||
/* Once connected, we will register */
|
||||
soup_session_websocket_connect_async (session, message, NULL, NULL, NULL,
|
||||
(GAsyncReadyCallback) on_server_connected, message);
|
||||
app_state = SERVER_CONNECTING;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
check_plugins (void)
|
||||
{
|
||||
int i;
|
||||
gboolean ret;
|
||||
GstPlugin *plugin;
|
||||
GstRegistry *registry;
|
||||
const gchar *needed[] = { "opus", "vpx", "nice", "webrtc", "dtls", "srtp",
|
||||
"rtpmanager", "videotestsrc", "audiotestsrc", NULL
|
||||
};
|
||||
|
||||
registry = gst_registry_get ();
|
||||
ret = TRUE;
|
||||
for (i = 0; i < g_strv_length ((gchar **) needed); i++) {
|
||||
plugin = gst_registry_find_plugin (registry, needed[i]);
|
||||
if (!plugin) {
|
||||
g_print ("Required gstreamer plugin '%s' not found\n", needed[i]);
|
||||
ret = FALSE;
|
||||
continue;
|
||||
}
|
||||
gst_object_unref (plugin);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
GOptionContext *context;
|
||||
GError *error = NULL;
|
||||
|
||||
context = g_option_context_new ("- gstreamer webrtc sendrecv demo");
|
||||
g_option_context_add_main_entries (context, entries, NULL);
|
||||
g_option_context_add_group (context, gst_init_get_option_group ());
|
||||
if (!g_option_context_parse (context, &argc, &argv, &error)) {
|
||||
g_printerr ("Error initializing: %s\n", error->message);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!check_plugins ())
|
||||
return -1;
|
||||
|
||||
if (!peer_id) {
|
||||
g_printerr ("--peer-id is a required argument\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Disable ssl when running a localhost server, because
|
||||
* it's probably a test server with a self-signed certificate */
|
||||
{
|
||||
GstUri *uri = gst_uri_from_string (server_url);
|
||||
if (g_strcmp0 ("localhost", gst_uri_get_host (uri)) == 0 ||
|
||||
g_strcmp0 ("127.0.0.1", gst_uri_get_host (uri)) == 0)
|
||||
disable_ssl = TRUE;
|
||||
gst_uri_unref (uri);
|
||||
}
|
||||
|
||||
loop = g_main_loop_new (NULL, FALSE);
|
||||
|
||||
connect_to_websocket_server_async ();
|
||||
|
||||
g_main_loop_run (loop);
|
||||
g_main_loop_unref (loop);
|
||||
|
||||
if (pipe1) {
|
||||
gst_element_set_state (GST_ELEMENT (pipe1), GST_STATE_NULL);
|
||||
g_print ("Pipeline stopped\n");
|
||||
gst_object_unref (pipe1);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
191
webrtc/sendrecv/gst/webrtc_sendrecv.py
Normal file
191
webrtc/sendrecv/gst/webrtc_sendrecv.py
Normal file
|
@ -0,0 +1,191 @@
|
|||
import random
|
||||
import ssl
|
||||
import websockets
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
|
||||
import gi
|
||||
gi.require_version('Gst', '1.0')
|
||||
from gi.repository import Gst
|
||||
gi.require_version('GstWebRTC', '1.0')
|
||||
from gi.repository import GstWebRTC
|
||||
gi.require_version('GstSdp', '1.0')
|
||||
from gi.repository import GstSdp
|
||||
|
||||
PIPELINE_DESC = '''
|
||||
webrtcbin name=sendrecv bundle-policy=max-bundle stun-server=stun://stun.l.google.com:19302
|
||||
videotestsrc is-live=true pattern=ball ! videoconvert ! queue ! vp8enc deadline=1 ! rtpvp8pay !
|
||||
queue ! application/x-rtp,media=video,encoding-name=VP8,payload=97 ! sendrecv.
|
||||
audiotestsrc is-live=true wave=red-noise ! audioconvert ! audioresample ! queue ! opusenc ! rtpopuspay !
|
||||
queue ! application/x-rtp,media=audio,encoding-name=OPUS,payload=96 ! sendrecv.
|
||||
'''
|
||||
|
||||
from websockets.version import version as wsv
|
||||
|
||||
class WebRTCClient:
|
||||
def __init__(self, id_, peer_id, server):
|
||||
self.id_ = id_
|
||||
self.conn = None
|
||||
self.pipe = None
|
||||
self.webrtc = None
|
||||
self.peer_id = peer_id
|
||||
self.server = server or 'wss://webrtc.nirbheek.in:8443'
|
||||
|
||||
|
||||
async def connect(self):
|
||||
sslctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
||||
self.conn = await websockets.connect(self.server, ssl=sslctx)
|
||||
await self.conn.send('HELLO %d' % self.id_)
|
||||
|
||||
async def setup_call(self):
|
||||
await self.conn.send('SESSION {}'.format(self.peer_id))
|
||||
|
||||
def send_sdp_offer(self, offer):
|
||||
text = offer.sdp.as_text()
|
||||
print ('Sending offer:\n%s' % text)
|
||||
msg = json.dumps({'sdp': {'type': 'offer', 'sdp': text}})
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.run_until_complete(self.conn.send(msg))
|
||||
loop.close()
|
||||
|
||||
def on_offer_created(self, promise, _, __):
|
||||
promise.wait()
|
||||
reply = promise.get_reply()
|
||||
offer = reply['offer']
|
||||
promise = Gst.Promise.new()
|
||||
self.webrtc.emit('set-local-description', offer, promise)
|
||||
promise.interrupt()
|
||||
self.send_sdp_offer(offer)
|
||||
|
||||
def on_negotiation_needed(self, element):
|
||||
promise = Gst.Promise.new_with_change_func(self.on_offer_created, element, None)
|
||||
element.emit('create-offer', None, promise)
|
||||
|
||||
def send_ice_candidate_message(self, _, mlineindex, candidate):
|
||||
icemsg = json.dumps({'ice': {'candidate': candidate, 'sdpMLineIndex': mlineindex}})
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.run_until_complete(self.conn.send(icemsg))
|
||||
loop.close()
|
||||
|
||||
def on_incoming_decodebin_stream(self, _, pad):
|
||||
if not pad.has_current_caps():
|
||||
print (pad, 'has no caps, ignoring')
|
||||
return
|
||||
|
||||
caps = pad.get_current_caps()
|
||||
assert (len(caps))
|
||||
s = caps[0]
|
||||
name = s.get_name()
|
||||
if name.startswith('video'):
|
||||
q = Gst.ElementFactory.make('queue')
|
||||
conv = Gst.ElementFactory.make('videoconvert')
|
||||
sink = Gst.ElementFactory.make('autovideosink')
|
||||
self.pipe.add(q, conv, sink)
|
||||
self.pipe.sync_children_states()
|
||||
pad.link(q.get_static_pad('sink'))
|
||||
q.link(conv)
|
||||
conv.link(sink)
|
||||
elif name.startswith('audio'):
|
||||
q = Gst.ElementFactory.make('queue')
|
||||
conv = Gst.ElementFactory.make('audioconvert')
|
||||
resample = Gst.ElementFactory.make('audioresample')
|
||||
sink = Gst.ElementFactory.make('autoaudiosink')
|
||||
self.pipe.add(q, conv, resample, sink)
|
||||
self.pipe.sync_children_states()
|
||||
pad.link(q.get_static_pad('sink'))
|
||||
q.link(conv)
|
||||
conv.link(resample)
|
||||
resample.link(sink)
|
||||
|
||||
def on_incoming_stream(self, _, pad):
|
||||
if pad.direction != Gst.PadDirection.SRC:
|
||||
return
|
||||
|
||||
decodebin = Gst.ElementFactory.make('decodebin')
|
||||
decodebin.connect('pad-added', self.on_incoming_decodebin_stream)
|
||||
self.pipe.add(decodebin)
|
||||
decodebin.sync_state_with_parent()
|
||||
self.webrtc.link(decodebin)
|
||||
|
||||
def start_pipeline(self):
|
||||
self.pipe = Gst.parse_launch(PIPELINE_DESC)
|
||||
self.webrtc = self.pipe.get_by_name('sendrecv')
|
||||
self.webrtc.connect('on-negotiation-needed', self.on_negotiation_needed)
|
||||
self.webrtc.connect('on-ice-candidate', self.send_ice_candidate_message)
|
||||
self.webrtc.connect('pad-added', self.on_incoming_stream)
|
||||
self.pipe.set_state(Gst.State.PLAYING)
|
||||
|
||||
def handle_sdp(self, message):
|
||||
assert (self.webrtc)
|
||||
msg = json.loads(message)
|
||||
if 'sdp' in msg:
|
||||
sdp = msg['sdp']
|
||||
assert(sdp['type'] == 'answer')
|
||||
sdp = sdp['sdp']
|
||||
print ('Received answer:\n%s' % sdp)
|
||||
res, sdpmsg = GstSdp.SDPMessage.new()
|
||||
GstSdp.sdp_message_parse_buffer(bytes(sdp.encode()), sdpmsg)
|
||||
answer = GstWebRTC.WebRTCSessionDescription.new(GstWebRTC.WebRTCSDPType.ANSWER, sdpmsg)
|
||||
promise = Gst.Promise.new()
|
||||
self.webrtc.emit('set-remote-description', answer, promise)
|
||||
promise.interrupt()
|
||||
elif 'ice' in msg:
|
||||
ice = msg['ice']
|
||||
candidate = ice['candidate']
|
||||
sdpmlineindex = ice['sdpMLineIndex']
|
||||
self.webrtc.emit('add-ice-candidate', sdpmlineindex, candidate)
|
||||
|
||||
def close_pipeline(self):
|
||||
self.pipe.set_state(Gst.State.NULL)
|
||||
self.pipe = None
|
||||
self.webrtc = None
|
||||
|
||||
async def loop(self):
|
||||
assert self.conn
|
||||
async for message in self.conn:
|
||||
if message == 'HELLO':
|
||||
await self.setup_call()
|
||||
elif message == 'SESSION_OK':
|
||||
self.start_pipeline()
|
||||
elif message.startswith('ERROR'):
|
||||
print (message)
|
||||
self.close_pipeline()
|
||||
return 1
|
||||
else:
|
||||
self.handle_sdp(message)
|
||||
self.close_pipeline()
|
||||
return 0
|
||||
|
||||
async def stop(self):
|
||||
if self.conn:
|
||||
await self.conn.close()
|
||||
self.conn = None
|
||||
|
||||
|
||||
def check_plugins():
|
||||
needed = ["opus", "vpx", "nice", "webrtc", "dtls", "srtp", "rtp",
|
||||
"rtpmanager", "videotestsrc", "audiotestsrc"]
|
||||
missing = list(filter(lambda p: Gst.Registry.get().find_plugin(p) is None, needed))
|
||||
if len(missing):
|
||||
print('Missing gstreamer plugins:', missing)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
Gst.init(None)
|
||||
if not check_plugins():
|
||||
sys.exit(1)
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('peerid', help='String ID of the peer to connect to')
|
||||
parser.add_argument('--server', help='Signalling server to connect to, eg "wss://127.0.0.1:8443"')
|
||||
args = parser.parse_args()
|
||||
our_id = random.randrange(10, 10000)
|
||||
c = WebRTCClient(our_id, args.peerid, args.server)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(c.connect())
|
||||
res = loop.run_until_complete(c.loop())
|
||||
sys.exit(res)
|
10
webrtc/sendrecv/js/Dockerfile
Normal file
10
webrtc/sendrecv/js/Dockerfile
Normal file
|
@ -0,0 +1,10 @@
|
|||
FROM nginx:latest
|
||||
|
||||
COPY . /usr/share/nginx/html
|
||||
|
||||
RUN sed -i 's/var default_peer_id;/var default_peer_id = 1;/g' \
|
||||
/usr/share/nginx/html/webrtc.js
|
||||
RUN sed -i 's/wss/ws/g' \
|
||||
/usr/share/nginx/html/webrtc.js
|
||||
|
||||
|
36
webrtc/sendrecv/js/index.html
Normal file
36
webrtc/sendrecv/js/index.html
Normal file
|
@ -0,0 +1,36 @@
|
|||
<!DOCTYPE html>
|
||||
<!--
|
||||
vim: set sts=2 sw=2 et :
|
||||
|
||||
|
||||
Demo Javascript app for negotiating and streaming a sendrecv webrtc stream
|
||||
with a GStreamer app. Runs only in passive mode, i.e., responds to offers
|
||||
with answers, exchanges ICE candidates, and streams.
|
||||
|
||||
Author: Nirbheek Chauhan <nirbheek@centricular.com>
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<style>
|
||||
.error { color: red; }
|
||||
</style>
|
||||
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
|
||||
<script src="webrtc.js"></script>
|
||||
<script>
|
||||
window.onload = websocketServerConnect;
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div><video id="stream" autoplay playsinline>Your browser doesn't support video</video></div>
|
||||
<div>Status: <span id="status">unknown</span></div>
|
||||
<div><textarea id="text" cols=40 rows=4></textarea></div>
|
||||
<div>Our id is <b id="peer-id">unknown</b></div>
|
||||
<br/>
|
||||
<div>
|
||||
<div>getUserMedia constraints being used:</div>
|
||||
<div><textarea id="constraints" cols=40 rows=4></textarea></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue