目录
前情提要
通过上一篇文章中对SELinux访问控制权限的修改,我们成功在相机服务中读取了到本地JPG图片文件,获取到了文件字节数,同时也通过日志看到相机APP默认预览画面的尺寸为1440x1080
本篇目标
修改一加5T手机Framework层源码,用本地JPG图片替换相机APP预览画面
在后面的文章中会不断深入修改,实现虚拟摄像头,并完成DY、ZFB等软件的刷脸验证
一、 解析JPEG文件头
1. 提供尺寸合适的JPG图片文件
通过上一篇文章我们了解到,相机APP默认预览画面的尺寸是 1440x1080,我们把 /sdcard/1.jpg 修改为相同的尺寸

2. 编写代码解析JPEG文件头
在 loadJPG 函数中编写代码
// Camera3Device.cpp
#include <turbojpeg.h>
void loadJPG() {
...
int width, height, subsamp, colorspace;
tjhandle tjInstance = tjInitDecompress();
if (tjDecompressHeader3(tjInstance, jpegData.data(), jpegData.size(),
&width, &height, &subsamp, &colorspace) != 0) {
ALOGE("JPEG header error: %s", tjGetErrorStr());
return;
}
ALOGE("JPEG header success, width: %d, height: %d", width, height);
}
编译测试,在报错信息中看到 tjInitDecompress 等函数未定义,导致链接失败

该函数来自头文件 turbojpeg.h,文件目录位置在:
~/android/lineage/external/libjpeg-turbo/
我们在当前代码文件顶部已经进行了导入,但依旧提示链接时未找到函数定义
3. 解决turbojpeg链接报错
libjpeg 和 libjpeg-turbo 是两个常见的JPEG处理库,libjpeg-turbo 在 libjpeg 的基础上提供了更好的编解码性能,处理速度更快,其中的函数通常以 tjXXX 命名
打开 libjpeg-turbo 目录下的 Android.bp文件,看到如下内容:
// ~/android/lineage/external/libjpeg-turbo/Android.bp
...
cc_defaults {
name: "libjpeg-defaults",
cflags: [...],
srcs: [...],
arch: {...},
target: {...},
}
...
我们在 cflags 和 srcs 块中添加如下内容增加对 turbojpeg 相关函数的支持:
// ~/android/lineage/external/libjpeg-turbo/Android.bp
...
cc_defaults {
name: "libjpeg-defaults",
cflags: [
...
"-DBMP_SUPPORTED",
"-DPPM_SUPPORTED",
],
srcs: [
...
"jdatadst-tj.c",
"jdatasrc-tj.c",
"transupp.c",
"turbojpeg.c",
"rdbmp.c",
"rdppm.c",
"wrbmp.c",
"wrppm.c",
],
arch: {...},
target: {...},
}
...
重新执行 mmm 命令编译 libcameraservice 模块,编译通过
但替换 libcameraservice.so 文件后,测试发现相机APP闪退,通过ADB执行命令查看日志
logcat |grep -i camera
看到如下报错信息:
07-24 15:13:04.259 1482 1671 E CameraManagerGlobal: Camera service is unavailable
07-24 15:13:05.033 4488 4488 F linker : CANNOT LINK EXECUTABLE "/system/bin/cameraserver": cannot locate symbol "tjInitDecompress" referenced by "/system/lib/libcameraservice.so"...
07-24 15:13:05.037 1 1 W libc : Unable to set property "ro.init.updatable_crashing_process_name" to "cameraserver": error code: 0xb

这个错误表明,libcameraservice.so 动态库尝试调用 tjInitDecompress 函数,但系统找不到该符号
我们刚才修改 libjpeg-turbo 库的编译配置文件,重新编译生成 libcameraservice.so 动态库时,libjpeg.so 动态库也更新了,我们需要执行下面的命令,把它也替换到手机里
# PC
adb push .\libjpeg.so /data/local/tmp
adb shell
# ADB
su
remount
cd /data/local/tmp
cp libjpeg.so /system/lib/
4. 读取JPEG文件头
完成文件替换后,再次打开相机,在日志中已成功解析JPEG文件头,获取到了图片的宽高信息

