通过 RootEncoder 进行安卓直播 RTSP 推流
当前需要把安卓摄像头所生成的视频流通过 RTSP 协议传输到服务器上,也就是推流。最开始用 libstreaming[1],直接 source 引入,不知为何压根没有推流,故放弃。估计是太老 API 吧,都七年前最后更新的。于是再网上搜索下,结论是没啥好的安卓直播推流组件,要么就是老掉牙的。
最后 AI 推荐这款 RootEncoder https://github.com/pedroSG94/RootEncoder[2],持续更新的。但我先要批判它一番,因为问题确实多多,搞得我头发掉不少:
•模块众多,甚至有 iOS 版本,比较混乱,文档也说不清楚的样子•没有文档,没有例子,要自己摸索,AI 的例子也跑不通•依赖是在 jitpack 的,你要另外配置,——我是安卓新手,这个搞半天•API 混乱,差一个小版本就没了某个类,——作者重构的任意性太大,搞的例子都不通用•它的名字也换来换去,搞得我不好搜索。早期叫 rtmp-rtsp-stream-client-java 后来改为 RootEncoder
虽然搞起来没有一帆风顺,但通过不懈的努力,在老外一篇文章[3]帮助下,终于调通 RSTP 推流,于是写就此外——以飨读者!
添加依赖
依赖是在 jitpack 的,其他地方没有。操作是:打开工程根目录下的settings.gradle加入maven { url 'https://jitpack.io' }
加入maven { url 'https://jitpack.io' }依赖源。
保存然后打开app/build.gradle,加入rtmp-rtsp-stream-client的依赖,注意版本不能错。新版本 API 又不同,代码也不晓得怎么改(这货就是这样)。
implementation 'com.github.pedroSG94.rtmp-rtsp-stream-client-java:rtplibrary:2.2.4'
最后点击菜单下面这里的才有效,执行网络远程下载相关的依赖。

