~~~基于开源音视频服务组件SRS5实现一对一、一对多和视频会议功能~~~
一、前言
在工作生活中经常使用音视频通话
功能,例如:微信视频聊天、钉钉在线视频会议、视频直播等,尤其现在短视频、直播带货比较火热,在线视频相关的需求也会多了起来,项目组也经常会遇到在线视频通话相关的需求,例如,医疗相关的在线问诊、安防相关的视频在线应急事件处理等;
二、WebRTC协议
WebRTC(Web Real-Time Communication)
作为一项开放的实时通信标准,为开发者提供了快速构建实时音视频通话系统的能力;
WebRTC 是一整套 API,为浏览器、移动应用提供实时通信(RealTime Communications)能力。它包含了流媒体协议的功能,但是不是以协议的方式暴露给开发者的WebRTC
支持 Chrome 23+、Firefox 22+、Chrome for Android,提供 Java / Objective-C 绑定WebRTC
主要有三个职责:
- 捕获客户端音视频,对应接口 MediaStream(也就是 getUserMedia)
- 音视频传输,对应接口 RTCPeerConnection
- 任意数据传输,对应接口 RTCDataChannel
WebRTC
由 Google 开发,并在 2011 年首次发布。WebRTC 已经被广泛使用,并被许多知名网站和应用程序所采用,例如 Google Meet、Zoom、Skype 等。
WebRTC
使用了一系列技术来实现实时通信,包括:
- SDP(Session Description Protocol):SDP
用于描述通信会话的属性,例如参与者、协议、传输方式等。因此,参与通信双方要了解彼此支持的媒体格式,必须要交换SDP信息,而交换SDP的过程,通常称之为媒体协商 - ICE(Interactive Connectivity Establishment):ICE 用于建立通信会话的连接。
- STUN(Session Traversal Utilities for NAT):STUN 用于发现和绕过 NAT(网络地址转换)设备。
- TURN(Traversal Using Relay NAT):TURN 用于在 NAT 设备后面建立通信会话。
WebRTC 内置了点对点的支持,也就是说流不一定需要经过服务器中转。
三、常见的音视频服务
为了快速开发和减少维护成本常使用第三方的即时通讯服务实现在线音视频服务,例如阿里的百川云旺、腾讯云的即时通讯服务(IM)和网易云信等,当前市面上有很多,之前在开发一个在线视频指挥调度项目使用就是使用的网易云信的即时通讯
功能实现在线音视频通话
功能;
这里功能实现方便不用维护音视频服务器资源,但是三方音视频服务的缺点也很明显就是收费,二期长时期的费用还很高;
本着白嫖的精神首先会想到有没有免费开源的音视频服务器,肯定是有的,在网上找到两个使用人群比较的音视频服务器组件:SRS、ZLMediaKit
注:
在使用网易云信使用音视频通话功能有一个比较坑的地方就是web端调用sdk接口只支持原生js,不支持
reactjs、vue等框架;
1、SRS音视频服务
SRS是一个开源的(MIT协议)
简单高效的实时视频服务器,支持RTMP、WebRTC、HLS、HTTP-FLV、SRT、MPEG-DASH和GB28181等协议。 SRS媒体服务器和FFmpeg、OBS、VLC、 WebRTC
等客户端配合使用,提供流的接收和分发的能力,是一个典型的发布 (推流)和订阅(播放)服务器模型。 SRS支持互联网广泛应用的音视频协议转换,比如可以将RTMP或SRT, 转成HLS或HTTP-FLV或WebRTC等协议。
2、ZLMediaKit音视频服务
ZLMediaKit是一个基于C++开发的开源流媒体服务器。它提供了高性能的音视频处理能力,支持常见的流媒体协议,如RTSP、RTMP、HLS和HTTP-FLV,并且具有低延迟和高并发处理能力;支持多种协议(RTSP/RTMP/HLS/HTTP-FLV/WebSocket-FLV/GB28181/HTTP-TS/WebSocket-TS/HTTP-fMP4/WebSocket-fMP4/MP4/WebRTC),支持协议互转。
ZLMediaKit官网介绍
SRS和ZLMediaKit各有优势,本文主要介绍基于SRS实现一对一、一对多音视频通话功能,关于ZLMediaKit的功能不在做过多介绍,有需求的自行了解。
四、SRS服务搭建
通过docker镜像搭建环境,搭建步骤在官网有详细介绍,位置如下:
本机启动SRS:
export CANDIDATE="192.168.1.10"
docker run --rm --env CANDIDATE=$CANDIDATE \
-p 1935:1935 -p 8080:8080 -p 1985:1985 -p 8000:8000/udp \
registry.cn-hangzhou.aliyuncs.com/ossrs/srs:5 \
objs/srs -c conf/rtc.conf
Note: 请将CANDIDATE设置为服务器的外网地址
本机启动信令:
docker run --rm -p 1989:1989 registry.cn-hangzhou.aliyuncs.com/ossrs/signaling:1
启动httpx-static,转换HTTPS和WSS协议:
export CANDIDATE="192.168.1.10"
docker run --rm -p 80:80 -p 443:443 registry.cn-hangzhou.aliyuncs.com/ossrs/httpx:1 \
./bin/httpx-static -http 80 -https 443 -ssk ./etc/server.key -ssc ./etc/server.crt \
-proxy http://$CANDIDATE:1989/sig -proxy http://$CANDIDATE:1985/rtc \
-proxy http://$CANDIDATE:8080/
Note: 请将CANDIDATE设置为服务器的外网地址
注:如果服务部署在阿里云或者腾讯云等云服务上,一定要映射相关端口,并注意是tcp协议
还是udp协议
,比如,8000端口
就是UDP协议
,本人在使用时把UDP协议设置为了TCP协议
,导致获取远端视频失败,视频传输是UDP协议
。
五、一对一、一对多代码实现
一对一视频通话示例:
web端:
手机端:
基于SRS实现web端一对一、一对多的音视频通话的样例在SRS源码中已经给出了,也是通过原生js实现的,位置如下图:
源码:
<!DOCTYPE html>
<html>
<head>
<title>SRS</title>
<meta charset="utf-8">
<style>
body{
padding-top: 30px;
}
</style>
<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css"/>
<script type="text/javascript" src="js/jquery-1.12.2.min.js"></script>
<script type="text/javascript" src="js/adapter-7.4.0.min.js"></script>
<script type="text/javascript" src="js/srs.sdk.js"></script>
<script type="text/javascript" src="js/srs.sig.js"></script>
</head>
<body>
<img src='https://ossrs.net/gif/v1/sls.gif?site=ossrs.net&path=/player/one2one'/>
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="brand" href="https://github.com/ossrs/srs">SRS</a>
<div class="nav-collapse collapse">
<ul class="nav srs_nav">
<li class="active"><a href="one2one.html">一对一通话</a></li>
<li><a href="room.html">多人通话</a></li>
<li class="srs_ignore">
<a href="https://github.com/ossrs/signaling">
<img alt="GitHub Repo stars" src="img/shields-io-signaling.svg">
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="container">
<div class="form-inline">
SRS:
<input type="text" id="txt_host" class="input-medium" value="">
Room:
<input type="text" id="txt_room" class="input-small" value="live">
Display:
<input type="text" id="txt_display" class="input-small" value="">
<button class="btn btn-primary" id="btn_start">开始通话</button>
</div>
<div class="row">
<div class="span4 hide" id="publisher">
<label></label>
<video id="rtc_media_publisher" width="310" autoplay muted controls></video>
<label></label>
<span id='self'></span>
</div>
<div class="span6 hide" id="player">
<label></label>
<video id="rtc_media_player" width="310" autoplay muted controls></video>
<label></label>
<span id='peer'></span>
<a href="javascript:control_refresh_peer()">Refresh</a>
<input type="text" id="txt_alert" class="input-medium" value="">
<a href="javascript:control_alert_peer()">Alert</a>
</div>
</div>
<label></label>
<div class="accordion hide srs_merge">
<div class="accordion-group">
<div class="accordion-heading">
<a href="javascript:void(0)" class="accordion-toggle">FFmpeg合流转直播</a>
</div>
<div class="accordion-body collapse in">
<div class="accordion-inner" style="overflow:auto">
ffmpeg -f flv -i rtmp://<span class="ff_host"></span>/<span class="ff_app"></span>/<span class="ff_first"></span> -f flv -i rtmp://<span class="ff_host"></span>/<span class="ff_app"></span>/<span class="ff_second"></span> \ <br/>
-filter_complex "[1:v]scale=w=96:h=72[ckout];[0:v][ckout]overlay=x=W-w-10:y=H-h-10[out]" -map "[out]" \ <br/>
-c:v libx264 -profile:v high -preset medium \ <br/>
-filter_complex amix -c:a aac \ <br/>
-f flv -y
<span id="ff_output">
rtmp://<span class="ff_host"></span>/<span class="ff_app"></span>/merge
</span>
<span id="ff_wxvideo"></span>
&& <br/>
echo "ok"
</div>
<div class="accordion-inner">
<a href="#" id="ff_preview" target="_blank" class="accordion-toggle">
预览:rtmp://<span class="ff_host"></span>/<span class="ff_app"></span>/merge
</a>
</div>
</div>
</div>
</div>
<label></label>
<div class="accordion hide srs_merge">
<div class="accordion-group">
<div class="accordion-heading">
<a href="javascript:void(0)" class="accordion-toggle">
视频号推流信息
</a>
</div>
<div class="accordion-body collapse in">
<div class="accordion-inner">
推流地址 <input type="text" id="txt_wx_video_tcurl" class="input-xxlarge">
</div>
<div class="accordion-inner">
推流密钥 <input type="text" id="txt_wx_video_stream" class="input-xxlarge">
</div>
<div class="accordion-inner">
<button class="btn btn-primary" id="btn_apply">应用</button>
</div>
</div>
</div>
</div>
</div>
<footer class="footer">
<div class="container">
<p>© SRS 2020</p>
</div>
</footer>
<script type="text/javascript">
var sig = null;
var publisher = null;
var player = null;
var control_refresh_peer = null;
var control_alert_peer = null;
$(function(){
console.log('?wss=x to specify the websocket schema, ws or wss');
console.log('?wsh=x to specify the websocket server ip');
console.log('?wsp=x to specify the websocket server port');
console.log('?host=x to specify the SRS server');
console.log('?room=x to specify the room to join');
console.log('?display=x to specify your nick name');
var startDemo = async function () {
var host = $('#txt_host').val();
var room = $('#txt_room').val();
var display = $('#txt_display').val();
// Connect to signaling first.
if (sig) {
sig.close();
}
sig = new SrsRtcSignalingAsync();
sig.onmessage = function (msg) {
console.log('Notify: ', msg);
if (msg.event === 'leave') {
$('#player').hide();
}
if (msg.event === 'publish') {
if (msg.peer && msg.peer.publishing && msg.peer.display !== display) {
startPlay(host, room, msg.peer.display);
}
}
if (msg.event === 'control') {
if (msg.param === 'refresh') {
setTimeout(function () {
window.location.reload();
}, 500);
} else if (msg.param === 'alert') {
alert('From ' + msg.peer.display + ': ' + msg.data);
}
}
if (msg.participants.length >= 2) {
$('.srs_merge').show();
} else {
$('.srs_merge').hide();
}
};
await sig.connect(conf.wsSchema, conf.wsHost, room, display);
control_refresh_peer = async function () {
let r1 = await sig.send({action:'control', room:room, display:display, call:'refresh'});
console.log('Signaling: control peer to refresh ok', r1);
};
control_alert_peer = async function () {
let r1 = await sig.send({action:'control', room:room, display:display, call:'alert', data:$('#txt_alert').val()});
console.log('Signaling: control peer to alert ok', r1);
};
let r0 = await sig.send({action:'join', room:room, display:display});
console.log('Signaling: join ok', r0);
// For one to one demo, alert and ignore when room is full.
if (r0.participants.length > 2) {
alert('Room is full, already ' + (r0.participants.length - 1) + ' participants');
sig.close();
return;
}
// Start publish media if signaling is ok.
await startPublish(host, room, display);
let r1 = await sig.send({action:'publish', room:room, display:display});
console.log('Signaling: publish ok', r1);
// Play the stream already in room.
r0.participants.forEach(function(participant) {
if (participant.display === display || !participant.publishing) return;
startPlay(host, room, participant.display);
});
if (r0.participants.length >= 2) {
$('.srs_merge').show();
}
};
var startPublish = function (host, room, display) {
$(".ff_first").each(function(i,e) {
$(e).text(display);
});
var url = 'webrtc://' + host + '/' + room + '/' + display + conf.query;
$('#rtc_media_publisher').show();
$('#publisher').show();
if (publisher) {
publisher.close();
}
publisher = new SrsRtcPublisherAsync();
$('#rtc_media_publisher').prop('srcObject', publisher.stream);
return publisher.publish(url).then(function(session){
$('#self').text('Self: ' + url);
}).catch(function (reason) {
publisher.close();
$('#rtc_media_publisher').hide();
console.error(reason);
});
};
var startPlay = function (host, room, display) {
$(".ff_second").each(function(i,e) {
$(e).text(display);
});
var url = 'webrtc://' + host + '/' + room + '/' + display + conf.query;
$('#rtc_media_player').show();
$('#player').show();
if (player) {
player.close();
}
player = new SrsRtcPlayerAsync();
$('#rtc_media_player').prop('srcObject', player.stream);
player.play(url).then(function(session){
$('#peer').text('Peer: ' + display);
$('#rtc_media_player').prop('muted', false);
}).catch(function (reason) {
player.close();
$('#rtc_media_player').hide();
console.error(reason);
});
};
// Pass-by to SRS url.
let conf = SrsRtcSignalingParse(window.location);
$('#txt_host').val(conf.host);
conf.room && $('#txt_room').val(conf.room);
$('#txt_display').val(conf.display);
$(".ff_host").each(function(i,e) {
$(e).text(conf.host);
});
$(".ff_app").each(function(i,e) {
$(e).text($('#txt_room').val());
});
$('#ff_preview').attr('href', 'http://ossrs.net/players/srs_player.html?app=' + $('#txt_room').val() + '&stream=merge.flv&server=' + conf.host + '&vhost=' + conf.host + '&autostart=true');
// Update href for all navs.
$('ul.srs_nav').children('li').not('.srs_ignore').children('a').each(function (i, e) {
$(e).attr('href', $(e).attr('href') + conf.rawQuery);
});
$('#btn_apply').click(function () {
if ($('#txt_wx_video_tcurl').val() !== '' && $('#txt_wx_video_stream').val() !== '') {
$('#ff_wxvideo').text('"' + $('#txt_wx_video_tcurl').val() + $('#txt_wx_video_stream').val() + '"').show();
$('#ff_output').hide();
$('#ff_preview').parent().hide();
} else {
$('#ff_wxvideo').hide();
$('#ff_output').show();
$('#ff_preview').parent().show();
}
});
$("#btn_start").click(startDemo);
// Never play util windows loaded @see https://github.com/ossrs/srs/issues/2732
if (conf.autostart) {
window.addEventListener("load", function(){ startDemo(); });
}
});
</script>
</body>
</html>
六、总结
更好的使用技术,以熟悉技术为前提!!!基于SRS实现一对一、一对多实现音视频通话的js实现的业务逻辑的分析,后面文章介绍!!!
----------------------------------👇👇👇注:更多信息请关注公众号获取
👇👇👇--------------------------------------------