食用前, 希望大家有一些安卓开发的相关知识, 最起码看到Java不会害怕, 另外CPP越熟悉越好, 这样才能理解起来更轻松.
整个结构就是:
GC2503摄像头->开发板->H.264编码->UDP->WiFi->安卓手机->FFMPEG解码->渲染到屏幕
疑问:
首先, 编码这边, 就是使用linux的send, 把数据帧, 分包之后发送出去, 这里注意跟TCP的区别, tcp是个两头堵死的管子, 一边写, 一边读, 数据是连续的, 即便基于TCP有分包, 粘包, 但是在读出那一端, 是不分h264帧的, 海思取出编码完成的数据的时候, 其实是一帧帧的, 所以如果你用TCP发送, 到时候读取的时候跟你读h.264的文件一样, 设置一个读取的buffer size, 每次读固定长度, 数据对你而言是没有帧的概念的, 你需要手动去用00 00 00 01的帧间隔数据来区分.
UDP发送的好处就是你可以按帧来发送, 一次发送一帧, 但是由于IP包最大长度在1500个字节, 而你一帧数据, 特别是I帧, 可能有几十K, 所以你要做一下分包, 就是把一个帧拆分成若干包发出去, 接收端同样需要根据帧间隔+数据帧的NAL Unit来区分帧.(如果你还不知道啥是NAL, 额, 请自行google一下, 或者等我回头录一期视频来讲解)
OK, 这篇文章的重点是安卓解码的部分.
首先, 安卓会使用jni的方式访问ffmpeg库, 我顺便就把之前写的UDP数据接收的部分也放进去了, 这样除了渲染的部分, Java/Kotlin就不做啥具体工作了.
用Android Studio新建一个NativC++的安卓项目, 目录展开如下:
plugins {
id 'com.android.application' version '7.2.1' apply false
id 'com.android.library' version '7.2.1' apply false
id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
}
用Derry老师的话说这里有个天坑, 坑了我一晚上, 就是如果你是自己建的项目, 它可能会自动用7.3.1版本, 编译会莫名其妙报错, 所以这里一定要把com.android.application, 跟com.android.library 版本设置为7.2.1
cmake_minimum_required(VERSION 3.18.1)
project("marcffmpegplayer")
# 设置变量方便下面使用
set(FFMPEG ${CMAKE_SOURCE_DIR}/ffmpeg)
set(RTMP ${CMAKE_SOURCE_DIR}/rtmp)
include_directories(${FFMPEG}/include)
# 指明动态库的存放位置
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${FFMPEG}/libs/${CMAKE_ANDROID_ARCH_ABI}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${RTMP}/libs/${CMAKE_ANDROID_ARCH_ABI}")
# 指明源文件的位置
file(GLOB src_files *.cpp)
# 将你的cpp编译成一个动态库供java使用
add_library(
marcffmpegplayer # 这个是动态库的名字, 你要在java中使用这个动态库, 就需要这个名字
SHARED
${src_files})
# 指明需要链接的库, 就是依赖
target_link_libraries( # Specifies the target library.
marcffmpegplayer
# 引入的库不分先后
-Wl,--start-group
avcodec avfilter avformat avutil swresample swscale
-Wl,--end-group
log # 引入log库
z # 引入z库
rtmp # rtmp
android # 引入android库
)
注释得还算清楚了, 那么引入库文件的时候, 注意目录跟CMakeLists是否一致, 不一致就会编译的时候提示找不到库之类奇怪的报错.
接着复制粘贴编译好的ffmpeg的动态库, 这个库是用ndk的交叉编译链编译的, 你也可以可以尝试自己下载源文件编译.
对了, 这个项目的github:->>>>>
这里有个子目录是armeabi-v7a是目前主流的手机平台, 还有一些更先进的手机是v8的, 但是可以向下兼容.
rtmp虽然我们这个项目用不到, 但是不一起导入, 会报错.
接着就是看看布局文件:
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<SurfaceView
android:id="@+id/video_surface_view"
android:layout_width="match_parent"
android:layout_height="200dp"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/playBtn"
android:text="@string/play_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/video_surface_view" />
androidx.constraintlayout.widget.ConstraintLayout>
一个SurfaceView, 用于渲染画面, 一个按键, 没用, 完全没用…
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
mSurfaceView = binding.videoSurfaceView
ktplayer = KTPlayer()
lifecycle.addObserver(ktplayer!!)
ktplayer?.setSurfaceView(mSurfaceView!!)
btn = binding.playBtn;
btn!!.setOnClickListener(this)
checkPermission()
}
就贴部分吧, 其实就实例化了一个Java层的Player的实例, 将其作为本view lifecycle的观察者, 并把显示画面用的surfaceView传进去
另外权限相关的部分是因为项目从文件播放转过来的, 读取sd卡需要申请权限, 所以这里保留了.
companion object {
init {
System.loadLibrary("marcffmpegplayer")
}
}
在View的lifecycle发生变化的时候, 比如启动的时候就告诉下层, 开始接收数据
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
when (event) {
Lifecycle.Event.ON_RESUME -> {
// 这里相当于安卓主线程来调用jni的准备函数, 千万不可以阻塞
nativePlayerObj = startRevNative()
}
Lifecycle.Event.ON_PAUSE -> {
}
Lifecycle.Event.ON_DESTROY -> {
}
else -> {
}
}
}
与下层的沟通, 主要是这两个方法:
private external fun startRevNative(): Long
private external fun setSurfaceNative(nativeObj: Long, surface: Surface)
一个是开启接收, 并获取下层的player实例的指针, 一个是将要渲染的窗口传下去
至此, 属于Java层的工作就没了, 是不是Java几乎啥都没干, 就建了个布局, 建了个SurfaceView, 并把它交给了Jni层.