Windows远程桌面实现之十五:投射浏览器摄像头到xdisp_virt以及再次模拟摄像头(一)

by fanxiushu 2024-07-01
            转载或引用请注明原始作者。

本文还是围绕xdisp_virt这个软件展开,
再次模拟成摄像头这个比较好理解,早在很久前,其实xdisp_virt项目中就有摄像头功能,
只是当时是分开的,使用起来可能不是那么方便,这次打算集成到xdisp_virt程序中去。

至于投射浏览器摄像头到xdisp_virt,这是什么意思呢?
简单的说,我们打开任何设备的浏览器
(不管是手机,平板,比如iPhone,android手机等,还是PC电脑,不管是windows中,还是macOS,linux等)
打开各种现代浏览器浏览网页的时候,其实我们也可以在浏览器中打开本设备的摄像头以及麦克风等。
这个功能有些人可能用的很少使用甚至可能都没留意过。
投射浏览器摄像头到xdisp_virt的意思就是把浏览器中打开的摄像头图像等数据,发送到xdisp_virt程序中,
而xdisp_virt通过自身处理再次把浏览器打开的摄像头图像共享出去。
很像上文讲的通过 AirPlay协议把苹果设备的屏幕图像数据,发送到xdisp_virt一样。
只是它们的传输方式不一样,下面会慢慢阐述。

其实几个月前早在实现AirPlay的时候,就在考虑如何把iPhone等手机的摄像头传输到电脑的问题。
手机摄像头都是非常高清的,比起电脑自带的摄像头不知道好多少。
我也不想在手机上专门开发App来传输摄像头,以前移植到iOS的xdisp_virt也渐渐放弃,因为有AirPlay这样更好的替代,
手机摄像头也没有像AirPlay那样的iOS等系统内部自带的功能。
那现在唯一能做文章的就是手机的浏览器了。
不过当实现了浏览器上传摄像头之后,发现了一个缺陷:
iOS手机好像耗电比较快,尤其是设置高分辨率和高清晰度的时候,
后来分析是因为浏览器WebRTC编码H264视频采用的软编码,目前的各个平台的浏览器内部实现好像都是这么做的。
以后的文章还会阐述把xdisp_virt程序里边的视频流再次模拟成电脑的摄像头。

等于是实现如下的流程:
1,iOS,android或其他设备通过浏览器访问摄像头          ----》
2,通过WebRTC等方式传输浏览器的摄像头数据给xdisp_virt程序  ----》
3,xdisp_virt程序通过各种方式共享摄像头图像数据                ----》
4,xdisp_virt程序把传输上来的视频再次模拟成电脑摄像头
 (这里也不再单纯局限于浏览器摄像头数据,凡是xdisp_virt能处理的视频都可以模拟成电脑摄像头。)

为了不让下面的阐述显得枯燥,我们先看看下面的视频和展示图
(至于如何再次模拟成电脑摄像头,后面文章会有相应的演示,因为目前还在开发中。)
 

浏览器摄像头投射到xdisp_virt程序演示


配置界面:



现在我们来讲讲实现这么一个功能的大致原理和流程:
首先,我们要访问浏览器的摄像头,肯定得使用JavaScript脚本语言,
同时WebRTC组件是我们能在浏览器中正常访问摄像头的基础组件,
现代浏览器都集成了WebRTC,,要正常使用WebRTC,我们还得需要WebSocket来传输信令数据。
目前的浏览器内核都支持这些功能,因此都不用担心,除非是那种很老的浏览器。
我们在js中,使用 getUserMedia 接口函数来访问摄像头和麦克风,
因为之前各种浏览器各自为战的问题,这个函数曾经有多种不同的调用方式,
但在新版本浏览器中都做了最终的统一:navigator.mediaDevices.getUserMedia
因此本文也是采用这种统一的调用方式。
从上面的配置页面可以看出xdisp_virt程序是需要列出浏览器所在设备的所有摄像头和麦克风的,而不是采用默认。
下面js脚本实现的就是查询所有的设备:
///获取所有设备
function enum_all_devices(is_audio, func )
{
    try{
        navigator.mediaDevices.getUserMedia({ audio: is_audio, video: true }).then(stream => {
            //调用 getUserMedia 目的是在浏览器中列举出全部的摄像头和音频     
            // audio 和video都设置true,这样才能全部查询到
            AFAICT in Safari this only gets default devices until getUserMedia is called :/
            navigator.mediaDevices.enumerateDevices().then(function (devices) {
                /在enumerateDevices列举之后关闭,这样Firefox中label才不会为空。
                stream.getTracks().forEach(track => {
                    track.stop();
                });
                console.log(devices);          
                func(devices, null); func
                ///
            });
            /
        }).catch(function (err) {
           
            func(null, err);
        });

    } catch (err) {
        func(null, err);
    }
}

