Signatur
Beschreibung
RTCEncodedAudioFrame ist ein einzelner, bereits kodierter Audio-Frame (z. B. Opus) in einer WebRTC-Verbindung. Du bekommst solche Frames nicht direkt im Hauptthread. Sie laufen durch einen WebRTC Encoded Transform: Auf der Senderseite liegen sie zwischen Encoder und Paketierer, auf der Empfängerseite zwischen Depaketierer und Decoder.
Typische Einsatzfälle:
- Ende-zu-Ende-Verschlüsselung (E2EE), zum Beispiel über einen SFU-Server, der die Inhalte nicht lesen soll
- Zusätzliche Metadaten an die Nutzdaten anhängen
- Analyse und Protokollierung, etwa von Frame-Größen oder RTP-Zeitstempeln
Wichtige Mitglieder:
dataist einArrayBuffermit den kodierten Nutzdaten und lässt sich beschreiben.timestampist der RTP-Zeitstempel. Diese Eigenschaft ist veraltet, stattdessen giltrtpTimestampin den Metadaten.getMetadata()gibt ein Objekt mitsynchronizationSource,payloadType,contributingSources,sequenceNumber,rtpTimestampundmimeTypezurück.
Nach dem Standard verarbeitest du die Frames in einem Worker. Dort empfängt der Handler des rtctransform-Events einen RTCRtpScriptTransformer mit einem readable- und einem writable-Stream. Jeden Frame, den du liest, schreibst du nach der Bearbeitung wieder in writable. Frames, die du nicht zurückschreibst, werden verworfen.
Der Konstruktor new RTCEncodedAudioFrame(originalFrame, options) kommt erst in neueren Browsern vor. Er kopiert einen Frame, auf Wunsch mit geänderten Metadaten.
Parameter
| Name | Typ | Default | Beschreibung |
|---|---|---|---|
| $originalFrame Pflicht | RTCEncodedAudioFrame | Nur für den Konstruktor: der Frame, dessen Daten und Metadaten kopiert werden. | |
| $options | { metadata?: RTCEncodedAudioFrameMetadata } | {} | Nur für den Konstruktor: Metadaten, die im neuen Frame die Werte des Original-Frames ersetzen, z. B. rtpTimestamp. |
Rückgabewert
data, timestamp und getMetadata(). Der Konstruktor gibt eine unabhängige Kopie zurück.Beispiele
Hauptthread: Transform an Sender und Empfänger hängen
const worker = new Worker('audio-transform.js');
const pc = new RTCPeerConnection();
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const [track] = stream.getAudioTracks();
const sender = pc.addTrack(track, stream);
// Ausgehende Frames im Worker bearbeiten
sender.transform = new RTCRtpScriptTransform(worker, { side: 'send' });
// Eingehende Frames ebenfalls über den Worker leiten
pc.addEventListener('track', ({ receiver }) => {
receiver.transform = new RTCRtpScriptTransform(worker, { side: 'receive' });
});
Worker: einfache XOR-Verschleierung der Nutzdaten
// audio-transform.js
const KEY = 0x5a;
const xorFrame = (frame, controller) => {
const bytes = new Uint8Array(frame.data);
for (let i = 0; i < bytes.length; i++) bytes[i] ^= KEY;
frame.data = bytes.buffer; // geänderte Daten zurückschreiben
controller.enqueue(frame);
};
addEventListener('rtctransform', ({ transformer }) => {
// Senden und Empfangen nutzen dieselbe (symmetrische) Operation
transformer.readable
.pipeThrough(new TransformStream({ transform: xorFrame }))
.pipeTo(transformer.writable);
});
Worker: Metadaten der Frames protokollieren
addEventListener('rtctransform', ({ transformer }) => {
const { side } = transformer.options;
transformer.readable
.pipeThrough(new TransformStream({
transform(frame, controller) {
const { synchronizationSource, payloadType, rtpTimestamp, mimeType } = frame.getMetadata();
console.log(side, mimeType, `SSRC=${synchronizationSource}`,
`PT=${payloadType}`, `TS=${rtpTimestamp}`, `${frame.data.byteLength} Bytes`);
controller.enqueue(frame);
}
}))
.pipeTo(transformer.writable);
});
Chromium-Fallback mit createEncodedStreams()
// Ältere Chrome-Versionen: nicht standardisierte API
const pc = new RTCPeerConnection({ encodedInsertableStreams: true });
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const sender = pc.addTrack(stream.getAudioTracks()[0], stream);
if (!('RTCRtpScriptTransform' in window) && sender.createEncodedStreams) {
const { readable, writable } = sender.createEncodedStreams();
const worker = new Worker('audio-transform-legacy.js');
// Streams sind übertragbar (Transferable)
worker.postMessage({ readable, writable }, [readable, writable]);
}
// Wichtig · Fallstricke
- Browser-Unterstützung: Firefox (ab 117) und Safari (ab 15.4) unterstützen den Standard mit
RTCRtpScriptTransform. Chromium hatte lange nurcreateEncodedStreams(), für dasencodedInsertableStreams: truein derRTCPeerConnection-Konfiguration nötig ist. Neuere Chrome-Versionen unterstützen auchRTCRtpScriptTransform. Prüfe beide Wege mit Feature-Detection. - Jeden Frame zurückschreiben: Frames, die du nicht per
enqueue()oder inwritableweitergibst, kommen beim Empfänger nicht an und erzeugen hörbare Aussetzer. timestampist veraltet. Verwende stattdessengetMetadata().rtpTimestamp.- Performance: Die Transform-Funktion läuft für jeden Frame, bei Opus meist alle 20 ms. Vermeide darin teure Speicherbelegungen und synchrone Blockaden.
- Sicherheit: XOR ist keine Verschlüsselung. Für echte Ende-zu-Ende-Verschlüsselung brauchst du z. B. AES-GCM über
crypto.subtle, eine eindeutige IV pro Frame und einen sicheren Schlüsselaustausch, etwa nach dem SFrame-Konzept. Sender und Empfänger müssen dasselbe Schema verwenden, sonst kann der Decoder die Daten nicht lesen. - Der Konstruktor und manche Metadaten-Felder, z. B.
sequenceNumberodermimeType, sind noch nicht überall verfügbar.