init project
| @ -0,0 +1,10 @@ | ||||
| build | ||||
| .gradle | ||||
| .idea | ||||
| *.iml | ||||
| local.properties | ||||
| gradle.properties | ||||
| 
 | ||||
| app/release/ | ||||
| *.apk | ||||
| app/release/output.json | ||||
| @ -0,0 +1,34 @@ | ||||
| 
 | ||||
| // Top-level build file where you can add configuration options common to all sub-projects/modules. | ||||
| 
 | ||||
| buildscript { | ||||
|     repositories { | ||||
|         google() | ||||
|         jcenter() | ||||
|     } | ||||
|     dependencies { | ||||
|         classpath 'com.android.tools.build:gradle:7.1.2' | ||||
| 
 | ||||
|         // NOTE: Do not place your application dependencies here; they belong | ||||
|         // in the individual module build.gradle files | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| allprojects { | ||||
|     repositories { | ||||
|         google() | ||||
|         jcenter() | ||||
| 
 | ||||
|         maven { url "https://jitpack.io" } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| task clean(type: Delete) { | ||||
|     delete rootProject.buildDir | ||||
| } | ||||
| 
 | ||||
| gradle.projectsEvaluated { | ||||
|     tasks.withType(JavaCompile) { | ||||
|         options.compilerArgs << "-Xmaxerrs" << "500" // or whatever number you want | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,6 @@ | ||||
| #Sat Aug 17 10:22:30 CST 2019 | ||||
| distributionBase=GRADLE_USER_HOME | ||||
| distributionPath=wrapper/dists | ||||
| zipStoreBase=GRADLE_USER_HOME | ||||
| zipStorePath=wrapper/dists | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip | ||||
| @ -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 "$@" | ||||
| @ -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 | ||||
| @ -0,0 +1,2 @@ | ||||
| /build | ||||
| .apk | ||||
| @ -0,0 +1,40 @@ | ||||
| apply plugin: 'com.android.library' | ||||
| 
 | ||||
| android { | ||||
|     compileSdkVersion 31 | ||||
| 
 | ||||
|     defaultConfig { | ||||
|         minSdkVersion 19 | ||||
|         targetSdkVersion 31 // 确保在后台预览时不崩溃。。。 | ||||
|         versionCode 13190817 | ||||
|         versionName "1.3.19.0817" | ||||
|     } | ||||
|     buildTypes { | ||||
|         release { | ||||
|             minifyEnabled false | ||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| repositories { | ||||
|     flatDir { | ||||
|         dirs 'libs' | ||||
|     } | ||||
|     mavenCentral() | ||||
| } | ||||
| 
 | ||||
| dependencies { | ||||
|     implementation fileTree(include: ['*.jar'], dir: 'libs') | ||||
|     testImplementation 'junit:junit:4.13.2' | ||||
|     implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' | ||||
|     implementation 'androidx.lifecycle:lifecycle-reactivestreams:2.4.1' | ||||
|     annotationProcessor 'androidx.lifecycle:lifecycle-compiler:2.0.0' | ||||
| 
 | ||||
|     implementation(name: 'libuvccamera-release', ext: 'aar') { | ||||
|         exclude module: 'support-v4' | ||||
|         exclude module: 'appcompat-v7' | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,17 @@ | ||||
| # Add project specific ProGuard rules here. | ||||
| # By default, the flags in this file are appended to flags specified | ||||
| # in D:\AndroidStudio\StudioSDK/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 *; | ||||
| #} | ||||
| @ -0,0 +1 @@ | ||||
| rtmp | ||||
| @ -0,0 +1,19 @@ | ||||
| /* | ||||
| 	Copyright (c) 2013-2016 EasyDarwin.ORG.  All rights reserved. | ||||
| 	Github: https://github.com/EasyDarwin
 | ||||
| 	WEChat: EasyDarwin | ||||
| 	Website: http://www.easydarwin.org
 | ||||
| */ | ||||
| package org.easydarwin.easypusher; | ||||
| 
 | ||||
| import android.app.Application; | ||||
| import android.test.ApplicationTestCase; | ||||
| 
 | ||||
| /** | ||||
|  * <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a> | ||||
|  */ | ||||
| public class ApplicationTest extends ApplicationTestCase<Application> { | ||||
|     public ApplicationTest() { | ||||
|         super(Application.class); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,80 @@ | ||||
| package org.easydarwin.easypusher; | ||||
| 
 | ||||
| 
 | ||||
| import android.support.test.espresso.ViewInteraction; | ||||
| import android.support.test.rule.ActivityTestRule; | ||||
| import android.support.test.runner.AndroidJUnit4; | ||||
| import android.test.suitebuilder.annotation.LargeTest; | ||||
| 
 | ||||
| import org.junit.Rule; | ||||
| import org.junit.Test; | ||||
| import org.junit.runner.RunWith; | ||||
| 
 | ||||
| import static android.support.test.espresso.Espresso.onView; | ||||
| import static android.support.test.espresso.Espresso.pressBack; | ||||
| import static android.support.test.espresso.action.ViewActions.click; | ||||
| import static android.support.test.espresso.action.ViewActions.scrollTo; | ||||
| import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; | ||||
| import static android.support.test.espresso.matcher.ViewMatchers.withId; | ||||
| import static android.support.test.espresso.matcher.ViewMatchers.withParent; | ||||
| import static android.support.test.espresso.matcher.ViewMatchers.withText; | ||||
| import static org.hamcrest.Matchers.allOf; | ||||
| 
 | ||||
| @LargeTest | ||||
| @RunWith(AndroidJUnit4.class) | ||||
| public class SplashActivityTest { | ||||
| 
 | ||||
|     @Rule | ||||
|     public ActivityTestRule<SplashActivity> mActivityTestRule = new ActivityTestRule<>(SplashActivity.class); | ||||
| 
 | ||||
|     @Test | ||||
|     public void splashActivityTest() { | ||||
|         // Added a sleep statement to match the app's execution delay.
 | ||||
|         // The recommended way to handle such scenarios is to use Espresso idling resources:
 | ||||
|         // https://google.github.io/android-testing-support-library/docs/espresso/idling-resource/index.html
 | ||||
| 
 | ||||
| 
 | ||||
|         ViewInteraction appCompatButton = onView( | ||||
|                 allOf(withId(R.id.btn_switch), withText("推送"), isDisplayed())); | ||||
|         appCompatButton.perform(click()); | ||||
| 
 | ||||
|         ViewInteraction appCompatButton2 = onView( | ||||
|                 allOf(withId(R.id.btn_setting), withText("设置"), isDisplayed())); | ||||
|         appCompatButton2.perform(click()); | ||||
| 
 | ||||
|         pressBack(); | ||||
| 
 | ||||
|         ViewInteraction appCompatCheckBox = onView( | ||||
|                 allOf(withId(R.id.only_push_audio), withText("仅推送音频"))); | ||||
|         appCompatCheckBox.perform(scrollTo(), click()); | ||||
| 
 | ||||
|         ViewInteraction appCompatButton3 = onView( | ||||
|                 allOf(withId(R.id.btn_save), withText("保存"))); | ||||
|         appCompatButton3.perform(scrollTo(), click()); | ||||
| 
 | ||||
|         ViewInteraction appCompatButton4 = onView( | ||||
|                 allOf(withId(R.id.btn_setting), withText("设置"), isDisplayed())); | ||||
|         appCompatButton4.perform(click()); | ||||
| 
 | ||||
|         pressBack(); | ||||
| 
 | ||||
|         ViewInteraction appCompatCheckBox2 = onView( | ||||
|                 allOf(withId(R.id.only_push_audio), withText("仅推送音频"))); | ||||
|         appCompatCheckBox2.perform(scrollTo(), click()); | ||||
| 
 | ||||
|         ViewInteraction appCompatButton5 = onView( | ||||
|                 allOf(withId(R.id.btn_save), withText("保存"))); | ||||
|         appCompatButton5.perform(scrollTo(), click()); | ||||
| 
 | ||||
|         pressBack(); | ||||
| 
 | ||||
|         ViewInteraction appCompatButton6 = onView( | ||||
|                 allOf(withId(android.R.id.button2), withText("取消"), | ||||
|                         withParent(allOf(withId(R.id.buttonPanel), | ||||
|                                 withParent(withId(R.id.parentPanel)))), | ||||
|                         isDisplayed())); | ||||
|         appCompatButton6.perform(click()); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,38 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     package="org.easydarwin.easypusher"> | ||||
| 
 | ||||
|     <uses-permission android:name="android.permission.INTERNET" /> | ||||
|     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | ||||
|     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> | ||||
|     <uses-permission android:name="android.permission.CAMERA" /> | ||||
|     <uses-permission android:name="android.permission.RECORD_AUDIO" /> | ||||
|     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> | ||||
| 
 | ||||
|     <uses-feature android:name="android.hardware.camera" /> | ||||
| 
 | ||||
|     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | ||||
|     <uses-feature | ||||
|         android:name="android.hardware.usb.host" | ||||
|         android:required="true" /> | ||||
|     <uses-feature | ||||
|         android:glEsVersion="0x00020000" | ||||
|         android:required="true" /> | ||||
| 
 | ||||
|     <application> | ||||
|         <service | ||||
|             android:name="org.easydarwin.push.PushScreenService" | ||||
|             android:enabled="true" /> | ||||
| 
 | ||||
|         <service | ||||
|             android:name="org.easydarwin.push.UVCCameraService" | ||||
|             android:enabled="true" /> | ||||
| 
 | ||||
| 
 | ||||
|         <service | ||||
|             android:name="org.easydarwin.push.MediaStream" | ||||
|             android:enabled="true" /> | ||||
| 
 | ||||
|     </application> | ||||
| 
 | ||||
| </manifest> | ||||
| @ -0,0 +1,441 @@ | ||||
| package com.android.webrtc.audio; | ||||
| 
 | ||||
| /** | ||||
|  * This class supports the acoustic echo cancellation for mobile edition. Please <b>bug me</b> if you find any bugs in | ||||
|  * this toolkit.<br> | ||||
|  * <br> | ||||
|  * <b>[Notice]</b><br> | ||||
|  * 1. there are 5 more native interface that I'm not trying to provide in this MobileAEC toolkit.<br> | ||||
|  * But I think I should mention it out as a list below, for secondary development if necessary: <br> | ||||
|  * <ul> | ||||
|  * <li>WebRtc_Word32 WebRtcAecm_get_config(void *, AecmConfig *);</li> | ||||
|  * <li>WebRtc_Word32 WebRtcAecm_InitEchoPath(void* , const void* , size_t);</li> | ||||
|  * <li>WebRtc_Word32 WebRtcAecm_GetEchoPath(void* , void* , size_t);</li> | ||||
|  * <li>size_t WebRtcAecm_echo_path_size_bytes();</li> | ||||
|  * <li>WebRtc_Word32 WebRtcAecm_get_error_code(void *);</li> | ||||
|  * </ul> | ||||
|  * 2. if you are working on an android platform, put the shared library "libwebrtc_aecm.so"<br> | ||||
|  * into path "/your project/libs/armeabi/", if the dir does not exist, you should create it, otherwise you<br> | ||||
|  * will get a "unsatisfied link error" at run time.<br> | ||||
|  * 3. you should always call <b>close()</b> method <b>manually</b> when all things are finished.<br> | ||||
|  * <br> | ||||
|  * <b>[Usage]</b> <br> | ||||
|  * <ul> | ||||
|  * 1. You create a MobileAEC object first(set the parameters to constructor or null are both Ok, if null are set, then | ||||
|  * we will use default values instead).<br> | ||||
|  * 2. change the aggressiveness or sampling frequency of the AECM instance if necessary.<br> | ||||
|  * 3. call <b>prepare()</b> method to make the AECM instance prepared. <br> | ||||
|  * 4. then call "farendBuffer" to set far-end signal to AECM instance. <br> | ||||
|  * 5. now you call "echoCancellation()" to deal with the acoustic echo things.<br> | ||||
|  * The order of step 1,2,3,4 and 5 is significant, when all settings are done or you changed previous<br> | ||||
|  * settings, <b>DO NOT</b> forget to call prepare() method, otherwise your new settings will be ignored by AECM | ||||
|  * instance. <br> | ||||
|  * 6. finally you should call <b>close()</b> method <b>manually</b> when all things are done, after that, the AECM | ||||
|  * instance is no longer available until next <b>prepare()</b> is called.<br> | ||||
|  * </ul> | ||||
|  * <b>[Samples]</b><br> | ||||
|  * <ul> | ||||
|  * see doAECM() in {@link combillhoo.android.aec.demo.DemoActivity DEMO} | ||||
|  * </ul> | ||||
|  *  | ||||
|  * @version 0.1 2013-3-8 | ||||
|  *  | ||||
|  * @author billhoo E-mail:billhoo@126.com | ||||
|  */ | ||||
| public class MobileAEC { | ||||
|     static { | ||||
|         System.loadLibrary("webrtc_aecm"); // to load the libwebrtc_aecm.so library.
 | ||||
|     } | ||||
| 
 | ||||
|     // /////////////////////////////////////////////////////////
 | ||||
|     // PUBLIC CONSTANTS
 | ||||
| 
 | ||||
|     /** | ||||
|      * constant unable mode for Aecm configuration settings. | ||||
|      */ | ||||
|     public static final short AECM_UNABLE = 0; | ||||
| 
 | ||||
|     /** | ||||
|      * constant enable mode for Aecm configuration settings. | ||||
|      */ | ||||
|     public static final short AECM_ENABLE = 1; | ||||
| 
 | ||||
|     // /////////////////////////////////////////////////////////
 | ||||
|     // PUBLIC NESTED CLASSES
 | ||||
| 
 | ||||
|     /** | ||||
|      * For security reason, this class supports constant sampling frequency values in | ||||
|      * {@link SamplingFrequency#FS_8000Hz FS_8000Hz}, {@link SamplingFrequency#FS_16000Hz FS_16000Hz} | ||||
|      */ | ||||
|     public static final class SamplingFrequency { | ||||
|         public long getFS() { | ||||
|             return mSamplingFrequency; | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * This constant represents sampling frequency in 8000Hz | ||||
|          */ | ||||
|         public static final SamplingFrequency FS_8000Hz  = new SamplingFrequency( | ||||
|                                                                  8000); | ||||
| 
 | ||||
|         /** | ||||
|          * This constant represents sampling frequency in 16000Hz | ||||
|          */ | ||||
|         public static final SamplingFrequency FS_16000Hz = new SamplingFrequency( | ||||
|                                                                  16000); | ||||
| 
 | ||||
|         private final long                    mSamplingFrequency; | ||||
| 
 | ||||
|         private SamplingFrequency(long fs) { | ||||
|             this.mSamplingFrequency = fs; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * For security reason, this class supports constant aggressiveness of the AECM instance in | ||||
|      * {@link AggressiveMode#MILD MILD}, {@link AggressiveMode#MEDIUM MEDIUM}, {@link AggressiveMode#HIGH HIGH}, | ||||
|      * {@link AggressiveMode#AGGRESSIVE AGGRESSIVE}, {@link AggressiveMode#MOST_AGGRESSIVE MOST_AGGRESSIVE}. | ||||
|      */ | ||||
|     public static final class AggressiveMode { | ||||
|         public int getMode() { | ||||
|             return mMode; | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * This constant represents the aggressiveness of the AECM instance in MILD_MODE | ||||
|          */ | ||||
|         public static final AggressiveMode MILD            = new AggressiveMode( | ||||
|                                                                    0); | ||||
| 
 | ||||
|         /** | ||||
|          * This constant represents the aggressiveness of the AECM instance in MEDIUM_MODE | ||||
|          */ | ||||
|         public static final AggressiveMode MEDIUM          = new AggressiveMode( | ||||
|                                                                    1); | ||||
| 
 | ||||
|         /** | ||||
|          * This constant represents the aggressiveness of the AECM instance in HIGH_MODE | ||||
|          */ | ||||
|         public static final AggressiveMode HIGH            = new AggressiveMode( | ||||
|                                                                    2); | ||||
| 
 | ||||
|         /** | ||||
|          * This constant represents the aggressiveness of the AECM instance in AGGRESSIVE_MODE | ||||
|          */ | ||||
|         public static final AggressiveMode AGGRESSIVE      = new AggressiveMode( | ||||
|                                                                    3); | ||||
| 
 | ||||
|         /** | ||||
|          * This constant represents the aggressiveness of the AECM instance in MOST_AGGRESSIVE_MODE | ||||
|          */ | ||||
|         public static final AggressiveMode MOST_AGGRESSIVE = new AggressiveMode( | ||||
|                                                                    4); | ||||
| 
 | ||||
|         private final int                  mMode; | ||||
| 
 | ||||
|         public AggressiveMode(int mode) { | ||||
|             mMode = mode; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // /////////////////////////////////////////////////////////
 | ||||
|     // PRIVATE MEMBERS
 | ||||
| 
 | ||||
|     private int               mAecmHandler = -1;   // the handler of AECM instance.
 | ||||
|     private AecmConfig        mAecmConfig  = null; // the configurations of AECM instance.
 | ||||
|     private SamplingFrequency mSampFreq    = null; // sampling frequency of input speech data.
 | ||||
|     private boolean           mIsInit      = false; // whether the AECM instance is initialized or not.
 | ||||
| 
 | ||||
|     // /////////////////////////////////////////////////////////
 | ||||
|     // CONSTRUCTOR
 | ||||
| 
 | ||||
|     /** | ||||
|      * To generate a new AECM instance, whether you set the sampling frequency of each parameter or not are both ok. | ||||
|      *  | ||||
|      * @param sampFreqOfData | ||||
|      *            - sampling frequency of input audio data. if null, then {@link SamplingFrequency#FS_16000Hz | ||||
|      *            FS_16000Hz} is set. | ||||
|      */ | ||||
|     public MobileAEC(SamplingFrequency sampFreqOfData) { | ||||
|         setSampFreq(sampFreqOfData); | ||||
|         mAecmConfig = new AecmConfig(); | ||||
| 
 | ||||
|         // create new AECM instance but without initialize. Init things are in prepare() method instead.
 | ||||
|         mAecmHandler = nativeCreateAecmInstance(); | ||||
|     } | ||||
| 
 | ||||
|     // /////////////////////////////////////////////////////////
 | ||||
|     // PUBLIC METHODS
 | ||||
| 
 | ||||
|     /** | ||||
|      * set the sampling rate of speech data. | ||||
|      *  | ||||
|      * @param fs | ||||
|      *            - sampling frequency of speech data, if null then {@link SamplingFrequency#FS_16000Hz FS_16000Hz} is | ||||
|      *            set. | ||||
|      */ | ||||
|     public void setSampFreq(SamplingFrequency fs) { | ||||
|         if (fs == null) | ||||
|             mSampFreq = SamplingFrequency.FS_16000Hz; | ||||
|         else | ||||
|             mSampFreq = fs; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * set the far-end signal of AECM instance. | ||||
|      *  | ||||
|      * @param farendBuf | ||||
|      * @param numOfSamples | ||||
|      * @return the {@link MobileAEC MobileAEC} object itself. | ||||
|      * @throws Exception | ||||
|      *             - if farendBuffer() is called on an unprepared AECM instance or you pass an invalid parameter.<br> | ||||
|      */ | ||||
|     public MobileAEC farendBuffer(short[] farendBuf, int numOfSamples) | ||||
|             throws Exception { | ||||
|         // check if AECM instance is not initialized.
 | ||||
|         if (!mIsInit) { | ||||
|             // TODO(billhoo) - create a custom exception instead of using java.lang.Exception
 | ||||
|             throw new Exception( | ||||
|                     "setFarendBuffer() called on an unprepared AECM instance."); | ||||
|         } | ||||
| 
 | ||||
|         if (nativeBufferFarend(mAecmHandler, farendBuf, numOfSamples) == -1) | ||||
|             // TODO(billhoo) - create a custom exception instead of using java.lang.Exception
 | ||||
|             throw new Exception( | ||||
|                     "setFarendBuffer() failed due to invalid arguments."); | ||||
| 
 | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * core process of AECM instance, must called on a prepared AECM instance. we only support 80 or 160 sample blocks | ||||
|      * of data. | ||||
|      *  | ||||
|      * @param nearendNoisy | ||||
|      *            - In buffer containing one frame of reference nearend+echo signal. If noise reduction is active, | ||||
|      *            provide the noisy signal here. | ||||
|      * @param nearendClean | ||||
|      *            - In buffer containing one frame of nearend+echo signal. If noise reduction is active, provide the | ||||
|      *            clean signal here. Otherwise pass a NULL pointer. | ||||
|      * @param out | ||||
|      *            - Out buffer, one frame of processed nearend. | ||||
|      * @param numOfSamples | ||||
|      *            - Number of samples in nearend buffer | ||||
|      * @param delay | ||||
|      *            - Delay estimate for sound card and system buffers <br> | ||||
|      *            delay = (t_render - t_analyze) + (t_process - t_capture)<br> | ||||
|      *            where<br> | ||||
|      *            - t_analyze is the time a frame is passed to farendBuffer() and t_render is the time the first sample | ||||
|      *            of the same frame is rendered by the audio hardware.<br> | ||||
|      *            - t_capture is the time the first sample of a frame is captured by the audio hardware and t_process is | ||||
|      *            the time the same frame is passed to echoCancellation(). | ||||
|      *  | ||||
|      * @throws Exception | ||||
|      *             - if echoCancellation() is called on an unprepared AECM instance or you pass an invalid parameter.<br> | ||||
|      */ | ||||
|     public void echoCancellation(short[] nearendNoisy, short[] nearendClean, | ||||
|             short[] out, short numOfSamples, short delay) throws Exception { | ||||
|         // check if AECM instance is not initialized.
 | ||||
|         if (!mIsInit) { | ||||
|             // TODO(billhoo) - create a custom exception instead of using java.lang.Exception
 | ||||
|             throw new Exception( | ||||
|                     "echoCancelling() called on an unprepared AECM instance."); | ||||
|         } | ||||
| 
 | ||||
|         if (nativeAecmProcess(mAecmHandler, nearendNoisy, nearendClean, out, | ||||
|                 numOfSamples, delay) == -1) | ||||
|             // TODO(billhoo) - create a custom exception instead of using java.lang.Exception
 | ||||
|             throw new Exception( | ||||
|                     "echoCancellation() failed due to invalid arguments."); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the aggressiveness mode of AECM instance, more higher the mode is, more aggressive the instance will be. | ||||
|      *  | ||||
|      * @param mode | ||||
|      * @return the {@link MobileAEC MobileAEC} object itself. | ||||
|      * @throws NullPointerException | ||||
|      *             - if mode is null. | ||||
|      */ | ||||
|     public MobileAEC setAecmMode(AggressiveMode mode) | ||||
|             throws NullPointerException { | ||||
|         // check the mode argument.
 | ||||
|         if (mode == null) | ||||
|             throw new NullPointerException( | ||||
|                     "setAecMode() failed due to null argument."); | ||||
| 
 | ||||
|         mAecmConfig.mAecmMode = (short) mode.getMode(); | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * When finished the pre-works or any settings are changed, call this to make AECM instance prepared. Otherwise your | ||||
|      * new settings will be ignored by the AECM instance. | ||||
|      *  | ||||
|      * @return the {@link MobileAEC MobileAEC} object itself. | ||||
|      */ | ||||
|     public MobileAEC prepare() { | ||||
|         if (mIsInit) { | ||||
|             close(); | ||||
|             mAecmHandler = nativeCreateAecmInstance(); | ||||
|         } | ||||
| 
 | ||||
|         mInitAecmInstance((int) mSampFreq.getFS()); | ||||
|         mIsInit = true; | ||||
| 
 | ||||
|         // set AecConfig to native side.
 | ||||
|         nativeSetConfig(mAecmHandler, mAecmConfig); | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Release the resources in AECM instance and the AECM instance is no longer available until next <b>prepare()</b> | ||||
|      * is called.<br> | ||||
|      * You should <b>always</b> call this <b>manually</b> when all things are done. | ||||
|      */ | ||||
|     public void close() { | ||||
|         if (mIsInit) { | ||||
|             nativeFreeAecmInstance(mAecmHandler); | ||||
|             mAecmHandler = -1; | ||||
|             mIsInit = false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // ////////////////////////////////////////////////////////
 | ||||
|     // PROTECTED METHODS
 | ||||
| 
 | ||||
|     @Override | ||||
|     protected void finalize() throws Throwable { | ||||
|         super.finalize(); | ||||
|         // TODO(billhoo) need a safety one.
 | ||||
|         if (mIsInit) { | ||||
|             close(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // ////////////////////////////////////////////////////////
 | ||||
|     // PRIVATE METHODS
 | ||||
| 
 | ||||
|     /** | ||||
|      * initialize the AECM instance | ||||
|      *  | ||||
|      * @param SampFreq | ||||
|      */ | ||||
|     private void mInitAecmInstance(int SampFreq) { | ||||
|         if (!mIsInit) { | ||||
|             nativeInitializeAecmInstance(mAecmHandler, SampFreq); | ||||
| 
 | ||||
|             // initialize configurations of AECM instance.
 | ||||
|             mAecmConfig = new AecmConfig(); | ||||
| 
 | ||||
|             // set default configuration of AECM instance
 | ||||
|             nativeSetConfig(mAecmHandler, mAecmConfig); | ||||
| 
 | ||||
|             mIsInit = true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // ////////////////////////////////////////////////////////
 | ||||
|     // PRIVATE NESTED CLASSES
 | ||||
| 
 | ||||
|     /** | ||||
|      * Acoustic Echo Cancellation for Mobile Configuration class, holds the config Info. of AECM instance.<br> | ||||
|      * [NOTE] <b>DO NOT</b> modify the name of members, or you must change the native code to match your modifying. | ||||
|      * Otherwise the native code could not find pre-binding members name.<br> | ||||
|      *  | ||||
|      */ | ||||
|     @SuppressWarnings("unused") | ||||
|     public class AecmConfig { | ||||
|         private short mAecmMode = (short) AggressiveMode.AGGRESSIVE.getMode(); // default AggressiveMode.AGGRESSIVE
 | ||||
|         private short mCngMode  = AECM_ENABLE;                                // AECM_UNABLE, AECM_ENABLE (default)
 | ||||
|     } | ||||
| 
 | ||||
|     // ///////////////////////////////////////////
 | ||||
|     // PRIVATE NATIVE INTERFACES
 | ||||
| 
 | ||||
|     /** | ||||
|      * Allocates the memory needed by the AECM. The memory needs to be initialized separately using the | ||||
|      * nativeInitializeAecmInstance() method. | ||||
|      *  | ||||
|      * @return -1: error<br> | ||||
|      *         other values: created AECM instance handler. | ||||
|      *  | ||||
|      */ | ||||
|     private static native int nativeCreateAecmInstance(); | ||||
| 
 | ||||
|     /** | ||||
|      * Release the memory allocated by nativeCreateAecmInstance(). | ||||
|      *  | ||||
|      * @param aecmHandler | ||||
|      *            - handler of the AECM instance created by nativeCreateAecmInstance() | ||||
|      * @return 0: OK<br> | ||||
|      *         -1: error | ||||
|      */ | ||||
|     private static native int nativeFreeAecmInstance(int aecmHandler); | ||||
| 
 | ||||
|     /** | ||||
|      * Initializes an AECM instance. | ||||
|      *  | ||||
|      * @param aecmHandler | ||||
|      *            - Handler of AECM instance | ||||
|      * @param samplingFrequency | ||||
|      *            - Sampling frequency of data | ||||
|      * @return: 0: OK<br> | ||||
|      *          -1: error | ||||
|      */ | ||||
|     private static native int nativeInitializeAecmInstance(int aecmHandler, | ||||
|             int samplingFrequency); | ||||
| 
 | ||||
|     /** | ||||
|      * Inserts an 80 or 160 sample block of data into the farend buffer. | ||||
|      *  | ||||
|      * @param aecmHandler | ||||
|      *            - Handler to the AECM instance | ||||
|      * @param farend | ||||
|      *            - In buffer containing one frame of farend signal for L band | ||||
|      * @param nrOfSamples | ||||
|      *            - Number of samples in farend buffer | ||||
|      * @return: 0: OK<br> | ||||
|      *          -1: error | ||||
|      */ | ||||
|     private static native int nativeBufferFarend(int aecmHandler, | ||||
|             short[] farend, int nrOfSamples); | ||||
| 
 | ||||
|     /** | ||||
|      * Runs the AECM on an 80 or 160 sample blocks of data. | ||||
|      *  | ||||
|      * @param aecmHandler | ||||
|      *            - Handler to the AECM handler | ||||
|      * @param nearendNoisy | ||||
|      *            - In buffer containing one frame of reference nearend+echo signal. If noise reduction is active, | ||||
|      *            provide the noisy signal here. | ||||
|      * @param nearendClean | ||||
|      *            - In buffer containing one frame of nearend+echo signal. If noise reduction is active, provide the | ||||
|      *            clean signal here.Otherwise pass a NULL pointer. | ||||
|      * @param out | ||||
|      *            - Out buffer, one frame of processed nearend. | ||||
|      * @param nrOfSamples | ||||
|      *            - Number of samples in nearend buffer | ||||
|      * @param msInSndCardBuf | ||||
|      *            - Delay estimate for sound card and system buffers <br> | ||||
|      * @return: 0: OK<br> | ||||
|      *          -1: error | ||||
|      */ | ||||
|     private static native int nativeAecmProcess(int aecmHandler, | ||||
|             short[] nearendNoisy, short[] nearendClean, short[] out, | ||||
|             short nrOfSamples, short msInSndCardBuf); | ||||
| 
 | ||||
|     /** | ||||
|      * Enables the user to set certain parameters on-the-fly. | ||||
|      *  | ||||
|      * @param aecmHandler | ||||
|      *            - Handler to the AECM instance | ||||
|      * @param aecmConfig | ||||
|      *            - the new configuration of AECM instance to set. | ||||
|      *  | ||||
|      * @return 0: OK<br> | ||||
|      *         -1: error | ||||
|      */ | ||||
|     private static native int nativeSetConfig(int aecmHandler, | ||||
|             AecmConfig aecmConfig); | ||||
| } | ||||
| @ -0,0 +1,244 @@ | ||||
| package org.easydarwin.audio; | ||||
| 
 | ||||
| import android.media.AudioFormat; | ||||
| import android.media.AudioRecord; | ||||
| import android.media.MediaCodec; | ||||
| import android.media.MediaCodecInfo; | ||||
| import android.media.MediaFormat; | ||||
| import android.media.MediaRecorder; | ||||
| import android.os.Process; | ||||
| import androidx.annotation.Nullable; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import org.easydarwin.easypusher.BuildConfig; | ||||
| import org.easydarwin.muxer.EasyMuxer; | ||||
| import org.easydarwin.push.Pusher; | ||||
| 
 | ||||
| import java.nio.ByteBuffer; | ||||
| 
 | ||||
| 
 | ||||
| public class AudioStream { | ||||
|     EasyMuxer muxer; | ||||
|     private int samplingRate = 8000; | ||||
|     private int bitRate = 16000; | ||||
|     private int BUFFER_SIZE = 1920; | ||||
|     int mSamplingRateIndex = 0; | ||||
|     AudioRecord mAudioRecord; | ||||
|     MediaCodec mMediaCodec; | ||||
|     Pusher easyPusher; | ||||
|     private Thread mThread = null; | ||||
|     String TAG = "AudioStream"; | ||||
|     //final String path = Environment.getExternalStorageDirectory() + "/123450001.aac";
 | ||||
| 
 | ||||
|     protected MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo(); | ||||
|     protected ByteBuffer[] mBuffers = null; | ||||
| 
 | ||||
|     /** | ||||
|      * There are 13 supported frequencies by ADTS. | ||||
|      **/ | ||||
|     public static final int[] AUDIO_SAMPLING_RATES = {96000, // 0
 | ||||
|             88200, // 1
 | ||||
|             64000, // 2
 | ||||
|             48000, // 3
 | ||||
|             44100, // 4
 | ||||
|             32000, // 5
 | ||||
|             24000, // 6
 | ||||
|             22050, // 7
 | ||||
|             16000, // 8
 | ||||
|             12000, // 9
 | ||||
|             11025, // 10
 | ||||
|             8000, // 11
 | ||||
|             7350, // 12
 | ||||
|             -1, // 13
 | ||||
|             -1, // 14
 | ||||
|             -1, // 15
 | ||||
|     }; | ||||
|     private Thread mWriter; | ||||
|     private MediaFormat newFormat; | ||||
| 
 | ||||
|     public AudioStream(Pusher easyPusher) { | ||||
|         this.easyPusher = easyPusher; | ||||
|         int i = 0; | ||||
|         for (; i < AUDIO_SAMPLING_RATES.length; i++) { | ||||
|             if (AUDIO_SAMPLING_RATES[i] == samplingRate) { | ||||
|                 mSamplingRateIndex = i; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 编码 | ||||
|      */ | ||||
|     public void startRecord() { | ||||
|         mThread = new Thread(new Runnable() { | ||||
|             @Override | ||||
|             public void run() { | ||||
|                 Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO); | ||||
|                 int len = 0, bufferIndex = 0; | ||||
|                 try { | ||||
|                     int bufferSize = AudioRecord.getMinBufferSize(samplingRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); | ||||
|                     mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, samplingRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize); | ||||
|                     mMediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm"); | ||||
|                     MediaFormat format = new MediaFormat(); | ||||
|                     format.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm"); | ||||
|                     format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); | ||||
|                     format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); | ||||
|                     format.setInteger(MediaFormat.KEY_SAMPLE_RATE, samplingRate); | ||||
|                     format.setInteger(MediaFormat.KEY_AAC_PROFILE, | ||||
|                             MediaCodecInfo.CodecProfileLevel.AACObjectLC); | ||||
|                     format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, BUFFER_SIZE); | ||||
|                     mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); | ||||
|                     mMediaCodec.start(); | ||||
| 
 | ||||
| 
 | ||||
|                     mWriter = new WriterThread(); | ||||
|                     mWriter.start(); | ||||
|                     mAudioRecord.startRecording(); | ||||
|                     final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers(); | ||||
| 
 | ||||
|                     long presentationTimeUs = 0; | ||||
|                     while (mThread != null) { | ||||
|                         bufferIndex = mMediaCodec.dequeueInputBuffer(1000); | ||||
|                         if (bufferIndex >= 0) { | ||||
|                             inputBuffers[bufferIndex].clear(); | ||||
|                             len = mAudioRecord.read(inputBuffers[bufferIndex], BUFFER_SIZE); | ||||
|                             long timeUs = System.nanoTime() / 1000; | ||||
| //                            Log.i(TAG, String.format("audio: %d [%d] ", timeUs, timeUs - presentationTimeUs));
 | ||||
|                             presentationTimeUs = timeUs; | ||||
|                             if (len == AudioRecord.ERROR_INVALID_OPERATION || len == AudioRecord.ERROR_BAD_VALUE) { | ||||
|                                 mMediaCodec.queueInputBuffer(bufferIndex, 0, 0, presentationTimeUs, 0); | ||||
|                             } else { | ||||
|                                 mMediaCodec.queueInputBuffer(bufferIndex, 0, len, presentationTimeUs, 0); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } catch (Exception e) { | ||||
|                     Log.e(TAG, "Record___Error!!!!!"); | ||||
|                     e.printStackTrace(); | ||||
|                 } finally { | ||||
|                     Thread t = mWriter; | ||||
|                     mWriter = null; | ||||
|                     while (t != null && t.isAlive()) { | ||||
|                         try { | ||||
|                             t.interrupt(); | ||||
|                             t.join(); | ||||
|                         } catch (InterruptedException e) { | ||||
|                             e.printStackTrace(); | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     try { | ||||
|                         if (mAudioRecord != null) { | ||||
|                             mAudioRecord.stop(); | ||||
|                             mAudioRecord.release(); | ||||
|                             mAudioRecord = null; | ||||
|                         } | ||||
|                     } catch (Throwable ex) { | ||||
|                         ex.printStackTrace(); | ||||
|                     } | ||||
| 
 | ||||
|                     try { | ||||
|                         if (mMediaCodec != null) { | ||||
|                             mMediaCodec.stop(); | ||||
|                             mMediaCodec.release(); | ||||
|                             mMediaCodec = null; | ||||
|                         } | ||||
|                     } catch (Throwable ex) { | ||||
|                         ex.printStackTrace(); | ||||
|                     } | ||||
| 
 | ||||
|                 } | ||||
|             } | ||||
|         }, "AACRecoder"); | ||||
|         mThread.start(); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public synchronized void setMuxer(EasyMuxer muxer) { | ||||
|         if (muxer != null) { | ||||
|             if (newFormat != null) | ||||
|                 muxer.addTrack(newFormat, false); | ||||
|         } | ||||
|         this.muxer = muxer; | ||||
|     } | ||||
| 
 | ||||
|     private class WriterThread extends Thread { | ||||
| 
 | ||||
| 
 | ||||
|         @Override | ||||
|         public void run() { | ||||
|             int index = 0; | ||||
|             if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { | ||||
|             } else { | ||||
|                 mBuffers = mMediaCodec.getOutputBuffers(); | ||||
|             } | ||||
|             ByteBuffer mBuffer = ByteBuffer.allocate(10240); | ||||
|             do { | ||||
|                 index = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10000); | ||||
|                 if (index >= 0) { | ||||
|                     if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) { | ||||
|                         continue; | ||||
|                     } | ||||
|                     mBuffer.clear(); | ||||
|                     ByteBuffer outputBuffer = null; | ||||
|                     if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { | ||||
|                         outputBuffer = mMediaCodec.getOutputBuffer(index); | ||||
|                     } else { | ||||
|                         outputBuffer = mBuffers[index]; | ||||
|                     } | ||||
| 
 | ||||
|                     if (muxer != null) | ||||
|                         muxer.pumpStream(outputBuffer, mBufferInfo, false); | ||||
|                     outputBuffer.get(mBuffer.array(), 7, mBufferInfo.size); | ||||
|                     outputBuffer.clear(); | ||||
|                     mBuffer.position(7 + mBufferInfo.size); | ||||
|                     addADTStoPacket(mBuffer.array(), mBufferInfo.size + 7); | ||||
|                     mBuffer.flip(); | ||||
|                     easyPusher.push(mBuffer.array(), 0, mBufferInfo.size + 7, mBufferInfo.presentationTimeUs / 1000, 0); | ||||
|                     if (BuildConfig.DEBUG) | ||||
|                         Log.i(TAG, String.format("push audio stamp:%d", mBufferInfo.presentationTimeUs / 1000)); | ||||
|                     mMediaCodec.releaseOutputBuffer(index, false); | ||||
|                 } else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { | ||||
|                     mBuffers = mMediaCodec.getOutputBuffers(); | ||||
|                 } else if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { | ||||
|                     synchronized (AudioStream.this) { | ||||
|                         Log.v(TAG, "output format changed..."); | ||||
|                         newFormat = mMediaCodec.getOutputFormat(); | ||||
|                         if (muxer != null) | ||||
|                             muxer.addTrack(newFormat, false); | ||||
|                     } | ||||
|                 } else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) { | ||||
| //                    Log.v(TAG, "No buffer available...");
 | ||||
|                 } else { | ||||
|                     Log.e(TAG, "Message: " + index); | ||||
|                 } | ||||
|             } while (mWriter != null); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void addADTStoPacket(byte[] packet, int packetLen) { | ||||
|         packet[0] = (byte) 0xFF; | ||||
|         packet[1] = (byte) 0xF1; | ||||
|         packet[2] = (byte) (((2 - 1) << 6) + (mSamplingRateIndex << 2) + (1 >> 2)); | ||||
|         packet[3] = (byte) (((1 & 3) << 6) + (packetLen >> 11)); | ||||
|         packet[4] = (byte) ((packetLen & 0x7FF) >> 3); | ||||
|         packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F); | ||||
|         packet[6] = (byte) 0xFC; | ||||
|     } | ||||
| 
 | ||||
|     public void stop() { | ||||
|         try { | ||||
|             Thread t = mThread; | ||||
|             mThread = null; | ||||
|             if (t != null) { | ||||
|                 t.interrupt(); | ||||
|                 t.join(); | ||||
|             } | ||||
|         } catch (InterruptedException e) { | ||||
|             e.fillInStackTrace(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,8 @@ | ||||
| package org.easydarwin.bus; | ||||
| 
 | ||||
| /** | ||||
|  * Created by apple on 2017/7/21. | ||||
|  */ | ||||
| 
 | ||||
| public class StartRecord { | ||||
| } | ||||
| @ -0,0 +1,8 @@ | ||||
| package org.easydarwin.bus; | ||||
| 
 | ||||
| /** | ||||
|  * Created by apple on 2017/7/21. | ||||
|  */ | ||||
| 
 | ||||
| public class StopRecord { | ||||
| } | ||||
| @ -0,0 +1,14 @@ | ||||
| package org.easydarwin.bus; | ||||
| 
 | ||||
| /** | ||||
|  * Created by apple on 2017/5/14. | ||||
|  */ | ||||
| 
 | ||||
| public class StreamStat { | ||||
|     public final int fps, bps; | ||||
| 
 | ||||
|     public StreamStat(int fps, int bps) { | ||||
|         this.fps = fps; | ||||
|         this.bps = bps; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,8 @@ | ||||
| package org.easydarwin.bus; | ||||
| 
 | ||||
| /** | ||||
|  * Created by apple on 2017/8/29. | ||||
|  */ | ||||
| 
 | ||||
| public class SupportResolution { | ||||
| } | ||||
| @ -0,0 +1,112 @@ | ||||
| package org.easydarwin.easypusher; | ||||
| 
 | ||||
| import android.app.Application; | ||||
| import android.content.SharedPreferences; | ||||
| import android.content.res.AssetManager; | ||||
| import android.preference.PreferenceManager; | ||||
| 
 | ||||
| import org.easydarwin.config.Config; | ||||
| 
 | ||||
| import java.io.File; | ||||
| import java.io.FileOutputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| 
 | ||||
| public class EasyApplication extends Application { | ||||
| 
 | ||||
|     public static final String KEY_ENABLE_VIDEO = "key-enable-video"; | ||||
|     private static EasyApplication mApplication; | ||||
| 
 | ||||
| 
 | ||||
|     public long mRecordingBegin; | ||||
|     public boolean mRecording; | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate() { | ||||
|         super.onCreate(); | ||||
|         mApplication = this; | ||||
|         // for compatibility
 | ||||
|         resetDefaultServer(); | ||||
|         File youyuan = getFileStreamPath("SIMYOU.ttf"); | ||||
|         if (!youyuan.exists()){ | ||||
|             AssetManager am = getAssets(); | ||||
|             try { | ||||
|                 InputStream is = am.open("zk/SIMYOU.ttf"); | ||||
|                 FileOutputStream os = openFileOutput("SIMYOU.ttf", MODE_PRIVATE); | ||||
|                 byte[] buffer = new byte[1024]; | ||||
|                 int len = 0; | ||||
|                 while ((len = is.read(buffer)) != -1) { | ||||
|                     os.write(buffer, 0, len); | ||||
|                 } | ||||
|                 os.close(); | ||||
|                 is.close(); | ||||
| 
 | ||||
|             } catch (IOException e) { | ||||
|                 e.printStackTrace(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private void resetDefaultServer() { | ||||
|         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); | ||||
|         String defaultIP = sharedPreferences.getString(Config.SERVER_IP, Config.DEFAULT_SERVER_IP); | ||||
|         if ("114.55.107.180".equals(defaultIP) | ||||
|                 || "121.40.50.44".equals(defaultIP) | ||||
|                 || "www.easydarwin.org".equals(defaultIP)){ | ||||
|             sharedPreferences.edit().putString(Config.SERVER_IP, Config.DEFAULT_SERVER_IP).apply(); | ||||
|         } | ||||
| 
 | ||||
|         String defaultRtmpURL = sharedPreferences.getString(Config.SERVER_URL, Config.DEFAULT_SERVER_URL); | ||||
|         int result1 = defaultRtmpURL.indexOf("rtmp://www.easydss.com/live"); | ||||
|         int result2 = defaultRtmpURL.indexOf("rtmp://121.40.50.44/live"); | ||||
|         if(result1 != -1 || result2 != -1){ | ||||
|             sharedPreferences.edit().putString(Config.SERVER_URL, Config.DEFAULT_SERVER_URL).apply(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static EasyApplication getEasyApplication() { | ||||
|         return mApplication; | ||||
|     } | ||||
| 
 | ||||
|     public void saveStringIntoPref(String key, String value) { | ||||
|         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); | ||||
|         SharedPreferences.Editor editor = sharedPreferences.edit(); | ||||
|         editor.putString(key, value); | ||||
|         editor.commit(); | ||||
|     } | ||||
| 
 | ||||
|     public String getIp() { | ||||
|         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); | ||||
|         String ip = sharedPreferences.getString(Config.SERVER_IP, Config.DEFAULT_SERVER_IP); | ||||
|         return ip; | ||||
|     } | ||||
| 
 | ||||
|     public String getPort() { | ||||
|         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); | ||||
|         String port = sharedPreferences.getString(Config.SERVER_PORT, Config.DEFAULT_SERVER_PORT); | ||||
|         return port; | ||||
|     } | ||||
| 
 | ||||
|     public String getId() { | ||||
|         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); | ||||
|         String id = sharedPreferences.getString(Config.STREAM_ID, Config.DEFAULT_STREAM_ID); | ||||
|         if (!id.contains(Config.STREAM_ID_PREFIX)) { | ||||
|             id = Config.STREAM_ID_PREFIX + id; | ||||
|         } | ||||
|         saveStringIntoPref(Config.STREAM_ID, id); | ||||
|         return id; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public String getUrl() { | ||||
|         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); | ||||
|         String defValue = Config.DEFAULT_SERVER_URL; | ||||
|         String ip = sharedPreferences.getString(Config.SERVER_URL, defValue); | ||||
|         if (ip.equals(defValue)){ | ||||
|             sharedPreferences.edit().putString(Config.SERVER_URL, defValue).apply(); | ||||
|         } | ||||
|         return ip; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,165 @@ | ||||
| /* | ||||
|  * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com | ||||
|  *  | ||||
|  * This file is part of Spydroid (http://code.google.com/p/spydroid-ipcamera/)
 | ||||
|  *  | ||||
|  * Spydroid is free software; you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation; either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  *  | ||||
|  * This source code 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 General Public License for more details. | ||||
|  *  | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with this source code; if not, write to the Free Software | ||||
|  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA | ||||
|  */ | ||||
| 
 | ||||
| package org.easydarwin.hw; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.HashSet; | ||||
| import java.util.Set; | ||||
| 
 | ||||
| import android.annotation.SuppressLint; | ||||
| import android.media.MediaCodecInfo; | ||||
| import android.media.MediaCodecList; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| @SuppressLint("InlinedApi") | ||||
| public class CodecManager { | ||||
| 
 | ||||
| 	public final static String TAG = "CodecManager"; | ||||
| 
 | ||||
| 	public static final int[] SUPPORTED_COLOR_FORMATS = { | ||||
| 		MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar, | ||||
| 		MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar, | ||||
| 		MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar, | ||||
| 		MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar, | ||||
| 		MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar | ||||
| 	};		 | ||||
| 
 | ||||
| 	private static Codec[] sEncoders = null; | ||||
| 	private static Codec[] sDecoders = null; | ||||
| 
 | ||||
| 	static class Codec { | ||||
| 		public Codec(String name, Integer[] formats) { | ||||
| 			this.name = name; | ||||
| 			this.formats = formats; | ||||
| 		} | ||||
| 		public String name; | ||||
| 		public Integer[] formats; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Lists all encoders that claim to support a color format that we know how to use. | ||||
| 	 * @return A list of those encoders | ||||
| 	 */ | ||||
| 	@SuppressLint("NewApi") | ||||
| 	public synchronized static Codec[] findEncodersForMimeType(String mimeType) { | ||||
| 		if (sEncoders != null) return sEncoders; | ||||
| 
 | ||||
| 		ArrayList<Codec> encoders = new ArrayList<Codec>(); | ||||
| 
 | ||||
| 		// We loop through the encoders, apparently this can take up to a sec (testes on a GS3)
 | ||||
| 		for(int j =0; j <  MediaCodecList.getCodecCount() - 1; j++){ | ||||
| 			MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(j); | ||||
| 			if (!codecInfo.isEncoder()) continue; | ||||
| 
 | ||||
| 			String[] types = codecInfo.getSupportedTypes(); | ||||
| 			for (int i = 0; i < types.length; i++) { | ||||
| 				if (types[i].equalsIgnoreCase(mimeType)) { | ||||
| 					try { | ||||
| 						MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(mimeType); | ||||
| 						Set<Integer> formats = new HashSet<Integer>(); | ||||
| 
 | ||||
| 						// And through the color formats supported
 | ||||
| 						for (int k = 0; k < capabilities.colorFormats.length; k++) { | ||||
| 							int format = capabilities.colorFormats[k]; | ||||
| 
 | ||||
| 							for (int l=0;l<SUPPORTED_COLOR_FORMATS.length;l++) { | ||||
| 								if (format == SUPPORTED_COLOR_FORMATS[l]) { | ||||
| 									formats.add(format); | ||||
| 									break; | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 						 | ||||
| 						Codec codec = new Codec(codecInfo.getName(), (Integer[]) formats.toArray(new Integer[formats.size()])); | ||||
| 						encoders.add(codec); | ||||
| 					} catch (Exception e) { | ||||
| 						Log.wtf(TAG,e); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		sEncoders = (Codec[]) encoders.toArray(new Codec[encoders.size()]); | ||||
| 		if (sEncoders.length == 0) { | ||||
| 			sEncoders = new Codec[]{new Codec(null, new Integer[]{0})}; | ||||
| 		} | ||||
| 		return sEncoders; | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Lists all decoders that claim to support a color format that we know how to use. | ||||
| 	 * @return A list of those decoders | ||||
| 	 */ | ||||
| 	@SuppressLint("NewApi") | ||||
| 	public synchronized static Codec[] findDecodersForMimeType(String mimeType) { | ||||
| 		if (sDecoders != null) return sDecoders; | ||||
| 		ArrayList<Codec> decoders = new ArrayList<Codec>(); | ||||
| 
 | ||||
| 		// We loop through the decoders, apparently this can take up to a sec (testes on a GS3)
 | ||||
| 		for(int j = MediaCodecList.getCodecCount() - 1; j >= 0; j--){ | ||||
| 			MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(j); | ||||
| 			if (codecInfo.isEncoder()) continue; | ||||
| 
 | ||||
| 			String[] types = codecInfo.getSupportedTypes(); | ||||
| 			for (int i = 0; i < types.length; i++) { | ||||
| 				if (types[i].equalsIgnoreCase(mimeType)) { | ||||
| 					try { | ||||
| 						MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(mimeType); | ||||
| 						Set<Integer> formats = new HashSet<Integer>(); | ||||
| 
 | ||||
| 						// And through the color formats supported
 | ||||
| 						for (int k = 0; k < capabilities.colorFormats.length; k++) { | ||||
| 							int format = capabilities.colorFormats[k]; | ||||
| 
 | ||||
| 							for (int l=0;l<SUPPORTED_COLOR_FORMATS.length;l++) { | ||||
| 								if (format == SUPPORTED_COLOR_FORMATS[l]) { | ||||
| 									formats.add(format); | ||||
| 									break; | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 
 | ||||
| 						Codec codec = new Codec(codecInfo.getName(), (Integer[]) formats.toArray(new Integer[formats.size()])); | ||||
| 						decoders.add(codec); | ||||
| 					} catch (Exception e) { | ||||
| 						Log.wtf(TAG,e); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		sDecoders = (Codec[]) decoders.toArray(new Codec[decoders.size()]); | ||||
| 
 | ||||
| 		// We will use the decoder from google first, it seems to work properly on many phones
 | ||||
| 		for (int i=0;i<sDecoders.length;i++) { | ||||
| 			if (sDecoders[i].name.equalsIgnoreCase("omx.google.h264.decoder")) { | ||||
| 				Codec codec = sDecoders[0]; | ||||
| 				sDecoders[0] = sDecoders[i]; | ||||
| 				sDecoders[i] = codec; | ||||
| 			}  | ||||
| 		} | ||||
| 
 | ||||
| 		return sDecoders; | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| @ -0,0 +1,579 @@ | ||||
| /* | ||||
|  * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com | ||||
|  *  | ||||
|  * This file is part of Spydroid (http://code.google.com/p/spydroid-ipcamera/)
 | ||||
|  *  | ||||
|  * Spydroid is free software; you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation; either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  *  | ||||
|  * This source code 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 General Public License for more details. | ||||
|  *  | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with this source code; if not, write to the Free Software | ||||
|  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA | ||||
|  */ | ||||
| 
 | ||||
| package org.easydarwin.hw; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.io.PrintWriter; | ||||
| import java.io.StringWriter; | ||||
| import java.nio.ByteBuffer; | ||||
| import java.util.Arrays; | ||||
| 
 | ||||
| import org.easydarwin.easypusher.BuildConfig; | ||||
| import org.easydarwin.hw.CodecManager.Codec; | ||||
| 
 | ||||
| import android.annotation.SuppressLint; | ||||
| import android.content.Context; | ||||
| import android.content.SharedPreferences; | ||||
| import android.content.SharedPreferences.Editor; | ||||
| import android.media.MediaCodec; | ||||
| import android.media.MediaCodec.BufferInfo; | ||||
| import android.media.MediaFormat; | ||||
| import android.os.Build; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.util.Base64; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| /** | ||||
|  * The purpose of this class is to detect and by-pass some bugs (or | ||||
|  * underspecified configuration) that encoders available through the MediaCodec | ||||
|  * API may have. <br /> | ||||
|  * Feeding the encoder with a surface is not tested here. Some bugs you may have | ||||
|  * encountered:<br /> | ||||
|  * <ul> | ||||
|  * <li>U and V panes reversed</li> | ||||
|  * <li>Some padding is needed after the Y pane</li> | ||||
|  * <li>stride!=width or slice-height!=height</li> | ||||
|  * </ul> | ||||
|  */ | ||||
| @SuppressLint("NewApi") | ||||
| public class EncoderDebugger { | ||||
| 
 | ||||
|     public final static String TAG = "EncoderDebugger"; | ||||
| 
 | ||||
|     /** | ||||
|      * Prefix that will be used for all shared preferences saved by | ||||
|      * libstreaming. | ||||
|      */ | ||||
|     private static final String PREF_PREFIX = "libstreaming-"; | ||||
| 
 | ||||
|     /** | ||||
|      * If this is set to false the test will be run only once and the result | ||||
|      * will be saved in the shared preferences. | ||||
|      */ | ||||
|     private static final boolean DEBUG = BuildConfig.DEBUG; | ||||
| 
 | ||||
|     /** | ||||
|      * Set this to true to see more logs. | ||||
|      */ | ||||
|     private static final boolean VERBOSE = false; | ||||
| 
 | ||||
|     /** | ||||
|      * Will be incremented every time this test is modified. | ||||
|      */ | ||||
|     private static final int VERSION = 3; | ||||
| 
 | ||||
|     /** | ||||
|      * Bitrate that will be used with the encoder. | ||||
|      */ | ||||
|     private final static int BITRATE = 1000000; | ||||
| 
 | ||||
|     /** | ||||
|      * Framerate that will be used to test the encoder. | ||||
|      */ | ||||
|     private final static int FRAMERATE = 20; | ||||
| 
 | ||||
|     private final static String MIME_TYPE = "video/avc"; | ||||
| 
 | ||||
|     private final static int NB_DECODED = 34; | ||||
|     private final static int NB_ENCODED = 50; | ||||
| 
 | ||||
|     private int mEncoderColorFormat; | ||||
|     private String mEncoderName, mErrorLog; | ||||
|     private MediaCodec mEncoder; | ||||
|     private int mWidth, mHeight, mSize; | ||||
|     private byte[] mSPS, mPPS; | ||||
| 
 | ||||
|     private byte[] mData, mInitialImage; | ||||
|     private NV21Convertor mNV21; | ||||
|     private SharedPreferences mPreferences; | ||||
|     private byte[][] mVideo, mDecodedVideo; | ||||
|     private String mB64PPS, mB64SPS; | ||||
| 
 | ||||
|     public synchronized static void asyncDebug(final Context context, | ||||
|                                                final int width, final int height) { | ||||
|         new Thread(new Runnable() { | ||||
|             @Override | ||||
|             public void run() { | ||||
|                 try { | ||||
|                     SharedPreferences prefs = PreferenceManager | ||||
|                             .getDefaultSharedPreferences(context); | ||||
|                     debug(prefs, width, height); | ||||
|                 } catch (Exception e) { | ||||
|                 } | ||||
|             } | ||||
|         }).start(); | ||||
|     } | ||||
| 
 | ||||
|     public synchronized static EncoderDebugger debug(Context context, int width, int height) { | ||||
|         SharedPreferences prefs = PreferenceManager | ||||
|                 .getDefaultSharedPreferences(context); | ||||
|         return debug(prefs, width, height); | ||||
|     } | ||||
| 
 | ||||
|     public synchronized static EncoderDebugger debug(SharedPreferences prefs, int width, int height) { | ||||
|         EncoderDebugger debugger = new EncoderDebugger(prefs, width, height); | ||||
|         debugger.debug(); | ||||
|         return debugger; | ||||
|     } | ||||
| 
 | ||||
|     public String getB64PPS() { | ||||
|         return mB64PPS; | ||||
|     } | ||||
| 
 | ||||
|     public String getB64SPS() { | ||||
|         return mB64SPS; | ||||
|     } | ||||
| 
 | ||||
|     public String getEncoderName() { | ||||
|         return mEncoderName; | ||||
|     } | ||||
| 
 | ||||
|     public int getEncoderColorFormat() { | ||||
|         return mEncoderColorFormat; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This {@link NV21Convertor} will do the necessary work to feed properly | ||||
|      * the encoder. | ||||
|      */ | ||||
|     public NV21Convertor getNV21Convertor() { | ||||
|         return mNV21; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * A log of all the errors that occured during the test. | ||||
|      */ | ||||
|     public String getErrorLog() { | ||||
|         return mErrorLog; | ||||
|     } | ||||
| 
 | ||||
|     private EncoderDebugger(SharedPreferences prefs, int width, int height) { | ||||
|         mPreferences = prefs; | ||||
|         mWidth = width; | ||||
|         mHeight = height; | ||||
|         mSize = width * height; | ||||
|         reset(); | ||||
|     } | ||||
| 
 | ||||
|     private void reset() { | ||||
|         mNV21 = new NV21Convertor(); | ||||
|         mVideo = new byte[NB_ENCODED][]; | ||||
|         mDecodedVideo = new byte[NB_DECODED][]; | ||||
|         mErrorLog = ""; | ||||
|         mPPS = null; | ||||
|         mSPS = null; | ||||
|     } | ||||
| 
 | ||||
|     private void debug() { | ||||
| 
 | ||||
|         // If testing the phone again is not needed,
 | ||||
|         // we just restore the result from the shared preferences
 | ||||
|         if (!checkTestNeeded()) { | ||||
|             String resolution = mWidth + "x" + mHeight + "-"; | ||||
| 
 | ||||
|             boolean success = mPreferences.getBoolean(PREF_PREFIX + resolution | ||||
|                     + "success", false); | ||||
|             if (!success) { | ||||
|                 throw new RuntimeException( | ||||
|                         "Phone not supported with this resolution (" + mWidth | ||||
|                                 + "x" + mHeight + ")"); | ||||
|             } | ||||
| 
 | ||||
|             mNV21.setSize(mWidth, mHeight); | ||||
|             mNV21.setSliceHeigth(mPreferences.getInt(PREF_PREFIX + resolution | ||||
|                     + "sliceHeight", 0)); | ||||
|             mNV21.setStride(mPreferences.getInt(PREF_PREFIX + resolution | ||||
|                     + "stride", 0)); | ||||
|             mNV21.setYPadding(mPreferences.getInt(PREF_PREFIX + resolution | ||||
|                     + "padding", 0)); | ||||
|             mNV21.setPlanar(mPreferences.getBoolean(PREF_PREFIX + resolution | ||||
|                     + "planar", false)); | ||||
|             mNV21.setColorPanesReversed(mPreferences.getBoolean(PREF_PREFIX | ||||
|                     + resolution + "reversed", false)); | ||||
|             mEncoderName = mPreferences.getString(PREF_PREFIX + resolution | ||||
|                     + "encoderName", ""); | ||||
|             mEncoderColorFormat = mPreferences.getInt(PREF_PREFIX + resolution | ||||
|                     + "colorFormat", 0); | ||||
|             mB64PPS = mPreferences.getString(PREF_PREFIX + resolution + "bps", | ||||
|                     ""); | ||||
|             mB64SPS = mPreferences.getString(PREF_PREFIX + resolution + "sps", | ||||
|                     ""); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (VERBOSE) | ||||
|             Log.d(TAG, ">>>> Testing the phone for resolution " + mWidth + "x" | ||||
|                     + mHeight); | ||||
| 
 | ||||
|         // Builds a list of available encoders and decoders we may be able to
 | ||||
|         // use
 | ||||
|         // because they support some nice color formats
 | ||||
|         Codec[] encoders = CodecManager.findEncodersForMimeType(MIME_TYPE); | ||||
|         Codec[] decoders = CodecManager.findDecodersForMimeType(MIME_TYPE); | ||||
| 
 | ||||
|         int count = 0, n = 1; | ||||
|         for (int i = 0; i < encoders.length; i++) { | ||||
|             count += encoders[i].formats.length; | ||||
|         } | ||||
| 
 | ||||
|         // Tries available encoders
 | ||||
|         for (int i = 0; i < encoders.length; i++) { | ||||
|             for (int j = 0; j < encoders[i].formats.length; j++) { | ||||
|                 reset(); | ||||
| 
 | ||||
|                 mEncoderName = encoders[i].name; | ||||
|                 mEncoderColorFormat = encoders[i].formats[j]; | ||||
| 
 | ||||
|                 if (VERBOSE) | ||||
|                     Log.v(TAG, ">> Test " + (n++) + "/" + count + ": " | ||||
|                             + mEncoderName + " with color format " | ||||
|                             + mEncoderColorFormat + " at " + mWidth + "x" | ||||
|                             + mHeight); | ||||
| 
 | ||||
|                 // Converts from NV21 to YUV420 with the specified parameters
 | ||||
|                 mNV21.setSize(mWidth, mHeight); | ||||
|                 mNV21.setSliceHeigth(mHeight); | ||||
|                 mNV21.setStride(mWidth); | ||||
|                 mNV21.setYPadding(0); | ||||
|                 mNV21.setEncoderColorFormat(mEncoderColorFormat); | ||||
| 
 | ||||
|                 // /!\ NV21Convertor can directly modify the input
 | ||||
|                 createTestImage(); | ||||
|                 mData = mNV21.convert(mInitialImage); | ||||
| 
 | ||||
|                 try { | ||||
| 
 | ||||
|                     // Starts the encoder
 | ||||
|                     configureEncoder(); | ||||
|                     searchSPSandPPS(); | ||||
| 
 | ||||
|                     saveTestResult(true); | ||||
|                     Log.v(TAG, "The encoder " + mEncoderName | ||||
|                             + " is usable with resolution " + mWidth + "x" | ||||
|                             + mHeight); | ||||
|                     return; | ||||
| 
 | ||||
|                 } catch (Exception e) { | ||||
|                     StringWriter sw = new StringWriter(); | ||||
|                     PrintWriter pw = new PrintWriter(sw); | ||||
|                     e.printStackTrace(pw); | ||||
|                     String stack = sw.toString(); | ||||
|                     String str = "Encoder " + mEncoderName | ||||
|                             + " cannot be used with color format " | ||||
|                             + mEncoderColorFormat; | ||||
|                     if (VERBOSE) | ||||
|                         Log.e(TAG, str, e); | ||||
|                     mErrorLog += str + "\n" + stack; | ||||
|                     e.printStackTrace(); | ||||
|                 } finally { | ||||
|                     releaseEncoder(); | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         saveTestResult(false); | ||||
|         Log.e(TAG, "No usable encoder were found on the phone for resolution " | ||||
|                 + mWidth + "x" + mHeight); | ||||
|         throw new RuntimeException( | ||||
|                 "No usable encoder were found on the phone for resolution " | ||||
|                         + mWidth + "x" + mHeight); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private boolean checkTestNeeded() { | ||||
|         String resolution = mWidth + "x" + mHeight + "-"; | ||||
| 
 | ||||
|         // Forces the test
 | ||||
|         if (DEBUG || mPreferences == null) | ||||
|             return true; | ||||
| 
 | ||||
|         // If the sdk has changed on the phone, or the version of the test
 | ||||
|         // it has to be run again
 | ||||
|         if (mPreferences.contains(PREF_PREFIX + resolution + "lastSdk")) { | ||||
|             int lastSdk = mPreferences.getInt(PREF_PREFIX + resolution | ||||
|                     + "lastSdk", 0); | ||||
|             int lastVersion = mPreferences.getInt(PREF_PREFIX + resolution | ||||
|                     + "lastVersion", 0); | ||||
|             if (Build.VERSION.SDK_INT > lastSdk || VERSION > lastVersion) { | ||||
|                 return true; | ||||
|             } | ||||
|         } else { | ||||
|             return true; | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Saves the result of the test in the shared preferences, we will run it | ||||
|      * again only if the SDK has changed on the phone, or if this test has been | ||||
|      * modified. | ||||
|      */ | ||||
|     private void saveTestResult(boolean success) { | ||||
|         String resolution = mWidth + "x" + mHeight + "-"; | ||||
|         Editor editor = mPreferences.edit(); | ||||
| 
 | ||||
|         editor.putBoolean(PREF_PREFIX + resolution + "success", success); | ||||
| 
 | ||||
|         if (success) { | ||||
|             editor.putInt(PREF_PREFIX + resolution + "lastSdk", | ||||
|                     Build.VERSION.SDK_INT); | ||||
|             editor.putInt(PREF_PREFIX + resolution + "lastVersion", VERSION); | ||||
|             editor.putInt(PREF_PREFIX + resolution + "sliceHeight", | ||||
|                     mNV21.getSliceHeigth()); | ||||
|             editor.putInt(PREF_PREFIX + resolution + "stride", | ||||
|                     mNV21.getStride()); | ||||
|             editor.putInt(PREF_PREFIX + resolution + "padding", | ||||
|                     mNV21.getYPadding()); | ||||
|             editor.putBoolean(PREF_PREFIX + resolution + "planar", | ||||
|                     mNV21.getPlanar()); | ||||
|             editor.putBoolean(PREF_PREFIX + resolution + "reversed", | ||||
|                     mNV21.getUVPanesReversed()); | ||||
|             editor.putString(PREF_PREFIX + resolution + "encoderName", | ||||
|                     mEncoderName); | ||||
|             editor.putInt(PREF_PREFIX + resolution + "colorFormat", | ||||
|                     mEncoderColorFormat); | ||||
|             editor.putString(PREF_PREFIX + resolution + "encoderName", | ||||
|                     mEncoderName); | ||||
|             editor.putString(PREF_PREFIX + resolution + "bps", mB64PPS); | ||||
|             editor.putString(PREF_PREFIX + resolution + "sps", mB64SPS); | ||||
|         } | ||||
| 
 | ||||
|         editor.commit(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Creates the test image that will be used to feed the encoder. | ||||
|      */ | ||||
|     private void createTestImage() { | ||||
|         mInitialImage = new byte[3 * mSize / 2]; | ||||
|         for (int i = 0; i < mSize; i++) { | ||||
|             mInitialImage[i] = (byte) (40 + i % 199); | ||||
|         } | ||||
|         for (int i = mSize; i < 3 * mSize / 2; i += 2) { | ||||
|             mInitialImage[i] = (byte) (40 + i % 200); | ||||
|             mInitialImage[i + 1] = (byte) (40 + (i + 99) % 200); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Converts the image obtained from the decoder to NV21. | ||||
|      */ | ||||
| 
 | ||||
|     /** | ||||
|      * Instantiates and starts the encoder. | ||||
|      * | ||||
|      * @throws IOException | ||||
|      */ | ||||
|     private void configureEncoder() throws IOException { | ||||
|         mEncoder = MediaCodec.createByCodecName(mEncoderName); | ||||
|         MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, | ||||
|                 mWidth, mHeight); | ||||
|         mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, BITRATE); | ||||
|         mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAMERATE); | ||||
|         mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, | ||||
|                 mEncoderColorFormat); | ||||
|         mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); | ||||
|         mEncoder.configure(mediaFormat, null, null, | ||||
|                 MediaCodec.CONFIGURE_FLAG_ENCODE); | ||||
|         mEncoder.start(); | ||||
|     } | ||||
| 
 | ||||
|     private void releaseEncoder() { | ||||
|         if (mEncoder != null) { | ||||
|             try { | ||||
|                 mEncoder.stop(); | ||||
|             } catch (Exception ignore) { | ||||
|             } | ||||
|             try { | ||||
|                 mEncoder.release(); | ||||
|             } catch (Exception ignore) { | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Tries to obtain the SPS and the PPS for the encoder. | ||||
|      */ | ||||
|     private long searchSPSandPPS() { | ||||
|         long elapsed = 0, now = timestamp(); | ||||
|         ByteBuffer[] inputBuffers = mEncoder.getInputBuffers(); | ||||
|         ByteBuffer[] outputBuffers = mEncoder.getOutputBuffers(); | ||||
|         BufferInfo info = new BufferInfo(); | ||||
|         byte[] csd = new byte[128]; | ||||
|         int len = 0, p = 4, q = 4; | ||||
| 
 | ||||
|         while (elapsed < 3000000 && (mSPS == null || mPPS == null)) { | ||||
| 
 | ||||
|             // Some encoders won't give us the SPS and PPS unless they receive
 | ||||
|             // something to encode first...
 | ||||
|             int bufferIndex = mEncoder.dequeueInputBuffer(1000000 / FRAMERATE); | ||||
|             if (bufferIndex >= 0) { | ||||
|                 check(inputBuffers[bufferIndex].capacity() >= mData.length, | ||||
|                         "The input buffer is not big enough."); | ||||
|                 inputBuffers[bufferIndex].clear(); | ||||
|                 inputBuffers[bufferIndex].put(mData, 0, mData.length); | ||||
|                 mEncoder.queueInputBuffer(bufferIndex, 0, mData.length, | ||||
|                         timestamp(), 0); | ||||
|             } else { | ||||
|                 if (VERBOSE) | ||||
|                     Log.e(TAG, "No buffer available !"); | ||||
|             } | ||||
| 
 | ||||
|             // We are looking for the SPS and the PPS here. As always, Android
 | ||||
|             // is very inconsistent, I have observed that some
 | ||||
|             // encoders will give those parameters through the MediaFormat
 | ||||
|             // object (that is the normal behaviour).
 | ||||
|             // But some other will not, in that case we try to find a NAL unit
 | ||||
|             // of type 7 or 8 in the byte stream outputed by the encoder...
 | ||||
| 
 | ||||
|             int index = mEncoder.dequeueOutputBuffer(info, 1000000 / FRAMERATE); | ||||
| 
 | ||||
|             if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { | ||||
| 
 | ||||
|                 // The PPS and PPS shoud be there
 | ||||
|                 MediaFormat format = mEncoder.getOutputFormat(); | ||||
|                 ByteBuffer spsb = format.getByteBuffer("csd-0"); | ||||
|                 ByteBuffer ppsb = format.getByteBuffer("csd-1"); | ||||
|                 mSPS = new byte[spsb.capacity() - 4]; | ||||
|                 spsb.position(4); | ||||
|                 spsb.get(mSPS, 0, mSPS.length); | ||||
|                 mPPS = new byte[ppsb.capacity() - 4]; | ||||
|                 ppsb.position(4); | ||||
|                 ppsb.get(mPPS, 0, mPPS.length); | ||||
|                 break; | ||||
| 
 | ||||
|             } else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { | ||||
|                 outputBuffers = mEncoder.getOutputBuffers(); | ||||
|             } else if (index >= 0) { | ||||
| 
 | ||||
|                 len = info.size; | ||||
|                 if (len < 128) { | ||||
|                     outputBuffers[index].get(csd, 0, len); | ||||
|                     if (len > 0 && csd[0] == 0 && csd[1] == 0 && csd[2] == 0 | ||||
|                             && csd[3] == 1) { | ||||
|                         // Parses the SPS and PPS, they could be in two
 | ||||
|                         // different packets and in a different order
 | ||||
|                         // depending on the phone so we don't make any
 | ||||
|                         // assumption about that
 | ||||
|                         while (p < len) { | ||||
|                             while (!(csd[p + 0] == 0 && csd[p + 1] == 0 | ||||
|                                     && csd[p + 2] == 0 && csd[p + 3] == 1) | ||||
|                                     && p + 3 < len) | ||||
|                                 p++; | ||||
|                             if (p + 3 >= len) | ||||
|                                 p = len; | ||||
|                             if ((csd[q] & 0x1F) == 7) { | ||||
|                                 mSPS = new byte[p - q]; | ||||
|                                 System.arraycopy(csd, q, mSPS, 0, p - q); | ||||
|                             } else { | ||||
|                                 mPPS = new byte[p - q]; | ||||
|                                 System.arraycopy(csd, q, mPPS, 0, p - q); | ||||
|                             } | ||||
|                             p += 4; | ||||
|                             q = p; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 mEncoder.releaseOutputBuffer(index, false); | ||||
|             } | ||||
| 
 | ||||
|             elapsed = timestamp() - now; | ||||
|         } | ||||
| 
 | ||||
|         check(mPPS != null & mSPS != null, "Could not determine the SPS & PPS."); | ||||
|         mB64PPS = Base64.encodeToString(mPPS, 0, mPPS.length, Base64.NO_WRAP); | ||||
|         mB64SPS = Base64.encodeToString(mSPS, 0, mSPS.length, Base64.NO_WRAP); | ||||
| 
 | ||||
|         return elapsed; | ||||
|     } | ||||
| 
 | ||||
|     static int getXPS(byte[] data, int offset, int length, byte[] dataOut, | ||||
|                       int[] outLen, int type) { | ||||
|         int i; | ||||
|         int pos0; | ||||
|         int pos1; | ||||
|         pos0 = -1; | ||||
|         for (i = offset; i < length - 4; i++) { | ||||
|             if ((0 == data[i]) && (0 == data[i + 1]) && (1 == data[i + 2]) | ||||
|                     && (type == (0x0F & data[i + 3]))) { | ||||
|                 pos0 = i; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|         if (-1 == pos0) { | ||||
|             return -1; | ||||
|         } | ||||
|         pos1 = -1; | ||||
|         for (i = pos0 + 4; i < length - 4; i++) { | ||||
|             if ((0 == data[i]) && (0 == data[i + 1]) && (0 == data[i + 2])) { | ||||
|                 pos1 = i; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|         if (-1 == pos1) { | ||||
|             return -2; | ||||
|         } | ||||
|         if (pos1 - pos0 + 1 > outLen[0]) { | ||||
|             return -3; // 输入缓冲区太小
 | ||||
|         } | ||||
|         dataOut[0] = 0; | ||||
|         System.arraycopy(data, pos0, dataOut, 1, pos1 - pos0); | ||||
|         // memcpy(pXPS+1, pES+pos0, pos1-pos0);
 | ||||
|         // *pMaxXPSLen = pos1-pos0+1;
 | ||||
|         outLen[0] = pos1 - pos0 + 1; | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private void check(boolean cond, String message) { | ||||
|         if (!cond) { | ||||
|             if (VERBOSE) | ||||
|                 Log.e(TAG, message); | ||||
|             throw new IllegalStateException(message); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private long timestamp() { | ||||
|         return System.nanoTime() / 1000; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String toString() { | ||||
|         return "EncoderDebugger [mEncoderColorFormat=" + mEncoderColorFormat | ||||
|                 + ", mEncoderName=" + mEncoderName + ", mErrorLog=" + mErrorLog + ", mEncoder=" | ||||
|                 + mEncoder + ", mWidth=" + mWidth | ||||
|                 + ", mHeight=" + mHeight + ", mSize=" + mSize + ", mSPS=" | ||||
|                 + Arrays.toString(mSPS) + ", mPPS=" + Arrays.toString(mPPS) | ||||
|                 + ", mData=" + Arrays.toString(mData) + ", mInitialImage=" | ||||
|                 + Arrays.toString(mInitialImage) + ", mNV21=" + mNV21 + ", mPreferences=" | ||||
|                 + mPreferences + ", mVideo=" + Arrays.toString(mVideo) | ||||
|                 + ", mDecodedVideo=" + Arrays.toString(mDecodedVideo) | ||||
|                 + ", mB64PPS=" + mB64PPS + ", mB64SPS=" + mB64SPS | ||||
|                 + "]"; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,174 @@ | ||||
| /* | ||||
|  * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com | ||||
|  *  | ||||
|  * This file is part of Spydroid (http://code.google.com/p/spydroid-ipcamera/)
 | ||||
|  *  | ||||
|  * Spydroid is free software; you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation; either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  *  | ||||
|  * This source code 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 General Public License for more details. | ||||
|  *  | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with this source code; if not, write to the Free Software | ||||
|  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | ||||
|  */ | ||||
| 
 | ||||
| package org.easydarwin.hw; | ||||
| 
 | ||||
| import java.nio.ByteBuffer; | ||||
| 
 | ||||
| import android.media.MediaCodecInfo; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| /** | ||||
|  * Converts from NV21 to YUV420 semi planar or planar. | ||||
|  */ | ||||
| public class NV21Convertor { | ||||
| 
 | ||||
| 	private int mSliceHeight, mHeight; | ||||
| 
 | ||||
| 	private int mStride, mWidth; | ||||
| 
 | ||||
| 	private int mSize; | ||||
| 
 | ||||
| 	private boolean mPlanar, mPanesReversed = false; | ||||
| 
 | ||||
| 	private int mYPadding; | ||||
| 
 | ||||
| 	private byte[] mBuffer; | ||||
| 
 | ||||
| 	ByteBuffer mCopy; | ||||
| 
 | ||||
| 	public void setSize(int width, int height) { | ||||
| 		mHeight = height; | ||||
| 		mWidth = width; | ||||
| 		mSliceHeight = height; | ||||
| 		mStride = width; | ||||
| 		mSize = mWidth * mHeight; | ||||
| 	} | ||||
| 
 | ||||
| 	public void setStride(int width) { | ||||
| 		mStride = width; | ||||
| 	} | ||||
| 
 | ||||
| 	public void setSliceHeigth(int height) { | ||||
| 		mSliceHeight = height; | ||||
| 	} | ||||
| 
 | ||||
| 	public void setPlanar(boolean planar) { | ||||
| 		mPlanar = planar; | ||||
| 	} | ||||
| 
 | ||||
| 	public void setYPadding(int padding) { | ||||
| 		mYPadding = padding; | ||||
| 	} | ||||
| 
 | ||||
| 	public int getBufferSize() { | ||||
| 		return 3 * mSize / 2; | ||||
| 	} | ||||
| 
 | ||||
| 	public void setEncoderColorFormat(int colorFormat) { | ||||
| 		switch (colorFormat) { | ||||
| 			case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar: | ||||
| 			case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar: | ||||
| 			case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar: | ||||
| 				setPlanar(false); | ||||
| 				break; | ||||
| 			case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar: | ||||
| 			case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar: | ||||
| 				setPlanar(true); | ||||
| 				break; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	public void setColorPanesReversed(boolean b) { | ||||
| 		mPanesReversed = b; | ||||
| 	} | ||||
| 
 | ||||
| 	public int getStride() { | ||||
| 		return mStride; | ||||
| 	} | ||||
| 
 | ||||
| 	public int getSliceHeigth() { | ||||
| 		return mSliceHeight; | ||||
| 	} | ||||
| 
 | ||||
| 	public int getYPadding() { | ||||
| 		return mYPadding; | ||||
| 	} | ||||
| 
 | ||||
| 	public boolean getPlanar() { | ||||
| 		return mPlanar; | ||||
| 	} | ||||
| 
 | ||||
| 	public boolean getUVPanesReversed() { | ||||
| 		return mPanesReversed; | ||||
| 	} | ||||
| 
 | ||||
| 	public void convert(byte[] data, ByteBuffer buffer) { | ||||
| 		byte[] result = convert(data); | ||||
| 		int min = buffer.capacity() < data.length ? buffer.capacity() : data.length; | ||||
| 		buffer.put(result, 0, min); | ||||
| 	} | ||||
| 
 | ||||
| 	public byte[] convert(byte[] data) { | ||||
| 
 | ||||
| 		// A buffer large enough for every case
 | ||||
| 		if (mBuffer == null || mBuffer.length != 3 * mSliceHeight * mStride / 2 + mYPadding) { | ||||
| 			mBuffer = new byte[3 * mSliceHeight * mStride / 2 + mYPadding]; | ||||
| 		} | ||||
| 
 | ||||
| 		if (!mPlanar) { | ||||
| 			if (mSliceHeight == mHeight && mStride == mWidth) { | ||||
| 				// Swaps U and V
 | ||||
| 				if (!mPanesReversed) { | ||||
| 					for (int i = mSize; i < mSize + mSize / 2; i += 2) { | ||||
| 						mBuffer[0] = data[i + 1]; | ||||
| 						data[i + 1] = data[i]; | ||||
| 						data[i] = mBuffer[0]; | ||||
| 					} | ||||
| 				} | ||||
| 				if (mYPadding > 0) { | ||||
| 					System.arraycopy(data, 0, mBuffer, 0, mSize); | ||||
| 					System.arraycopy(data, mSize, mBuffer, mSize + mYPadding, mSize / 2); | ||||
| 					return mBuffer; | ||||
| 				} | ||||
| 				return data; | ||||
| 			} | ||||
| 		} | ||||
| 		else { | ||||
| 			if (mSliceHeight == mHeight && mStride == mWidth) { | ||||
| 				// De-interleave U and V
 | ||||
| 				if (!mPanesReversed) { | ||||
| 					for (int i = 0; i < mSize / 4; i += 1) { | ||||
| 						mBuffer[i] = data[mSize + 2 * i + 1]; | ||||
| 						mBuffer[mSize / 4 + i] = data[mSize + 2 * i]; | ||||
| 					} | ||||
| 				} | ||||
| 				else { | ||||
| 					for (int i = 0; i < mSize / 4; i += 1) { | ||||
| 						mBuffer[i] = data[mSize + 2 * i]; | ||||
| 						mBuffer[mSize / 4 + i] = data[mSize + 2 * i + 1]; | ||||
| 					} | ||||
| 				} | ||||
| 				if (mYPadding == 0) { | ||||
| 					System.arraycopy(mBuffer, 0, data, mSize, mSize / 2); | ||||
| 				} | ||||
| 				else { | ||||
| 					System.arraycopy(data, 0, mBuffer, 0, mSize); | ||||
| 					System.arraycopy(mBuffer, 0, mBuffer, mSize + mYPadding, mSize / 2); | ||||
| 					return mBuffer; | ||||
| 				} | ||||
| 				return data; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		return data; | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,148 @@ | ||||
| package org.easydarwin.muxer; | ||||
| 
 | ||||
| import android.annotation.TargetApi; | ||||
| import android.media.MediaCodec; | ||||
| import android.media.MediaFormat; | ||||
| import android.media.MediaMuxer; | ||||
| import android.os.Build; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import org.easydarwin.bus.StartRecord; | ||||
| import org.easydarwin.bus.StopRecord; | ||||
| import org.easydarwin.easypusher.BuildConfig; | ||||
| import org.easydarwin.easypusher.EasyApplication; | ||||
| import org.easydarwin.push.EasyPusher; | ||||
| 
 | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.nio.ByteBuffer; | ||||
| 
 | ||||
| /** | ||||
|  * Created by John on 2017/1/10. | ||||
|  */ | ||||
| 
 | ||||
| public class EasyMuxer { | ||||
| 
 | ||||
|     private static final boolean VERBOSE = BuildConfig.DEBUG; | ||||
|     private static final String TAG = EasyMuxer.class.getSimpleName(); | ||||
|     private final String mFilePath; | ||||
|     private MediaMuxer mMuxer; | ||||
|     private final long durationMillis; | ||||
|     private int index = 0; | ||||
|     private int mVideoTrackIndex = -1; | ||||
|     private int mAudioTrackIndex = -1; | ||||
|     private long mBeginMillis; | ||||
|     private MediaFormat mVideoFormat; | ||||
|     private MediaFormat mAudioFormat; | ||||
| 
 | ||||
|     @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) | ||||
|     public EasyMuxer(String path, long durationMillis) throws IOException { | ||||
|         if(path.endsWith(".mp4")) { | ||||
|             path = path.substring(0, path.lastIndexOf(".mp4")); | ||||
|         } | ||||
|         mFilePath = path; | ||||
|         this.durationMillis = durationMillis; | ||||
|         mMuxer = new MediaMuxer(path + "_" + index++ + ".mp4", MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); | ||||
|     } | ||||
| 
 | ||||
|     public synchronized void addTrack(MediaFormat format, boolean isVideo) { | ||||
|         // now that we have the Magic Goodies, start the muxer
 | ||||
|         if (mAudioTrackIndex != -1 && mVideoTrackIndex != -1) | ||||
|             throw new RuntimeException("already add all tracks"); | ||||
| 
 | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { | ||||
|             int track = mMuxer.addTrack(format); | ||||
|             if (VERBOSE) | ||||
|                 Log.i(TAG, String.format("addTrack %s result %d", isVideo ? "video" : "audio", track)); | ||||
|             if (isVideo) { | ||||
|                 mVideoFormat = format; | ||||
|                 mVideoTrackIndex = track; | ||||
|                 if (mAudioTrackIndex != -1) { | ||||
|                     if (VERBOSE) | ||||
|                         Log.i(TAG, "both audio and video added,and muxer is started"); | ||||
|                     mMuxer.start(); | ||||
|                     mBeginMillis = System.currentTimeMillis(); | ||||
|                 } | ||||
|             } else { | ||||
|                 mAudioFormat = format; | ||||
|                 mAudioTrackIndex = track; | ||||
|                 if (mVideoTrackIndex != -1) { | ||||
|                     mMuxer.start(); | ||||
|                     mBeginMillis = System.currentTimeMillis(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public synchronized void pumpStream(ByteBuffer outputBuffer, MediaCodec.BufferInfo bufferInfo, boolean isVideo) { | ||||
|         if (mAudioTrackIndex == -1 || mVideoTrackIndex == -1) { | ||||
|             Log.i(TAG, String.format("pumpStream [%s] but muxer is not start.ignore..", isVideo ? "video" : "audio")); | ||||
|             return; | ||||
|         } | ||||
|         if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { | ||||
|             // The codec config data was pulled out and fed to the muxer when we got
 | ||||
|             // the INFO_OUTPUT_FORMAT_CHANGED status.  Ignore it.
 | ||||
|         } else if (bufferInfo.size != 0) { | ||||
|             if (isVideo && mVideoTrackIndex == -1) { | ||||
|                 throw new RuntimeException("muxer hasn't started"); | ||||
|             } | ||||
| 
 | ||||
|             // adjust the ByteBuffer values to match BufferInfo (not needed?)
 | ||||
|             outputBuffer.position(bufferInfo.offset); | ||||
|             outputBuffer.limit(bufferInfo.offset + bufferInfo.size); | ||||
| 
 | ||||
|             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { | ||||
|                 mMuxer.writeSampleData(isVideo ? mVideoTrackIndex : mAudioTrackIndex, outputBuffer, bufferInfo); | ||||
|             } | ||||
|             if (VERBOSE) | ||||
|                 Log.d(TAG, String.format("sent %s [" + bufferInfo.size + "] with timestamp:[%d] to muxer", isVideo ? "video" : "audio", bufferInfo.presentationTimeUs / 1000)); | ||||
|         } | ||||
| 
 | ||||
|         if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { | ||||
|             if (VERBOSE) | ||||
|                 Log.i(TAG, "BUFFER_FLAG_END_OF_STREAM received"); | ||||
|         } | ||||
| 
 | ||||
|         if (System.currentTimeMillis() - mBeginMillis >= durationMillis && isVideo && ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0)) { | ||||
|             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { | ||||
|                 if (VERBOSE) | ||||
|                     Log.i(TAG, String.format("record file reach expiration.create new file:" + index)); | ||||
|                 mMuxer.stop(); | ||||
|                 mMuxer.release(); | ||||
|                 mMuxer = null; | ||||
|                 mVideoTrackIndex = mAudioTrackIndex = -1; | ||||
|                 try { | ||||
|                     mMuxer = new MediaMuxer(mFilePath + "-" + ++index + ".mp4", MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); | ||||
|                     addTrack(mVideoFormat, true); | ||||
|                     addTrack(mAudioFormat, false); | ||||
|                     pumpStream(outputBuffer, bufferInfo, isVideo); | ||||
|                 } catch (IOException e) { | ||||
|                     e.printStackTrace(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public synchronized void release() { | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { | ||||
|             if (mMuxer != null) { | ||||
|                 if (mAudioTrackIndex != -1 && mVideoTrackIndex != -1) { | ||||
|                     if (VERBOSE) | ||||
|                         Log.i(TAG, String.format("muxer is started. now it will be stoped.")); | ||||
|                     try { | ||||
|                         mMuxer.stop(); | ||||
|                         mMuxer.release(); | ||||
|                     } catch (IllegalStateException ex) { | ||||
|                         ex.printStackTrace(); | ||||
|                     } | ||||
| 
 | ||||
|                     if (System.currentTimeMillis() - mBeginMillis <= 1500){ | ||||
|                         new File(mFilePath + "-" + index + ".mp4").delete(); | ||||
|                     } | ||||
|                     mAudioTrackIndex = mVideoTrackIndex = -1; | ||||
| //                    EasyApplication.BUS.post(new StopRecord());
 | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,263 @@ | ||||
| package org.easydarwin.push; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.media.MediaCodec; | ||||
| import android.media.MediaFormat; | ||||
| import android.os.Build; | ||||
| import android.os.Bundle; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import org.easydarwin.easypusher.BuildConfig; | ||||
| import org.easydarwin.muxer.EasyMuxer; | ||||
| import org.easydarwin.sw.JNIUtil; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.nio.ByteBuffer; | ||||
| 
 | ||||
| import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar; | ||||
| import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar; | ||||
| import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar; | ||||
| 
 | ||||
| /** | ||||
|  * Created by apple on 2017/5/13. | ||||
|  */ | ||||
| public class HWConsumer extends Thread implements VideoConsumer { | ||||
|     private static final String TAG = "Pusher"; | ||||
|     private final MediaStream.CodecInfo info; | ||||
|     public EasyMuxer mMuxer; | ||||
|     private final Context mContext; | ||||
|     private final Pusher mPusher; | ||||
|     private int mHeight; | ||||
|     private int mWidth; | ||||
|     private MediaCodec mMediaCodec; | ||||
|     private ByteBuffer[] inputBuffers; | ||||
|     private ByteBuffer[] outputBuffers; | ||||
|     private volatile boolean mVideoStarted; | ||||
|     private MediaFormat newFormat; | ||||
| 
 | ||||
|     public HWConsumer(Context context, Pusher pusher, MediaStream.CodecInfo info) { | ||||
|         mContext = context; | ||||
|         mPusher = pusher; | ||||
|         this.info = info; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     @Override | ||||
|     public void onVideoStart(int width, int height) throws IOException { | ||||
|         newFormat = null; | ||||
|         this.mWidth = width; | ||||
|         this.mHeight = height; | ||||
|         startMediaCodec(); | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP + 1) { | ||||
|             inputBuffers = outputBuffers = null; | ||||
|         } else { | ||||
|             inputBuffers = mMediaCodec.getInputBuffers(); | ||||
|             outputBuffers = mMediaCodec.getOutputBuffers(); | ||||
|         } | ||||
|         start(); | ||||
|         mVideoStarted = true; | ||||
|     } | ||||
| 
 | ||||
|     final int millisPerframe = 1000 / 20; | ||||
|     long lastPush = 0; | ||||
| 
 | ||||
|     @Override | ||||
|     public int onVideo(byte[] data, int format) { | ||||
|         if (!mVideoStarted) return 0; | ||||
| 
 | ||||
|         try { | ||||
|             if (lastPush == 0) { | ||||
|                 lastPush = System.currentTimeMillis(); | ||||
|             } | ||||
|             long time = System.currentTimeMillis() - lastPush; | ||||
|             if (time >= 0) { | ||||
|                 time = millisPerframe - time; | ||||
|                 if (time > 0) Thread.sleep(time / 2); | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             if (info.mColorFormat == COLOR_FormatYUV420SemiPlanar) { | ||||
|                 JNIUtil.yuvConvert(data, mWidth, mHeight, 6); | ||||
|             } else if (info.mColorFormat == COLOR_TI_FormatYUV420PackedSemiPlanar) { | ||||
|                 JNIUtil.yuvConvert(data, mWidth, mHeight, 6); | ||||
|             } else if (info.mColorFormat == COLOR_FormatYUV420Planar) { | ||||
|                 JNIUtil.yuvConvert(data, mWidth, mHeight, 5); | ||||
|             } else { | ||||
|                 JNIUtil.yuvConvert(data, mWidth, mHeight, 5); | ||||
|             } | ||||
|             int bufferIndex = mMediaCodec.dequeueInputBuffer(0); | ||||
|             if (bufferIndex >= 0) { | ||||
|                 ByteBuffer buffer = null; | ||||
|                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||
|                     buffer = mMediaCodec.getInputBuffer(bufferIndex); | ||||
|                 } else { | ||||
|                     buffer = inputBuffers[bufferIndex]; | ||||
|                 } | ||||
|                 buffer.clear(); | ||||
|                 buffer.put(data); | ||||
|                 buffer.clear(); | ||||
|                 mMediaCodec.queueInputBuffer(bufferIndex, 0, data.length, System.nanoTime() / 1000, MediaCodec.BUFFER_FLAG_KEY_FRAME); | ||||
|             } | ||||
|             if (time > 0) Thread.sleep(time / 2); | ||||
|             lastPush = System.currentTimeMillis(); | ||||
|         } catch (InterruptedException ex) { | ||||
|             ex.printStackTrace(); | ||||
|         } | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void run() { | ||||
|         MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); | ||||
|         int outputBufferIndex = 0; | ||||
|         byte[] mPpsSps = new byte[0]; | ||||
|         byte[] h264 = new byte[mWidth * mHeight]; | ||||
|         do { | ||||
|             outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 10000); | ||||
|             if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) { | ||||
|                 // no output available yet
 | ||||
|             } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { | ||||
|                 // not expected for an encoder
 | ||||
|                 outputBuffers = mMediaCodec.getOutputBuffers(); | ||||
|             } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { | ||||
|                 synchronized (HWConsumer.this) { | ||||
|                     newFormat = mMediaCodec.getOutputFormat(); | ||||
|                     EasyMuxer muxer = mMuxer; | ||||
|                     if (muxer != null) { | ||||
|                         // should happen before receiving buffers, and should only happen once
 | ||||
| 
 | ||||
|                         muxer.addTrack(newFormat, true); | ||||
|                     } | ||||
|                 } | ||||
|             } else if (outputBufferIndex < 0) { | ||||
|                 // let's ignore it
 | ||||
|             } else { | ||||
|                 ByteBuffer outputBuffer; | ||||
|                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||
|                     outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex); | ||||
|                 } else { | ||||
|                     outputBuffer = outputBuffers[outputBufferIndex]; | ||||
|                 } | ||||
|                 outputBuffer.position(bufferInfo.offset); | ||||
|                 outputBuffer.limit(bufferInfo.offset + bufferInfo.size); | ||||
|                 EasyMuxer muxer = mMuxer; | ||||
|                 if (muxer != null) { | ||||
|                     muxer.pumpStream(outputBuffer, bufferInfo, true); | ||||
|                 } | ||||
| 
 | ||||
|                 boolean sync = false; | ||||
|                 if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {// sps
 | ||||
|                     sync = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0; | ||||
|                     if (!sync) { | ||||
|                         byte[] temp = new byte[bufferInfo.size]; | ||||
|                         outputBuffer.get(temp); | ||||
|                         mPpsSps = temp; | ||||
|                         mMediaCodec.releaseOutputBuffer(outputBufferIndex, false); | ||||
|                         continue; | ||||
|                     } else { | ||||
|                         mPpsSps = new byte[0]; | ||||
|                     } | ||||
|                 } | ||||
|                 sync |= (bufferInfo.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0; | ||||
|                 int len = mPpsSps.length + bufferInfo.size; | ||||
|                 if (len > h264.length) { | ||||
|                     h264 = new byte[len]; | ||||
|                 } | ||||
|                 if (sync) { | ||||
|                     System.arraycopy(mPpsSps, 0, h264, 0, mPpsSps.length); | ||||
|                     outputBuffer.get(h264, mPpsSps.length, bufferInfo.size); | ||||
|                     mPusher.push(h264, 0, mPpsSps.length + bufferInfo.size, bufferInfo.presentationTimeUs / 1000, 1); | ||||
|                     if (BuildConfig.DEBUG) | ||||
|                         Log.i(TAG, String.format("push i video stamp:%d", bufferInfo.presentationTimeUs / 1000)); | ||||
|                 } else { | ||||
|                     outputBuffer.get(h264, 0, bufferInfo.size); | ||||
|                     mPusher.push(h264, 0, bufferInfo.size, bufferInfo.presentationTimeUs / 1000, 1); | ||||
|                     if (BuildConfig.DEBUG) | ||||
|                         Log.i(TAG, String.format("push video stamp:%d", bufferInfo.presentationTimeUs / 1000)); | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                 mMediaCodec.releaseOutputBuffer(outputBufferIndex, false); | ||||
|             } | ||||
|         } | ||||
|         while (mVideoStarted); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onVideoStop() { | ||||
|         do { | ||||
|             newFormat = null; | ||||
|             mVideoStarted = false; | ||||
|             try { | ||||
|                 join(); | ||||
|             } catch (InterruptedException e) { | ||||
|                 e.printStackTrace(); | ||||
|             } | ||||
|         } while (isAlive()); | ||||
|         if (mMediaCodec != null) { | ||||
|             stopMediaCodec(); | ||||
|             mMediaCodec = null; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public synchronized void setMuxer(EasyMuxer muxer) { | ||||
|         if (muxer != null) { | ||||
|             if (newFormat != null) | ||||
|                 muxer.addTrack(newFormat, true); | ||||
|         } | ||||
|         mMuxer = muxer; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * 初始化编码器 | ||||
|      */ | ||||
|     private void startMediaCodec() throws IOException { | ||||
|             /* | ||||
|         SD (Low quality) SD (High quality) HD 720p | ||||
| 1 HD 1080p | ||||
| 1 | ||||
| Video resolution 320 x 240 px 720 x 480 px 1280 x 720 px 1920 x 1080 px | ||||
| Video frame rate 20 fps 30 fps 30 fps 30 fps | ||||
| Video bitrate 384 Kbps 2 Mbps 4 Mbps 10 Mbps | ||||
|         */ | ||||
|         int framerate = 20; | ||||
| //        if (width == 640 || height == 640) {
 | ||||
| //            bitrate = 2000000;
 | ||||
| //        } else if (width == 1280 || height == 1280) {
 | ||||
| //            bitrate = 4000000;
 | ||||
| //        } else {
 | ||||
| //            bitrate = 2 * width * height;
 | ||||
| //        }
 | ||||
| 
 | ||||
|         int bitrate = (int) (mWidth * mHeight * 20 * 2 * 0.05f); | ||||
|         if (mWidth >= 1920 || mHeight >= 1920) bitrate *= 0.3; | ||||
|         else if (mWidth >= 1280 || mHeight >= 1280) bitrate *= 0.4; | ||||
|         else if (mWidth >= 720 || mHeight >= 720) bitrate *= 0.6; | ||||
|         mMediaCodec = MediaCodec.createByCodecName(info.mName); | ||||
|         MediaFormat mediaFormat = MediaFormat.createVideoFormat(info.mime, mWidth, mHeight); | ||||
|         mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); | ||||
|         mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, framerate); | ||||
|         mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, info.mColorFormat); | ||||
|         mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); | ||||
|         mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); | ||||
|         mMediaCodec.start(); | ||||
| 
 | ||||
|         Bundle params = new Bundle(); | ||||
|         params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0); | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { | ||||
|             mMediaCodec.setParameters(params); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 停止编码并释放编码资源占用 | ||||
|      */ | ||||
|     private void stopMediaCodec() { | ||||
|         mMediaCodec.stop(); | ||||
|         mMediaCodec.release(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,9 @@ | ||||
| package org.easydarwin.push; | ||||
| 
 | ||||
| /** | ||||
|  * Created by john on 2017/5/6. | ||||
|  */ | ||||
| 
 | ||||
| public interface InitCallback { | ||||
|     public void onCallback(int code); | ||||
| } | ||||
| @ -0,0 +1,371 @@ | ||||
| package org.easydarwin.push; | ||||
| 
 | ||||
| import android.annotation.TargetApi; | ||||
| import android.app.Application; | ||||
| import android.app.Notification; | ||||
| import android.app.PendingIntent; | ||||
| import android.app.Service; | ||||
| import android.content.BroadcastReceiver; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.IntentFilter; | ||||
| import android.hardware.display.DisplayManager; | ||||
| import android.hardware.display.VirtualDisplay; | ||||
| import android.media.MediaCodec; | ||||
| import android.media.MediaCodecInfo; | ||||
| import android.media.MediaFormat; | ||||
| import android.media.projection.MediaProjection; | ||||
| import android.media.projection.MediaProjectionManager; | ||||
| import android.os.Binder; | ||||
| import android.os.Build; | ||||
| import android.os.Environment; | ||||
| import android.os.IBinder; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.annotation.RequiresApi; | ||||
| import android.text.TextUtils; | ||||
| import android.util.DisplayMetrics; | ||||
| import android.util.Log; | ||||
| import android.view.Surface; | ||||
| import android.view.WindowManager; | ||||
| 
 | ||||
| import org.easydarwin.easypusher.BuildConfig; | ||||
| import org.easydarwin.easypusher.R; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.nio.ByteBuffer; | ||||
| 
 | ||||
| import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; | ||||
| 
 | ||||
| 
 | ||||
| public class PushScreenService extends Service { | ||||
| 
 | ||||
|     private static final String TAG = "RService"; | ||||
|     public static final String ACTION_CLOSE_PUSHING_SCREEN = "ACTION_CLOSE_PUSHING_SCREEN"; | ||||
|     private String mVideoPath; | ||||
|     private MediaProjectionManager mMpmngr; | ||||
|     private MediaProjection mMpj; | ||||
|     private VirtualDisplay mVirtualDisplay; | ||||
|     private int windowWidth; | ||||
|     private int windowHeight; | ||||
|     private int screenDensity; | ||||
| 
 | ||||
|     private Surface mSurface; | ||||
|     private MediaCodec mMediaCodec; | ||||
| 
 | ||||
|     private WindowManager wm; | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     MediaStream.CodecInfo info = new MediaStream.CodecInfo(); | ||||
|     private MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo(); | ||||
| 
 | ||||
|     private Thread mPushThread; | ||||
|     private byte[] mPpsSps; | ||||
|     private BroadcastReceiver mReceiver = new BroadcastReceiver() { | ||||
|         @Override | ||||
|         public void onReceive(Context context, Intent intent) { | ||||
|             Application app = (Application) context.getApplicationContext(); | ||||
|             MediaStream.stopPushScreen(app); | ||||
|         } | ||||
|     }; | ||||
|     private final Pusher mEasyPusher = new EasyPusher(); | ||||
|     private String ip; | ||||
|     private String port; | ||||
|     private String id; | ||||
|     private MediaStream.PushingScreenLiveData liveData; | ||||
| 
 | ||||
| 
 | ||||
|     public class MyBinder extends Binder | ||||
|     { | ||||
|         public PushScreenService getService(){ | ||||
|             return PushScreenService.this; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     MyBinder binder = new MyBinder(); | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public IBinder onBind(Intent intent) { | ||||
|         return binder; | ||||
|     } | ||||
| 
 | ||||
|     @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) | ||||
|     @Override | ||||
|     public void onCreate() { | ||||
|         super.onCreate(); | ||||
|         mMpmngr = (MediaProjectionManager) getApplicationContext().getSystemService(MEDIA_PROJECTION_SERVICE); | ||||
|         createEnvironment(); | ||||
| 
 | ||||
|         registerReceiver(mReceiver,new IntentFilter(ACTION_CLOSE_PUSHING_SCREEN)); | ||||
|     } | ||||
| 
 | ||||
|     @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) | ||||
|     private void configureMedia() throws IOException { | ||||
|         MediaStream.initEncoder(this, info); | ||||
|         if (TextUtils.isEmpty(info.mName) && info.mColorFormat == 0){ | ||||
|             throw new IOException("media codec init error"); | ||||
|         } | ||||
|         MediaFormat mediaFormat = MediaFormat.createVideoFormat(info.mime, windowWidth, windowHeight); | ||||
|         mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 1200000); | ||||
|         mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 25); | ||||
|         mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); | ||||
|         mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); | ||||
|         mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); | ||||
|         mediaFormat.setInteger(MediaFormat.KEY_CAPTURE_RATE, 25); | ||||
|         mediaFormat.setInteger(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, 1000000); | ||||
|         mMediaCodec = MediaCodec.createByCodecName(info.mName); | ||||
|         mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); | ||||
|         mSurface = mMediaCodec.createInputSurface(); | ||||
|         mMediaCodec.start(); | ||||
|     } | ||||
| 
 | ||||
|     private void createEnvironment() { | ||||
|         mVideoPath = Environment.getExternalStorageDirectory().getPath() + "/"; | ||||
|         wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); | ||||
|         windowWidth = wm.getDefaultDisplay().getWidth(); | ||||
|         windowHeight = wm.getDefaultDisplay().getHeight(); | ||||
|         DisplayMetrics displayMetrics = new DisplayMetrics(); | ||||
|         wm.getDefaultDisplay().getMetrics(displayMetrics); | ||||
|         screenDensity = displayMetrics.densityDpi; | ||||
| 
 | ||||
|         while (windowWidth > 480){ | ||||
|             windowWidth /= 2; | ||||
|             windowHeight /=2; | ||||
|         } | ||||
| 
 | ||||
|         windowWidth /= 16; | ||||
|         windowWidth *= 16; | ||||
| 
 | ||||
| 
 | ||||
|         windowHeight /= 16; | ||||
|         windowHeight *= 16; | ||||
|     } | ||||
| 
 | ||||
|     private void startPush() { | ||||
| //        liveData.postValue(new MediaStream.PushingState(0, "未开始", true));
 | ||||
|         mPushThread = new Thread(){ | ||||
|             @TargetApi(Build.VERSION_CODES.LOLLIPOP) | ||||
|             @Override | ||||
|             public void run() { | ||||
| 
 | ||||
|                 startForeground(111, new Notification.Builder(PushScreenService.this).setContentTitle(getString(R.string.screen_pushing)) | ||||
|                         .setSmallIcon(R.drawable.ic_pusher_screen_pushing) | ||||
|                         .addAction(new Notification.Action(R.drawable.ic_close_pushing_screen, "关闭", | ||||
|                                 PendingIntent.getBroadcast(getApplicationContext(), 10000, new Intent(ACTION_CLOSE_PUSHING_SCREEN), FLAG_CANCEL_CURRENT))).build()); | ||||
| 
 | ||||
|                 final String url = String.format("rtsp://%s:%s/%s.sdp", ip, port, id); | ||||
|                 InitCallback _callback = new InitCallback() { | ||||
|                     @Override | ||||
|                     public void onCallback(int code) { | ||||
|                         String msg = ""; | ||||
|                         switch (code) { | ||||
|                             case EasyPusher.OnInitPusherCallback.CODE.EASY_ACTIVATE_INVALID_KEY: | ||||
|                                 msg = ("无效Key"); | ||||
|                                 break; | ||||
|                             case EasyPusher.OnInitPusherCallback.CODE.EASY_ACTIVATE_SUCCESS: | ||||
|                                 msg = ("未开始"); | ||||
|                                 break; | ||||
|                             case EasyPusher.OnInitPusherCallback.CODE.EASY_PUSH_STATE_CONNECTING: | ||||
|                                 msg = ("连接中"); | ||||
|                                 break; | ||||
|                             case EasyPusher.OnInitPusherCallback.CODE.EASY_PUSH_STATE_CONNECTED: | ||||
|                                 msg = ("连接成功"); | ||||
|                                 break; | ||||
|                             case EasyPusher.OnInitPusherCallback.CODE.EASY_PUSH_STATE_CONNECT_FAILED: | ||||
|                                 msg = ("连接失败"); | ||||
|                                 break; | ||||
|                             case EasyPusher.OnInitPusherCallback.CODE.EASY_PUSH_STATE_CONNECT_ABORT: | ||||
|                                 msg = ("连接异常中断"); | ||||
|                                 break; | ||||
|                             case EasyPusher.OnInitPusherCallback.CODE.EASY_PUSH_STATE_PUSHING: | ||||
|                                 msg = ("推流中"); | ||||
|                                 break; | ||||
|                             case EasyPusher.OnInitPusherCallback.CODE.EASY_PUSH_STATE_DISCONNECTED: | ||||
|                                 msg = ("断开连接"); | ||||
|                                 break; | ||||
|                             case EasyPusher.OnInitPusherCallback.CODE.EASY_ACTIVATE_PLATFORM_ERR: | ||||
|                                 msg = ("平台不匹配"); | ||||
|                                 break; | ||||
|                             case EasyPusher.OnInitPusherCallback.CODE.EASY_ACTIVATE_COMPANY_ID_LEN_ERR: | ||||
|                                 msg = ("授权使用商不匹配"); | ||||
|                                 break; | ||||
|                             case EasyPusher.OnInitPusherCallback.CODE.EASY_ACTIVATE_PROCESS_NAME_LEN_ERR: | ||||
|                                 msg = ("进程名称长度不匹配"); | ||||
|                                 break; | ||||
|                         } | ||||
|                         liveData.postValue(new MediaStream.PushingState(url, code, msg, true)); | ||||
|                     } | ||||
|                 }; | ||||
| //        startStream(ip, port, id, _callback);
 | ||||
|                 mEasyPusher.initPush( getApplicationContext(), _callback); | ||||
|                 MediaStream.PushingState.sCodec = (info.hevcEncode ? "hevc":"avc"); | ||||
|                 mEasyPusher.setMediaInfo(info.hevcEncode ? Pusher.Codec.EASY_SDK_VIDEO_CODEC_H265:Pusher.Codec.EASY_SDK_VIDEO_CODEC_H264, 25, Pusher.Codec.EASY_SDK_AUDIO_CODEC_AAC, 1, 8000, 16); | ||||
|                 mEasyPusher.start(ip, port, String.format("%s.sdp", id), Pusher.TransType.EASY_RTP_OVER_TCP); | ||||
|                 try { | ||||
|                     byte[] h264 = new byte[102400]; | ||||
|                     while (mPushThread != null) { | ||||
|                         int index = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10000); | ||||
|                         Log.d(TAG, "dequeue output buffer index=" + index); | ||||
| 
 | ||||
|                         if (index == MediaCodec.INFO_TRY_AGAIN_LATER) {//请求超时
 | ||||
|                             try { | ||||
|                                 // wait 10ms
 | ||||
|                                 Thread.sleep(10); | ||||
|                             } catch (InterruptedException e) { | ||||
|                             } | ||||
|                         } else if (index >= 0) {//有效输出
 | ||||
|                             ByteBuffer outputBuffer = mMediaCodec.getOutputBuffer(index); | ||||
|                             outputBuffer.position(mBufferInfo.offset); | ||||
|                             outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size); | ||||
| 
 | ||||
| 
 | ||||
|                             boolean sync = false; | ||||
|                             if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {// sps
 | ||||
|                                 sync = (mBufferInfo.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0; | ||||
|                                 if (!sync) { | ||||
|                                     byte[] temp = new byte[mBufferInfo.size]; | ||||
|                                     outputBuffer.get(temp); | ||||
|                                     mPpsSps = temp; | ||||
|                                     mMediaCodec.releaseOutputBuffer(index, false); | ||||
|                                     continue; | ||||
|                                 } else { | ||||
|                                     mPpsSps = new byte[0]; | ||||
|                                 } | ||||
|                             } | ||||
|                             sync |= (mBufferInfo.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0; | ||||
|                             int len = mPpsSps.length + mBufferInfo.size; | ||||
|                             if (len > h264.length) { | ||||
|                                 h264 = new byte[len]; | ||||
|                             } | ||||
|                             if (sync) { | ||||
|                                 System.arraycopy(mPpsSps, 0, h264, 0, mPpsSps.length); | ||||
|                                 outputBuffer.get(h264, mPpsSps.length, mBufferInfo.size); | ||||
|                                 mEasyPusher.push(h264, 0, mPpsSps.length + mBufferInfo.size, mBufferInfo.presentationTimeUs / 1000, 1); | ||||
|                                 if (BuildConfig.DEBUG) | ||||
|                                     Log.i(TAG, String.format("push i video stamp:%d", mBufferInfo.presentationTimeUs / 1000)); | ||||
|                             } else { | ||||
|                                 outputBuffer.get(h264, 0, mBufferInfo.size); | ||||
|                                 mEasyPusher.push(h264, 0, mBufferInfo.size, mBufferInfo.presentationTimeUs / 1000, 1); | ||||
|                                 if (BuildConfig.DEBUG) | ||||
|                                     Log.i(TAG, String.format("push video stamp:%d", mBufferInfo.presentationTimeUs / 1000)); | ||||
|                             } | ||||
| 
 | ||||
| 
 | ||||
|                             mMediaCodec.releaseOutputBuffer(index, false); | ||||
|                         } | ||||
| 
 | ||||
|                     } | ||||
|                     stopForeground(true); | ||||
|                 }finally { | ||||
|                     mEasyPusher.stop(); | ||||
|                     liveData.postValue(new MediaStream.PushingState("", 0, "未开始", true)); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|         mPushThread.start(); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     @TargetApi(Build.VERSION_CODES.LOLLIPOP) | ||||
|     private void stopPush(){ | ||||
|         Thread t = mPushThread; | ||||
|         if (t != null){ | ||||
|             mPushThread = null; | ||||
|             try { | ||||
|                 t.join(); | ||||
|             } catch (InterruptedException e) { | ||||
|                 e.printStackTrace(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @TargetApi(Build.VERSION_CODES.LOLLIPOP) | ||||
|     void startVirtualDisplay(int resultCode, Intent resultData, String ip, String port, String id, final MediaStream.PushingScreenLiveData liveData) { | ||||
| 
 | ||||
|         try { | ||||
|             configureMedia(); | ||||
|         } catch (IOException e) { | ||||
|             e.printStackTrace(); | ||||
|             liveData.postValue(new MediaStream.PushingState("",-1, "编码器初始化错误", true)); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (mMpj == null) { | ||||
|             mMpj = mMpmngr.getMediaProjection(resultCode, resultData); | ||||
|         } | ||||
|         if (mMpj == null) { | ||||
|             liveData.postValue(new MediaStream.PushingState("",-1, "未知错误", true)); | ||||
|             return; | ||||
|         } | ||||
|         mVirtualDisplay = mMpj.createVirtualDisplay("record_screen", windowWidth, windowHeight, screenDensity, | ||||
|                 DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR|DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC|DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION, mSurface, null, null); | ||||
| 
 | ||||
| 
 | ||||
|         this.ip = ip; | ||||
|         this.port = port; | ||||
|         this.id = id; | ||||
|         this.liveData = liveData; | ||||
| 
 | ||||
|         startPush(); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     @TargetApi(Build.VERSION_CODES.LOLLIPOP) | ||||
|     private void encodeToVideoTrack(int index) { | ||||
|         ByteBuffer encodedData = mMediaCodec.getOutputBuffer(index); | ||||
| 
 | ||||
|         if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {//是编码需要的特定数据,不是媒体数据
 | ||||
|             // The codec config data was pulled out and fed to the muxer when we got
 | ||||
|             // the INFO_OUTPUT_FORMAT_CHANGED status.
 | ||||
|             // Ignore it.
 | ||||
|             Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG"); | ||||
|             mBufferInfo.size = 0; | ||||
|         } | ||||
|         if (mBufferInfo.size == 0) { | ||||
|             Log.d(TAG, "info.size == 0, drop it."); | ||||
|             encodedData = null; | ||||
|         } else { | ||||
|             Log.d(TAG, "got buffer, info: size=" + mBufferInfo.size | ||||
|                     + ", presentationTimeUs=" + mBufferInfo.presentationTimeUs | ||||
|                     + ", offset=" + mBufferInfo.offset); | ||||
|         } | ||||
|         if (encodedData != null) { | ||||
|             encodedData.position(mBufferInfo.offset); | ||||
|             encodedData.limit(mBufferInfo.offset + mBufferInfo.size); | ||||
| //            mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo);//写入
 | ||||
|             Log.i(TAG, "sent " + mBufferInfo.size + " bytes to muxer..."); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     @TargetApi(Build.VERSION_CODES.KITKAT) | ||||
|     private void release() { | ||||
| 
 | ||||
|         Log.i(TAG, " release() "); | ||||
|         if (mMediaCodec != null) { | ||||
|             mMediaCodec.stop(); | ||||
|             mMediaCodec.release(); | ||||
|             mMediaCodec = null; | ||||
|         } | ||||
|         if (mSurface != null){ | ||||
|             mSurface.release(); | ||||
|         } | ||||
|         if (mVirtualDisplay != null) { | ||||
|             mVirtualDisplay.release(); | ||||
|             mVirtualDisplay = null; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @TargetApi(Build.VERSION_CODES.LOLLIPOP) | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         stopPush(); | ||||
|         release(); | ||||
|         if (mMpj != null) { | ||||
|             mMpj.stop(); | ||||
|         } | ||||
|         unregisterReceiver(mReceiver); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,40 @@ | ||||
| package org.easydarwin.push; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| 
 | ||||
| /** | ||||
|  * Created by john on 2017/5/6. | ||||
|  */ | ||||
| 
 | ||||
| public interface Pusher { | ||||
| 
 | ||||
|     public static class Codec { | ||||
|         /* 视频编码 */ | ||||
|         public static final int EASY_SDK_VIDEO_CODEC_H264 = 0x1C; | ||||
|         public static final int EASY_SDK_VIDEO_CODEC_H265 = 0x48323635; | ||||
| 
 | ||||
|         /* 音频编码 */ | ||||
|         public static final int EASY_SDK_AUDIO_CODEC_AAC = 0x15002; | ||||
|         public static final int EASY_SDK_AUDIO_CODEC_G711U = 0x10006; | ||||
|         public static final int EASY_SDK_AUDIO_CODEC_G711A = 0x10007; | ||||
|         public static final int EASY_SDK_AUDIO_CODEC_G726 = 0x1100B; | ||||
|     } | ||||
| 
 | ||||
|     public static class TransType { | ||||
|         public static final int EASY_RTP_OVER_TCP = 1;   //TCP推送
 | ||||
|         public static final int EASY_RTP_OVER_UDP = 2;   //UDP推送
 | ||||
|     } | ||||
| 
 | ||||
|     public void stop() ; | ||||
| 
 | ||||
|     public  void initPush(final Context context, final InitCallback callback); | ||||
|     public  void initPush(final String url, final Context context, final InitCallback callback, int pts); | ||||
|     public  void initPush(final String url, final Context context, final InitCallback callback); | ||||
| 
 | ||||
|     public void setMediaInfo(int videoCodec, int videoFPS, int audioCodec, int audioChannel, int audioSamplerate, int audioBitPerSample); | ||||
|     public void start(String serverIP, String serverPort, String streamName, int transType); | ||||
| 
 | ||||
|     public  void push(byte[] data, int offset, int length, long timestamp, int type); | ||||
| 
 | ||||
|     public  void push(byte[] data, long timestamp, int type); | ||||
| } | ||||
| @ -0,0 +1,129 @@ | ||||
| package org.easydarwin.push; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import org.easydarwin.muxer.EasyMuxer; | ||||
| import org.easydarwin.sw.JNIUtil; | ||||
| import org.easydarwin.sw.X264Encoder; | ||||
| 
 | ||||
| import java.util.concurrent.ArrayBlockingQueue; | ||||
| 
 | ||||
| /** | ||||
|  * Created by apple on 2017/5/13. | ||||
|  */ | ||||
| 
 | ||||
| public class SWConsumer extends Thread implements VideoConsumer { | ||||
|     private static final String TAG = "SWConsumer"; | ||||
|     private int mHeight; | ||||
|     private int mWidth; | ||||
|     private X264Encoder x264; | ||||
|     private final Pusher mPusher; | ||||
|     private volatile boolean mVideoStarted; | ||||
|     public SWConsumer(Context context, Pusher pusher){ | ||||
|         mPusher = pusher; | ||||
|     } | ||||
|     @Override | ||||
|     public void onVideoStart(int width, int height) { | ||||
|         this.mWidth = width; | ||||
|         this.mHeight = height; | ||||
| 
 | ||||
|         x264 = new X264Encoder(); | ||||
|         int bitrate = (int) (mWidth*mHeight*20*2*0.07f); | ||||
|         x264.create(width, height, 20, bitrate/500); | ||||
|         mVideoStarted = true; | ||||
|         start(); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     class TimedBuffer { | ||||
|         byte[] buffer; | ||||
|         long time; | ||||
| 
 | ||||
|         public TimedBuffer(byte[] data) { | ||||
|             buffer = data; | ||||
|             time = System.currentTimeMillis(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private ArrayBlockingQueue<TimedBuffer> yuvs = new ArrayBlockingQueue<TimedBuffer>(2); | ||||
|     private ArrayBlockingQueue<byte[]> yuv_caches = new ArrayBlockingQueue<byte[]>(10); | ||||
| 
 | ||||
|     @Override | ||||
|     public void run(){ | ||||
| 
 | ||||
|         byte[]h264 = new byte[mWidth*mHeight*3/2]; | ||||
|         byte[] keyFrm = new byte[1]; | ||||
|         int []outLen = new int[1]; | ||||
|         do { | ||||
|             try { | ||||
|                 int r = 0; | ||||
|                 TimedBuffer tb = yuvs.take(); | ||||
|                 byte[] data = tb.buffer; | ||||
|                 long begin = System.currentTimeMillis(); | ||||
|                 r = x264.encode(data, 0, h264, 0, outLen, keyFrm); | ||||
|                 if (r > 0) { | ||||
|                     Log.i(TAG, String.format("encode spend:%d ms. keyFrm:%d", System.currentTimeMillis() - begin, keyFrm[0])); | ||||
| //                                newBuf = new byte[outLen[0]];
 | ||||
| //                                System.arraycopy(h264, 0, newBuf, 0, newBuf.length);
 | ||||
|                 } | ||||
|                 keyFrm[0] = 0; | ||||
|                 yuv_caches.offer(data); | ||||
|                 mPusher.push(h264, 0, outLen[0], tb.time, 1); | ||||
|             } catch (InterruptedException e) { | ||||
|                 e.printStackTrace(); | ||||
|             } | ||||
|         }while (mVideoStarted); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     final int millisPerframe = 1000/20; | ||||
|     long lastPush = 0; | ||||
|     @Override | ||||
|     public int onVideo(byte[] data, int format) { | ||||
|         try { | ||||
|             if (lastPush == 0) { | ||||
|                 lastPush = System.currentTimeMillis(); | ||||
|             } | ||||
|             long time = System.currentTimeMillis() - lastPush; | ||||
|             if (time >= 0) { | ||||
|                 time = millisPerframe - time; | ||||
|                 if (time > 0) Thread.sleep(time / 2); | ||||
|             } | ||||
|             byte[] buffer = yuv_caches.poll(); | ||||
|             if (buffer == null || buffer.length != data.length) { | ||||
|                 buffer = new byte[data.length]; | ||||
|             } | ||||
|             System.arraycopy(data, 0, buffer, 0, data.length); | ||||
|             JNIUtil.yuvConvert(buffer, mWidth, mHeight, 4); | ||||
|             yuvs.offer(new TimedBuffer(buffer)); | ||||
|             if (time > 0) Thread.sleep(time / 2); | ||||
|             lastPush = System.currentTimeMillis(); | ||||
|         }catch (InterruptedException ex){ | ||||
|             ex.printStackTrace(); | ||||
|         } | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onVideoStop() { | ||||
|         do { | ||||
|             mVideoStarted = false; | ||||
|             try { | ||||
|                 interrupt(); | ||||
|                 join(); | ||||
|             } catch (InterruptedException e) { | ||||
|                 e.printStackTrace(); | ||||
|             } | ||||
|         }while (isAlive()); | ||||
|         if (x264 != null) { | ||||
|             x264.close(); | ||||
|         } | ||||
|         x264 = null; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setMuxer(EasyMuxer muxer) { | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,200 @@ | ||||
| package org.easydarwin.push; | ||||
| 
 | ||||
| import android.app.Service; | ||||
| import androidx.lifecycle.LiveData; | ||||
| import android.content.Intent; | ||||
| import android.hardware.usb.UsbDevice; | ||||
| import android.os.Binder; | ||||
| import android.os.IBinder; | ||||
| import android.util.Log; | ||||
| import android.util.SparseArray; | ||||
| import android.widget.Toast; | ||||
| 
 | ||||
| import com.serenegiant.usb.DeviceFilter; | ||||
| import com.serenegiant.usb.IButtonCallback; | ||||
| import com.serenegiant.usb.IStatusCallback; | ||||
| import com.serenegiant.usb.USBMonitor; | ||||
| import com.serenegiant.usb.UVCCamera; | ||||
| 
 | ||||
| import org.easydarwin.easypusher.BuildConfig; | ||||
| import org.easydarwin.easypusher.R; | ||||
| 
 | ||||
| import java.nio.ByteBuffer; | ||||
| 
 | ||||
| public class UVCCameraService extends Service { | ||||
| 
 | ||||
| 
 | ||||
|     public static class UVCCameraLivaData extends LiveData<UVCCamera>{ | ||||
|         @Override | ||||
|         protected void postValue(UVCCamera value) { | ||||
|             super.postValue(value); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static final UVCCameraLivaData liveData = new UVCCameraLivaData(); | ||||
|     public static class MyUVCCamera extends UVCCamera { | ||||
| 
 | ||||
|         boolean prev = false; | ||||
|         @Override | ||||
|         public synchronized void startPreview() { | ||||
|             if (prev ) return; | ||||
|             super.startPreview(); | ||||
|             prev = true; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public synchronized void stopPreview() { | ||||
|             if (!prev )return; | ||||
|             super.stopPreview(); | ||||
|             prev = false; | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public synchronized void destroy() { | ||||
|             prev = false; | ||||
|             super.destroy(); | ||||
|         } | ||||
|     } | ||||
|     private static final String TAG = "OutterCamera"; | ||||
|     private USBMonitor mUSBMonitor; | ||||
|     private UVCCamera mUVCCamera; | ||||
| 
 | ||||
|     private SparseArray<UVCCamera> cameras = new SparseArray<>(); | ||||
| 
 | ||||
|     public class MyBinder extends Binder { | ||||
| 
 | ||||
|         public UVCCameraService getService() { | ||||
|             return UVCCameraService.this; | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     MyBinder binder = new MyBinder(); | ||||
| 
 | ||||
|     @Override | ||||
|     public IBinder onBind(Intent intent) { | ||||
|         return binder; | ||||
|     } | ||||
| 
 | ||||
|     public UVCCamera getCamera() { | ||||
|         return mUVCCamera; | ||||
|     } | ||||
| 
 | ||||
|     private void releaseCamera() { | ||||
|         if (mUVCCamera != null) { | ||||
|             try { | ||||
|                 mUVCCamera.close(); | ||||
|                 mUVCCamera.destroy(); | ||||
|                 mUVCCamera = null; | ||||
|             } catch (final Exception e) { | ||||
|                 //
 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate() { | ||||
|         super.onCreate(); | ||||
| 
 | ||||
|         mUSBMonitor = new USBMonitor(this, new USBMonitor.OnDeviceConnectListener() { | ||||
|             @Override | ||||
|             public void onAttach(final UsbDevice device) { | ||||
|                 Log.v(TAG, "onAttach:" + device); | ||||
|                 mUSBMonitor.requestPermission(device); | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onConnect(final UsbDevice device, final USBMonitor.UsbControlBlock ctrlBlock, final boolean createNew) { | ||||
|                 releaseCamera(); | ||||
|                 if (BuildConfig.DEBUG) Log.v(TAG, "onConnect:"); | ||||
|                 try { | ||||
|                     final UVCCamera camera = new MyUVCCamera(); | ||||
|                     camera.open(ctrlBlock); | ||||
|                     camera.setStatusCallback(new IStatusCallback() { | ||||
|                         @Override | ||||
|                         public void onStatus(final int statusClass, final int event, final int selector, | ||||
|                                              final int statusAttribute, final ByteBuffer data) { | ||||
|                             Log.i(TAG, "onStatus(statusClass=" + statusClass | ||||
|                                     + "; " + | ||||
|                                     "event=" + event + "; " + | ||||
|                                     "selector=" + selector + "; " + | ||||
|                                     "statusAttribute=" + statusAttribute + "; " + | ||||
|                                     "data=...)"); | ||||
|                         } | ||||
|                     }); | ||||
|                     camera.setButtonCallback(new IButtonCallback() { | ||||
|                         @Override | ||||
|                         public void onButton(final int button, final int state) { | ||||
|                             Log.i(TAG, "onButton(button=" + button + "; " + "state=" + state + ")"); | ||||
|                         } | ||||
|                     }); | ||||
| //					camera.setPreviewTexture(camera.getSurfaceTexture());
 | ||||
|                     mUVCCamera = camera; | ||||
|                     liveData.postValue(camera); | ||||
| 
 | ||||
|                     Toast.makeText(UVCCameraService.this, "UVCCamera connected!", Toast.LENGTH_SHORT).show(); | ||||
|                     if (device != null) | ||||
|                         cameras.append(device.getDeviceId(), camera); | ||||
|                 }catch (Exception ex){ | ||||
|                     ex.printStackTrace(); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onDisconnect(final UsbDevice device, final USBMonitor.UsbControlBlock ctrlBlock) { | ||||
|                 Log.v(TAG, "onDisconnect:"); | ||||
| //                Toast.makeText(MainActivity.this, R.string.usb_camera_disconnected, Toast.LENGTH_SHORT).show();
 | ||||
| 
 | ||||
| //                releaseCamera();
 | ||||
| 
 | ||||
|                 if (device != null) { | ||||
|                     UVCCamera camera = cameras.get(device.getDeviceId()); | ||||
|                     if (mUVCCamera == camera) { | ||||
|                         mUVCCamera = null; | ||||
|                         Toast.makeText(UVCCameraService.this, "UVCCamera disconnected!", Toast.LENGTH_SHORT).show(); | ||||
|                         liveData.postValue(null); | ||||
|                     } | ||||
|                     cameras.remove(device.getDeviceId()); | ||||
|                 }else { | ||||
|                     Toast.makeText(UVCCameraService.this, "UVCCamera disconnected!", Toast.LENGTH_SHORT).show(); | ||||
|                     mUVCCamera = null; | ||||
|                     liveData.postValue(null); | ||||
|                 } | ||||
| 
 | ||||
| //                if (mUSBMonitor != null) {
 | ||||
| //                    mUSBMonitor.destroy();
 | ||||
| //                }
 | ||||
| //
 | ||||
| //                mUSBMonitor = new USBMonitor(OutterCameraService.this, this);
 | ||||
| //                mUSBMonitor.setDeviceFilter(DeviceFilter.getDeviceFilters(OutterCameraService.this, R.xml.device_filter));
 | ||||
| //                mUSBMonitor.register();
 | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onCancel(UsbDevice usbDevice) { | ||||
|                 releaseCamera(); | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onDettach(final UsbDevice device) { | ||||
|                 Log.v(TAG, "onDettach:"); | ||||
|                 releaseCamera(); | ||||
| //                AppContext.getInstance().bus.post(new UVCCameraDisconnect());
 | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         mUSBMonitor.setDeviceFilter(DeviceFilter.getDeviceFilters(this, R.xml.device_filter)); | ||||
|         mUSBMonitor.register(); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         releaseCamera(); | ||||
|         if (mUSBMonitor != null) { | ||||
|             mUSBMonitor.unregister(); | ||||
|         } | ||||
|         super.onDestroy(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,19 @@ | ||||
| package org.easydarwin.push; | ||||
| 
 | ||||
| import org.easydarwin.muxer.EasyMuxer; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| 
 | ||||
| /** | ||||
|  * Created by apple on 2017/5/13. | ||||
|  */ | ||||
| 
 | ||||
| public interface VideoConsumer { | ||||
|     public void onVideoStart(int width, int height) throws IOException; | ||||
| 
 | ||||
|     public int onVideo(byte[] data, int format); | ||||
| 
 | ||||
|     public void onVideoStop(); | ||||
| 
 | ||||
|     public void setMuxer(EasyMuxer muxer); | ||||
| } | ||||
| @ -0,0 +1,59 @@ | ||||
| package org.easydarwin.sw; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.text.TextUtils; | ||||
| 
 | ||||
| import java.io.File; | ||||
| 
 | ||||
| /** | ||||
|  * Created by John on 2017/2/23. | ||||
|  */ | ||||
| 
 | ||||
| public class TxtOverlay { | ||||
| 
 | ||||
|     static { | ||||
|         System.loadLibrary("TxtOverlay"); | ||||
|     } | ||||
| 
 | ||||
|     private final Context context; | ||||
| 
 | ||||
|     public TxtOverlay(Context context){ | ||||
|         this.context = context; | ||||
|     } | ||||
|     private long ctx; | ||||
| 
 | ||||
|     public void init(int width, int height,String fonts) { | ||||
|         if (TextUtils.isEmpty(fonts)){ | ||||
|             throw new IllegalArgumentException("the font file must be valid!"); | ||||
|         } | ||||
|         if (!new File(fonts).exists()){ | ||||
|             throw new IllegalArgumentException("the font file must be exists!"); | ||||
|         } | ||||
|         ctx = txtOverlayInit(width, height,fonts); | ||||
|     } | ||||
| 
 | ||||
|     public void overlay(byte[] data, | ||||
|                         String txt) { | ||||
| //        txt = "drawtext=fontfile="+context.getFileStreamPath("SIMYOU.ttf")+": text='EasyPusher 2017':x=(w-text_w)/2:y=H-60 :fontcolor=white :box=1:boxcolor=0x00000000@0.3";
 | ||||
| //        txt = "movie=/sdcard/qrcode.png [logo];[in][logo] "
 | ||||
| //                + "overlay=" + 0 + ":" + 0
 | ||||
| //                + " [out]";
 | ||||
| //        if (ctx == 0) throw new RuntimeException("init should be called at first!");
 | ||||
|         if (ctx == 0) return; | ||||
|         txtOverlay(ctx, data, txt); | ||||
|     } | ||||
| 
 | ||||
|     public void release() { | ||||
|         if (ctx == 0) return; | ||||
|         txtOverlayRelease(ctx); | ||||
|         ctx = 0; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private static native long txtOverlayInit(int width, int height, String fonts); | ||||
| 
 | ||||
|     private static native void txtOverlay(long ctx, byte[] data, String txt); | ||||
| 
 | ||||
|     private static native void txtOverlayRelease(long ctx); | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,30 @@ | ||||
| package org.easydarwin.util; | ||||
| 
 | ||||
| import org.reactivestreams.Subscriber; | ||||
| import org.reactivestreams.Subscription; | ||||
| 
 | ||||
| /** | ||||
|  * Created by apple on 2017/10/21. | ||||
|  */ | ||||
| 
 | ||||
| public abstract class AbstractSubscriber<T> implements Subscriber<T> { | ||||
|     @Override | ||||
|     public void onSubscribe(Subscription s) { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onNext(T t) { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onError(Throwable t) { | ||||
|         t.printStackTrace(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onComplete() { | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| After Width: | Height: | Size: 266 B | 
| After Width: | Height: | Size: 230 B | 
| After Width: | Height: | Size: 198 B | 
| After Width: | Height: | Size: 176 B | 
| After Width: | Height: | Size: 323 B | 
| After Width: | Height: | Size: 247 B | 
| After Width: | Height: | Size: 531 B | 
| After Width: | Height: | Size: 375 B | 
| @ -0,0 +1,4 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|     <string name="screen_pushing">正在推送屏幕</string> | ||||
| </resources> | ||||
| @ -0,0 +1,22 @@ | ||||
| /* | ||||
| 	Copyright (c) 2013-2016 EasyDarwin.ORG.  All rights reserved. | ||||
| 	Github: https://github.com/EasyDarwin
 | ||||
| 	WEChat: EasyDarwin | ||||
| 	Website: http://www.easydarwin.org
 | ||||
| */ | ||||
| 
 | ||||
| package org.easydarwin.easypusher; | ||||
| 
 | ||||
| import org.junit.Test; | ||||
| 
 | ||||
| import static org.junit.Assert.*; | ||||
| 
 | ||||
| /** | ||||
|  * To work on unit tests, switch the Test Artifact in the Build Variants view. | ||||
|  */ | ||||
| public class ExampleUnitTest { | ||||
|     @Test | ||||
|     public void addition_isCorrect() throws Exception { | ||||
|         assertEquals(4, 2 + 2); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1 @@ | ||||
| /build | ||||
| @ -0,0 +1,21 @@ | ||||
| # Add project specific ProGuard rules here. | ||||
| # You can control the set of applied configuration files using the | ||||
| # proguardFiles setting in build.gradle. | ||||
| # | ||||
| # For more details, see | ||||
| #   http://developer.android.com/guide/developing/tools/proguard.html | ||||
| 
 | ||||
| # 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 *; | ||||
| #} | ||||
| 
 | ||||
| # Uncomment this to preserve the line number information for | ||||
| # debugging stack traces. | ||||
| #-keepattributes SourceFile,LineNumberTable | ||||
| 
 | ||||
| # If you keep the line number information, uncomment this to | ||||
| # hide the original source file name. | ||||
| #-renamesourcefileattribute SourceFile | ||||
| @ -0,0 +1,54 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     package="com.example.myapplication"> | ||||
| 
 | ||||
|     <uses-feature | ||||
|         android:name="android.hardware.usb.host" | ||||
|         android:required="true" /> | ||||
| 
 | ||||
|     <uses-permission android:name="android.permission.INTERNET" /> | ||||
|     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | ||||
|     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> | ||||
|     <uses-permission android:name="android.permission.CAMERA" /> | ||||
|     <uses-permission android:name="android.permission.RECORD_AUDIO" /> | ||||
|     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> | ||||
| 
 | ||||
|     <uses-feature | ||||
|         android:glEsVersion="0x00020000" | ||||
|         android:required="true" /> | ||||
| 
 | ||||
|     <application | ||||
|         android:allowBackup="true" | ||||
|         android:icon="@mipmap/ic_launcher" | ||||
|         android:label="@string/app_name" | ||||
|         android:roundIcon="@mipmap/ic_launcher_round" | ||||
|         android:supportsRtl="true" | ||||
|         android:theme="@style/AppTheme"> | ||||
|         <activity | ||||
|             android:name=".MainActivity" | ||||
|             android:exported="true" | ||||
|             android:launchMode="singleInstance"> | ||||
| 
 | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.MAIN" /> | ||||
| 
 | ||||
|                 <category android:name="android.intent.category.LAUNCHER" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".UVCActivity" | ||||
|             android:exported="false" | ||||
|             android:launchMode="singleInstance"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.hardware.usb.action.USB_DEVICE_DETACHED" /> | ||||
|             </intent-filter> | ||||
| 
 | ||||
|             <meta-data | ||||
|                 android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" | ||||
|                 android:resource="@xml/device_filter" /> | ||||
|         </activity> | ||||
|     </application> | ||||
| </manifest> | ||||
| @ -0,0 +1,238 @@ | ||||
| package com.example.myapplication; | ||||
| 
 | ||||
| import androidx.lifecycle.Observer; | ||||
| import android.content.Intent; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.graphics.SurfaceTexture; | ||||
| import android.media.projection.MediaProjectionManager; | ||||
| import android.os.Build; | ||||
| import android.os.Bundle; | ||||
| import android.preference.PreferenceManager; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.core.app.ActivityCompat; | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import android.view.TextureView; | ||||
| import android.view.View; | ||||
| import android.widget.CheckBox; | ||||
| import android.widget.CompoundButton; | ||||
| import android.widget.TextView; | ||||
| import android.widget.Toast; | ||||
| 
 | ||||
| import org.easydarwin.push.MediaStream; | ||||
| 
 | ||||
| import io.reactivex.Single; | ||||
| import io.reactivex.functions.Consumer; | ||||
| 
 | ||||
| public class MainActivity extends AppCompatActivity { | ||||
| 
 | ||||
|     private static final int REQUEST_CAMERA_PERMISSION = 1000; | ||||
|     private static final int REQUEST_MEDIA_PROJECTION = 1001; | ||||
|     public static final String HOST = "cloud.easydarwin.org"; | ||||
|     private MediaStream mediaStream; | ||||
| 
 | ||||
|     private Single<MediaStream> getMediaStream() { | ||||
|         Single<MediaStream> single = RxHelper.single(MediaStream.getBindedMediaStream(this, this), mediaStream); | ||||
|         if (mediaStream == null) { | ||||
|             return single.doOnSuccess(new Consumer<MediaStream>() { | ||||
|                 @Override | ||||
|                 public void accept(MediaStream ms) throws Exception { | ||||
|                     mediaStream = ms; | ||||
|                 } | ||||
|             }); | ||||
|         } else { | ||||
|             return single; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setContentView(R.layout.activity_main); | ||||
| 
 | ||||
|         CheckBox hevc_enable = findViewById(R.id.enable_265); | ||||
|         hevc_enable.setChecked(PreferenceManager.getDefaultSharedPreferences(this).getBoolean("try_265_encode", false)); | ||||
|         hevc_enable.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { | ||||
|             @Override | ||||
|             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { | ||||
|                 PreferenceManager.getDefaultSharedPreferences(MainActivity.this).edit().putBoolean("try_265_encode", isChecked).apply(); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         // 启动服务...
 | ||||
|         Intent intent = new Intent(this, MediaStream.class); | ||||
|         startService(intent); | ||||
| 
 | ||||
|         getMediaStream().subscribe(new Consumer<MediaStream>() { | ||||
|             @Override | ||||
|             public void accept(final MediaStream ms) throws Exception { | ||||
|                 ms.observeCameraPreviewResolution(MainActivity.this, new Observer<int[]>() { | ||||
|                     @Override | ||||
|                     public void onChanged(@Nullable int[] size) { | ||||
|                         Toast.makeText(MainActivity.this, "当前摄像头分辨率为:" + size[0] + "*" + size[1], Toast.LENGTH_SHORT).show(); | ||||
|                     } | ||||
|                 }); | ||||
|                 final TextView pushingStateText = findViewById(R.id.pushing_state); | ||||
|                 final TextView pushingBtn = findViewById(R.id.pushing); | ||||
|                 ms.observePushingState(MainActivity.this, new Observer<MediaStream.PushingState>() { | ||||
| 
 | ||||
|                     @Override | ||||
|                     public void onChanged(@Nullable MediaStream.PushingState pushingState) { | ||||
|                         if (pushingState.screenPushing) { | ||||
|                             pushingStateText.setText("屏幕推送"); | ||||
| 
 | ||||
|                             // 更改屏幕推送按钮状态.
 | ||||
| 
 | ||||
|                             TextView tview = findViewById(R.id.pushing_desktop); | ||||
|                             if (ms.isScreenPushing()) { | ||||
|                                 tview.setText("取消推送"); | ||||
|                             } else { | ||||
|                                 tview.setText("推送屏幕"); | ||||
|                             } | ||||
|                             findViewById(R.id.pushing_desktop).setEnabled(true); | ||||
|                         } else { | ||||
|                             pushingStateText.setText("推送"); | ||||
|                             if (ms.isCameraPushing()) { | ||||
|                                 pushingBtn.setText("停止"); | ||||
|                             } else { | ||||
|                                 pushingBtn.setText("推送"); | ||||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|                         pushingStateText.append(":\t" + pushingState.msg); | ||||
|                         if (pushingState.state > 0) { | ||||
|                             pushingStateText.append(pushingState.url); | ||||
|                             pushingStateText.append("\n"); | ||||
|                             if ("avc".equals(pushingState.videoCodec)) { | ||||
|                                 pushingStateText.append("视频编码方式:" + "H264硬编码"); | ||||
|                             }else if ("hevc".equals(pushingState.videoCodec)) { | ||||
|                                 pushingStateText.append("视频编码方式:"  + "H265硬编码"); | ||||
|                             }else if ("x264".equals(pushingState.videoCodec)) { | ||||
|                                 pushingStateText.append("视频编码方式:"  + "x264"); | ||||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|                     } | ||||
|                 }); | ||||
|                 TextureView textureView = findViewById(R.id.texture_view); | ||||
|                 if (textureView.isAvailable()) { | ||||
|                     ms.setSurfaceTexture(textureView.getSurfaceTexture()); | ||||
|                 } else { | ||||
|                     textureView.setSurfaceTextureListener(new SurfaceTextureListenerWrapper() { | ||||
|                         @Override | ||||
|                         public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) { | ||||
|                             ms.setSurfaceTexture(surfaceTexture); | ||||
|                         } | ||||
| 
 | ||||
|                         @Override | ||||
|                         public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { | ||||
|                             ms.setSurfaceTexture(null); | ||||
|                             return true; | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
| 
 | ||||
|                 if (ActivityCompat.checkSelfPermission(MainActivity.this, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED || | ||||
|                         ActivityCompat.checkSelfPermission(MainActivity.this, android.Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { | ||||
|                     ActivityCompat.requestPermissions(MainActivity.this, new String[]{android.Manifest.permission.CAMERA, android.Manifest.permission.RECORD_AUDIO}, REQUEST_CAMERA_PERMISSION); | ||||
|                 } | ||||
|             } | ||||
|         }, new Consumer<Throwable>() { | ||||
|             @Override | ||||
|             public void accept(Throwable throwable) throws Exception { | ||||
|                 Toast.makeText(MainActivity.this, "创建服务出错!", Toast.LENGTH_SHORT).show(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void onPushing(View view) { | ||||
|         getMediaStream().subscribe(new Consumer<MediaStream>() { | ||||
|             @Override | ||||
|             public void accept(MediaStream mediaStream) throws Exception { | ||||
|                 MediaStream.PushingState state = mediaStream.getPushingState(); | ||||
|                 if (state != null && state.state > 0) { // 终止推送和预览
 | ||||
|                     mediaStream.stopStream(); | ||||
|                     mediaStream.closeCameraPreview(); | ||||
|                 } else {                                // 启动预览和推送.
 | ||||
|                     mediaStream.openCameraPreview(); | ||||
|                     String id = PreferenceManager.getDefaultSharedPreferences(MainActivity.this).getString("caemra-id", null); | ||||
|                     if (id == null) { | ||||
|                         double v = Math.random() * 1000; | ||||
|                         id = "c_" + (int) v; | ||||
|                         PreferenceManager.getDefaultSharedPreferences(MainActivity.this).edit().putString("caemra-id", id).apply(); | ||||
|                     } | ||||
|                     mediaStream.startStream(HOST, "554", id); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     @Override | ||||
|     public void onRequestPermissionsResult(int requestCode, | ||||
|                                            String permissions[], int[] grantResults) { | ||||
|         switch (requestCode) { | ||||
|             case REQUEST_CAMERA_PERMISSION: { | ||||
|                 if (grantResults.length > 1 | ||||
|                         && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) { | ||||
|                     getMediaStream().subscribe(new Consumer<MediaStream>() { | ||||
|                         @Override | ||||
|                         public void accept(MediaStream mediaStream) throws Exception { | ||||
|                             mediaStream.notifyPermissionGranted(); | ||||
|                         } | ||||
|                     }); | ||||
|                 } else { | ||||
|                     finish(); | ||||
|                 } | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // 推送屏幕.
 | ||||
|     public void onPushScreen(final View view) { | ||||
|         getMediaStream().subscribe(new Consumer<MediaStream>() { | ||||
|             @Override | ||||
|             public void accept(MediaStream mediaStream) { | ||||
|                 if (mediaStream.isScreenPushing()) { // 正在推送,那取消推送。
 | ||||
|                     // 取消推送。
 | ||||
|                     mediaStream.stopPushScreen(); | ||||
|                 } else {    // 没在推送,那启动推送。
 | ||||
|                     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {     // lollipop 以前版本不支持。
 | ||||
|                         return; | ||||
|                     } | ||||
|                     MediaProjectionManager mMpMngr = (MediaProjectionManager) getApplicationContext().getSystemService(MEDIA_PROJECTION_SERVICE); | ||||
|                     startActivityForResult(mMpMngr.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION); | ||||
|                     // 防止点多次.
 | ||||
|                     view.setEnabled(false); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onActivityResult(int requestCode, final int resultCode, final Intent data) { | ||||
|         if (requestCode == REQUEST_MEDIA_PROJECTION) { | ||||
|             getMediaStream().subscribe(new Consumer<MediaStream>() { | ||||
|                 @Override | ||||
|                 public void accept(MediaStream mediaStream) { | ||||
|                     mediaStream.pushScreen(resultCode, data, HOST, "554", "screen111"); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void onSwitchCamera(View view) { | ||||
|         getMediaStream().subscribe(new Consumer<MediaStream>() { | ||||
|             @Override | ||||
|             public void accept(MediaStream mediaStream) throws Exception { | ||||
|                 mediaStream.switchCamera(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void onUVCCamera(View view) { | ||||
|         Intent intent = new Intent(this, UVCActivity.class); | ||||
|         startActivity(intent); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,47 @@ | ||||
| package com.example.myapplication; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| import org.easydarwin.util.AbstractSubscriber; | ||||
| import org.reactivestreams.Publisher; | ||||
| 
 | ||||
| import io.reactivex.Single; | ||||
| import io.reactivex.subjects.PublishSubject; | ||||
| 
 | ||||
| /** | ||||
|  * Created by apple on 2017/12/22. | ||||
|  */ | ||||
| 
 | ||||
| public class RxHelper { | ||||
|     static boolean IGNORE_ERROR = false; | ||||
| 
 | ||||
|     public static <T> Single<T> single(@NonNull Publisher<T> t, @Nullable T defaultValueIfNotNull){ | ||||
|         if (defaultValueIfNotNull != null) return Single.just(defaultValueIfNotNull); | ||||
|         final PublishSubject sub = PublishSubject.create(); | ||||
|         t.subscribe(new AbstractSubscriber<T>() { | ||||
|             @Override | ||||
|             public void onNext(T t) { | ||||
|                 super.onNext(t); | ||||
|                 sub.onNext(t); | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onError(Throwable t) { | ||||
|                 if (IGNORE_ERROR) { | ||||
|                     super.onError(t); | ||||
|                     sub.onComplete(); | ||||
|                 }else { | ||||
|                     sub.onError(t); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public void onComplete() { | ||||
|                 super.onComplete(); | ||||
|                 sub.onComplete(); | ||||
|             } | ||||
|         }); | ||||
|         return sub.firstOrError(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,27 @@ | ||||
| package com.example.myapplication; | ||||
| 
 | ||||
| import android.graphics.SurfaceTexture; | ||||
| import android.view.TextureView; | ||||
| 
 | ||||
| /** | ||||
|  * Created by apple on 2017/9/11. | ||||
|  */ | ||||
| 
 | ||||
| public abstract class SurfaceTextureListenerWrapper implements TextureView.SurfaceTextureListener{ | ||||
| 
 | ||||
|     @Override | ||||
|     public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,208 @@ | ||||
| package com.example.myapplication; | ||||
| 
 | ||||
| import androidx.lifecycle.Observer; | ||||
| import android.content.Intent; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.graphics.SurfaceTexture; | ||||
| import android.os.Environment; | ||||
| import android.preference.PreferenceManager; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.core.app.ActivityCompat; | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import android.os.Bundle; | ||||
| import android.view.TextureView; | ||||
| import android.view.View; | ||||
| import android.widget.TextView; | ||||
| import android.widget.Toast; | ||||
| 
 | ||||
| import org.easydarwin.push.MediaStream; | ||||
| 
 | ||||
| import io.reactivex.Single; | ||||
| import io.reactivex.functions.Consumer; | ||||
| 
 | ||||
| public class UVCActivity extends AppCompatActivity { | ||||
|     private MediaStream mediaStream; | ||||
| 
 | ||||
|     private static final int REQUEST_CAMERA_PERMISSION = 1000; | ||||
|     private Single<MediaStream> getMediaStream() { | ||||
|         Single<MediaStream> single = RxHelper.single(MediaStream.getBindedMediaStream(this, this), mediaStream); | ||||
|         if (mediaStream == null) { | ||||
|             return single.doOnSuccess(new Consumer<MediaStream>() { | ||||
|                 @Override | ||||
|                 public void accept(MediaStream ms) throws Exception { | ||||
|                     mediaStream = ms; | ||||
|                 } | ||||
|             }); | ||||
|         } else { | ||||
|             return single; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setContentView(R.layout.activity_uvc); | ||||
| 
 | ||||
|         // 启动服务...
 | ||||
|         Intent intent = new Intent(this, MediaStream.class); | ||||
|         startService(intent); | ||||
| 
 | ||||
|         getMediaStream().subscribe(new Consumer<MediaStream>() { | ||||
|             @Override | ||||
|             public void accept(final MediaStream ms) throws Exception { | ||||
| 
 | ||||
|                 final TextView pushingStateText = findViewById(R.id.pushing_state); | ||||
|                 final TextView pushingBtn = findViewById(R.id.pushing); | ||||
|                 ms.observePushingState(UVCActivity.this, new Observer<MediaStream.PushingState>() { | ||||
| 
 | ||||
|                     @Override | ||||
|                     public void onChanged(@Nullable MediaStream.PushingState pushingState) { | ||||
|                         if (pushingState.screenPushing) { | ||||
|                             pushingStateText.setText("屏幕推送"); | ||||
|                         } else { | ||||
|                             pushingStateText.setText("推送"); | ||||
| 
 | ||||
|                             if (pushingState.state > 0) { | ||||
|                                 pushingBtn.setText("停止"); | ||||
|                             } else { | ||||
|                                 pushingBtn.setText("推送"); | ||||
|                             } | ||||
| 
 | ||||
|                         } | ||||
|                         pushingStateText.append(":\t" + pushingState.msg); | ||||
|                         if (pushingState.state > 0) { | ||||
|                             pushingStateText.append(pushingState.url); | ||||
|                         } | ||||
| 
 | ||||
|                     } | ||||
|                 }); | ||||
|                 TextureView textureView = findViewById(R.id.texture_view); | ||||
|                 textureView.setSurfaceTextureListener(new SurfaceTextureListenerWrapper() { | ||||
|                     @Override | ||||
|                     public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) { | ||||
|                         ms.setSurfaceTexture(surfaceTexture); | ||||
|                     } | ||||
|                     @Override | ||||
|                     public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { | ||||
|                         ms.setSurfaceTexture(null); | ||||
|                         return true; | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
| 
 | ||||
|                 if (ActivityCompat.checkSelfPermission(UVCActivity.this, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED || | ||||
|                         ActivityCompat.checkSelfPermission(UVCActivity.this, android.Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { | ||||
|                     ActivityCompat.requestPermissions(UVCActivity.this, new String[]{android.Manifest.permission.CAMERA, android.Manifest.permission.RECORD_AUDIO}, REQUEST_CAMERA_PERMISSION); | ||||
|                 } | ||||
|             } | ||||
|         }, new Consumer<Throwable>() { | ||||
|             @Override | ||||
|             public void accept(Throwable throwable) throws Exception { | ||||
|                 Toast.makeText(UVCActivity.this, "创建服务出错!", Toast.LENGTH_SHORT).show(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     // 权限获取到了.
 | ||||
|     @Override | ||||
|     public void onRequestPermissionsResult(int requestCode, | ||||
|                                            String permissions[], int[] grantResults) { | ||||
|         switch (requestCode) { | ||||
|             case REQUEST_CAMERA_PERMISSION: { | ||||
|                 if (grantResults.length > 1 | ||||
|                         && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) { | ||||
|                     getMediaStream().subscribe(new Consumer<MediaStream>() { | ||||
|                         @Override | ||||
|                         public void accept(MediaStream mediaStream) throws Exception { | ||||
|                             mediaStream.notifyPermissionGranted(); | ||||
|                         } | ||||
|                     }); | ||||
|                 } else { | ||||
|                     // 没有获取到权限,退出....
 | ||||
|                     Intent intent = new Intent(this, MediaStream.class); | ||||
|                     stopService(intent); | ||||
| 
 | ||||
|                     finish(); | ||||
|                 } | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void onPush(View view) { | ||||
| 
 | ||||
|         // 异步获取到MediaStream对象.
 | ||||
|         getMediaStream().subscribe(new Consumer<MediaStream>() { | ||||
|             @Override | ||||
|             public void accept(final MediaStream mediaStream) throws Exception { | ||||
|                 // 判断当前的推送状态.
 | ||||
|                 MediaStream.PushingState state = mediaStream.getPushingState(); | ||||
|                 if (state != null && state.state > 0) { // 当前正在推送,那终止推送和预览
 | ||||
|                     mediaStream.stopStream(); | ||||
|                     mediaStream.closeCameraPreview(); | ||||
|                 }else{ | ||||
|                     // switch 0表示后置,1表示前置,2表示UVC摄像头
 | ||||
|                     RxHelper.single(mediaStream.switchCamera(2), null).subscribe(new Consumer<Object>() { | ||||
|                         @Override | ||||
|                         public void accept(Object o) throws Exception { | ||||
|                             String id = PreferenceManager.getDefaultSharedPreferences(UVCActivity.this).getString("uvc-id", null); | ||||
|                             if (id == null) { | ||||
|                                 double v = Math.random() * 1000; | ||||
|                                 id = "uvc_" + (int) v; | ||||
|                                 PreferenceManager.getDefaultSharedPreferences(UVCActivity.this).edit().putString("uvc-id", id).apply(); | ||||
|                             } | ||||
|                             mediaStream.startStream("cloud.easydarwin.org", "554", id); | ||||
|                         } | ||||
|                     }, new Consumer<Throwable>() { | ||||
|                         @Override | ||||
|                         public void accept(final Throwable t) throws Exception { | ||||
|                             t.printStackTrace(); | ||||
|                             runOnUiThread(new Runnable() { | ||||
|                                 @Override | ||||
|                                 public void run() { | ||||
|                                     Toast.makeText(UVCActivity.this, "UVC摄像头启动失败.." + t.getMessage(), Toast.LENGTH_SHORT).show(); | ||||
|                                 } | ||||
|                             }); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void onRecord(View view) {       // 开始或结束录像.
 | ||||
|         final TextView txt = (TextView) view; | ||||
|         getMediaStream().subscribe(new Consumer<MediaStream>() { | ||||
|             @Override | ||||
|             public void accept(MediaStream mediaStream) throws Exception { | ||||
|                 if (mediaStream.isRecording()){ // 如果正在录像,那停止.
 | ||||
|                     mediaStream.stopRecord(); | ||||
|                     txt.setText("录像"); | ||||
|                 }else { // 没在录像,开始录像...
 | ||||
|                     // 表示最大录像时长为30秒,30秒后如果没有停止,会生成一个新文件.依次类推...
 | ||||
|                     // 文件格式为test_uvc_0.mp4,test_uvc_1.mp4,test_uvc_2.mp4,test_uvc_3.mp4
 | ||||
|                     String path = getExternalFilesDir(Environment.DIRECTORY_MOVIES) + "/test_uvc.mp4"; | ||||
|                     mediaStream.startRecord(path, 30000); | ||||
| 
 | ||||
|                     final TextView pushingStateText = findViewById(R.id.pushing_state); | ||||
|                     pushingStateText.append("\n录像地址:" + path); | ||||
|                     txt.setText("停止"); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void onQuit(View view) {     // 退出
 | ||||
|         finish(); | ||||
| 
 | ||||
|         // 终止服务...
 | ||||
|         Intent intent = new Intent(this, MediaStream.class); | ||||
|         stopService(intent); | ||||
|     } | ||||
| 
 | ||||
|     public void onBackground(View view) {   // 后台
 | ||||
|         finish(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,113 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="24dp" | ||||
|     android:height="24dp" | ||||
|     android:viewportHeight="108.0" | ||||
|     android:viewportWidth="108.0"> | ||||
|     <path | ||||
|         android:fillColor="#26A69A" | ||||
|         android:pathData="M0,0h108v108h-108z" | ||||
|         android:strokeColor="#66FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,0L19,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M9,0L9,108" | ||||
|         android:strokeColor="#66FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M39,0L39,108" | ||||
|         android:strokeColor="#66FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M29,0L29,108" | ||||
|         android:strokeColor="#66FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M59,0L59,108" | ||||
|         android:strokeColor="#66FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M49,0L49,108" | ||||
|         android:strokeColor="#66FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M79,0L79,108" | ||||
|         android:strokeColor="#66FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M69,0L69,108" | ||||
|         android:strokeColor="#66FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M89,0L89,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M99,0L99,108" | ||||
|         android:strokeColor="#66FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,89L108,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,99L108,99" | ||||
|         android:strokeColor="#66FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,69L108,69" | ||||
|         android:strokeColor="#66FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,79L108,79" | ||||
|         android:strokeColor="#66FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,49L108,49" | ||||
|         android:strokeColor="#66FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,59L108,59" | ||||
|         android:strokeColor="#66FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,29L108,29" | ||||
|         android:strokeColor="#66FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,39L108,39" | ||||
|         android:strokeColor="#66FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,19L108,19" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,9L108,9" | ||||
|         android:strokeColor="#66FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
| </vector> | ||||
| 
 | ||||
| @ -0,0 +1,106 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.constraintlayout.widget.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:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     tools:context="com.example.myapplication.MainActivity"> | ||||
| 
 | ||||
| 
 | ||||
|     <TextureView | ||||
|         android:id="@+id/texture_view" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="0dp" | ||||
|         app:layout_constraintDimensionRatio="h,640:480" | ||||
|         app:layout_constraintLeft_toLeftOf="parent" | ||||
|         app:layout_constraintRight_toRightOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" /> | ||||
| 
 | ||||
|     <CheckBox | ||||
|         android:id="@+id/enable_265" | ||||
|         android:layout_width="wrap_content" | ||||
| 
 | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginStart="8dp" | ||||
|         android:layout_marginLeft="8dp" | ||||
|         android:layout_marginTop="8dp" | ||||
|         android:text="265格式编码" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintTop_toBottomOf="@+id/texture_view" /> | ||||
| 
 | ||||
|     <TableLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_gravity="bottom" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         android:stretchColumns="*" | ||||
|         android:layout_height="wrap_content"> | ||||
|         <TextView | ||||
|             android:id="@+id/pushing_state" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             app:layout_constraintBottom_toTopOf="@+id/pushing" | ||||
|             android:background="#66ffffff" | ||||
|             app:layout_constraintEnd_toEndOf="parent" | ||||
|             app:layout_constraintStart_toStartOf="parent" /> | ||||
|         <TableRow> | ||||
|             <Button | ||||
|                 android:id="@+id/pushing" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:text="推送" | ||||
|                 android:onClick="onPushing" | ||||
|                 app:layout_constraintBottom_toBottomOf="parent" | ||||
|                 app:layout_constraintStart_toStartOf="parent" /> | ||||
| 
 | ||||
|             <Button | ||||
|                 android:id="@+id/switching_camera" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:onClick="onSwitchCamera" | ||||
|                 android:text="切换" | ||||
|                 app:layout_constraintEnd_toEndOf="parent" | ||||
|                 app:layout_constraintTop_toTopOf="parent" /> | ||||
| 
 | ||||
| 
 | ||||
|             <Button | ||||
|                 android:id="@+id/uvc_camera" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:onClick="onUVCCamera" | ||||
|                 android:text="UVC" | ||||
|                 app:layout_constraintEnd_toEndOf="parent" | ||||
|                 app:layout_constraintTop_toTopOf="parent" /> | ||||
|             <Button | ||||
|                 android:id="@+id/pushing_desktop" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:onClick="onPushScreen" | ||||
|                 android:text="推送屏幕" | ||||
|                 app:layout_constraintRight_toLeftOf="@+id/switching_camera" | ||||
|                 app:layout_constraintTop_toTopOf="parent" /> | ||||
|         </TableRow> | ||||
| 
 | ||||
|         <TableRow> | ||||
|         <Button | ||||
|             android:id="@+id/press_record" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:text="按住录像" | ||||
|             app:layout_constraintBottom_toBottomOf="parent" /> | ||||
| 
 | ||||
|         <TextView | ||||
|             android:id="@+id/record_time" | ||||
|             style="@style/Base.TextAppearance.AppCompat.Large" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:text="00:00" | ||||
|             android:textColor="#ff0000" | ||||
|             android:visibility="gone" | ||||
|             app:layout_constraintEnd_toEndOf="parent" | ||||
|             app:layout_constraintStart_toStartOf="parent" | ||||
|             app:layout_constraintTop_toTopOf="@+id/texture_view" /> | ||||
|         </TableRow> | ||||
|     </TableLayout> | ||||
| 
 | ||||
| 
 | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| @ -0,0 +1,68 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.constraintlayout.widget.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:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     tools:context="com.example.myapplication.UVCActivity"> | ||||
| 
 | ||||
| 
 | ||||
|     <TextureView | ||||
|         android:id="@+id/texture_view" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="0dp" | ||||
|         app:layout_constraintDimensionRatio="h,640:480" | ||||
|         app:layout_constraintLeft_toLeftOf="parent" | ||||
|         app:layout_constraintRight_toRightOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" /> | ||||
| 
 | ||||
|     <TableLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginBottom="8dp" | ||||
|         app:layout_constraintBottom_toBottomOf="parent"> | ||||
| 
 | ||||
|         <TextView | ||||
|             android:id="@+id/pushing_state" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:background="#66ffffff" | ||||
|             app:layout_constraintBottom_toTopOf="@+id/pushing" | ||||
|             app:layout_constraintEnd_toEndOf="parent" | ||||
|             app:layout_constraintStart_toStartOf="parent" /> | ||||
| 
 | ||||
|         <LinearLayout | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content"> | ||||
| 
 | ||||
|             <Button | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:id="@+id/pushing" | ||||
|                 android:onClick="onPush" | ||||
|                 android:text="推送" /> | ||||
| 
 | ||||
| 
 | ||||
|             <Button | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:onClick="onRecord" | ||||
|                 android:text="录像" /> | ||||
| 
 | ||||
|             <Button | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:onClick="onQuit" | ||||
|                 android:text="退出" /> | ||||
| 
 | ||||
|             <Button | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:onClick="onBackground" | ||||
|                 android:text="后台" /> | ||||
| 
 | ||||
|         </LinearLayout> | ||||
|     </TableLayout> | ||||
| 
 | ||||
| 
 | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| @ -0,0 +1,6 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.constraintlayout.widget.ConstraintLayout | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent"> | ||||
| 
 | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| @ -0,0 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <background android:drawable="@drawable/ic_launcher_background" /> | ||||
|     <foreground android:drawable="@mipmap/ic_launcher_foreground" /> | ||||
| </adaptive-icon> | ||||
| @ -0,0 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <background android:drawable="@drawable/ic_launcher_background" /> | ||||
|     <foreground android:drawable="@mipmap/ic_launcher_foreground" /> | ||||
| </adaptive-icon> | ||||
| After Width: | Height: | Size: 3.3 KiB | 
| After Width: | Height: | Size: 5.0 KiB | 
| After Width: | Height: | Size: 5.0 KiB | 
| After Width: | Height: | Size: 2.3 KiB | 
| After Width: | Height: | Size: 2.6 KiB | 
| After Width: | Height: | Size: 3.1 KiB | 
| After Width: | Height: | Size: 4.5 KiB | 
| After Width: | Height: | Size: 6.8 KiB | 
| After Width: | Height: | Size: 7.2 KiB | 
| After Width: | Height: | Size: 6.8 KiB | 
| After Width: | Height: | Size: 14 KiB | 
| After Width: | Height: | Size: 11 KiB | 
| After Width: | Height: | Size: 9.2 KiB | 
| After Width: | Height: | Size: 21 KiB | 
| After Width: | Height: | Size: 16 KiB | 
| @ -0,0 +1,6 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|     <color name="colorPrimary">#3F51B5</color> | ||||
|     <color name="colorPrimaryDark">#303F9F</color> | ||||
|     <color name="colorAccent">#FF4081</color> | ||||
| </resources> | ||||
| @ -0,0 +1,3 @@ | ||||
| <resources> | ||||
|     <string name="app_name">My Application</string> | ||||
| </resources> | ||||
| @ -0,0 +1,11 @@ | ||||
| <resources> | ||||
| 
 | ||||
|     <!-- Base application theme. --> | ||||
|     <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> | ||||
|         <!-- Customize your theme here. --> | ||||
|         <item name="colorPrimary">@color/colorPrimary</item> | ||||
|         <item name="colorPrimaryDark">@color/colorPrimaryDark</item> | ||||
|         <item name="colorAccent">@color/colorAccent</item> | ||||
|     </style> | ||||
| 
 | ||||
| </resources> | ||||
| @ -0,0 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| 
 | ||||
| <usb> | ||||
| 	<usb-device class="239" subclass="2" />	<!-- all device of UVC --> | ||||
| </usb> | ||||