上傳了網(wǎng)站源碼怎么做新聞最新熱點(diǎn)
介紹
WebRTC (Web Real-Time Communications) 是一個(gè)實(shí)時(shí)通訊技術(shù),也是實(shí)時(shí)音視頻技術(shù)的標(biāo)準(zhǔn)和框架。簡(jiǎn)單來(lái)說(shuō)WebRTC是一個(gè)集大成的實(shí)時(shí)音視頻技術(shù)集,包含了各種客戶端api、音視頻編/解碼lib、流媒體傳輸協(xié)議、回聲消除、安全傳輸?shù)?。?duì)于開發(fā)者來(lái)說(shuō)可以借助webrtc非常方便的實(shí)現(xiàn)低延時(shí)視頻通話能力。目前大多主流的直播系統(tǒng)、會(huì)議系統(tǒng)基本都是基于WebRTC來(lái)實(shí)現(xiàn)。
三種架構(gòu)
WebRTC針對(duì)不同場(chǎng)景以及性能考慮提供了三種架構(gòu)Mesh架構(gòu)、MCU、FSU。
Mesh架構(gòu)
Mesh架構(gòu),需要所有參與連接的peer建立與所有其他peer的媒體連接(兩兩連接)。該架構(gòu)需要n-1個(gè)上下行,以此帶來(lái)的帶寬消耗(流量)、編/解碼消耗(設(shè)備性能)成線性增長(zhǎng)。該架構(gòu)只能適用3-4個(gè)人的小型會(huì)議場(chǎng)景。
MCU架構(gòu)
所有參與連接的peer將本地媒體流推到遠(yuǎn)程媒體服務(wù)器,由媒體服務(wù)器進(jìn)行混流,然后再推到所有連接的peer端。該架構(gòu)的優(yōu)點(diǎn)就是只需要1路上下行,隨著peer人數(shù)不斷增加,依然不會(huì)對(duì)用戶造成帶寬、手機(jī)性能影響。該架構(gòu)將壓力轉(zhuǎn)嫁到服務(wù)端,由專用媒體服務(wù)器來(lái)完成混流,轉(zhuǎn)推等功能。
SFU架構(gòu)
相對(duì)于MCU來(lái)說(shuō)SFU只做轉(zhuǎn)發(fā),媒體服務(wù)器壓力有限。與Mesh架構(gòu)相比,只需要n-1個(gè)下行,1個(gè)上行,減少了服務(wù)器壓力。在大規(guī)模的場(chǎng)合該架構(gòu)具有伸縮性。
點(diǎn)對(duì)點(diǎn)視頻連接
根據(jù)上面,我們對(duì)基本W(wǎng)ebRTC有了最基本的認(rèn)識(shí),下面就從點(diǎn)對(duì)點(diǎn)實(shí)際例子來(lái)從代碼角度進(jìn)一步了解其原理。
先從下圖來(lái)看看使用MCU來(lái)實(shí)現(xiàn)點(diǎn)對(duì)點(diǎn)需要哪些東西
在介紹流程之前,先簡(jiǎn)單介紹下上圖中出現(xiàn)的名詞代表什么意思:
- Peer:通信雙方設(shè)備。
- Signaling Server: 信令服務(wù)器,用于交互連接雙方的信令數(shù)據(jù)(SDP、ICE等),以保證通信的對(duì)等連接建立。
- NAT:處理私有網(wǎng)絡(luò)和公共網(wǎng)絡(luò)之間的地址轉(zhuǎn)換問(wèn)題(因?yàn)榇蠖鄶?shù)設(shè)置都處于內(nèi)網(wǎng)中,需要轉(zhuǎn)換為公共網(wǎng)絡(luò)才能進(jìn)行外網(wǎng)訪問(wèn))
- STUN:用于發(fā)現(xiàn)設(shè)備的公共地址(通過(guò)NAT轉(zhuǎn)換的公網(wǎng)地址),輔助穿越NAT進(jìn)行點(diǎn)對(duì)點(diǎn)連接。
- TURN:在無(wú)法建立直接連接時(shí)提供數(shù)據(jù)中繼,確保通信的可靠性。對(duì)等連接異常時(shí)的兜底方案。
- SDP:會(huì)話描述協(xié)議,用于描述和協(xié)商媒體會(huì)話的協(xié)議,它定義了會(huì)話的所有技術(shù)細(xì)節(jié),包括媒體格式、編解碼器、網(wǎng)絡(luò)地址等。,
- ICE:用于發(fā)現(xiàn)和選擇最優(yōu)網(wǎng)絡(luò)路徑的框架,確保在各種網(wǎng)絡(luò)環(huán)境下都能成功建立和維持連接。
代碼實(shí)現(xiàn)
實(shí)現(xiàn)點(diǎn)對(duì)點(diǎn)連接主要是兩點(diǎn):1、信令數(shù)據(jù)交互 2、對(duì)等連接建立
在代碼中使用到了socket.io來(lái)將設(shè)備和信令服務(wù)器通信,使用了simple-peer來(lái)建立對(duì)等連接,由于該demo在本地運(yùn)行所以沒(méi)有使用STUN/TRUN服務(wù)器,有興趣的可以使用Chrome提供的公共服務(wù)器stun:stun.l.google.com:19302
主要步驟如下:
- 1、和信令服務(wù)器建立連接,并獲取自身的socketId作為唯一標(biāo)識(shí)
- 2、申請(qǐng)方將信令(由simple-peer生成)通過(guò)信令服務(wù)器到達(dá)接受方
- 3、接受方接受,將發(fā)起方的信令保存到對(duì)等連接peer中,并且將自己的信令通過(guò)信令服務(wù)器給到發(fā)送方
- 4、發(fā)送方將接受方的信令數(shù)據(jù)保存到對(duì)等連接peer中,至此發(fā)送方-接受方對(duì)等連接建立完成
- 5、在發(fā)送方和接受方監(jiān)聽peer的stream,來(lái)獲取視頻流,然后展示在頁(yè)面
和信令服務(wù)器建立連接
新建一個(gè)server,js使用node+express搭建的簡(jiǎn)易信令服務(wù)器,用于交換雙方信令。通過(guò)create-react-app來(lái)創(chuàng)建一個(gè)前端頁(yè)面。
信令服務(wù)器代碼如下:
const express = require("express");
const http = require("http");
const cors = require("cors");const app = express();
const server = http.createServer(app);
app.use(cors);
const io = require("socket.io")(server, {cors: {origin: "*",methods: ["POST", "GET"],},
});server.listen(5001, () => {console.log("listening on 5000 ...");
});io.on("connection", (socket) => {// 分發(fā)socket idsocket.emit("offer", socket.id);// 發(fā)送發(fā)起方的信令數(shù)據(jù)別answersocket.on("callUser", (data) => {io.to(data.answerId).emit("callUser", data);});// 發(fā)送接收放信令給申請(qǐng)方socket.on("answerSignalInfo", (data) => {io.to(data.to).emit("answerSignalInfo", data);});socket.on("disconnect", () => {socket.broadcast.emit("callEnded", socket);});
});
// frontend
// 通過(guò)socket.io和服務(wù)器進(jìn)行連接
const socket = io("http://localhost:5001");// 獲取自身的socket id
socket.on("offer", (offerId) => {console.log("offer socket ID", offerId);setOfferId(offerId);getLocalStream(); // 獲取本地視頻流
});
傳遞信令數(shù)據(jù)
// 通過(guò)simple-peer 交換信令數(shù)據(jù) offer -> 信令服務(wù)器 -> answer
const peer = new Peer({initiator: true, // 是否是發(fā)起方stream: localStream, // 傳遞的視頻流trickle: false, // 點(diǎn)對(duì)點(diǎn)傳輸,獲取單個(gè)信號(hào)// 設(shè)置STUN服務(wù)器,Chrome提供的公共服務(wù)器config: {iceServers: [{ urls: "stun:stun.l.google.com:19302" }],},
});
peer.on("signal", (data: any) => {socket.emit("callUser", {singleData: data, // 發(fā)送通話方的信令數(shù)據(jù)answerId: answerId, // 需要和誰(shuí)通話from: offerId, // 誰(shuí)申請(qǐng)通話});
});
接收信令數(shù)據(jù)
接收方接收發(fā)起方的信令數(shù)據(jù),并保存到Peer中,然后將自身的信令數(shù)據(jù)返回給發(fā)起方
const peer = new Peer({initiator: false,stream: localStream,trickle: false,config: {iceServers: [{ urls: "stun:stun.l.google.com:19302" }],},
});
peer.on("signal", (data) => {socket.emit("answerSignalInfo", {answerSignalInfo: data,to: offerUserInfo?.id,from: offerId,});
});if (offerUserInfo?.singleData) {peer.signal(offerUserInfo.singleData);
}
對(duì)等連接建立,獲取雙方視頻流
交互信令之后,通過(guò)simple-peer成功建立對(duì)等連接,監(jiān)聽stream視頻流然后顯示在頁(yè)面上
// 監(jiān)聽通過(guò)對(duì)等連接傳遞的stream
peer.on("stream", (stream) => {if (remoteVideoRef.current) {remoteVideoRef.current.srcObject = stream;remoteVideoRef.current.play();}
});
完整頁(yè)面代碼:CSS樣式文件省略
import React, { useCallback, useEffect, useRef, useState } from "react";
import { io } from "socket.io-client";
import Peer from "simple-peer";
import "./App.css";const socket = io("http://localhost:5001");type UserInfo = {singleData: any;id: string;
};function App() {// 用于引用 DOM 元素const localVideoRef = useRef<HTMLVideoElement>(null);const remoteVideoRef = useRef<HTMLVideoElement>(null);// 用于管理狀態(tài)const [localStream, setLocalStream] = useState<MediaStream | undefined>();const [offerId, setOfferId] = useState("");const [answerId, setAnswerId] = useState("");const [offerUserInfo, setOfferUserInfo] = useState<UserInfo>();// 獲取本地視頻流const getLocalStream = useCallback(async () => {try {const stream = await navigator.mediaDevices.getUserMedia({video: {width: { ideal: 200 }, // 理想的寬度height: { ideal: 200 }, // 理想的高度},audio: false,});console.log("local media", stream);setLocalStream(stream);if (localVideoRef.current) {localVideoRef.current.srcObject = stream;}} catch (error) {console.error("Error accessing media devices.", error);}}, []);// 手動(dòng)設(shè)置通話方idconst onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {console.log("onChange call id", e);setAnswerId(e.target.value);}, []);// 獲取信令牌服務(wù)器發(fā)送的socket idconst init = useCallback(() => {socket.on("offer", (offerId) => {console.log("offer socket ID", offerId);setOfferId(offerId);getLocalStream(); // 獲取本地視頻流});// 監(jiān)聽信令服務(wù)器發(fā)送的通話申請(qǐng)方的信令牌數(shù)據(jù)socket.on("callUser", ({ singleData, answerId, from }) => {console.log(`${from}發(fā)起通話`, from);setOfferUserInfo({singleData: singleData,id: from,});});}, [getLocalStream]);// 創(chuàng)建和發(fā)送 offerconst startCall = useCallback(async () => {// 通過(guò)simple-peer 交換信令數(shù)據(jù) offer -> 信令服務(wù)器 -> answerconst peer = new Peer({initiator: true,stream: localStream,trickle: false,// 設(shè)置STUN服務(wù)器,Chrome提供的公共服務(wù)器config: {iceServers: [{ urls: "stun:stun.l.google.com:19302" }],},});peer.on("signal", (data: any) => {socket.emit("callUser", {singleData: data, // 發(fā)送通話方的信令數(shù)據(jù)answerId: answerId, // 需要和誰(shuí)通話from: offerId, // 誰(shuí)申請(qǐng)通話});});// 獲取到接收方的信令數(shù)據(jù)socket.on("answerSignalInfo", (data) => {console.log(`${data.from}已經(jīng)接受通話`, data, peer);peer.signal(data.answerSignalInfo);});// 監(jiān)聽通過(guò)對(duì)等連接傳遞的streampeer.on("stream", (stream) => {if (remoteVideoRef.current) {remoteVideoRef.current.srcObject = stream;remoteVideoRef.current.play();}});// setPeer(peer);}, [answerId, localStream, offerId, remoteVideoRef]);const acceptCall = useCallback(() => {const peer = new Peer({initiator: false,stream: localStream,trickle: false,config: {iceServers: [{ urls: "stun:stun.l.google.com:19302" }],},});peer.on("signal", (data) => {socket.emit("answerSignalInfo", {answerSignalInfo: data,to: offerUserInfo?.id,from: offerId,});});if (offerUserInfo?.singleData) {peer.signal(offerUserInfo.singleData);}// 監(jiān)聽通過(guò)對(duì)等連接傳遞的streampeer.on("stream", (stream) => {if (remoteVideoRef.current) {remoteVideoRef.current.srcObject = stream;remoteVideoRef.current.play();}});}, [localStream, offerUserInfo, offerId, remoteVideoRef]);useEffect(() => {init();}, [init]);return (<div className="App"><video autoPlay muted ref={localVideoRef} className="video" /><video autoPlay muted ref={remoteVideoRef} className="video" /><input value={answerId} onChange={onChange} placeholder="call id" /><button onClick={startCall}>發(fā)起通話</button><button onClick={acceptCall}>同意通話</button></div>);
}export default App;
至此可以啟動(dòng)項(xiàng)目,并本地瀏覽器打開兩個(gè)tab即可體驗(yàn)點(diǎn)對(duì)點(diǎn)視頻服務(wù)。
總結(jié)
點(diǎn)對(duì)點(diǎn)通信,主要就是信令數(shù)據(jù)的交換,知道通信雙方具體的配置信息(通信參數(shù)、IP地址等)以保證對(duì)等連接的成功建立,然后傳遞視頻流在頁(yè)面展示。
其中信令服務(wù)器僅用于對(duì)等連接前的信令交換,不會(huì)進(jìn)行數(shù)據(jù)傳輸。NAT是將設(shè)備內(nèi)網(wǎng)地址轉(zhuǎn)換為外網(wǎng)公共地址。STUN來(lái)獲取設(shè)置的公網(wǎng)地址。TURN服務(wù)器是用于對(duì)等連接異常時(shí)的兜底方案,可進(jìn)行數(shù)據(jù)傳輸。