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

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()}

相关文章:

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

音视频编解码系列目录&#xff1a; Android 音视频基础知识 Android 音视频播放器 Demo&#xff08;一&#xff09;—— 视频解码与渲染 Android 音视频播放器 Demo&#xff08;二&#xff09;—— 音频解码与音视频同步 RTMP 直播推流 Demo&#xff08;一&#xff09;—— 项目…...

安卓获取SHA

1&#xff1a;安卓通过签名key获取SHA 方式有两种&#xff0c; 1、电脑上来存在eclipse的用户或正在使用此开发工具的用户就简单了&#xff0c;直接利用eclipse 走打包流程&#xff0c;再打包的时候选择相应的签名&#xff0c;那么在当前面板的下面便会出现签名的相关信息。 2、…...

【Qt 学习笔记】Qt常用控件 | 输入类控件 | Dial的使用及说明

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Qt 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Qt常用控件 | 输入类控件 | Dial的使用及说明 文章编号&#xff1a;Qt…...

【C语言】项目实践-贪吃蛇小游戏(Windows环境的控制台下)

一.游戏要实现基本的功能&#xff1a; • 贪吃蛇地图绘制 • 蛇吃食物的功能 &#xff08;上、下、左、右方向键控制蛇的动作&#xff09; • 蛇撞墙死亡 • 蛇撞自身死亡 • 计算得分 • 蛇身加速、减速 • 暂停游戏 二.技术要点 C语言函数、枚举、结构体、动态内存管…...

在做题中学习(50):搜索插入位置

35. 搜索插入位置 - 力扣&#xff08;LeetCode&#xff09; 解法&#xff1a;二分查找 思路&#xff1a;题目是有序的&#xff0c;时间复杂度O(logN),二分没跑了&#xff0c;题目说如果找不到target&#xff0c;返回它应该被插入位置的下标&#xff0c;所以可以分析一下示例2&…...

【mysql】mysql单表查询、多表查询、分组查询、子查询等案例详细解析

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…...

【Gateway远程开发】0.5GB of free space is necessary to run the IDE.

【Gateway远程开发】0.5GB of free space is necessary to run the IDE. 报错 0.5GB of free space is necessary to run the IDE. Make sure that there’s enough space in following paths: /root/.cache/JetBrains /root/.config/JetBrains 原因 下面两个路径的空间不…...

普通组件的注册-局部注册和全局注册

目录 一、局部注册和全局注册-概述 二、局部注册的使用示例 三、全局注册的使用示例 一、局部注册和全局注册-概述 组件注册有两种方式&#xff1a; 局部注册&#xff1a;只能在注册的组件内使用。使用方法&#xff1a;创建.vue文件&#xff0c;在使用的组件内导入并注册。…...

Apache Dubbo知识点表格总结

Dubbo是一个高性能的Java RPC框架&#xff0c;它提供了一系列的功能来支持分布式系统的开发。通常用于微服务之间的服务调用&#xff0c;顺便提一下也是用于微服务之间调用的OpenFeign&#xff0c;OpenFeign是Spring Cloud体系中的一个声明式HTTP客户端&#xff0c;用于简化HTT…...

电路板/硬件---器件

电阻 电阻作用 电阻在电路中扮演着重要的角色&#xff0c;其作用包括&#xff1a; 限制电流&#xff1a;电阻通过阻碍电子流动的自由而限制电流。这是电阻最基本的功能之一。根据欧姆定律&#xff0c;电流与电阻成正比&#xff0c;电阻越大&#xff0c;通过电阻的电流就越小。…...

STC15W1K16S和VC6.0串口通讯收发测试实例

/********************************************* STC USB 串口板 2014 4 7 20:12 发送接收数据 使用STC串口调试助手通讯正常&#xff0c;L161 **********************************************/ #include "reg51.h" #include "intrins.h" #define…...

Python程序设计 函数(三)

练习十一 函数 第1关&#xff1a; 一元二次方程的根 定义一个函数qg&#xff0c;输入一元二次方程的系数a,b,c 当判别式大于0&#xff0c;返回1和两个根 当判别式等于0&#xff0c;返回0和两个根 当判别式小于0&#xff0c;访问-1和两个根 在主程序中&#xff0c;根据函数返回…...

linux之ssh