在调用 enumerateDevices 查询所有设备之前,需要调用 getUserMedia 打开摄像头和麦克风设备,
这是比较奇特的事,但是不调用的话,浏览器基本无法查询到正确的所有摄像头和麦克风。
如下调用上面的查询函数:
enum_all_devices(true, function(devices, err){
         if(err){ return; }
         var dev = devices[i];
         if (dev['kind'] == 'videoinput') { /// 查询到摄像头
              var deviceId= dev['deviceId']; //这个deviceId,用来getUserMedia 打开指定的摄像头
              。。。
         }
         else if(dev['kind'] == 'audioinput'){//查询到麦克风
            var deviceId= dev['deviceId']; //这个deviceId,用来getUserMedia 打开指定的麦克风
              。。。
         }
});

通过上面的设备查询过程,假设我们准备打开指定的摄像头和麦克风,
假设对应的deviceId是 cameraId和microPhoneId,
navigator.mediaDevices.getUserMedia({
                    audio: { deviceId: microPhoneId } ,
                    video: {
                        width: {ideal:width} , //建议的摄像头宽度,比如1920
                        height:{ideal:height}, //建议的摄像头高度,比如1080
                        deviceId:cameraId
                    }
            }).then(stream => {
                 ///  正确的打开了摄像头和麦克风,同时生成了 stream 流,
                   现在我们就是需要通过WebRTC接口,把stream流上传到xdisp_virt程序
                 camera_webrtc_create(..., stream, ...);  创建WebRTC,并且上传 stream 流
                 ......
                
            }).catch(function (err) {
                   //打开异常,也就是无法正确打开设备
            });

