上周五的上午突然接到了一个需求,要在前端页面中增加的摄像头的画面预览功能,要在周一(昨天)完成,我本来以为接入视频流并不会很困难,毕竟之前也做过视频相关的需求。
但是,真正做起来却不是那么简单。因为底层的同学给的视频流地址是一个rtsp协议的(例:rtsp://192.168.0.123:8081),在跟百娘“缠绵”了一番之后,最终得出结论——前端无法直接使用rtsp流播放(可能有,如果知道的大佬请评论区告知)。
在告知底层同学之后,过了一会我收到了一个链接
他们吧rtsp流转成了WebRTC,乍一看我还有点兴奋,因为不久前刚看过WebRTC,感觉这回“简单”了。打开这个链接看到了监控的实时画面
但是,给提供的材料有且仅有这一个链接,这我又摸不着头脑了,我了解的WebRTC是P2P的啊,需要双方的SDP进行协商连接,但这只有一个链接我怎么玩?!
一筹莫展之际,刻在前端开发DNA中的记忆被唤醒了——F12
,然后发现了一个东西,直觉告诉我,真相就在不远处
找到这个文件之后,一段段我熟悉的代码映入眼帘,我悟了。(suuid对应的就是摄像头的设备名称)
最关键的在于下面那两个jquery请求,最后一个请求正是获取对端SDP的请求,一般使用P2P获取SDP都是用的socket,这里因为只有一端能够发起会话,所以用了http的形式。
事后我找到了他们rtsp转WebRTC所使用的工具RTSPtoWebRTC,打开github的第一眼我就认出来了
书归正题,看懂了这段代码之后,我结合项目将这段代码转化为了我们项目中能用的形式
import { getRTCCodec, getRTCReceiver } from '@/services/camera';
/**
* 预览摄像头
* @param ele 要播放视频的video 标签元素
* @param ip 视频流IP 地址
* @param port 视频流端口
* @param cameraName 摄像头名称
*/
export async function preview(ele, ip, port, cameraName) {
// 创建新的媒体流
let stream = new MediaStream();
// 初始化 ice 配置
let config = {
iceServers: [
{
// stun 服务器地址, P2P打洞需要
urls: ['stun:stun.l.google.com:19302'],
},
],
};
// 兼容不同的浏览器
const PeerConnection =
window.RTCPeerConnection ||
// @ts-ignore
window.mozRTCPeerConnection ||
window.webkitRTCPeerConnection;
// 新建RTC连接
const pc = new PeerConnection(config);
// 需要协商时触发事件
pc.onnegotiationneeded = handleNegotiationNeededEvent;
pc.ontrack = function (event) {
stream.addTrack(event.track);
// 设置srcObject
ele.srcObject = stream;
};
// 获取codec
const codec = await getRTCCodec(ip, port, cameraName);
codec.forEach((c) =>
pc.addTransceiver(c.Type, {
direction: 'sendrecv',
}),
);
// 协商事件
async function handleNegotiationNeededEvent() {
// 生成本地Offer
let offer = await pc.createOffer();
// 设置本地Offer
await pc.setLocalDescription(offer);
let receiver;
try {
// 获取远端 answer
receiver = await getRTCReceiver(
ip,
port,
cameraName,
pc.localDescription?.sdp,
);
// 设置远端 answer
await pc.setRemoteDescription(
new RTCSessionDescription({
type: 'answer',
// 解码远端answer
sdp: atob(receiver),
}),
);
} catch (e) {
console.log(e);
}
}
}
这里有个小坑,我一开始没太注意,这里卡了一段时间:获取answer的post请求参数,是一个字符串,类似于get请求的query格式参数,只不过挪到了body中,并不是使用json
/**
* 获取摄像头的receiver
* @param ip
* @param port
* @param cameraName
*/
export async function getRTCReceiver(
ip: string,
port: string,
cameraName: string,
sdp: any,
): Promise<any> {
return await basePost(
`http://${ip}:${port}/stream/receiver/${cameraName}`,
// base64 sdp
`suuid=${cameraName}&data=${btoa(sdp)}`,
{
'Content-Type': 'application/x-www-form-urlencoded',
},
true,
true,
);
}
效果展示