【W(wǎng)ebRTC - OpenAI 都在用? 實(shí)戰(zhàn)秘籍劫窒,10分鐘! 帶你速通WebRTC,構(gòu)建你自己的RTC服務(wù)】

WebRTC

什么是WebRTC

WebRTC(Web Real-Time Communication)是一項(xiàng)由 W3C 和 IETF 推動(dòng)的開源項(xiàng)目拆座,旨在為瀏覽器和移動(dòng)應(yīng)用提供實(shí)時(shí)通信(RTC)功能主巍。

WebRTC 支持音頻、視頻和數(shù)據(jù)在點(diǎn)對(duì)點(diǎn)(P2P)連接中直接傳輸挪凑,而無(wú)需中間服務(wù)器孕索。也支持配合SFU(Selective Forwarding Unit),MCU(Multipoint Control Unit)技術(shù)躏碳,構(gòu)建多對(duì)多的傳輸通信搞旭。

特點(diǎn)

  • 實(shí)時(shí)通信: 支持音頻、視頻和數(shù)據(jù)的實(shí)時(shí)傳輸,延遲低肄渗,適用于實(shí)時(shí)互動(dòng)場(chǎng)景镇眷。
  • 點(diǎn)對(duì)點(diǎn)連接: 通過(guò) P2P 連接,數(shù)據(jù)直接在客戶端之間傳輸翎嫡,減少了服務(wù)器的負(fù)載和延遲欠动。
  • 跨平臺(tái)支持: 支持主流瀏覽器(如 Chrome、Firefox惑申、Safari翁垂、Edge)和移動(dòng)平臺(tái)(如 Android、iOS)硝桩。
  • 安全性: 默認(rèn)使用 SRTP(Secure Real-time Transport Protocol)加密音視頻流,DTLS(Datagram Transport Layer Security)加密數(shù)據(jù)通道枚荣,確保通信安全碗脊。
  • 開源和標(biāo)準(zhǔn)化: WebRTC 是一個(gè)開源項(xiàng)目,并且由 W3C 和 IETF 標(biāo)準(zhǔn)化橄妆,確保廣泛的兼容性和支持衙伶。

誰(shuí)在用

可以看出 基于 網(wǎng)絡(luò)語(yǔ)音/視頻通話 的場(chǎng)景,尤其是類似 實(shí)時(shí)網(wǎng)絡(luò) 語(yǔ)音電話這種害碾。

各大語(yǔ)音app (whats app, Facebook, Google系軟件) 都有基于webrtc或者參考webrtc的思路進(jìn)行實(shí)現(xiàn)矢劲。refs: https://telnyx.com/resources/5-applications-that-demonstrate-the-power-of-webrtc-and-sip

OPENAPI 也推出了實(shí)時(shí)流視頻接口,Realtime API with WebRTC https://platform.openai.com/docs/guides/realtime-websocket

快速構(gòu)建

速覽圖

一圖速覽慌随,可以看出 建立WebRTC的通信芬沉,整體分 建立網(wǎng)絡(luò)連接 和 推流 兩大步。

而網(wǎng)絡(luò)連接 又分P2P模式 和 中繼模式阁猜。

構(gòu)建一個(gè)完整的WebRTC丸逸,可以滿足不同環(huán)境,以及網(wǎng)絡(luò)情況下的端到端通信 剃袍,則我們需要構(gòu)建

  • 信令服務(wù)
  • STUN服務(wù)
  • TURN服務(wù)

此外演示的代碼跑在瀏覽器上黄刚,訪問(wèn)瀏覽器展示頁(yè)面,創(chuàng)建WebRTC Client民效,所以還需要一個(gè)Web服務(wù)

  • Web HTTP服務(wù)

廢話不多說(shuō)憔维,逐一部署,一個(gè)個(gè)擊破畏邢。接下來(lái)业扒,上家伙 (完整代碼見附錄)

1. 部署 Web HTTP 服務(wù) + Signaling 信令服務(wù)