下面是 camera_webrtc_create 大致伪代码,
因为这其中与xdisp_virt程序通过WebSocket通讯,并且有多次的信令交互,所以具体处理起来还是比较麻烦。
(当然比起xdisp_virt程序的C/C++代码中的处理过程,浏览器端js的处理代码还是挺简单)
所以伪代码中,只是大致描述一下流程。
function logError(err) {
    ///发生错误
}
function camera_webrtc_create(..., stream, .....)
{
       。。。与xdisp_virt服务端初次交互,获取一些相关信息,比如获取 iceServers 信息,
      iceServers用于创建WebRTC提供服务器地址等。
      同时从 xdisp_virt 服务端获取到WebRTC的OfferSdp描述符,
      因为本文的实现中,xdisp_virt服务端主动提供offserSdp, 浏览器javascript端被动接受

    然后就是创建 WebRTC
    pc = new RTCPeerConnection(iceServers);    //创建 WebRTC连接
    
    设置服务端传来的offerSdp
    pc.setRemoteDescription(
        new RTCSessionDescription(offserSdp)).then(() => {
             /  把流添加到 WebRTC连接中,
             /  让浏览器的WebRTC把摄像头图像数据流和麦克风音频数据流编码之后上传到xdisp_virt端。
             stream.getTracks().forEach((track) => {//
                    var sender = pc.addTrack(track, stream); 
             });

             //
             ///创建 answerSdp, 并且上传answerSdp和设置到本地WebRTC中。
            pc.createAnswer().then(desc=> {

                console.log('local desc=' + JSON.stringify(desc));

                /// send answer to server
                send_answer_sdp(desc); // 这个函数通过WebSocket把answerSdp发送给xdisp_virt

                set local desc
                pc.setLocalDescription(desc).catch(logError); //设置到本地WebRTC中

            }).catch(logError);

        }).catch(logError);

    ///
    /// 处理 WebRTC的 event
    pc.signalingstatechange = () => {
        console.log('Signaling state:', pc.signalingState);
    };
    pc.oniceconnectionstatechange = () => {
        console.log('ICE connection state:', pc.iceConnectionState);
        switch (pc.iceConnectionState) {
            case 'disconnected':
            case 'failed':
            case 'closed':
                重新连接
                retry_connect_webrtc();
                break;
        }
    };
    pc.onicegatheringstatechange = () => {
        console.log('ICE gathering state:', pc.iceGatheringState);
        switch(pc.iceGatheringState){
            case 'complete':
               /
                break;
        }
    };
    pc.onconnectionstatechange = () => {
        console.log('Connection state:', pc.connectionState);
        switch (pc.connectionState) {
            case 'disconnected':
            case 'failed':
            case 'closed':
                重新连接
                retry_connect_webrtc();
                break;

            case 'connected':
               
                 // alert('connected')
                break;
        }
    };
    pc.onicecandidate = (event) => {
        console.log('ice candidate=' + JSON.stringify(event.candidate));
        // 通过webSocket把candidate发送给xdisp_virt服务端
        // 整个过程中还包括从xdisp_virt端接收candidate,然后调用addIceCandidate 设置到本地webRTC中
        if (event.candidate) send_ice_candidate(event.candidate);
    };
    pc.onicecandidateerror = (event) => {
        console.error('ICE candidate error:', event);
    };

    ............................
}

以上基本上是javascript中标准的创建WebRTC的过程,
当然其中会穿插信令传输,信令传输则是采用自己定义的协议即可。

因为首先是xdisp_virt的WebRTC端提供OffsetSdp,
并且在 OfferSdp 中固定视频编码为 H264, 音频编码固定为OPUS。

至此,浏览器端处理的核心部分就算完成,接下来则是服务端xdisp_virt的事情。
浏览器是什么都封装了,使用 javascript 脚本压根无法访问具体的数据。
但是xdisp_virt需要处理具体的数据流,
要做到这点,肯定得自己开发实现WebRTC,当然最好使用开源的。
xdisp_virt使用的亚马逊的kvswebrtc, 很早前的文章就曾经讲述过,
有兴趣可以去查阅,这里不再赘述。

xdisp_virt从kvswebrtc获取到H264视频码流和OPUS音频码流,
然后就是解码,解码成RGBA原始图像数据流和PCM音频数据流,
然后再根据具体配置,再编码成其他编码传输出去。

之所以要解码然后再编码这么麻烦,浪费CPU等资源,
是因为xdisp_virt需要做其他各种处理,
一个最简单的原因,xdisp_virt通过各种途径共享图像,
每个xdisp_virt客户端连上来都需要xdisp_virt立即刷出关键帧,
否则客户端老半天都出不来图像。
xdisp_virt也无法通知上传摄像头图像数据的浏览器的WebRTC刷出关键帧,因为javascript的WebRTC没这个接口。
同样的,前面阐述的AirPlay协议上传苹果设备的屏幕镜像则更夸张,
整个AirPlay连接,就只有第一个数据是关键帧,之后都不会出现关键帧,除非发生屏幕大小改变,横竖屏切换等。
因此综合考虑之下,不得不采用解码-再编码的转码方式。

本文讲述的xdisp_virt实现浏览器摄像头投射的功能,大部分都是以前开发的基础上扩展,
所以相对来说比较容易,不过也比较繁琐。
下文阐述的内容是如何实现摄像头,并且把摄像头集成到xdisp_virt中,
让xdisp_virt处理的视频流再次模拟成电脑的摄像头。
不过到时主要是以linux的实现为主,
至于windows下的虚拟摄像头,我以前的文章已经阐述过多次,不想再次赘述了。

