jjzjj

虚拟摄像头之二: 配置v4l2loopback虚拟摄像头为前置或后置摄像头

老理说的好 2023-04-29 原文

前言

在android8.1系统中调用摄像头是通过 CameraManager::getCameraIdList() 方法获取系统摄像头列表,从列表中选择满足需要摄像头,用以拍照、录像或全景拍照。
上篇中以介绍如何把v4l2loopback移植到android内核,本章介绍如何配置虚拟摄像头参数,以满足android用户程序直接使用。

谁在读取camera配置信息

首先,我们看一下 class CameraBase 这个类的定义,文件路径:
@frameworks/av/camera/include/camera/CameraBase.h

class CameraBase : public IBinder::DeathRecipient
{
public:
	static status_t      getCameraInfo(int cameraId,
                                       /*out*/
                                       struct hardware::CameraInfo* cameraInfo);

    sp<TCamUser>         remote();

    // Status is set to 'UNKNOWN_ERROR' after successful (re)connection
    status_t             getStatus();
protected:
    Mutex                            mLock;
    // helper function to obtain camera service handle
    static const sp<::android::hardware::ICameraService> getCameraService();

    sp<TCamUser>                     mCamera;
    status_t                         mStatus;

    sp<TCamListener>                 mListener;

    const int                        mCameraId;

    typedef CameraBase<TCam>         CameraBaseT;    
}

基类中 getCameraInfo() 是获取摄像头配置信息,其实现方法如下:
@frameworks/av/camera/CameraBase.cpp

// this can be in BaseCamera but it should be an instance method
template <typename TCam, typename TCamTraits>
status_t CameraBase<TCam, TCamTraits>::getCameraInfo(int cameraId,
        struct hardware::CameraInfo* cameraInfo) {
    const sp<::android::hardware::ICameraService> cs = getCameraService();
    if (cs == 0) return UNKNOWN_ERROR;
    binder::Status res = cs->getCameraInfo(cameraId, cameraInfo);
    return res.isOk() ? OK : res.serviceSpecificErrorCode();
}
template class CameraBase<Camera>;

因为camera在android系统中是采用C/S结构,/system/bin/mediaserverr是服务端守护进程,frameworks/av/camera下实现的是client内容,
此方法是通过 getCameraService() 获取 sp<::android::hardware::ICameraService> cs 智能指针,从而调用服务端的
cs->getCameraInfo(cameraId, cameraInfo) 的方法,我们进一步跟踪。

camera 服务端守护进程

文件路径
@frameworks/av/services/camera/libcameraservice/CameraService.cpp

Status CameraService::getCameraInfo(int cameraId,
        CameraInfo* cameraInfo) {
    ATRACE_CALL();
    Mutex::Autolock l(mServiceLock);

    if (!mInitialized) {
        return STATUS_ERROR(ERROR_DISCONNECTED,
                "Camera subsystem is not available");
    }

    if (cameraId < 0 || cameraId >= mNumberOfCameras) {
        return STATUS_ERROR(ERROR_ILLEGAL_ARGUMENT,
                "CameraId is not valid");
    }

    Status ret = Status::ok();
    status_t err = mCameraProviderManager->getCameraInfo(std::to_string(cameraId), cameraInfo);
    if (err != OK) {
        ret = STATUS_ERROR_FMT(ERROR_INVALID_OPERATION,
                "Error retrieving camera info from device %d: %s (%d)", cameraId,
                strerror(-err), err);
    }
    return ret;
}

通过跟踪源码、发现摄像属性参数是在CameraHAL.cpp文件中,构造摄像头时配置 Facing和Orientation属性值,
源码路径如下:
@vendor/nxp-opensource/imx/libcamera3/CameraHAL.cpp文件中,笔者使用的NXP的android8.1软件系统,
硬件平台IMX8QM。以此类推,摄像头属性是在摄像头HAL层构建设备时通过源码类配置该属性。

CameraHAL 配置参数分析

因此参数因硬件平台的差异,配置文件会有差异,请读者根据自有硬件平台代码追踪此过程。
@vendor/nxp-opensource/imx/libcamera3/CameraHAL.cpp

