上一篇 說明了完成聊天室必備的基礎觀念,接著就來進行實作吧!
視訊情境
裝置 A 開啟了一個聊天室,接著裝置 B 連接該聊天室進行視訊聊天
圖片參考:MDN
流程解析(搭配 STUN 協定)
- 裝置 A 透過 STUN server 取得本地私有 IP 以及公有 IP
- 裝置 B 透過 STUN server 取得本地私有 IP 以及公有 IP
- 裝置 A 發送 offer SDP,並設定為本地 SDP
- 裝置 B 收到 offer SDP,並設定為遠端 SDP
- 裝置 B 發送 answer SDP,並設定為本地 SDP
- 裝置 A 收到 answer SDP,並設定為遠端 SDP
- 裝置 A 傳送 ICE 候選位址
- 裝置 B 寫入裝置 A ICE 候選位址
- 裝置 B 傳送 ICE 候選位址
- 裝置 A 寫入裝置 B ICE 候選位址
- P2P 連線建立,進行即時媒體串流(音訊、視訊)
視訊實作
實作分為 Server(Signaling Server)以及 Client(視訊聊天室)進行
檔案結構如下:
| server.jschat.html
 chat.js
 
 | 
Server(server.js)
套件安裝(NPM)
- Node.js 框架 Express
- Websocket 套件 Socket.io
套件引入
| const express = require('express');const app = express();
 const http = require('http');
 const socket = require('socket.io');
 
 | 
啟動伺服器
port 設定 3000,因此伺服器連線網址為 http://localhost:3000
| const server = http.createServer(app).listen('3000');
 | 
連接 Client 頁面(轉換為絕對路徑 __dirname)
| app.get('/chat', function(req, res) {
 es.sendfile(`${__dirname}/chat.html`);
 });
 
 
 app.get(/(.*)\.(jpg|gif|png|ico|css|js|txt)/i, function(req, res) {
 console.log(__dirname);
 res.sendfile(`${__dirname}/${req.params[0]}.${req.params[1]}`);
 });
 
 | 
加入 Socket.io
- socket.on()監聽 join, offer, answer, ice_candidate, hangup 等 Client 自訂事件
- socket.emit()傳遞事件給 Client
| const io = socket(server);
 io.on('connection', (socket) => {
 console.log('connection');
 
 
 socket.on('join', (room) => {
 console.log('join');
 socket.join(room);
 socket.to(room).emit('ready', '準備通話');
 });
 
 
 socket.on('offer', (room, description) => {
 socket.to(room).emit('offer', description);
 });
 
 
 socket.on('answer', (room, desc) => {
 socket.to(room).emit('answer', description);
 });
 
 
 socket.on('ice_candidate', (room, data) => {
 socket.to(room).emit('ice_candidate', data);
 });
 
 
 socket.on('hangup', (room) => {
 console.log('hangup');
 socket.leave(room);
 });
 });
 
 | 
Client(視訊聊天室)
套件安裝(CDN)
- WebRTC Adapter(解決相容性問題)
- Socket.io-client
HTML(chat.html)
- 加入 Video Dom
- #localVideo 接收本地媒體串流
- #remoteVideo 接收遠端媒體串流
 
- CDN WebRTC Adapter 以及 Socket.io-client
- 引入 chat.js
| <!DOCTYPE html><html>
 <head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
 <title>chat</title>
 <style lang="scss">
 video {
 transform: scaleX(-1);
 }
 </style>
 </head>
 <body>
 <div>
 <video muted="false" width="320" autoplay playsinline id="localVideo"></video>
 <video width="320" autoplay playsinline id="remoteVideo"></video>
 
 <button type="button" id="call">call</button>
 <button type="button" id="hangup">hangup</button>
 </div>
 <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
 <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/socket.io.js"></script>
 <script src="chat.js" type="module" async></script>
 </body>
 </html>
 
 | 
Javascript(chat.js)
任務步驟拆分
- 連接 Socket Server(Signaling Server)
 透過 Socket 監聽 Offer SDP、Answer SDP、ICE Candidate
- getUserMedia 取得本地媒體串流
- RTCPeerConnection 建立 P2P 連線,與 RTCPeerConnection 事件監聽
定義常數與變數
| const localVideo = document.querySelector('#localVideo');const remoteVideo = document.querySelector('#remoteVideo');
 const callBtn = document.querySelector('#call');
 const hangupBtn = document.querySelector('#hangup');
 let peerConnection;
 let socket;
 let localStream;
 const room = 'room1';
 
 | 
