WebRTC DEMO
花了兩天時(shí)間簡(jiǎn)單了解了一下WEB RTC
烹笔,并由此寫(xiě)入三個(gè)DEMO。
- p2p 點(diǎn)對(duì)點(diǎn)
- o2m 一對(duì)多
- live 直播
目前主要都是按p2p
進(jìn)行的簡(jiǎn)單擴(kuò)展打厘。
WebRTC 簡(jiǎn)單了解
目前資料不算少,不過(guò)確實(shí)也不多,而且理論偏多玻驻,新手入門(mén)其實(shí)還是有點(diǎn)壓力的供常。
這邊推薦幾個(gè)資料和視頻摊聋。
MDN 文檔 記得出問(wèn)題看看文檔先
WebRTC samples 沒(méi)有思路的時(shí)候記得看看
嗶哩嗶哩 - 一只斌 這個(gè)b站up,大概算是由淺入深講了這個(gè)東西栈暇,但是有些基礎(chǔ)概念被帶過(guò)了(應(yīng)該主要是我基礎(chǔ)較薄弱)麻裁,其中圣誕特輯中的流程其實(shí)還是比較清晰的。
知乎 - 為什么webrtc那么貴? 注意看評(píng)論區(qū)
知乎 - 可以用WebRTC來(lái)做視頻直播嗎煎源? 注意看評(píng)論區(qū)
WebRTC 的一些概念
WebRTC基礎(chǔ)情況下只需要一個(gè) ‘信令服務(wù)’ 作為業(yè)務(wù)需求色迂,并不需要直接管理流。
p2p
點(diǎn)對(duì)點(diǎn)通信(pc與pc直接通信)薪夕,不通過(guò)服務(wù)器
信令服務(wù)
用于建立通信和業(yè)務(wù)交互的服務(wù)端
SDP
存放媒體信息脚草、會(huì)話的描述。如編碼解碼信息
NAT / STUN / TURN / ice
strn
用于p2p連接原献。(下面僅個(gè)人理解
由于沒(méi)有公網(wǎng)ip的兩個(gè)主機(jī)沒(méi)有辦法直接進(jìn)行直接通信馏慨,所以需要一個(gè)“中轉(zhuǎn)的服務(wù)器”,但是由于中轉(zhuǎn)服務(wù)器過(guò)于依賴服務(wù)器帶寬姑隅,所以采用NAT穿刺
写隶,這樣雙方通信就不需要依賴服務(wù)器。
ice
整合了STUN和TURN的框架
實(shí)際具體不需要管讲仰,ice 服務(wù)器可以使用公開(kāi)的
實(shí)踐
webrtc建立還是很簡(jiǎn)單的慕趴,只需要交換雙方sdp
和ice-candidate
,即可建立通信鄙陡。
具體流程
p2p 1對(duì)1 視頻
呼叫方創(chuàng)建
offer sdp
接收方根據(jù)offer sdp
創(chuàng)建answer sdp
一冕房、 sdp 交換
- 呼叫方 建立WebRTC
-
接收方 等待 信令服務(wù)器 轉(zhuǎn)發(fā) 類型為
offer
的sdp
-
呼叫方 監(jiān)聽(tīng)
onnegotiationneeded
并創(chuàng)建offer sdp
并調(diào)用setLocalDescription
設(shè)置為本地描述 -
呼叫方 向 信令服務(wù)器 發(fā)送
offer sdp
并監(jiān)聽(tīng)answer sdp
-
接收方 得到
offer sdp
并調(diào)用setRemoteDescription
設(shè)置為遠(yuǎn)程描述 -
接收方 創(chuàng)建
answer sdp
并設(shè)置本地描述(setLocalDescription
) 同時(shí)向 信令服務(wù)器 發(fā)送answer sdp
-
呼叫方 收到
answer sdp
并設(shè)遠(yuǎn)程描述(setRemoteDescription
)
二、 ice-candidate 交換
- 監(jiān)聽(tīng)
onicecandidate
得到candidate
后進(jìn)行發(fā)送 - 監(jiān)聽(tīng)
信令服務(wù)器
ice-candidate 得到后調(diào)用addIceCandidate
獲取媒體 流
function getUserMedia(constrains) {
let promise = null;
if (navigator.mediaDevices.getUserMedia) {
//最新標(biāo)準(zhǔn)API
promise = navigator.mediaDevices.getUserMedia(constrains)
} else if (navigator.webkitGetUserMedia) {
//webkit內(nèi)核瀏覽器
promise = navigator.webkitGetUserMedia(constrains)
} else if (navigator.mozGetUserMedia) {
//Firefox瀏覽器
promise = navagator.mozGetUserMedia(constrains)
} else if (navigator.getUserMedia) {
//舊版API
promise = navigator.getUserMedia(constrains);
}
return promise;
}
// 得到流
const stream = await getUserMedia({
video: true,
audio: true,
});
// 展示
$('video').srcObject = stream;
信令服務(wù)器
demo使用 node + socket-io 做信令服務(wù)器
目前邏輯很簡(jiǎn)單趁矾,只為數(shù)據(jù)定向轉(zhuǎn)發(fā)
目前主要對(duì)sdp
和ice candidate
進(jìn)行一個(gè)定向轉(zhuǎn)發(fā)
const SocketIo = require('socket.io');
const consola = require('consola'); // log 工具
const users = new Map(); // 用戶存儲(chǔ)
/**
*
* @param {http.Server} server
*/
const signaling = (server) => {
// 創(chuàng)建socketio服務(wù)
const io = new SocketIo.Server(server);
const p2p = io.of('/p2p');
// 連接
p2p.on('connect', (socket) => {
consola.info('[%s] connect', socket.id);
// **********
// 用戶操作
// sdp 轉(zhuǎn)發(fā)
socket.on('sdp', (data) => {
console.log('sdp data.to[%s] type[%s]', data.to, data.type);
const user = users.get(data.to)
if (user) {
user.emit('sdp', data);
}
});
// ice-candidate 轉(zhuǎn)發(fā)
socket.on('ice-candidate', (data) => {
console.log('ice-candidate data.to', data.to);
const user = users.get(data.to)
if (user) {
user.emit('ice-candidate', data);
}
});
// 用戶操作
// **********
// ----------
// 用戶操作
socket.once('disconnect', () => {
consola.info('[%s] disconnect', socket.id);
users.delete(socket.id);
p2p.emit('leave', {
user: socket.id
});
});
socket.emit('users', {
users: Array.from(users.keys())
});
p2p.emit('join', {
user: socket.id
});
users.set(socket.id, socket);
// 用戶操作
// ----------
});
};
module.exports = signaling;
建立WebRTC
這里在代碼中區(qū)分了發(fā)送和接收具體可參考業(yè)務(wù)
呼叫方
// ************
// 呼叫方
// ************
// 1. 建立rct連接
const pc = new RTCPeerConnection({
iceServers: [
{
urls: ["stun:stun.counterpath.net:3478"] // 可以直接百度找一些開(kāi)放的stun服務(wù)器
}
]
});
// 2. 綁定流
const stream = await getUserMedia({
video: true,
audio: true,
});
// 添加媒體軌道 如果 video 和 audio 都為true 則 getTracks 可以獲得兩個(gè)軌道
stream.getTracks().forEach(track => pc[toUser].addTrack(track, stream));
// 3. 監(jiān)聽(tīng)
pc.onnegotiationneeded = ()=>{
pc
.createOffer() // 創(chuàng)建 offer sdp
.then((offer) => {
// 設(shè)置為本地描述
return pc.setLocalDescription(offer);
})
.then(() => {
// 定向轉(zhuǎn)發(fā) sdp
socket.emit('sdp', {
type: 'sender',
value: pc.localDescription
});
});
}
pc.onicecandidate = (ev)=>{
// 轉(zhuǎn)發(fā) ice-candidate
socket.emit('ice-candidate', {
type: 'sender',
value: ev.candidate,
});
}
pc.ontrack = (ev)=>{
// 這里可以的得到對(duì)方的流
let stream = ev.streams[0];
}
// 監(jiān)聽(tīng) ice 和 sdp
socket.on('ice-candidate', (data)=>{
if(data.type === 'receive'){
const candidate = new RTCIceCandidate(data.value);
pc.addIceCandidate(candidate)
}
});
socket.on('sdp', (data)=>{
if(data.type === 'receive'){
const sdp = new RTCSessionDescription(data.value);
pc.setRemoteDescription(sdp);
}
});
接收方
// ************
// 接收方
// ************
// 建立rct連接
const pc = new RTCPeerConnection({
iceServers: [
{
urls: ["stun:stun.counterpath.net:3478"] // 可以直接百度找一些開(kāi)放的stun服務(wù)器
}
]
});
socket.on('sdp', async (data)=>{
if(data.type === 'sender'){
const sdp = new RTCSessionDescription(data.value);
pc.setRemoteDescription(sdp);
// 綁定流
const stream = await getUserMedia({
video: true,
audio: true,
});
// 添加媒體軌道 如果 video 和 audio 都為true 則 getTracks 可以獲得兩個(gè)軌道
stream.getTracks().forEach(track => pc[toUser].addTrack(track, stream));
// 監(jiān)聽(tīng)
pc.onnegotiationneeded = ()=>{
pc
.createAnswer() // 創(chuàng)建 offer sdp
.then((answer) => {
// 設(shè)置為本地描述
return pc.setLocalDescription(answer);
})
.then(() => {
// 定向轉(zhuǎn)發(fā) sdp
socket.emit('sdp', {
type: 'receive',
value: pc.localDescription
});
});
}
pc.onicecandidate = (ev)=>{
// 轉(zhuǎn)發(fā) ice-candidate
socket.emit('ice-candidate', {
type: 'receive',
value: ev.candidate,
});
}
pc.ontrack = (ev)=>{
// 這里可以的得到對(duì)方的流
let stream = ev.streams[0];
}
}
});
// 監(jiān)聽(tīng) ice 和 sdp
socket.on('ice-candidate', (data)=>{
if(data.type === 'sender'){
const candidate = new RTCIceCandidate(data.value);
pc.addIceCandidate(candidate)
}
});