How to Play Streams via WHEP with Portable RTC

A complete guide to using Happytime Portable RTC as a WHEP (WebRTC HTTP Egress Protocol) server. Expose local files, proxy streams, and capture devices to any WHEP-compliant client — browsers, players, or other servers — using standard HTTP-based SDP exchange.

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

WHEP Playback Data Flow
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:

  1. The client creates an RTCPeerConnection with recvonly transceivers and sends its SDP offer to the WHEP endpoint via HTTP POST.
  2. Portable RTC matches the stream from the URL, initializes the media source, negotiates codecs, and creates an SDP answer.
  3. Portable RTC responds with HTTP 201 Created, the SDP answer in the body, and a Location header pointing to the session resource.
  4. ICE connectivity is established, DTLS-SRTP handshake completes, and media flows from Portable RTC to the client.
  5. 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:

portablertc.cfg — WHEP Path Configuration
<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, whep becomes /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}:

WHEP URL Structure
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.

portablertc.cfg — WHEP Playback Configuration
<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 H265 and the client supports H265 → use H265.
  • If the config video codec is H264 and 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 OPUS and the client supports opus → use OPUS.
  • If the config audio codec is G711A and the client supports PCMA → use G711A.
  • If the config audio codec is G711U and 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:

WHEP POST Request
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:

WHEP 201 Response
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=mid values and BUNDLE group.
  • Uses the negotiated payload types from the client's offer.
  • Declares a=sendonly for a recvonly offer.
  • 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:

WHEP DELETE Request
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:

  1. Open the player page in a browser (Chrome / Edge / Firefox).
  2. The WHEP URL input is pre-filled from the page origin, e.g.:
    • http://192.168.3.36/whep/myapp/test.mp4
    • https://192.168.3.36:8443/whep/myapp/test.mp4
  3. Modify the URL as needed (the stream path follows {whep_path}/{app}/{stream}).
  4. Click Play. The video starts when the connection is established.
  5. 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

  1. Configure <whep_path>whep</whep_path>, application myapp, and place test.mp4 in the working directory.
  2. Open http://192.168.3.36/whep.html.
  3. Enter http://192.168.3.36/whep/myapp/test.mp4.
  4. Click Play.

Example 2: Play a Stream over HTTPS

  1. Ensure HTTPS is enabled (<https_enable>1</https_enable>, with cert_file and key_file).
  2. Open https://192.168.3.36:8443/whep.html.
  3. Enter https://192.168.3.36:8443/whep/myapp/test.mp4.
  4. Click Play.

Example 3: Play a Proxy Stream (RTSP Camera)

With a proxy configured for myapp/live:

portablertc.cfg — Proxy for WHEP Playback
<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.cfg and use the https:// 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.html to 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.