Skip to content

Build a WHIP Service

The latest Go SDK separates authentication, WHIP accept, business resources, and Echo diagnostics into composable layers.

Initialize and start the SDK

The initialization values come from service registration and the deployment environment:

ValueMeaning
serviceNameThe confirmed registered service_name
accessKeyThe registered Access Key ID used to select the verification key
publicKeyThe registered Ed25519 public key paired with the business server's signing key
candidateIPOptional public IP of the service. Configure it when the service is behind NAT

Initialize and start the process-wide TiRTC engine once. Call tirtcx.Init before any other tirtcx API:

go
if err := tirtcx.Init(); err != nil {
    return err
}
defer tirtcx.Uninit()

verifier, err := tirtcxauth.NewEd25519TokenVerifier(map[string]string{
    accessKey: publicKey,
})
// auth is an HTTP interceptor that verifies the Bearer token and
// adds its claims to the request context.
auth := tirtcxauth.BearerInterceptor(serviceName, verifier)
// acceptor accepts SDP Offers and creates WHIP sessions.
acceptor := tirtcx.NewWhipAcceptor(candidateIP)

// Other initialization.
...

// Start the SDK.
if err := tirtcx.Start(); err != nil {
    return err
}
defer tirtcx.Stop()

Authenticate resource creation

Apply BearerInterceptor only to the session-creation POST handler:

go
mux.HandleFunc("POST /whip", auth(postWHIP))
mux.HandleFunc("DELETE /whip/resource/{session_id}", deleteResource)

After authentication, read the device identity from the request context:

go
claims, ok := tirtcxauth.ClaimsFromContext(r.Context())

Accept a WHIP session

go
func (s *mediaService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Accept only SDP Offers.
    mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
    if err != nil || !strings.EqualFold(mediaType, "application/sdp") {
        http.Error(w, "content-type must be application/sdp", http.StatusUnsupportedMediaType)
        return
    }

    // Limit the SDP Offer to 1 MiB.
    offer, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    // Generate an unpredictable session ID from 128 bits of cryptographic randomness.
    sessionID, err := newSessionID()
    if err != nil {
        http.Error(w, "create session ID: "+err.Error(), http.StatusInternalServerError)
        return
    }

    // Accept the SDP Offer and produce an SDP Answer within 30 seconds.
    acceptCtx, cancelAccept := context.WithTimeout(r.Context(), 30*time.Second)

    // acceptor was created during initialization.
    whipSession, err := acceptor.WhipAccept(
        acceptCtx,
        offer,
        tirtcx.ConnEventOptions{
            ErrorBuffer:        tirtcx.MinEventBufferSize,
            DisconnectedBuffer: tirtcx.MinEventBufferSize,
        },
    )
    cancelAccept()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // Store the session, then wait for the connection and process business media
    // in a separate goroutine.
    sessionCtx, cancelSession := context.WithCancel(context.Background())
    session := &mediaSession{cancel: cancelSession, whipSession: whipSession}
    s.mu.Lock()
    if s.closed {
        s.mu.Unlock()
        cancelSession()
        _ = whipSession.Close()
        http.Error(w, "service is shutting down", http.StatusServiceUnavailable)
        return
    }
    s.sessions[sessionID] = session
    s.wg.Add(1)
    s.mu.Unlock()
    go s.run(sessionCtx, sessionID, session)

    // Return 201, the SDP Answer, and the Location used to delete the session.
    w.Header().Set("Content-Type", "application/sdp")
    w.Header().Set("Location", "/whip/resource/"+sessionID)
    w.WriteHeader(http.StatusCreated)
    _, _ = w.Write(whipSession.AnswerSDP())
}

func newSessionID() (string, error) {
    var value [16]byte
    if _, err := rand.Read(value[:]); err != nil {
        return "", err
    }
    return hex.EncodeToString(value[:]), nil
}

Own the lifecycle

Call WaitConn in a separate goroutine with a connection timeout. It is safe for concurrent and repeated calls. A canceled waiter does not close the session. Call the idempotent WhipSession.Close() to terminate a pending or connected session.

The application owns the normal-session registry and DELETE behavior. DELETE has no token authentication; the random session ID is the access capability. On DELETE <Location>, remove the resource, cancel the business context, and close the session. Return 204 No Content for unknown or already deleted IDs. Support HTTP or HTTPS according to device capabilities, rate-limit the endpoint, and keep complete resource paths out of routine logs.

Use Conn.Done() as the terminal signal and read Conn.Err() afterward. A normal disconnect returns nil.

Configure events

Enable only channels that the application continuously consumes. Full buffers drop events; monitor Conn.EventStats().

Subscription decisions use synchronous SubscribeAudioHandler and SubscribeVideoHandler callbacks. They run on native callbacks and must return quickly.

Optionally embed Echo

go
echo, err := whipecho.NewHTTPAdapter(
    acceptor,
    "/whip/echo/resource",
    whipecho.HTTPOptions{},
)
mux.HandleFunc("POST /whip", auth(echo.Wrap(businessHandler)))
mux.HandleFunc(
    "DELETE /whip/echo/resource/{session_id}",
    echo.DeleteHandler(),
)

whipecho automatically intercepts and manages Echo diagnostic sessions when _tg_mode=echo is present. Parameters prefixed with _tg_ are reserved by Tange and must not be used by business applications.

Ordinary requests are not intercepted by whipecho.

Deployment requirements

Set the Linux local port range on every WHIP Service host to 12768 63999:

bash
sudo sysctl -w net.ipv4.ip_local_port_range="12768 63999"

For a persistent setting, add the following under /etc/sysctl.d/:

text
net.ipv4.ip_local_port_range = 12768 63999

Then run sudo sysctl --system. Allow all UDP ports in the 12768-63999 range through the host firewall, cloud security groups, and upstream network devices.

Shut down

Stop HTTP ingress, close and join normal sessions, call echo.Shutdown(ctx), then call tirtcx.Stop() and tirtcx.Uninit().

Monitor connection success, latency, active sessions, disconnects, send failures, and event drops. Map Echo Stats() and observers into the application's monitoring system.

TiRTC WHIP