安卓代码开发
添加权限
老操作了,对AndroidManifest.xml添加权限:
<uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.RECORD_AUDIO" /><uses-permission android:name="android.permission.CAMERA" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><!--Optional for play store--><uses-feature android:name="android.hardware.camera" android:required="false" /><uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
添加布局文件
目录res/layout下新建activity_open_gl_rtsp.xml:
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".OpenGlRtspActivity"><com.pedro.rtplibrary.view.OpenGlViewandroid:id="@+id/surfaceView"android:layout_width="match_parent"android:layout_height="match_parent" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_alignParentBottom="true"android:orientation="vertical"android:padding="16dp"><EditTextandroid:id="@+id/et_rtp_url"android:layout_width="match_parent"android:layout_height="wrap_content"android:hint="RTSP URL"android:inputType="textUri"android:padding="8dp" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:layout_marginTop="16dp"><Buttonandroid:id="@+id/b_start_stop"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="Start"android:layout_marginEnd="8dp" /><Buttonandroid:id="@+id/switch_camera"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="Switch Camera" /></LinearLayout><Buttonandroid:id="@+id/b_record"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="Record"android:layout_marginTop="8dp" /></LinearLayout></RelativeLayout>
回到里面注册一下新布局:
<activityandroid:name=".OpenGlRtspActivity"android:exported="false"android:theme="@style/Theme.MyApplication" />
页面逻辑
新建OpenGlRtspActivity.kt:
import android.os.Bundleimport android.util.Logimport android.view.Menuimport android.view.MenuItemimport android.view.MotionEventimport android.view.SurfaceHolderimport android.view.Viewimport android.view.View.OnTouchListenerimport android.view.WindowManagerimport android.widget.Buttonimport android.widget.EditTextimport android.widget.Toastimport androidx.activity.ComponentActivityimport com.pedro.encoder.input.gl.SpriteGestureControllerimport com.pedro.encoder.input.video.CameraOpenExceptionimport com.pedro.rtplibrary.rtsp.RtspCamera1import com.pedro.rtplibrary.view.OpenGlViewimport com.pedro.rtsp.utils.ConnectCheckerRtspimport java.io.Fileclass OpenGlRtspActivity : ComponentActivity(), ConnectCheckerRtsp, View.OnClickListener,SurfaceHolder.Callback, OnTouchListener {private var rtspCamera1: RtspCamera1? = nullprivate lateinit var button: Buttonprivate lateinit var bRecord: Buttonprivate lateinit var etUrl: EditTextprivate var currentDateAndTime = ""private var folder: File? = nullprivate lateinit var openGlView: OpenGlViewprivate val spriteGestureController = SpriteGestureController()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)setContentView(R.layout.activity_open_gl_rtsp)openGlView = findViewById<OpenGlView>(R.id.surfaceView)button = findViewById<Button>(R.id.b_start_stop)button.setOnClickListener(this)bRecord = findViewById<Button>(R.id.b_record)bRecord.setOnClickListener(this)etUrl = findViewById<EditText>(R.id.et_rtp_url)etUrl.setHint("RTSP")etUrl.setText("")val switchCamera = findViewById<Button>(R.id.switch_camera)switchCamera.setOnClickListener(this)rtspCamera1 = RtspCamera1(openGlView, this)openGlView.holder.addCallback(this)openGlView.setOnTouchListener(this)}override fun onCreateOptionsMenu(menu: Menu): Boolean {// I commented this line because I don't need this menu// menuInflater.inflate(R.menu.gl_menu, menu)return true}override fun onOptionsItemSelected(item: MenuItem): Boolean {//Stop listener for image, text and gif stream objects.spriteGestureController.stopListener()return false}override fun onConnectionStartedRtsp(rtspUrl: String) {}override fun onConnectionSuccessRtsp() {runOnUiThread {Toast.makeText(this@OpenGlRtspActivity,"Connection success",Toast.LENGTH_SHORT).show()}}override fun onConnectionFailedRtsp(reason: String) {Log.e("RTTMA", reason)runOnUiThread {Toast.makeText(this@OpenGlRtspActivity,"Connection failed. $reason",Toast.LENGTH_SHORT).show()rtspCamera1!!.stopStream()button.setText("Start")}}override fun onNewBitrateRtsp(bitrate: Long) {}override fun onDisconnectRtsp() {runOnUiThread {Toast.makeText(this@OpenGlRtspActivity, "Disconnected", Toast.LENGTH_SHORT).show()}}override fun onAuthErrorRtsp() {runOnUiThread {Toast.makeText(this@OpenGlRtspActivity, "Auth error", Toast.LENGTH_SHORT).show()}}override fun onAuthSuccessRtsp() {runOnUiThread {Toast.makeText(this@OpenGlRtspActivity, "Auth success", Toast.LENGTH_SHORT).show()}}override fun onClick(view: View) {when (view.id) {R.id.b_start_stop -> if (!rtspCamera1!!.isStreaming) {if (rtspCamera1!!.isRecording|| rtspCamera1!!.prepareAudio() && rtspCamera1!!.prepareVideo()) {button.text = "Stop"rtspCamera1!!.startStream(etUrl!!.text.toString())} else {Toast.makeText(this, "Error preparing stream, This device cant do it",Toast.LENGTH_SHORT).show()}} else {button.text = "Start"rtspCamera1!!.stopStream()}R.id.switch_camera -> try {rtspCamera1!!.switchCamera()} catch (e: CameraOpenException) {Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show()}else -> {}}}override fun surfaceCreated(surfaceHolder: SurfaceHolder) {}override fun surfaceChanged(surfaceHolder: SurfaceHolder, i: Int, i1: Int, i2: Int) {rtspCamera1!!.startPreview()}override fun surfaceDestroyed(surfaceHolder: SurfaceHolder) {if (rtspCamera1!!.isStreaming) {rtspCamera1!!.stopStream()button.text = "Start"}rtspCamera1!!.stopPreview()}override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {if (spriteGestureController.spriteTouched(view, motionEvent)) {spriteGestureController.moveSprite(view, motionEvent)spriteGestureController.scaleSprite(motionEvent)return true}return false}}
最后制作一个按钮作为入口:
Button(onClick = {val intent = Intent(context, OpenGlRtspActivity::class.java)context.startActivity(intent)},modifier = Modifier.padding(top = 16.dp)) {Text(text = "RootEncoder推流")}
搞定~ 下面是安卓的运行界面:

其他开源
•Android端RTMP推流实现- RtmpPublishKit,挺不错的,作者有文章[4]专门介绍实现思路[5],可惜的是不再开源; 博客文章《深度解析RTMP直播协议:从保姆级入门到高级优化!》[6]•编写一个简单的RTSP协议-主流程[7]https://github.com/ImSjt/RtspServer[8]•ZLMediaKit-Android-Stream[9]•播放器,不是推流 https://github.com/alexeyvasilyev/rtsp-client-android[10]
References
[1] libstreaming: https://github.com/fyhertz/libstreaming-examples[2]: https://github.com/pedroSG94/RootEncoder[3] 一篇文章: https://yeahlowflicker.com/blog/developing-an-android-app-for-rtsp-streaming[4] 文章: https://www.jianshu.com/p/0318ff29ac32[5] 实现思路: https://www.jianshu.com/p/00aceabce944[6] 《深度解析RTMP直播协议:从保姆级入门到高级优化!》: https://www.pixpark.net/blog/rtmp-protocol.html[7] 编写一个简单的RTSP协议-主流程: https://100ask.net/article/82[8]: https://github.com/ImSjt/RtspServer[9] ZLMediaKit-Android-Stream: https://github.com/yunianvh/ZLMediaKit-Android-Stream[10]: https://github.com/alexeyvasilyev/rtsp-client-android
夜雨聆风