Move gstwebrtc-demos into gst-examples

Original repository location: https://github.com/centricular/gstwebrtc-demos
This commit is contained in:
Matthew Waters 2020-06-19 00:13:38 +10:00
commit a88e90fa9e
110 changed files with 15305 additions and 0 deletions

55
webrtc/.gitignore vendored Normal file
View 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
View 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
View 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
View file

@ -0,0 +1,5 @@
.externalNativeBuild/
assets/
gst-build-*/
src/main/java/org/freedesktop/gstreamer/GStreamer.java
src/main/java/org/freedesktop/gstreamer/androidmedia/

View 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'
}

View 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
View 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
View 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
View 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 *;
#}

View 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>

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View 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

View 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

View file

@ -0,0 +1 @@
/* This is needed purely to force linking libc++_shared */

View 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 (&current_jni_env, detach_current_thread);
return JNI_VERSION_1_4;
}

View 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>

View 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>

View 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
}

View 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
View 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
View 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

View file

@ -0,0 +1 @@
include ':app'

144
webrtc/check/basic.py Normal file
View 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
View 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

View 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`

View 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)

View file

View 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

View 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)

View 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)

View 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

View 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()

View file

@ -0,0 +1 @@
set-vars, local_bundle_policy=balanced, remote_bundle_policy=balanced

View file

@ -0,0 +1 @@
set-vars, local_bundle_policy=balanced, remote_bundle_policy=max-bundle

View file

@ -0,0 +1 @@
set-vars, local_bundle_policy=balanced, remote_bundle_policy=max-compat

View file

@ -0,0 +1 @@
set-vars, local_bundle_policy=max-bundle, remote_bundle_policy=balanced

View file

@ -0,0 +1 @@
set-vars, local_bundle_policy=max-bundle, remote_bundle_policy=max-bundle

View file

@ -0,0 +1 @@
set-vars, local_bundle_policy=max-bundle, remote_bundle_policy=max-compat

View file

@ -0,0 +1 @@
set-vars, local_bundle_policy=max-compat, remote_bundle_policy=balanced

View file

@ -0,0 +1 @@
set-vars, local_bundle_policy=max-compat, remote_bundle_policy=max-bundle

View file

@ -0,0 +1 @@
set-vars, local_bundle_policy=max-compat, remote_bundle_policy=max-compat

View file

@ -0,0 +1 @@
set-vars, local_bundle_policy=none, remote_bundle_policy=balanced

View file

@ -0,0 +1 @@
set-vars, local_bundle_policy=none, remote_bundle_policy=max-bundle

View file

@ -0,0 +1 @@
set-vars, local_bundle_policy=none, remote_bundle_policy=max-compat

View file

@ -0,0 +1 @@
set-vars, negotiation_initiator=local, negotiation_responder=remote

View 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"

View 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";

View file

@ -0,0 +1 @@
set-vars, negotiation_initiator=remote, negotiation_responder=local

View file

@ -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";

View 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"

View 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)

View 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

View 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>

View 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");
}

View 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
View 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

View 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
View 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')

File diff suppressed because it is too large Load diff

View 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"

View 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()
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
mp-webrtc-sendrecv

View 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 $@

View 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 ])

View 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;
}

View file

@ -0,0 +1 @@
subdir('gst')

11
webrtc/sendonly/Makefile Normal file
View 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 $@

View 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;
}

View 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;
}

View 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

View 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) }
}
}

Binary file not shown.

View 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
View 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
View 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

View 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);
}
});
}
}

View file

@ -0,0 +1 @@
/target/

6
webrtc/sendrecv/gst-rust/.gitignore vendored Normal file
View 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

File diff suppressed because it is too large Load diff

View 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"

View 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

View 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()
}

View 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()))
}

View 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();
}
}
}

View 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)

View 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())

View file

@ -0,0 +1,4 @@
[wrap-git]
directory=bindinator
url=https://github.com/GLibSharp/bindinator.git
revision=master

View file

@ -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

View file

@ -0,0 +1,4 @@
[wrap-git]
directory=gtk-sharp
url=https://github.com/gtk-sharp/gtk-sharp.git
revision=master

View 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

View 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 $@

View 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')

View 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;
}

View 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)

View 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

View 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