連接 Socket Server(Signaling Server)
透過 Socket 監聽 Offer SDP、Answer SDP、ICE Candidate
socket.emit() 以及 socket.on() 傳遞與接收資料給 Server
| const socketConnect = () => {
 socket = io('ws://localhost:3000');
 
 
 socket.emit('join', room);
 
 
 socket.on('ready', (msg) => {
 
 sendSDP('offer');
 });
 
 
 socket.on('offer', async (desc) => {
 
 await peerConnection.setRemoteDescription(desc);
 
 await sendSDP('answer');
 });
 
 
 socket.on('answer', (desc) => {
 
 peerConnection.setRemoteDescription(desc)
 });
 
 
 socket.on('ice_candidate', (data) => {
 
 const candidate = new RTCIceCandidate({
 sdpMLineIndex: data.label,
 candidate: data.candidate
 });
 
 peerConnection.addIceCandidate(candidate);
 });
 };
 
 | 
處理 Offer SDP / Answer SDP
| 
 
 const sendSDP = async (type) => {
 try {
 if (!peerConnection) {
 console.log('尚未開啟視訊');
 return;
 }
 
 const method = type === 'offer' ? 'createOffer' : 'createAnswer';
 const offerOptions = {
 offerToReceiveAudio: true,
 offerToReceiveVideo: true
 };
 
 
 const localSDP = await peerConnection[method](offerOptions);
 
 
 await peerConnection.setLocalDescription(localSDP);
 
 
 socket.emit(type, room, peerConnection.localDescription);
 } catch (err) {
 console.log('error: ', err);
 }
 };
 
 | 
| const createStream = async () => {try {
 const constraints = { audio: true, video: true };
 
 
 const stream = await navigator.mediaDevices.getUserMedia(constraints);
 
 
 localVideo.srcObject = stream;
 
 
 localStream = stream;
 } catch (err) {
 console.log('getUserMedia error: ', err.message, err.name);
 }
 };
 
 | 
RTCPeerConnection 建立 P2P 連線,與 RTCPeerConnection 事件監聽
- RTCPeerConnection 內的 iceServers用來建立兩台裝置點對點連線的伺服器
- getTracks():取得媒體串流(MediaStream)中媒體軌道(MediaStreamTrack)陣列
- addTrack(MediaStreamTrack, MediaStream):增加媒體軌道(MediaStreamTrack)到 RTCPeerConnection 中指定的媒體串流(MediaStream)
| const createPeerConnection = () => {
 const configuration = {
 iceServers: [{
 urls: 'stun:stun.l.google.com:19302'
 }]
 };
 
 peerConnection = new RTCPeerConnection(configuration);
 
 
 localStream.getTracks().forEach((track) => {
 peerConnection.addTrack(track, localStream);
 });
 
 
 peerConnection.onicecandidate = (e) => {
 if (e.candidate) {
 
 socket.emit('ice_candidate', room, {
 label: e.candidate.sdpMLineIndex,
 id: e.candidate.sdpMid,
 candidate: e.candidate.candidate,
 });
 }
 };
 
 
 peerConnection.oniceconnectionstatechange = (e) => {
 
 if (e.target.iceConnectionState === 'disconnected') {
 hangup();
 }
 };
 
 
 peerConnection.onaddstream = ({ stream }) => {
 
 remoteVideo.srcObject = stream;
 };
 };
 
 | 
點擊按鈕開始連線/關閉連線
| createStream();
 
 
 const call = () => {
 socketConnect();
 createPeerConnection();
 };
 
 
 const hangup = () => {
 
 peerConnection.onicecandidate = null;
 peerConnection.onnegotiationneeded = null;
 
 
 peerConnection.close();
 peerConnection = null;
 
 
 socket.emit('hangup', room);
 socket = null;
 
 
 remoteVideo.srcObject = null;
 };
 
 callBtn.addEventListener('click', call);
 hangupBtn.addEventListener('click', hangup);
 
 | 
這樣一對一聊天室就完成囉,後續會說明如何達成多方視訊功能
參考文章:
https://ithelp.ithome.com.tw/articles/10251454
https://ithelp.ithome.com.tw/articles/10209193
https://ithelp.ithome.com.tw/articles/10278727
https://medium.com/@jedy05097952/初探-webrtc-手把手建立線上視訊-1-5e9d4702e8e8
        
    
     
    
        
評論