使用 WebRTC 編碼轉換

基準線 2025
新推出

自 2025 年 10 月起,此功能已在最新的裝置和瀏覽器版本中可用。此功能可能不適用於較舊的裝置或瀏覽器。

WebRTC 編碼轉換提供了一種機制,可以在傳入和傳出的 WebRTC 流水線中注入高效能的 Stream API,以修改編碼的影片和音訊幀。這使得第三方程式碼能夠實現編碼幀的端到端加密等用例。

API 定義了主執行緒和 Worker 側物件。主執行緒介面是一個 RTCRtpScriptTransform 例項,它在構造時指定了將實現轉換器程式碼的 Worker。執行在 Worker 中的轉換透過將 RTCRtpScriptTransform 新增到 RTCRtpReceiver.transformRTCRtpSender.transform 中,分別插入到傳入或傳出的 WebRTC 流水線中。

在 Worker 執行緒中建立了一個對應的 RTCRtpScriptTransformer 物件,該物件具有一個 ReadableStream readable 屬性、一個 WritableStream writable 屬性,以及一個從關聯的 RTCRtpScriptTransform 建構函式傳遞的 options 物件。來自 WebRTC 流水線的編碼影片幀 (RTCEncodedVideoFrame) 或音訊幀 (RTCEncodedAudioFrame) 被新增到 readable 中進行處理。

當編碼幀被排隊等待處理時(以及最初在構造相應的 RTCRtpScriptTransform 時),rtctransform 事件會在 Worker 全域性作用域觸發,而 RTCRtpScriptTransformer 作為該事件的 transformer 屬性提供給程式碼。Worker 程式碼必須實現一個事件處理程式,該處理程式從 transformer.readable 中讀取編碼幀,根據需要修改它們,然後以相同的順序且不重複地將它們寫入 transformer.writable

雖然介面沒有對實現施加任何其他限制,但轉換幀的自然方式是建立一個 管道鏈,該管道鏈透過 TransformStream 將在 event.transformer.readable 流上排隊的幀傳送到 event.transformer.writable 流。我們可以使用 event.transformer.options 屬性來配置任何依賴於轉換是將來自打包器的傳入幀還是來自編解碼器的傳出幀排隊的轉換程式碼。

RTCRtpScriptTransformer 介面還提供了在傳送編碼影片時用於讓編解碼器生成“關鍵”幀以及在接收影片時請求傳送新關鍵幀的方法。這些可能有助於接收方更快地開始觀看影片,例如,如果他們加入一個正在傳送增量幀的電話會議。

以下示例提供了使用基於 TransformStream 的實現來使用該框架的更具體示例。

測試是否支援編碼轉換

透過檢查是否存在 RTCRtpSender.transform(或 RTCRtpReceiver.transform)來測試是否支援編碼轉換

js
const supportsEncodedTransforms =
  window.RTCRtpSender && "transform" in RTCRtpSender.prototype;

為傳出幀新增轉換

透過將其對應的 RTCRtpScriptTransform 分配給傳出軌道的 RTCRtpSender.transform,在 Worker 中執行的轉換被插入到傳出的 WebRTC 流水線中。

此示例展示瞭如何透過 WebRTC 從使用者的網路攝像頭流式傳輸影片,並新增 WebRTC 編碼轉換來修改傳出流。程式碼假設有一個名為 peerConnectionRTCPeerConnection,該連線已連線到遠端對等方。

首先,我們獲取一個 MediaStreamTrack,使用 getUserMedia() 從媒體裝置獲取影片 MediaStream,然後使用 MediaStream.getTracks() 方法獲取流中的第一個 MediaStreamTrack

使用 addTrack() 將軌道新增到對等連線,這會開始將其流式傳輸到遠端對等方。addTrack() 方法返回用於傳送軌道的 RTCRtpSender

js
// Get Video stream and MediaTrack
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
const [track] = stream.getTracks();
const videoSender = peerConnection.addTrack(track, stream);

然後構造一個 RTCRtpScriptTransform,它接受一個 Worker 指令碼(定義轉換)和一個可選物件,該物件可用於向 Worker 傳遞任意訊息(在此示例中,我們使用值為“senderTransform”的 name 屬性來告知 Worker 此轉換將新增到出站流中)。我們透過將其分配給 RTCRtpSender.transform 屬性來將轉換新增到傳出流水線中。

js
// Create a worker containing a TransformStream
const worker = new Worker("worker.js");
videoSender.transform = new RTCRtpScriptTransform(worker, {
  name: "senderTransform",
});

下面使用單獨的傳送方和接收方轉換部分展示瞭如何在 Worker 中使用 name

請注意,您可以隨時新增轉換,但透過在呼叫 addTrack() 後立即新增它,轉換將獲取傳送的第一個編碼幀。

為傳入幀新增轉換

透過將其對應的 RTCRtpScriptTransform 分配給傳入軌道的 RTCRtpReceiver.transform,在 Worker 中執行的轉換被插入到傳入的 WebRTC 流水線中。

