笔者所在一家智能家居公司,安防camera业务组,负责核心的音视频直播、视频回放等业务员功能。原有播放器基于hybrid分层,由原生提供,js层进行调用。存在问题是性能消耗大,接口繁多,调用负责,难以维护、tutk+p2p连接不稳定、无法移植pc端等。so,由架构牵头最近开始了h5video流媒体播放器的研究,分享一下经验和体会。
webrtc流媒体服务器介绍
流媒体服务器的主要功能是以流式协议(RTP/RTSP、MMS、RTMP, webrtc等)将视频文件传输到客户端,供用户在线观看;也可从视频采集、压缩软件接收实时视频流,再以流式协议直播给客户端。
webrtc 流媒体服务器主要以webrtc协议为核心, 进行视频采集与播放.
以TURN/信令支持webrtc标准交互后, 就可以提供点对点直播, 设备带宽性能足够时也支持一对多(少量)直播. 如下图: 多方通信架构一般有三种方案:
Mesh 方案,即多个终端之间两两进行连接,形成一个网状结构。比如 A、B、C 三个终端进行多对多通信,当 A 想要共享媒体(比如音频、视频)时,它需要分别向 B 和 C 发送数据。同样的道理,B 想要共享媒体,就需要分别向 A、C 发送数据,依次类推。这种方案对各终端的带宽要求比较高。
MCU(Multipoint Conferencing Unit)方案,该方案由一个服务器和多个终端组成一个星形结构。各终端将自己要共享的音视频流发送给服务器,服务器端会将在同一个房间中的所有终端的音视频流进行混合,最终生成一个混合后的音视频流再发给各个终端,这样各终端就可以看到 / 听到其他终端的音视频了。实际上服务器端就是一个音视频混合器,这种方案服务器的压力会非常大。
SFU(Selective Forwarding Unit)方案,该方案也是由一个服务器和多个终端组成,但与 MCU 不同的是,SFU 不对音视频进行混流,收到某个终端共享的音视频流后,就直接将该音视频流转发给房间内的其他终端。它实际上就是一个音视频路由转发器。
webrtc 默认支持 Mesh架构. SFU是当前主流的方案,大多数厂商都采用SFU架构模型.
实现步骤
h5使用video标签初始化播放器。
import React, { memo } from 'react';
import { Button, Flex } from '@leedarson/ui-mobile';
import useWebRTC from './useWebRTC';
const WebRTCPlayer = memo(() => {
const { handleStart, call, handleStop } = useWebRTC();
return (
<>
<video id="webRTC-player" autoPlay muted />
<Flex>
<Button width="30%" onClick={handleStart}>
START
</Button>
<Button width="30%" onClick={call}>
Call
</Button>
<Button width="30%" onClick={handleStop}>
STOP
</Button>
</Flex>
</>
);
});
export default WebRTCPlayer;
使用websocket进行鉴权和流媒体传输,考虑安全性使用wss协议传输,即ssl+ws,类https。
import logger from '@leedarson/logger';
class WSWebRTC {
constructor(url) {
this.ws = new WebSocket(url);
this.ws.conn_status = false;
}
init(localId, remoteId, processSignalingMessage) {
this.ws.onopen = () => {
const login = { clientId: localId };
this.ws.conn_status = true;
this.ws.send(JSON.stringify(login));
};
this.ws.sendFormatMsg = obj => {
const m = { peerId: remoteId, payload: obj };
const str = JSON.stringify(m);
this.ws.send(str);
};
this.ws.onmessage = event => {
if (typeof event.data === 'string') {
// 鉴权
processSignalingMessage(event.data);
return;
}
if (event.data instanceof ArrayBuffer) {
const buffer = event.data;
logger.log('Received arraybuffer', buffer);
this.ws.close();
}
logger.log(`Received Message: ${event.data}`, typeof event.data);
};
// 指定连接关闭后的回调函数
this.ws.onclose = () => {
this.ws.conn_status = false;
};
this.ws.onerror = event => {
this.ws.conn_status = false;
logger.error('webRTC_Error:', event);
};
}
}
export default WSWebRTC;
使用navigator.mediaDevices.getUserMedia方法获取流
该方法会提示用户给予使用媒体输入的许可,媒体输入会产生一个MediaStream,里面包含了请求的媒体类型的轨道。需要注意在移动端设备上调用需要在https的基础之上,主要原因是浏览器基于安全考虑,防止视频流被攻击窃取。(pc端无此限制)笔者在app上调用时,由于代码打包在app本地使用 http://localhost:3000 加载,调用时该方法直接报错。需要注意这里https的限制时无法跳过的,笔者解决方案是将webrtc播放器相关的前端页面部署在https证书加密的服务器上远程调用来解决的。
const handleStart = useCallback(async () => {
// 1.连接webSocket
handleWSConnect();
try {
// 2.获取视频流通道
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true,
});
localVideo.srcObject = stream;
localStreamRef.current = stream;
// 3.拉流播放
call();
} catch (error) {
logger.error('读取权限or视屏流失败', error);
}
}, [call, handleWSConnect, localVideo]);
使用webRTC接口RTCPeerConnection拉去视频流播放。
const onIceCandidate = useCallback(
event => {
if (event.candidate) {
const msg = {
candidate: event.candidate.candidate,
sdpMLineIndex: event.candidate.sdpMLineIndex,
sdpMid: event.candidate.sdpMid,
};
if (answerReadyRef.current || localRole === 'Callee') wsClientRef.current.sendFormatMsg(msg, true);
else {
qRemoteCandidates.current.push(msg);
}
}
},
[localRole],
);
const handleAddStreamEvent = useCallback(
event => {
const { streams = [], stream } = event;
if (callerRef.current.ontrack) {
if (remoteVideo.srcObject !== streams[0]) {
const stream0 = streams[0];
remoteVideo.srcObject = stream0;
logger.log(` received remote stream`);
}
} else if (remoteVideo.srcObject !== stream) {
remoteVideo.srcObject = stream;
logger.log(` received remote stream`);
}
},
[remoteVideo],
);
// 成功回调
const onCreateOfferSuccess = useCallback(desc => {
callerRef.current.setLocalDescription(desc).then(
() => {
logger.log('successful');
},
error => logger.error('error', error),
);
const msg = {
type: desc.type,
sdp: desc.sdp,
};
wsClientRef.current.sendFormatMsg(msg, true);
}, []);
const call = useCallback(() => {
// 1.验证wss连接
if (!wsClientRef.current || !wsClientRef.current.conn_status) {
logger.error("call is failed case mqtt don't sub ok, please click call later!");
}
// 2.创建连接
callerRef.current = new RTCPeerConnection(configuration);
const caller = callerRef.current;
caller.onicecandidate = onIceCandidate;
caller.oniceconnectionstatechange = event => logger.log('ICE state change event: ', event);
caller.ontrack = handleAddStreamEvent;
if (localStreamRef.current){
localStreamRef.current.getTracks().forEach(track => {
caller.addTrack(track, localStreamRef.current);
});
}
caller.createOffer(offerOptions).then(onCreateOfferSuccess, error => logger.log(error));
}, [configuration, handleAddStreamEvent, onCreateOfferSuccess, onIceCandidate]);
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!