# [實作篇]WebRTC - Video Chat (data channel)
# 目標
上一章節已經完成WebRTC基本應用的部分, 本章會加入最後一部分 RTCDataChannel 來實作的範例。
預計完成的功能有:
- message service (訊息傳遞)
- file transfer (檔案傳輸)
# 實作
附上完整程式碼 - github
# Message service 訊息傳遞
<!-- ./public/index.html -->
<!-- ... -->
<body>
<h1>Video Chat With WebRTC</h1>
<!-- ...略 -->
<section>
<h2>訊息傳遞</h2>
<textarea id="dataChannelSend" placeholder="enter some text, then press Send."></textarea>
<div id="buttons">
<button onclick="sendMessage()">Send</button>
</div>
</section>
</body>
sendMessage()
: 傳送訊息
- Javascript
function createPeerConnection() {
peer = new RTCPeerConnection();
// ....略
peer.ondatachannel = handleDataChannel;
dataChannel = peer.createDataChannel("my local channel", {negotiated: true, id: 9527});
dataChannel.onmessage = event => console.log("Received Message ==> ", event.data);;
dataChannel.onopen = /** event handler ... */;
dataChannel.onclose = /** event handler ... */;
}
在一開始建立RTCPeerConnection instance時,順便建立 RTCDataChannel,並且這次有指定參數
dataChannel = peer.createDataChannel("my local channel", {negotiated: true, id: 9527});
- id:範圍於0-65534的16位元數值。
- negotiated: default是false,那指定為true的作用,就是指定雙方必須都建立相同ID的channel時才能彼此傳遞訊息,如果沒有指定預設為一方建立一方接受的模式(如下)。
localPeer.createDataChannel('localChannel')
remotePeer.ondatachannel = handleDataChannel
function handleDataChannel (event) {
console.log("Receive Data Channel Callback", event);
const receiveChannel = event.channel;
receiveChannel.onmessage = event => console.log("Received Message ==> ", event.data);
receiveChannel.onopen = /** event handler ... */;
receiveChannel.onclose = /** event handler ... */;
}
簡單做個發送訊息的event handler:
function sendMessage() {
const textArea = document.querySelector('#dataChannelSend')
if (dataChannel.readyState === 'open') dataChannel.send(textArea.value)
}
以上就完成基本的訊息傳遞功能~ 試著用用看,觀察console裡的資訊變化~
# file transfer 檔案傳輸
先前實作過的檔案傳輸,這邊也稍做修改為更接近實務的版本~
<!-- ...略 -->
<section>
<h2>檔案傳送</h2>
<div>
<form id="fileInfo">
<input type="file" id="fileInput" name="files"/>
</form>
<button onclick="sendFileData()">Send File</button>
</div>
<div class="progress">
<div class="label">Send progress: </div>
<progress id="sendProgress" max="0" value="0"></progress>
</div>
<div class="progress">
<div class="label">Receive progress: </div>
<progress id="receiveProgress" max="0" value="0"></progress>
</div>
</section>
- Javascript
peer.ondatachannel = handleDataChannel;
// Receive Remote DataChannel Event Handler
function handleDataChannel (event) {
const receiveChannel = event.channel;
receiveChannel.onmessage = onReceiveMessageCallback;
receiveChannel.onopen = /** event handler ... */;
receiveChannel.onclose = /** event handler ... */;
}
function onReceiveMessageCallback(event) {
const FILE_CHANNEL_LABEL = 'FileChannel' // 自定義
const channelLabel = event.target.label
if (channelLabel === FILE_CHANNEL_LABEL) onReceiveFile(event)
else console.log("Received Message ==> ", event.data);
}
let receiveBuffer = [];
let receivedSize = 0;
function onReceiveFile(event) {
console.log(`Received Message ${event.data.byteLength}`);
receiveBuffer.push(event.data);
receivedSize += event.data.byteLength;
const receiveProgress = document.querySelector('progress#receiveProgress');
receiveProgress.value = receivedSize;
}
sendFileData()
: 檔案傳送
function sendFileData() {
const fileInput = document.querySelector('input#fileInput');
const file = fileInput.files[0];
// Handle 0 size files.
if (file.size === 0) {
alert('File is empty, please select a non-empty file');
return;
}
// FILE_CHANNEL_LABEL = 'FileChannel'
const fileChannel = peer.createDataChannel(FILE_CHANNEL_LABEL)
fileChannel.onopen = () => {
const sendProgress = document.querySelector('progress#sendProgress');
sendProgress.max = file.size;
const chunkSize = 16384;
const fileReader = new FileReader();
let offset = 0;
fileReader.addEventListener('error', error => console.error('Error reading file:', error));
fileReader.addEventListener('abort', event => console.log('File reading aborted:', event));
fileReader.addEventListener('load', e => {
console.log('FileRead.onload ', e);
fileChannel.send(e.target.result);
offset += e.target.result.byteLength;
sendProgress.value = offset;
if (offset < file.size) {
readSlice(offset);
}
});
const readSlice = o => {
console.log('readSlice ', o);
const slice = file.slice(offset, o + chunkSize);
fileReader.readAsArrayBuffer(slice);
};
readSlice(0);
}
fileChannel.onclose = () => console.log('closing File Channel')
}
試試看檔案傳送~
上面檔案傳送範例不太了解的可以看看前面該章節~
延伸思考:
- 檔案傳送的實作是 沒有指定ID,所以透過 ondatachannel 的事件監聽來獲取新的channel, 也可以試著用 指定ID 的方式實作看看,了解差異在哪,未來應用上也能更容易找到符合應用情境的使用方式~
# 總結
RTCDataChannel 的接收方式有兩種:
不指定ID: 某一方 createDataChannel ,接收方就必須透過 ondatachannel 的事件監聽來獲取新的channel
指定ID: 雙方必須都 createDataChannel 建立相同ID的channel 資料才能夠相通
注意: 指定ID時,negotiated也必須指定是true!
能因應需求決定使用哪種方式~