此示例展示瞭如何新增轉換以修改傳入流。程式碼假設有一個名為 peerConnectionRTCPeerConnection,該連線已連線到遠端對等方。

首先,我們新增一個 RTCPeerConnection track 事件處理程式,以捕獲對等方開始接收新軌道時的事件。在處理程式中,我們構造一個 RTCRtpScriptTransform 並將其新增到 event.receiver.transformevent.receiver 是一個 RTCRtpReceiver)。與上一節一樣,建構函式接受一個具有 name 屬性的物件,但在這裡我們使用 receiverTransform 作為值來告知 Worker 幀是傳入的。

js
peerConnection.ontrack = (event) => {
  const worker = new Worker("worker.js");
  event.receiver.transform = new RTCRtpScriptTransform(worker, {
    name: "receiverTransform",
  });
  received_video.srcObject = event.streams[0];
};

再次注意,您可以隨時新增轉換流。但是,透過在 track 事件處理程式中新增它,可以確保轉換流將獲取該軌道的第一個編碼幀。

Worker 實現

Worker 指令碼必須實現 rtctransform 事件的處理程式,建立一個 管道鏈,將 event.transformer.readable (ReadableStream) 流透過 TransformStream 管道到 event.transformer.writable (WritableStream) 流。

Worker 可能支援轉換傳入或傳出的編碼幀,或者兩者都支援,並且轉換可以是硬編碼的,也可以在執行時使用從 Web 應用程式傳遞的資訊進行配置。

基本 WebRTC 編碼轉換

下面的示例展示了一個基本的 WebRTC 編碼轉換,它對排隊幀中的所有位進行反轉。它不需要使用從主執行緒傳入的選項,因為相同的演算法可以在傳送方流水線中用於反轉位,並在接收方流水線中用於恢復位。

程式碼實現了 rtctransform 事件的事件處理程式。它構造一個 TransformStream,然後使用 ReadableStream.pipeThrough() 透過它進行管道傳輸,最後使用 ReadableStream.pipeTo() 管道到 event.transformer.writable

js
addEventListener("rtctransform", (event) => {
  const transform = new TransformStream({
    start() {}, // Called on startup.
    flush() {}, // Called when the stream is about to be closed.
    async transform(encodedFrame, controller) {
      // Reconstruct the original frame.
      const view = new DataView(encodedFrame.data);

      // Construct a new buffer
      const newData = new ArrayBuffer(encodedFrame.data.byteLength);
      const newView = new DataView(newData);

      // Negate all bits in the incoming frame
      for (let i = 0; i < encodedFrame.data.byteLength; ++i) {
        newView.setInt8(i, ~view.getInt8(i));
      }

      encodedFrame.data = newData;
      controller.enqueue(encodedFrame);
    },
  });
  event.transformer.readable
    .pipeThrough(transform)
    .pipeTo(event.transformer.writable);
});

WebRTC 編碼轉換的實現類似於“通用”TransformStream,但有一些重要的區別。與通用流一樣,其建構函式接受一個定義了可選 start() 方法(在構造時呼叫)、flush() 方法(在流即將關閉時呼叫)和 transform() 方法(每次有資料塊需要處理時呼叫)的物件。與通用建構函式不同,在建構函式物件中傳遞的任何 writableStrategyreadableStrategy 屬性都會被忽略,並且排隊策略完全由使用者代理管理。

transform() 方法也有所不同,因為它傳遞的是 RTCEncodedVideoFrameRTCEncodedAudioFrame,而不是通用的“資料塊”。這裡顯示的該方法的實際程式碼除了演示如何將幀轉換為可以修改並隨後在流上排隊的格式之外,並沒有什麼值得注意的。

使用單獨的傳送方和接收方轉換

如果傳送和接收時的轉換函式相同,則前面的示例有效,但在許多情況下,演算法會不同。您可以為傳送方和接收方使用單獨的 Worker 指令碼,或者在一個 Worker 中處理這兩種情況,如下所示。

如果 Worker 用於傳送方和接收方,它需要知道當前編碼幀是來自編解碼器的傳出幀,還是來自打包器的傳入幀。此資訊可以使用 RTCRtpScriptTransform 建構函式中的第二個選項指定。例如,我們可以為傳送方和接收方定義一個單獨的 RTCRtpScriptTransform,傳遞相同的 Worker,以及一個具有 name 屬性的 options 物件,該屬性指示轉換是在傳送方還是接收方中使用(如前面幾節所示)。然後,該資訊在 Worker 中的 event.transformer.options 中可用。

在此示例中,我們在全域性專用 Worker 作用域物件上實現了 onrtctransform 事件處理程式。name 屬性的值用於確定要構造哪個 TransformStream(實際的構造方法未顯示)。

js
// Code to instantiate transform and attach them to sender/receiver pipelines.
onrtctransform = (event) => {
  let transform;
  if (event.transformer.options.name === "senderTransform")
    transform = createSenderTransform(); // returns a TransformStream
  else if (event.transformer.options.name === "receiverTransform")
    transform = createReceiverTransform(); // returns a TransformStream
  else return;
  event.transformer.readable
    .pipeThrough(transform)
    .pipeTo(event.transformer.writable);
};