SSH远程连接协议 SSH远程管理 定义 SSH&#xff08;Secure Shell &#xff09;是一种安全通道协议&#xff0c;主要用来实现字符界面的远程的登录、远程复制等功能。 SSH协议对通信双方的数据传输进行了加密处理&#xff0c;其中包括用户登录时输入的用户口令。因此SSH协议具…...

excel如何将多列数据转换为一列?

这个数据整理借用数据透视表也可以做到&#xff1a; 1.先将数据源的表头补齐&#xff0c;“姓名” 2.点击插入选项卡&#xff0c;数据透视表&#xff0c;在弹出对话框中&#xff0c;数据透视位置选择 现有工作表&#xff0c;&#xff08;实际使用时新建也没有问题&#xff09;…...

【Java 刷题记录】前缀和

前缀和 25. 一维前缀和 示例1&#xff1a; 输入&#xff1a; 3 2 1 2 4 1 2 2 3输出&#xff1a; 3 6import java.util.Scanner;// 注意类名必须为 Main, 不要有任何 package xxx 信息 public class Main {public static void main(String[] args) {Scanner in new Scanner(S…...

NVIDIA: RULER新测量方法让大模型现形

1 引言 最近在人工智能系统工程和语言模型设计方面的进展已经实现了语言模型上下文长度的高效扩展。以前的工作通常采用合成任务,如密钥检索和大海捞针来评估长上下文语言模型(LMs)。然而,这些评估在不同工作中使用不一致,仅揭示了检索能力,无法衡量其他形式的长上下文理解。 …...

2024数学-微积分和线性代数/本科研究生专业考试/考研/论文/重点公式考点汇总/最难公式投票

## 整体公式汇总列表 http://www.deepnlp.org/equation/category/math #### 微积分 ## 几何级数http://www.deepnlp.org/equation/arithmetic-and-geometric-progressions ## 级数收敛http://www.deepnlp.org/equation/convergence-of-series ## 二项式展开 http://www.dee…...

代码随想录训练营Day33(贪心算法):Leetcode1005、134、135(难得有一天能完全独立做出题目)

Leetcode1005: 题目描述&#xff1a; 给你一个整数数组 nums 和一个整数 k &#xff0c;按以下方法修改该数组&#xff1a; 选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。 重复这个过程恰好 k 次。可以多次选择同一个下标 i 。 以这种方式修改数组后&#xff0c;返回数…...

Flutter笔记:Widgets Easier组件库(12)使用消息吐丝(Notify Toasts)

Flutter笔记 Widgets Easier组件库&#xff08;12&#xff09;使用消息吐丝&#xff08;Notify Toasts&#xff09; - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 29114848416…...

从《春色寄情人》学习如何面对死亡

经典台词&#xff0c;很震撼又很实用&#xff0c;记录一下。 ❤️01 有的时候好人不长命百岁&#xff0c;是因为老天爷觉得他们太累&#xff0c;让他们提前休息了&#xff01; ❤️02 跟我们亲近的人离世&#xff0c;有可能是老天给我们发的信号&#xff0c;提醒我们&#xff…...

使用moveit控制机械臂

在这篇博客中&#xff0c;我们将详细探讨如何利用Python和Robot Operating System&#xff08;ROS&#xff09;配合MoveIt! 控制机械臂执行精确的抓取任务。机械臂技术在工业自动化、医疗服务以及研究领域扮演着越来越关键的角色。本文将通过介绍安装必要的软件、编写控制脚本以…...

Mysql报错红温集锦(一)(ipynb配置、pymysql登录、密码带@、to_sql如何加速、触发器SIGNAL阻止插入数据)

一、jupyter notebook无法使用%sql来添加sql代码 可能原因&#xff1a; 1、没装jupyter和notebook库、没装ipython-sql库 pip install jupyter notebook ipython-sql 另外如果是vscode的话还需要安装一些相关的插件 2、没load_ext %load_ext sql 3、没正确的登录到mysql…...

ASP.NET Core SignalR 配置与集成测试究极指南

这篇文章也可以在我的博客中查看 前言 哥们最近都在埋头苦干&#xff0c;沉默是金&#xff0c;有一段时间没更新博客了。然而今儿SignalR集成测试实属是给我整破防了。虽说SignalR是.NET官方维护的实时通信库&#xff0c;已经开发了有十几年&#xff0c;甚至已经编入至了core…...

JENKINS 安装,学习运维从这里开始

