RTMP 直播推流 Demo(一)—— 项目配置与视频预览

音视频编解码系列目录:

Android 音视频基础知识
Android 音视频播放器 Demo(一)—— 视频解码与渲染
Android 音视频播放器 Demo(二)—— 音频解码与音视频同步
RTMP 直播推流 Demo(一)—— 项目配置与视频预览
RTMP 直播推流 Demo(二)—— 音频推流与视频推流

前面的视频播放器 Demo 是在拉流端进行音视频解码,接下来介绍的 RTMP 直播推流的 Demo 是推流端进行音视频编码。Android 设备作为推流端将摄像头拍摄的图像上传至服务器,在 PC 端通过 FFmpeg 提供的 ffplay 工具或者 EVPlayer 拉流播放视频。

1、项目结构

首先来看直播架构示意图:

2024-1-4.直播推流示意图

主要有三个角色:

  1. 推流端:安卓设备,使用摄像头采集图像,麦克风采集声音,通过 RTMP 协议将音视频流传输到服务器上
  2. 服务器:一般是 NGINX 服务器,需要进行 RTMP 的相关配置以接收推流端的数据
  3. 拉流端:可以是移动设备也可以是 PC,能播放 RTMP 流即可。后续演示时会在 PC 端通过 FFmpeg 提供的 ffplay 工具拉流

除了上述三个重要角色,还会有房间服务模块,服务器的管理与 Web 播放就是通过 HTTP 协议了:

2024-1-4.直播推流服务器搭建

2、开源库的使用与项目配置

在推流过程中,我们会使用几个开源库:

  1. 服务器端:NGINX 服务器需要下载 NGINX 源码,在 Linux 环境编译并启动。此外,还需要支持 RTMP 通信的 RTMP 模块编译进 NGINX 中
  2. Android 推流端:需要三个开源库:
    • 视频编码需要 x264
    • 音频编码需要 faac
    • RTMP 通信需要 RTMPDump

我们首先来看服务器如何配置。

编译环境:Alibaba Cloud Linux 3,NDK 17,NGINX 1.18,RTMP Module 1.2.1,RTMPDump 2.3,FFmpeg 4.2.2。

2.1 配置 NGINX 服务器

下载源码

需要下载 NGINX 源码以及 RTMP 模块源码。先下载 NGINX 源码并解压:

wget https://nginx.org/download/nginx-1.18.0.tar.gz
tar -xvf nginx-1.18.0.tar.gz

然后下载 NGINX RTMP 模块并解压得到 nginx-rtmp-module-1.2.1 目录:

wget https://codeload.github.com/arut/nginx-rtmp-module/tar.gz/v1.2.1
tar xvf v1.2.1

编译 NGINX 源码

进入 NGINX 根目录,运行脚本进行编译:

./configure --prefix=./output --add-module=../nginx-rtmp-module-1.2.1

参数说明:

  • –prefix 指定编译产物的输出目录,./output 表示在当前目录的 output 文件夹下,如果该目录不存在会自动创建
  • –add-module 指定添加一个模块,这里我们添加的是 rtmp-module,在上级目录的 nginx-rtmp-module-1.2.1 文件夹下

由于 NGINX 依赖 gcc、PCRE、OpenSSL、zlib 这些库,缺少其一编译就会报错,比如缺少 PCRE:

checking for PCRE library ... not found
checking for PCRE library in /usr/local/ ... not found
checking for PCRE library in /usr/include/pcre/ ... not found
checking for PCRE library in /usr/pkg/ ... not found
checking for PCRE library in /opt/local/ ... not found./configure: error: the HTTP rewrite module requires the PCRE library.
You can either disable the module by using --without-http_rewrite_module
option, or install the PCRE library into the system, or build the PCRE library
statically from the source with nginx by using --with-pcre=<path> option.

此时需要安装 PCRE:

yum install -y pcre pcre-devel

依赖库的具体安装方法可以参考以下文章:

  • Centos:centos7安装Nginx
  • Ubuntu:ubuntu下安装nginx时依赖库zlib,pcre,openssl安装方法

编译成功之后,会有类似的输出:

2024-1-5.NGINX编译成功

当然目前并不会在 nginx-1.18.0 目录下生成 output 目录以及可执行文件,需要在执行完安装命令之后才能看见该文件夹。

安装 NGINX

接着安装 NGINX:

make && make install

报错:

cc1: all warnings being treated as errors
make[1]: *** [objs/Makefile:1339: objs/addon/nginx-rtmp-module-1.2.1/ngx_rtmp_eval.o] Error 1
make[1]: Leaving directory '/root/AndroidNDK/nginx-1.18.0'
make: *** [Makefile:8: build] Error 2

原因是将警告当成了错误处理,需要修改 /nginx-1.18.0/objs/Makefile 的编译参数:

# 去掉下面的 -Werror 选项
CFLAGS =  -pipe  -O -W -Wall -Wpointer-arith -Wno-unused-parameter -Werror -g