xdisp_virt开发的浏览器摄像头投射功能也已经完成,
不过得等xdisp_virt集成了模拟虚拟摄像头功能之后再一并发布出来。
有兴趣可以关注我github上的xdisp_virt软件。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/781730.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【SpringBoot】IDEA查看spring bean的依赖关系

前因:在研究springcloud config组件时,我发现config-server包下的EnvironmentController不在扫描的包路径下却可以响应客户端的请求,这吸引了我的注意,我的问题是:EnvironmentController是怎么被添加进bean工厂的。本章…

Golang | Leetcode Golang题解之第218题天际线问题

题目: 题解: type pair struct{ right, height int } type hp []pairfunc (h hp) Len() int { return len(h) } func (h hp) Less(i, j int) bool { return h[i].height > h[j].height } func (h hp) Swap(i, j int) { h[i], h[j]…

26_嵌入式系统网络接口

以太网接口基本原理 IEEE802标准 局域网标准协议工作在物理层和数据链路层,其将数据链路层又划分为两层,从下到上分别为介质访问控制子层(不同的MAC子层,与具体接入的传输介质相关),逻辑链路控制子层(统一的LLC子层,为上层提供统…

CosyVoice多语言、音色和情感控制模型,one-shot零样本语音克隆模型本地部署(Win/Mac),通义实验室开源

近日,阿里通义实验室开源了CosyVoice语音模型,它支持自然语音生成,支持多语言、音色和情感控制,在多语言语音生成、零样本语音生成、跨语言声音合成和指令执行能力方面表现卓越。 CosyVoice采用了总共超15万小时的数据训练&#…

GuitarPro2024音乐软件#创作神器#音乐梦想

嘿,亲爱的朋友们!👋👋👋今天我要给你们安利一款超赞的软件——Guitar Pro。这款软件简直是吉他手的福音啊!🎉🎉🎉 Guitar Pro免费绿色永久安装包下载:&#…

如何快速申请免费SSL证书,实现网站HTTPS安全传输

随着互联网技术的飞速发展,网络安全已成为不可忽视的重要议题。HTTPS协议,作为HTTP协议的安全版本,通过SSL协议加密客户端与服务器之间的数据传输,从而保障信息在传输过程中的安全性。对于网站运营者而言,为网站部署SS…

SpringBoot测试类注入Bean失败的原因

针对SpringBoot的测试类&#xff0c;2.2版本之前和之后是不一样的。 2.2版本之后 导包pom.xml 添加test依赖 <!-- starter-test&#xff1a;junit spring-test mockito --> <dependency><groupId>org.springframework.boot</groupId><artifac…

论文解析——FTRANS: Energy-Efficient Acceleration of Transformers using FPGA

作者及发刊详情 Li B , Pandey S , Fang H ,et al.FTRANS: energy-efficient acceleration of transformers using FPGA[J].ACM, 2020.DOI:10.1145/3370748.3406567. 摘要 正文 主要工作贡献 与CPU和GPU在执行Transformer和RoBERTa相比&#xff0c;提出的FTRANS框架获得了…

ansible常见问题配置好了密码还是报错

| FAILED! > { “msg”: “Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support this. Please add this host’s fingerprint to your known_hosts file to manage this host.” } 怎么解决&#xf…

[终端安全]-3 移动终端之硬件安全(TEE)

&#xff08;参考资料&#xff1a;TrustZone for V8-A. pdf&#xff0c;来源ARM DEVELOPER官网&#xff09; TEE&#xff08;Trusted Execution Environment&#xff0c;可信执行环境&#xff09;是用于执行敏感代码和处理敏感数据的独立安全区域&#xff1b;以ARM TrustZone为…

分布式技术栈、微服务架构 区分

1.分布式技术栈 这些技术栈都是为了更好的开发分布式架构的项目。 &#xff08;大营销平台的系统框架如下图&#xff0c;扩展的分布式技术栈&#xff09; &#xff08;1&#xff09;Dubbo——分布式技术栈 DubboNacos注册中心是应用可以分布式部署&#xff0c;并且提供RPC接…

HTML5使用<pre>标签:保留原始排版方式

在网页创作中&#xff0c;一般是通过各种标记对文字进行排版的。但是在实际应用中&#xff0c;往往需要一些特殊的排版效果&#xff0c;这样使用标记控制起来会比较麻烦。解决的方法就是保留文本格式的排版效果&#xff0c;如空格、制表符等。 如果要保留原始的文本排版效果&a…

redis并发、穿透、雪崩

Redis如何实现高并发 首先是单线程模型&#xff1a;redis采用单线程可以避免多线程下切换和竞争的开销&#xff0c;提高cpu的利用率&#xff0c;如果是多核cpu&#xff0c;可以部署多个redis实例。基于内存的数据存储&#xff1a;redis将数据存储在内存中&#xff0c;相比于硬…

回溯算法-以景点门票销售管理系统为例

1.回溯算法介绍 1.来源 回溯算法也叫试探法&#xff0c;它是一种系统地搜索问题的解的方法。 用回溯算法解决问题的一般步骤&#xff1a; 1、 针对所给问题&#xff0c;定义问题的解空间&#xff0c;它至少包含问题的一个&#xff08;最优&#xff09;解。 2 、确定易于搜…

唤醒知识循环,共筑绿色阅读梦——探索旧书回收小程序的无限可能

在这个信息爆炸的时代&#xff0c;书籍作为知识与智慧的载体&#xff0c;其重要性不言而喻。然而&#xff0c;随着电子阅读的兴起和书籍更新换代的加速&#xff0c;大量旧书被束之高阁&#xff0c;甚至面临被遗弃的命运。这不仅是对宝贵文化资源的浪费&#xff0c;也是对环境保…

12 电商高并发缓存实战

序章 项目代码缓存的数据一致性 延时双删 淘汰缓存写数据库休眠1s,再次淘汰缓存缺点:如果mysql是主从复制,去从库中拿去数据,此时同步数据还未完成,拿到的数据是旧数据。 先更新 DB,后删除缓存 采用异步延时删除策略. ①利用消息队列进行删除的补偿。②Mysql 数据库更新操…

Android项目中,查看项目依赖树的多种方式

1.使用预设的Task来进行查看 1.1 命令行 查看某个模块的所有依赖树&#xff1a; gradlew [模块名称]:dependencies 例如&#xff1a;gradlew app:dependencies查看某个模块的某功能的依赖树&#xff1a; gradlew [模块名称]:dependencies --configuration [功能名称] 例如&…

华为路由器静态路由配置(eNSP模拟实验)

实验目标 如图下所示&#xff0c;让PC1ping通PC2 具体操作 配置PC设备ip 先配置PC1的ip、掩码、网关。PC2也做这样的配置 配置路由器ip 配置G0/0/0的ip信息 #进入系统 <Huawei>system-view #进入GigabitEthernet0/0/0接口 [Huawei]int G0/0/0 #设置接口的ip和掩码 […

排序 -- 万能测试oj

. - 力扣&#xff08;LeetCode&#xff09; 这道题我们可以使用我们学过的那些常见的排序方法来进行解答 //插入排序 void InsertSort(int* nums, int n) {for (int i 0; i < n-1; i){int end i;int tmp nums[end 1];while (end > 0){if (tmp < nums[end]){nums[…

阿里云ecs服务器,nginx多域名多项目部署教程,含本地部署教程

nginx多域名部署项目 本地部署线上部署一、本地部署 第一步:win+r 输入drivers 打开hosts文件,编辑 加行 127.0.0.1 自定义域名 … 第二步:下载 nginx 安装好以后 打开ngin安装目录,选择nginx.conf 打开 #user Administrator; worker_processes