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软件。