运行代码
webRTC介绍
WebRTC是一个由Google发起的实时通讯解决方案,其中包含视频音频采集,编解码,数据传输,音视频展示等功能,我们可以通过技术快速地构建出一个音视频通讯应用。 虽然其名为WebRTC,但是实际上它不光支持Web之间的音视频通讯,还支持Android以及IOS端,此外由于该项目是开源的,我们也可以通过编译C++代码,从而达到全平台的互通。
getUserMedia
为一个 RTC 连接获取设备的摄像头与 (或) 麦克风权限,并为此 RTC 连接接入设备的摄像头与 (或) 麦克风的信号。
RTCPeerConnection
用于配置音频或视频聊天。
RTCDataChannel
用于设置两个浏览器之间的端到端 (en-US) 数据连接。
SRS流媒体服务器
SRS(Simple Realtime Server)是一个简单高效的实时视频服务器,支持RTMP/WebRTC/HLS/HTTP-FLV/SRT/GB28181。
SRS服务自带一个简单的信令服务器,用于webRTC交换SDP,提供了2个接口分别用于发布端(推流)和播放端(拉流)进行SDP交换
推流API:POST /rtc/v1/publish/
使用WebRTC推流到SRS时,需要先调用API交换SDP。例如:
POST /rtc/v1/publish/
Body in JSON:
{
"api": "https://d.ossrs.net/rtc/v1/publish/"
"streamurl": "webrtc://d.ossrs.net/live/3abd9f34",
"sdp": "v=0\r\n......\r\na=ssrc:2064016335 label:c8243ce9-ace5-4d17-9184-41a2543101b5\r\n"
}
服务器响应对应的SDP如下:
{
"code": 0
"sdp": "v=0\r\n......\r\na=candidate:1 1 udp 2130706431 172.18.0.4 8000 typ host generation 0\r\n"
"sessionid": "186tj710:hMub"
}
假如部署SRS服务的服务器IP地址为:192.168.5.104,SRS的http_api监听端口为1985,则推流端交换SDP的API请求地址为:http://192.168.5.104:1985/rtc/v1/publish/,发送post请求
POST /rtc/v1/publish/
Body in JSON:
{
"api": "http://192.168.5.104:1985/rtc/v1/publish/"
"streamurl": "webrtc://192.168.5.104:8000/live/stream",
"sdp": "v=0\r\n......\r\na=ssrc:2064016335 label:c8243ce9-ace5-4d17-9184-41a2543101b5\r\n"
}
拉流API:POST /rtc/v1/play/
拉流或播放时,需要调用另外的API,请求格式和publish一样。例如:
POST /rtc/v1/play/
Body in JSON:
{
"api": "https://d.ossrs.net/rtc/v1/play/"
"streamurl": "webrtc://d.ossrs.net/live/3abd9f34",
"sdp": "v=0\r\n......\r\na=ssrc:2064016335 label:c8243ce9-ace5-4d17-9184-41a2543101b5\r\n"
}
服务器响应对应的SDP如下:
{
"code": 0
"sdp": "v=0\r\n......\r\na=candidate:1 1 udp 2130706431 172.18.0.4 8000 typ host generation 0\r\n"
"sessionid": "186tj710:hMub"
}
SRS服务配置开启RTC服务
创建web应用
编写代码,vue3项目
import { onMounted,ref } from 'vue';
// 定义全局属性
let videoStream = null;
let videoElement = null;
// 全局的RTCPeerConnection
let pc = null;
// 全局音频轨道,用于RTCRtpSender发送和停止对应轨道
let audioTrack = null;
// 全局的RTCRtpSender
let audioSender = null;
// 获取按钮元素
let button_one = ref(null);
let button_two = ref(null);
let button_three = ref(null);
let button_four = ref(null);
let button_five = ref(null);
const publish = async()=>{
if(pc!==null&& pc!==undefined){
console.log("已开始推流");
return ;
}
var httpURL = "http://192.168.5.104:1985/rtc/v1/publish/";
var webRTCURL = "webRTC://192.168.5.104/live/1";
var constraints = {
audio: {
echoCancellation : true, // 回声消除
noiseSuppression : true, // 降噪
autoGainControl : true // 自动增益
},
video: {
frameRate : { min : 30 }, // 最小帧率
width : { min : 640, ideal : 1080}, // 宽度
height : { min : 360, ideal : 720}, // 高度
aspectRadio : 16/9 // 宽高比
}
}
// 通过摄像头、麦克风获取音视频流
videoStream = await navigator.mediaDevices.getUserMedia(constraints);
// 获取video元素
videoElement = document.querySelector("#video")
//video播放流数据
videoElement.srcObject = videoStream;
// 静音
videoElement.volume=0;
// 创建RTC连接对象
pc = new RTCPeerConnection();
// RTCPeerConnection方法addTransceiver()创建一个新的RTCRtpTransceiver,并将其添加到与RTCPeerConnection关联的收发器集中。
// 每个收发器代表一个双向流,RTCRtpSender和RTCRtpReceiver都与之相关联。
// 注意添加顺序为audio、video,后续RTCPeerConnection创建offer时SDP的m线顺序遵循此顺序创建,SRS自带的信令服务器响应的SDP中m线总是先audio后video。
// 若本端SDP和远端SDP中的m线顺序不一直,则设置远端描述时会异常,显示offer中的m线与answer中的m线顺序不匹配
pc.addTransceiver("audio", {direction: "recvonly"});
pc.addTransceiver("video", {direction: "recvonly"});
// 遍历getUserMedia()获取到的流数据,拿到其中的音频轨道和视频轨道,加入到RTCPeerConnection连接的音频轨道和视频轨道中
videoStream.getTracks().forEach((track)=>{
pc.addTrack(track);
});
// 创建本端offer
var offer = await pc.createOffer();
// 设置本端
await pc.setLocalDescription(offer);
var data = {
"api": httpURL,
"streamurl":webRTCURL,
"sdp":offer.sdp
}
// SDP交换,请求SRS自带的信令服务器
httpApi(httpURL,data).then(async(data)=>{
console.log("answer",data);
// 设置远端描述,开始连接
await pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: data.sdp}));
button_one.value.disabled=true;
button_two.value.disabled=false;
button_three.value.disabled=false;
button_five.value.disabled=false;
}).catch((data)=>{
if(data.code===400){
console.log("SDP交换失败");
}
});
}
const play = async()=>{
var httpURL = "http://192.168.5.104:1985/rtc/v1/play/";
var webRTCURL = "webRTC://192.168.5.104/live/1";
// 创建RTCPeerConnection连接对象
var pc = new RTCPeerConnection();
// 创建媒体流对象
var stream = new MediaStream();
// 获取播放流的容器video
var videoElement2 = document.querySelector("#video2");
// 监听流
pc.ontrack = (event)=>{
// 监听到的流加入MediaStream对象中让video播放
stream.addTrack(event.track);
videoElement2.srcObject = stream;
}
// RTCPeerConnection方法addTransceiver()创建一个新的RTCRtpTransceiver,并将其添加到与RTCPeerConnection关联的收发器集中。
// 每个收发器代表一个双向流,RTCRtpSender和RTCRtpReceiver都与之相关联。
// 注意添加顺序为audio、video,后续RTCPeerConnection创建offer时SDP的m线顺序遵循此顺序创建,SRS自带的信令服务器响应的SDP中m线总是先audio后video。
// 若本端SDP和远端SDP中的m线顺序不一直,则设置远端描述时会异常,显示offer中的m线与answer中的m线顺序不匹配
pc.addTransceiver("audio", {direction: "recvonly"});
pc.addTransceiver("video", {direction: "recvonly"});
var offer =await pc.createOffer();
await pc.setLocalDescription(offer)
var data = {
"api": httpURL,
"streamurl":webRTCURL,
"sdp":offer.sdp
}
// SDP交换,请求SRS自带的信令服务器
httpApi(httpURL,data).then(async(data)=>{
console.log("answer",data);
// 设置远端描述,开始连接
await pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: data.sdp}));
button_five.value.disabled=true;
}).catch((data)=>{
if(data.code===400){
console.log("SDP交换失败");
}
});
}
// 关闭连接
const close = ()=>{
if(pc!==null&&pc!==undefined){
pc.close();
pc = null;
button_one.value.disabled=false;
button_two.value.disabled=true;
button_three.value.disabled=true;
button_four.value.disabled=true;
button_five.value.disabled=true;
}
}
// 关闭音频
const stopAudio = ()=>{
if(pc!==null&&pc!==undefined){
// RTCPeerConnection方法getSenders()返回RTCRtpSender对象的数组,
// 每个对象代表负责传输一个轨道的数据的RTP发送器。
// sender对象提供了检查和控制音轨数据的编码和传输的方法和属性。
pc.getSenders().forEach((sender)=>{
if(sender.track!==null&&sender.track.kind==="audio"){
// 拿到音频轨道
audioTrack = sender.track;
// 拿到音频轨道发送者对象RTCRtpSender
audioSender = sender;
// RTCRtpSender的replaceTrack()可以在无需重新媒体协商的情况下用另一个媒体轨道更换当前正在发送轨道
// 参数为空则将当前正在发送的轨道停止,比如关闭音频,再次开启时将音频轨道作为参数传入
audioSender.replaceTrack(null);
button_three.value.disabled=true;
button_four.value.disabled=false;
}
});
}
}
// 开启音频
const startAudio = ()=>{
console.log(audioSender);
if(pc!==null&&pc!==undefined){
if(audioSender.track===null){
audioSender.replaceTrack(audioTrack);
button_three.value.disabled=false;
button_four.value.disabled=true;
}
}
}
const httpApi = (httpURL,data)=>{
var promise = new Promise((resolve,reject)=>{
var xhr = new XMLHttpRequest();
xhr.open('POST', httpURL, true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send(JSON.stringify(data));
xhr.onload = ()=>{
if (xhr.readyState !== xhr.DONE) reject(xhr);
if (xhr.status !== 200 && xhr.status !== 201) reject(xhr) ;
var data = JSON.parse(xhr.responseText);
if(data.code===0){
resolve(data);
}else{
reject(data)
}
}
});
return promise;
}
onMounted(()=>{
button_one.value.disabled=false;
button_two.value.disabled=true;
button_three.value.disabled=true;
button_four.value.disabled=true;
button_five.value.disabled=true;
});
*{
margin: 0;
padding: 0;
border: 0;
box-sizing: border-box;
}
#box{
width: 100%;
text-align: center;
}
video{
background-color: black;
width: 500px;
height: 400px;
object-fit: cover;
}
#btn{
width: 80%;
height: 100px;
display: flex;
margin:10px 10%;
}
button{
flex: 1;
height: 100px;
background-color: aqua;
border-radius: 20px;
margin-left: 10px;
}
button:nth-child(1){
margin-left: 0;
}
运行代码
2个客户端双向推拉流即可实现实时视频通话