CameraHAL::CameraHAL()
  : mCameraCount(0),
    mCallbacks(NULL)
{
    // Allocate camera array and instantiate camera devices
    mCameras = new Camera*[MAX_CAMERAS];
    memset(mSets, 0, sizeof(mSets));
    memset(mCameras, 0, MAX_CAMERAS * sizeof(Camera*));

    // enumerate all camera sensors.
    enumSensorSet();                  //> 配置 camera 的参数子函数

    // check if camera exists.
    for (int32_t index=0; index<MAX_CAMERAS; index++) {
        if (!mSets[index].mExisting) {
            continue;
        }
        mCameras[index] = Camera::createCamera(index, mSets[index].mSensorName,
                                mSets[index].mFacing, mSets[index].mOrientation,
                                mSets[index].mDevPath);

        if (mCameras[index] == NULL) {
            // camera sensor is not supported now.
            // So, camera count should not change.
            ALOGW("Error: camera:%d, %s create failed", index,
                                 mSets[index].mSensorName);
        }
        else {
            mCameraCount++;
        }
    }
    ALOGI("camera number is %d", mCameraCount);

    mHotplugThread = new HotplugThread(this);
}

//> 配置 camera 的参数子函数
void CameraHAL::enumSensorSet()
{
    // get property from init.rc mSets.
    char orientStr[CAMERA_SENSOR_LENGTH];

    ALOGI("%s", __func__);
    // get back camera property.
    memset(orientStr, 0, sizeof(orientStr));
    property_get("back_camera_name", mSets[BACK_CAMERA_ID].mPropertyName, "0");
    property_get("back_camera_orient", orientStr, "0");
    mSets[BACK_CAMERA_ID].mOrientation = atoi(orientStr);
    mSets[BACK_CAMERA_ID].mFacing = CAMERA_FACING_BACK;
    mSets[BACK_CAMERA_ID].mExisting = false;

    // get front camera property.
    memset(orientStr, 0, sizeof(orientStr));
    property_get("front_camera_name", mSets[FRONT_CAMERA_ID].mPropertyName, "0");
    property_get("front_camera_orient", orientStr, "0");
    mSets[FRONT_CAMERA_ID].mOrientation = atoi(orientStr);
    mSets[FRONT_CAMERA_ID].mFacing = CAMERA_FACING_FRONT;
    mSets[FRONT_CAMERA_ID].mExisting = false;

    // make sure of back&front camera parameters.
    matchDevNodes();
}

配置摄像头前置或后置

通过源码分析、camera的参数配置是通过property设置来管理,此参数配置是在与硬件平台相关的 init.rc 中
进行配置,IMX8QM的摄像配置参数如下:

@device/fsl/imx8qm/mek_8q/init.rc 文件

    #Define the config for dual camera
    #For landscape mode, orient is 0
    #For portrait mode, orient is 90
    #the android before honycomb are all in portrait mode
    setprop camera.disable_zsl_mode 1
    # 后置摄像头
    setprop back_camera_name imx8_ov5640_mipi,max9286_mipi,imx8_ov5640
    setprop back_camera_orient 0
    # 前置摄像头
    setprop front_camera_name imx8_ov5640,imx8_ov5640_mipi,uvc
    setprop front_camera_orient 0

我们只需要把此摄像头名称修改为 v4l2loopback_back 和 v4l2loopback_front 名称,在构建 CameraHAL 库
时,采用此名称就可以标注出并区别开来。

NXP是如何匹配 Camera 节点属性

int32_t CameraHAL::matchDevNodes()
{
    DIR *vidDir = NULL;
    struct dirent *dirEntry;
    size_t nameLen = CAMERA_SENSOR_LENGTH - 1;
    nodeSet *nodes = NULL, *node = NULL, *last = NULL;

    ALOGI("%s", __func__);
    vidDir = opendir("/sys/class/video4linux");
    if (vidDir == NULL) {
        return -1;
    }

    while ((dirEntry = readdir(vidDir)) != NULL) {
        if (strncmp(dirEntry->d_name, "video", 5)) {
            continue;
        }

        node = (nodeSet*)malloc(sizeof(nodeSet));
        if (node == NULL) {
            ALOGE("%s malloc failed", __func__);
            break;
        }

        memset(node, 0, sizeof(nodeSet));
        if (nodes == NULL) {
            nodes = node;
        }
        else {
            last->next = node;
        }
        last = node;
        sprintf(node->devNode, "/dev/%s", dirEntry->d_name);
        getNodeName(node->devNode, node->nodeName, nameLen);
    }

    closedir(vidDir);

    for (int32_t index=0; index<MAX_CAMERAS; index++) {
        matchPropertyName(nodes, index);
    }

    node = nodes;
    while (node != NULL) {
        last = node->next;
        free(node);
        node = last;
    }

    return 0;
}

我们看一下 sys/class/video4linux 路径下都有哪些摄像头设备

1|mek_8q:/ # ls sys/class/video4linux/                                                                                                               
video0 video1 video12 video13 video2 video3 video4 video5 

