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

【Android虚拟摄像头】七、安卓15系统实现虚拟摄像头

目录

前情提要

本篇目标

一、刷入LineageOS系统

二、搭建LineageOS22.2编译环境

三、修改CameraServer进程文件读写权限

1. 修改SELinux策略文件

2. 修改内核namei.c文件

3. 处理FUSE文件系统

四、用本地YUV文件替换摄像头画面

1. 降级Gralloc模块版本

2. 修改Surface创建时配置的图像格式

3. 读取本地YUV文件替换摄像头画面

4. 把摄像头预览画面替换为黑白图像(可选)

五、刷机测试

1. 整包刷机测试

2. 单文件编译替换测试

完整代码下载

总结


前情提要

在上一篇文章中,我们通过对内存初始化流程进行优化,及通过libyuv库进行优化,成功对额外处理的耗时从26.9毫秒优化到了7.2毫秒,掉帧问题得到了显著改善

但实际测试中,发现抖音直播时会认为一加5T的手机硬件性能较差,对直播画面质量及帧率严重压缩,导致直播效果大打折扣


本篇目标

把一加5T(Android10)设备替换为一加9(Android15)并实现虚拟摄像头,从硬件层面彻底提升性能


一、刷入LineageOS系统

参考另一篇文章 一加9刷入LineageOS 22.2 + Root教程 为一加9手机刷入LineageOS 22.2系统,并获取root权限


二、搭建LineageOS22.2编译环境

参考另一篇文章 为一加9手机编译LineageOS 22.2(Android 15)系统 完成编译环境搭建,并完成初次编译


三、修改CameraServer进程文件读写权限

1. 修改SELinux策略文件


修改 ./system/sepolicy/private/cameraserver.te 策略配置文件,加入 /sdcard目录读写权限

