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

news2024/11/25 11:02:32

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.coloradmin.cn/o/1904118.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

centos docker 安装mysql:8.0.21 天坑记录

docker pull mysql:8.0.21 安装的mysql 8.0.21 版本,当创建表时只要创建表的字段大于10,就会报错 > 2013 - Lost connection to MySQL server during query 当删除一个字段,刚好9个字段时就可以创建成功,打印等于10个&#…

时间处理的未来:Java 8全新日期与时间API完全解析

文章目录 一、改进背景二、本地日期时间三、时区日期时间四、格式化 一、改进背景 Java 8针对时间处理进行了全面的改进,重新设计了所有日期时间、日历及时区相关的 API。并把它们都统一放置在 java.time 包和子包下。 Java5的不足之处: 非线程安全&…

Nginx auth 的权限验证

基本流程 整个流程为;以用户视角访问API开始,进入 Nginx 的 auth 认证模块,调用 SpringBoot 提供的认证服务。根据认证结果调用重定向到对应的 API 接口或者 404 页面。 查看版本保证有 Nginx auth 模块 由于 OpenAI 或者本身自己训练的一套…

实现多数相加,但是传的参不固定

一、情景 一般实现的加法和减法等简单的相加减函数的话。一般都是写好固定传的参数。比如: function add(a,b) {return a b;} 这是固定的传入俩个,如果是三个呢,有人说当然好办! 这样写不就行了! function add(a…

前端JS特效第22波:jQuery滑动手风琴内容切换特效

jQuery滑动手风琴内容切换特效&#xff0c;先来看看效果&#xff1a; 部分核心的代码如下&#xff1a; <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xm…

Day4 用 rustlings 练习 Rust 语言

大家好 今天 完成 2024年自动驾驶OS开发训练营-初阶营第四期-导学 Day4用 rustlings 练习 Rust 语言 欢迎加入国家智能网联汽车创新中心OS开发训练营大家庭。&#x1f680; 导学阶段启动 在正式开营之前&#xff0c;我们特别设置了导学阶段&#xff0c;旨在帮助大家更好地迎接颇…

万和day01代码分析

将了数据库的多表之间的操作&#xff0c;实际应用到JDBC中去。 一共五张表&#xff0c; info存储的是具体的信息&#xff0c;edu job role 和info都是多对一的关系。 采用的是Java FX&#xff0c;界面采用xml去编写。 项目理解一 在JavaFX中&#xff0c;ObservableList 是一个…

SSL/CA 证书及其相关证书文件解析

在当今数字化的时代&#xff0c;网络安全变得至关重要。SSL&#xff08;Secure Socket Layer&#xff09;证书和CA&#xff08;Certificate Authority&#xff09;证书作为保护网络通信安全的重要工具&#xff0c;发挥着关键作用。 一、SSL证书 SSL证书是数字证书的一种&…

汉诺塔与青蛙跳台阶

1.汉诺塔 根据汉诺塔 - 维基百科 介绍 1.1 背景 最早发明这个问题的人是法国数学家爱德华卢卡斯。 传说越南河内某间寺院有三根银棒&#xff0c;上串 64 个金盘。寺院里的僧侣依照一个古老的预言&#xff0c;以上述规则移动这些盘子&#xff1b;预言说当这些盘子移动完毕&am…

使用Charles mock服务端响应数据

背景 服务端未提供接口/服务端接口返回结果有逻辑限制&#xff08;次数限制&#xff09;&#xff0c;不能通过原始接口返回多次模拟预期的返回结果&#xff0c;例如边界值情况 客户端受到接口响应数据的限制&#xff0c;无法继续开发或测试&#xff0c;会极大影响开发测试效率…

C# WPF 3D 数据孪生 系列六

数字孪生应用开发 应用开发中的布局需求 Grid基本使用 WPF 3D绘图 点云 系列五-CSDN博客 WPF UI 3D 多轴 机械臂 stl 模型UI交互-CSDN博客 WPF UI 3D 基本概念 点线三角面 相机对象 材质对象与贴图 3D地球 光源 变形处理 动作交互 辅助交互插件 系列三-CSDN博客 数字孪生 介…

550kg级大载重长航时无人机直升机技术详解

550kg级大载重长航时无人机直升机&#xff0c;作为一种高性能的无人机系统&#xff0c;具备了多项先进的技术特点&#xff0c;以满足高海拔、高寒等复杂环境下的应用需求。这些无人机直升机通常具备高载重、长航时、强适应性、高可靠性和良好的任务拓展性。 设备由无人直升机平…

刷题之移除元素(leetcode)

移除元素 这题简单题&#xff0c;但是前面思路是先找到左边第一个不是val的&#xff0c;和右边第一个不是val的&#xff0c;进行交换&#xff0c;边界条件没有处理好&#xff0c;导致报错&#xff08;水平真菜&#xff09; 也可以直接把left是val的与right进行交换&#xf…

个人视角,社会影响力:自媒体的魅力所在

随着数字化时代的到来&#xff0c;自媒体正成为信息传播领域的一场革命。个人视角与社会影响力的结合&#xff0c;赋予了自媒体独特的魅力。在传统媒体受限制的同时&#xff0c;自媒体为每个人提供了表达自己观点和思想的自由。个体的真实视角使得自媒体在信息传播中发挥着重要…

匠心筑智:探索AI智能问答系统的设计之道

在科技日新月异的今天&#xff0c;人工智能&#xff08;AI&#xff09;已悄然渗透到我们生活的方方面面&#xff0c;其中&#xff0c;AI智能问答系统作为人机交互的重要桥梁&#xff0c;正逐步改变着人们获取信息、解决问题的方式。本文将带您深入探索如何设计一个高效、智能且…

即时通讯平台项目测试(登录/注册页面)

http://8.130.98.211:8080/login.html项目访问地址&#xff1a;即时通讯平台http://8.130.98.211:8080/login.html 本篇文章进行登录和注册页面的测试。自动化脚本的依赖在文章末尾。 登录页面测试 UI测试 测试环境&#xff1a;Win11&#xff1b;IntelliJ IDEA 2023.2&#…

汇聚荣拼多多电商的技巧有哪些?

在电商平台上&#xff0c;汇聚荣拼多多以其独特的商业模式和创新的营销策略吸引了大量消费者。那么&#xff0c;如何在这样一个竞争激烈的平台上脱颖而出&#xff0c;成为销售佼佼者呢?本文将深入探讨汇聚荣拼多多电商的成功技巧。 一、精准定位目标客户群体 首先&#xff0c;…

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

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

fasttext工具介绍

fastText是由Facebook Research团队于2016年开源的一个词向量计算和文本分类工具。尽管在学术上并未带来巨大创新&#xff0c;但其在实际应用中的表现却非常出色&#xff0c;特别是在文本分类任务中&#xff0c;fastText往往能以浅层网络结构取得与深度网络相媲美的精度&#x…

Git中两个开发分支merge的原理

一 分支合并 1.1 原理 分支合并&#xff1a;就是将A分支修改后且commit的内容&#xff0c;合并到B分支&#xff0c;这些修改且提交的内容和B分支对应的内容和位置进行比较&#xff1a; 1.不一样的话&#xff0c;提示冲突&#xff0c;需要人工干预。 2.一样的话&#xff0c;…