# 使用流程
git clone https://gitee.com/wjj0720/webrtc.git
cd ./webRTC
npm i
npm run dev
# 访问 127.0.0.1:3003/test-1.html 演示h5媒体流捕获
# 访问 127.0.0.1:3003/local.html 演示rtc 本地传输
# 访问 127.0.0.1:3003/p2p.html 演示局域网端对端视屏
闲话:目前主流实时流媒体 实现方式
RTP :(Real-time Transport Protocol) 建立在 UDP 协议上的一种协议加控制
HLS(HTTP Live Streamin)苹果公司实现的基于HTTP的流媒体传输协议
RTMP(Real Time Messaging Protocol) Adobe公司基于TCP
WebRTC google 基于RTP协议
navigator.mediaDevices.getUserMedia({
video: true, // 摄像头
audio: true // 麦克风
}).then(steam => {
// video标签的srcObject
video.srcObject = stream
}).catch(e => {
console.log(e)
})
以 A<=>B 创建p2p连接为例
A端:
1.创建RTCPeerConnection实例:peerA
2.将自己本地媒体流(音、视频)加入实例,peerA.addStream
3.监听来自远端传输过来的媒体流 peerA.onaddstream
4.创建[SDP offer]目的是启动到远程(此时的远端也叫候选人)))对等点的新WebRTC连接 peerA.createOffer
5.通过[信令服务器]将offer传递给呼叫方
6.收到answer后去[stun]服务拿到自己的IP,通过信令服务将其发送给呼叫放
B端:
1.收到信令服务的通知 创建RTCPeerConnection peerB,
2.也需要将自己本地媒体流加入通信 peerB.addstream
3.监听来自远端传输过来的媒体流 peerA.onaddstream
4.同样创建[SDP offer] peerA.createAnswer
5.通过[信令服务器]将Answer传递给呼叫方
6.收到对方IP 同样去[stun]服务拿到自己的IP 传递给对方
至此完成p2p连接 触发双发onaddstream事件
信令服务器:
webRTC中负责呼叫建立、监控(Supervision)、拆除(Teardown)的系统
为什么需要:
webRTC是p2p连接,那么连接之前如何获得对方信息,有如何将自己的信息发送给对方,这就需要信令服务
什么是SDP
SDP 完全是一种会话描述格式 ― 它不属于传输协议
它只使用不同的适当的传输协议,包括会话通知协议(SAP)、会话初始协议(SIP)、实时流协议(RTSP)、MIME 扩展协议的电子邮件以及超文本传输协议(HTTP)
SDP协议是基于文本的协议,可扩展性比较强,这样就使其具有广泛的应用范围。
WebRTC中SDP
SDP不支持会话内容或媒体编码的协商。webrtc中sdp用于媒体信息(编码解码信息)的描述,媒体协商这一块要用RTP来实现
1.什么是STUN
STUN(Session Traversal Utilities for NAT,NAT会话穿越应用程序)是一种网络协议,它允许位于NAT(或多重NAT)后的客户端找出自己的公网地址,查出自己位于哪种类型的NAT之后以及NAT为某一个本地端口所绑定的Internet端端口。这些信息被用来在两个同时处于NAT路由器之后的主机之间创建UDP通信。这种通过穿过路由直接通信的方式叫穿墙
2.什么是NAT
NAT(Network Address Translation,网络地址转换),是1994年提出的。当在专用网内部的一些主机本来已经分配到了本地IP地址,但现在又想和因特网上的主机通信时,于是乎在路由器上安装NAT软件。装有NAT软件的路由器叫做NAT路由器,它可以通过一个全球IP地址。使所有使用本地地址的主机在和外界通信时,这种通过使用少量的公有IP地址代表较多的私有IP地址的方式,将有助于减缓可用的IP地址空间的枯竭
3.WebRTC的穿墙
目前常用的针对UDP连接的NAT穿透方法主要有:STUN、TURN、ICE、uPnP等。其中ICE方式由于其结合了STUN和TURN的特点 webrtc是用的就是这个
google提供的免费地址:https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>端对端</title>
</head>
<body>
<div class="page-container">
<div class="message-box">
<ul class="message-list"></ul>
<div class="send-box">
<textarea class="send-content"></textarea>
<button class="sendbtn">发送</button>
</div>
</div>
<div class="user-box">
<video id="local-video" autoplay class="local-video"></video>
<video id="remote-video" autoplay class="remote-video"></video>
<p class="title">在线用户</p>
<ul class="user-list"></ul>
</div>
<div class="mask">
<div class="mask-content">
<input class="myname" type="text" placeholder="输入用户名加入房间">
<button class="add-room">加入</button>
</div>
</div>
<div class="video-box">
</div>
</div>
<script src="/js/jquery.js"></script>
<script src="/js/socket.io.js"></script>
<script>
// 简单封装一下
class Chat {
constructor({ calledHandle, host, socketPath, getCallReject } = {}) {
this.host = host
this.socketPath = socketPath
this.socket = null
this.calledHandle = calledHandle
this.getCallReject = getCallReject
this.peer = null
this.localMedia = null
}
async init() {
this.socket = await this.connentSocket()
return this
}
async connentSocket() {
if (this.socket) return this.socket
return new Promise((resolve, reject) => {
let socket = io(this.host, { path: this.socketPath })
socket.on("connect", () => {
console.log("连接成功!")
resolve(socket)
})
socket.on("connect_error", e => {
console.log("连接失败!")
throw e
reject()
})
// 呼叫被接受
socket.on('answer', ({ answer }) => {
this.peer && this.peer.setRemoteDescription(answer)
})
// 被呼叫事件
socket.on('called', callingInfo => {
this.called && this.called(callingInfo)
})
// 呼叫被拒
socket.on('callRejected', () => {
this.getCallReject && this.getCallReject()
})
socket.on('iceCandidate', ({ iceCandidate }) => {
console.log('远端添加iceCandidate');
this.peer && this.peer.addIceCandidate(new RTCIceCandidate(iceCandidate))
})
})
}
addEvent(name, cb) {
if (!this.socket) return
this.socket.on(name, (data) => {
cb.call(this, data)
})
}
sendMessage(name, data) {
if (!this.socket) return
this.socket.emit(name, data)
}
// 获取本地媒体流
async getLocalMedia() {
let localMedia = await navigator.mediaDevices
.getUserMedia({ video: { facingMode: "user" }, audio: true })
.catch(e => {
console.log(e)
})
this.localMedia = localMedia
return this
}
// 设置媒体流到video
setMediaTo(eleId, media) {
document.getElementById(eleId).srcObject = media
}
// 被叫响应
called(callingInfo) {
this.calledHandle && this.calledHandle(callingInfo)
}
// 创建RTC
createLoacalPeer() {
this.peer = new RTCPeerConnection()
return this
}
// 将媒体流加入通信
addTrack() {
if (!this.peer || !this.localMedia) return
//this.localMedia.getTracks().forEach(track => this.peer.addTrack(track, this.localMedia));
this.peer.addStream(this.localMedia)
return this
}
// 创建 SDP offer
async createOffer(cb) {
if (!this.peer) return
let offer = await this.peer.createOffer({ OfferToReceiveAudio: true, OfferToReceiveVideo: true })
this.peer.setLocalDescription(offer)
cb && cb(offer)
return this
}
async createAnswer(offer, cb) {
if (!this.peer) return
this.peer.setRemoteDescription(offer)
let answer = await this.peer.createAnswer({ OfferToReceiveAudio: true, OfferToReceiveVideo: true })
this.peer.setLocalDescription(answer)
cb && cb(answer)
return this
}
listenerAddStream(cb) {
this.peer.addEventListener('addstream', event => {
console.log('addstream事件触发', event.stream);
cb && cb(event.stream);
})
return this
}
// 监听候选加入
listenerCandidateAdd(cb) {
this.peer.addEventListener('icecandidate', event => {
let iceCandidate = event.candidate;
if (iceCandidate) {
console.log('发送candidate给远端');
cb && cb(iceCandidate);
}
})
return this
}
// 检测ice协商过程
listenerGatheringstatechange () {
this.peer.addEventListener('icegatheringstatechange', e => {
console.log('ice协商中: ', e.target.iceGatheringState);
})
return this
}
// 关闭RTC
closeRTC() {
// ....
}
}
</script>
<script>
$(function () {
let chat = new Chat({
host: 'http://127.0.0.1:3003',
socketPath: "/websocket",
calledHandle: calledHandle,
getCallReject: getCallReject
})
// 更新用户列表视图
function updateUserList(list) {
$(".user-list").html(list.reduce((temp, li) => {
temp += `<li>${li.name} <button data-calling=${li.calling} data-id=${li.id} class=${li.id === this.socket.id || li.calling ? 'cannot-call' : 'can-call'}> 通话</button></li>`
return temp
}, ''))
}
// 更新消息li表视图
function updateMessageList(msg) {
$('.message-list').append(`<li class=${msg.userId === this.socket.id ? 'left' : 'right'}>${msg.user}: ${msg.content}</li>`)
}
// 加入房间
$('.add-room').on('click', async () => {
let name = $('.myname').val()
if (!name) return
$('.mask').fadeOut()
await chat.init()
// 用户加入事件
chat.addEvent('updateUserList', updateUserList)
// 消息更新事件
chat.addEvent('updateMessageList', updateMessageList)
chat.sendMessage('addUser', { name })
})
// 发送消息
$('.sendbtn').on('click', () => {
let sendContent = $('.send-content').val()
if (!sendContent) return
$('.send-content').val('')
chat.sendMessage('sendMessage', { content: sendContent })
})
// 视屏
$('.user-list').on('click', '.can-call', async function () {
// 被叫方信息
let calledParty = $(this).data()
if (calledParty.calling) return console.log('对方正在通话');
// 初始本地视频
$('.local-video').fadeIn()
await chat.getLocalMedia()
chat.setMediaTo('local-video', chat.localMedia)
chat.createLoacalPeer()
.listenerGatheringstatechange()
.addTrack()
.listenerAddStream(function (stream) {
$('.remote-video').fadeIn()
chat.setMediaTo('remote-video', stream)
})
.listenerCandidateAdd(function (iceCandidate) {
chat.sendMessage('iceCandidate', { iceCandidate, id: calledParty.id })
})
.createOffer(function (offer) {
chat.sendMessage('offer', { offer, ...calledParty })
})
})
//呼叫被拒绝
function getCallReject() {
chat.closeRTC()
$('.local-video').fadeIn()
console.log('呼叫被拒');
}
// 被叫
async function calledHandle(callingInfo) {
if (!confirm(`是否接受${callingInfo.name}的视频通话`)) {
chat.sendMessage('rejectCall', callingInfo.id)
return
}
$('.local-video').fadeIn()
await chat.getLocalMedia()
chat.setMediaTo('local-video', chat.localMedia)
chat.createLoacalPeer()
.listenerGatheringstatechange()
.addTrack()
.listenerCandidateAdd(function (iceCandidate) {
chat.sendMessage('iceCandidate', { iceCandidate, id: callingInfo.id })
})
.listenerAddStream(function (stream) {
$('.remote-video').fadeIn()
chat.setMediaTo('remote-video', stream)
})
.createAnswer(callingInfo.offer, function (answer) {
chat.sendMessage('answer', { answer, id: callingInfo.id })
})
}
})
</script>
</body>
</html>
const SocketIO = require('socket.io')
const socketIO = new SocketIO({
path: '/websocket'
})
let userRoom = {
list: [],
add(user) {
this.list.push(user)
return this
},
del(id) {
this.list = this.list.filter(u => u.id !== id)
return this
},
sendAllUser(name, data) {
this.list.forEach(({ id }) => {
console.log('>>>>>', id)
socketIO.to(id).emit(name, data)
})
return this
},
sendTo(id) {
return (eventName, data) => {
socketIO.to(id).emit(eventName, data)
}
},
findName(id) {
return this.list.find(u => u.id === id).name
}
}
socketIO.on('connection', function(socket) {
console.log('连接加入.', socket.id)
socket.on('addUser', function(data) {
console.log(data.name, '加入房间')
let user = {
id: socket.id,
name: data.name,
calling: false
}
userRoom.add(user).sendAllUser('updateUserList', userRoom.list)
})
socket.on('sendMessage', ({ content }) => {
console.log('转发消息:', content)
userRoom.sendAllUser('updateMessageList', { userId: socket.id, content, user: userRoom.findName(socket.id) })
})
socket.on('iceCandidate', ({ id, iceCandidate }) => {
console.log('转发信道')
userRoom.sendTo(id)('iceCandidate', { iceCandidate, id: socket.id })
})
socket.on('offer', ({id, offer}) => {
console.log('转发offer')
userRoom.sendTo(id)('called', { offer, id: socket.id, name: userRoom.findName(socket.id)})
})
socket.on('answer', ({id, answer}) => {
console.log('接受视频');
userRoom.sendTo(id)('answer', {answer})
})
socket.on('rejectCall', id => {
console.log('转发拒接视频')
userRoom.sendTo(id)('callRejected')
})
socket.on('disconnect', () => {
// 断开删除
console.log('连接断开', socket.id)
userRoom.del(socket.id).sendAllUser('updateUserList', userRoom.list)
})
})
module.exports = socketIO
// www.js 这就不关键了
const http = require('http')
const app = require('../app')
const socketIO = require('../socket.js')
const server = http.createServer(app.callback())
socketIO.attach(server)
server.listen(3003, () => {
console.log('server start on 127.0.0.1:3003')
})
# 编译
cd coturn
./configure --prefix=/usr/local/coturn
sudo make -j 4 && make install
# 配置
listening-port=3478 #指定侦听的端口
external-ip=39.105.185.198 #指定云主机的公网IP地址
user=aaaaaa:bbbbbb #访问 stun/turn服务的用户名和密码
realm=stun.xxx.cn #域名,这个一定要设置
#启动
cd /usr/local/coturn/bin
turnserver -c ../etc/turnserver.conf
trickle-ice https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice 按里面的要求输入 stun/turn 地址、用户和密码
输入的信息分别是:
STUN or TURN URI 的值为: turn:stun.xxx.cn
用户名为: aaaaaa
密码为: bbbbbb
let ice = {"iceServers": [
{"url": "stun:stun.l.google.com:19302"}, // 无需密码的
// TURN 一般需要自己去定义
{
'url': 'turn:192.158.29.39:3478?transport=udp',
'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=', // 密码
'username': '28224511:1379330808' // 用户名
},
{
'url': 'turn:192.158.29.39:3478?transport=tcp',
'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
'username': '28224511:1379330808'
}
]}
// 可以提供多iceServers地址,但RTC追选择一个进行协商
// 实例化的是给上参数 RTC会在合适的时候去获取本地墙后IP
let pc = new RTCPeerConnection(ice);
/*
// 据说这些免费的地址都可以用
stun:stun1.l.google.com:19302
stun:stun2.l.google.com:19302
stun:stun3.l.google.com:19302
stun:stun4.l.google.com:19302
stun:23.21.150.121
stun:stun01.sipphone.com
stun:stun.ekiga.net
stun:stun.fwdnet.net
stun:stun.ideasip.com
stun:stun.iptel.org
stun:stun.rixtelecom.se
stun:stun.schlund.de
stun:stunserver.org
stun:stun.softjoys.com
stun:stun.voiparound.com
stun:stun.voipbuster.com
stun:stun.voipstunt.com
stun:stun.voxgratia.org
stun:stun.xten.com
*/
来源:https://segmentfault.com/a/1190000020741658
在前面的章节中,已经对WebRTC相关的重要知识点进行了介绍,包括涉及的网络协议、会话描述协议、如何进行网络穿透等,剩下的就是WebRTC的API了。WebRTC通信相关的API非常多,主要完成了如下功能:
WebRTC API包括媒体捕获、音频视频的编码和解码、传输层和会话管理。getUserMedia():捕获音频和视频。MediaRecorder:录制音频和视频。RTCPeerConnection:在用户之间传输音频和视频。RTCDataChannel:用户之间的流数据。
RTCPeerConnection API是每个浏览器之间点对点连接的核心,RTCPeerConnection是WebRTC组件,用于处理对等体之间流数据的稳定和有效通信。RTCPeerConnection可以保护Web开发人员免受潜伏在其中的无数复杂性的影响。
这是 MDN 上对 WebRTC 的描述,初次接触时无法理解 WebRTC 为什么要和 WebSocket 搭配,明明说的很清楚 不借助中间媒介 ,那 WebSocket 充当的是什么角色?整个 WebRTC 通话建立的流程又是怎样的?
本文介绍如何基于WebRTC快速实现一个简单的实时音视频通话。在开始之前,您可以先了解一些实时音视频推拉流相关的基础概念:流:一组按指定编码格式封装的音视频数据内容。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!