Download and deployJenkins – an open source automation server which enables developers around the world to reliably build, test, and deploy their softwarehttps://www.jenkins.io/download/首先点击上面。下载Jenkins 为了学习&#xff0c;从windows开始&#x…...

大语言模型从Scaling Laws到MoE

1、摩尔定律和伸缩法则 摩尔定律&#xff08;Moores law&#xff09;是由英特尔&#xff08;Intel&#xff09;创始人之一戈登摩尔提出的。其内容为&#xff1a;集成电路上可容纳的晶体管数目&#xff0c;约每隔两年便会增加一倍&#xff1b;而经常被引用的“18个月”&#xf…...

四级英语翻译随堂笔记

降维表达&#xff1a;中译英&#xff0c;英译英 没有强调主语&#xff0c;没有说明主语&#xff1a;用被动 但如果实在不行&#xff0c;再增添主语 不会就不翻译&#xff0c;不要乱翻译 以xxx为背景&#xff1a;against the backdrop of the xxx eg:against the backdrop of…...

Nacos支持的配置格式及其在微服务架构中的应用

今天&#xff0c;我想和大家探讨一下Nacos这一重要的微服务组件&#xff0c;特别是它所支持的配置格式以及这些格式在微服务架构中的应用。 一、Nacos简介 Nacos是阿里巴巴开源的一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。它提供了服务发现、配置管理…...

2024年华为OD机试真题-小明找位置-(C++)-OD统一考试(C卷D卷)

题目描述: 小朋友出操,按学号从小到大排成一列;小明来迟了,请你给小明出个主意,让他尽快找到他应该排的位置。 算法复杂度要求不高于nLog(n);学号为整数类型,队列规模<=10000; 输入描述: 1、第一行:输入已排成队列的小朋友的学号(正整数),以”,”隔开; …...

机器人系统ros2内部接口介绍

内部 ROS 接口是公共 C API &#xff0c;供创建客户端库或添加新的底层中间件的开发人员使用&#xff0c;但不适合典型 ROS 用户使用。 ROS客户端库提供大多数 ROS 用户熟悉的面向用户的API&#xff0c;并且可能采用多种编程语言。 内部API架构概述 内部接口主要有两个&#x…...

跟随Facebook的足迹:社交媒体背后的探索之旅

在当今数字化时代&#xff0c;社交媒体已经成为了人们日常生活中不可或缺的一部分。而在这庞大的社交媒体网络中&#xff0c;Facebook作为其中的巨头&#xff0c;一直在引领着潮流。从创立之初的一个大学社交网络到如今的全球性平台&#xff0c;Facebook的发展历程承载了无数故…...

面试题分享之Java并发篇

注意&#xff1a;文章若有错误的地方&#xff0c;欢迎评论区里面指正 &#x1f36d; 系列文章目录 面试题分享之Java集合篇&#xff08;三&#xff09; 面试题分享之Java集合篇&#xff08;二&#xff09; 面试题分享之Java基础篇&#xff08;三&#xff09; 前言 今天给小…...

bpmn-js 多实例配置MultiInstanceLoopCharacteristics实现或签会签

使用bpmn-js流程图开发过程中会遇到会签和或签的问题,这个时候我们就需要使用多实例配置来实现BPMN 2.0的配置实现了,多实例任务,是从流程编辑概念之初也就是Activiti时期就存在的一个方式。所谓的多实例任务也就是字面意思,一个任务由多个人完成,常见于我们的审批流程的或…...

【gpedit.msc】组策略编辑器的安装,针对windows家庭版,没有此功能

创建一个记事本文件然后放入以下内容 echo offpushd "%~dp0"dir /b %systemroot%\Windows\servicing\Packages\Microsoft-Windows-GroupPolicy-ClientExtensions-Package~3*.mum >gp.txtdir /b %systemroot%\servicing\Packages\Microsoft-Windows-GroupPolicy-…...

带EXCEL附件邮件发送相关代码

1.查看生成的邮件 2.1 非面向对象的方式&#xff08;demo直接copy即可&#xff09; ​ REPORT Z12. DATA: IT_DOCUMENT_DATA TYPE SODOCCHGI1,IT_CONTENT_TEXT TYPE STANDARD TABLE OF SOLISTI1 WITH HEADER LINE,IT_PACKING_LIST TYPE TABLE OF SOPCKLSTI1 WITH HEADER LIN…...

