Start · Sprachen · JavaScript · Referenz · RTCRtpSender.transform

RTCRtpSender.transform

Eigenschaft

Legt eine <code>RTCRtpScriptTransform</code> fest, mit der kodierte Audio- und Videoframes in einem Worker bearbeitet werden, bevor der Sender sie verpackt und verschickt.

seit JavaScript Firefox 117+, Safari 15.4+; Chrome erst in neueren Kategorie: webapi

Signatur

RTCRtpSender.transform

Beschreibung

Die Eigenschaft transform eines RTCRtpSender gehört zur API WebRTC Encoded Transform. Sie erlaubt einen Eingriff in die Sende-Pipeline: Jeder Frame, den der Encoder erzeugt hat, läuft durch einen TransformStream in einem Web Worker, bevor er in RTP-Pakete aufgeteilt und verschickt wird. Die Frames sind dabei bereits kodiert, also schon komprimiert.

Typische Einsatzfälle sind:

  • Ende-zu-Ende-Verschlüsselung (E2EE), etwa über einen SFU-Server, der die Inhalte nicht lesen darf. Siehe auch SFrame.
  • Das Anhängen eigener Metadaten an Frames, zum Beispiel für Synchronisation oder Kennungen.
  • Analyse und Protokollierung von Frame-Größen, Frame-Typen (Schlüsselframe oder Delta-Frame) und Zeitstempeln.

Zugewiesen wird ein Objekt vom Typ RTCRtpScriptTransform. Ihr Konstruktor erhält den Worker und optional eigene Optionen. Im Worker wird das Ereignis rtctransform ausgelöst. Dessen event.transformer stellt einen readable- und einen writable-Stream bereit, die man per pipeThrough() und pipeTo() verbindet. Wenn man null zuweist, wird die Transformation wieder entfernt.

Das Gegenstück auf der Empfangsseite ist RTCRtpReceiver.transform. Bei einer Verschlüsselung muss der Empfänger die Frames mit derselben Logik wieder entschlüsseln.

Parameter

Name Typ Default Beschreibung
$value RTCRtpScriptTransform | null null Die Transformation, die auf alle ausgehenden kodierten Frames dieses Senders angewendet wird. Mit null wird die Transformation abgeschaltet.

Rückgabewert

Typ
RTCRtpScriptTransform | null
Beschreibung
Die aktuell gesetzte Transformation. Ist keine gesetzt, ist der Wert null.

Beispiele

Transformation am Sender einrichten (Haupt-Thread)

const pc = new RTCPeerConnection();
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
const worker = new Worker('sender-worker.js');

for (const track of stream.getTracks()) {
  const sender = pc.addTrack(track, stream);
  // Eigene Optionen landen im Worker unter event.transformer.options
  sender.transform = new RTCRtpScriptTransform(worker, {
    operation: 'encrypt',
    kind: track.kind
  });
}

console.log(pc.getSenders().map((s) => s.transform !== null));
[true, true]

Worker: Frames per XOR „verschlüsseln“ (nur zur Veranschaulichung)

// sender-worker.js
const KEY = 0x5a;

const xorFrame = (frame, controller) => {
  const data = new Uint8Array(frame.data);
  // Die ersten Bytes (Codec-Header) unverändert lassen,
  // damit der Paketierer den Frame noch zerlegen kann
  const skip = frame.type === 'key' ? 10 : 3;
  for (let i = skip; i < data.length; i++) {
    data[i] ^= KEY;
  }
  frame.data = data.buffer;
  controller.enqueue(frame);
};

addEventListener('rtctransform', (event) => {
  const { readable, writable, options } = event.transformer;
  console.log('Transformation aktiv für', options.kind);
  readable
    .pipeThrough(new TransformStream({ transform: xorFrame }))
    .pipeTo(writable);
});
Transformation aktiv für video Transformation aktiv für audio

Frame-Statistik erfassen und Transformation wieder entfernen

// Worker: Größe und Typ der Frames zählen und unverändert weitergeben
self.onrtctransform = ({ transformer }) => {
  let bytes = 0;
  transformer.readable
    .pipeThrough(new TransformStream({
      transform(frame, controller) {
        bytes += frame.data.byteLength;
        if (frame.type === 'key') {
          postMessage({ keyframe: frame.getMetadata().synchronizationSource, bytes });
        }
        controller.enqueue(frame);
      }
    }))
    .pipeTo(transformer.writable);
};

// Haupt-Thread: nach 10 Sekunden abschalten
const [sender] = pc.getSenders();
setTimeout(() => {
  sender.transform = null;
  console.log('Transformation entfernt:', sender.transform);
}, 10_000);
Transformation entfernt: null

Feature-Erkennung mit Fallback

const supportsScriptTransform = 'RTCRtpScriptTransform' in window &&
  'transform' in RTCRtpSender.prototype;
const supportsLegacyStreams = 'createEncodedStreams' in RTCRtpSender.prototype;

if (supportsScriptTransform) {
  console.log('Standard-API: sender.transform verwenden');
} else if (supportsLegacyStreams) {
  console.log('Alte Chrome-API: encodedInsertableStreams + createEncodedStreams()');
} else {
  console.log('Keine Encoded Transforms verfügbar');
}
Standard-API: sender.transform verwenden

// Wichtig · Fallstricke

  • Browser-Unterstützung: Firefox (ab 117) und Safari (ab 15.4) unterstützen den Standard mit RTCRtpScriptTransform. Chrome bot lange nur die nicht standardisierte API createEncodedStreams() an, für die new RTCPeerConnection({ encodedInsertableStreams: true }) nötig ist. Prüfe die Unterstützung daher immer per Feature-Erkennung.
  • Ein RTCRtpScriptTransform lässt sich nur einem Sender oder Empfänger zuordnen. Wird es ein zweites Mal verwendet, wird eine InvalidStateError-Ausnahme ausgelöst. Deshalb braucht jeder Sender ein eigenes Objekt. Den Worker selbst kann man dagegen gemeinsam nutzen.
  • Die Transformation läuft ausschließlich im Worker. Im Haupt-Thread gibt es keinen Zugriff auf die Frames.
  • Jeder Frame muss per controller.enqueue() weitergegeben werden. Frames, die unterwegs verloren gehen, führen zu Bildfehlern oder Aussetzern. Eine langsame Transformation erhöht die Latenz.
  • Bei Videocodecs wie VP8 oder H.264 müssen die Header-Bytes teilweise unverschlüsselt bleiben, sonst kann der Paketierer bzw. der Empfänger den Frame nicht verarbeiten. Das XOR-Beispiel ist keine sichere Verschlüsselung. Für echte E2EE nutzt man crypto.subtle (z. B. AES-GCM) oder SFrame.
  • Wenn der Inhalt verschlüsselt wird, braucht auch der Empfänger eine passende RTCRtpReceiver.transform. Sonst empfängt er nur unlesbare Daten.