構(gòu)建一個(gè)Web Http 服務(wù)器,可以返回 前端頁(yè)面所需數(shù)據(jù)舒萎,

...
const server = http.createServer((req, res) => {
    console.log(`request url: ${req.url}, method: ${req.method}`);

    // 提供靜態(tài)文件服務(wù)
    // 返回主頁(yè)
    if (req.method === "GET" && req.url === "/") {
        console.log("request index.html");
        const filePath = path.join(
            __dirname,
            "static/my-webrtc-client/index.html"
        );
        fs.readFile(filePath, (err, data) => {
            if (err) {
                res.writeHead(404, { "Content-Type": "text/plain" });
                res.end("404 Not Found");
                return;
            }
            // 根據(jù)文件擴(kuò)展名設(shè)置正確的 Content-Type
            const ext = path.extname(filePath).toLowerCase();
            const contentType = mimeTypes[ext] || "application/octet-stream";
            res.writeHead(200, { "Content-Type": contentType });
            res.end(data);
        });
    } else if (req.method === "GET" && isRequestFile(req)) {
    // 返回 js / css 等文件
        const dirpath = __dirname + "/static/my-webrtc-client";
        const filePath = path.join(dirpath, req.url);
        console.log(`request ${filePath}`);

        fs.readFile(filePath, (err, data) => {
            if (err) {
                res.writeHead(404, { "Content-Type": "text/plain" });
                res.end("404 Not Found");
                return;
            }

            // 根據(jù)文件擴(kuò)展名設(shè)置正確的 Content-Type
            const ext = path.extname(filePath).toLowerCase();
            const contentType = mimeTypes[ext] || "application/octet-stream";
            res.writeHead(200, { "Content-Type": contentType });
            res.end(data);
        });
    } else {
        res.writeHead(404, { "Content-Type": "text/plain" });
        res.end("404 Not Found");
    }
});
...

構(gòu)建一個(gè)Signaling server凶赁,用于兩端數(shù)據(jù)的交換。

io.on("connection", (sock) => {
    console.log("連接成功...");
    // 向客戶端發(fā)送連接成功的消息
    sock.emit("connectionSuccess");

    // 監(jiān)聽客戶端event
    sock.on("offer", (event) => {
        console.log(`receive offer from device : ${event.fromDeviceId}`);
        // 向其余客戶端發(fā)送offer
        sock.broadcast.emit("offer", event);
    });

    sock.on("candidate", (event) => {
        console.log(`receive candidate from device : ${event.fromDeviceId}`);
        // 向其余客戶端發(fā)送offer
        sock.broadcast.emit("candidate", event);
    });

    sock.on("answer", (event) => {
        console.log(
            `receive answer from deviceId : ${event.fromDeviceId}, to deviceId : ${event.toDeviceId}`
        );
        // 向其余客戶端發(fā)送offer
        sock.broadcast.emit("answer", event);
    });

    ...
});

2. 部署 STUN服務(wù) + TURN服務(wù)

此處選用coturn服務(wù),部署安裝

安裝

apt install coturn

修改coturn config, 參考 【W(wǎng)ebRTC - STUN/TURN服務(wù) - COTURN配置】

vim /etc/turnserver.conf

啟動(dòng)coturn服務(wù)

turnserver --log-file stdout

3. 使用 WebRTC Client進(jìn)行連接

構(gòu)建webrtc client 虱肄, webrtc場(chǎng)景下致板, 端到端通信,A為 Caller咏窿,B為 Called斟或。WebRTC Client A → WebRTC Client B 。

  • Caller 和 Called 兩端分別初始化
caller / called = new RTCPeerConnection({
    encodedInsertableStreams: true, // needed by chrome shim
    iceServers: [
        {
            // default "stun:stun.l.google.com:19302",
            urls: STUN_URL,
        },
        {
            urls: TURN_URL,
            username: TURN_USERNAME,
            credential: TURN_CREDENTIAL,
        },
    ],
});

  • Caller 請(qǐng)求對(duì)端 集嵌,創(chuàng)建offer然后存儲(chǔ)在本地萝挤,然后發(fā)送offer
