# [實作篇]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!

能因應需求決定使用哪種方式~