# cameraserver.te
...
neverallow cameraserver domain:{ udp_socket rawip_socket } *;
neverallow cameraserver { domain userdebug_or_eng(`-su') }:tcp_socket *;### ******************************************************
### 【新增策略开始】
### ******************************************************allow cameraserver storage_file:dir { search getattr open read write add_name };
allow cameraserver storage_file:file { create read write open getattr };allow cameraserver mnt_user_file:dir { search getattr open read write add_name };
allow cameraserver mnt_user_file:file { create read write open getattr };
allow cameraserver mnt_user_file:lnk_file read;allow cameraserver sdcardfs:dir { search getattr open read write add_name };
allow cameraserver sdcardfs:file { create read write open getattr };allow cameraserver media_rw_data_file:dir { search getattr open read write add_name };
allow cameraserver media_rw_data_file:file { create read write open getattr };allow cameraserver mnt_pass_through_file:dir { search getattr open read write add_name };
allow cameraserver mnt_pass_through_file:file { create read write open getattr };
allow cameraserver mnt_pass_through_file:lnk_file read;### ******************************************************
### 【新增策略结束】
### ******************************************************

特别说明:添加mnt_pass_through_file策略是为了绕开FUSE,下文中会具体说明

2. 修改内核namei.c文件


修改 ./kernel/oneplus/sm8350/fs/namei.c 内核代码文件,添加 相机用户组文件系统访问放行代码

// namei.c
int generic_permission(struct inode *inode, int mask)
{int ret;/******************************************************************* 【添加放行代码】******************************************************************/const struct cred *cred = current_cred();kuid_t cameraserver_uid = KUIDT_INIT(1047);if (uid_eq(cred->fsuid, cameraserver_uid)) {// 当前进程属于camera组return 0;}/******************************************************************* 【添加放行代码结束】******************************************************************//** Do the basic permission checks.*/ret = acl_permission_check(inode, mask);...return -EACCES;
}

特别说明:如果不判断特定用户组直接全部放行不会影响功能,但会导致抖音刷脸界面无法拉起,猜测可能是抖音在刷脸环境检测时做了权限拒绝试探,期望访问某个目录后被系统拒绝,但当我们全部放行后,抖音在试探中未被拒绝,就认为设备环境有风险

3. 处理FUSE文件系统


安卓11及更高的版本引入了FUSE文件系统安全机制,导致CameraServer进程直接读写/sdcard目录时,即使在内核和SELinux中完成了放行,依旧会被拒绝访问

一般我们可以通过关闭FUSE模块等方案解决该问题

但经我们测试发现,一加9的LineageOS22.2系统中存在/mnt/pass_through/0/emulated/0路径,该路径也指向/sdcard目录,且未被FUSE保护,所以我们可以直接使用该路径,而无需费心处理FUSE

四、用本地YUV文件替换摄像头画面

1. 降级Gralloc模块版本


经测试发现,由于一加9设备在处理摄像头返回buffer时未采用标准YUV格式,导致 GraphicBufferMapper::get().lockYCbCr 函数在一加9设备上返回的 android_ycbcr 结构体未被正确赋值,但可通过降级Gralloc模块版本解决该问题

我们在这里把默认的 gralloc4 降级为 gralloc3 解决该问题

修改 ./frameworks/native/libs/ui/GraphicBufferMapper.cpp 代码文件,在 mMapper初始化环节注释掉Gralloc5及Gralloc4版本检查代码

// GraphicBufferMapper.cpp
GraphicBufferMapper::GraphicBufferMapper() {/******************************************************************* 【注释原有代码】******************************************************************/// mMapper = std::make_unique<const Gralloc5Mapper>();// if (mMapper->isLoaded()) {//     mMapperVersion = Version::GRALLOC_5;//     return;// }// mMapper = std::make_unique<const Gralloc4Mapper>();// if (mMapper->isLoaded()) {//     mMapperVersion = Version::GRALLOC_4;//     return;// }/******************************************************************* 【注释代码结束】******************************************************************/mMapper = std::make_unique<const Gralloc3Mapper>();if (mMapper->isLoaded()) {mMapperVersion = Version::GRALLOC_3;return;}...
}

2. 修改Surface创建时配置的图像格式


经测试发现,一加9设备默认采用的厂商私有图像格式存在压缩优化处理,并不是NV12或NV21等标准YUV平面格式,导致我们无法解析和替换

我们采用修改默认图像格式的方案,解决图像数据无法正确解析的问题

修改 ./frameworks/av/services/camera/libcameraservice/device3/Camera3Device.cpp 代码文件,在 输出流配置环节 把 厂商私有格式 修改为 YCbCr_420_888格式

// Camera3Device.cpp
status_t Camera3Device::configureStreamsLocked(int operatingMode,const CameraMetadata& sessionParams, bool notifyRequestThread) {.../******************************************************************* 【新增代码开始】:在调用HAL的configureStreams之前,遍历所有输出流,修改格式******************************************************************/ALOGD("%s: Attempting to override stream formats for YUV output", __FUNCTION__);for (size_t i = 0; i < streams.size(); i++) {camera3::camera_stream_t* stream = streams[i];// 只处理输出流if (stream->stream_type != CAMERA_STREAM_OUTPUT) {continue;}// 识别 IMPLEMENTATION_DEFINED 格式的流if (stream->format == HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED) {ALOGD("Stream %zu: Overriding format from IMPLEMENTATION_DEFINED (0x%x) to YCbCr_420_888 (0x%x)",i, stream->format, HAL_PIXEL_FORMAT_YCbCr_420_888);// 1. 修改格式stream->format = HAL_PIXEL_FORMAT_YCbCr_420_888;// 2. 修改 usage flags,移除硬件渲染相关的标志,添加CPU读取标志stream->usage = (stream->usage & ~(GRALLOC_USAGE_HW_TEXTURE | GRALLOC_USAGE_HW_COMPOSER |GRALLOC_USAGE_HW_VIDEO_ENCODER)) |GRALLOC_USAGE_SW_READ_OFTEN;mComposerOutput = false;ALOGD("Stream %zu: New usage flags: 0x%x", i, stream->usage);}}/******************************************************************* 【新增代码结束】******************************************************************/// Do the HAL configuration; will potentially touch stream// max_buffers, usage, priv fields, data_space and format// fields for IMPLEMENTATION_DEFINED formats as well as hal buffer managed// streams and use_hal_buf_manager (in case aconfig flag session_hal_buf_manager// is not enabled but the HAL supports session specific hal buffer manager).int64_t logId = mCameraServiceProxyWrapper->getCurrentLogIdForCamera(mId);const camera_metadata_t *sessionBuffer = sessionParams.getAndLock();res = mInterface->configureStreams(sessionBuffer, &config, bufferSizes, logId);sessionParams.unlock(sessionBuffer);...return OK;
}

3. 读取本地YUV文件替换摄像头画面


修改 ./frameworks/av/services/camera/libcameraservice/device3/Camera3OutputStream.cpp 代码文件,把 摄像头预览画面 替换本地YUV图片文件

// Camera3OutputStream.cpp
...
#include <libyuv.h>
...
status_t Camera3OutputStream::returnBufferCheckedLocked(const camera_stream_buffer &buffer,nsecs_t timestamp,nsecs_t readoutTimestamp,[[maybe_unused]] bool output,int32_t transform,const std::vector<size_t>& surface_ids,/*out*/sp<Fence> *releaseFenceOut) {...ANativeWindowBuffer *anwBuffer = container_of(buffer.buffer, ANativeWindowBuffer, handle);/******************************************************************* 【替换相机预览画面】******************************************************************/android_ycbcr ycbcr = {};GraphicBufferMapper &gmapper = GraphicBufferMapper::get();res = gmapper.lockYCbCr(*buffer.buffer,GRALLOC_USAGE_SW_WRITE_OFTEN,Rect(anwBuffer->width, anwBuffer->height),&ycbcr);gmapper.unlock(*buffer.buffer);if (res == OK) {ALOGD("YCbCr lock success. w=%d, h=%d, y=%p, cb=%p, cr=%p, ystride=%zu, cstride=%zu",anwBuffer->width, anwBuffer->height, ycbcr.y, ycbcr.cb, ycbcr.cr, ycbcr.ystride, ycbcr.cstride);// 读取YUV数据std::ifstream file("/mnt/pass_through/0/emulated/0/1.yuv", std::ios::binary);std::ifstream wFile("/mnt/pass_through/0/emulated/0/1.width");std::ifstream hFile("/mnt/pass_through/0/emulated/0/1.height");int yuvWidth;int yuvHeight;wFile >> yuvWidth;hFile >> yuvHeight;wFile.close();hFile.close();// 分别计算Y分量和UV分量的尺寸int ySize = yuvWidth * yuvHeight;int uvSize = (yuvWidth / 2) * (yuvHeight / 2);// 初始化YUV分量std::vector<uint8_t>yPlane(ySize);std::vector<uint8_t>uPlane(uvSize);std::vector<uint8_t>vPlane(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();// 缩放int scaledYSize = anwBuffer->width * anwBuffer->height;int scaledUVSize = (anwBuffer->width / 2) * (anwBuffer->height / 2);std::vector<uint8_t>scaledYPlane(scaledYSize);std::vector<uint8_t>scaledUPlane(scaledUVSize);std::vector<uint8_t>scaledVPlane(scaledUVSize);libyuv::I420Scale(yPlane.data(), yuvWidth,                   // 源YuPlane.data(), yuvWidth / 2,               // 源UvPlane.data(), yuvWidth / 2,               // 源VyuvWidth, yuvHeight,                       // 源分辨率scaledYPlane.data(), anwBuffer->width,     // 目标YscaledUPlane.data(), anwBuffer->width/2,   // 目标UscaledVPlane.data(), anwBuffer->width/2,   // 目标VanwBuffer->width, anwBuffer->height,       // 目标分辨率libyuv::kFilterBox                         // 高质量缩放);// 替换画面libyuv::I420ToNV12(scaledYPlane.data(),                // 源Y平面指针anwBuffer->width,                   // 源Y stride (I420通常连续存储,stride=width)scaledUPlane.data(),                // 源U平面指针anwBuffer->width / 2,               // 源U stride (I420 U/V宽度是Y的一半)scaledVPlane.data(),                // 源V平面指针anwBuffer->width / 2,               // 源V stride (同U)static_cast<uint8_t*>(ycbcr.y),     // 目标Y平面ycbcr.ystride,                      // 目标Y stridestatic_cast<uint8_t*>(ycbcr.cb),    // 目标UV平面(NV12中cb指向UV交织数据)ycbcr.cstride,                      // 目标UV strideanwBuffer->width,                   // 图像宽度anwBuffer->height                   // 图像高度);gmapper.unlock(*buffer.buffer);}/******************************************************************* 【替换相机预览画面结束】******************************************************************/  ...return res;
}

4. 把摄像头预览画面替换为黑白图像(可选)


在调试期间,为了方便验证修改效果,我们可以通过下面的代码把摄像头预览画面替换为黑白图像

// Camera3OutputStream.cpp
...
status_t Camera3OutputStream::returnBufferCheckedLocked(const camera_stream_buffer &buffer,nsecs_t timestamp,nsecs_t readoutTimestamp,[[maybe_unused]] bool output,int32_t transform,const std::vector<size_t>& surface_ids,/*out*/sp<Fence> *releaseFenceOut) {...ANativeWindowBuffer *anwBuffer = container_of(buffer.buffer, ANativeWindowBuffer, handle);/******************************************************************* 【替换相机预览画面】******************************************************************/android_ycbcr ycbcr = {};GraphicBufferMapper &gmapper = GraphicBufferMapper::get();res = gmapper.lockYCbCr(*buffer.buffer,GRALLOC_USAGE_SW_WRITE_OFTEN,Rect(anwBuffer->width, anwBuffer->height),&ycbcr);gmapper.unlock(*buffer.buffer);if (res == OK) {ALOGD("YCbCr lock success. w=%d, h=%d, y=%p, cb=%p, cr=%p, ystride=%zu, cstride=%zu",anwBuffer->width, anwBuffer->height, ycbcr.y, ycbcr.cb, ycbcr.cr, ycbcr.ystride, ycbcr.cstride);uint8_t* dstUV = static_cast<uint8_t*>(ycbcr.cb);int uvWidth = anwBuffer->width / 2;int uvHeight = anwBuffer->height / 2;// 把UV分量设置为0x80(128)把画面变为黑白for (int y = 0; y < uvHeight; y++) {// 复制有效数据for (int x = 0; x < uvWidth; x++) {size_t dstPos = y * ycbcr.cstride + 2 * x;dstUV[dstPos] = 0x80;     // UdstUV[dstPos + 1] = 0x80; // V}}gmapper.unlock(*buffer.buffer);}/******************************************************************* 【替换相机预览画面结束】******************************************************************/  ...return res;
}

五、刷机测试

1. 整包刷机测试


参考之前的文章,执行 source build/envsetup.shbrunch lemonade 命令,编译生成 ./out/target/product/lemonade/lineage-22.2-2025XXXX-UNOFFICIAL-lemonade.zip 刷机包,刷入手机即可

特别说明:如需降级刷机,需要先重刷dtbo、vbmeta、vendor_boot及boot降低安卓安全版本后才可正常刷入

2. 单文件编译替换测试


执行 mmm frameworks/av/camera/cameraserver 命令重新编译生成 ./out/target/product/lemonade/system/bin/cameraserver 可执行文件后,通过带有root权限的ADB控制台,执行 killall cameraserver && cp /sdcard/cameraserver /system/bin/ 命令替换即可

特别说明:旧版安卓中替换的文件是lib文件,而高版本安卓中需要替换的则是bin文件

完整代码下载

Camera3OutputStream.cpp等5个文件 (安卓15实现虚拟摄像头) - 夸克网盘https://pan.quark.cn/s/4b3aeb622ae0

总结

作者因为很害怕,所以这里并没有对文章进行总结,但贴了一张Hanser的壁纸XD


文章转载自:

http://hJKXhMUL.bpddc.cn
http://S5NiDWOq.bpddc.cn
http://8bucOy5b.bpddc.cn
http://9w8OWBPL.bpddc.cn
http://6cUzRD8e.bpddc.cn
http://VrHIe2W6.bpddc.cn
http://ABsbKT86.bpddc.cn
http://rWqhEIJD.bpddc.cn
http://j0wp1VEC.bpddc.cn
http://EMCzMpq1.bpddc.cn
http://MIBjrydV.bpddc.cn
http://26nULd44.bpddc.cn
http://wuxeu7AQ.bpddc.cn
http://NB4smFYt.bpddc.cn
http://QVFDW5j4.bpddc.cn
http://2H97Xp0r.bpddc.cn
http://UfRes8US.bpddc.cn
http://CC32RYnk.bpddc.cn
http://GKKbFjo4.bpddc.cn
http://RZkgVYJ6.bpddc.cn
http://nEIStVvd.bpddc.cn
http://KPBPU0Ws.bpddc.cn
http://E9uzd5s5.bpddc.cn
http://AFIBAXLx.bpddc.cn
http://WTg3f1ZP.bpddc.cn
http://K6XeMlvq.bpddc.cn
http://OG99DJHJ.bpddc.cn
http://90V0AYzu.bpddc.cn
http://6V9kAZXA.bpddc.cn
http://1YEx1TPz.bpddc.cn
http://www.dtcms.com/a/373606.html

相关文章:

  • FxSound:提升音频体验,让音乐更动听
  • Don‘t Sleep:保持电脑唤醒,确保任务不间断
  • android/java中,配置更改导致activity销毁重建的解决方法
  • C++day8作业
  • 【CI/CD】GitHub Actions 快速入门
  • 如何在安卓手机/平板上找到下载文件?
  • Claude Code Windows 原生版安装指南
  • AR技术:多行业数字化转型的加速引擎
  • C++初阶(4)类和对象(上)
  • SpringAI企业级应用开发面试全流程解析:核心技术、架构落地与业务场景实战
  • 从旋转位置编码RoPE到YaRN的原理与实现
  • xfs inode cluster lock order导致的死锁
  • @PostMapping 是什么
  • Vue笔记2+3
  • Android 倒车影像
  • 哈希表-49.字母异位词分组-力扣(LeetCode)
  • JLINK 调试器单步调试单片机
  • AWS TechFest 2025: 智能体企业级开发流程、Strands Agents
  • Cy3-Tyramide,Cyanine 3 Tyramide; 174961-75-2
  • Neural Jacobian Field学习笔记 - jaxtyping
  • 从0到1学习Vue框架Day02
  • 人工智能学习:Transformer结构(编码器及其掩码张量)
  • ThreeJS骨骼示例
  • 网络工程师软考:网络自动化与可编程网络深度解析
  • 天工开物:耐达讯自动化RS232转ProfiBus网关连接变频器的“重生“术
  • WPF资源字典合并报错
  • DevExpress WPF 中文教程:如何将 WPF 数据网格绑定虚拟数据源?
  • TypeORM 入门教程:@ManyToOne 与 @OneToMany 关系详解
  • 开关电源基础知识
  • C++-RAII