最近喜欢研究视频流,所以思考了双向通信socket,接下来我们就一起来看看本地如何实现双向视频通讯的功能吧~
客户端获取视频流
首先思考如何获取视频流呢?
其实跟录音的功能差不多,都是查询电脑上是否有媒体设备,如果有录音和录像的设备,首先就需要授权,然后将视频流通过socket传输给服务端。
获取媒体设备
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true })
因为是打视频的功能,那A客户端本身也希望看到A的摄像头,所以我们直接将其赋值给一个video标签,就能看到图像了.
<p>这是A页面</p> <div class="local-stream-page"> <video autoplay controls muted id="elA"></video> <button onclick="onStart()">打视频给B页面</button> </div> <script> try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true }) if (videoElA) { videoElA.srcObject = stream // 在 video 标签上播放媒体流 } peerInit(stream) // 初始化连接 } catch (error) { console.log('error:', error) } </script>
然后就是重要部分了,我们需要用到WebRTC的API
RTCPeerConnection
RTCPeerConnection是WebRTC API中的一个对象,用于建立和管理两个或多个用户之间的实时通信。它允许通过互联网进行音频和视频通话,以及共享数据流。
RTCPeerConnection对象提供了一系列的方法和事件,用于配置、管理和控制媒体流的传输。它支持使用不同的技术,如ICE(Interactive Connectivity Establishment)和STUN(Session Traversal Utilities for NAT)来解决网络地址转换(NAT)问题,以便在防火墙后面的不同设备之间建立连接。
使用RTCPeerConnection对象,您可以创建媒体流并将其发送到其他设备,也可以接收来自其他设备的媒体流。它还支持使用SDP(Session Description Protocol)描述媒体会话的配置,以及通过ICE和STUN协议协商和转发媒体数据包的路由。
const peerInit = stream => { // 1. 创建连接实例 peerA = new RTCPeerConnection() // 2. 添加视频流轨道 stream.getTracks().forEach(track => { peerA.addTrack(track, stream) }) // peerA 端 peerA.onicecandidate = event => { if (event.candidate) { socketA.send(JSON.stringify({ type: 'candid', data: event.candidate })) // socketA发送数据 } } // 检测连接状态 peerA.onconnectionstatechange = event => { if (peerA.connectionState === 'connected') { console.log('对等连接成功!') } } // 互换sdp认证 transSDP() }
到这里我们发送数据部分就是这样子啦,但是还不行,因为两者通视频,还需要SDP认证,什么是SDP认证呢?
SDP(Session Description Protocol)认证是指通过在SDP协议中添加特定的信息来验证身份或其他属性的方法。SDP协议是一种用于描述多媒体会话的信息协议,它包含了音频、视频等媒体的编码格式、分辨率、网络地址等信息,用于在通话双方之间建立和维护媒体连接。
在SDP认证中,通过在SDP协议中添加特定的信息,如用户名、会议ID等,双方可以互相验证身份。此外,还可以通过在SDP协议中包含数字签名或加密信息等技术来增强认证的安全性。
SDP认证通常用于多媒体通信、视频会议等应用场景中,以确保通信的安全性和可信度。在SDP认证中,需要使用相应的协议或算法来验证SDP信息的来源和完整性,以确认身份或其他属性的合法性。
【免费分享】音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击788280672加群免费领取~
互换SDP认证
// peerA 端 const transSDP = async () => { let offer = await peerA.createOffer() // 向 peerB 传输 offer socketA.send(JSON.stringify({ type: 'offer', data: offer })) // 接收 peerB 传来的 answer socketA.onmessage = async evt => { let reader = new FileReader() reader.readAsText(evt.data, 'utf-8') reader.onload = async function() { let { type, data } = JSON.parse(reader.result) console.log(JSON.parse(reader.result), 111) if (type == 'answer') { await peerA.setLocalDescription(offer) await peerA.setRemoteDescription(data) } } } }
这就是A客户端的全部代码啦~
放心,全部代码文章末尾会给到.
node服务端socket传输
接下来我们来看看服务端是如何处理的.对了,这里必须说一下,两个socket之间的通信,必须要靠服务端管理,所以这就是为什么一定要学node的原因
const WebSocket = require('ws'); // 创建一个 WebSocket 服务器,监听 8080 端口 const wss = new WebSocket.Server({ port: 8000 }); // 当有客户端连接时,创建一个 WebSocket 并将其添加到客户端列表中 wss.on('connection', function connection(ws) { console.log('Client connected'); // 当客户端发送消息时,将消息发送给所有客户端 ws.on('message', function incoming(message) { console.log('Received message:', message.toString('utf8')); // 接受的对象,客户端发送的是字符串,Buffer // 将消息发送给所有客户端 wss.clients.forEach(function each(client) { if (client !== ws && client.readyState === WebSocket.OPEN) { client.send(message); // 客户端接受的是blob格式数据 } }); }); // 当客户端断开连接时,将其从客户端列表中删除 ws.on('close', function close() { console.log('Client disconnected'); }); });
服务端用到了ws依赖, 如何区分两个不同的socket客户端, 特别是在同一个服务器下,同一个端口,不同的页面下,我发现必须要给两个socket一个唯一的标识才能做到,所以这期就先出功能,后面再继续补一下ws的源码学习.
不过这里要区分清楚,这是将当前的client客户端发送给处理自己以外的,其他所以socket客户端,发送消息这里,就是一对多的关系哦.
客户端接受视频流
服务端处理完了,就进行下一个客户端如何接受视频流
刚刚的sdp认证,肯定不止止A页面的事情,都说了是认证,那肯定通信双方需要知晓.
这里有一个顺序问题
1.首先是A页面创建offer---createOffer
2.然后是B页面设置远程描述---setRemoteDescription
3.B页面生成发送到A页面的answer---createAnswer
4.B页面设置本地描述---setLocalDescription
5.A页面设置本地描述---setLocalDescription(传参是A页面的offer)
6.A页面设备远程描述--setRemoteDescription(传参是B页面的answer)
只要这上面6步都正常执行,B页面才能接收到A页面的视频流和音频流
const transSDP = async () => { // 1. 创建 offer let offer = await peerA.createOffer() await peerB.setRemoteDescription(offer) // 2. 创建 answer let answer = await peerB.createAnswer() await peerB.setLocalDescription(answer) // 3. 发送端设置 SDP await peerA.setLocalDescription(offer) await peerA.setRemoteDescription(answer) }
加上socket之后就是这样
不过既然是socket了,所以数据上要做转换处理,接收到的是blob数据
// B接收A的消息 // peerB 端,接收 peerA 传来的 offer socketB.onmessage = evt => { // console.log(evt.data) handleBlobToText(evt.data) } const handleBlobToText = (blob) => { let reader = new FileReader() reader.readAsText(blob, 'utf-8') // 接收到的是blob数据,先转成文本 reader.onload = async function() { console.log(reader.result) let { type, data } = JSON.parse(reader.result) // 文本转对象 console.log(JSON.parse(reader.result)) if (type == 'offer') { await peerB.setRemoteDescription(data) console.log('2.然后是B页面设置远程描述', new Date().getTime()) let answer = await peerB.createAnswer() console.log('3.B页面生成发送到A页面的answer', new Date().getTime()) await peerB.setLocalDescription(answer) console.log('4.B页面设置本地描述', new Date().getTime()) // 向 peerA 传输 answer socketB.send(JSON.stringify({ type: 'answer', data: answer })) } if (type == 'candid') { peerB.addIceCandidate(data) } } } socketB.onerror = function() { console.log('WebSocket error. Ready state:', socketB.readyState); };
根据时间戳,就能发现这六步的顺序.
将接收到的视频流渲染到B页面的video标签中,这就能接受的A页面的视频流了.
const socketB = new WebSocket('ws://localhost:8000'); const peerB = new RTCPeerConnection() const videoElB = document.getElementById('elB') // 监听数据传来 peerB.ontrack = async event => { const [remoteStream] = event.streams videoElB.srcObject = remoteStream }
效果
这就是两个页面视频通讯的结果如下:
全部源码已经上传在GitHub上啦~
github.com/0522skylar/…
原文链接 socket实现视频通话-WebRTC - 掘金