├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── live.png
│ │ │ │ ├── stop.png
│ │ │ │ ├── watemark.png
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── camera_change.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── live_logo.jpeg
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── styles.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── layout
│ │ │ │ ├── address_dialog.xml
│ │ │ │ └── activity_live.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── devyk
│ │ │ │ └── av
│ │ │ │ └── rtmppush
│ │ │ │ ├── Utils.java
│ │ │ │ └── base
│ │ │ │ └── BaseActivity.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── devyk
│ │ │ └── av
│ │ │ └── rtmppush
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── devyk
│ │ └── av
│ │ └── rtmppush
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── library
├── .gitignore
├── src
│ ├── main
│ │ ├── cpp
│ │ │ ├── common
│ │ │ │ ├── IPush.cpp
│ │ │ │ ├── IObserver.cpp
│ │ │ │ ├── IObserver.h
│ │ │ │ ├── IThread.cpp
│ │ │ │ ├── IThread.h
│ │ │ │ ├── IPush.h
│ │ │ │ ├── PushProxy.cpp
│ │ │ │ └── PushProxy.h
│ │ │ ├── librtmp
│ │ │ │ ├── libs
│ │ │ │ │ ├── arm64-v8a
│ │ │ │ │ │ └── librtmp.a
│ │ │ │ │ └── armeabi-v7a
│ │ │ │ │ │ └── librtmp.a
│ │ │ │ └── include
│ │ │ │ │ ├── http.h
│ │ │ │ │ └── log.h
│ │ │ ├── push
│ │ │ │ ├── AVQueue.h
│ │ │ │ ├── AVQueue.cpp
│ │ │ │ └── RTMPPush.h
│ │ │ ├── callback
│ │ │ │ ├── JavaCallback.h
│ │ │ │ └── JavaCallback.cpp
│ │ │ ├── CMakeLists.txt
│ │ │ └── jni
│ │ │ │ └── native_rtmp_push.cpp
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ └── attrs.xml
│ │ │ ├── raw
│ │ │ │ ├── fragment_shader.glsl
│ │ │ │ ├── vertex_shader.glsl
│ │ │ │ ├── fragment_shader_camera.glsl
│ │ │ │ └── vertex_shader_matrix.glsl
│ │ │ └── layout
│ │ │ │ └── include_live.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── devyk
│ │ │ └── av
│ │ │ └── rtmp
│ │ │ └── library
│ │ │ ├── camera
│ │ │ ├── exception
│ │ │ │ ├── NoCameraException.java
│ │ │ │ ├── NoCameraException.kt
│ │ │ │ ├── CameraDisabledException.kt
│ │ │ │ ├── CameraHardwareException.kt
│ │ │ │ └── CameraNotSupportException.kt
│ │ │ ├── renderer
│ │ │ │ └── DefaultRenderer.kt
│ │ │ ├── Watermark.kt
│ │ │ ├── CameraData.kt
│ │ │ ├── CameraRecorder.kt
│ │ │ ├── EglHelper.kt
│ │ │ ├── ShaderHelper.kt
│ │ │ └── GLThread.kt
│ │ │ ├── callback
│ │ │ ├── OnAudioDataListener.kt
│ │ │ ├── OnVideoEncodeListener.kt
│ │ │ ├── ICameraOpenListener.kt
│ │ │ ├── ILog.kt
│ │ │ ├── OnAudioEncodeListener.kt
│ │ │ ├── OnConnectListener.kt
│ │ │ ├── IRenderer.kt
│ │ │ ├── IGLThreadConfig.kt
│ │ │ └── IController.kt
│ │ │ ├── stream
│ │ │ ├── sender
│ │ │ │ ├── Sender.kt
│ │ │ │ └── rtmp
│ │ │ │ │ └── RtmpSender.kt
│ │ │ ├── PacketType.kt
│ │ │ ├── amf
│ │ │ │ ├── AmfData.java
│ │ │ │ ├── AmfNull.java
│ │ │ │ ├── AmfUndefined.java
│ │ │ │ ├── AmfType.java
│ │ │ │ ├── AmfBoolean.java
│ │ │ │ ├── AmfDecoder.java
│ │ │ │ ├── AmfNumber.java
│ │ │ │ ├── AmfMap.java
│ │ │ │ ├── AmfArray.java
│ │ │ │ ├── AmfString.java
│ │ │ │ ├── AmfObject.java
│ │ │ │ └── Util.java
│ │ │ └── packer
│ │ │ │ ├── Packer.kt
│ │ │ │ ├── DefaultPacker.kt
│ │ │ │ └── rtmp
│ │ │ │ └── RtmpPacker.kt
│ │ │ ├── Contacts.kt
│ │ │ ├── mediacodec
│ │ │ ├── IAudioCodec.kt
│ │ │ ├── IVideoCodec.kt
│ │ │ ├── AudioEncoder.kt
│ │ │ ├── VideoEncoder.kt
│ │ │ ├── BaseAudioCodec.kt
│ │ │ ├── AudioMediaCodec.kt
│ │ │ └── VideoMediaCodec.kt
│ │ │ ├── common
│ │ │ ├── IThread.kt
│ │ │ └── ThreadImpl.kt
│ │ │ ├── annotation
│ │ │ └── RendererMode.kt
│ │ │ ├── utils
│ │ │ ├── LogHelper.kt
│ │ │ └── BitmapUtils.kt
│ │ │ ├── black
│ │ │ └── BlackListHelper.kt
│ │ │ ├── controller
│ │ │ ├── VideoController.kt
│ │ │ └── AudioController.kt
│ │ │ ├── config
│ │ │ ├── CameraConfiguration.kt
│ │ │ ├── RendererConfiguration.kt
│ │ │ ├── VideoConfiguration.kt
│ │ │ └── AudioConfiguration.kt
│ │ │ ├── audio
│ │ │ ├── AudioProcessor.kt
│ │ │ └── AudioUtils.kt
│ │ │ └── widget
│ │ │ └── GLSurfaceView.kt
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── devyk
│ │ │ └── av
│ │ │ └── rtmp
│ │ │ └── library
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── devyk
│ │ └── av
│ │ └── rtmp
│ │ └── library
│ │ └── ExampleInstrumentedTest.java
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .idea
├── encodings.xml
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── runConfigurations.xml
└── misc.xml
├── gradle.properties
├── .gitignore
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/library/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':library'
2 |
--------------------------------------------------------------------------------
/library/src/main/cpp/common/IPush.cpp:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 阳坤 on 2020-07-03.
3 | //
4 |
5 | #include "IPush.h"
6 |
--------------------------------------------------------------------------------
/library/src/main/cpp/common/IObserver.cpp:
--------------------------------------------------------------------------------
1 | //
2 | // Created by 阳坤 on 2020-07-03.
3 | //
4 |
5 | #include "IObserver.h"
6 |
--------------------------------------------------------------------------------
/library/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
5 | * author : devyk on 2020-07-16 10:30 6 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 7 | * github : https://github.com/yangkun19921001 8 | * mailbox : yang1001yk@gmail.com 9 | * desc : This is OnAudioDataListener 10 | *11 | */ -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/camera/exception/NoCameraException.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.camera.exception 2 | /** 3 | *
4 | * author : devyk on 2020-05-28 23:32 5 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 6 | * github : https://github.com/yangkun19921001 7 | * mailbox : yang1001yk@gmail.com 8 | * desc : This is NoCameraException 9 | *10 | */ -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/callback/OnVideoEncodeListener.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.callback 2 | 3 | import android.media.MediaCodec 4 | import android.media.MediaFormat 5 | import java.nio.ByteBuffer 6 | 7 | /** 8 | * 编码回调 9 | */ 10 | interface OnVideoEncodeListener { 11 | abstract fun onVideoEncode(bb: ByteBuffer?, bi: MediaCodec.BufferInfo?) 12 | fun onVideoOutformat(outputFormat: MediaFormat?) 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 |
5 | * author : devyk on 2020-05-28 23:31 6 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 7 | * github : https://github.com/yangkun19921001 8 | * mailbox : yang1001yk@gmail.com 9 | * desc : This is CameraDisabledException 10 | *11 | */ 12 | class CameraDisabledException : Exception() -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/callback/ICameraOpenListener.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.callback 2 | 3 | /** 4 | *
5 | * author : devyk on 2020-07-08 21:35 6 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 7 | * github : https://github.com/yangkun19921001 8 | * mailbox : yang1001yk@gmail.com 9 | * desc : This is ICameraOpenListener 10 | *11 | */ 12 | 13 | public interface ICameraOpenListener { 14 | fun onCameraOpen() 15 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/camera/exception/CameraHardwareException.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.camera.exception 2 | 3 | /** 4 | *
5 | * author : devyk on 2020-05-29 15:04 6 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 7 | * github : https://github.com/yangkun19921001 8 | * mailbox : yang1001yk@gmail.com 9 | * desc : This is CameraHardwareException 10 | *11 | */ 12 | class CameraHardwareException(t: Throwable) : Exception(t) -------------------------------------------------------------------------------- /library/src/test/java/com/devyk/av/rtmp/library/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/camera/exception/CameraNotSupportException.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.camera.exception 2 | 3 | /** 4 | *
5 | * author : devyk on 2020-05-28 23:26 6 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 7 | * github : https://github.com/yangkun19921001 8 | * mailbox : yang1001yk@gmail.com 9 | * desc : This is CameraNotSupportException 10 | *11 | */ 12 | public class CameraNotSupportException :Exception(){ 13 | 14 | } -------------------------------------------------------------------------------- /library/src/main/cpp/common/IThread.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by 阳坤 on 2020-07-03. 3 | // 4 | 5 | #ifndef RTMPPUSH_ITHREAD_H 6 | #define RTMPPUSH_ITHREAD_H 7 | 8 | 9 | #include
7 | * author : devyk on 2020-07-16 21:26 8 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 9 | * github : https://github.com/yangkun19921001 10 | * mailbox : yang1001yk@gmail.com 11 | * desc : This is Sender 12 | *13 | */ 14 | public interface Sender{ 15 | fun onData(data: ByteArray, type: PacketType) 16 | fun onData(sps: ByteArray,pps: ByteArray, type: PacketType){} 17 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/Contacts.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library 2 | 3 | /** 4 | *
5 | * author : devyk on 2020-07-15 21:44 6 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 7 | * github : https://github.com/yangkun19921001 8 | * mailbox : yang1001yk@gmail.com 9 | * desc : This is Contacts 10 | *11 | */ 12 | public object Contacts { 13 | public var TAG = "AVRtmpPush" 14 | //rtmp 初始化失败 15 | var RTMP_INIT_ERROR = -9 16 | //设置 rtmp url 失败 17 | var RTMP_SET_URL_ERROR = -10 18 | //连接服务器失败 19 | var RTMP_CONNECT_ERROR = -11 20 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/callback/ILog.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.callback 2 | 3 | /** 4 | *
5 | * author : devyk on 2020-07-15 21:26 6 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 7 | * github : https://github.com/yangkun19921001 8 | * mailbox : yang1001yk@gmail.com 9 | * desc : This is ILog 10 | *11 | */ 12 | public interface ILog { 13 | fun i(tag: String = javaClass.simpleName, info: String?); 14 | fun e(tag: String = javaClass.simpleName, info: String?); 15 | fun w(tag: String = javaClass.simpleName, info: String?); 16 | fun d(tag: String = javaClass.simpleName, info: String?); 17 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/mediacodec/IAudioCodec.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.mediacodec 2 | 3 | /** 4 | *
5 | * author : devyk on 2020-06-13 23:55 6 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 7 | * github : https://github.com/yangkun19921001 8 | * mailbox : yang1001yk@gmail.com 9 | * desc : This is ICodec 10 | *11 | */ 12 | public interface IAudioCodec{ 13 | 14 | 15 | /** 16 | * 准备编码 17 | */ 18 | fun start() 19 | 20 | /** 21 | * 将数据送入编解码器 22 | */ 23 | fun enqueueCodec(input: ByteArray?); 24 | 25 | /** 26 | * 停止编码 27 | */ 28 | fun stop(); 29 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/stream/PacketType.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.stream 2 | 3 | /** 4 | *
5 | * author : devyk on 2020-07-16 21:29 6 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 7 | * github : https://github.com/yangkun19921001 8 | * mailbox : yang1001yk@gmail.com 9 | * desc : This is PacketType 10 | *11 | */ 12 | public enum class PacketType { 13 | FIRST_AUDIO(1), 14 | FIRST_VIDEO(2), 15 | SPS_PPS(3), 16 | AUDIO(4), 17 | KEY_FRAME(5), 18 | VIDEO(6); 19 | 20 | 21 | var type = -1; 22 | constructor(types: Int){ 23 | type = types 24 | } 25 | 26 | 27 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/callback/OnAudioEncodeListener.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.callback 2 | 3 | import android.media.MediaCodec 4 | import android.media.MediaFormat 5 | import java.nio.ByteBuffer 6 | 7 | /** 8 | *
9 | * author : devyk on 2020-06-13 16:09 10 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 11 | * github : https://github.com/yangkun19921001 12 | * mailbox : yang1001yk@gmail.com 13 | * desc : This is OnAudioEncodeListener 14 | *15 | */ 16 | public interface OnAudioEncodeListener { 17 | fun onAudioEncode(bb: ByteBuffer, bi: MediaCodec.BufferInfo) 18 | fun onAudioOutformat(outputFormat: MediaFormat?) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/devyk/av/rtmppush/Utils.java: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmppush; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | /** 7 | *
8 | * author : devyk on 2020-07-16 22:32 9 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 10 | * github : https://github.com/yangkun19921001 11 | * mailbox : yang1001yk@gmail.com 12 | * desc : This is Utils 13 | *14 | */ 15 | class Utils { 16 | private static Application sApp; 17 | 18 | public static Context getApp() { 19 | return sApp; 20 | } 21 | 22 | public static void init(Application application) { 23 | sApp = application; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /library/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 |
5 | * author : devyk on 2020-07-16 23:10 6 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 7 | * github : https://github.com/yangkun19921001 8 | * mailbox : yang1001yk@gmail.com 9 | * desc : This is OnConnectListener 10 | *11 | */ 12 | public interface OnConnectListener { 13 | /** 14 | * 开始链接 15 | */ 16 | fun onConnecting() 17 | 18 | /** 19 | * 连接成功 20 | */ 21 | fun onConnected() 22 | 23 | /** 24 | * 推送失败 25 | */ 26 | fun onFail(message:String) 27 | 28 | /** 29 | * 关闭 30 | */ 31 | fun onClose() 32 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/devyk/av/rtmppush/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmppush 2 | 3 | import androidx.test.InstrumentationRegistry 4 | import androidx.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.devyk.av.rtmppush", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /library/src/main/res/layout/include_live.xml: -------------------------------------------------------------------------------- 1 | 2 |
5 | * author : devyk on 2020-07-15 20:42 6 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 7 | * github : https://github.com/yangkun19921001 8 | * mailbox : yang1001yk@gmail.com 9 | * desc : This is IThread 10 | *11 | */ 12 | public interface IThread { 13 | 14 | /** 15 | * 开始执行线程 16 | */ 17 | fun start(main:()->Unit) 18 | 19 | /** 20 | * 停止执行 21 | */ 22 | fun stop() 23 | 24 | /** 25 | *设置是否暂停 26 | */ 27 | fun setPause(pause: Boolean) 28 | 29 | /** 30 | * 停止 31 | */ 32 | fun isPause(): Boolean 33 | 34 | /** 35 | * 是否运行 36 | */ 37 | fun isRuning(): Boolean 38 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/callback/IRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.camera_recorder.callback 2 | 3 | /** 4 | *
5 | * author : devyk on 2020-07-06 11:05 6 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 7 | * github : https://github.com/yangkun19921001 8 | * mailbox : yang1001yk@gmail.com 9 | * desc : This is IRenderer 10 | * 11 | * 12 | * OpenGL ES 坐标系: 13 | * @see  14 | *15 | */ 16 | public interface IRenderer { 17 | /** 18 | * 当 Surface 创建的时候 19 | */ 20 | public fun onSurfaceCreate(width: Int, height: Int); 21 | 22 | /** 23 | * 当 surface 窗口改变的时候 24 | */ 25 | public fun onSurfaceChange(width: Int, height: Int); 26 | 27 | /** 28 | * 绘制的时候 29 | */ 30 | public fun onDraw(); 31 | } -------------------------------------------------------------------------------- /library/src/androidTest/java/com/devyk/av/rtmp/library/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library; 2 | 3 | import android.content.Context; 4 | import androidx.test.InstrumentationRegistry; 5 | import androidx.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.devyk.av.rtmp.library.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/annotation/RendererMode.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.annotation 2 | 3 | import androidx.annotation.IntDef 4 | import com.devyk.av.rtmp.library.widget.GLSurfaceView 5 | import java.lang.annotation.Retention 6 | import java.lang.annotation.RetentionPolicy 7 | 8 | /** 9 | *
10 | * author : devyk on 2020-07-06 11:35 11 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 12 | * github : https://github.com/yangkun19921001 13 | * mailbox : yang1001yk@gmail.com 14 | * desc : This is RendererMode 用于 OpenGL ES 渲染模式 @link com.devyk.av.camera_recorder.widget.base.GLSurfaceView 15 | *16 | */ 17 | @IntDef(GLSurfaceView.RENDERERMODE_WHEN_DIRTY, GLSurfaceView.RENDERERMODE_CONTINUOUSLY) 18 | @Target(AnnotationTarget.VALUE_PARAMETER) //用于参数上 19 | @Retention(RetentionPolicy.SOURCE) //编译器 20 | annotation class RendererMode {} -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/callback/IGLThreadConfig.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.camera_recorder.callback 2 | 3 | import android.view.Surface 4 | import javax.microedition.khronos.egl.EGLContext 5 | 6 | /** 7 | *
8 | * author : devyk on 2020-07-08 20:51 9 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 10 | * github : https://github.com/yangkun19921001 11 | * mailbox : yang1001yk@gmail.com 12 | * desc : This is IGLThreadConfig ELThread 需要的配置 13 | *14 | */ 15 | public interface IGLThreadConfig { 16 | /** 17 | * 拿到渲染器 18 | */ 19 | fun getRenderer(): IRenderer? 20 | 21 | /** 22 | * 拿到渲染的 Surface 23 | */ 24 | fun getSurface(): Surface? 25 | 26 | /** 27 | * 拿到 EGL 环境的上下文 28 | */ 29 | fun getEGLContext(): EGLContext? 30 | 31 | /** 32 | * 拿到渲染模式 33 | */ 34 | fun getRendererMode(): Int 35 | 36 | 37 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/camera/renderer/DefaultRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.camera.renderer 2 | 3 | import android.opengl.GLES20 4 | import com.devyk.av.camera_recorder.callback.IRenderer 5 | 6 | /** 7 | *
8 | * author : devyk on 2020-07-06 11:57 9 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 10 | * github : https://github.com/yangkun19921001 11 | * mailbox : yang1001yk@gmail.com 12 | * desc : This is DefaultRenderer 渲染 13 | *14 | */ 15 | public class DefaultRenderer : IRenderer { 16 | override fun onSurfaceCreate(width: Int, height: Int) { 17 | } 18 | 19 | override fun onSurfaceChange(width: Int, height: Int) { 20 | //清屏 21 | GLES20.glViewport(0, 0, width, height) 22 | } 23 | 24 | override fun onDraw() { 25 | GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) 26 | GLES20.glClearColor(0f, 1f, 0f, 1f) 27 | } 28 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/mediacodec/IVideoCodec.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.mediacodec 2 | 3 | import android.media.MediaCodec 4 | 5 | import com.devyk.av.rtmp.library.config.VideoConfiguration 6 | 7 | import java.nio.ByteBuffer 8 | 9 | /** 10 | *
11 | * author : devyk on 2020-06-15 21:42 12 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 13 | * github : https://github.com/yangkun19921001 14 | * mailbox : yang1001yk@gmail.com 15 | * desc : This is ICodec 16 | *17 | */ 18 | 19 | public interface IVideoCodec { 20 | 21 | 22 | 23 | /** 24 | * 初始化编码器 25 | */ 26 | fun prepare(videoConfiguration: VideoConfiguration = VideoConfiguration.createDefault()){}; 27 | 28 | /** 29 | * start 编码 30 | */ 31 | fun start(); 32 | 33 | /** 34 | * 停止编码 35 | */ 36 | fun stop(); 37 | 38 | /** 39 | * 返回编码好的 H264 数据 40 | */ 41 | abstract fun onVideoEncode(bb: ByteBuffer?, mBufferInfo: MediaCodec.BufferInfo) 42 | 43 | 44 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/stream/amf/AmfData.java: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.stream.amf; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | 7 | /** 8 | * @Title: AmfData 9 | * @Package com.jimfengfly.rtmppublisher.amf 10 | * @Description: 11 | * @Author Jim 12 | * @Date 2016/11/28 13 | * @Time 上午11:40 14 | * @Version 15 | */ 16 | 17 | public interface AmfData { 18 | /** 19 | * Write/Serialize this AMF data intance (Object/string/integer etc) to 20 | * the specified OutputStream 21 | */ 22 | void writeTo(OutputStream out) throws IOException; 23 | 24 | /** 25 | * Read and parse bytes from the specified input stream to populate this 26 | * AMFData instance (deserialize) 27 | * 28 | * @return the amount of bytes read 29 | */ 30 | void readFrom(InputStream in) throws IOException; 31 | 32 | /** @return the amount of bytes required for this object */ 33 | int getSize(); 34 | 35 | /** @return the bytes of this object */ 36 | byte[] getBytes(); 37 | } 38 | -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/utils/LogHelper.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.utils 2 | 3 | import android.os.Build 4 | import android.util.Log 5 | import com.devyk.av.rtmp.library.callback.ILog 6 | 7 | /** 8 | *
9 | * author : devyk on 2020-06-02 00:07 10 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 11 | * github : https://github.com/yangkun19921001 12 | * mailbox : yang1001yk@gmail.com 13 | * desc : This is LogHelper 14 | *15 | */ 16 | public object LogHelper : ILog { 17 | 18 | var isShowLog = false 19 | 20 | 21 | override fun i(tag: String, info: String?) { 22 | if (isShowLog) 23 | Log.i(tag, info) 24 | 25 | } 26 | 27 | override fun e(tag: String, info: String?) { 28 | if (isShowLog) 29 | Log.e(tag, info) 30 | } 31 | 32 | override fun w(tag: String, info: String?) { 33 | if (isShowLog) 34 | Log.w(tag, info) 35 | } 36 | 37 | override fun d(tag: String, info: String?) { 38 | if (isShowLog) 39 | Log.d(tag, info) 40 | } 41 | } -------------------------------------------------------------------------------- /library/src/main/cpp/common/IPush.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by 阳坤 on 2020-07-03. 3 | // 4 | 5 | #ifndef RTMPPUSH_IPUSH_H 6 | #define RTMPPUSH_IPUSH_H 7 | 8 | 9 | #include "IThread.h" 10 | 11 | class IPush : public IThread { 12 | 13 | public: 14 | /** 15 | * 开启线程 16 | */ 17 | virtual void start() = 0; 18 | 19 | /** 20 | * 停止推流 21 | */ 22 | virtual void stop() = 0; 23 | 24 | /** 25 | * 子线程入口 26 | */ 27 | virtual void main() = 0; 28 | 29 | /** 30 | * 推送视频第一帧 SPS PPS 数据 31 | * @param sps 32 | * @param sps_len 33 | * @param pps 34 | * @param pps_len 35 | */ 36 | virtual void pushSpsPps(uint8_t *sps, int sps_len, uint8_t *pps, int pps_len) = 0; 37 | 38 | /** 39 | * 推送音频数据 40 | * @param audio 41 | * @param len 42 | */ 43 | virtual void pushAudioData(uint8_t *audio, int len,int type) = 0; 44 | 45 | /** 46 | * 推送视频数据 47 | * @param video 48 | * @param len 49 | * @param keyframe 50 | */ 51 | virtual void pushVideoData(uint8_t *video, int len, int type) = 0; 52 | }; 53 | 54 | 55 | #endif //RTMPPUSH_IPUSH_H 56 | -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/camera/Watermark.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.camera 2 | 3 | import android.graphics.Bitmap 4 | 5 | /** 6 | *
7 | * author : devyk on 2020-07-18 19:43 8 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 9 | * github : https://github.com/yangkun19921001 10 | * mailbox : yang1001yk@gmail.com 11 | * desc : This is Watermark 12 | *13 | */ 14 | class Watermark { 15 | 16 | var markImg: Bitmap? = null 17 | var txt: String? = null 18 | var textColor = -1 19 | var textSize = -1 20 | 21 | var floatArray: FloatArray? = null 22 | 23 | 24 | constructor( 25 | markImg: Bitmap 26 | , floatArray: FloatArray? 27 | ) { 28 | this.markImg = markImg 29 | this.floatArray = floatArray 30 | } 31 | 32 | constructor( 33 | txt: String, 34 | txtColor: Int, 35 | txtSize: Int 36 | , floatArray: FloatArray? 37 | ) { 38 | this.txt = txt 39 | this.textSize = txtSize 40 | this.textColor = txtColor 41 | this.floatArray = floatArray 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /library/src/main/cpp/callback/JavaCallback.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by 阳坤 on 2020-07-17. 3 | // 4 | 5 | #ifndef RTMPPUSH_JAVACALLBACK_H 6 | #define RTMPPUSH_JAVACALLBACK_H 7 | 8 | #define THREAD_MAIN 1 9 | #define THREAD_CHILD 2 10 | 11 | 12 | //rtmp 初始化失败 13 | #define RTMP_INIT_ERROR -9 14 | //设置 rtmp url 失败 15 | #define RTMP_SET_URL_ERROR -10 16 | //连接服务器失败 17 | #define RTMP_CONNECT_ERROR -11 18 | //RTMP 关闭成功 19 | #define RTMP_CLOSE -12 20 | 21 | 22 | 23 | #include
5 | * author : devyk on 2020-05-28 23:18 6 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 7 | * github : https://github.com/yangkun19921001 8 | * mailbox : yang1001yk@gmail.com 9 | * desc : This is CameraData 10 | *11 | */ 12 | class CameraData { 13 | 14 | var cameraID: Int = 0 //camera的id 15 | var cameraFacing: Int = 0 //区分前后摄像头 16 | var cameraWidth: Int = 0 //camera的宽度 17 | var cameraHeight: Int = 0 //camera的高度 18 | var hasLight: Boolean = false 19 | var orientation: Int = 0 20 | var supportTouchFocus: Boolean = false 21 | var touchFocusMode: Boolean = false 22 | 23 | constructor(id: Int, facing: Int, width: Int, height: Int) { 24 | cameraID = id 25 | cameraFacing = facing 26 | cameraWidth = width 27 | cameraHeight = height 28 | } 29 | 30 | constructor(id: Int, facing: Int) { 31 | cameraID = id 32 | cameraFacing = facing 33 | } 34 | 35 | companion object { 36 | val FACING_FRONT = 1 37 | val FACING_BACK = 2 38 | } 39 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/stream/amf/AmfUndefined.java: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.stream.amf; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | 7 | /** 8 | * @Title: AmfUndefined 9 | * @Package com.jimfengfly.rtmppublisher.amf 10 | * @Description: 11 | * @Author Jim 12 | * @Date 2016/11/28 13 | * @Time 下午12:53 14 | * @Version 15 | */ 16 | 17 | public class AmfUndefined implements AmfData { 18 | public static final int SIZE = 1; 19 | 20 | @Override 21 | public void writeTo(OutputStream out) throws IOException { 22 | out.write(AmfType.UNDEFINED.getValue()); 23 | } 24 | 25 | @Override 26 | public void readFrom(InputStream in) throws IOException { 27 | // Skip data type byte (we assume it's already read) 28 | } 29 | 30 | public static void writeUndefinedTo(OutputStream out) throws IOException { 31 | out.write(AmfType.UNDEFINED.getValue()); 32 | } 33 | 34 | @Override 35 | public int getSize() { 36 | return SIZE; 37 | } 38 | 39 | @Override 40 | public byte[] getBytes() { 41 | byte[] data = new byte[1]; 42 | data[0] = AmfType.UNDEFINED.getValue(); 43 | return data; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | 19 | # AndroidX package structure to make it clearer which packages are bundled with the 20 | # Android operating system, and which are packaged with your app's APK 21 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 22 | android.useAndroidX=true 23 | # Automatically convert third-party libraries to use AndroidX 24 | android.enableJetifier=true 25 | 26 | # Kotlin code style for this project: "official" or "obsolete": 27 | kotlin.code.style=official 28 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 |
10 | * author : devyk on 2020-07-16 21:24 11 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 12 | * github : https://github.com/yangkun19921001 13 | * mailbox : yang1001yk@gmail.com 14 | * desc : This is Packer 对音视频数据打包 15 | *16 | */ 17 | public interface Packer { 18 | interface OnPacketListener { 19 | fun onPacket(byteArray: ByteArray, packetType: PacketType) 20 | fun onPacket(sps: ByteArray?,pps: ByteArray?, packetType: PacketType){} 21 | } 22 | 23 | /** 24 | * 设置打包监听器 25 | */ 26 | fun setPacketListener(packetListener: OnPacketListener) 27 | 28 | /** 29 | *处理视频硬编编码器输出的数据 30 | */ 31 | fun onVideoData(bb: ByteBuffer?, bi: MediaCodec.BufferInfo?) 32 | 33 | /** 34 | * 处理音频硬编编码器输出的数据 35 | * */ 36 | fun onAudioData(bb: ByteBuffer, bi: MediaCodec.BufferInfo) 37 | 38 | /** 39 | * 处理视频 SPS PPS 数据 40 | */ 41 | fun onVideoSpsPpsData(sps: ByteArray, pps: ByteArray, spsPps: PacketType) { 42 | 43 | } 44 | 45 | 46 | fun start(); 47 | fun stop(); 48 | } 49 | -------------------------------------------------------------------------------- /library/src/main/cpp/common/PushProxy.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by 阳坤 on 2020-07-17. 3 | // 4 | 5 | #include "PushProxy.h" 6 | 7 | 8 | IPush *PushProxy::getPushEngine() { 9 | return this->rtmpPush; 10 | } 11 | 12 | PushProxy::PushProxy() { 13 | } 14 | 15 | 16 | PushProxy *PushProxy::getInstance() { 17 | static PushProxy proxy[1]; 18 | return &proxy[0]; 19 | } 20 | 21 | void PushProxy::init(const char *url, JavaCallback **javaCallback) { 22 | this->url = url; 23 | this->javaCallback = *javaCallback; 24 | this->rtmpPush = new RTMPPush(url, javaCallback); 25 | } 26 | 27 | void PushProxy::start() { 28 | getPushEngine()->start(); 29 | } 30 | 31 | void PushProxy::stop() { 32 | if (getPushEngine()) { 33 | getPushEngine()->stop(); 34 | delete (this->getPushEngine()); 35 | } 36 | 37 | if (javaCallback) { 38 | delete (javaCallback); 39 | } 40 | 41 | 42 | 43 | } 44 | 45 | void PushProxy::pushSpsPps(uint8_t *sps, int sps_len, uint8_t *pps, int pps_len) { 46 | getPushEngine()->pushSpsPps(sps, sps_len, pps, pps_len); 47 | } 48 | 49 | 50 | 51 | void PushProxy::pushVideoData(uint8_t *video, int len, int keyframe) { 52 | getPushEngine()->pushVideoData(video, len, keyframe); 53 | } 54 | 55 | void PushProxy::pushAudioData(uint8_t *audio, int len, int type) { 56 | getPushEngine()->pushAudioData(audio, len,type); 57 | } 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/stream/amf/AmfType.java: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.stream.amf; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * @Title: AmfType 8 | * @Package com.jimfengfly.rtmppublisher.amf 9 | * @Description: 10 | * @Author Jim 11 | * @Date 2016/11/28 12 | * @Time 上午11:13 13 | * @Version 14 | */ 15 | 16 | public enum AmfType { 17 | /** Number (encoded as IEEE 64-bit double precision floating point number) */ 18 | NUMBER(0x00), 19 | /** Boolean (Encoded as a single byte of value 0x00 or 0x01) */ 20 | BOOLEAN(0x01), 21 | /** String (ASCII encoded) */ 22 | STRING(0x02), 23 | /** Object - set of key/value pairs */ 24 | OBJECT(0x03), 25 | NULL(0x05), 26 | UNDEFINED(0x06), 27 | MAP(0x08), 28 | ARRAY(0x0A); 29 | private byte value; 30 | private static final Map
11 | * author : devyk on 2020-06-13 16:08 12 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 13 | * github : https://github.com/yangkun19921001 14 | * mailbox : yang1001yk@gmail.com 15 | * desc : This is AudioEncoder AAC 编码 16 | *17 | */ 18 | 19 | class AudioEncoder(private val mAudioConfiguration: AudioConfiguration?) : BaseAudioCodec(mAudioConfiguration) { 20 | 21 | override fun onAudioOutformat(outputFormat: MediaFormat?) { 22 | mListener?.onAudioOutformat(outputFormat) 23 | } 24 | 25 | public var mListener: OnAudioEncodeListener? = null 26 | 27 | override fun onAudioData(bb: ByteBuffer, bi: MediaCodec.BufferInfo) { 28 | mListener?.onAudioEncode(bb, bi) 29 | } 30 | 31 | fun setOnAudioEncodeListener(listener: OnAudioEncodeListener?) { 32 | mListener = listener 33 | } 34 | 35 | 36 | override fun start() { 37 | super.start() 38 | } 39 | 40 | override fun stop() { 41 | super.stop() 42 | mListener = null 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/mediacodec/VideoEncoder.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.mediacodec 2 | 3 | import android.media.MediaCodec 4 | import android.media.MediaFormat 5 | import com.devyk.av.rtmp.library.callback.OnVideoEncodeListener 6 | import com.devyk.av.rtmp.library.config.VideoConfiguration 7 | 8 | import java.nio.ByteBuffer 9 | 10 | /** 11 | *
12 | * author : devyk on 2020-07-09 22:57 13 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 14 | * github : https://github.com/yangkun19921001 15 | * mailbox : yang1001yk@gmail.com 16 | * desc : This is H264Encoder 17 | *18 | */ 19 | public open class VideoEncoder : BaseVideoEncoder() { 20 | 21 | 22 | override fun onVideoOutformat(outputFormat: MediaFormat?) { 23 | mListener?.onVideoOutformat(outputFormat) 24 | } 25 | 26 | override fun prepare(videoConfiguration: VideoConfiguration) { 27 | super.prepare(videoConfiguration) 28 | } 29 | 30 | 31 | private var mListener: OnVideoEncodeListener? = null 32 | 33 | /** 34 | * 视频编码完成的回调 35 | */ 36 | override fun onVideoEncode(bb: ByteBuffer?, bi: MediaCodec.BufferInfo) { 37 | mListener?.onVideoEncode(bb!!, bi) 38 | } 39 | 40 | /** 41 | * 设置编码回调 42 | */ 43 | fun setOnVideoEncodeListener(listener: OnVideoEncodeListener) { 44 | mListener = listener 45 | } 46 | 47 | 48 | } -------------------------------------------------------------------------------- /library/src/main/cpp/common/PushProxy.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by 阳坤 on 2020-07-17. 3 | // 4 | 5 | #ifndef RTMPPUSH_PUSHPROXY_H 6 | #define RTMPPUSH_PUSHPROXY_H 7 | 8 | #include "../push/RTMPPush.h" 9 | #include "IPush.h" 10 | 11 | class PushProxy { 12 | 13 | private: 14 | 15 | RTMPPush *rtmpPush = 0; 16 | /** 17 | * 拿到推流的实体类 18 | * @return 19 | */ 20 | IPush *getPushEngine(); 21 | 22 | 23 | public: 24 | PushProxy(); 25 | 26 | static PushProxy * getInstance(); 27 | 28 | 29 | 30 | 31 | void init(const char*url,JavaCallback ** javaCallback); 32 | 33 | const char *url = 0; 34 | JavaCallback *javaCallback = 0; 35 | 36 | 37 | 38 | /** 39 | * 开启线程 40 | */ 41 | void start() ; 42 | 43 | /** 44 | * 停止推流 45 | */ 46 | void stop() ; 47 | 48 | 49 | 50 | /** 51 | * 推送视频第一帧 SPS PPS 数据 52 | * @param sps 53 | * @param sps_len 54 | * @param pps 55 | * @param pps_len 56 | */ 57 | void pushSpsPps(uint8_t *sps, int sps_len, uint8_t *pps, int pps_len) ; 58 | 59 | /** 60 | * 推送音频数据 61 | * @param audio 62 | * @param len 63 | */ 64 | void pushAudioData(uint8_t *audio, int len,int type) ; 65 | 66 | /** 67 | * 推送视频数据 68 | * @param video 69 | * @param len 70 | * @param keyframe 71 | */ 72 | void pushVideoData(uint8_t *video, int len, int keyframe) ; 73 | 74 | }; 75 | 76 | 77 | #endif //RTMPPUSH_PUSHPROXY_H 78 | -------------------------------------------------------------------------------- /library/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.4.1) 2 | 3 | #配置 librtmp 库的路径 4 | set(LIBRTMP_ROOT_PATH ${CMAKE_SOURCE_DIR}/librtmp) 5 | set(JNI_ROOT_PATH ${CMAKE_SOURCE_DIR}/jni) 6 | set(PUSH_ROOT_PATH ${CMAKE_SOURCE_DIR}/push) 7 | set(COMMON_ROOT_PATH ${CMAKE_SOURCE_DIR}/common) 8 | set(CALLBACK_ROOT_PATH ${CMAKE_SOURCE_DIR}/callback) 9 | 10 | #librtmp H 文件 11 | include_directories(${LIBRTMP_ROOT_PATH}/include) 12 | #push H 文件 13 | include_directories(${PUSH_ROOT_PATH}) 14 | #JNI H 文件 15 | include_directories(${JNI_ROOT_PATH}) 16 | #核心库 H 文件 17 | include_directories(${COMMON_ROOT_PATH}) 18 | #Java callback 19 | include_directories(${CALLBACK_ROOT_PATH}) 20 | 21 | 22 | #jni cpp 文件 23 | FILE(GLOB JNI_ALL_CPP ${JNI_ROOT_PATH}/*.cpp) 24 | #push cpp 文件 25 | FILE(GLOB PUSH_ALL_CPP ${PUSH_ROOT_PATH}/*.cpp) 26 | #核心库 cpp 文件 27 | FILE(GLOB COMMON_ALL_CPP ${COMMON_ROOT_PATH}/*.cpp) 28 | #callback 29 | FILE(GLOB CALLBACK_ALL_CPP ${CALLBACK_ROOT_PATH}/*.cpp) 30 | 31 | 32 | add_library( 33 | AVRtmpPush 34 | SHARED 35 | ${JNI_ALL_CPP} 36 | ${PUSH_ALL_CPP} 37 | ${COMMON_ALL_CPP} 38 | ${CALLBACK_ALL_CPP} 39 | ) 40 | 41 | #librtmp 静态库 42 | add_library(rtmp STATIC IMPORTED) 43 | set_target_properties(rtmp PROPERTIES IMPORTED_LOCATION ${LIBRTMP_ROOT_PATH}/libs/${CMAKE_ANDROID_ARCH_ABI}/librtmp.a) 44 | 45 | 46 | 47 | 48 | find_library(log-lib log) 49 | 50 | target_link_libraries( 51 | AVRtmpPush 52 | rtmp 53 | ${log-lib} 54 | ) -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/black/BlackListHelper.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.black 2 | 3 | import android.os.Build 4 | import android.text.TextUtils 5 | import java.util.* 6 | 7 | /** 8 | *
9 | * author : devyk on 2020-06-14 22:11 10 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 11 | * github : https://github.com/yangkun19921001 12 | * mailbox : yang1001yk@gmail.com 13 | * desc : This is BlackListHelper 14 | *15 | */ 16 | 17 | object BlackListHelper { 18 | private val BLACKLISTED_AEC_MODELS = arrayOf("Nexus 5")// Nexus 5 19 | 20 | private val BLACKLISTED_FPS_MODELS = arrayOf("OPPO R9", "Nexus 6P") 21 | 22 | 23 | fun deviceInAecBlacklisted(): Boolean { 24 | val blackListedModels = Arrays.asList(*BLACKLISTED_AEC_MODELS) 25 | for (blackModel in blackListedModels) { 26 | val model = Build.MODEL 27 | if (!TextUtils.isEmpty(model) && model.contains(blackModel)) { 28 | return true 29 | } 30 | } 31 | return false 32 | } 33 | 34 | fun deviceInFpsBlacklisted(): Boolean { 35 | val blackListedModels = Arrays.asList(*BLACKLISTED_FPS_MODELS) 36 | for (blackModel in blackListedModels) { 37 | val model = Build.MODEL 38 | if (!TextUtils.isEmpty(model) && model.contains(blackModel)) { 39 | return true 40 | } 41 | } 42 | return false 43 | } 44 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/common/ThreadImpl.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.common 2 | 3 | import com.devyk.av.rtmp.library.utils.LogHelper 4 | 5 | /** 6 | *
7 | * author : devyk on 2020-07-15 20:48 8 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 9 | * github : https://github.com/yangkun19921001 10 | * mailbox : yang1001yk@gmail.com 11 | * desc : This is ThreadImpl 12 | *13 | */ 14 | public open class ThreadImpl : IThread { 15 | 16 | private var isPause = false 17 | 18 | private var isRuning = false 19 | 20 | private var TAG = javaClass.simpleName 21 | 22 | 23 | override fun start(main: () -> Unit) { 24 | if (isRuning())return 25 | isRuning = true 26 | isPause = false 27 | Thread { 28 | main() 29 | LogHelper.d(TAG, "thread start!") 30 | }.start() 31 | } 32 | 33 | /** 34 | * 线程停止 35 | */ 36 | override fun stop() { 37 | isRuning = false 38 | isPause = true 39 | LogHelper.d(TAG, "thread stop!") 40 | } 41 | 42 | /** 43 | * 设置停止 44 | */ 45 | override fun setPause(pause: Boolean) { 46 | this.isPause = pause 47 | LogHelper.d(TAG, "thread pause:${pause}!") 48 | } 49 | 50 | /** 51 | * 是否停止 52 | */ 53 | override fun isPause(): Boolean = isPause 54 | 55 | 56 | /** 57 | * 是否执行 58 | */ 59 | override fun isRuning(): Boolean = isRuning 60 | 61 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/address_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 |
9 | * author : devyk on 2020-07-15 22:13 10 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 11 | * github : https://github.com/yangkun19921001 12 | * mailbox : yang1001yk@gmail.com 13 | * desc : This is IController 14 | *15 | */ 16 | public interface IController { 17 | 18 | fun start() 19 | 20 | fun pause() 21 | 22 | fun resume() 23 | 24 | fun stop() 25 | 26 | fun setMute(isMute: Boolean) {} 27 | 28 | fun setAudioDataListener(audioDataListener: OnAudioDataListener) {} 29 | fun setVideoDataListener(videoDataListener: OnVideoDataListener) {} 30 | fun setVideoBps(bps:Int){} 31 | 32 | 33 | public interface OnAudioDataListener { 34 | /** 35 | * 当 Audio 编码数据的时候 36 | */ 37 | fun onAudioData(bb: ByteBuffer, bi: MediaCodec.BufferInfo); 38 | 39 | /** 40 | * 编码的输出格式 41 | */ 42 | fun onAudioOutformat(outputFormat: MediaFormat?) 43 | 44 | fun onError(error:String?); 45 | 46 | 47 | } 48 | 49 | public interface OnVideoDataListener { 50 | /** 51 | * 当 Audio 编码数据的时候 52 | */ 53 | fun onVideoData(bb: ByteBuffer?, bi: MediaCodec.BufferInfo?); 54 | 55 | /** 56 | * 编码的输出格式 57 | */ 58 | fun onVideoOutformat(outputFormat: MediaFormat?); 59 | 60 | fun onError(error:String?); 61 | } 62 | } -------------------------------------------------------------------------------- /library/src/main/cpp/push/AVQueue.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by yangw on 2018-9-14. 3 | // 4 | 5 | #include "AVQueue.h" 6 | 7 | AVQueue::AVQueue() { 8 | pthread_mutex_init(&mutexPacket, 0); 9 | pthread_cond_init(&condPacket, 0); 10 | } 11 | 12 | AVQueue::~AVQueue() { 13 | clearQueue(); 14 | pthread_mutex_destroy(&mutexPacket); 15 | pthread_cond_destroy(&condPacket); 16 | 17 | } 18 | 19 | int AVQueue::putRtmpPacket(RTMPPacket *packet) { 20 | pthread_mutex_lock(&mutexPacket); 21 | queuePacket.push(packet); 22 | pthread_cond_signal(&condPacket); 23 | pthread_mutex_unlock(&mutexPacket); 24 | return 0; 25 | } 26 | 27 | RTMPPacket *AVQueue::getRtmpPacket() { 28 | pthread_mutex_lock(&mutexPacket); 29 | 30 | RTMPPacket *p = 0; 31 | if(!queuePacket.empty()) 32 | { 33 | p = queuePacket.front(); 34 | queuePacket.pop(); 35 | } else{ 36 | pthread_cond_wait(&condPacket, &mutexPacket); 37 | } 38 | pthread_mutex_unlock(&mutexPacket); 39 | return p; 40 | } 41 | 42 | void AVQueue::clearQueue() { 43 | 44 | pthread_mutex_lock(&mutexPacket); 45 | while(true) 46 | { 47 | if(queuePacket.empty()) 48 | { 49 | break; 50 | } 51 | RTMPPacket *p = queuePacket.front(); 52 | queuePacket.pop(); 53 | RTMPPacket_Free(p); 54 | p = 0; 55 | } 56 | pthread_mutex_unlock(&mutexPacket); 57 | 58 | } 59 | 60 | void AVQueue::notifyQueue() { 61 | 62 | pthread_mutex_lock(&mutexPacket); 63 | pthread_cond_signal(&condPacket); 64 | pthread_mutex_unlock(&mutexPacket); 65 | 66 | } 67 | -------------------------------------------------------------------------------- /library/src/main/cpp/push/RTMPPush.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by 阳坤 on 2020-07-03. 3 | // 4 | 5 | #ifndef RTMPPUSH_RTMPPUSH_H 6 | #define RTMPPUSH_RTMPPUSH_H 7 | 8 | 9 | #include
16 | * author : devyk on 2020-07-15 22:07 17 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 18 | * github : https://github.com/yangkun19921001 19 | * mailbox : yang1001yk@gmail.com 20 | * desc : This is VideoController Camera 预览和 H264 编码的控制 21 | *22 | */ 23 | 24 | public class VideoController(context: Context,textureId:Int,eglContext: EGLContext?,videoConfiguration: VideoConfiguration) : IController ,OnVideoEncodeListener{ 25 | 26 | 27 | private var mCameraVideoController: CameraRecorder? = null 28 | 29 | private var mListener : IController.OnVideoDataListener?=null 30 | 31 | init { 32 | mCameraVideoController = CameraRecorder(context, textureId, eglContext) 33 | mCameraVideoController?.prepare(videoConfiguration) 34 | mCameraVideoController?.setOnVideoEncodeListener(this) 35 | } 36 | override fun start() { 37 | mCameraVideoController?.start() 38 | } 39 | 40 | override fun stop() { 41 | mCameraVideoController?.stop() 42 | } 43 | 44 | override fun pause() { 45 | mCameraVideoController?.pause() 46 | } 47 | 48 | override fun resume() { 49 | mCameraVideoController?.resume() 50 | } 51 | 52 | 53 | override fun onVideoEncode(bb: ByteBuffer?, bi: MediaCodec.BufferInfo?) { 54 | mListener?.onVideoData(bb,bi) 55 | } 56 | 57 | override fun onVideoOutformat(outputFormat: MediaFormat?) { 58 | mListener?.onVideoOutformat(outputFormat) 59 | } 60 | 61 | 62 | override fun setVideoBps(bps: Int) { 63 | mCameraVideoController?.setEncodeBps(bps) 64 | } 65 | 66 | override fun setVideoDataListener(videoDataListener: IController.OnVideoDataListener) { 67 | mListener = videoDataListener 68 | } 69 | 70 | fun setWatermark(watermark: Watermark) { 71 | mCameraVideoController?.setWatermark(watermark) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/stream/amf/AmfMap.java: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.stream.amf; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.nio.ByteBuffer; 7 | import java.util.Map; 8 | 9 | /** 10 | * @Title: AmfMap 11 | * @Package com.jimfengfly.rtmppublisher.amf 12 | * @Description: 13 | * @Author Jim 14 | * @Date 2016/11/28 15 | * @Time 下午2:48 16 | * @Version 17 | */ 18 | 19 | public class AmfMap extends AmfObject { 20 | @Override 21 | public void writeTo(OutputStream out) throws IOException { 22 | // Begin the map/object/array/whatever exactly this is 23 | out.write(AmfType.MAP.getValue()); 24 | 25 | // Write the "array size" 26 | Util.writeUnsignedInt32(out, properties.size()); 27 | 28 | // Write key/value pairs in this object 29 | for (Map.Entry
13 | * author : devyk on 2020-07-16 21:28 14 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 15 | * github : https://github.com/yangkun19921001 16 | * mailbox : yang1001yk@gmail.com 17 | * desc : This is DefaultPacker 存在 bug 18 | *19 | */ 20 | public class DefaultPacker : Packer { 21 | override fun start() { 22 | 23 | } 24 | 25 | override fun stop() { 26 | } 27 | 28 | private var TAG = javaClass.simpleName 29 | 30 | override fun onVideoSpsPpsData(sps: ByteArray, pps: ByteArray, spsPps: PacketType) { 31 | mPacketListener?.onPacket(sps, pps, PacketType.SPS_PPS) 32 | } 33 | 34 | override fun onVideoData(bb: ByteBuffer?, bi: MediaCodec.BufferInfo?) { 35 | bb?.let { buffer -> 36 | bi?.let { mediaBuffer -> 37 | buffer.position(mediaBuffer.offset) 38 | buffer.limit(mediaBuffer.offset + mediaBuffer.size) 39 | val video = ByteArray(mediaBuffer.size) 40 | buffer.get(video) 41 | val tag = video[4].and(0x1f).toInt() 42 | 43 | var keyFrame = PacketType.VIDEO 44 | if (tag == 0x05) {//关键帧 45 | keyFrame = PacketType.KEY_FRAME 46 | } else { 47 | keyFrame = PacketType.VIDEO 48 | } 49 | mPacketListener?.onPacket(video, keyFrame) 50 | } 51 | } 52 | } 53 | 54 | private var mPacketListener: Packer.OnPacketListener? = null 55 | 56 | 57 | override fun onAudioData(bb: ByteBuffer, bi: MediaCodec.BufferInfo) { 58 | bb?.let { buffer -> 59 | bi?.let { mediaBuffer -> 60 | buffer.position(mediaBuffer.offset) 61 | buffer.limit(mediaBuffer.offset + mediaBuffer.size) 62 | val audio = ByteArray(mediaBuffer.size) 63 | buffer.get(audio) 64 | mPacketListener?.onPacket(audio, PacketType.AUDIO) 65 | } 66 | } 67 | } 68 | 69 | override fun setPacketListener(packetListener: Packer.OnPacketListener) { 70 | mPacketListener = packetListener 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | //apply plugin: 'com.novoda.bintray-release' 4 | apply plugin: 'kotlin-android-extensions' 5 | apply plugin: 'com.novoda.bintray-release'//添加 bintray-release 配置 6 | android { 7 | compileSdkVersion rootProject.ext.android["compileSdkVersion"] 8 | buildToolsVersion rootProject.ext.android["buildToolsVersion"] 9 | 10 | compileOptions { 11 | targetCompatibility JavaVersion.VERSION_1_8 12 | sourceCompatibility JavaVersion.VERSION_1_8 13 | } 14 | 15 | 16 | defaultConfig { 17 | minSdkVersion rootProject.ext.android["minSdkVersion"] 18 | targetSdkVersion rootProject.ext.android["targetSdkVersion"] 19 | versionCode rootProject.ext.android["versionCode"] 20 | versionName rootProject.ext.android["versionName"] 21 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 22 | 23 | externalNativeBuild { 24 | cmake { 25 | //java.lang.UnsatisfiedLinkError: dlopen failed: library "libc++_shared.so" not found 26 | arguments "-DANDROID_STL=c++_shared" 27 | // arguments "-DANDROID_ARM_NEON=TRUE", "-DANDROID_TOOLCHAIN=clang", "-DANDROID_STL=c++_shared" 28 | abiFilters "armeabi-v7a", "arm64-v8a" 29 | //lame undefined reference to `bcopy' 30 | // cppFlags "-frtti -fexceptions" 31 | cFlags "-DSTDC_HEADERS"/* -DSTDC_HEADERS*/ 32 | // cppFlags "-std=c++11" 33 | } 34 | } 35 | } 36 | 37 | buildTypes { 38 | release { 39 | minifyEnabled false 40 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 41 | } 42 | } 43 | externalNativeBuild { 44 | cmake { 45 | path file('src/main/cpp/CMakeLists.txt') 46 | } 47 | } 48 | } 49 | 50 | publish { 51 | //bintray.com 用户名 52 | userOrg = 'yangkun19921001' 53 | // bintray 上仓库的名字 54 | repoName = 'AVRtmpPushSDK' 55 | //jcenter上 的路径 56 | groupId = 'com.devyk.av.rtmp.library' 57 | //项目名称 58 | artifactId = 'AVRtmpPushSDK' 59 | //版本号 60 | publishVersion = '1.0.0'// 描述 61 | //描述 62 | desc = '这是一个由 kotlin 、C++ 编写的 RTMP 推流项目' 63 | //一般填 github 项目地址,一定是要有效的地址 64 | website = 'https://github.com/yangkun19921001/AVRtmpPushSDK' 65 | } 66 | 67 | dependencies { 68 | implementation fileTree(dir: 'libs', include: ['*.jar']) 69 | //test 70 | testImplementation rootProject.ext.testDeps["junit"] 71 | api rootProject.ext.kotlin["core-ktx"] 72 | api rootProject.ext.kotlin["kotlin-stdlib-jdk7"] 73 | } 74 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 |
20 | * author : devyk on 2020-07-09 21:38 21 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 22 | * github : https://github.com/yangkun19921001 23 | * mailbox : yang1001yk@gmail.com 24 | * desc : This is BitmapUtils 25 | *26 | */ 27 | public object BitmapUtils { 28 | /** 29 | * 将文字 生成 文字图片 生成显示编码的Bitmap,目前这个方法是可用的 30 | * 31 | * @param contents 32 | * @param context 33 | * @return 34 | */ 35 | fun creatBitmap(contents: String, context: Context, testSize: Int, testColor: Int, bg: Int): Bitmap { 36 | var scale = context.getResources().getDisplayMetrics().scaledDensity; 37 | var tv = TextView(context); 38 | var layoutParams = LinearLayout.LayoutParams( 39 | LinearLayout.LayoutParams.MATCH_PARENT, 40 | LinearLayout.LayoutParams.WRAP_CONTENT 41 | ); 42 | tv.setLayoutParams(layoutParams); 43 | tv.setText(contents); 44 | tv.setTextSize(scale * testSize); 45 | tv.setGravity(Gravity.CENTER_HORIZONTAL); 46 | tv.setDrawingCacheEnabled(true); 47 | tv.setTextColor(testColor); 48 | tv.measure( 49 | View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), 50 | View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) 51 | ); 52 | tv.layout(0, 0, tv.getMeasuredWidth(), tv.getMeasuredHeight()); 53 | tv.setBackgroundColor(bg); 54 | tv.buildDrawingCache(); 55 | return tv.getDrawingCache(); 56 | } 57 | 58 | fun changeBitmapSize(context: Context, src: Int, width: Float, height: Float): Bitmap { 59 | var bitmap = BitmapFactory.decodeResource(context.applicationContext.resources, src); 60 | return getBitmap(bitmap,width, height); 61 | } 62 | 63 | public fun getBitmap(bitmap: Bitmap,width: Float,height: Float): Bitmap { 64 | var bitmap1 = bitmap 65 | val oldWidth = bitmap1.width 66 | val oldHeight = bitmap1.height 67 | //设置想要的大小 68 | var newWidth = width; 69 | var newHeight = height; 70 | 71 | //计算压缩的比率 72 | var scaleWidth = (newWidth) / oldWidth; 73 | var scaleHeight = (newHeight) / oldHeight; 74 | 75 | //获取想要缩放的matrix 76 | var matrix = Matrix(); 77 | matrix.postScale(scaleWidth, scaleHeight); 78 | //获取新的bitmap 79 | bitmap1 = Bitmap.createBitmap(bitmap, 0, 0, oldWidth, oldHeight, matrix, true); 80 | return bitmap1 81 | } 82 | 83 | 84 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/config/CameraConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.config 2 | 3 | /** 4 | *
5 | * author : devyk on 2020-05-28 23:20 6 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 7 | * github : https://github.com/yangkun19921001 8 | * mailbox : yang1001yk@gmail.com 9 | * desc : This is CameraConfiguration 10 | *11 | */ 12 | class CameraConfiguration private constructor(builder: Builder) { 13 | 14 | val height: Int 15 | val width: Int 16 | val fps: Int 17 | val rotation: Int 18 | val facing: Facing 19 | val orientation: Orientation 20 | val focusMode: FocusMode 21 | 22 | init { 23 | height = builder.height 24 | width = builder.width 25 | facing = builder.facing 26 | fps = builder.fps 27 | orientation = builder.orientation 28 | focusMode = builder.focusMode 29 | rotation = builder.rotation 30 | } 31 | 32 | enum class Facing { 33 | FRONT, 34 | BACK 35 | } 36 | 37 | enum class Orientation { 38 | LANDSCAPE, 39 | PORTRAIT 40 | } 41 | 42 | enum class FocusMode { 43 | AUTO, 44 | TOUCH 45 | } 46 | 47 | 48 | class Builder { 49 | var height = DEFAULT_HEIGHT 50 | var width = DEFAULT_WIDTH 51 | var fps = DEFAULT_FPS 52 | var rotation = DEFAULT_ROTATION 53 | var facing = DEFAULT_FACING 54 | var orientation = DEFAULT_ORIENTATION 55 | var focusMode = DEFAULT_FOCUSMODE 56 | 57 | fun setPreview(height: Int, width: Int): Builder { 58 | this.height = height 59 | this.width = width 60 | return this 61 | } 62 | 63 | fun setFacing(facing: Facing): Builder { 64 | this.facing = facing 65 | return this 66 | } 67 | 68 | fun setOrientation(orientation: Orientation): Builder { 69 | this.orientation = orientation 70 | return this 71 | } 72 | 73 | fun setFps(fps: Int): Builder { 74 | this.fps = fps 75 | return this 76 | } 77 | 78 | fun setFocusMode(focusMode: FocusMode): Builder { 79 | this.focusMode = focusMode 80 | return this 81 | } 82 | 83 | fun setRotation(rot: Int): Builder { 84 | this.rotation = rot 85 | return this 86 | } 87 | 88 | fun build(): CameraConfiguration { 89 | return CameraConfiguration(this) 90 | } 91 | } 92 | 93 | companion object { 94 | val DEFAULT_HEIGHT = 1280 95 | val DEFAULT_WIDTH = 720 96 | val DEFAULT_FPS = 25 97 | val DEFAULT_ROTATION = 0 98 | val DEFAULT_FACING = Facing.BACK 99 | val DEFAULT_ORIENTATION = Orientation.PORTRAIT 100 | val DEFAULT_FOCUSMODE = FocusMode.AUTO 101 | 102 | fun createDefault(): CameraConfiguration { 103 | return Builder().build() 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /library/src/main/cpp/jni/native_rtmp_push.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by 阳坤 on 2020-07-03. 3 | // 4 | #include
12 | * author : devyk on 2020-07-16 21:27 13 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 14 | * github : https://github.com/yangkun19921001 15 | * mailbox : yang1001yk@gmail.com 16 | * desc : This is RtmpSender 17 | *18 | */ 19 | public class RtmpSender : Sender { 20 | private var TAG = javaClass.simpleName 21 | private var listener: OnConnectListener? = null 22 | private var mRtmpUrl: String? = null 23 | 24 | companion object { 25 | init { 26 | System.loadLibrary("AVRtmpPush") 27 | } 28 | } 29 | 30 | 31 | override fun onData(data: ByteArray, type: PacketType) { 32 | if (type == PacketType.FIRST_AUDIO || type == PacketType.AUDIO) { 33 | //音频数据 34 | pushAudio(data, data.size, type.type) 35 | } else if (type == PacketType.FIRST_VIDEO || 36 | type == PacketType.KEY_FRAME || type == PacketType.VIDEO) { 37 | //视频数据 38 | pushVideo(data, data.size, type.type) 39 | } 40 | } 41 | 42 | 43 | fun setDataSource(source: String) { 44 | mRtmpUrl = source 45 | } 46 | 47 | fun connect() { 48 | NativeRtmpConnect(mRtmpUrl) 49 | 50 | } 51 | 52 | fun close() { 53 | NativeRtmpClose() 54 | onClose() 55 | } 56 | 57 | fun setOnConnectListener(lis: OnConnectListener) { 58 | listener = lis 59 | } 60 | 61 | 62 | /** 63 | * C++ 层调用 64 | * 开始链接 65 | */ 66 | fun onConnecting() { 67 | listener?.onConnecting() 68 | } 69 | 70 | /** 71 | * C++ 层调用 72 | * 连接成功 73 | */ 74 | fun onConnected() { 75 | listener?.onConnected() 76 | } 77 | 78 | /** 79 | * C++ 层调用 80 | * 关闭成功 81 | */ 82 | fun onClose() { 83 | listener?.onClose() 84 | } 85 | 86 | 87 | /** 88 | * C++ 层调用 89 | * 推送失败 90 | */ 91 | fun onError(errorCode: Int) { 92 | listener?.onFail(errorCode2errorMessage(errorCode)) 93 | } 94 | 95 | private fun errorCode2errorMessage(errorCode: Int): String { 96 | 97 | var message = "未知错误,请联系管理员!" 98 | if (errorCode == Contacts.RTMP_CONNECT_ERROR) { 99 | message = "RTMP server connect fail!" 100 | } else if (errorCode == Contacts.RTMP_INIT_ERROR) { 101 | message = "RTMP native init fail!" 102 | } else if (errorCode == Contacts.RTMP_SET_URL_ERROR) { 103 | message = "RTMP url set fail!" 104 | } 105 | return message 106 | } 107 | 108 | 109 | private external fun NativeRtmpConnect(url: String?); 110 | private external fun NativeRtmpClose(); 111 | private external fun pushAudio(data: ByteArray, size: Int, type: Int) 112 | private external fun pushVideo(data: ByteArray, size: Int, isKeyFrame: Int) 113 | private external fun pushSpsPps(sps: ByteArray, size: Int, pps: ByteArray, size1: Int) 114 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/config/RendererConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.config 2 | 3 | import android.view.Surface 4 | import com.devyk.av.camera_recorder.callback.IRenderer 5 | import com.devyk.av.rtmp.library.annotation.RendererMode 6 | import com.devyk.av.rtmp.library.camera.renderer.DefaultRenderer 7 | import com.devyk.av.rtmp.library.widget.GLSurfaceView 8 | import javax.microedition.khronos.egl.EGLContext 9 | 10 | /** 11 | *
12 | * author : devyk on 2020-07-06 11:45 13 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 14 | * github : https://github.com/yangkun19921001 15 | * mailbox : yang1001yk@gmail.com 16 | * desc : This is RendererConfiguration 17 | *18 | */ 19 | public class RendererConfiguration private constructor(builder: Builder) { 20 | 21 | 22 | val renderer: IRenderer 23 | 24 | val rendererMode: Int 25 | 26 | val surface: Surface? = null 27 | 28 | var eglContext: EGLContext? = null 29 | 30 | val width: Int 31 | 32 | val height: Int 33 | 34 | 35 | init { 36 | renderer = builder.renderer 37 | rendererMode = builder.rendererMode 38 | width = builder.width 39 | height = builder.height 40 | builder.eglContext?.let { 41 | eglContext = it 42 | } 43 | } 44 | 45 | 46 | class Builder { 47 | var renderer: IRenderer = DEFAULT_RENDERER 48 | 49 | var rendererMode: Int = DEFAULT_RENDERERMODE 50 | 51 | var surface: Surface? = null 52 | 53 | var eglContext: EGLContext? = null 54 | 55 | var width: Int = DEFAULT_WIDTH 56 | 57 | var height: Int = DEFAULT_HEIGHT 58 | 59 | 60 | /** 61 | * 设置渲染器 62 | */ 63 | fun setRenderer(renderer: IRenderer): Builder { 64 | this.renderer = renderer; 65 | return this 66 | } 67 | 68 | /** 69 | * 设置渲染模式 70 | */ 71 | fun setRendererMode(@RendererMode mode: Int): Builder { 72 | this.rendererMode = mode 73 | return this 74 | } 75 | 76 | /** 77 | * 设置显示的 Surface 78 | */ 79 | fun setSurface(surface: Surface): Builder { 80 | this.surface = surface 81 | return this 82 | } 83 | 84 | /** 85 | * 设置 EGL 上下文 86 | */ 87 | fun setEGLContext(context: EGLContext?): Builder { 88 | this.eglContext = context 89 | return this; 90 | } 91 | 92 | /** 93 | * 设置窗口大小 94 | */ 95 | fun setSize(width: Int, height: Int): Builder { 96 | this.width = width 97 | this.height = height 98 | return this 99 | } 100 | 101 | /** 102 | * 构建配置 103 | */ 104 | fun build(): RendererConfiguration { 105 | return RendererConfiguration(this) 106 | } 107 | 108 | 109 | } 110 | 111 | 112 | companion object { 113 | val DEFAULT_RENDERER = DefaultRenderer() 114 | val DEFAULT_RENDERERMODE = GLSurfaceView.RENDERERMODE_CONTINUOUSLY 115 | val DEFAULT_WIDTH = 720 116 | val DEFAULT_HEIGHT = 1280 117 | 118 | 119 | fun createDefault(): RendererConfiguration { 120 | return Builder().build() 121 | } 122 | } 123 | 124 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/mediacodec/BaseAudioCodec.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.mediacodec 2 | 3 | import android.media.MediaCodec 4 | import android.media.MediaFormat 5 | import android.util.Log 6 | import com.devyk.av.rtmp.library.config.AudioConfiguration 7 | import com.devyk.av.rtmp.library.utils.LogHelper 8 | 9 | import java.nio.ByteBuffer 10 | 11 | /** 12 | *
13 | * author : devyk on 2020-06-13 23:53 14 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 15 | * github : https://github.com/yangkun19921001 16 | * mailbox : yang1001yk@gmail.com 17 | * desc : This is BaseCoder 18 | *19 | */ 20 | abstract class BaseAudioCodec(private val mAudioConfiguration: AudioConfiguration?) : IAudioCodec { 21 | private var mMediaCodec: MediaCodec? = null 22 | 23 | 24 | internal var mBufferInfo = MediaCodec.BufferInfo() 25 | 26 | private var TAG = javaClass.simpleName 27 | private var mPts = 0L 28 | 29 | /** 30 | * 编码完成的函数自己不处理,交由子类处理 31 | */ 32 | abstract fun onAudioData(bb: ByteBuffer, bi: MediaCodec.BufferInfo); 33 | 34 | 35 | @Synchronized 36 | override fun start() { 37 | mMediaCodec = AudioMediaCodec.getAudioMediaCodec(mAudioConfiguration!!) 38 | mMediaCodec!!.start() 39 | Log.e("encode", "--start") 40 | } 41 | 42 | /** 43 | * 将数据入队 java.lang.IllegalStateException 44 | */ 45 | @Synchronized 46 | override fun enqueueCodec(input: ByteArray?) { 47 | if (mMediaCodec == null) { 48 | return 49 | } 50 | val inputBuffers = mMediaCodec!!.inputBuffers 51 | val outputBuffers = mMediaCodec!!.outputBuffers 52 | val inputBufferIndex = mMediaCodec!!.dequeueInputBuffer(12000) 53 | 54 | if (inputBufferIndex >= 0) { 55 | val inputBuffer = inputBuffers[inputBufferIndex] 56 | inputBuffer.clear() 57 | inputBuffer.put(input) 58 | mMediaCodec!!.queueInputBuffer(inputBufferIndex, 0, input!!.size, 0, 0) 59 | } 60 | 61 | var outputBufferIndex = mMediaCodec!!.dequeueOutputBuffer(mBufferInfo, 12000) 62 | if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { 63 | onAudioOutformat(mMediaCodec?.outputFormat) 64 | } 65 | 66 | 67 | 68 | while (outputBufferIndex >= 0) { 69 | val outputBuffer = outputBuffers[outputBufferIndex] 70 | 71 | if (mPts == 0L) 72 | mPts = System.nanoTime() / 1000; 73 | 74 | mBufferInfo!!.presentationTimeUs = System.nanoTime() / 1000 - mPts; 75 | 76 | LogHelper.e(TAG, "音频时间戳:${mBufferInfo!!.presentationTimeUs / 1000_000}") 77 | onAudioData(outputBuffer, mBufferInfo) 78 | mMediaCodec!!.releaseOutputBuffer(outputBufferIndex, false) 79 | outputBufferIndex = mMediaCodec!!.dequeueOutputBuffer(mBufferInfo, 0) 80 | } 81 | } 82 | 83 | abstract fun onAudioOutformat(outputFormat: MediaFormat?) 84 | 85 | @Synchronized 86 | override fun stop() { 87 | if (mMediaCodec != null) { 88 | mMediaCodec!!.stop() 89 | mMediaCodec!!.release() 90 | mMediaCodec = null 91 | } 92 | } 93 | 94 | 95 | /** 96 | * 获取输出的格式 97 | */ 98 | public fun getOutputFormat(): MediaFormat? = mMediaCodec?.outputFormat 99 | 100 | 101 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/camera/CameraRecorder.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.camera 2 | 3 | import android.content.Context 4 | import android.view.Surface 5 | import com.devyk.av.camera_recorder.callback.IGLThreadConfig 6 | import com.devyk.av.camera_recorder.callback.IRenderer 7 | import com.devyk.av.rtmp.library.camera.renderer.EncodeRenderer 8 | import com.devyk.av.rtmp.library.mediacodec.VideoEncoder 9 | import com.devyk.av.rtmp.library.widget.GLSurfaceView 10 | import java.lang.ref.WeakReference 11 | import javax.microedition.khronos.egl.EGLContext 12 | 13 | /** 14 | *
15 | * author : devyk on 2020-07-11 15:18 16 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 17 | * github : https://github.com/yangkun19921001 18 | * mailbox : yang1001yk@gmail.com 19 | * desc : This is CameraRecorder 摄像头录制 20 | *21 | */ 22 | public class CameraRecorder(context: Context, textureId: Int, eglContext: EGLContext?) : VideoEncoder(), 23 | IGLThreadConfig { 24 | 25 | 26 | protected lateinit var mRenderer: EncodeRenderer 27 | protected var mEGLContext: EGLContext? 28 | protected var mRendererMode = GLSurfaceView.RENDERERMODE_CONTINUOUSLY 29 | protected var mGLThread: EncodeRendererThread? = null 30 | protected var mSurface: Surface? = null 31 | 32 | init { 33 | this.mEGLContext = eglContext 34 | this.mRenderer = EncodeRenderer(context, textureId) 35 | } 36 | 37 | 38 | /** 39 | * surface 创建的时候开始进行 GL 线程渲染 40 | */ 41 | override fun onSurfaceCreate(surface: Surface?) { 42 | super.onSurfaceCreate(surface) 43 | mSurface = surface 44 | mGLThread = EncodeRendererThread(WeakReference(this)) 45 | mGLThread?.run { 46 | setRendererSize(mConfiguration!!.width, mConfiguration!!.height) 47 | isCreate = true 48 | isChange = true 49 | start() 50 | } 51 | } 52 | 53 | 54 | override fun start() { 55 | super.start() 56 | 57 | } 58 | 59 | override fun stop() { 60 | super.stop() 61 | mGLThread?.onDestory() 62 | } 63 | 64 | 65 | public fun pause() { 66 | mGLThread?.setPause() 67 | 68 | } 69 | 70 | public fun resume() { 71 | mGLThread?.setResume() 72 | } 73 | 74 | override fun getSurface(): Surface? { 75 | if (mSurface != null) 76 | return mSurface 77 | return super.getSurface() 78 | } 79 | 80 | override fun getRenderer(): IRenderer? = mRenderer 81 | override fun getEGLContext(): EGLContext? = mEGLContext 82 | override fun getRendererMode(): Int = mRendererMode 83 | 84 | fun setWatermark(watermark: Watermark) { 85 | mRenderer?.setWatemark(watermark) 86 | } 87 | 88 | 89 | /** 90 | * 摄像头渲染线程 91 | */ 92 | class EncodeRendererThread(weakReference: WeakReference
14 | * author : devyk on 2020-07-15 22:05 15 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 16 | * github : https://github.com/yangkun19921001 17 | * mailbox : yang1001yk@gmail.com 18 | * desc : This is AudioController 音频采集和音频编码的控制 19 | *20 | */ 21 | 22 | public class AudioController(audioConfiguration: AudioConfiguration) : IController, AudioProcessor.OnRecordListener, 23 | OnAudioEncodeListener { 24 | 25 | 26 | /** 27 | * 音频采集-》编解码 需要用到的默认参数 28 | */ 29 | private var mAudioConfiguration = AudioConfiguration.createDefault() 30 | 31 | /** 32 | * 音频编解码用到的实体程序 33 | */ 34 | private lateinit var mAudioEncoder: AudioEncoder 35 | 36 | /** 37 | * 音频采集用到的实体程序 38 | */ 39 | private lateinit var mAudioProcessor: AudioProcessor 40 | 41 | /** 42 | * 音频数据的监听 43 | */ 44 | private var mAudioDataListener: IController.OnAudioDataListener? = null 45 | 46 | 47 | init { 48 | mAudioConfiguration = audioConfiguration 49 | mAudioProcessor = AudioProcessor() 50 | mAudioEncoder = AudioEncoder(mAudioConfiguration) 51 | mAudioProcessor.init( 52 | mAudioConfiguration.audioSource, 53 | mAudioConfiguration.frequency, 54 | mAudioConfiguration.channelCount 55 | ) 56 | mAudioProcessor.addRecordListener(this) 57 | mAudioEncoder.setOnAudioEncodeListener(this) 58 | } 59 | 60 | 61 | /** 62 | * 触发 开始 63 | */ 64 | override fun start() { 65 | mAudioProcessor.startRcording() 66 | } 67 | 68 | /** 69 | * 触发 暂停 70 | */ 71 | override fun pause() { 72 | mAudioProcessor.setPause(true) 73 | } 74 | 75 | /** 76 | * 触发恢复 77 | */ 78 | override fun resume() { 79 | mAudioProcessor.setPause(false) 80 | } 81 | 82 | /** 83 | * 触发停止 84 | */ 85 | override fun stop() { 86 | mAudioProcessor.stop() 87 | 88 | } 89 | 90 | /** 91 | * 当采集 PCM 数据的时候返回 92 | */ 93 | override fun onPcmData(pcmData: ByteArray) { 94 | mAudioEncoder?.enqueueCodec(pcmData) 95 | } 96 | 97 | /** 98 | * 当开始采集 99 | */ 100 | override fun onStart(sampleRate: Int, channels: Int, sampleFormat: Int) { 101 | mAudioEncoder?.start() 102 | } 103 | 104 | /** 105 | * 设置禁言 106 | */ 107 | override fun setMute(isMute: Boolean) { 108 | super.setMute(isMute) 109 | mAudioProcessor?.setMute(isMute) 110 | 111 | 112 | } 113 | 114 | override fun onStop() { 115 | super.onStop() 116 | mAudioEncoder?.stop() 117 | } 118 | 119 | /** 120 | * 当采集出现错误 121 | */ 122 | override fun onError(meg: String?) { 123 | mAudioDataListener?.onError(meg) 124 | } 125 | 126 | /** 127 | * 当 Audio 编码数据的时候 128 | */ 129 | override fun onAudioEncode(bb: ByteBuffer, bi: MediaCodec.BufferInfo) { 130 | mAudioDataListener?.onAudioData(bb, bi) 131 | } 132 | 133 | /** 134 | * 编码的输出格式 135 | */ 136 | override fun onAudioOutformat(outputFormat: MediaFormat?) { 137 | mAudioDataListener?.onAudioOutformat(outputFormat) 138 | } 139 | 140 | override fun setAudioDataListener(audioDataListener: IController.OnAudioDataListener) { 141 | super.setAudioDataListener(audioDataListener) 142 | mAudioDataListener = audioDataListener 143 | } 144 | 145 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/mediacodec/AudioMediaCodec.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.mediacodec 2 | 3 | import android.media.MediaCodec 4 | import android.media.MediaCodecInfo 5 | import android.media.MediaCodecList 6 | import android.media.MediaFormat 7 | import com.devyk.av.rtmp.library.audio.AudioUtils 8 | import com.devyk.av.rtmp.library.config.AudioConfiguration 9 | 10 | import java.nio.ByteBuffer 11 | 12 | /** 13 | *
14 | * author : devyk on 2020-06-13 15:28 15 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 16 | * github : https://github.com/yangkun19921001 17 | * mailbox : yang1001yk@gmail.com 18 | * desc : This is AudioMediaCodec 19 | *20 | */ 21 | public class AudioMediaCodec { 22 | companion object { 23 | 24 | fun selectCodec(mimeType: String): MediaCodecInfo? { 25 | val numCodecs = MediaCodecList.getCodecCount() 26 | for (i in 0 until numCodecs) { 27 | val codecInfo = MediaCodecList.getCodecInfoAt(i) 28 | if (!codecInfo.isEncoder) { 29 | continue 30 | } 31 | val types = codecInfo.supportedTypes 32 | for (j in types.indices) { 33 | if (types[j].equals(mimeType, ignoreCase = true)) { 34 | return codecInfo 35 | } 36 | } 37 | } 38 | return null 39 | } 40 | 41 | 42 | fun getAudioMediaCodec(configuration: AudioConfiguration): MediaCodec? { 43 | //数据类型 "audio/mp4a-latm" 44 | val format = 45 | MediaFormat.createAudioFormat(configuration.mime, configuration.frequency, configuration.channelCount) 46 | if (configuration.mime.equals(AudioConfiguration.DEFAULT_MIME)) { 47 | //用来标记aac的类型 48 | format.setInteger(MediaFormat.KEY_AAC_PROFILE, configuration.aacProfile) 49 | } 50 | //比特率 51 | format.setInteger(MediaFormat.KEY_BIT_RATE, configuration.maxBps * 1024) 52 | //采样率 53 | format.setInteger(MediaFormat.KEY_SAMPLE_RATE, configuration.frequency) 54 | //缓冲区大小 55 | val maxInputSize = 56 | AudioUtils.getMinBufferSize(configuration.frequency, configuration.channelCount, configuration.encoding) 57 | //最大的缓冲区 58 | format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize) 59 | //通道数量 60 | format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, configuration.channelCount) 61 | var mediaCodec: MediaCodec? = null 62 | try { 63 | if (configuration.codeType == AudioConfiguration.CodeType.ENCODE) { 64 | mediaCodec = MediaCodec.createEncoderByType(configuration.mime) 65 | mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) 66 | } else if (configuration.codeType == AudioConfiguration.CodeType.DECODE) { 67 | //用来标记AAC是否有adts头,1->有 68 | format.setInteger(MediaFormat.KEY_IS_ADTS, configuration.adts); 69 | //ByteBuffer key(暂时不了解该参数的含义,但必须设置) 70 | val data = byteArrayOf(0x11.toByte(), 0x90.toByte()) 71 | val csd_0 = ByteBuffer.wrap(data) 72 | //配置解码器 csd-0 信息 73 | //参考:https://developer.android.com/reference/android/media/MediaCodec 74 | format.setByteBuffer("csd-0", csd_0) 75 | mediaCodec = MediaCodec.createDecoderByType(configuration.mime) 76 | mediaCodec.configure(format, null, null, 0) 77 | } 78 | 79 | } catch (e: Exception) { 80 | e.printStackTrace() 81 | if (mediaCodec != null) { 82 | mediaCodec.stop() 83 | mediaCodec.release() 84 | mediaCodec = null 85 | } 86 | } 87 | 88 | return mediaCodec 89 | } 90 | } 91 | 92 | 93 | } -------------------------------------------------------------------------------- /app/src/main/java/com/devyk/av/rtmppush/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.ikavedit.base 2 | 3 | import android.annotation.SuppressLint 4 | import android.graphics.Color 5 | import android.os.Bundle 6 | import android.os.SystemClock 7 | import android.view.Choreographer 8 | import android.view.View 9 | import android.view.Window 10 | import android.view.WindowManager 11 | import android.widget.Chronometer 12 | import android.widget.Toast 13 | import androidx.appcompat.app.AppCompatActivity 14 | import com.devyk.av.rtmppush.R 15 | import com.devyk.av.rtmppush.SPUtils 16 | import com.tbruyelle.rxpermissions2.RxPermissions 17 | 18 | /** 19 | *
20 | * author : devyk on 2020-05-24 23:40 21 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 22 | * github : https://github.com/yangkun19921001 23 | * mailbox : yang1001yk@gmail.com 24 | * desc : This is BaseActivity 25 | *26 | */ 27 | 28 | abstract class BaseActivity
9 | * author : devyk on 2020-06-14 22:07 10 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 11 | * github : https://github.com/yangkun19921001 12 | * mailbox : yang1001yk@gmail.com 13 | * desc : This is VideoConfiguration 14 | *15 | */ 16 | class VideoConfiguration private constructor(builder: Builder) { 17 | 18 | val height: Int 19 | val width: Int 20 | val minBps: Int 21 | val maxBps: Int 22 | val fps: Int 23 | val mediaCodec: Boolean 24 | val ifi: Int 25 | val codeType: ICODEC 26 | val mime: String 27 | var surface: Surface?=null 28 | var spspps: ByteBuffer?=null 29 | 30 | init { 31 | height = builder.height 32 | width = builder.width 33 | minBps = builder.minBps 34 | maxBps = builder.maxBps 35 | fps = builder.fps 36 | ifi = builder.ifi 37 | codeType = builder.codeType 38 | mime = builder.mime 39 | mediaCodec = builder.mediaCodec 40 | surface = builder.surface 41 | spspps = builder.byteBuffer 42 | } 43 | 44 | class Builder { 45 | var mediaCodec = DEFAULT_MEDIA_CODEC 46 | var codeType = DEFAULT_CODEC_TYPE 47 | var height = DEFAULT_HEIGHT 48 | var width = DEFAULT_WIDTH 49 | var minBps = DEFAULT_MIN_BPS 50 | var maxBps = DEFAULT_MAX_BPS 51 | var fps = DEFAULT_FPS 52 | var ifi = DEFAULT_IFI 53 | var mime = DEFAULT_MIME 54 | var surface = DEFAULT_DECODE_SURFACE 55 | var byteBuffer = DEFAULT_SPS_PPS_BUFFER 56 | 57 | fun setSize(width: Int, height: Int): Builder { 58 | this.width = width 59 | this.height = height 60 | return this 61 | } 62 | 63 | fun setBps(minBps: Int, maxBps: Int): Builder { 64 | this.minBps = minBps 65 | this.maxBps = maxBps 66 | return this 67 | } 68 | 69 | fun setFps(fps: Int): Builder { 70 | this.fps = fps 71 | return this 72 | } 73 | 74 | fun setIfi(ifi: Int): Builder { 75 | this.ifi = ifi 76 | return this 77 | } 78 | 79 | fun setMime(mime: String): Builder { 80 | this.mime = mime 81 | return this 82 | } 83 | 84 | fun setMediaCodec(meidaCodec: Boolean): Builder { 85 | this.mediaCodec = meidaCodec 86 | return this 87 | } 88 | 89 | fun setCodeType(codeType: ICODEC): Builder { 90 | this.codeType = codeType 91 | return this 92 | } 93 | 94 | fun setSurface(surface: Surface):Builder{ 95 | this.surface = surface; 96 | return this; 97 | } 98 | 99 | fun setSpsPpsBuffer(buffer: ByteBuffer):Builder{ 100 | this.byteBuffer = buffer; 101 | return this; 102 | } 103 | 104 | fun build(): VideoConfiguration { 105 | return VideoConfiguration(this) 106 | } 107 | } 108 | 109 | companion object { 110 | val DEFAULT_HEIGHT = 1280 111 | val DEFAULT_WIDTH = 720 112 | val DEFAULT_FPS = 25 113 | val DEFAULT_MAX_BPS = 1800 114 | val DEFAULT_MIN_BPS = 400 115 | val DEFAULT_IFI = 2 116 | val DEFAULT_MIME = MediaFormat.MIMETYPE_VIDEO_AVC 117 | val DEFAULT_MEDIA_CODEC = true 118 | val DEFAULT_CODEC_TYPE = ICODEC.ENCODE 119 | val DEFAULT_SPS_PPS_BUFFER : ByteBuffer?= null 120 | /** 121 | * 用于解码显示的 surface 122 | */ 123 | val DEFAULT_DECODE_SURFACE: Surface? = null 124 | 125 | fun createDefault(): VideoConfiguration { 126 | return Builder().build() 127 | } 128 | } 129 | 130 | 131 | public enum class ICODEC(type: Int) { 132 | ENCODE(1), 133 | DECODE(2), 134 | } 135 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/mediacodec/VideoMediaCodec.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.mediacodec 2 | 3 | import android.media.MediaCodec 4 | import android.media.MediaCodec.CONFIGURE_FLAG_ENCODE 5 | import android.media.MediaCodecInfo 6 | import android.media.MediaFormat 7 | import android.util.Log 8 | import com.devyk.av.rtmp.library.black.BlackListHelper 9 | import com.devyk.av.rtmp.library.config.VideoConfiguration 10 | import com.devyk.av.rtmp.library.utils.LogHelper 11 | 12 | /** 13 | *
14 | * author : devyk on 2020-06-14 22:09 15 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 16 | * github : https://github.com/yangkun19921001 17 | * mailbox : yang1001yk@gmail.com 18 | * desc : This is VideoMediaCodec 19 | *20 | */ 21 | object VideoMediaCodec { 22 | private val TAG = this.javaClass.simpleName 23 | 24 | fun getVideoMediaCodec(videoConfiguration: VideoConfiguration): MediaCodec? { 25 | var mediaCodec: MediaCodec? = null 26 | val videoWidth = getVideoSize(videoConfiguration.width) 27 | val videoHeight = getVideoSize(videoConfiguration.height) 28 | val format = MediaFormat.createVideoFormat(videoConfiguration.mime, videoWidth, videoHeight) 29 | if (videoConfiguration.codeType == VideoConfiguration.ICODEC.ENCODE) { 30 | format.setInteger( 31 | MediaFormat.KEY_COLOR_FORMAT, 32 | MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface 33 | ) 34 | format.setInteger(MediaFormat.KEY_BIT_RATE, videoConfiguration.maxBps * 1024) 35 | var fps = videoConfiguration.fps 36 | 37 | 38 | //设置摄像头预览帧率 39 | if (BlackListHelper.deviceInFpsBlacklisted()) { 40 | Log.d(TAG, "Device in fps setting black list, so set mediacodec fps 15") 41 | fps = 15 42 | } 43 | format.setInteger(MediaFormat.KEY_FRAME_RATE, fps) 44 | format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, videoConfiguration.ifi) 45 | format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR) 46 | format.setInteger(MediaFormat.KEY_COMPLEXITY, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR) 47 | 48 | try { 49 | mediaCodec = MediaCodec.createEncoderByType(videoConfiguration.mime) 50 | mediaCodec.configure(format, null, null, CONFIGURE_FLAG_ENCODE) 51 | LogHelper.d(TAG, "mediacodec init successed!") 52 | } catch (e: Exception) { 53 | e.printStackTrace() 54 | mediaCodec = release(mediaCodec) 55 | } 56 | } else if (videoConfiguration.codeType == VideoConfiguration.ICODEC.DECODE) { 57 | try { 58 | mediaCodec = MediaCodec.createDecoderByType(videoConfiguration.mime) 59 | if (videoConfiguration.surface == null) 60 | throw NullPointerException("surface is null?") 61 | if (videoConfiguration.spspps == null) 62 | throw NullPointerException("spspps buffer is null?") 63 | 64 | //csd-0 含义此处有介绍 65 | // @see https://developer.android.com/reference/android/media/MediaCodec 66 | format.setByteBuffer("csd-0", videoConfiguration.spspps) 67 | mediaCodec.configure(format, videoConfiguration.surface, null, 0) 68 | } catch (e: Exception) { 69 | e.printStackTrace() 70 | mediaCodec = release(mediaCodec) 71 | } 72 | } 73 | return mediaCodec 74 | } 75 | 76 | private fun release(mediaCodec: MediaCodec?): MediaCodec? { 77 | var mediaCodec1 = mediaCodec 78 | if (mediaCodec1 != null) { 79 | mediaCodec1.stop() 80 | mediaCodec1.release() 81 | mediaCodec1 = null 82 | } 83 | return mediaCodec1 84 | } 85 | 86 | // We avoid the device-specific limitations on width and height by using values that 87 | // are multiples of 16, which all tested devices seem to be able to handle. 88 | fun getVideoSize(size: Int): Int { 89 | val multiple = Math.ceil(size / 16.0).toInt() 90 | return multiple * 16 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/stream/amf/AmfString.java: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.stream.amf; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.nio.ByteBuffer; 7 | 8 | /** 9 | * @Title: AmfString 10 | * @Package com.jimfengfly.rtmppublisher.amf 11 | * @Description: 12 | * @Author Jim 13 | * @Date 2016/11/28 14 | * @Time 下午2:08 15 | * @Version 16 | */ 17 | 18 | public class AmfString implements AmfData { 19 | private static final String TAG = "AmfString"; 20 | 21 | private String value; 22 | private boolean key; 23 | private int size = -1; 24 | 25 | public AmfString() { 26 | } 27 | 28 | public AmfString(String value, boolean isKey) { 29 | this.value = value; 30 | this.key = isKey; 31 | } 32 | 33 | public AmfString(String value) { 34 | this(value, false); 35 | } 36 | 37 | public AmfString(boolean isKey) { 38 | this.key = isKey; 39 | } 40 | 41 | public String getValue() { 42 | return value; 43 | } 44 | 45 | public void setValue(String value) { 46 | this.value = value; 47 | } 48 | 49 | public boolean isKey() { 50 | return key; 51 | } 52 | 53 | public void setKey(boolean key) { 54 | this.key = key; 55 | } 56 | 57 | @Override 58 | public void writeTo(OutputStream out) throws IOException { 59 | // Strings are ASCII encoded 60 | byte[] byteValue = this.value.getBytes(); 61 | // Write the STRING data type definition (except if this String is used as a key) 62 | if (!key) { 63 | out.write(AmfType.STRING.getValue()); 64 | } 65 | // Write 2 bytes indicating string length 66 | Util.writeUnsignedInt16(out, byteValue.length); 67 | // Write string 68 | out.write(byteValue); 69 | } 70 | 71 | @Override 72 | public void readFrom(InputStream in) throws IOException { 73 | // Skip data type byte (we assume it's already read) 74 | int length = Util.readUnsignedInt16(in); 75 | size = 3 + length; // 1 + 2 + length 76 | // Read string value 77 | byte[] byteValue = new byte[length]; 78 | Util.readBytesUntilFull(in, byteValue); 79 | value = new String(byteValue); 80 | } 81 | 82 | public static String readStringFrom(InputStream in, boolean isKey) throws IOException { 83 | if (!isKey) { 84 | // Read past the data type byte 85 | in.read(); 86 | } 87 | int length = Util.readUnsignedInt16(in); 88 | // Read string value 89 | byte[] byteValue = new byte[length]; 90 | Util.readBytesUntilFull(in, byteValue); 91 | return new String(byteValue); 92 | } 93 | 94 | public static void writeStringTo(OutputStream out, String string, boolean isKey) throws IOException { 95 | // Strings are ASCII encoded 96 | byte[] byteValue = string.getBytes(); 97 | // Write the STRING data type definition (except if this String is used as a key) 98 | if (!isKey) { 99 | out.write(AmfType.STRING.getValue()); 100 | } 101 | // Write 2 bytes indicating string length 102 | Util.writeUnsignedInt16(out, byteValue.length); 103 | // Write string 104 | out.write(byteValue); 105 | } 106 | 107 | @Override 108 | public int getSize() { 109 | size = (isKey() ? 0 : 1) + 2 + value.getBytes().length; 110 | return size; 111 | } 112 | 113 | @Override 114 | public byte[] getBytes() { 115 | int size = getSize(); 116 | ByteBuffer dataBuffer = ByteBuffer.allocate(size); 117 | if(!isKey()) { 118 | dataBuffer.put(AmfType.STRING.getValue()); 119 | } 120 | dataBuffer.putShort((short) value.getBytes().length); 121 | dataBuffer.put(value.getBytes()); 122 | return dataBuffer.array(); 123 | } 124 | 125 | /** @return the byte size of the resulting AMF string of the specified value */ 126 | public static int sizeOf(String string, boolean isKey) { 127 | int size = (isKey ? 0 : 1) + 2 + string.getBytes().length; 128 | return size; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/audio/AudioProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.audio 2 | 3 | import android.media.AudioFormat 4 | import android.media.MediaRecorder 5 | import com.devyk.av.rtmp.library.Contacts 6 | import com.devyk.av.rtmp.library.audio.AudioUtils.AUDIO_CHANNEL_CONFIG 7 | import com.devyk.av.rtmp.library.audio.AudioUtils.AUDIO_FROMAT 8 | import com.devyk.av.rtmp.library.audio.AudioUtils.SAMPLE_RATE_IN_HZ 9 | import com.devyk.av.rtmp.library.audio.AudioUtils.getBufferSize 10 | import com.devyk.av.rtmp.library.common.ThreadImpl 11 | import com.devyk.av.rtmp.library.utils.LogHelper 12 | import java.util.* 13 | 14 | /** 15 | *
16 | * author : devyk on 2020-07-15 21:16 17 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 18 | * github : https://github.com/yangkun19921001 19 | * mailbox : yang1001yk@gmail.com 20 | * desc : This is AudioProcessor 21 | *22 | */ 23 | public class AudioProcessor : ThreadImpl() { 24 | /** 25 | * 读取大小 26 | */ 27 | private var mReadSize = 1024; 28 | 29 | /** 30 | * 录制监听 31 | */ 32 | private var mRecordListener: OnRecordListener? = null 33 | 34 | 35 | /** 36 | * Java 中的锁 37 | */ 38 | private val mLock = java.lang.Object() 39 | 40 | /** 41 | * 是否禁言 42 | */ 43 | private var isMute = false 44 | 45 | /** 46 | * 初始化 47 | */ 48 | public fun init( 49 | audioSource: Int = AudioUtils.AUDIO_SOURCE, 50 | sampleRateInHz: Int = AudioUtils.SAMPLE_RATE_IN_HZ, 51 | channelConfig: Int = AudioUtils.AUDIO_CHANNEL_CONFIG, 52 | audioFormat: Int = AudioUtils.AUDIO_FROMAT 53 | ) { 54 | try { 55 | if (AudioUtils.initAudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat)) { 56 | mReadSize = getBufferSize() 57 | } 58 | } catch (error: Exception) { 59 | mRecordListener?.onError(error.message) 60 | LogHelper.e(Contacts.TAG, error.message) 61 | } 62 | } 63 | 64 | override fun setPause(pause: Boolean) { 65 | super.setPause(pause) 66 | if (pause == true) { 67 | mRecordListener?.onPause() 68 | } else { 69 | mLock.notifyAll() 70 | mRecordListener?.onResume() 71 | } 72 | } 73 | 74 | 75 | /** 76 | * 开始执行 77 | */ 78 | fun startRcording() { 79 | super.start { main() } 80 | AudioUtils.startRecord() 81 | mRecordListener?.onStart() 82 | } 83 | 84 | 85 | override fun stop() { 86 | super.stop() 87 | AudioUtils.stopRecord() 88 | mRecordListener?.onStop() 89 | } 90 | 91 | /** 92 | * 设置禁言 93 | */ 94 | public fun setMute(mute: Boolean) { 95 | this.isMute = mute 96 | } 97 | 98 | public fun isMute() = isMute 99 | 100 | /** 101 | * 子线程执行的函数入口 102 | */ 103 | public fun main() { 104 | var data = ByteArray(mReadSize); 105 | while (isRuning()) { 106 | val name = Thread.currentThread().name 107 | synchronized(mLock) { 108 | 109 | if (isPause()) { 110 | mLock.wait() 111 | } 112 | 113 | if (isMute()) { 114 | Arrays.fill(data, 0) 115 | mRecordListener?.onPcmData(data) 116 | return@synchronized 117 | } 118 | 119 | 120 | if (AudioUtils.read(data.size, data) > 0) { 121 | mRecordListener?.onPcmData(data) 122 | } 123 | } 124 | } 125 | } 126 | 127 | 128 | public fun addRecordListener(listener: OnRecordListener) { 129 | mRecordListener = listener 130 | } 131 | 132 | public interface OnRecordListener { 133 | fun onStart( 134 | sampleRate: Int = SAMPLE_RATE_IN_HZ, 135 | channels: Int = AUDIO_CHANNEL_CONFIG, 136 | sampleFormat: Int = AUDIO_FROMAT 137 | ) 138 | fun onError(meg: String?) 139 | fun onPcmData(byteArray: ByteArray); 140 | fun onPause(){} 141 | fun onResume(){} 142 | fun onStop(){} 143 | } 144 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/stream/packer/rtmp/RtmpPacker.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.stream.packer.rtmp 2 | 3 | import android.media.MediaCodec 4 | import com.devyk.av.rtmp.library.stream.AnnexbHelper 5 | import com.devyk.av.rtmp.library.stream.PacketType 6 | import com.devyk.av.rtmp.library.stream.packer.Packer 7 | import com.devyk.av.rtmp.library.stream.packer.flv.FlvPackerHelper 8 | import com.devyk.av.rtmp.library.stream.packer.flv.FlvPackerHelper.* 9 | import java.nio.ByteBuffer 10 | 11 | /** 12 | *
13 | * author : devyk on 2020-07-18 15:56 14 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 15 | * github : https://github.com/yangkun19921001 16 | * mailbox : yang1001yk@gmail.com 17 | * desc : This is RtmpPacker 18 | *19 | */ 20 | 21 | class RtmpPacker : Packer, AnnexbHelper.AnnexbNaluListener { 22 | 23 | private var packetListener: Packer.OnPacketListener? = null 24 | private var isHeaderWrite: Boolean = false 25 | private var isKeyFrameWrite: Boolean = false 26 | 27 | private var mAudioSampleRate: Int = 44100 28 | private var mAudioSampleSize: Int = 16 29 | private var mIsStereo: Boolean = false 30 | 31 | private val mAnnexbHelper: AnnexbHelper 32 | 33 | init { 34 | mAnnexbHelper = AnnexbHelper() 35 | } 36 | 37 | override fun setPacketListener(listener: Packer.OnPacketListener) { 38 | packetListener = listener 39 | } 40 | 41 | override fun start() { 42 | mAnnexbHelper.setAnnexbNaluListener(this) 43 | } 44 | 45 | override fun onVideoData(bb: ByteBuffer?, bi: MediaCodec.BufferInfo?) { 46 | mAnnexbHelper.analyseVideoData(bb!!, bi!!) 47 | } 48 | 49 | override fun onAudioData(bb: ByteBuffer, bi: MediaCodec.BufferInfo) { 50 | if (packetListener == null || !isHeaderWrite || !isKeyFrameWrite) { 51 | return 52 | } 53 | bb.position(bi.offset) 54 | bb.limit(bi.offset + bi.size) 55 | 56 | val audio = ByteArray(bi.size) 57 | bb.get(audio) 58 | val size = AUDIO_HEADER_SIZE + audio.size 59 | val buffer = ByteBuffer.allocate(size) 60 | FlvPackerHelper.writeAudioTag(buffer, audio, false, mAudioSampleSize) 61 | packetListener!!.onPacket(buffer.array(), PacketType.AUDIO) 62 | } 63 | 64 | override fun stop() { 65 | isHeaderWrite = false 66 | isKeyFrameWrite = false 67 | mAnnexbHelper.stop() 68 | } 69 | 70 | override fun onVideo(video: ByteArray, isKeyFrame: Boolean) { 71 | if (packetListener == null || !isHeaderWrite) { 72 | return 73 | } 74 | var packetType = PacketType.VIDEO 75 | if (isKeyFrame) { 76 | isKeyFrameWrite = true 77 | packetType = PacketType.KEY_FRAME 78 | } 79 | //确保第一帧是关键帧,避免一开始出现灰色模糊界面 80 | if (!isKeyFrameWrite) { 81 | return 82 | } 83 | val size = VIDEO_HEADER_SIZE + video.size 84 | val buffer = ByteBuffer.allocate(size) 85 | FlvPackerHelper.writeH264Packet(buffer, video, isKeyFrame) 86 | packetListener!!.onPacket(buffer.array(),packetType) 87 | } 88 | 89 | 90 | override fun onSpsPps(sps: ByteArray?, pps: ByteArray?) { 91 | if (packetListener == null) { 92 | return 93 | } 94 | //写入第一个视频信息 95 | writeFirstVideoTag(sps, pps) 96 | //写入第一个音频信息 97 | writeFirstAudioTag() 98 | isHeaderWrite = true 99 | } 100 | 101 | private fun writeFirstVideoTag(sps: ByteArray?, pps: ByteArray?) { 102 | val size = VIDEO_HEADER_SIZE + VIDEO_SPECIFIC_CONFIG_EXTEND_SIZE + sps!!.size + pps!!.size 103 | val buffer = ByteBuffer.allocate(size) 104 | FlvPackerHelper.writeFirstVideoTag(buffer, sps, pps) 105 | packetListener!!.onPacket(buffer.array(), PacketType.FIRST_VIDEO) 106 | } 107 | 108 | private fun writeFirstAudioTag() { 109 | val size = AUDIO_SPECIFIC_CONFIG_SIZE + AUDIO_HEADER_SIZE 110 | val buffer = ByteBuffer.allocate(size) 111 | FlvPackerHelper.writeFirstAudioTag(buffer, mAudioSampleRate, mIsStereo, mAudioSampleSize) 112 | packetListener!!.onPacket(buffer.array(), PacketType.FIRST_AUDIO) 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/camera/EglHelper.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.camera 2 | 3 | import android.opengl.EGL14 4 | import android.view.Surface 5 | import javax.microedition.khronos.egl.* 6 | 7 | /** 8 | *
9 | * author : devyk on 2020-07-04 14:49 10 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 11 | * github : https://github.com/yangkun19921001 12 | * mailbox : yang1001yk@gmail.com 13 | * desc : This is EglHelper EGL 环境 14 | *15 | */ 16 | 17 | 18 | class EglHelper { 19 | 20 | private var mEgl: EGL10? = null 21 | private var mEglDisplay: EGLDisplay? = null 22 | private var mEglContext: EGLContext? = null 23 | private var mEglSurface: EGLSurface? = null 24 | 25 | /** 26 | * 初始化 EGL 27 | * @param surface 28 | * @param eglContext 29 | */ 30 | fun initEgl(surface: Surface?, eglContext: EGLContext?) { 31 | 32 | //1、得到 EGL 实例 33 | mEgl = EGLContext.getEGL() as EGL10 34 | 35 | //2、得到默认的显示设备 36 | mEglDisplay = mEgl!!.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY) 37 | if (mEglDisplay === EGL10.EGL_NO_DISPLAY) { 38 | throw RuntimeException("eglGetDisplay failed") 39 | } 40 | 41 | //3、初始化默认的显示设备 42 | val version = IntArray(2) 43 | if (!mEgl!!.eglInitialize(mEglDisplay, version)) { 44 | throw RuntimeException("eglInitialize failed") 45 | } 46 | 47 | //4、设置显示设备的属性 48 | val attrbutes = intArrayOf( 49 | EGL10.EGL_RED_SIZE, 50 | 8, 51 | EGL10.EGL_GREEN_SIZE, 52 | 8, 53 | EGL10.EGL_BLUE_SIZE, 54 | 8, 55 | EGL10.EGL_ALPHA_SIZE, 56 | 8, 57 | EGL10.EGL_DEPTH_SIZE, 58 | 8, 59 | EGL10.EGL_STENCIL_SIZE, 60 | 8, 61 | EGL10.EGL_RENDERABLE_TYPE, 62 | 4, 63 | EGL10.EGL_NONE 64 | ) 65 | 66 | 67 | val num_config = IntArray(1) 68 | require(mEgl!!.eglChooseConfig(mEglDisplay, attrbutes, null, 1, num_config)) { "eglChooseConfig failed" } 69 | 70 | val numConfigs = num_config[0] 71 | require(numConfigs > 0) { "No configs match configSpec" } 72 | 73 | //5、重系统从获取对应属性的配置 74 | val configs = arrayOfNulls
7 | * author : devyk on 2020-06-13 15:26 8 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 9 | * github : https://github.com/yangkun19921001 10 | * mailbox : yang1001yk@gmail.com 11 | * desc : This is AudioConfiguration 12 | *13 | */ 14 | class AudioConfiguration private constructor(builder: Builder) { 15 | 16 | val minBps: Int 17 | val maxBps: Int 18 | val frequency: Int 19 | val encoding: Int 20 | val codeType: CodeType 21 | val channelCount: Int 22 | val adts: Int 23 | val aacProfile: Int 24 | val mime: String 25 | val aec: Boolean 26 | val mediaCodec: Boolean 27 | val audioSource:Int 28 | 29 | init { 30 | minBps = builder.minBps 31 | maxBps = builder.maxBps 32 | frequency = builder.frequency 33 | encoding = builder.encoding 34 | codeType = builder.codeType 35 | channelCount = builder.channelCount 36 | adts = builder.adts 37 | mime = builder.mime 38 | aacProfile = builder.aacProfile 39 | aec = builder.aec 40 | mediaCodec = builder.mediaCodec 41 | audioSource = builder.audioSource 42 | } 43 | 44 | class Builder { 45 | public var mediaCodec = DEFAULT_MEDIA_CODEC 46 | public var minBps = DEFAULT_MIN_BPS 47 | public var maxBps = DEFAULT_MAX_BPS 48 | public var frequency = DEFAULT_FREQUENCY 49 | public var encoding = DEFAULT_AUDIO_ENCODING 50 | public var channelCount = DEFAULT_CHANNEL_COUNT 51 | public var adts = DEFAULT_ADTS 52 | public var mime = DEFAULT_MIME 53 | public var codeType = DEFAULT_CODE_TYPE 54 | public var aacProfile = DEFAULT_AAC_PROFILE 55 | public var aec = DEFAULT_AEC 56 | public var audioSource = DEFAULT_AUDIO_SOURCE 57 | 58 | fun setBps(minBps: Int, maxBps: Int): Builder { 59 | this.minBps = minBps 60 | this.maxBps = maxBps 61 | return this 62 | } 63 | 64 | fun setFrequency(frequency: Int): Builder { 65 | this.frequency = frequency 66 | return this 67 | } 68 | 69 | fun setEncoding(encoding: Int): Builder { 70 | this.encoding = encoding 71 | return this 72 | } 73 | 74 | fun setChannelCount(channelCount: Int): Builder { 75 | this.channelCount = channelCount 76 | return this 77 | } 78 | 79 | fun setAdts(adts: Int): Builder { 80 | this.adts = adts 81 | return this 82 | } 83 | 84 | fun setAacProfile(aacProfile: Int): Builder { 85 | this.aacProfile = aacProfile 86 | return this 87 | } 88 | 89 | fun setMime(mime: String): Builder { 90 | this.mime = mime 91 | return this 92 | } 93 | 94 | fun setAec(aec: Boolean): Builder { 95 | this.aec = aec 96 | return this 97 | } 98 | 99 | fun setMediaCodec(meidaCodec: Boolean): AudioConfiguration.Builder { 100 | this.mediaCodec = meidaCodec 101 | return this 102 | } 103 | 104 | fun setCodecType(codeType: CodeType): Builder { 105 | this.codeType = codeType 106 | return this 107 | } 108 | 109 | fun setAudioSource(source:Int):Builder{ 110 | this.audioSource = source 111 | return this 112 | } 113 | 114 | fun build(): AudioConfiguration { 115 | return AudioConfiguration(this) 116 | } 117 | } 118 | 119 | companion object { 120 | val DEFAULT_FREQUENCY = 44100 121 | val DEFAULT_MAX_BPS = 64 122 | val DEFAULT_MIN_BPS = 32 123 | val DEFAULT_ADTS = 0 124 | val DEFAULT_CODE_TYPE = CodeType.ENCODE 125 | val DEFAULT_MIME = MediaFormat.MIMETYPE_AUDIO_AAC 126 | val DEFAULT_AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT 127 | val DEFAULT_AAC_PROFILE = MediaCodecInfo.CodecProfileLevel.AACObjectLC 128 | val DEFAULT_CHANNEL_COUNT = 1 129 | val DEFAULT_AEC = false 130 | val DEFAULT_AUDIO_SOURCE = MediaRecorder.AudioSource.MIC 131 | val DEFAULT_MEDIA_CODEC = true 132 | 133 | fun createDefault(): AudioConfiguration { 134 | return Builder().build() 135 | } 136 | } 137 | 138 | 139 | public enum class CodeType(codeType: Int) { 140 | ENCODE(1), 141 | DECODE(2), 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/stream/amf/AmfObject.java: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.stream.amf; 2 | 3 | import java.io.BufferedInputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.nio.ByteBuffer; 8 | import java.util.LinkedHashMap; 9 | import java.util.Map; 10 | 11 | /** 12 | * @Title: AmfObject 13 | * @Package com.jimfengfly.rtmppublisher.amf 14 | * @Description: 15 | * @Author Jim 16 | * @Date 2016/11/28 17 | * @Time 下午1:58 18 | * @Version 19 | */ 20 | 21 | public class AmfObject implements AmfData { 22 | protected Map
18 | * author : devyk on 2020-07-04 15:36 19 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 20 | * github : https://github.com/yangkun19921001 21 | * mailbox : yang1001yk@gmail.com 22 | * desc : This is GLSurfaceView 23 | *24 | * 25 | * 26 | * 步骤: 27 | * 1、继承 SurfaceView 并实现 CallBack 回调 28 | * 2、自定义 GLThread 线程类,主要用于 OpenGL 绘制工作 29 | * 3、添加设置 Surface 和 EglContext 的方法 30 | * 4、提供和系统 GLSurfaceView 相同的调用方法 31 | * 32 | */ 33 | public open class GLSurfaceView : SurfaceView, SurfaceHolder.Callback, IGLThreadConfig { 34 | 35 | 36 | 37 | 38 | 39 | public val TAG = javaClass.simpleName 40 | 41 | 42 | /** 43 | * 渲染模式-默认自动模式 44 | */ 45 | private var mRendererMode = RENDERERMODE_CONTINUOUSLY 46 | 47 | /** 48 | * 渲染配置 49 | */ 50 | private lateinit var mRendererConfiguration: RendererConfiguration 51 | 52 | /** 53 | * GLES 渲染线程 54 | */ 55 | private lateinit var mEglThread: GLSurfaceThread 56 | 57 | 58 | private var mSurface: Surface? = null 59 | 60 | private var mEGLContext: EGLContext? = null 61 | 62 | 63 | 64 | 65 | companion object { 66 | /** 67 | * 手动调用渲染 68 | */ 69 | const val RENDERERMODE_WHEN_DIRTY = 0 70 | /** 71 | * 自动渲染 72 | */ 73 | const val RENDERERMODE_CONTINUOUSLY = 1 74 | } 75 | 76 | init { 77 | mRendererConfiguration = RendererConfiguration.createDefault() 78 | } 79 | 80 | /** 81 | * 渲染器 82 | */ 83 | public var mRenderer: IRenderer? = null 84 | 85 | constructor(context: Context?) : this(context, null) 86 | constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0) 87 | constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { 88 | holder.addCallback(this) 89 | } 90 | 91 | override fun surfaceCreated(holder: SurfaceHolder) { 92 | if (mSurface == null) { 93 | mSurface = holder.surface 94 | } 95 | this.mEglThread = GLSurfaceThread( 96 | WeakReference
17 | * author : devyk on 2020-07-06 16:48 18 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 19 | * github : https://github.com/yangkun19921001 20 | * mailbox : yang1001yk@gmail.com 21 | * desc : This is ShaderHelper 对 shader 加载的一些操作帮助类 22 | *23 | */ 24 | public object ShaderHelper { 25 | 26 | private var TAG = this.javaClass.simpleName 27 | 28 | 29 | /** 30 | * 从资源文件中加载着色器源代码 31 | */ 32 | fun getRawShaderResource(context: Context?, id: Int): String { 33 | context?.let { ctx -> 34 | val inputStream = ctx.resources.openRawResource(id) 35 | val bufferedReader = BufferedReader(InputStreamReader(inputStream)) 36 | var sb = StringBuffer() 37 | var line: String? = null 38 | 39 | while (true) { 40 | try { 41 | line = bufferedReader.readLine() 42 | if (TextUtils.isEmpty(line)) { 43 | bufferedReader.close() 44 | return sb.toString() 45 | } 46 | sb.append(line).append("\n") 47 | } catch (error: IOException) { 48 | LogHelper.e(TAG, error.message) 49 | } 50 | } 51 | 52 | } 53 | return "" 54 | } 55 | 56 | /** 57 | * 加载 着色器 代码 58 | */ 59 | @Synchronized 60 | private fun loadShader(shaderType: Int, source: String): Int { 61 | //1、创建着色器 62 | var shader = GLES20.glCreateShader(shaderType) 63 | if (shader == 0) return -1 64 | //2、加载着色器源码 65 | GLES20.glShaderSource(shader, source) 66 | //3. 编译着色器 67 | GLES20.glCompileShader(shader) 68 | //4. 检查是否编译成功 69 | val compile = IntArray(1) 70 | GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compile, 0) 71 | if (compile[0] != GLES20.GL_TRUE) { 72 | LogHelper.e(TAG, "shader compile error"); 73 | GLES20.glDeleteShader(shader); 74 | shader = -1; 75 | } 76 | return shader; 77 | } 78 | 79 | /** 80 | * 创建一个 着色器 的执行程序代码 81 | */ 82 | fun createProgram(vertecSource: String, frameSource: String): Int { 83 | val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertecSource) 84 | val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, frameSource) 85 | if (vertexShader == -1 || fragmentShader == -1) return -1 86 | //1. 创建一个渲染程序 87 | val program = GLES20.glCreateProgram() 88 | //2. 将着色器程序添加到渲染程序中 89 | GLES20.glAttachShader(program, vertexShader) 90 | GLES20.glAttachShader(program, fragmentShader) 91 | //3. 链接程序 92 | GLES20.glLinkProgram(program) 93 | return program 94 | } 95 | 96 | 97 | fun createTextImage(text: String, textSize: Int, textColor: String, bgColor: String?, padding: Int): Bitmap { 98 | val paint = Paint() 99 | paint.color = Color.parseColor(textColor) 100 | paint.textSize = textSize.toFloat() 101 | paint.style = Paint.Style.FILL 102 | paint.isAntiAlias = true 103 | 104 | val width = paint.measureText(text, 0, text.length) 105 | 106 | val top = paint.fontMetrics.top 107 | val bottom = paint.fontMetrics.bottom 108 | 109 | val bm = Bitmap.createBitmap( 110 | (width + padding * 2).toInt(), 111 | (bottom - top + padding * 2).toInt(), 112 | Bitmap.Config.ARGB_8888 113 | ) 114 | val canvas = Canvas(bm) 115 | 116 | canvas.drawColor(Color.parseColor(bgColor)) 117 | 118 | 119 | canvas.drawText(text, padding.toFloat(), -top + padding, paint) 120 | return bm 121 | } 122 | 123 | fun loadBitmapTexture(bitmap: Bitmap): Int { 124 | val textureIds = IntArray(1) 125 | GLES20.glGenTextures(1, textureIds, 0) 126 | GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIds[0]) 127 | GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT) 128 | GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT) 129 | GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR) 130 | GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR) 131 | 132 | val bitmapBuffer = ByteBuffer.allocate(bitmap.height * bitmap.width * 4) 133 | bitmap.copyPixelsToBuffer(bitmapBuffer) 134 | bitmapBuffer.flip() 135 | 136 | GLES20.glTexImage2D( 137 | GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, bitmap.width, 138 | bitmap.height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, bitmapBuffer 139 | ) 140 | return textureIds[0] 141 | } 142 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/stream/amf/Util.java: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.stream.amf; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | 7 | /** 8 | * @Title: Util 9 | * @Package com.jimfengfly.rtmppublisher.amf 10 | * @Description: 11 | * @Author Jim 12 | * @Date 2016/11/28 13 | * @Time 下午12:53 14 | * @Version 15 | */ 16 | public class Util { 17 | 18 | private static final String HEXES = "0123456789ABCDEF"; 19 | 20 | public static void writeUnsignedInt32(OutputStream out, int value) throws IOException { 21 | out.write((byte) (value >>> 24)); 22 | out.write((byte) (value >>> 16)); 23 | out.write((byte) (value >>> 8)); 24 | out.write((byte) value); 25 | } 26 | 27 | public static int readUnsignedInt32(InputStream in) throws IOException { 28 | return ((in.read() & 0xff) << 24) | ((in.read() & 0xff) << 16) | ((in.read() & 0xff) << 8) | (in.read() & 0xff); 29 | } 30 | 31 | public static int readUnsignedInt24(InputStream in) throws IOException { 32 | return ((in.read() & 0xff) << 16) | ((in.read() & 0xff) << 8) | (in.read() & 0xff); 33 | } 34 | 35 | public static int readUnsignedInt16(InputStream in) throws IOException { 36 | return ((in.read() & 0xff) << 8) | (in.read() & 0xff); 37 | } 38 | 39 | public static void writeUnsignedInt24(OutputStream out, int value) throws IOException { 40 | out.write((byte) (value >>> 16)); 41 | out.write((byte) (value >>> 8)); 42 | out.write((byte) value); 43 | } 44 | 45 | public static void writeUnsignedInt16(OutputStream out, int value) throws IOException { 46 | out.write((byte) (value >>> 8)); 47 | out.write((byte) value); 48 | } 49 | 50 | public static int toUnsignedInt32(byte[] bytes) { 51 | return (((int) bytes[0] & 0xff) << 24) | (((int)bytes[1] & 0xff) << 16) | (((int)bytes[2] & 0xff) << 8) | ((int)bytes[3] & 0xff); 52 | } 53 | 54 | public static int toUnsignedInt32LittleEndian(byte[] bytes) { 55 | return ((bytes[3] & 0xff) << 24) | ((bytes[2] & 0xff) << 16) | ((bytes[1] & 0xff) << 8) | (bytes[0] & 0xff); 56 | } 57 | 58 | public static void writeUnsignedInt32LittleEndian(OutputStream out, int value) throws IOException { 59 | out.write((byte) value); 60 | out.write((byte) (value >>> 8)); 61 | out.write((byte) (value >>> 16)); 62 | out.write((byte) (value >>> 24)); 63 | } 64 | 65 | public static int toUnsignedInt24(byte[] bytes) { 66 | return ((bytes[1] & 0xff) << 16) | ((bytes[2] & 0xff) << 8) | (bytes[3] & 0xff); 67 | } 68 | 69 | public static int toUnsignedInt16(byte[] bytes) { 70 | return ((bytes[2] & 0xff) << 8) | (bytes[3] & 0xff); 71 | } 72 | 73 | public static String toHexString(byte[] raw) { 74 | if (raw == null) { 75 | return null; 76 | } 77 | final StringBuilder hex = new StringBuilder(2 * raw.length); 78 | for (final byte b : raw) { 79 | hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F))); 80 | } 81 | return hex.toString(); 82 | } 83 | 84 | public static String toHexString(byte b) { 85 | return new StringBuilder().append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F))).toString(); 86 | } 87 | 88 | /** 89 | * Reads bytes from the specified inputstream into the specified target buffer until it is filled up 90 | */ 91 | public static void readBytesUntilFull(InputStream in, byte[] targetBuffer) throws IOException { 92 | int totalBytesRead = 0; 93 | int read; 94 | final int targetBytes = targetBuffer.length; 95 | do { 96 | read = in.read(targetBuffer, totalBytesRead, (targetBytes - totalBytesRead)); 97 | if (read != -1) { 98 | totalBytesRead += read; 99 | } else { 100 | throw new IOException("Unexpected EOF reached before read buffer was filled"); 101 | } 102 | } while (totalBytesRead < targetBytes); 103 | } 104 | 105 | public static byte[] toByteArray(double d) { 106 | long l = Double.doubleToRawLongBits(d); 107 | return new byte[]{ 108 | (byte) ((l >> 56) & 0xff), 109 | (byte) ((l >> 48) & 0xff), 110 | (byte) ((l >> 40) & 0xff), 111 | (byte) ((l >> 32) & 0xff), 112 | (byte) ((l >> 24) & 0xff), 113 | (byte) ((l >> 16) & 0xff), 114 | (byte) ((l >> 8) & 0xff), 115 | (byte) (l & 0xff),}; 116 | } 117 | 118 | public static byte[] unsignedInt32ToByteArray(int value) throws IOException { 119 | return new byte[]{ 120 | (byte) (value >>> 24), 121 | (byte) (value >>> 16), 122 | (byte) (value >>> 8), 123 | (byte) value}; 124 | } 125 | 126 | public static double readDouble(InputStream in) throws IOException { 127 | long bits = ((long) (in.read() & 0xff) << 56) | ((long) (in.read() & 0xff) << 48) | ((long) (in.read() & 0xff) << 40) | ((long) (in.read() & 0xff) << 32) | ((in.read() & 0xff) << 24) | ((in.read() & 0xff) << 16) | ((in.read() & 0xff) << 8) | (in.read() & 0xff); 128 | return Double.longBitsToDouble(bits); 129 | } 130 | 131 | public static void writeDouble(OutputStream out, double d) throws IOException { 132 | long l = Double.doubleToRawLongBits(d); 133 | out.write(new byte[]{ 134 | (byte) ((l >> 56) & 0xff), 135 | (byte) ((l >> 48) & 0xff), 136 | (byte) ((l >> 40) & 0xff), 137 | (byte) ((l >> 32) & 0xff), 138 | (byte) ((l >> 24) & 0xff), 139 | (byte) ((l >> 16) & 0xff), 140 | (byte) ((l >> 8) & 0xff), 141 | (byte) (l & 0xff)}); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/audio/AudioUtils.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.audio 2 | 3 | import android.media.AudioFormat 4 | import android.media.AudioRecord 5 | import android.media.MediaRecorder 6 | import android.util.Log 7 | import java.lang.RuntimeException 8 | import java.nio.ByteBuffer 9 | 10 | /** 11 | *
12 | * author : devyk on 2020-07-15 21:10 13 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 14 | * github : https://github.com/yangkun19921001 15 | * mailbox : yang1001yk@gmail.com 16 | * desc : This is AudioUtils 17 | *18 | */ 19 | 20 | /** 21 | * 调用 startRecording 开始录音,调用 stopRecord 停止录音 22 | * 利用adb pull 导出PCM文件 23 | * adb pull record . 24 | * 利用ffplay播放声音 25 | * ffplay -f s16le -sample_rate 44100 -channels 1 -i /record.pcm 26 | * 利用ffmpeg将PCM文件转换为WAV文件 27 | * ffmpeg -f s16le -sample_rate 44100 -channels 1 -i record.pcm -acodec pcm_s16le record.wav 28 | */ 29 | public object AudioUtils { 30 | 31 | 32 | private var TAG = javaClass.simpleName; 33 | 34 | 35 | /** 36 | * 录音对象 37 | * @see AudioRecord 38 | */ 39 | private var mAudioRecord: AudioRecord? = null; 40 | 41 | /** 42 | * 声音通道 43 | * 默认 单声道 44 | * @see AudioFormat.CHANNEL_IN_MONO 单声道 45 | * @see AudioFormat.CHANNEL_IN_STEREO 立体声 46 | */ 47 | public var AUDIO_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO 48 | 49 | /** 50 | * 采样率 如果 AudioRecord 初始化失败,那么可以降低为 16000 ,或者检查权限是否开启 51 | * 默认 44100 52 | */ 53 | public var SAMPLE_RATE_IN_HZ = 44100 54 | 55 | /** 56 | * 采样格式 57 | * 默认 16bit 存储 58 | * 59 | * @see AudioFormat.ENCODING_PCM_16BIT 兼容大部分手机 60 | */ 61 | public var AUDIO_FROMAT = AudioFormat.ENCODING_PCM_16BIT 62 | 63 | /** 64 | * 录音源 65 | * @see MediaRecorder.AudioSource.MIC 手机麦克风 66 | * @see MediaRecorder.AudioSource.VOICE_RECOGNITION 用于语音识别,等同于默认 67 | * @see MediaRecorder.AudioSource.VOICE_COMMUNICATION 用于 VOIP 应用 68 | */ 69 | public var AUDIO_SOURCE = MediaRecorder.AudioSource.MIC; 70 | 71 | /** 72 | * 配置内部音频缓冲区的大小,由于不同厂商会有不同的实现。那么我们可以通过一个静态函数来 getMinBufferSize 来定义 73 | * @see AudioRecord.getMinBufferSize 74 | */ 75 | private var mBufferSizeInBytes = 0; 76 | 77 | 78 | /** 79 | * 获取音频缓冲区大小 80 | */ 81 | public fun getMinBufferSize( 82 | sampleRateInHz: Int = SAMPLE_RATE_IN_HZ, 83 | channelConfig: Int = AUDIO_CHANNEL_CONFIG, 84 | audioFormat: Int = AUDIO_FROMAT 85 | ): Int = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat); 86 | 87 | /** 88 | * 拿到 AudioRecord 对象 89 | */ 90 | public fun initAudioRecord( 91 | audioSource: Int = AUDIO_SOURCE, 92 | sampleRateInHz: Int = SAMPLE_RATE_IN_HZ, 93 | channelConfig: Int = AUDIO_CHANNEL_CONFIG, 94 | audioFormat: Int = AUDIO_FROMAT 95 | ): Boolean { 96 | this.AUDIO_FROMAT = audioFormat 97 | this.AUDIO_CHANNEL_CONFIG = channelConfig 98 | this.AUDIO_SOURCE = audioSource 99 | this.SAMPLE_RATE_IN_HZ = sampleRateInHz 100 | 101 | //如果 AudioRecord 不为 null 那么直接销毁 102 | mAudioRecord?.run { 103 | release(); 104 | } 105 | try { 106 | //得到录音缓冲大小 107 | mBufferSizeInBytes = getMinBufferSize(sampleRateInHz, channelConfig, audioFormat) 108 | mAudioRecord = AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, mBufferSizeInBytes) 109 | } catch (error: Exception) { 110 | Log.e(TAG, "AudioRecord init error :${error.message}") 111 | return false 112 | } 113 | 114 | //如果初始化失败那么降低采样率 115 | if (mAudioRecord == null || mAudioRecord?.state != AudioRecord.STATE_INITIALIZED) { 116 | throw RuntimeException("检查音频源是否为占用,或者是否打开录音权限?") 117 | } 118 | return true 119 | } 120 | 121 | /** 122 | * 拿到录音设备 123 | */ 124 | public fun getAudioRecord(): AudioRecord? = mAudioRecord 125 | 126 | /** 127 | * 开始录制 128 | */ 129 | public fun startRecord() { 130 | mAudioRecord?.run { 131 | if (state == AudioRecord.STATE_INITIALIZED) 132 | startRecording() 133 | } 134 | 135 | } 136 | 137 | /** 138 | * 开始录制 139 | */ 140 | public fun stopRecord() { 141 | mAudioRecord?.run { 142 | if (mAudioRecord?.state == AudioRecord.STATE_INITIALIZED) 143 | stop() 144 | } 145 | } 146 | 147 | /** 148 | * 释放资源 149 | */ 150 | public fun releaseRecord() { 151 | mAudioRecord?.run { 152 | release() 153 | } 154 | mAudioRecord = null 155 | } 156 | 157 | /** 158 | * 读取音频数据 159 | */ 160 | public fun read(bufferSize: Int = mBufferSizeInBytes, offsetInBytes: Int = 0, byte: ByteArray): Int { 161 | var ret = 0; 162 | mAudioRecord?.run { 163 | ret = read(byte, offsetInBytes, bufferSize) 164 | } 165 | return ret; 166 | } 167 | 168 | /** 169 | * 读取音频数据 170 | */ 171 | public fun read(bufferSize: Int = mBufferSizeInBytes, offsetInBytes: Int = 0, short: ShortArray): Int { 172 | var ret = 0; 173 | mAudioRecord?.run { 174 | ret = read(short, offsetInBytes, bufferSize) 175 | } 176 | return ret; 177 | } 178 | 179 | /** 180 | * 读取音频数据 181 | */ 182 | public fun read(bufferSize: Int = mBufferSizeInBytes, buffer: ByteBuffer): Int { 183 | var ret = 0; 184 | mAudioRecord?.run { 185 | ret = read(buffer, bufferSize) 186 | } 187 | return ret; 188 | } 189 | 190 | /** 191 | * 读取音频数据 192 | */ 193 | public fun read(bufferSize: Int = mBufferSizeInBytes, buffer: ByteArray): Int { 194 | var ret = 0; 195 | mAudioRecord?.run { 196 | ret = read(buffer, 0, bufferSize) 197 | } 198 | return ret; 199 | } 200 | 201 | 202 | /** 203 | * 拿到缓冲大小 204 | */ 205 | public fun getBufferSize(): Int = mBufferSizeInBytes 206 | 207 | 208 | } -------------------------------------------------------------------------------- /library/src/main/java/com/devyk/av/rtmp/library/camera/GLThread.kt: -------------------------------------------------------------------------------- 1 | package com.devyk.av.rtmp.library.camera 2 | 3 | import com.devyk.av.camera_recorder.callback.IGLThreadConfig 4 | import com.devyk.av.rtmp.library.utils.LogHelper 5 | import com.devyk.av.rtmp.library.widget.GLSurfaceView 6 | import java.lang.Exception 7 | import java.lang.ref.WeakReference 8 | 9 | /** 10 | *
11 | * author : devyk on 2020-07-08 21:03 12 | * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts 13 | * github : https://github.com/yangkun19921001 14 | * mailbox : yang1001yk@gmail.com 15 | * desc : This is GLThread 自定义 GLThread 线程类,主要用于 OpenGL 的绘制操作 16 | *17 | */ 18 | public open class GLThread(weakReference: WeakReference