Skip to content

feat: Support different rtp header extentions and payload for each consumer#1783

Open
jiyeyuran wants to merge 4 commits into
versatica:v3from
jiyeyuran:fix-issue-1126
Open

feat: Support different rtp header extentions and payload for each consumer#1783
jiyeyuran wants to merge 4 commits into
versatica:v3from
jiyeyuran:fix-issue-1126

Conversation

@jiyeyuran
Copy link
Copy Markdown

Per-Consumer egress PT / header-extension-id remap

Closes #1126.

Motivation

Today Consumer.rtpParameters are fully derived from producer.consumableRtpParameters (the Router's canonical RTP space). In scenarios where the signaling answer dictates the wire-level RTP shape — most prominently WHEP, where the server sends the offer and the client's answer may choose different PT / header-extension-id values — there is no way to make a single Consumer emit RTP whose PT / extension ids differ from the Router's canonical ones. Rewriting the answer SDP is not an option because the offer is already setLocalDescription'd.

This PR adds that capability while preserving full backward compatibility and without touching ingress, routing, RTCP or pipe semantics.

Design

Public API: a new optional rtpParameters field on ConsumerOptions (exposed identically in Node and Rust):

  • If absent → behavior is bit-for-bit identical to today (default ORTC-driven derivation).
  • If present → it is used verbatim as the wire-level Consumer.rtpParameters. The signaling layer validates compatibility against the Producer's consumableRtpParameters:
    • 1:1 codec mapping by MIME type, clock rate, channels and relevant fmtp (H.264 profile-level-id family, VP9 profile-id, RTX apt).
    • RTX entries' apt must resolve to a primary codec in the same list.
    • headerExtensions[].uri must be a subset of the consumable ones.
    • Encoding layer/simulcast structure must match.
  • SSRCs are still allocated by mediasoup; rtpParameters.encodings[].ssrc is ignored on input and overwritten.
  • Not allowed with pipe=true or on a PipeTransport (explicit error).

IPC: the signaling layer ships down a new ConsumerRtpMapping alongside the Consumer's rtpParameters:

table ConsumerCodecMapping            { producer_payload_type: uint8; consumer_payload_type: uint8; }
table ConsumerHeaderExtensionMapping  { producer_ext_id: uint8;       consumer_ext_id: uint8;       }
table ConsumerRtpMapping              { codecs: [ConsumerCodecMapping] (required);
                                        header_extensions: [ConsumerHeaderExtensionMapping] (required); }

Worker — O(1) in-place egress rewrite: each Consumer precomputes two tiny dense lookup tables from the mapping:

  • egressPayloadTypeMap: std::array<uint8_t, 128> (producer PT → consumer PT; 0 = identity).
  • egressHeaderExtensionIdMap: std::array<uint8_t, 15> (producer 1B ext id → consumer 1B ext id; 0 = identity).

Only populated when rtpParameters override is present. On the send path:

RTC::RTP::Packet::ApplyEgressRewrite(newPT, extIdRemap, newExtIds, undo);
// ... rtpStream->ReceivePacket() / SendRtpPacket() / listener->OnConsumerSendRtpPacket() ...
RTC::RTP::Packet::RevertEgressRewrite(undo);
  • Mutates the PT byte in the fixed header and rewrites the ID nibble/byte of each 1-byte (or 2-byte) extension in place — no payload copy, no allocation.
  • EgressRewriteUndo captures the original PT, extension-offset tables and HeaderExtensionIds, so the packet is restored byte-for-byte after the Consumer is done, leaving fan-out to other Consumers unaffected.
  • Upstream Consumer validation guarantees new ext ids are in 1..14, so the hot path has no range checks.

This is applied in all four Consumer flavors: SimpleConsumer, SimulcastConsumer, SvcConsumer, PipeConsumer (the pipe case is kept as an error at API level but the rewrite machinery lives on the base class for uniformity).

NACK/RTX correctness: retransmission buffers must contain packets that match what went out on the wire. For remap Consumers we use a per-call local SharedPacket that receives the rewritten (wire-level) clone, and it is this local clone that RtpStreamSend stores for retransmission. The Router-level sharedPacket remains untouched, so non-remap Consumers on the same fan-out still share a single clone — there is no extra cost for the existing default path. Remap Consumers pay exactly one additional Clone() per stored packet, bounded by the retransmission window.

What changes / what does not

Area Default path (no rtpParameters) Remap path (rtpParameters set)
Producer ingress, RTP forwarding, RTCP, pipe-to-router unchanged unchanged
Consumer.rtpParameters returned to the app canonical (as today) the caller-supplied, wire-level values
Per-packet cost unchanged 1 PT byte + at most N ext-id nibbles/bytes in place, then revert
Allocations in fan-out unchanged at most 1 extra clone per remap Consumer when the packet is stored for RTX

Bindings

Implemented end-to-end in Node and Rust with matching validation, error messages, types and serialization. Default callers (mediasoup-client, libmediasoupclient, pipe consumers, existing tests) are unaffected because the new field is optional.

Backward compatibility

Fully backward compatible. No FBS field is required on the ingress side; the new ConsumerRtpMapping is only emitted when the caller opts in via ConsumerOptions.rtpParameters. All existing callers — including mediasoup-client, libmediasoupclient and pipe producers/consumers — retain their current behavior byte-for-byte.


…nsumer

This commit introduces the ability to specify custom RTP parameters for consumers, allowing for advanced scenarios where the wire-level payload types and header-extension IDs differ from the router's canonical values. The implementation includes validation against the producer's consumable RTP parameters and ensures compatibility. Additionally, tests have been added to verify the correct behavior of this feature, including rejection of overrides in pipe transports and validation of codec mappings.
@jiyeyuran
Copy link
Copy Markdown
Author

@ibc The Node.js and Rust implementations were generated by AI based on my Go code (referencing PR: #1783). This AI-generated code requires a thorough review. I have validated that the WHEP functionality works correctly using my Golang implementation.

@ibc
Copy link
Copy Markdown
Member

ibc commented Apr 23, 2026

Thanks a lot @jiyeyuran. Please let us get the proper time to review this in depth. It's gonna be long due to the heavy implications of this sensitive change.

@ibc ibc requested review from ibc and jmillan April 23, 2026 10:15
wuxinfei added 3 commits April 23, 2026 19:57
This update modifies the handling of rtcpFeedback in both TypeScript and Rust implementations to ensure that the final Consumer RTP parameters adhere to RFC 4585 §4.2.2. The changes preserve the caller-advertised rtcpFeedback list, preventing the Router's consumable feedback from leaking into WHEP answers. The implementation includes filtering logic to maintain compatibility with the enableRtx setting.
This commit enhances the readability of the test code by formatting the codec lookups for H264 and RTX into multi-line statements. Additionally, it updates the error type thrown in the test for invalid RTP parameters from TypeError to UnsupportedError, ensuring more accurate error handling in the test cases.
@ibc
Copy link
Copy Markdown
Member

ibc commented May 5, 2026

Definitely it's gonna take longer than expected for us to review this PR. Very busy. But will do in next weeks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Support different rtp header extentions and payload for each consumer.

2 participants