請注意,建立管道鏈的程式碼與前面的示例相同。

與轉換的執行時通訊

RTCRtpScriptTransform 建構函式允許您將選項和可傳輸物件傳遞給 Worker。在前面的示例中,我們傳遞了靜態資訊,但有時您可能希望在執行時修改 Worker 中的轉換演算法,或者從 Worker 獲取資訊。例如,支援加密的 WebRTC 電話會議可能需要向轉換使用的演算法新增新金鑰。

雖然可以使用 Worker.postMessage() 在執行轉換程式碼的 Worker 和主執行緒之間共享資訊,但通常更容易共享 MessageChannel 作為 RTCRtpScriptTransform 建構函式選項,因為這樣在處理新的編碼幀時,通道上下文可以直接在 event.transformer.options 中使用。

下面的程式碼建立一個 MessageChannel 並將其第二個埠傳輸到 Worker。主執行緒和轉換隨後可以使用第一個和第二個埠進行通訊。

js
// Create a worker containing a TransformStream
const worker = new Worker("worker.js");

// Create a channel
// Pass channel.port2 to the transform as a constructor option
// and also transfer it to the worker
const channel = new MessageChannel();
const transform = new RTCRtpScriptTransform(
  worker,
  { purpose: "encrypt", port: channel.port2 },
  [channel.port2],
);

// Use the port1 to send a string.
// (we can send and transfer basic types/objects).
channel.port1.postMessage("A message for the worker");
channel.port1.start();

在 Worker 中,該埠作為 event.transformer.options.port 可用。下面的程式碼展示瞭如何偵聽埠的 message 事件以從主執行緒獲取訊息。您還可以使用該埠將訊息傳送回主執行緒。

js
event.transformer.options.port.onmessage = (event) => {
  // The message payload is in 'event.data';
  console.log(event.data);
};

觸發關鍵幀

原始影片很少被髮送或儲存,因為它需要大量空間和頻寬來將每個幀表示為完整的影像。相反,編解碼器會定期生成一個“關鍵幀”,其中包含足以構建完整影像的資訊,並在關鍵幀之間傳送僅包含自上次增量幀以來變化的“增量幀”。雖然這比傳送原始影片效率高得多,但這意味著為了顯示與特定增量幀關聯的影像,您需要最後一個關鍵幀和所有後續增量幀。

這可能會導致新使用者加入 WebRTC 會議應用程式時出現延遲,因為他們無法顯示影片,直到收到第一個關鍵幀。同樣,如果使用編碼轉換來加密幀,則接收方將無法顯示影片,直到他們收到用其金鑰加密的第一個關鍵幀。

為了確保在需要時儘快傳送新的關鍵幀,event.transformer 中的 RTCRtpScriptTransformer 物件有兩個方法:RTCRtpScriptTransformer.generateKeyFrame(),它使編解碼器生成關鍵幀,以及 RTCRtpScriptTransformer.sendKeyFrameRequest(),接收方可以使用它從傳送方請求關鍵幀。

下面的示例展示了主執行緒如何將加密金鑰傳遞給傳送方轉換,並觸發編解碼器生成關鍵幀。請注意,主執行緒無法直接訪問 RTCRtpScriptTransformer 物件,因此它需要將金鑰和限制識別符號(“rid”)傳遞給 Worker(“rid”是流 ID,指示必須生成關鍵幀的編碼器)。這裡我們使用 MessageChannel 來實現,採用與上一節相同的模式。程式碼假設已經存在對等連線,並且 videoSender 是一個 RTCRtpSender

js
const worker = new Worker("worker.js");
const channel = new MessageChannel();

videoSender.transform = new RTCRtpScriptTransform(
  worker,
  { name: "senderTransform", port: channel.port2 },
  [channel.port2],
);

// Post rid and new key to the sender
channel.port1.start();
channel.port1.postMessage({
  rid: "1",
  key: "93ae0927a4f8e527f1gce6d10bc6ab6c",
});

Worker 中的 rtctransform 事件處理程式獲取埠並使用它偵聽來自主執行緒的 message 事件。如果收到事件,它會獲取 ridkey,然後呼叫 generateKeyFrame()

js
event.transformer.options.port.onmessage = (event) => {
  const { rid, key } = event.data;
  // key is used by the transformer to encrypt frames (not shown)

  // Get codec to generate a new key frame using the rid
  // Here 'rcEvent' is the rtctransform event.
  rcEvent.transformer.generateKeyFrame(rid);
};

接收方請求新關鍵幀的程式碼幾乎相同,只是沒有指定“rid”。這是僅埠訊息處理程式的程式碼

js
event.transformer.options.port.onmessage = (event) => {
  const { key } = event.data;
  // key is used by the transformer to decrypt frames (not shown)

  // Request sender to emit a key frame.
  transformer.sendKeyFrameRequest();
};

瀏覽器相容性

另見