Simple android app

This commit is contained in:
Matthew Waters 2018-11-08 00:32:31 +11:00 committed by Matthew Waters
parent 7b8d466cbb
commit 421f21adb1
20 changed files with 2087 additions and 0 deletions

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,61 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "26.0.2"
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', 'armeabi'
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
}
afterEvaluate {
compileDebugJavaWithJavac.dependsOn 'externalNativeBuildDebug'
compileReleaseJavaWithJavac.dependsOn 'externalNativeBuildRelease'
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile '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,25 @@
<?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.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<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,51 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := gstwebrtc
LOCAL_SRC_FILES := webrtc.c
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 := $(GSTREAMER_PLUGINS_CORE) \
$(GSTREAMER_PLUGINS_PLAYBACK) \
$(GSTREAMER_PLUGINS_NET) \
$(GSTREAMER_PLUGINS_SYS) \
$(GSTREAMER_PLUGINS_CODECS_RESTRICTED) \
$(GSTREAMER_CODECS_GPL) \
$(GSTREAMER_PLUGINS_ENCODING) \
$(GSTREAMER_PLUGINS_VIS) \
$(GSTREAMER_PLUGINS_EFFECTS) \
$(GSTREAMER_PLUGINS_NET_RESTRICTED) \
subparse ogg theora vorbis opus ivorbisdec alaw apetag audioparsers auparse avi dv flac flv flxdec icydemux id3demux isomp4 jpeg lame matroska mpg123 mulaw multipart png speex taglib vpx wavenc wavpack wavparse y4menc adpcmdec adpcmenc dashdemux dvbsuboverlay dvdspu hls id3tag kate midi mxf openh264 opusparse pcapparse pnm rfbsrc siren smoothstreaming subenc videoparsersbad y4mdec jpegformat gdp rsvg openjpeg spandsp sbc \
nice androidmedia
# $(GSTREAMER_PLUGINS_CODECS)
GSTREAMER_EXTRA_DEPS := gstreamer-webrtc-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,3 @@
APP_PLATFORM = 15
APP_ABI = armeabi armeabi-v7a arm64-v8a x86 x86_64
#APP_ABI = armeabi-v7a arm64-v8a x86_64

View file

@ -0,0 +1,900 @@
/* 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;
gchar *desc;
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);
}
#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 "
"videotestsrc pattern=ball ! queue ! vp8enc deadline=1 error-resilient=default ! rtpvp8pay picture-id-mode=15-bit ! "
"queue ! " 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);
pad = gst_element_get_static_pad (webrtc->webrtcbin, "sink_0");
gst_util_set_object_arg (G_OBJECT (pad), "fec-type", "ulp-red");
g_object_set (pad, "do-nack", FALSE);
gst_object_unref (pad);
/* 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;
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 ();
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);
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);
}
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://10.6.5.229: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,25 @@
// 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.0.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
google()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

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

160
webrtc/android/gradlew vendored Executable 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/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

View file

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