二、JPG图片转换为YUV格式保存到手机
1. 编写格式转换代码
编写代码,完成 JPEG >> BGR24 >> YUVI420 的转换
JPEG (压缩格式)
BGR24 (记录每个像素点的RGB色彩,width*height*3)
YUVI420 (分别记录每个像素点的亮度Y、每4个像素点的Cr红偏移和Cb蓝偏移,width*height*1.5)
// Camera3Device.cpp
class ImageReplacer {
private:
std::vector<uint8_t> jpegData;
std::vector<uint8_t> yPlane;
std::vector<uint8_t> uPlane;
std::vector<uint8_t> vPlane;
public:
...
void loadJPG() {
...
// 1. 解码JPEG头获取尺寸
if (tjDecompressHeader3(tjInstance, jpegData.data(), jpegData.size(),
&width, &height, &subsamp, &colorspace) != 0) {
ALOGE("JPEG header error: %s", tjGetErrorStr());
return;
}
ALOGE("JPEG header success, width: %d, height: %d", width, height);
// 2. 分配RGB缓冲区
std::vector<uint8_t> rgbBuf(width * height * 3); // BGR24格式需要 width*height*3
ALOGE("Allocated BGR buffer: %zu bytes (width=%d, height=%d)", rgbBuf.size(), width, height);
// 3. 解码JPEG到RGB(使用TJPF_BGR格式)
if (tjDecompress2(tjInstance,
jpegData.data(), jpegData.size(),
rgbBuf.data(),
width, // 输出图像宽度
width * 3, // 输出行字节数(BGR24的stride=width*3)
height,
TJPF_BGR, // 像素格式:BGR顺序
0 // 标志位(无特殊处理)
) != 0) {
ALOGE("JPEG to RGB error: %s", tjGetErrorStr());
return;
}
// 4. 转换RGB到YUV(使用libyuv)
ALOGE("RGB to YUV");
yPlane.resize(width * height);
uPlane.resize(((width + 1)/2) * ((height + 1)/2));
vPlane.resize(((width + 1)/2) * ((height + 1)/2));
libyuv::RGB24ToI420(
rgbBuf.data(), width * 3, // RGB源数据和stride
yPlane.data(), width, // Y平面
uPlane.data(), (width + 1)/2, // U平面
vPlane.data(), (width + 1)/2, // V平面
width, height
);
}
...
};
2. 添加YUV文件保存代码
转换完成后,保存到 /sdcard/1.yuv 文件
// Camera3Device.cpp
class ImageReplacer {
...
public:
...
void loadJPG() {
...
// 5. 保存YUV文件
FILE* fp = fopen("/sdcard/1.yuv", "wb");
if (fp) {
// 写入Y平面
fwrite(yPlane.data(), 1, yPlane.size(), fp);
// 写入U平面
fwrite(uPlane.data(), 1, uPlane.size(), fp);
// 写入V平面
fwrite(vPlane.data(), 1, vPlane.size(), fp);
fclose(fp);
ALOGE("YUV planes saved to 1.yuv");
} else {
ALOGE("Failed to save YUV planes: %s", strerror(errno));
}
}
...
};
3. 补充SELinux权限
执行命令完成编译,更新 libcameraservice.so 动态库到手机后,打开相机,看到如下日志:

执行命令查看 AVC 日志
su
dmesg |grep avc |grep cameraserver
看到缺少对文件夹的写入权限
[ 2752.414283] [20250725_15:23:37.262946]@1 type=1400 audit(1753496617.258:194): avc: denied { write } for comm="cameraserver" name="0" dev="sdcardfs" ino=3194883 scontext=u:r:cameraserver:s0 tcontext=u:object_r:sdcardfs:s0 tclass=dir permissive=0
打开SELinux配置文件 cameraserver.te
~/android/lineage/device/oneplus/msm8998-common/sepolicy/vendor/cameraserver.te
按错误提示依次为文件夹添加 write 和 add_name 权限
// cameraserver.te
...
allow cameraserver storage_file:dir { search getattr open read write add_name };
allow cameraserver storage_file:file { create read write open getattr };
allow cameraserver storage_file:lnk_file read;
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 };
执行命令重新编译刷机包,刷机测试
cd ~/android/lineage
source build/envsetup.sh
breakfast dumpling
brunch dumpling
4. 测试YUV文件
打开相机,看到如下日志,即说明YUV文件生成成功,大小为 4665600 字节

拷贝 /sdcard/1.yuv 文件到电脑,执行 ffplay 命令预览测试
ffplay -f rawvideo -pixel_format yuv420p -video_size 1440x1080 1.yuv
成功显示YUV格式图片

三、用图片替换相机预览画面
1. 编写替换代码
在 replaceYUVBuffer 函数中编写如下代码,替换 ycbcr 中的预览画面
// Camera3Device.cpp
class ImageReplacer {
...
public:
...
void replaceYUVBuffer(const android_ycbcr &ycbcr, uint32_t srcWidth, uint32_t srcHeight) {
// 1. 处理Y平面(每次复制一行,考虑stride)
uint8_t* dstY = static_cast<uint8_t*>(ycbcr.y);
for (int y = 0; y < (int)srcHeight; y++) {
memcpy(dstY + y * ycbcr.ystride, yPlane.data() + y * srcWidth, static_cast<size_t>(srcWidth));
}
// 2. 处理UV平面(NV12)
uint8_t* dstUV = static_cast<uint8_t*>(ycbcr.cb);
int uvWidth = (srcWidth + 1) / 2;
int uvHeight = (srcHeight + 1) / 2;
for (int y = 0; y < uvHeight; y++) {
// 复制有效数据
for (int x = 0; x < uvWidth; x++) {
size_t dstPos = y * ycbcr.cstride + 2 * x;
dstUV[dstPos] = uPlane[y * uvWidth + x]; // U
dstUV[dstPos + 1] = vPlane[y * uvWidth + x]; // V
}
// 填充行末padding(如有)
if (ycbcr.cstride > static_cast<size_t>(srcWidth)) {
size_t padStart = y * ycbcr.cstride + srcWidth;
memset(dstUV + padStart, 128, ycbcr.cstride - srcWidth);
}
}
}
};
在上一篇中,我们在 Camera3Device::returnOutputBuffers 函数中调用了该函数,应用层 每次从 Framework层 获取摄像头预览画面时,会被替换为我们提供的画面
2. UV平面数据格式说明
高通设备通常采用 NV12 作为默认内存排列格式,具体的格式确认方式、各格式数据处理方式会在本系列完结后单开一篇介绍
3. 测试替换效果
编译文件,替换到手机后,打开相机APP,我们看到预览画面已经被成功替换为我们提供的JPG图片

完整代码下载
Camer3Device.cpp、cameraserver.te等4个文件 (用本地JPG图片替换相机预览画面) - 夸克网盘
https://pan.quark.cn/s/9464c601391c
总结
作者因为很害怕,所以这里并没有对文章进行总结,但贴了一张Hanser的壁纸XD
1万+

被折叠的 条评论
为什么被折叠?



