用 Peer.js 愉快上手 P2P 通訊
前言
哈嘍,大家好,我是海怪。
最近公司專案需要用到視訊流的技術,所以就研究了一下 WebRTC,正好看到 Peer.js 這個框架,用它做了一個小 Demo,今天就跟大家做個簡單的分享吧。
WebRTC 是什麼
WebRTC(Web Real Time Communication)也叫做 網路實時通訊 ,它可以 允許網頁應用不通過中間伺服器就能互相直接傳輸任意資料,比如視訊流、音訊流、檔案流、普通資料等。 它逐漸也成為了瀏覽器的一套規範,提供瞭如下能力:
- 捕獲視訊和音訊流
- 進行音訊和視訊通訊
- 進行任意資料的通訊
這 3 個功能分別對應了 3 個 API:
- MediaStream (又稱getUserMedia)
- RTCPeerConnection
- RTCDataChannel
雖然這些 API 看起來很簡單,但是使用起來非常複雜。不僅要了解 "candidate", "ICE" 這些概念,還要寫很多回調才能實現端對端通訊。而且由於不同瀏覽器對 WebRTC 的支援不盡相同,所以還需要引入 Adapter.js 來做相容。光看下面這個連線步驟圖就頭疼:
所以,為了更簡單地使用 WebRTC 來做端對端傳輸, Peer.js 做了底層的 API 呼叫以及相容,簡化了整個端對端實現過程。
下面就用它來實現一個視訊聊天室吧。
文章程式碼都放在 Github 專案 上了,你也可以點選預覽連結 來檢視。
專案初始化
首先使用 create-react-app
來建立一個 React 專案:
create-react-app react-chatroom
將一些無用的檔案清理掉,只留下一個 App.js
即可。為了介面更好看,這裡可以使用 antd
作為 UI 庫:
npm i antd
最後在 index.js
中引入 CSS:
import 'antd/dist/antd.css'
佈局
安裝 peer.js
:
npm i peerjs
寫好整個頁面的佈局:
const App = () => { const [loading, setLoading] = useState(true); const [localId, setLocalId] = useState(''); const [remoteId, setRemoteId] = useState(''); const [messages, setMessages] = useState([]); const [customMsg, setCustomMsg] = useState(''); const currentCall = useRef(); const currentConnection = useRef(); const peer = useRef() const localVideo = useRef(); const remoteVideo = useRef(); useEffect(() => { createPeer() return () => { endCall() } }, []) // 結束通話 const endCall = () => {} // 建立本地 Peer const createPeer = () => {} // 開始通話 const callUser = async () => {} // 傳送文字 const sendMsg = () => {} return ( <div className={styles.container}> <h1>本地 Peer ID: {localId || <Spin spinning={loading} />}</h1> <div> <Space> <Input value={remoteId} onChange={e => setRemoteId(e.target.value)} type="text" placeholder="對方 Peer 的 Id"/> <Button type="primary" onClick={callUser}>視訊通話</Button> <Button type="primary" danger onClick={endCall}>結束通話</Button> </Space> </div> <Row gutter={16} className={styles.live}> <Col span={12}> <h2>本地攝像頭</h2> <video controls autoPlay ref={localVideo} muted /> </Col> <Col span={12}> <h2>遠端攝像頭</h2> <video controls autoPlay ref={remoteVideo} /> </Col> </Row> <h1>傳送訊息</h1> <div> <h2>訊息列表</h2> <List itemLayout="horizontal" dataSource={messages} renderItem={msg => ( <List.Item key={msg.id}> <div> <span>{msg.type === 'local' ? <Tag color="red">我</Tag> : <Tag color="green">對方</Tag>}</span> <span>{msg.data}</span> </div> </List.Item> )} /> <h2>自定義訊息</h2> <TextArea placeholder="傳送自定義內容" value={customMsg} onChange={e => setCustomMsg(e.target.value)} onEnter={sendMsg} rows={4} /> <Button disabled={!customMsg} type="primary" onClick={sendMsg} style={{ marginTop: 16 }}> 傳送 </Button> </div> </div> ); }
效果如下:
建立本地 Peer
由於我們要對接外部別人的 Peer,所以在載入這個頁面時就要建立一個 Peer,在剛剛的 createPeer
中寫入:
const createPeer = () => { peer.current = new Peer(); peer.current.on("open", (id) => { setLocalId(id) setLoading(false) }); // 純資料傳輸 peer.current.on('connection', (connection) => { // 接受對方傳來的資料 connection.on('data', (data) => { setMessages((curtMessages) => [ ...curtMessages, { id: curtMessages.length + 1, type: 'remote', data } ]) }) // 記錄當前的 connection currentConnection.current = connection }) // 媒體傳輸 peer.current.on('call', async (call) => { if (window.confirm(`是否接受 ${call.peer}?`)) { // 獲取本地流 const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }) localVideo.current.srcObject = stream localVideo.current.play() // 響應 call.answer(stream) // 監聽視訊流,並更新到 remoteVideo 上 call.on('stream', (stream) => { remoteVideo.current.srcObject = stream; remoteVideo.current.play() }) currentCall.current = call } else { call.close() } }) }
上面主要做了這麼幾件事:
- new 了一個 Peer 例項,並在這個例項上監聽了很多事件
- 監聽
open
事件,開啟通道後更新本地localId
- 監聽
connect
事件,在連線成功後,將對方 Peer 的訊息都更新到messages
陣列 - 監聽
call
事件,當對方 Peermake call
後 -
getUserMedia
捕獲本地的音視訊流,並更新到localVideo
上 - 監聽
stream
事件,將對方 Peer 的音視訊流更新到remoteVideo
上
整個建立以及監聽的過程就完成了。不過別忘了要在這個頁面關閉後結束整個連結:
useEffect(() => { createPeer() return () => { endCall() } }, []) const endCall = () => { if (currentCall.current) { currentCall.current.close() } }
傳送邀請
如果這個頁面要作為傳送方,那麼這個 Peer 就需要完成 make a call
的任務,在 callUser
寫入:
const callUser = async () => { // 獲取本地視訊流 const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }) localVideo.current.srcObject = stream localVideo.current.play() // 資料傳輸 const connection = peer.current.connect(remoteId); currentConnection.current = connection connection.on('open', () => { message.info('已連線') }) // 多媒體傳輸 const call = peer.current.call(remoteId, stream) call.on("stream", (stream) => { remoteVideo.current.srcObject = stream; remoteVideo.current.play() }); call.on("error", (err) => { console.error(err); }); call.on('close', () => { endCall() }) currentCall.current = call }
這裡主要做了如下事件:
- 捕獲本地音視訊流,並更新到
localVideo
上 - 通過
remote peer id
連線對方 Peer - 通過
remote peer id
給對方make a call
,並監聽這個call
的內容 - 監聽
stream
事件,將對方傳送的流更新到remoteVideo
上 - 監聽
error
事件,上報qyak - 監聽
close
事件,隨時關閉
總體來說和上面的 建立 Peer
的流程是差不多的,唯一的區別就是之前是 new Peer()
和 answer
,這裡是 connect
和 call
。
傳送文字
除了音視訊流的互傳,還可以傳輸普通文字,這裡我們再完善一下 sendMsg
:
const sendMsg = () => { // 傳送自定義內容 if (!currentConnection.current) { message.warn('還未建立連結') } if (!customMsg) { return; } currentConnection.current.send(customMsg) setMessages((curtMessages) => [ ...curtMessages, { id: curtMessages.length + 1, type: 'local', data: customMsg } ]) setCustomMsg(''); }
這裡直接呼叫當前的 connection
去 send()
就可以了。
效果
第一步,開啟兩個頁面 A 和 B。
第二步,將 B 頁面(接收方)的 peer id
填在 A 頁面(發起方)的輸入框內,點選【視訊通話】。
第三步,在 B 頁面(接收方)點選 confirm
的【確認】:
然後就可以完成視訊通話啦:
總結
總的來說,使用 Peer.js 來做端對端的資訊互傳還是比較方便的。
P2P 一大特點就是可以不需要中間伺服器就能完成兩點之間的資料傳輸。不過也並不是所有情況都能 “完全脫離伺服器” ,在某些情況下,比如防火牆阻隔的通訊,還是需要一箇中介伺服器來關聯兩端,然後再開始端對端的通訊。而 Peer.js 自己就實現了一個免費的中介伺服器,預設下是連線到它的中介伺服器上(資料傳輸不走這個 Server),當然你也可以使用它的 PeerServer
來建立自己的伺服器。
// server.js const { PeerServer } = require('peer'); const peerServer = PeerServer({ port: 9000, path: '/myapp' }); // index.html <script> const peer = new Peer('someid', { host: 'localhost', port: 9000, path: '/myapp' }); </script>
WebRTC API 在安全性方面也有很多限制:
總之,端對端技術在某些要求實時性很強的場景下是很有用的。使用 Peer.js 可以很方便地實現端對端互傳。
- React 原理系列 —— Hook 是這樣工作的
- A100 買不到了,只有小顯示卡怎麼訓大模型
- MedISeg:面向醫學影象語義分割的技巧、挑戰和未來的方向
- CoRL 2022 | SurroundDepth: 自監督環視深度估計
- 【機器學習】邏輯迴歸(非常詳細)
- dnn實踐-特徵處理
- Google資料安全自動化建設之路(白皮書)
- 用typescript型別來實現快排
- 基於自建 VTree 的全鏈路埋點方案
- 除了鮑威爾講話,全球央行年會還揭露了什麼?
- coost v3.0.0 (微型boost庫)釋出
- 仔細研究 Go(golang) 型別系統
- 乾貨 | 嵌入式資料分析最佳實踐
- 關於高頻量化交易的程式碼專案
- 徹底解決 qiankun 找不到入口的問題
- VIM 外掛推薦
- 全網最通透:MySQL 的 redo log 保證資料不丟的原理
- 蟻群演算法的簡要分析
- 7月美聯儲會議紀要
- 容器平臺架構之道