再次执行安装命令可以成功安装。

配置 NGINX 服务器

成功安装 NGINX 服务器后需要对其进行配置,修改 /nginx-1.18.0/output/conf/nginx.conf 文件:

user  root; # 指定 root 权限,否则可能会因权限不足而启动失败
worker_processes  1; # 工作在哪个进程#error_log  logs/error.log; # 错误日志
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;#pid        logs/nginx.pid;events {worker_connections  1024; # 支持最大的直播人数
}# 对 RTMP 协议的配置
rtmp {server {listen 1935; # 1935 端口application myapp {live on; # 打开直播drop_idle_publisher 5s; # 闲置 5s 后断开连接}}
}# 对 HTTP 协议的配置
http {server {listen  8081;location /stat {rtmp_stat all;rtmp_stat_stylesheet stat.xsl;}location /stat.xsl {root /root/AndroidNDK/nginx-rtmp-module-1.2.1/;}location control {rtmp_control all;}location /rtmp_publisher {root /root/AndroidNDK/nginx-rtmp-module-1.2.1/test;}location / {root /root/AndroidNDK/nginx-rtmp-module-1.2.1/test/www;}}
}

nginx.conf 是使用 NGINX 自定义的语法 Nginx Configuration Language 编写的,并不属于任何传统的编程语言。

配置时需要注意几点:

  • location 标签内 root 后面配置的路径要换成你实际的路径,比如你的 nginx-rtmp-module-1.2.1 文件夹的绝对路径是 /root/AndroidNDK/nginx-rtmp-module-1.2.1/,那么你配置的 root 后面就要跟这个路径,而不是我给出的 /root/nginx-rtmp-module-1.2.1/

  • 如果你因为配置错误而修改了 nginx.conf 文件,并且 NGINX 服务器已经启动了,那么你需要先停掉 NGINX 服务器再重新启动它才可使修改生效:

    [root@frank nginx-1.18.0]# ./output/sbin/nginx -s stop
    [root@frank nginx-1.18.0]# ./output/sbin/nginx
    

启动 NGINX 服务器

在 NGINX 根目录 nginx-1.18.0 下执行可执行文件 nginx 启动服务器:

如果显示 8081 端口被占用了,可以 kill 掉占用 8081 端口的进程:

# 通过该命令查询到占用 8081 端口的进程号为 28764
netstat -tunlp|grep 8081
# kill 掉 28764 号进程解除 8081 端口的占用
kill -9 28764

这时候去访问 NGINX 服务器地址。如果你使用的是云服务器,那么就访问服务器的公网 IP + 端口号。例如我的 Linux 服务器公网 IP 为 118.24.126.13,那么你就去访问 118.24.126.13:8081;如果你是在本地 Linux 虚拟机上搭建的服务器,那么就访问本地服务器地址,如 192.168.31.39:8081。成功访问的页面如下:

2024-1-5.NGINX成功访问

由于环境不同,配置复杂可能还会有各种各样的问题,这里我再列举一些问题和解决方法:

  • 主机能 ping 通虚拟机,但是虚拟机 ping 不到主机:参考Ubuntu虚拟机无法ping通windows,反之可以的解决办法

  • 如果使用的云服务器,还需要配置服务器的安全组,把 1935 和 8081 端口打开:
    2024-2-26.阿里云配置开放端口

  • 假如在配置脚本时忘记在第一行指定 user root,访问后台页面时可能会显示 nginx 403 forbid。查看 nginx-1.18.0/output/logs/error.log 发现是权限问题:

    [error] 3848#0: *1 open() "/root/nginx-rtmp-module-1.2.1/stat.xsl" failed (13: Permission denied), client: x.x.x.x, server: , request: "GET /stat.xsl HTTP/1.1", host: "118.24.126.13:8081", referrer: "http://118.24.126.13:8081/stat"
    [error] 3848#0: *1 open() "/root/nginx-rtmp-module-1.2.1/test/www/favicon.ico" failed (13: Permission denied), client: x.x.x.x, server: , request: "GET /favicon.ico HTTP/1.1", host: "118.24.126.13:8081", referrer: "http://118.24.126.13:8081/stat"
    

    通过命令查看哪些用户运行了 NGINX:

    ps -ef | grep nginx
    ps aux | grep "nginx: worker process" | awk '{print $1}'
    

    以上两个命令运行其一即可,得到的结果是 root 和 nobody:

    root      3896     1  0 15:56 ?        00:00:00 nginx: master process ./bin/sbin/nginx
    nobody      3898  3896  0 15:56 ?        00:00:00 nginx: worker process
    root      4068  4036  0 17:55 pts/2    00:00:00 grep --color=auto nginx
    

    由于所有命令都是在 root 用户下进行的,因此需要在脚本中指定 user 为 root

2.2 RTMPDump 编译与配置

RTMP 是一个协议,而 RTMPDump 是处理 RTMP 协议数据的开源库:

  • RTMP(Real Time Messaging Protocol),实时消息传输协议,是基于 TCP 的应用层协议
  • RTMPDump 是用 C 语言开发的处理 RTMP 流媒体的开源工具包。它能够单独使用进行 RTMP 的通信, 也可以集成到 FFmpeg 中通过 FFmpeg 接口来使用 RTMPDump。它封装了 Socket 建立 TCP 通信,实现了 RTMP 数据的收发。借助 RTMPDump 可以通过调用 C 的 API 的方式实现推流与拉流,而无需考虑 RTMP 底层细节(类似于 OkHttp 库与 HTTP 协议的关系)

由于 RTMPDump 的源码并不多,并且我们会对其源码稍加修改,因此就不在 Linux 服务器编译出它的库之后再放入 AS 中使用,而是直接放入 AS 中编译。

首先,在 RTMPDump 的官网找到下载页面,下载最新的 2.3 版本 rtmpdump-2.3.tgz,解压后会看到一个 librtmp 目录。先查看该目录下的 Makefile,了解如何编译。关键信息如下:

OBJS=rtmp.o log.o amf.o hashswf.o parseurl.olibrtmp.a: $(OBJS)log.o: log.c log.h Makefile
rtmp.o: rtmp.c rtmp.h rtmp_sys.h handshake.h dh.h log.h amf.h Makefile
amf.o: amf.c amf.h bytes.h log.h Makefile
hashswf.o: hashswf.c http.h rtmp.h rtmp_sys.h Makefile
parseurl.o: parseurl.c rtmp.h rtmp_sys.h log.h Makefile

要编译出 librtmp.a 这个静态库,需要 OBJS 变量定义的几个目标文件,而编译目标文件所需的源文件也在后续给出了。因此,我们将 librtmp 目录下的这些文件,拷贝到 AS 项目的 /src/main/cpp/librtmp 下,并新建 CMakeLists.txt 用来编译静态库:

cmake_minimum_required(VERSION 3.22.1)# 将源文件定义为 rtmp_src 变量
file(GLOB rtmp_src *.c)
# 用 C 不是 C++ 了,因为 RTMP 是用 C 写的
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DNO_CRYPTO")
# 声明如下源文件编译出来的库文件名称为 librtmp.a
add_library(rtmp STATIC ${rtmp_src})

我们注意到在 set 命令中通过 -D 参数声明了一个宏 NO_CRYPTO,如果不添加该参数编译会报错:

[1/1] Re-running CMake...
-- Configuring done
-- Generating done
-- Build files have been written to: F:/Code/Android/VideoLive/app/.externalNativeBuild/cmake/debug/x86_64
[1/8] Building C object src/main/cpp/librtmp/CMakeFiles/rtmp.dir/log.c.o
[2/8] Building C object src/main/cpp/librtmp/CMakeFiles/rtmp.dir/hashswf.c.o
[3/8] Building C object src/main/cpp/librtmp/CMakeFiles/rtmp.dir/rtmp.c.o
[4/8] Building C object src/main/cpp/librtmp/CMakeFiles/rtmp.dir/amf.c.o
[5/8] Building C object src/main/cpp/librtmp/CMakeFiles/rtmp.dir/parseurl.c.o
...
src/main/cpp/librtmp/CMakeFiles/rtmp.dir/hashswf.c.o   -c F:\Code\Android\VideoLive\app\src\main\cpp\librtmp\hashswf.c
F:\Code\Android\VideoLive\app\src\main\cpp\librtmp\hashswf.c:56:10: fatal error: 'openssl/ssl.h' file not found#include <openssl/ssl.h>^~~~~~~~~~~~~~~1 error generated.

意思是在编译 hashswf.c 文件时,找不到 openssl/ssl.h 文件。实际上是因为我们没有引入 openssl 工具包。openssl 是用来进行数据加密的,加密意味着耗时,由于视频直播对时效性要求高,因此我们暂时不考虑引入 openssl。那如何规避掉编译错误呢?

我们先来看报错的 hashswf.c:

#ifdef CRYPTO...
#include <openssl/ssl.h>
#include <openssl/sha.h>
#include <openssl/hmac.h>
#include <openssl/rc4.h>...
#endif

它只有在定义了 CRYPTO 这个宏的情况下才会导入 openssl,而 CRYPTO 是在 rtmp.h 中定义的:

#if !defined(NO_CRYPTO) && !defined(CRYPTO)
#define CRYPTO
#endif

就是没有定义 NO_CRYPTO 和 CRYPTO 这两个宏时,才会定义 CRYPTO。所以这里才会通过定义 NO_CRYPTO 宏的方式来规避 openssl 的导入。

最后配置 app 模块下的 CMakeLists,将上面的 CMakeLists 嵌套进来:

cmake_minimum_required(VERSION 3.22.1)project("pusher")# 添加 librtmp 目录进来
add_subdirectory(librtmp)# 包含 librtmp 目录,这样导入其文件时就可以不再用""而是用<>
# 使用<>可以避免要导入的文件路径过深而需要写出一长串路径,直接写最终文件名即可
include_directories(librtmp)add_library( pusherSHAREDnative-lib.cpp)find_library( log-liblog)target_link_libraries( pusherrtmp # 添加 RTMP 静态库${log-lib})

2.3 x264 编译与配置

x264 是一个开源的实现了 H.264 协议的视频编码库,提供了 H.264 编码器。它是通过将视频源压缩为 H.264 格式的比特流来实现视频压缩。x264 使用一系列复杂的算法和技术,如运动估计、变换编码、熵编码等,以高效地压缩视频,并提供高质量的图像和视频编码。总的来讲,H.264 是一种视频压缩标准,而 x264 是 H.264 的一个开源实现。

在 VideoLAN 可以下载 x264 的源码,也可以使用 git:

# git clone https://code.videolan.org/videolan/x264.git

接下来使用 NDK 交叉编译 x264 源码,脚本如下:

#!/bin/bash# NDK 根目录
NDK_ROOT=/root/Android/android-ndk-r17c# 编译产物的输出目录
PREFIX=./android/armeabi-v7a# 交叉编译工具所在目录
TOOLCHAIN=$NDK_ROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64# 编译参数,可以参考 AS 中的 build.ninja 的参数
FLAGS="-isysroot $NDK_ROOT/sysroot -isystem $NDK_ROOT/sysroot/usr/include/arm-linux-androideabi -D__ANDROID_API__=17 -g -DANDROID -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16 -mthumb -Wa,--noexecstack -Wformat -Werror=format-security  -O0 -fPIC"# 执行脚本的命令,--disable-cli 表示关闭命令行
./configure \
--prefix=$PREFIX \
--disable-cli \
--enable-static \
--enable-pic \
--host=arm-linux \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--sysroot=$NDK_ROOT/platforms/android-17/arch-arm \
--extra-cflags="$FLAGS"make clean
make install

在指定的编译产物目录 /android/armeabi-v7a 下会生成两个目录 include 和 lib,分别包含头文件和静态库文件,直接将include 目录拷贝到项目的 src/main/cpp/libx264 下,将 lib 内的静态库文件 libx264.a 拷贝到 src/main/cpp/libx264/libs/armeabi-v7a 下。然后在顶级的 CMakeLIsts.txt 中添加相关配置:

# 添加头文件
include_directories(src/main/cpp/include)# 添加编译库文件,实际上 CMAKE_CXX_FLAGS 这个编译参数会被传到 build.ninja 的 FLAGS 中
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/cpp/libs/${ANDROID_ABI}")target_link_libraries( native-librtmp${log-lib}x264 # 链接到目标库
)

最后在 build.gradle 中配置 CPU 架构过滤参数:

android {defaultConfig {externalNativeBuild {cmake {// 添加这句,这样 CMake 只会编译 armeabi-v7a 架构的库,而不编译 x86 和其他的库// CPU 是哪个架构就只配置那个架构,这样可以避免 APK 打入不使用的库而增大体积abiFilters "armeabi-v7a"}}ndk {// 控制 ndk 只编译 armeabi-v7a 的库,这个也必须配置,否则// 在 System.loadLibrary() 时会因为找不到库而崩溃abiFilters "armeabi-v7a"}}
}

2.4 faac 编译与配置

faac 的 GitHub 主页上可以下载当下最新的 1.30 版本,如果想使用过往版本,可以在 SourceForge 的 faac 主页下载想要的版本。比如下载 1.29 版本:

[root@frank ~]# wget https://zenlayer.dl.sourceforge.net/project/faac/faac-src/faac-1.29/faac-1.29.9.2.tar.gz

解压后编写脚本:

#!/bin/bash
PREFIX=`pwd`/android/armeabi-v7a
NDK_ROOT=/root/AndroidNDK/android-ndk-r17c
TOOLCHAIN=$NDK_ROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64
CROSS_COMPILE=$TOOLCHAIN/bin/arm-linux-androideabiFLAGS="-isysroot $NDK_ROOT/sysroot -isystem $NDK_ROOT/sysroot/usr/include/arm-linux-androideabi -D__ANDROID_API__=17 -g -DANDROID -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16 -mthumb -Wa,--noexecstack -Wformat -Werror=format-security -std=c++11  -O0  -fPIC"export CC="$CROSS_COMPILE-gcc --sysroot=$NDK_ROOT/platforms/android-17/arch-arm"
export CFLAGS="$FLAGS"./configure \
--prefix=$PREFIX \
--host=arm-linux \
--with-pic \
--enable-shared=nomake clean
make install

将编译产物中 include 目录下的两个头文件以及 lib 目录下的 libfaac.a 静态库拷贝到 AS 中并配置 CMakeList:

# 添加 faac 头文件
include_directories(libfaac/include)# 添加 faac 静态库文件路径
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/libfaac/libs/${CMAKE_ANDROID_ARCH_ABI}")target_link_libraries(pusherrtmp # 添加 RTMP 静态库x264 # 链接 x 264faac # 链接 faac${log-lib})

至此,所有第三方库导入完毕,准备工作完成。

3、实现思路

整体思路如下:

2024-2-27.直播推流端结构图

摄像头采集视频数据进行视频编码封装进 RTMP 包中,最后通过 RTMPDump 的 RTMP_SendPacket() 将视频包发送给服务器,音频也是类似的过程。

从代码分层的角度看上图,信息采集是在上层完成的,编码与推流是在 Native 层完成的:

在这里插入图片描述

按照从上到下的顺序:

  • Activity 通过 LivePusher 控制 VideoChannel 采集视频、AudioChannel 采集音频
  • Channel 采集到每一帧数据后,都调用 LivePusher 的 Native 方法将数据交给 Native 层
  • Native 层的入口 native-lib 将视频帧交给 VideoChannel 进行视频编码,将音频帧交给 AudioChannel 进行音频编码,编码后的数据转换成 RTMPPacket 存入 RTMPPacket 队列中
  • native-lib 负责连接 RTMP 服务器,并从 RTMPPacket 队列中取出 RTMPPacket 发送给 RTMP 服务器完成推流

上层的结构图如下:

2024-1-5.推流思路图

各部分职责:

  • LivePusher 作为推流功能的入口,控制负责视频的 VideoChannel 和负责音频的 AudioChannel,同时还会定义 Native 方法作为与 Native 层交互的入口
  • VideoChannel 控制 CameraHelper 驱动摄像头采集视频图像,将采集到的图像显示在预览界面的同时,还要经由 LivePusher 传递给 Native 层进行编码
  • AudioChannel 使用 AudioRecord 读取麦克风的录音数据,也是经由 LivePusher 调用 Native 方法传给 Native 层编码发送

4、视频预览

采集视频数据传给底层进行编码之前,需要先实现视频预览,效果如下:

在这里插入图片描述

Android 系统提供了 Camera、Camera2 以及封装了 Camera2 的 Jetpack CameraX 来操控摄像头,我们以 Camera 为例,来看 CameraHelper 的实现。

4.1 初始化

初始化代码如下:

class CameraHelper(private var mActivity: Activity,private var mCameraId: Int,private var mHeight: Int,private var mWidth: Int
) : SurfaceHolder.Callback {private lateinit var mSurfaceHolder: SurfaceHolder/*** 我们需要监听 Surface 的变化,比如当 Surface 销毁时停止 Camera* 的预览,当 Surface 大小发生变化时,重启 Camera 的预览*/fun setPreviewDisplay(surfaceHolder: SurfaceHolder) {mSurfaceHolder = surfaceHolder// 添加监听 Surface 变化的回调mSurfaceHolder.addCallback(this)}// SurfaceHolder.Callback startoverride fun surfaceCreated(holder: SurfaceHolder) {// 在 SurfaceView 创建成功后开启预览才有意义,但是因为还有切换前后摄像头// 的操作,切换不会回调本方法,因此将开启预览的逻辑都放到 surfaceChanged() 中// startPreview()}override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {// 除了 SurfaceView 的创建,还会有切换前后摄像头的操作,surfaceChanged()// 在两种情况下都会被回调,因此在这个回调方法中开启/关闭预览stopPreview()startPreview()}override fun surfaceDestroyed(holder: SurfaceHolder) {stopPreview()}// SurfaceHolder.Callback end
}

简单解释一下各项参数:

  • 需要通过 Activity 获取到手机旋转的方向,以便对摄像头采集到的数据做出相应的旋转
  • CameraId 用来指明当前使用前置还是后置摄像头
  • 宽高是用户希望使用的摄像参数,该参数会传给 Camera,但是由于不同厂商的摄像头具有不同的参数规格,因此 Camera 最终使用的宽高参数很可能与传入的不同,只是接近而已
  • 我们使用 SurfaceView 展现预览画面,那么就需要获取 SurfaceHolder,一方面是监听 SurfaceView 尺寸的变化,当发生变化时,需要重新开启预览;另一方面,Camera 提供了 setPreviewDisplay() 可以传入 SurfaceHolder 直接将拍摄到的画面显示在对应的 SurfaceView 上

4.2 开启预览与结束预览

主要操作包括:

  • 根据传入的 CameraId,即前置还是后置摄像头,打开该摄像头获取到 Camera 对象
  • 设置 Camera 参数,包括预览格式、宽高、旋转角度等
  • 设置使用缓冲区进行预览回调,并指定该缓冲区
  • 设置在 SurfaceHolder 持有的 SurfaceView 上进行预览,并开启预览
	// 开启预览fun startPreview() {// 1.打开 CameramCamera = Camera.open(mCameraId)if (mCamera == null) {Log.d(TAG, "Open camera failed.")return}// 2.设置 Camera 参数val cameraParam = mCamera?.parameters// 2.1 设置预览格式为 NV21cameraParam?.previewFormat = ImageFormat.NV21// 2.2 设置预览界面的宽高setPreviewSize(cameraParam)// 2.3 设置预览画面需要旋转的角度和方向setPreviewOrientation(cameraParam)// 2.4 更新 Camera 参数mCamera?.parameters = cameraParam// 3.Camera 数据设置// 3.1 Camera 采集的是 NV21 格式的数据,其占用空间为总像素的 3/2,// mBuffer 用于保存预览数据,mBytes 用于保存推流到服务器上的数据mBuffer = ByteArray(mWidth * mHeight * 3 / 2)mBytes = ByteArray(mBuffer.size)// 3.2 设置预览回调缓冲区,将 Camera 采集的数据存入 mBuffermCamera?.setPreviewCallbackWithBuffer(this)mCamera?.addCallbackBuffer(mBuffer)// 4.开启预览mCamera?.setPreviewDisplay(mSurfaceHolder)mCamera?.startPreview()}// 结束预览private fun stopPreview() {// 设置预览回调为空并停止预览mCamera?.setPreviewCallback(null)mCamera?.stopPreview()// 释放 mCamera 并置为空mCamera?.release()mCamera = null}

该方法内有一些需要解释的内容,在下面几个小节中讲解。

设置预览界面宽高

手机摄像头的宽高参数是有很多规格的,不同的厂商之间规格也都不同。当然,选择不同的宽高参数时,看到的预览画面的尺寸也不同:

严格来说,我们需要通过 setPreviewSize() 设置摄像头的拍摄所使用的参数,并且随之改变预览画面。但是当前我们仅实现设置摄像头参数,预览画面的 SurfaceView 的大小暂时先不动(感兴趣可自行实现)。

在设置摄像头宽高时,由于摄像头可能不支持与传入的宽高一模一样的规格,因此我们要先获取摄像头支持的拍摄规格,再选择与要求的宽高最相近的规格:

	/*** 从摄像头支持的宽高参数中选取与预览界面宽高差值最小的参数,并将其作为预览界面宽高*/private fun setPreviewSize(cameraParam: Camera.Parameters?) {if (cameraParam == null) {return}// 获取摄像头支持的宽高参数val supportedPreviewSizes = cameraParam.supportedPreviewSizesvar selectedSize = supportedPreviewSizes[0]val iterator = supportedPreviewSizes.iterator()var tempValue: Intvar minValue = Integer.MAX_VALUEvar tempSize: Camera.Size// 遍历找到与 mWidth 和 mHeight 最接近的规格while (iterator.hasNext()) {tempSize = iterator.next()tempValue = abs(tempSize.width * tempSize.height - mWidth * mHeight)if (tempValue < minValue) {minValue = tempValueselectedSize = tempSize}}// 将选定的宽高保存到成员变量和 cameraParam 中mWidth = selectedSize.widthmHeight = selectedSize.heightcameraParam.setPreviewSize(mWidth, mHeight)}

设置预览画面的旋转角度

为什么要对预览界面的数据进行旋转?因为 Android 设备的摄像头是横向摆放的:

2024-1-9.Android摄像头横放示意图

如你所见,摄像头是相对于设备顺时针旋转了 90° 放置的,它输出的图像需要顺时针旋转 90° 才与手机摆放的方向相同。所以当手机竖直正向摆放时,你需要将摄像头采集到的像素矩阵顺时针旋转 90° 才能得到正常的视频。参考代码如下:

	// SurfaceView 的宽高发生变化时,需要通知 Native 层重新初始化编码器的interface OnSurfaceSizeChangedListener {fun onSizeChanged(width: Int, height: Int)}private var mOrientation = 0private var mOnSurfaceSizeChangedListener: OnSurfaceSizeChangedListener? = null/*** 根据当前手机的旋转角度调整预览界面的旋转角度,保证预览画面跟随手机的旋转* 而旋转,主要参考 Camera#setDisplayOrientation 注释给出的参考代码*/private fun setPreviewOrientation(cameraParam: Camera.Parameters?) {mOrientation = mActivity.windowManager.defaultDisplay.orientationval degree = when (mOrientation) {Surface.ROTATION_0 -> {mOnSurfaceSizeChangedListener?.onSizeChanged(mHeight, mWidth)0}// 横屏,左边是头部,home 键在右边Surface.ROTATION_90 -> {mOnSurfaceSizeChangedListener?.onSizeChanged(mWidth, mHeight)90}Surface.ROTATION_180 -> {mOnSurfaceSizeChangedListener?.onSizeChanged(mHeight, mWidth)180}// 横屏,头部在右边,home 在左边Surface.ROTATION_270 -> {mOnSurfaceSizeChangedListener?.onSizeChanged(mWidth, mHeight)270}else -> 0}// 获取 CameraInfo 以便后续从中获取前后置摄像头val cameraInfo = Camera.CameraInfo()Camera.getCameraInfo(mCameraId, cameraInfo)// 根据 degree 计算预览界面需要旋转的角度var result: Intif (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) {// 前置摄像头,需要做镜像转换result = (cameraInfo.orientation + degree) % 360result = (360 - result) % 360} else {// 后置摄像头result = (cameraInfo.orientation - degree + 360) % 360}mCamera?.setDisplayOrientation(result)}

当然,以上仅是对预览画面进行了旋转,要传递给 Native 进行编码的数据 mBytes 还没有做旋转处理,我们下一节再说。

mBuffer 与 mBytes

为什么 mBuffer 的大小是 mWidth * mHeight * 3 / 2,这与 YUV 的编码方式有关。先看下面这幅图:

3.2.5.1-RGB与YUV内存对比

YUV 编码中,每个像素点都有一个 Y 分量,UV 分量则是 4 个像素点共用一个,也就是说,在一个 Width * Height 的像素矩阵中,Y 分量的个数就是 Width * Height,而 UV 分量分别为 Width * Height / 4,那么 YUV 分量总计就是 Width * Height * 3 / 2

再来解释 mBuffer 是如何接收到数据的。注意 setPreviewDisplay() 内的这段代码:

	fun setPreviewDisplay(surfaceHolder: SurfaceHolder) {...// 3.2 设置预览回调缓冲区,将 Camera 采集的数据存入 mBuffermCamera?.addCallbackBuffer(mBuffer)mCamera?.setPreviewCallbackWithBuffer(this)...}

首先,addCallbackBuffer() 会将 mBuffer 添加到一个预览回调缓冲队列中,当视频帧到来时,如果队列中有这个 mBuffer,就会把视频帧的数据保存到 mBuffer 中并将其从队列中移除。

其次,CameraHelper 设置了一个预览回调,当摄像头采集到一帧画面时,就通过 Camera.PreviewCallback 接口的 onPreviewFrame() 把数据传给我们:

	interface OnPreviewListener {fun onPreviewFrame(data: ByteArray)}private var mOnPreviewListener: OnPreviewListener? = nulloverride fun onPreviewFrame(data: ByteArray?, camera: Camera?) {if (data == null) {Log.d(TAG, "onPreviewFrame: data 为空,直接返回")return}// 将传给服务器的图像数据旋转 90° 放入 mBytes 中if (mOrientation == Surface.ROTATION_0) {rotate90(data)}// 将页面数据回调给 VideoChannel,再传给 LivePusher 的 native 方法mOnPreviewListener?.onPreviewFrame(mBytes)// 再次将 mBuffer 添加到预览回调缓冲队列中,当有回调数据后就会填入 mBuffermCamera?.addCallbackBuffer(mBuffer)}

在这里,将摄像头采集到的每一帧视频旋转 90° 赋值给 mBytes,再回调给 VideoChannel 传给 Native 层编码发送,至于原因前面已经提过了:

	/*** 对摄像头采集到的数据旋转 90° 后才是调正的图像,* 后置摄像头数据需要顺时针旋转 90°,而前置需要逆时针旋转 90°*/private fun rotate90(data: ByteArray) {var index = 0;val ySize = mWidth * mHeightval uvHeight = mHeight / 2if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) {// 后置,先旋转 y,再旋转 uv,旋转后的数据存入 mBytes 中for (i in 0 until mWidth) {for (j in mHeight - 1 downTo 0) {mBytes[index++] = data[j * mWidth + i]}}// 拷贝 uv,还是 NV21 格式for (i in 0 until mWidth step 2) {for (j in uvHeight - 1 downTo 0) {// vmBytes[index++] = data[ySize + j * mWidth + i]// umBytes[index++] = data[ySize + j * mWidth + i + 1]}}} else {// 前置for (i in 0 until mWidth) {var nPos = mWidth - 1for (j in 0 until mHeight) {mBytes[index++] = data[nPos - i]nPos += mWidth}}// u vfor (i in 0 until mWidth step 2) {var pos = ySize + mWidth - 1for (j in 0 until uvHeight) {mBytes[index++] = data[pos - i - 1]mBytes[index++] = data[pos - i]pos += mWidth}}}}

4.3 前后置摄像头切换

切换 CameraId 再重启预览:

	fun switchCamera() {// 切换摄像头 ID 再重启预览mCameraId = if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) {Camera.CameraInfo.CAMERA_FACING_FRONT} else {Camera.CameraInfo.CAMERA_FACING_BACK}stopPreview()startPreview()}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/661154.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

es环境安装及php对接使用

Elasticsearch Elasticsearch 是一个分布式、高扩展、高实时的搜索与数据分析引擎。它提供了一个分布式多用户能力的全文搜索引擎&#xff0c;基于RESTful web接口。Elasticsearch是用Java语言开发的&#xff0c;并作为Apache许可条款下的开放源码发布&#xff0c;是一种流行的…

Notes for the missing semester. Useful and basic knowledge about Linux.

The Shell Contents The first course is to introduce some simple commands. I’ll list some commands that I’m not familiar with: # --silent means dont give log info, # --head means we only want the http head. curl --head --silent bing.com.cn# cut --deli…

上位机开发PyQt(五)【Qt Designer】

PyQt5提供了一个可视化图形工具Qt Designer&#xff0c;文件名为designer.exe。如果在电脑上找不到&#xff0c;可以用如下命令进行安装&#xff1a; pip install PyQt5-tools 安装完毕后&#xff0c;可在如下目录找到此工具软件&#xff1a; %LOCALAPPDATA%\Programs\Python\…

nginx下载安装配置(含ssl)

下载安装环节 wget https://nginx.org/download/nginx-1.24.0.tar.gz tar -zxvf xxx.tar.gz yum -y install pcre-devel openssl openssl-devel ./configure --prefix/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module --with-stream make & make i…

【T1】T1飞跃专业版,点击财务报表没有任何反应。

【问题描述】 在T1飞跃专业版软件中&#xff0c; 点击标准财务–财务报表–资产负债表&#xff0c;没有任何反应。 【解决方法】 退出客户端后&#xff0c;打开客户端所在目录&#xff0c; 找到【UjRegSys.bat】文件&#xff0c;双击运行&#xff08;管理员身份&#xff09;后…

【HMGD】使用CubeMx配置GD32F303系列单片机进行DMA ADC

原理图查看 查原理图可以看到GD32F103C8T6的官方开发板GD32303C-START-V1.0的PA1没有接任何东西 使用PA1作为ADC端口 CubeMX配置ADC和时钟 配置ADC通道 启用循环模式 配置此通道ADC分频 配置ADC DMA为循环模式 配置时钟 可根据手册配置最大HZ GD32F303最高频率设定 生成…

Oracle系统参数调整【数据库实例优化系列一】

Oracle实例是:内存组件和相关的后台进程组成。这些内存组件提高了数据库的运行,而后台进程负责管理系统和内存组件。 一、SGA和实例优化 Oracle的SGA是指的系统全局区。sga是数据库运行期间使用的一段公有内存,即数据库用户都可以访问这段内存,包括: 共享池、重做日志缓冲…

第2节:UIOTOS前端零代码应用 蓝图连线 信号值变化小示例02

目标 通过连线&#xff0c;实现信号值随机变化。 最终效果 实现过程 步骤1&#xff1a;接11节&#xff0c;选中底板设置其背景颜色 步骤2&#xff1a;拖入普通按钮V2组件&#xff0c;设置“text”值为“1”&#xff0c;并做form绑定 步骤3&#xff1a;选中按钮对输入框进行交…

宠物领养|基于SprinBoot+vue的宠物领养管理系统(源码+数据库+文档)

宠物领养目录 基于Spring Boot的宠物领养系统的设计与实现 一、前言 二、系统设计 三、系统功能设计 1前台 1.1 宠物领养 1.2 宠物认领 1.3 教学视频 2后台 2.1宠物领养管理 2.2 宠物领养审核管理 2.3 宠物认领管理 2.4 宠物认领审核管理 2.5 教学视频管理 四、…

『FPGA通信接口』DDR(3)DDR3颗粒读写测试

文章目录 前言1.配套工程简介2.测试内容与策略3. 测试程序分析4.程序结果分析5.一个IP控制两颗DDR36.传送门 前言 以四颗MT41K512M16HA-125AIT颗粒为例&#xff0c;介绍如何在一块新制板卡上做关于DDR3的器件测试。前面两篇介绍了什么是DDR&#xff0c;并介绍了xilinx给出的FPG…

使用RTSP将笔记本摄像头的视频流推到开发板

一、在Windows端安装ffmpeg 1. 下载ffmpeg:下载ffmpeg 解压ffmpeg-master-latest-win64-gpl.zip bin 目录下是 dll 动态库 , 以及 可执行文件 ;将 3 33 个可执行文件拷贝到 " C:\Windows " 目录下 ,将所有的 " .dll " 动态库拷贝到 " C:\Windows\Sy…

在AndroidStudio创建Flutter项目并运行到模拟器

1.Flutter简介 Flutter是Google开源的构建用户界面&#xff08;UI&#xff09;工具包&#xff0c;帮助开发者通过一套代码库高效构建多平台精美应用&#xff0c;支持移动、Web、桌面和嵌入式平台。Flutter 开源、免费&#xff0c;拥有宽松的开源协议&#xff0c;适合商…