// 創(chuàng)建offer
const offer = await pc.createOffer(offerOptions);
// 存儲(chǔ)在本地
caller.setLocalDescription(offer)
// 發(fā)送到對(duì)端
sock.emit("offer", {
    offer: offer,
    ...
});

  • Called接收到offer,存儲(chǔ)在本地根欧,然后創(chuàng)建answer 然后回復(fù)answer給Caller
// 收到offer
// 存儲(chǔ)offer在本地
called.setRemoteDescription(event.offer)
// 創(chuàng)建answer
answer = await called.createAnswer();
// 存儲(chǔ)answer在本地
await called.setLocalDescription(answer);

  • Caller在收到answer以后怜珍,存儲(chǔ)在本地
await pc.setRemoteDescription(event.answer);

  • 同時(shí) 在無(wú)論是Caller 還是 Called 在調(diào)用setLocalDescription后,會(huì)觸發(fā)瀏覽器請(qǐng)求STUN服務(wù)器獲取candidate信息凤粗,Caler / Called 需要獲取到candidate 然后存在本地
// 收到candidate
caller/called.addEventListener("icecandidate", (e) => {
    // 發(fā)給對(duì)端
    sock.emit("candidate", {
        candidate: candidate,
        ...
    });
})

// 對(duì)端收到后 酥泛,存儲(chǔ)在本地
await caller/called.addIceCandidate(candidate);

當(dāng)兩端互換candidate 協(xié)商后,根據(jù)candidate的type (host嫌拣, relay 等) 嘗試連通柔袁,最終決定是p2p通信,還是 relay通信异逐。至此便完成了兩端網(wǎng)絡(luò)連通捶索。

當(dāng)網(wǎng)絡(luò)連通后,Caller獲取本地音視頻數(shù)據(jù)后 灰瞻,用剛剛打通的網(wǎng)絡(luò)腥例,便可進(jìn)行數(shù)據(jù)傳輸。Called接收數(shù)據(jù)酝润。

Caller

// 獲取 音視頻數(shù)據(jù)
stream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: true,
});
// 傳輸
stream.getTracks().forEach((track) => {
    caller.addTrack(track, stream);
});

Called

// 收到后院崇, 將數(shù)據(jù)放到 video element上,進(jìn)行播放
called.addEventListener("track", (e) => {
    const remoteVideo = document.getElementById("remoteVideo");
    remoteVideo.srcObject = e.streams[0];
});

WebRTC - 展示

p2p 模式:兩個(gè)設(shè)備都在同一個(gè)局域網(wǎng)內(nèi) 袍祖,

打開兩個(gè)網(wǎng)頁(yè)端底瓣,分別是Caller 和 Called。
Caller 點(diǎn)擊 Start 獲取媒體流蕉陋,然后點(diǎn)擊Call 和 對(duì)端Called連通網(wǎng)絡(luò)后捐凭,便可以推流到對(duì)端。

中繼模式: 兩個(gè)移動(dòng)端設(shè)備在公網(wǎng)環(huán)境 凳鬓,一個(gè)筆記本電腦 茁肠,一個(gè)手機(jī)

筆記本視角:

手機(jī)端視角:

注意:

  • 如果兩端不在同一局域網(wǎng)內(nèi),大概率是需要中繼服務(wù)的缩举,則此時(shí)垦梆,信令服務(wù)匹颤,TURN/STUN服務(wù)需要部署在有公網(wǎng)地址的機(jī)器上。
  • STUN有免費(fèi)公共的可以使用托猩,但是TURN由于帶寬費(fèi)用高印蓖,沒(méi)有免費(fèi)公共的,所以必須要自部署京腥。

附錄

代碼

示例代碼赦肃,包括 前后端代碼,信令服務(wù) 公浪。

【Your WebRTC】

名詞解釋

ICE 候選者 (Candidate)