获取摄像头名称

int32_t CameraHAL::getNodeName(const char* devNode, char name[], size_t length)
{
    int32_t ret = -1;
    int32_t fd = -1;
    size_t strLen = 0;
    struct v4l2_capability vidCap;
    struct v4l2_dbg_chip_ident vidChip;

    ALOGI("getNodeName: dev path:%s", devNode);
    if ((fd = open(devNode, O_RDWR, O_NONBLOCK)) < 0) {
        ALOGW("%s open dev path:%s failed:%s", __func__, devNode,
                strerror(errno));
        return ret;
    }

    ret = ioctl(fd, VIDIOC_QUERYCAP, &vidCap);
    if (ret < 0) {
        ALOGW("%s QUERYCAP dev path:%s failed", __func__, devNode);
        close(fd);
        fd = -1;
        return ret;
    }

    if (!(vidCap.capabilities &
          (V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_VIDEO_CAPTURE_MPLANE))) {
        ALOGW("%s dev path:%s is not capture", __func__, devNode);
        close(fd);
        fd = -1;
        ret = -1;
        return ret;
    }

    strncat(name, (const char*)vidCap.driver, length);
    strLen = strlen((const char*)vidCap.driver);
    length -= strLen;
    ALOGI("getNodeName: node name:%s", name);

    ret = ioctl(fd, VIDIOC_DBG_G_CHIP_IDENT, &vidChip);
    if (ret < 0) {
        ALOGI("%s CHIP_IDENT dev path:%s failed", __func__, devNode);
        strncat(name, ",", length);
        strLen = 1;
        length -= strLen;
        strncat(name, (const char*)vidCap.card, length);
        ALOGI("getNodeNames: node name:%s", name);
        close(fd);
        fd = -1;
        return ret;
    }

    strncat(name, ",", length);
    strLen = 1;
    length -= strLen;
    strncat(name, vidChip.match.name, length);

    ALOGI("getNodeNames: node name:%s", name);
    close(fd);
    return ret;
}

摄像头名称属性是在驱动中标注的,需修改v4l2loopback驱动中节点名称,以此把该摄像头配置为
前置或后置摄像头。

至此位置,我们把 android8.1 中的、摄像头前置或后置的属性配置逻辑给梳理清晰了,如果感觉对你
有所启发或帮助,请点赞或关注,以资鼓励笔者,感谢喽。

