易百纳 海思3516 UDP推流 WiFi 安卓端 FFMPEG解码 低延时 手把手写安卓Jni项目 <一>

食用前, 希望大家有一些安卓开发的相关知识, 最起码看到Java不会害怕, 另外CPP越熟悉越好, 这样才能理解起来更轻松.

整个结构就是:
GC2503摄像头->开发板->H.264编码->UDP->WiFi->安卓手机->FFMPEG解码->渲染到屏幕

疑问:

  1. 为啥用UDP
    因为在WiFi内, 即内网, IP都是已知的, 所以用UDP,
  2. 为啥用ffmpeg软解码, 安卓自带的MediaCodec硬解码不香么?
    使用FFMPEG是我发现安卓自带的MediaCodec解码速度竟然不如软解, 神奇不?
  3. 最终目标是啥?
    快!!! 低延时.

首先, 编码这边, 就是使用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++的安卓项目, 目录展开如下:
易百纳 海思3516 UDP推流 WiFi 安卓端 FFMPEG解码 低延时 手把手写安卓Jni项目 <一>_第1张图片

  1. 修改你的build.gradle, 注意,是下面那个全局的:
    易百纳 海思3516 UDP推流 WiFi 安卓端 FFMPEG解码 低延时 手把手写安卓Jni项目 <一>_第2张图片
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

  1. 然后修改CMakeLists.txt
    这个文件是你建Native C++的时候自动生成的, 改成下面的样子:
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是否一致, 不一致就会编译的时候提示找不到库之类奇怪的报错.

  1. 接着复制粘贴编译好的ffmpeg的动态库, 这个库是用ndk的交叉编译链编译的, 你也可以可以尝试自己下载源文件编译.
    对了, 这个项目的github:->>>>>
    易百纳 海思3516 UDP推流 WiFi 安卓端 FFMPEG解码 低延时 手把手写安卓Jni项目 <一>_第3张图片
    这里有个子目录是armeabi-v7a是目前主流的手机平台, 还有一些更先进的手机是v8的, 但是可以向下兼容.
    rtmp虽然我们这个项目用不到, 但是不一起导入, 会报错.

  2. 接着就是看看布局文件:


<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, 用于渲染画面, 一个按键, 没用, 完全没用…

  1. MainActivity
    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卡需要申请权限, 所以这里保留了.

  1. Java层的Player, 主要用于与Jni层的沟通, 所以这里会载入下层的库来调用
    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层.

你可能感兴趣的:(ffmpeg,udp,android)