背景
工作中遇到要进行实时视频截图,webRtc和socket.io 简单实现一个视频通信的示例,视频由clientA发出,clientB接收视频,clientB接收视频进行截图
WebRtc介绍
WebRTC (Web Real-Time Communications) 是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。WebRTC包含的这些标准使用户在无需安装任何插件或者第三方的软件的情况下,创建点对点(Peer-to-Peer)的数据分享和电话会议成为可能。 主要作用
获取音频和视频
进行音频和视频通信
进行任意数据的通信
WebRTC共分成三个API,分别对应上面三个作用。
MediaStream
RTCPeerConnection
RTCDataChannel
上面的介绍来自MDN和阮大大(阮一峰)。
对webRtc的理解
两个页面之间的实时视频通信,clientA给clientB发视频,需要知道对方的外网地址,但在现实Internet网络环境中,大多数计算机主机都位于防火墙或NAT之后,我们想要他们直接通信,而不通过服务器中转,即pee-to-pee,所以我们需要用到STUN服务器来穿透NAT(具体的NAT穿透技术请自行查阅)获取地址,获取对方的地址后(webRtc默认是使用STUN来获取当前主机的外网地址和端口),开始建立连接,pee-to-peee建立过程中需依赖信令中转(socket.io来发送一些对方的信息)通道建立后,双方通过信令来交换一些信息(比如媒体适配,网络的配置信息)来确定传输的规定。准备就绪后,clientA就可以传输视频了,这样在clientB就能实时接收视频了。虽然是实时的,但是事实上肯定是有一定的差异的,只是这种差异肉眼察觉不出来可以忽略掉,具体webRtc的原理,文末有链接推荐
正文
webRtc流程看图
客户端A
html
<div>
<video id="video" muted autoplay controls width="400px"></video>
</div>
let videoDom = document.getElementById('video')
let localSteam
// socket.io 连接
const socket = io.connect('http://localhost:3000')
socket.emit('joinRoom', '123')
socket.on('create', (room) => {
console.log('创建或加入房间' + room)
})
// 接电话
socket.on('call', (msg) => {
console.log(msg)
if (msg === 'refresh') {
createRTC()
}
})
获取媒体设备getUserMedia
// 获取媒体设备
navigator.getUserMedia = navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia;
if (navigator.getUserMedia) {
// 支持
navigator.getUserMedia({ video: true, audio: true }, onSuccess, onError)
} else {
// 不支持
console.log('该浏览器不支持获取音视频')
}
// 打开视频成功
function onSuccess(stream) {
localSteam = stream
videoDom.srcObject = stream
videoDom.play()
createRTC()
}
// 打开视频失败
function onError(err) {
console.log('打开视频失败')
console.log(err)
}
webRtc通信,发起视频的客户
// webRtc通信
let config = {
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
}
]
}
let pc
const offerOptions = {
offerToReceiveVideo: 1,
offerToReceiveAudio: 1
};
// 创建RTC
function createRTC() {
// 创建RTC
pc = new RTCPeerConnection(config)
// 视频流添加
pc.addStream(localSteam)
// 创建offer 的 SDP 对象
pc.createOffer(offerOptions).then((offer) => {
// 创建成功后通过signal服务器发送给clientB
pc.setLocalDescription(offer)
socket.emit('signal', offer, '123')
}).catch((err) => {
console.log(err)
})
// 接收到candidate信息后,通过onIceCandidate 接口发送给ClientB
pc.addEventListener('icecandidate', event => {
let icecandidate = event.candidate
if (icecandidate) {
socket.emit('ice', icecandidate, '123')
}
})
}
socket.on('signal', (offer) => {
// 接受保存clientA的应答SDP对象
if (pc !== 'undefined') { // pc 有时为undefined
pc.setRemoteDescription(new RTCSessionDescription(offer))
}
})
socket.on('ice', (message) => {
if (pc !== 'undefined') {
pc.addIceCandidate(new RTCIceCandidate(message));
}
})
客户端B
html
<div>
<video id="video" muted autoplay controls width="400px" height="300px"></video>
<br />
<canvas id="canvas" style="display: none;" width="200px" height="200px"></canvas>
<button id='button' style="margin: 10px">点击截图</button>
<br />
<img src="" alt="" width="200px" height="150px">
</div>
<script src="socket.io/socket.io.js"></script>
js
let videoDom = document.getElementById('video')
let canvasDom = document.getElementById('canvas')
let imgDom = document.getElementsByTagName('img')[0]
let btnDom = document.getElementById('button')
let ctx = canvasDom.getContext('2d')
// 点击截图
btnDom.addEventListener('click', () => {
ctx.drawImage(videoDom, 0, 0, 200, 200)
imgDom.src = canvasDom.toDataURL('image/webp')
})
// 下面这个是自动截图,最新图片
// videoDom.addEventListener('play', () => {
// setInterval(() => {
// ctx.drawImage(videoDom, 0, 0, 200, 200)
// imgDom.src = canvasDom.toDataURL('image/webp')
// }, 20)
// })
// 视频接受失败请求再次发送
console.log(videoDom.srcObject)
// socket.io 连接
const socket = io.connect('http://localhost:3000')
socket.emit('joinRoom', '123')
socket.on('create', (room) => {
console.log('创建或加入房间' + room)
})
// 接电话
socket.on('call', (msg) => {
console.log(msg)
})
// webRtc通信
let config = {
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
}
]
}
let pc
socket.on('signal', (message) => {
pc = new RTCPeerConnection(config)
// 通过setRemoteDescription保存clientA传过来的offer对象
pc.setRemoteDescription(new RTCSessionDescription(message))
// createAnswer 创建一个应答SDP对象,通过服务器发送给ClineA
pc.createAnswer().then(answer => {
pc.setLocalDescription(answer)
socket.emit('signal', answer, '123')
}).catch(err => {
console.log('创建应答SDP 失败')
console.log(err)
})
// 接收到candidate信息后,通过onIceCandidate 接口发送给ClientA
pc.addEventListener('icecandidate', event => {
let icecandidate = event.candidate
if (icecandidate) {
socket.emit('ice', icecandidate, 123)
}
})
// 音视频通道建立后接受clientA 传过来的视频流
pc.addEventListener('addstream', (event) => {
if (event) {
videoDom.srcObject = event.stream
console.log(videoDom.srcObject)
} else {
socket.emit('call', 'refresh', '123')
console.log('视频接受失败')
}
})
})
socket.on('ice', (message) => {
if (pc !== 'undefined') {
try {
pc.addIceCandidate(new RTCIceCandidate(message));
} catch (err) {
console.log(err)
}
}
})
// 如果视频为空打电话给ClientA 重新发一次
if (!videoDom.srcObject) {
socket.emit('call', 'refresh', '123')
}
socket.io服务端的代码请看下面连接
const app = require("express")();
const server = require("http").createServer(app);
const io = require("socket.io")(server);
server.listen(3000);
console.log("创建socket.io服务");
io.on("connection", (socket) => {
// 创建房间
socket.on("joinRoom", (room) => {
socket.join(room, () => {
socket.emit("create", room);
});
});
// 视频通信
socket.on("signal", (message, room) => {
socket.to(room).emit("signal", message);
});
socket.on("ice", (message, room) => {
socket.to(room).emit("ice", message);
});
// 客户端断开
socket.on("disconnecting", () => {
const rooms = Object.keys(socket.rooms);
console.log(rooms[0] + "离开房间");
socket.to(rooms[0]).emit("leaveRoom", rooms[0]);
});
// 接受消息打电话
socket.on('call', (msg, room) => {
socket.to(room).emit('call', msg)
})
});
最后
WebRtc原理: www.jianshu.com/p/24363820b… MDN:developer.mozilla.org/zh-CN/docs/…