有关虚拟摄像头之二: 配置v4l2loopback虚拟摄像头为前置或后置摄像头的更多相关文章

  1. 在VMware16虚拟机安装Ubuntu详细教程 - 2

    在VMware16.2.4安装Ubuntu一、安装VMware1.打开VMwareWorkstationPro官网,点击即可进入。2.进入后向下滑动找到Workstation16ProforWindows,点击立即下载。3.下载完成,文件大小615MB,如下图:4.鼠标右击,以管理员身份运行。5.点击下一步6.勾选条款,点击下一步7.先勾选,再点击下一步8.去掉勾选,点击下一步9.点击下一步10.点击安装11.点击许可证12.在百度上搜索VM16许可证,复制填入,然后点击输入即可,亲测有效。13.点击完成14.重启系统,点击是15.双击VMwareWorkstationPro图标,进入虚拟机主

  2. kvm虚拟机安装centos7基于ubuntu20.04系统 - 2

    需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc

  3. ruby-on-rails - Rails 验证虚拟属性 - 2

    我这个模型:classBunny每当我提交一个表单来创建这个模型时,我都会收到以下错误:#的未定义方法“number_before_type_cast” 最佳答案 我通过将此方法添加到我的Bunny模型中解决了这个问题:defnumber_before_type_castnumberend我不喜欢它,但我想在有人发布更好的解决方案之前它会起作用。 关于ruby-on-rails-Rails验证虚拟属性,我们在StackOverflow上找到一个类似的问题: h

  4. L2TP连接尝试失败,因为安全层在初始化与远程计算机的协商时遇到了一个处理错误 - 2

    废话不多先看bug解决方案在下面!!!!启动服务查看服务是否开启首先我的电脑-右键-管理-服务和应用程序-服务-找到IPsecPolicyAgent-右键属性-启动方式改为自动,并重启服务,如下图打开设置-更改适配器选项如下图点击连接失败的连接-右键-属性-安全-允许使用这些协议编辑注册表按Ctrl+R打开命令行窗口输入regedit打开注册表输入下面命令进入以下页面HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\RasMan\Parameters如下图在右侧编辑菜单上,鼠标右键新建,然后单击DWORD(32)位值。键入Prohibit

  5. 【云计算】私有云在VMware下虚拟机的创建与配置(图文教程) - 2

    【适用平台】私有云   说明:完成私有云部分是需要两台虚拟机的,分别为controller、compute两个节点,但我们只需配置一台,然后克隆就方便多啦!需要用到的映射文件:关于vm的安装我就不介绍的,毕竟挺简单的,下面让我们看看基于私有云模块中,虚拟机的搭建吧。1、创建新的虚拟机,这里一般我会选择自定义,毕竟后面的配置都要根据私有云相关来进行搭建,会比较复杂。(如果是基础的可以选择典型,典型的满足一般虚拟机的配置) 2、选择稍后安装操作系统会比较方便后续的选择,这里你也可以自己选择自己的映像文件(但不建议)  3、我们是基于Linux下操作的,所以选择Linux客户机操作系统,版本选择自己

  6. VMware虚拟机与本地主机进行磁盘共享(详解) - 2

    VMware虚拟机与本地主机进行磁盘共享前提虚拟机版本为Windows10(专业版,不是可能有问题)本地主机为家庭版或学生版(此版本会有问题,但有替代方式)最好是专业版VMware操作1.关闭防火墙,全部关闭。2.打开电脑属性3.点击共享-》高级共享-》权限4.如果没有everyone,就添加权限选择完全控制,然后应用确定。5.打开cmd输入lusrmgr.msc(只有专业版可以打开)如果不是专业版,可以跳过这一步。点击用户-》administrator密码要复杂密码,否则不行。推荐admaiN@1234类型的密码。设置完密码,点击属性,将禁用解开。6.如果虚拟机的windows不是专业版,可

  7. 虚拟机上进行java项目部署 - 2

    🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀虚拟机上进行java项目部署,自己的一点总结,一起学习,一起进步,一起成长!🛸🛸🛸🛸🛸🛸🛸🛸🛸🛸目录文章目录虚拟机上进行java项目部署1.JDK安装2.TOMCAT安装3.DOCKER1、YUM安装2、docker部署java4、最后 【yzh2022.9】1.JDK安装1、我们安装VM的时候,使用命令java-version查看 java-version这里显示JDK的信息是openjdkversion"1.8.0_262",我们会发现这个JDK是VM自带的当然你也可以通过命令rpm-qa|grepjava来查看相关的java信息 rpm-qa|grepjava【如果不

  8. 【操作系统实验】Ubuntu Linux 虚拟机用户管理 - 2

    文章目录一、用户二、用户分类1、普通用户2、超级用户3、系统用户三、用户相关文件1、/etc/passwd文件2、/etc/shadow文件四、用户管理命令1、useradd2、adduser3、passwd4、usermod5、userdel一、用户Linux系统是一个多用户多任务的分时操作系统,任何一个要使用系统资源的用户都必须先向系统管理员申请一个账号,然后以这个账号的身份进入系统。在Linux系统中,任何文件都属于某一特定用户,而任何用户都隶属于至少一个用户组。用户名(username):每个用户账号都拥有一个惟一的用户名和各自的口令。用户在登录时键入正确的用户名和口令后,就能够进入系

  9. Conda虚拟环境的复制和迁移 - 2

    Conda虚拟环境的复制和迁移在本机复制Conda虚拟环境condacreate--namesnapshot--clonemyenv相同操作系统之间复制环境方法一:requirements.txt这个方法不推荐,因为只会导出你使用pip安装的依赖包,不会导出虚拟环境所依赖的包,并不适用于虚拟环境的迁移的应用场景。事实上,此方法比较适用于,已经明确知道依赖哪些包,我们只需要package信息的情况,如写项目文档,告诉别人运行我这个系统必须安装哪些依赖包。而忽略虚拟环境本身的依赖环境。pipfreeze>requirements.txt#生成requirements.txtpipinstall-r

  10. ruby - 是否可以在 Vagrant 完成所有配置后在虚拟机上运行脚本? - 2

    我正在使用Vagrantv1.5.1创建虚拟机(VM)集群。在供应了所有VM之后,是否可以在其中一台机器上运行单个脚本?我要运行的脚本将设置从一个VM到所有其他VM的无密码SSH。例如我在Vagrant(CentOS6.5)中配置的节点如下。节点1节点2节点3节点4我的Vagrantfile如下所示。(1..4).eachdo|i|config.vm.define"node-#{i}"do|node|node.vm.box="centos65"...omitted..endend完成所有这些后,我需要在node1上运行一个脚本,以启用到node2、node3和node4的无密码SSH。

随机推荐