# [實作篇]WebRTC APIs - RTCDataChannel ( transfer file / data )

# 學習目標

  • 運用 RTCDataChannel 在 peers 間傳遞文檔(File data)

附上完整程式碼

# 實作

利用 RTCDataChannel 實作簡單檔案上傳及傳送

# HTML

以下是這次的模板:

  • input[file]: 檔案上傳
  • sendFile Button / abort Button
  • progress bar / bitrate / status : 方便展示拆分檔案傳輸的狀態顯示
<section>
  <div>
    <form id="fileInfo">
      <input type="file" id="fileInput" name="files"/>
    </form>
    <button disabled id="sendFile">Send</button>
    <button disabled id="abortButton">Abort</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>

  <div id="bitrate"></div>
  <a id="download"></a>
  <span id="status"></span>
</section>

# 功能

引用先前範例,一樣先用RTCPeerConnection建立起兩端點的連線溝通, 並在localPeer(發送端)透過createDataChannel()建立RTCDataChannel實例,而這次要實作傳輸檔案。

const configuration = null;
localPeer = buildPeerConnection(localPeer, configuration);
datachannel = localPeer.createDataChannel("my local channel");
datachannel.binaryType = 'arraybuffer'; // datachannel 只夠接受兩種二進制資料類型: Blob / ArrayBuffer
datachannel.onopen = onChannelStageChange(datachannel);
datachannel.onclose = onChannelStageChange(datachannel);

remotePeer = buildPeerConnection(remotePeer, configuration);
remotePeer.ondatachannel = receiveChannelCallback;
await communication(localPeer, remotePeer);
function sendData() {
  const file = fileInput.files[0];

  // Handle 0 size files.
  if (file.size === 0) {
    statusMessage.textContent = 'File is empty';
    closeDataChannels();
    return;
  }
  document.querySelector('progress#sendProgress').max = file.size;
  document.querySelector('progress#receiveProgress').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 => {
    datachannel.send(e.target.result); // send data
    offset += e.target.result.byteLength; // ArrayBuffer 大小
    document.querySelector('progress#sendProgress').value = offset;
    if (offset < file.size) {
      readSlice(offset);
    }
  });
  const readSlice = o => {
    const slice = file.slice(offset, o + chunkSize); // 為了限制單次傳輸大小,透過Blob.slice 取出一定範圍內的資料
    fileReader.readAsArrayBuffer(slice); // 透過fileReader讀取Blob資料並轉為ArrayBuffer
  };
  readSlice(0);
}

<input type=file>中取出的檔案(File object),是一種特殊的Blobobject,可被用在任何接受 Blob 物件的地方, 而這邊透過datachannel傳輸檔案上每次的傳輸量有其上限, 所以透過FileReader.readAsArrayBuffer()讀取Blob並轉為ArrayBuffer(因為datachannel只接受Blob或ArrayBuffer兩種binary type), 在FileReader.readAsArrayBuffer讀取Blob時觸發的loadEvent 進行資料的拆分跟傳送(send)。

function receiveChannelCallback(event) {
  // ...
  receiveChannel = event.channel;
  receiveChannel.binaryType = 'arraybuffer';
  receiveChannel.onmessage = onReceiveMessageCallback;
  receiveChannel.onopen = onReceiveChannelStateChange;
  receiveChannel.onclose = onReceiveChannelStateChange;
  // ...
}
let receiveBuffer = []; // 已接收的 arrayBuffer 數據
let receivedSize = 0; // 已接收的數據大小

// ...
function onReceiveMessageCallback(event) {
  receiveBuffer.push(event.data);
  receivedSize += event.data.byteLength;

  document.querySelector('progress#receiveProgress').value = receivedSize;

  // 這邊的file之後應該要透過signaling server傳遞過來才能得知(ex: file name, size...等)
  const file = fileInput.files[0];
  if (receivedSize === file.size) {
    const received = new Blob(receiveBuffer); // ArrayBuffer to Blob
    receiveBuffer = [];

    document.querySelector('a#download').href = URL.createObjectURL(received);
    document.querySelector('a#download').download = file.name;
    // ...
    closeDataChannels();
  }
}

接收端(receive)接收到資料後,先暫存起來,等完整資料接收完畢後,再進行處理, 這邊是將完整資料轉為Blob,方便對文件的操作(例如範例:利用URL.createObjectURL轉為url賦值給a.href讓使用者能下載檔案)。

注意: 這只是簡單演示WebRTC api的運用,所以目前沒有Signal Server作為中介者幫我們傳遞兩邊的信令,因此也不適用於真正的應用上。

延伸思考:

  • 嘗試將receive的datachannel設定的binaryTypeBlob
  • 嘗試調高chunkSize看看各瀏覽器的行為。
  • 嘗試在read file過程中使用FileReader.abort()

# 總結

本章節了解到:

  • 如何在兩個 WebRTC端點間透過 data channel 進行檔案傳輸
  • Data channel所能傳遞的檔案類型Blob & ArrayBuffer
  • Data channel每次傳輸量的上限