【Android虚拟摄像头】四、用本地JPG图片替换相机预览画面

目录

前情提要

本篇目标 

一、 解析JPEG文件头

1. 提供尺寸合适的JPG图片文件

2. 编写代码解析JPEG文件头

3. 解决turbojpeg链接报错

4. 读取JPEG文件头

二、JPG图片转换为YUV格式保存到手机

1. 编写格式转换代码

2.  添加YUV文件保存代码

3. 补充SELinux权限

4. 测试YUV文件

三、用图片替换相机预览画面

1. 编写替换代码

2. UV平面数据格式说明

3. 测试替换效果

完整代码下载 

总结


前情提要

通过上一篇文章中对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-turbolibjpeg 的基础上提供了更好的编解码性能,处理速度更快,其中的函数通常以 tjXXX 命名

打开 libjpeg-turbo 目录下的 Android.bp文件,看到如下内容:

// ~/android/lineage/external/libjpeg-turbo/Android.bp
...
cc_defaults {
    name: "libjpeg-defaults",
    cflags: [...],
    srcs: [...],
    arch: {...},
    target: {...},
}
...

我们在 cflagssrcs 块中添加如下内容增加对 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

按错误提示依次为文件夹添加 writeadd_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

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值