当前位置: 首页 > news >正文

做网站主页百度营销大学

做网站主页,百度营销大学,网页和移动端界面设计,网站备案添加域名音视频编解码系列目录#xff1a; Android 音视频基础知识 Android 音视频播放器 Demo#xff08;一#xff09;—— 视频解码与渲染 Android 音视频播放器 Demo#xff08;二#xff09;—— 音频解码与音视频同步 RTMP 直播推流 Demo#xff08;一#xff09;—— 项目…音视频编解码系列目录 Android 音视频基础知识 Android 音视频播放器 Demo一—— 视频解码与渲染 Android 音视频播放器 Demo二—— 音频解码与音视频同步 RTMP 直播推流 Demo一—— 项目配置与视频预览 RTMP 直播推流 Demo二—— 音频推流与视频推流 前面的视频播放器 Demo 是在拉流端进行音视频解码接下来介绍的 RTMP 直播推流的 Demo 是推流端进行音视频编码。Android 设备作为推流端将摄像头拍摄的图像上传至服务器在 PC 端通过 FFmpeg 提供的 ffplay 工具或者 EVPlayer 拉流播放视频。 1、项目结构 首先来看直播架构示意图 主要有三个角色 推流端安卓设备使用摄像头采集图像麦克风采集声音通过 RTMP 协议将音视频流传输到服务器上服务器一般是 NGINX 服务器需要进行 RTMP 的相关配置以接收推流端的数据拉流端可以是移动设备也可以是 PC能播放 RTMP 流即可。后续演示时会在 PC 端通过 FFmpeg 提供的 ffplay 工具拉流 除了上述三个重要角色还会有房间服务模块服务器的管理与 Web 播放就是通过 HTTP 协议了 2、开源库的使用与项目配置 在推流过程中我们会使用几个开源库 服务器端NGINX 服务器需要下载 NGINX 源码在 Linux 环境编译并启动。此外还需要支持 RTMP 通信的 RTMP 模块编译进 NGINX 中Android 推流端需要三个开源库 视频编码需要 x264音频编码需要 faacRTMP 通信需要 RTMPDump 我们首先来看服务器如何配置。 编译环境Alibaba Cloud Linux 3NDK 17NGINX 1.18RTMP Module 1.2.1RTMPDump 2.3FFmpeg 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-pcrepath option.此时需要安装 PCRE yum install -y pcre pcre-devel依赖库的具体安装方法可以参考以下文章 Centoscentos7安装NginxUbuntuubuntu下安装nginx时依赖库zlibpcreopenssl安装方法 编译成功之后会有类似的输出 当然目前并不会在 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 服务器再重新启动它才可使修改生效 [rootfrank nginx-1.18.0]# ./output/sbin/nginx -s stop [rootfrank 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。成功访问的页面如下 由于环境不同配置复杂可能还会有各种各样的问题这里我再列举一些问题和解决方法 主机能 ping 通虚拟机但是虚拟机 ping 不到主机参考Ubuntu虚拟机无法ping通windows反之可以的解决办法 如果使用的云服务器还需要配置服务器的安全组把 1935 和 8081 端口打开 假如在配置脚本时忘记在第一行指定 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 --colorauto nginx由于所有命令都是在 root 用户下进行的因此需要在脚本中指定 user 为 root 2.2 RTMPDump 编译与配置 RTMP 是一个协议而 RTMPDump 是处理 RTMP 协议数据的开源库 RTMPReal 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了解如何编译。关键信息如下 OBJSrtmp.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 -marcharmv7-a -mfloat-abisoftfp -mfpuvfpv3-d16 -mthumb -Wa,--noexecstack -Wformat -Werrorformat-security -O0 -fPIC# 执行脚本的命令--disable-cli 表示关闭命令行 ./configure \ --prefix$PREFIX \ --disable-cli \ --enable-static \ --enable-pic \ --hostarm-linux \ --cross-prefix$TOOLCHAIN/bin/arm-linux-androideabi- \ --sysroot$NDK_ROOT/platforms/android-17/arch-arm \ --extra-cflags$FLAGSmake 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 版本 [rootfrank ~]# wget https://zenlayer.dl.sourceforge.net/project/faac/faac-src/faac-1.29/faac-1.29.9.2.tar.gz解压后编写脚本 #!/bin/bash PREFIXpwd/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 -marcharmv7-a -mfloat-abisoftfp -mfpuvfpv3-d16 -mthumb -Wa,--noexecstack -Wformat -Werrorformat-security -stdc11 -O0 -fPICexport CC$CROSS_COMPILE-gcc --sysroot$NDK_ROOT/platforms/android-17/arch-arm export CFLAGS$FLAGS./configure \ --prefix$PREFIX \ --hostarm-linux \ --with-pic \ --enable-sharednomake 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、实现思路 整体思路如下 摄像头采集视频数据进行视频编码封装进 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 服务器完成推流 上层的结构图如下 各部分职责 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 设备的摄像头是横向摆放的 如你所见摄像头是相对于设备顺时针旋转了 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 的编码方式有关。先看下面这幅图 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.sczhlp.com/news/224382/

相关文章:

  • 公司的网站如何编辑建设网站的流程
  • 建外贸网站有效果吗免费源码大全无用下载
  • 广扬建设集团网站免费高清屏幕录像
  • 百度给做网站吗企业网站报价方案模板
  • 济南网站改版跨境电商网络营销是什么
  • 做网站怎么引流免费企业注册
  • app和微网站的对比黄图网站有哪些 推荐
  • 酒店网站的建设方案wordpress二级标签
  • 贵阳市观山湖区网站建设网站制作容易吗
  • 自己做的网站突然打不开软件项目管理是做什么
  • 做网站前期需要什么wap是什么意思歌词
  • 滨江建设工程网站北京高端网站建设价格
  • 设计师网站有哪些域名解析ip138在线查询
  • 个人网站公司网站区别经营区别竞价sem托管公司
  • 南通网站建设搭建中国十大电商公司排名
  • 培训中心网站建设论文做酒业网站的要求
  • 网站做行测题网站开发的趋势
  • 用wordpress做微网站百度广告服务商
  • 舟山建设技术学校网站闽清住房和城乡建设局网站
  • 可信的邢台做网站网站建设合作合同范文
  • 网站结构是什么 怎么做昌平网站建设
  • 黔南服务好的高端网站设计公司吉安市城乡规划建设局网站
  • 网站备案时 首页网站开发需要用例图吗
  • 网站开发公司erp株洲做网站的公司
  • 热水器网站建设 中企动力百度外卖网站建设与维护方法
  • 广州网站建设推广服务nginx wordpress 二级目录
  • 网络服务公司经营范围东莞网站建设分享seo
  • 建设电影播放网站提高网站收录
  • 郑州 网站制作视频拍摄公司
  • 残疾人网站服务平台横岗网站建设多少钱