【Android虚拟摄像头】五、用视频替换相机预览画面_安卓虚拟摄像头
目录
前情提要
本篇目标
一、配置OBS及RTMP推流服务器
1. 配置RTMP推流服务器
2. 配置OBS推流软件
3. 通过FFPlay测试RTMP视频流
二、修改相机服务代码,读取视频流YUV数据
1. 程序逻辑设计
2. 修改CameraServer进程代码
3. 编译测试
三、 编写VCAM程序,为相机服务提供YUV数据
1. 编写VCam.cpp程序代码
2. 添加编译配置
3. 编译生成可执行文件
四、测试虚拟摄像头
1. 拷贝可执行文件到手机
2. 启动虚拟摄像头程序
3. 测试虚拟摄像头
完整代码下载
总结
前情提要
在上一篇文章中,我们读取了本地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...// 移除头文件class ImageReplacer {private: ... // 移除jpegData变量public: ... // 移除loadJPG()函数};...status_t Camera3Device::initialize(...) { // 移除gImageReplacer.loadJPG()调用点 ...}...
修改 replaceYUVBuffer 函数,从VCAM进程生成的YUV文件中读取 YUVPlane 数据
// Camera3Device.cppvoid 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(ySize); uPlane = std::vector(uvSize); vPlane = std::vector(uvSize); // 分别读取YUV分量 file.read(reinterpret_cast(yPlane.data()), ySize); file.read(reinterpret_cast(uPlane.data()), uvSize); file.read(reinterpret_cast(vPlane.data()), uvSize); file.close(); // 1. 处理Y平面(每次复制一行,考虑stride) ...}
3. 编译测试
执行 mmm 命令编译后,通过ADB替换 libcameraservice.so 动态库到手机
cd ~/android/lineagesource build/envsetup.shbreakfast dumplingmmm frameworks/av/services/camera/libcameraservice/
删除上一篇中我们通过JPG图片转换后生成的 /sdcard/1.yuv 文件(如果这里不删除,接下来预览时会看到上一篇中测试的图片)
打开相机APP,看到预览画面变为 绿色,即说明当前步骤测试通过
由于我们提供的测试视频分辨率是 1920x1080,在这里我们需要先把 相机设置 里的 画面尺寸 改为 16:9 再进行下一步(如果这里不修改,也不影响下一步预览,但画面会出现裁剪)
三、 编写VCAM程序,为相机服务提供YUV数据
1. 编写VCam.cpp程序代码
编写程序,调用FFMPEG读取视频流,把最新一帧的画面保存到 /sdcard/1.yuv 文件
#include #include #include #include #include #include #include #include #include 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 cmdArgs = { ffmpegPath, \"-i\", rtmpURL, // 如果不希望显示FFMPEG输出,可以设置loglevel参数 // \"-loglevel\", \"quiet\", \"-f\", \"rawvideo\", \"-pix_fmt\", \"yuv420p\", \"-vcodec\", \"rawvideo\", // 如果OBS画面是竖屏,需要添加transpose参数 // \"-vf\", \"transpose=2\", \"-\" }; // 按execvp要求,转换为char*数组 std::vector cmdArgv; for (auto& arg : cmdArgs) { cmdArgv.push_back(const_cast(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::system_clock::now() ).time_since_epoch().count(); std::vector 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::system_clock::now() ).time_since_epoch().count(); // 等待一帧的间隔时间 while(t1 - t0 < duration) { usleep(1); t1 = std::chrono::time_point_cast( 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/tmpchmod 755 ffmpegchmod 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