目录
前情提要
在上一篇文章中,我们读取了本地JPG图片,并转换为YUV(NV12)数据,成功用静态图片替换了相机预览画面,为接下来的工作增添了信心
本篇目标
修改一加5T手机Framework层源码,用OBS软件把视频流推送到本地RTMP服务器,并通过手机拉流,用拉取的视频流替换相机APP预览画面,完成虚拟摄像头的雏形
在后面的文章中会不断深入修改,实现虚拟摄像头,并完成DY、ZFB等软件的刷脸验证
一、配置OBS及RTMP推流服务器
1. 配置RTMP推流服务器
在电脑上下载 SRS 软件安装包
SRS安装包 - 夸克网盘https://pan.quark.cn/s/6df019351035
在Windows安全中心关闭电脑防火墙
双击SRS安装包完成安装后,打开开始菜单中的 SRS 软件,看到如下提示即说明 RTMP推流服务器 启动成功
也可以在电脑上使用Nginx作为RTMP服务器,但SRS安装简单,无需配置,相对更方便
2. 配置OBS推流软件
在电脑上下载 OBS Studio 安装包和 测试视频
OBS Studio安装包 + 测试视频 - 夸克网盘https://pan.quark.cn/s/a7ab735e8953双击OBS安装包完成安装后,打开开始菜单中的 OBS Studio 软件,进入设置界面
在 直播设置 中填写 RTMP服务器 地址
rtmp://127.0.0.1:1935/live/test
在 视频设置 中填写 1920x1080分辨率 30帧
在主界面拖入我们准备好的 测试视频,在 混音器列表 中关闭所有音频输入
在 源列表 中双击视频文件,勾选 循环播放
点击主界面的 开始直播 按钮,完成OBS推流配置
也可以在电脑上使用FFMPEG实现推流,作者因为还有其他需要用到OBS的测试工作,所以直接使用OBS客户端
3. 通过FFPlay测试RTMP视频流
在电脑上下载 ffplay.exe 程序,添加到环境变量
ffplay.exe等3个文件 - 夸克网盘https://pan.quark.cn/s/098b641ca97d执行下面的命令
ffplay -i rtmp://127.0.0.1:1935/live/test
看到视频预览画面即说明RTMP推流配置成功
二、修改相机服务代码,读取视频流YUV数据
1. 程序逻辑设计
本篇和上一篇不同,不再是显示静态图片,而是需要显示不断变化的视频画面
结合上一篇中我们 读取JPG图片,解码为YUV格式数据,并在预览画面中显示 的经验,我们把程序分为 相机服务程序(CameraServer进程,读取YUV数据) 和 数据提供程序(VCAM进程,生成YUV数据) 两部分:
VCAM进程在手机上通过FFMPEG拉取视频流,并不断把最新一帧保存为YUV格式数据
CameraServer进程在每次相机APP预览最新一帧摄像头画面时,把画面替换为最新的YUV数据
2. 修改CameraServer进程代码
移除上一篇 Camera3Device.cpp 中的 JPG文件读取 相关代码
// Camera3Device.cpp
...
// 移除<turbojpeg.h>头文件
class ImageReplacer {
private:
...
// 移除jpegData变量
public:
...
// 移除loadJPG()函数
};
...
status_t Camera3Device::initialize(...) {
// 移除gImageReplacer.loadJPG()调用点
...
}
...
修改 replaceYUVBuffer 函数,从VCAM进程生成的YUV文件中读取 YUVPlane 数据
// Camera3Device.cpp
void replaceYUVBuffer(const android_ycbcr &ycbcr, uint32_t srcWidth, uint32_t srcHeight) {
// 读取YUV数据
std::ifstream file("/sdcard/1.yuv", std::ios::binary);
// 分别计算Y分量和UV分量的尺寸
int ySize = srcWidth * srcHeight;
int uvSize = (srcWidth / 2) * (srcHeight / 2);
// 初始化YUV分量
yPlane = std::vector<uint8_t>(ySize);
uPlane = std::vector<uint8_t>(uvSize);
vPlane = std::vector<uint8_t>(uvSize);
// 分别读取YUV分量
file.read(reinterpret_cast<char*>(yPlane.data()), ySize);
file.read(reinterpret_cast<char*>(uPlane.data()), uvSize);
file.read(reinterpret_cast<char*>(vPlane.data()), uvSize);
file.close();
// 1. 处理Y平面(每次复制一行,考虑stride)
...
}
3. 编译测试
执行 mmm 命令编译后,通过ADB替换 libcameraservice.so 动态库到手机
cd ~/android/lineage
source build/envsetup.sh
breakfast dumpling
mmm frameworks/av/services/camera/libcameraservice/
删除上一篇中我们通过JPG图片转换后生成的 /sdcard/1.yuv 文件(如果这里不删除,接下来预览时会看到上一篇中测试的图片)
打开相机APP,看到预览画面变为 绿色,即说明当前步骤测试通过
由于我们提供的测试视频分辨率是 1920x1080,在这里我们需要先把 相机设置 里的 画面尺寸 改为 16:9 再进行下一步(如果这里不修改,也不影响下一步预览,但画面会出现裁剪)
三、 编写VCAM程序,为相机服务提供YUV数据
1. 编写VCam.cpp程序代码
编写程序,调用FFMPEG读取视频流,把最新一帧的画面保存到 /sdcard/1.yuv 文件
#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <sys/wait.h>
#include <cstdio>
#include <fstream>
#include <chrono>
#include <queue>
int main(int argc, char* argv[]) {
if (argc != 6) {
std::cout << "启动命令: ./vcam /data/local/tmp/ffmpeg rtmp://192.168.5.170:1935/live/test 1920 1080 30" << std::endl;
return 1;
}
const char* tmpPath = "/sdcard/0.yuv";
const char* yuvPath = "/sdcard/1.yuv";
char* ffmpegPath = argv[1];
char* rtmpURL = argv[2];
int width = atoi(argv[3]);
int height = atoi(argv[4]);
int fps = atoi(argv[5]);
// 构造ffmpeg命令参数,把视频流输出到stdout
std::vector<std::string> cmdArgs = {
ffmpegPath,
"-i", rtmpURL,
// 如果不希望显示FFMPEG输出,可以设置loglevel参数
// "-loglevel", "quiet",
"-f", "rawvideo",
"-pix_fmt", "yuv420p",
"-vcodec", "rawvideo",
// 如果OBS画面是竖屏,需要添加transpose参数
// "-vf", "transpose=2",
"-"
};
// 按execvp要求,转换为char*数组
std::vector<char*> cmdArgv;
for (auto& arg : cmdArgs) {
cmdArgv.push_back(const_cast<char*>(arg.c_str()));
}
cmdArgv.push_back(nullptr);
while (true) {
// 创建管道,pipeFd[0]读端,pipeFd[1]写端
int pipeFd[2];
if (pipe(pipeFd) == -1) {
perror("pipe失败");
}
// 创建子进程,运行ffmpeg
pid_t pid = fork();
if (pid == -1) {
perror("fork失败");
} else if (pid == 0) {
// 子进程:执行ffmpeg
// 关闭读端
close(pipeFd[0]);
// 将子进程的stdout重定向到管道的写端
if (dup2(pipeFd[1], STDOUT_FILENO) == -1) {
perror("dup2失败");
}
// 关闭原始写端
close(pipeFd[1]);
// 执行ffmpeg
execvp(argv[1], cmdArgv.data());
// 如果execvp失败,才会执行到这里
perror("execvp失败");
} else {
// 父进程:读取管道数据
// 关闭写端
close(pipeFd[1]);
// 将管道的读端转换为FILE*,方便读取
FILE* pipeOut = fdopen(pipeFd[0], "rb");
if (!pipeOut) {
perror("fdopen失败");
}
auto t0 = std::chrono::time_point_cast<std::chrono::microseconds>(
std::chrono::system_clock::now()
).time_since_epoch().count();
std::vector<char> buffer(int(width * height * 1.5));
// 每一帧画面持续时间,单位:微秒
double duration = 1000 / fps * 1000;
// 循环读取YUV数据
while (true) {
size_t bufferSize = fread(buffer.data(), 1, buffer.size(), pipeOut);
// 各种意外情况造成未读取到数据时,跳出循环,重启子进程
if (bufferSize == 0) {
break;
}
std::ofstream outFile(tmpPath, std::ios::binary);
if (outFile) {
outFile.write(buffer.data(), bufferSize);
outFile.close();
}
// 通过对临时文件重命名,减少耗时,避免文件在写入的同时被相机服务进程读取
rename(tmpPath, yuvPath);
auto t1 = std::chrono::time_point_cast<std::chrono::microseconds>(
std::chrono::system_clock::now()
).time_since_epoch().count();
// 等待一帧的间隔时间
while(t1 - t0 < duration) {
usleep(1);
t1 = std::chrono::time_point_cast<std::chrono::microseconds>(
std::chrono::system_clock::now()
).time_since_epoch().count();
}
t0 = t1;
}
// 关闭管道读端
fclose(pipeOut);
close(pipeFd[0]);
// 结束子进程
kill(pid, SIGKILL);
// 等待子进程退出
int status;
waitpid(pid, &status, 0);
std::cout << "子进程退出状态: " << WEXITSTATUS(status) << std::endl;
// 等待1秒,重启子进程
sleep(1);
std::cout << "重启子进程" << std::endl;
}
}
return 0;
}
特别说明1:由于文件写入耗时远大于重命名耗时,我们在代码里通过对临时文件重命名,减少耗时,避免文件在写入的同时被相机服务进程读取
特别说明2:当发生如网络丢包等各种意外情况,造成未读取到数据、程序阻塞时,我们在代码里通过跳出内层循环的方式,重启FFMPEG进程
2. 添加编译配置
我们在相机服务模块的 Android.bp 文件里添加编译配置,让编译工具在编译相机服务模块时,联通我们编写的 VCam.cpp 代码一起编译
在 Camera3Device.cpp 的上一层目录中找到 Android.bp 文件
~/android/lineage/frameworks/av/services/camera/libcameraservice/Amdroid.bp
在文件末尾添加编译配置
// Android.bp
...
cc_binary {
name: "vcam",
srcs: ["device3/VCam.cpp"],
shared_libs: [],
include_dirs: [],
cflags: [
"-Wall",
"-Wextra",
"-Werror",
"-Wno-ignored-qualifiers",
],
}
把 VCam.cpp 代码文件拷贝到 Camera3Device.cpp 的同级目录
~/android/lineage/frameworks/av/services/camera/libcameraservice/device3/VCam.cpp
3. 编译生成可执行文件
执行模块编译命令,完成编译后,在下面的目录中找到并拷贝可执行文件
~/android/lineage/out/target/product/dumpling/system/bin/vcam
如果不想放在相机模块中,也可以直接执行命令交叉编译独立文件
四、测试虚拟摄像头
1. 拷贝可执行文件到手机
通过ADB命令拷贝上一步编译的vcam文件到手机
adb push .\vcam /data/local/tmp
下载FFMPEG可执行文件,通过ADB命令拷贝到手机
FFMPEG for Android - 夸克网盘https://pan.quark.cn/s/2e335761d724
adb push .\ffmpeg /data/local/tmp
进入ADB终端,设置可执行权限
cd /data/local/tmp
chmod 755 ffmpeg
chmod 755 vcam
2. 启动虚拟摄像头程序
确保手机和电脑在同一个局域网内,在ADB终端中执行如下命令启动虚拟摄像头程序
# RTMP服务器的IP地址替换为自己SRS电脑的IP地址
./vcam /data/local/tmp/ffmpeg rtmp://192.168.5.170:1935/live/test 1920 1080 30
看到如下输出,即说明启动成功
3. 测试虚拟摄像头
保持刚才的ADB终端不要关闭,打开相机,看到画面已替换为OBS推送的视频流
完整代码下载
VCam.cpp等3个文件 (用视频替换相机预览画面) - 夸克网盘https://pan.quark.cn/s/ca157cde1e58
总结
作者因为很害怕,所以这里并没有对文章进行总结,但贴了一张Hanser的壁纸XD