【算法作业】均分卡牌,购买股票

问题描述 John 有两个孩子&#xff0c;在 John病逝后&#xff0c;留下了一组价值不一定相同的魔卡&#xff0c; 现在要求你设计一种策略&#xff0c;帮John的经管人将John的这些遗产分给他的两个孩子&#xff0c;使得他们获得的遗产差异最小&#xff08;每张魔卡不能分拆&#…...

python作业

题目 分析 步骤&#xff1a; 判断先画空格还是数字 当有n层时&#xff0c;第i层有多少个空格第i层的起始数字是几&#xff0c;结尾是几&#xff0c;即数字取值范围当有n层时&#xff0c;第i层有多少个数字 代码 模式A n int(input("请输入行数:")) for i in range(…...

【Linux的文件篇章 - 管道文件】

Linux学习笔记---013 Linux的管道文件1、进程间通信1.1、进程为什么要通信&#xff1f;1.2、进程如何通信&#xff1f;1.3、进程通信的方式&#xff1f; 2、匿名管道2.1、理解一种现象2.2、基本概念和管道原理 3、管道的使用3.1、代码样例3.2、如何使用管道通信呢&#xff1f;3…...

C# 局部静态函数,封闭方法中的最佳选择

C# 局部静态函数&#xff0c;封闭方法中的最佳选择 简介特性 应用场景辅助计算递归与尾递归优化筛选与过滤操作查找与映射操作 生命周期静态局部函数 vs 普通局部函数性能封装性可读性 简介 C# 局部静态函数&#xff08;Local Static Functions&#xff09;是一种函数作用域内…...

【MySQL】MySQL 8.4.0 长期支持版(LTS)安装

就在2024年 “5.1” 节前&#xff0c;MySQL官方发布了8.4.0长期支持版&#xff08;LTS - Long Term Support&#xff09;。根据官方提供的文档&#xff0c;在本地虚拟机进行安装测试。 安装、配置和启动过程记录如下&#xff1a; 第一步&#xff0c;上传到安装包&#xff08;my…...

nest中的ORM

在 Nest.js 中执行 SQL 查询通常涉及使用 TypeORM 或 Sequelize 这样的 ORM&#xff08;对象-关系映射&#xff09;库。这些库使得在 Nest.js 应用程序中连接和操作 SQL 数据库变得更加简单和直观。 以下是一个使用 TypeORM 在 Nest.js 中执行 SQL 查询的示例代码&#xff1a;…...

TCP(Transmission Control Protocol,传输控制协议)如何保证数据的完整性?

TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09;通过一系列机制来保证数据传输的可靠性和无错性&#xff0c;这些机制主要包括&#xff1a; 校验和&#xff1a;TCP报文段包含一个校验和字段&#xff0c;用于检测数据在传输过程中是否出错。…...

Numpy库介绍

NumPy&#xff08;Numerical Python的缩写&#xff09;是Python中用于科学计算的一个强大的库。它提供了高性能的多维数组对象&#xff08;即ndarray&#xff09;、用于处理这些数组的工具以及用于数学函数操作的函数。让我为你介绍一下它的一些主要功能&#xff1a; 1. 多维数…...

临时有事无法及时签字盖章?试试用契约锁设置“代理人”

遇到“领导休假中、在开重要会议、外出考察或者主任医生手术中等”一段时间内不方便或者无法及时签字盖章的情况怎么办&#xff1f;业务推进不了只能干等&#xff1f; 契约锁电子签及印控平台支持印章、签名“临时授权”、“代理签署”&#xff0c;实现指定人、指定时间段、指定…...

数据库权限管理

1.查看系统级权限&#xff08;global level) Select * from mysql.user\G; 2.查看数据库中所有表的权限 Select * from mysql.db\G 3.远程连接数据库 第一步在有数据库服务上的主机上&#xff1a;授权 grant all on *.* to root192.168.40.83 identified by Zxy20234; 第…...

如何创建一个 Django 应用并连接到数据库

简介 Django 是一个用 Python 编写的免费开源的 Web 框架。这个工具支持可扩展性、可重用性和快速开发。 在本教程中&#xff0c;您将学习如何为一个博客网站建立与 MySQL 数据库的初始基础。这将涉及使用 django-admin 创建博客 Web 应用程序的骨架结构&#xff0c;创建 MyS…...

