What is WHEP?
WHEP (WebRTC HTTP Egress Protocol) is a standardized protocol for consuming WebRTC streams from a server. It is the egress (playback) counterpart to WHIP (ingestion). Where WHIP pushes media into a server, WHEP pulls media out of a server.
Portable RTC acts as a WHEP server: external WHEP clients (browsers, media players, or other servers) connect to Portable RTC over HTTP/HTTPS, exchange SDP offers/answers, and receive WebRTC media streams.
WHEP offers the same benefits as WHIP — standards-based interoperability, firewall simplicity (uses standard HTTP ports), and no proprietary signaling — but for the playback direction.
How Portable RTC WHEP Playback Works
Browser / Player (WHEP client) PortableRTC (WHEP server)
| |
| POST /whep/{app}/{stream} |
| (Content-Type: application/sdp) |
| body = SDP offer (recvonly) |
|----------------------------------->|
| | create PeerConnection
| | negotiate codecs
| | init media source
| |
| 201 Created (SDP answer, Location)|
|<-----------------------------------|
| |
| WebRTC (ICE / DTLS-SRTP) |
|===================================>|
| media flows |
| |
| DELETE {Location} |
|----------------------------------->|
| | tear down session
The complete flow:
- The client creates an RTCPeerConnection with recvonly transceivers and sends its SDP offer to the WHEP endpoint via HTTP POST.
- Portable RTC matches the stream from the URL, initializes the media source, negotiates codecs, and creates an SDP answer.
- Portable RTC responds with HTTP 201 Created, the SDP answer in the body, and a
Locationheader pointing to the session resource. - ICE connectivity is established, DTLS-SRTP handshake completes, and media flows from Portable RTC to the client.
- The client sends HTTP DELETE to the resource URL to tear down the session.
Step 1: Configure the WHEP URL Path
The WHEP endpoint prefix is configurable via the <whep_path> element in portablertc.cfg:
<config>
<http_enable>1</http_enable>
<http_port>80</http_port>
<https_enable>1</https_enable>
<https_port>8443</https_port>
<whep_path>whep</whep_path>
<!-- application blocks follow -->
</config>
WHEP path rules:
- The configured value is normalized to start with
/and end with/. For example,whepbecomes/whep/. - If
<whep_path>is empty or missing, the default/whep/is used. - The WHEP feature is served on both the HTTP server (
<http_port>) and the HTTPS server (<https_port>).
Step 2: Understand the WHEP URL Stream Mapping
The WHEP URL maps directly to a stream using the pattern {whep_path}/{app}/{stream}:
POST {whep_path}{app}/{stream}
# Example:
POST /whep/myapp/test.mp4
# URL path parsed as: {whep_path}/{app}/{stream}
# /whep/myapp/live → app = "myapp", stream = "live"
Where:
| Component | Description |
|---|---|
{whep_path} |
The WHEP endpoint prefix (default /whep/, configurable via <whep_path>). |
{app} |
The application name, which must match an <application><name> in the configuration. |
{stream} |
The stream/playpath, resolved by the normal media source logic (file, proxy, device, live). |
Step 3: Configure the Application and Default Output
The <output> block within the application defines what codecs/format the media is transcoded to for WHEP playback. The <url> field is left empty for the default output configuration, which applies to all streams not matching a specific output rule.
<config>
<http_enable>1</http_enable>
<http_port>80</http_port>
<https_enable>1</https_enable>
<https_port>8443</https_port>
<whep_path>whep</whep_path>
<application>
<name>myapp</name>
<!-- Default output config: what codecs/format the media is transcoded to -->
<output>
<url></url>
<video>
<codec>H264</codec>
</video>
<audio>
<codec>OPUS</codec>
<samplerate>48000</samplerate>
<channels>2</channels>
</audio>
</output>
</application>
</config>
Step 4: Understand Codec Negotiation
Portable RTC negotiates codecs between the configured output codec and the client's supported codecs. The negotiated codec is applied to the media transcoder before media initialization, so the stream is produced in the negotiated format and the SDP answer matches what is actually sent.
Video Codec Negotiation
- If the config video codec is
H265and the client supports H265 → use H265. - If the config video codec is
H264and the client supports H264 → use H264. - Otherwise, prefer the client's supported codec: H264, then H265.
- If the client supports neither, video is omitted.
Audio Codec Negotiation
- If the config audio codec is
OPUSand the client supports opus → use OPUS. - If the config audio codec is
G711Aand the client supports PCMA → use G711A. - If the config audio codec is
G711Uand the client supports PCMU → use G711U. - Otherwise, prefer the client's supported codec: OPUS, then PCMA, then PCMU.
- If the client supports none, audio is omitted.
WHEP Protocol Interaction
HTTP POST (Client Sends SDP Offer)
The client sends its SDP offer (with a=recvonly media) as the request body:
POST /whep/myapp/test.mp4 HTTP/1.1
Host: 192.168.3.36:80
Content-Type: application/sdp
Content-Length: <length>
<SDP offer>
HTTP 201 Created (Server Sends SDP Answer)
Portable RTC responds with 201 Created, the SDP answer in the body, and the session Location:
HTTP/1.1 201 Created
Content-Type: application/sdp
Location: /whep/session/{session-id}
Content-Length: <length>
<SDP answer>
The answer SDP:
- Echoes the client's
a=midvalues and BUNDLE group. - Uses the negotiated payload types from the client's offer.
- Declares
a=sendonlyfor arecvonlyoffer. - Carries Portable RTC's ICE candidates, ICE ufrag/pwd, DTLS fingerprint, and
a=setup:active.
WebRTC Session
ICE, DTLS-SRTP, and media transmission proceed normally. Media starts when the connection reaches the completed peer connection state.
HTTP DELETE (Client Tears Down)
The client sends DELETE to the session resource:
DELETE /whep/session/{session-id} HTTP/1.1
Host: 192.168.3.36:80
Portable RTC tears down the session and responds with 200 OK.
Step 5: Use the Built-in WHEP Player Page
Portable RTC ships with a built-in WHEP player page for quick testing:
- HTTP:
http://{host}:{http_port}/whep.html - HTTPS:
https://{host}:{https_port}/whep.html
Usage steps:
- Open the player page in a browser (Chrome / Edge / Firefox).
- The WHEP URL input is pre-filled from the page origin, e.g.:
http://192.168.3.36/whep/myapp/test.mp4https://192.168.3.36:8443/whep/myapp/test.mp4
- Modify the URL as needed (the stream path follows
{whep_path}/{app}/{stream}). - Click Play. The video starts when the connection is established.
- Click Stop to tear down the session.
Player page notes:
- The page automatically uses the same protocol (http/https) as the page itself, avoiding mixed-content blocking.
- Both audio and video tracks are attached to the same
<video>element so the native volume control works. - The video is initially muted for autoplay; it unmutes automatically once an audio track is received.
Examples
Example 1: Play a Local File over HTTP
- Configure
<whep_path>whep</whep_path>, applicationmyapp, and placetest.mp4in the working directory. - Open
http://192.168.3.36/whep.html. - Enter
http://192.168.3.36/whep/myapp/test.mp4. - Click Play.
Example 2: Play a Stream over HTTPS
- Ensure HTTPS is enabled (
<https_enable>1</https_enable>, withcert_fileandkey_file). - Open
https://192.168.3.36:8443/whep.html. - Enter
https://192.168.3.36:8443/whep/myapp/test.mp4. - Click Play.
Example 3: Play a Proxy Stream (RTSP Camera)
With a proxy configured for myapp/live:
<application>
<name>myapp</name>
<proxy>
<suffix>live</suffix>
<url>rtsp://192.168.1.100:554/live/stream1</url>
</proxy>
</application>
The WHEP URL is {whep_path}/myapp/live, e.g. http://192.168.3.36/whep/myapp/live.
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
Page shows error immediately |
Mixed content (HTTPS page + HTTP WHEP URL) | Use the same scheme as the page. The built-in player auto-fills the URL from the page origin. |
No video, checking then failed |
ICE role conflict / wrong DTLS role | Use a browser that supports the negotiated codec; check the log for details. |
| Audio button disabled | Old player version | Use the current whep.html/whep.js which combine audio and video tracks. |
| No audio but video plays | Codec mismatch (config vs client) | Ensure the config audio codec is supported by the client, or rely on automatic codec negotiation. |
| 404 / stream not found | Wrong app or stream path | Verify the URL follows {whep_path}/{app}/{stream} and that the app/stream exists in the configuration. |
Limitations
- No Trickle ICE: ICE candidates are bundled in the SDP offer/answer; trickle ICE is not supported (no PATCH endpoint).
- No Authentication: WHEP endpoints do not yet support Bearer token authentication.
- DTLS Role: The answer uses
a=setup:active, meaning Portable RTC acts as the DTLS client.
Best Practices
- Prefer OPUS for Audio: Configure
OPUS(with 48 kHz sample rate) as the default audio codec. OPUS is supported by all modern browsers and offers the best quality-to-bitrate ratio. G.711 should only be used for compatibility with legacy systems. - Use HTTPS for Production: Enable HTTPS in
portablertc.cfgand use thehttps://WHEP URL. This prevents mixed-content issues and encrypts the SDP exchange (which contains ICE credentials). - Leverage Codec Negotiation: Rather than forcing a single codec, configure sensible defaults and let Portable RTC negotiate with the client. The negotiation falls back gracefully when the client lacks support for the configured codec.
- Test with the Built-in Player First: Use
whep.htmlto verify that WHEP playback works end-to-end before integrating WHEP into your own application. This isolates configuration issues from client implementation issues. - Keep the Default WHEP Path: Unless there is a specific reason to change it, use the default
/whep/path. This simplifies client configuration and aligns with common WHEP conventions.