ICE 候選者包含關(guān)于如何連接到對(duì)等方的網(wǎng)絡(luò)信息他宛。每個(gè)候選者提供一個(gè)可能的網(wǎng)絡(luò)路徑。具體信息包括:

  • IP 地址: 設(shè)備的公共或私有 IP 地址欠气。
  • 端口: 用于通信的端口號(hào)厅各。
  • 優(yōu)先級(jí): 候選者的優(yōu)先級(jí),用于選擇最佳路徑预柒。
  • 類型: 候選者的類型(如 host队塘、srflx、relay)卫旱。
  • 協(xié)議: 使用的傳輸協(xié)議(如 UDP、TCP)围段。

candidate 示例

candidate:842163049 1 udp 1677729535 192.168.1.2 56143 typ srflx raddr 10.0.0.1 rport 8998 generation 0 ufrag EEtu network-id 3 network-cost 10

SDP (Session Description Protocol)

SDP 是一種格式化的文本顾翼,用于描述多媒體會(huì)話的參數(shù)。它包含的信息包括:

  • 會(huì)話描述: 包括會(huì)話的名稱奈泪、時(shí)間适贸、參與者等。
  • 媒體描述: 包括媒體類型(音頻涝桅、視頻)拜姿、編解碼器、比特率等冯遂。
  • 連接信息: 包括 IP 地址和端口號(hào)蕊肥。
  • 媒體格式: 支持的媒體格式和編解碼器。
  • 帶寬信息: 會(huì)話的帶寬要求蛤肌。
  • 屬性: 其他會(huì)話屬性壁却,如方向(發(fā)送、接收)裸准。

sdp 示例

v=0
o=- 46117327 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104
c=IN IP4 192.168.1.2
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:EEtu
a=ice-pwd:asd88fgpdd777uzjYhagZg
a=mid:audio
a=sendrecv
a=rtpmap:111 opus/48000/2
m=video 9 UDP/TLS/RTP/SAVPF 100 101
c=IN IP4 192.168.1.2
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:EEtu
a=ice-pwd:asd88fgpdd777uzjYhagZg
a=mid:video
a=sendrecv
a=rtpmap:100 VP8/90000

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末展东,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子炒俱,更是在濱河造成了極大的恐慌盐肃,老刑警劉巖爪膊,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異砸王,居然都是意外死亡推盛,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門处硬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)小槐,“玉大人,你說(shuō)我怎么就攤上這事荷辕≡涮” “怎么了?”我有些...
    開封第一講書人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵疮方,是天一觀的道長(zhǎng)控嗜。 經(jīng)常有香客問(wèn)我,道長(zhǎng)骡显,這世上最難降的妖魔是什么疆栏? 我笑而不...
    開封第一講書人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮惫谤,結(jié)果婚禮上壁顶,老公的妹妹穿的比我還像新娘。我一直安慰自己溜歪,他們只是感情好若专,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蝴猪,像睡著了一般调衰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上自阱,一...
    開封第一講書人閱讀 51,573評(píng)論 1 305
  • 那天嚎莉,我揣著相機(jī)與錄音,去河邊找鬼沛豌。 笑死趋箩,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的加派。 我是一名探鬼主播阁簸,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼哼丈!你這毒婦竟也來(lái)了启妹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤醉旦,失蹤者是張志新(化名)和其女友劉穎饶米,沒(méi)想到半個(gè)月后桨啃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡檬输,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年照瘾,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丧慈。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡析命,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出逃默,到底是詐尸還是另有隱情鹃愤,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布完域,位于F島的核電站软吐,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏吟税。R本人自食惡果不足惜凹耙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望肠仪。 院中可真熱鬧肖抱,春花似錦、人聲如沸异旧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)泽艘。三九已至欲险,卻和暖如春镐依,著一層夾襖步出監(jiān)牢的瞬間匹涮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工槐壳, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留然低,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓务唐,卻偏偏與公主長(zhǎng)得像雳攘,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子枫笛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容