webrtc实时视频语音实现
webrtc介绍
WebRTC实现了基于网页的视频会议,标准是WHATWG 协议,目的是通过浏览器提供简单的javascript就可以达到实时通讯(Real-Time Communications (RTC))能力。
实时语音demo
环境搭建
需要信令服务器与中继服务器作为中转
1.express,socket下载,搭node后端(express)(信令服务器)
点击查看代码
npm install express
npm install socket.io
npm i加载包
node index.js启动
2.后端index.js编写
点击查看代码
var express = require('express');
var app = express();
var http = require('http').createServer(app);
//var io = require('socket.io')(http);
const fs = require('fs');
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
})
3.监听端口与websoket连接,见图3
点击查看代码
http.listen(443, () => {
console.log('https listen on');
});
io.on("connection", (socket) => {
//连接加入子房间
socket.join( socket.id );
console.log("a user connected " + socket.id);
socket.on("disconnect", () => {
console.log("user disconnected: " + socket.id);
//某个用户断开连接的时候,我们需要告诉所有还在线的用户这个信息
socket.broadcast.emit('user disconnected', socket.id);
//交流,前后端通过emit发送信息
//除自己外,对其他人广播:socket.broadcast.emit
//自己和其他人广播:io.emit
});
socket.on('new user greet', (data) => {
console.log(data);
console.log(socket.id + ' greet ' + data.msg);
socket.broadcast.emit('need connect', {sender: socket.id, msg : data.msg});
});
//在线用户回应新用户消息的转发
socket.on('ok we connect', (data) => {
io.to(data.receiver).emit('ok we connect', {sender : data.sender});
});
//sdp 消息的转发
socket.on( 'sdp', ( data ) => {
console.log('sdp');
console.log(data.description);
//console.log('sdp: ' + data.sender + ' to:' + data.to);
socket.to( data.to ).emit( 'sdp', {
description: data.description,
sender: data.sender
} );
} );
//candidates 消息的转发
socket.on( 'ice candidates', ( data ) => {
console.log('ice candidates: ');
console.log(data);
socket.to( data.to ).emit( 'ice candidates', {
candidate: data.candidate,
sender: data.sender
} );
} );
});
4.index.html代码
点击查看代码
<h1 id="user-id">用户名称</h1>
<ul id="user-list">
<li>用户列表</li>
</ul>
<audio id="video-local" preload="auto" controls>
<!-- <source src="https://192.168.3.56/a8485020-39a2-4c09-904b-2e2b87d71363" type="audio/mpeg" /> -->
</audio>
<audio id="audio">
<!-- <source src="/public/imgs/wss.wav" type="audio/mpeg" /> -->
</audio>
<ul id="videos"></ul>
<script src="//cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.js
"></script>
<script src="//cdn.bootcdn.net/ajax/libs/socket.io/3.0.4/socket.io.js
"></script>
<script>
function getUserMedia(constrains, success, error) {
let promise;
if (navigator.mediaDevices.getUserMedia) {
//最新标准API
promise = navigator.mediaDevices
.getUserMedia(constrains)
.then(success)
.catch(error);
} else if (navigator.webkitGetUserMedia) {
//webkit内核浏览器
promise = navigator
.webkitGetUserMedia(constrains)
.then(success)
.catch(error);
} else if (navigator.mozGetUserMedia) {
//Firefox浏览器
promise = navagator
.mozGetUserMedia(constrains)
.then(success)
.catch(error);
} else if (navigator.getUserMedia) {
//旧版API
promise = navigator
.getUserMedia(constrains)
.then(success)
.catch(error);
}
return promise;
}
function canGetUserMediaUse() {
return !!(
navigator.mediaDevices.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia
);
}
//中继服务器:最好自己搭,TURN中继服务器
const iceServer = {
iceServers: [
{ urls: ["stun:ss-turn1.xirsys.com"] },
{
username:
"CEqIDkX5f51sbm7-pXxJVXePoMk_WB7w2J5eu0Bd00YpiONHlLHrwSb7hRMDDrqGAAAAAF_OT9V0dWR1d2Vi",
credential: "446118be-38a4-11eb-9ece-0242ac140004",
urls: [
"turn:ss-turn1.xirsys.com:80?transport=udp",
"turn:ss-turn1.xirsys.com:3478?transport=udp",
],
},
],
};
let audio = document.getElementById("audio");
var pc = [];
function StartCall(parterName, createOffer) {
pc[parterName] = new RTCPeerConnection(iceServer);
//如果已经有本地流,那么直接获取Tracks并调用addTrack添加到RTC对象中。
if (localStream) {
localStream.getTracks().forEach((track) => {
pc[parterName].addTrack(track, localStream); //should trigger negotiationneeded event
});
} else {
//否则需要重新启动摄像头并获取
if (canGetUserMediaUse()) {
getUserMedia(
{
video: true,
audio: false,
},
function (stream) {
localStream = stream;
localVideoElm.srcObject = stream;
$(localVideoElm).width(800);
},
function (error) {
console.log(
"访问用户媒体设备失败:",
error.name,
error.message
);
}
);
} else {
alert("您的浏览器不兼容");
}
}
//如果是呼叫方,那么需要createOffer请求
if (createOffer) {
//每当WebRTC基础结构需要你重新启动会话协商过程时,都会调用此函数。它的工作是创建和发送一个请求,给被叫方,要求它与我们联系。
pc[parterName].onnegotiationneeded = () => {
//https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/createOffer
pc[parterName]
.createOffer()
.then((offer) => {
return pc[parterName].setLocalDescription(offer);
})
.then(() => {
//把发起者的描述信息通过Signal Server发送到接收者
socket.emit("sdp", {
type: "video-offer",
description: pc[parterName].localDescription,
to: parterName,
sender: socket.id,
});
});
};
}
//当需要你通过信令服务器将一个ICE候选发送给另一个对等端时,本地ICE层将会调用你的 icecandidate 事件处理程序。有关更多信息,请参阅Sending ICE candidates 以查看此示例的代码。
pc[parterName].onicecandidate = ({ candidate }) => {
socket.emit("ice candidates", {
candidate: candidate,
to: parterName,
sender: socket.id,
});
};
//当向连接中添加磁道时,track 事件的此处理程序由本地WebRTC层调用。例如,可以将传入媒体连接到元素以显示它。详见 Receiving new streams 。
pc[parterName].ontrack = (ev) => {
let str = ev.streams[0];
if (document.getElementById(`${parterName}-video`)) {
document.getElementById(`${parterName}-video`).srcObject = str;
} else {
let newVideo = document.createElement("audio");
newVideo.id = `${parterName}-video`;
newVideo.autoplay = true;
newVideo.controls = true;
//newVideo.className = 'remote-video';
newVideo.srcObject = str;
let li = document.createElement("li");
li.id = `audio-${parterName}`;
li.innerHTML = `<p>与${parterName}通话<p>`;
li.appendChild(newVideo);
let button = $('<button class="uncall" >结束通话</button>');
button.appendTo(li);
document.getElementById("videos").appendChild(li);
$('#user-list li[user-id="' + parterName + '"] .call').css(
"display",
"none"
);
}
};
}
var socket = io();
let b = 0;
socket.on("connect", () => {
InitCamera(socket.id);
//输出内容 其中 socket.id 是当前socket连接的唯一ID
console.log("connect " + socket.id);
$("form").submit(function (e) {
//禁止页面重新加载
e.preventDefault(); //prevents page reloading
//发送事件,其值为文本框中输入的值
socket.emit("chat message", $("#m").val());
$("#messages").append(
$("<li>")
.text($("#m").val() + ":" + socket.id)
.attr("class", "my")
);
//清空文本框的值
$("#m").val("");
//返回false 禁止原始的提交
return false;
});
$("#user-id").text(socket.id);
pc.push(socket.id);
socket.emit("new user greet", {
sender: socket.id,
msg: "hello world",
});
socket.on("need connect", (data) => {
console.log("need!!!!!!!!!!!!!");
console.log(data);
//创建新的li并添加到用户列表中
let li = $("<li></li>")
.text(data.sender)
.attr("user-id", data.sender);
$("#user-list").append(li);
//同时创建一个按钮
let button = $('<button class="call" >通话</button>');
button.appendTo(li);
//监听按钮的点击事件, 这是个demo 需要添加很多东西,比如不能重复拨打已经连接的用户
$(button).click(function () {
//$(this).parent().attr('user-id')
console.log($(this).parent().attr("user-id"));
//点击时,开启对该用户的通话
StartCall($(this).parent().attr("user-id"), true);
});
socket.emit("ok we connect", {
receiver: data.sender,
sender: socket.id,
});
});
//某个用户失去连接时,我们需要获取到这个信息
socket.on("user disconnected", (socket_id) => {
console.log("disconnect : " + socket_id);
$('#user-list li[user-id="' + socket_id + '"]').remove();
$("#audio-" + socket_id).remove();
$('#user-list li[user-id="' + socket_id + '"] .call').css(
"display",
"block"
);
});
//链接吧..
socket.on("ok we connect", (data) => {
console.log(data);
let li = $("<li></li>")
.text(data.sender)
.attr("user-id", data.sender);
$("#user-list").append(li);
let button = $('<button class="call">通话</button>');
button.appendTo(li);
//监听按钮的点击事件, 这是个demo 需要添加很多东西,比如不能重复拨打已经连接的用户
$(button).click(function () {
//$(this).parent().attr('user-id')
console.log($(this).parent().attr("user-id"));
//点击时,开启对该用户的通话
StartCall($(this).parent().attr("user-id"), true);
});
//这里少了程序,比如之前的按钮啊,按钮的点击监听都没有。
});
//监听发送的sdp事件
socket.on("sdp", (data) => {
//如果时offer类型的sdp
if (data.description.type === "offer") {
//那么被呼叫者需要开启RTC的一套流程,同时不需要createOffer,所以第二个参数为false
StartCall(data.sender, false);
//把发送者(offer)的描述,存储在接收者的remoteDesc中。
let desc = new RTCSessionDescription(data.description);
//按1-13流程走的
pc[data.sender].setRemoteDescription(desc).then(() => {
pc[data.sender]
.createAnswer()
.then((answer) => {
return pc[data.sender].setLocalDescription(answer);
})
.then(() => {
socket.emit("sdp", {
type: "video-answer",
description: pc[data.sender].localDescription,
to: data.sender,
sender: socket.id,
});
})
.catch(); //catch error function empty
});
} else if (data.description.type === "answer") {
//如果使应答类消息(那么接收到这个事件的是呼叫者)
let desc = new RTCSessionDescription(data.description);
pc[data.sender].setRemoteDescription(desc);
}
});
//如果是ice candidates的协商信息
socket.on("ice candidates", (data) => {
console.log("ice candidate: " + data.candidate);
//{ candidate: candidate, to: partnerName, sender: socketID }
//如果ice candidate非空(当candidate为空时,那么本次协商流程到此结束了)
if (data.candidate) {
var candidate = new RTCIceCandidate(data.candidate);
//讲对方发来的协商信息保存
pc[data.sender].addIceCandidate(candidate).catch(); //catch err function empty
}
});
});
以上domo只能本地运行,如果需要网址运行且移动端,需要https。可以选择自签证书尝试,自签证书见下一篇博客
本文来自博客园,作者:流云君,转载请注明原文链接:https://www.cnblogs.com/yun10011/p/16519819.html