【算法刷题day44】Leetcode:518. 零钱兑换 II、377. 组合总和 Ⅳ

文章目录 Leetcode 518. 零钱兑换 II解题思路代码总结 Leetcode 377. 组合总和 Ⅳ解题思路代码总结 草稿图网站 java的Deque Leetcode 518. 零钱兑换 II 题目&#xff1a;518. 零钱兑换 II 解析&#xff1a;代码随想录解析 解题思路 先遍历物品&#xff0c;再遍历背包。 代码…...

『51单片机』AT24C02[IIC总线]

存储器的介绍 ⒈ROM的功能⇢ROM的数据在程序运行的时候是不容改变的&#xff0c;除非你再次烧写程序&#xff0c;他就会改变&#xff0c;就像我们的书本&#xff0c;印上去就改不了了&#xff0c;除非再次印刷&#xff0c;这个就是ROM的原理。 注→在后面发展的ROM是可以可写可…...

Jenkins与Rancher的配合使用

Jenkins和Rancher是两个常用的DevOps工具&#xff0c;可以很好地配合使用来实现持续集成和持续部署。 Jenkins是一个开源的自动化构建工具&#xff0c;可以实现自动化的代码构建、测试和部署等一系列操作。可以通过Jenkins来触发构建任务&#xff0c;例如从代码仓库中拉取最新的…...

GIS入门,常用的多边形平滑曲线算法介绍和JavaScript的多边形平滑曲线算法库chaikin-smooth的实现原理和使用

前言 本章介绍一下常用的多边形平滑曲线算法及其使用案例。 多边形平滑算法通常用于图形处理或计算机图形学中,以使线条或曲线在连接处平滑过渡,而不出现明显的棱角或断裂。多边形平滑算法有多种实现方法,其中一些常见的有下面几种: 贝塞尔曲线插值(Bezier Curve Interpo…...

气膜体育馆内部的采光效果如何?—轻空间

气膜体育馆内部的采光效果如何&#xff1f;这是许多人对这种创新建筑的一个关键关注点。 首先&#xff0c;气膜体育馆的采光性非常好。阳光透过屋顶时以漫射光的方式进入室内&#xff0c;这种透射方式使得室内的光线柔和而均匀。从内部观察&#xff0c;整个屋顶就像一个连续的明…...

android 目录

https://www.cnblogs.com/yuanqiangfei/p/16880668.html 顶层目录 apex&#xff1a; apex文件安装路径&#xff0c; android10引进的技术&#xff0c; Android Pony EXpress (APEX) &#xff0c; APEX和APK类似&#xff0c;它原来存在于只读系统分区的功能模块搞成一个个可更新…...

前端启若依项目 后端配置 若依

...

码农慎入 | 入坑软路由,退烧IDC,Homelab折腾记

点击文末“阅读原文”即可参与节目互动 剪辑、音频 / 卷圈 运营 / SandLiu 卷圈 监制 / 姝琦 封面 / 姝琦Midjourney 产品统筹 / bobo 场地支持 / 声湃轩北京录音间 俗话说&#xff0c;入门软路由&#xff0c;退坑IDC 这一期&#xff0c;我们将深入探讨一个许多科技爱好者…...

Golang面向对象编程

在Go语言中&#xff0c;并没有像传统面向对象编程语言&#xff08;如Java或C&#xff09;那样直接支持类和继承的概念。但是&#xff0c;Go通过结构体&#xff08;struct&#xff09;和方法&#xff08;method&#xff09;来模拟面向对象编程中的对象和行为。 下面是一个简单的…...

workerman 实现推送实时数据到前端

首先要说明一下&#xff0c;实时推送是有两个socket服务端和两个socket的客户端 我们分别起名叫做 外层服务端 &#xff08;开启服务主进程 创建一个 websoket 连接&#xff09;ws:// 内层服务端 &#xff08;在开户主进程的时候&#xff0c;内部又创建的一个 socket&#xff0…...

基于网络爬虫技术的网络新闻分析(二)

目录 2 系统需求分析 2.1 系统需求概述 2.2 系统需求分析 2.2.1 系统功能要求 2.2.2 系统IPO图 2.2 系统非功能性需求分析 3 系统概要设计 3.1 设计约束 3.1.1 需求约束 3.1.2 设计策略 3.1.3 技术实现 3.3 模块结构 3.3.1 模块结构图 3.3.2 系统层次图 3.3.3…...