Skip to content

Integrate a custom RTC service

Deploy and operate a custom RTC service in your own cloud to provide real-time audio, video, or other media workloads to devices through TiRTC.

Core concepts

ConceptMeaning
Custom RTC serviceA service deployed and operated in your own cloud that uses TiRTC to provide real-time media, commands, or media workloads to devices
WHIPShort for WebRTC-HTTP Ingestion Protocol, an IETF Standards Track protocol defined by RFC 9725. A device uses WHIP through Tange Platform to connect to a custom RTC service
RegionA TiRTC service region. Tange Platform selects the registered custom RTC service endpoint for the device's Region
peer_idA service identifier and connection parameters in the form whips://{service_name}?{query}
Connection tokenA signed credential bound to the device ID, service name, and complete query, used to authenticate session creation

Architecture

Custom RTC service connection sequence

The following sequence diagram shows the complete flow from business authorization and the device initiating a connection to the custom RTC service accepting the WHIP connection request. Deploy and operate both the authorization service and the custom RTC service in your own cloud.

Custom RTC service authorization and device connection sequence

Server SDK architecture

This guide uses the server SDK to build a custom RTC service. The following diagram shows how the custom RTC service uses each Go package in the server SDK.

Server SDK layers

The server SDK uses the Go module github.com/tangeai/tirtc-service-sdk/v2. This guide uses the following three packages under pkg/:

Go packageResponsibilityUsage
tirtcxInitialize the TiRTC engine, accept WHIP sessions, and handle connections, media, and commandsRequired core package for a custom RTC service
tirtcxauthIssue and verify connection tokens and provide HTTP bearer authenticationThe authorization service can issue tokens with it; the custom RTC service uses it for verification
whipechoEmbed Echo diagnostics in an existing HTTP handlerOptional; use it only for integration testing and path diagnostics

Enable the service

Before deployment, provide the following information to Tange Intelligence:

  • A globally unique service_name.
  • At least one Custom RTC Key ID and Custom RTC Key. A Custom RTC Key is an Ed25519 public key; generate its paired private key and keep it only on the authorization service.
  • A public WHIP endpoint for every target Region. Use HTTP or HTTPS according to device capabilities.
RegionArea
cn01Mainland China
na01Americas
ea01Asia
we01Europe

Device-side testing also requires the test device's device_id and device_secret_key. Contact Tange technical support if you do not yet have the service registration details or these two device identity values.

Get tirtc-service-sdk/v2 from Download the server SDK and prepare the TiRTC C library version specified on that page.

Implement the authorization service

The authorization service validates a device's business access, generates peer_id, and issues a connection token to the device. The device passes these two parameters to TiRtcWhipConnect to connect to the target service.

Issue device connection parameters

After authorizing a device, the authorization service generates a peer_id in the following format and issues the corresponding connection token:

text
peer_id = whips://{service_name}?{raw_query}

Use standard URL encoding and keep the total length at or below 2048 bytes. Do not include passwords, private keys, or long-lived credentials.

Connection tokens use Ed25519 signatures. The signing algorithm is as follows:

text
# CanonicalizeQuery parses parameters as application/x-www-form-urlencoded, sorts parameter names
# in ascending byte order, preserves duplicate-value order, and re-encodes them with standard URL query rules.
canonical_query = CanonicalizeQuery(raw_query)
query_digest = Hex(SHA256(canonical_query))

claims = {
  sub: device_id,
  scope: "connect:" + service_name + ":" + query_digest,
  iss: custom_rtc_key_id,
  iat: issued_at,
  exp: expires_at
}

payload = Base64URL(JSON(claims))
signature = Ed25519Sign(private_key, payload)
token = "v1." + payload + "." + Base64URL(signature)

Use the SDK's tirtcxauth package to issue the token as shown above. See the complete tirtcxauth authentication specification.

The token can be reused until exp, and tirtcxauth does not perform replay checks. The device SDK automatically protects the complete TiRtcWhipConnect call from replay by another device. To reject direct replay of requests to the custom RTC service, add a one-time business parameter to the peer_id query and have the custom RTC service consume it on first use.

go
issuer, err := tirtcxauth.NewEd25519TokenIssuer(map[string]string{
    customRtcKeyID: ed25519PrivateKey,
})
if err != nil {
    return err
}

token, err := issuer.Issue(
    serviceName,
    customRtcKeyID,
    deviceID,
    rawQuery,
    30*24*time.Hour,
)
if err != nil {
    return err
}

Return the matching peer_id and token only to an authorized device. The device passes them to TiRtcWhipConnect(peer_id, token, ...); it must not construct peer_id itself. For device SDK initialization and callbacks, see Connect to a device.

Implement the custom RTC service

The custom RTC service must support WHIP to receive the SDP Offer forwarded by Tange Platform, create a session resource, and return an SDP Answer. Before implementing the service, review the TiRTC WHIP protocol constraints.

WHIP protocol constraints

TiRTC is based on IETF RFC 9725 and currently uses non-trickle ICE. Based on the Region, Tange Platform forwards the device's complete SDP Offer and Bearer token to the custom RTC service in one HTTP POST. After the service returns an SDP Answer, Tange Platform completes the device connection and media is carried through TiRTC.

CapabilityStatusImplementation
SDP Offer/Answer over HTTP POSTSupportedThe custom RTC service receives the Offer and returns the Answer
Location session resourceSupportedThe custom RTC service returns and maintains the session resource
End a session with DELETESupportedImplemented by the custom RTC service
Trickle ICE / PATCHNot currently supported
ICE, DTLS, and SRTPSupportedHandled by tirtc-service-sdk

On successful resource creation, return 201 Created, Content-Type: application/sdp, the SDP Answer, and a unique relative Location. Generate the session ID with a cryptographically secure random number generator and ensure it contains at least 128 bits of random entropy. DELETE <Location> does not require authorization; return 204 No Content whether the resource was just removed or was already absent.

StatusMeaning
400 Bad RequestInvalid SDP or request parameters
401 UnauthorizedMissing Bearer token
403 ForbiddenToken verification failed
405 Method Not AllowedMethod is not supported by the endpoint
415 Unsupported Media TypeContent-Type is not application/sdp
500 Internal Server ErrorInternal service failure

Initialize and start tirtc-service-sdk

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

ValueMeaning
serviceNameThe confirmed registered service_name
customRtcKeyIdThe registered Custom RTC Key ID used to select the verification key
customRtcKeyThe registered Ed25519 public key paired with the authorization service'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{
    customRtcKeyId: customRtcKey,
})
if err != nil {
    return err
}
// 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(context.Background()); 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 16 cryptographically secure random bytes.
    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{},
    )
    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, so defer the idempotent WhipSession.Close() before waiting. This releases the session after a timeout, connection failure, or normal disconnect.

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 diagnostics

go
echo, err := whipecho.NewHTTPAdapter(
    acceptor,
    "/whip/echo/resource",
    whipecho.HTTPOptions{},
)
if err != nil {
    return err
}
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.

Run the Echo diagnostic example

examples/quick-start in tangeai/tirtc-service-sdk is a runnable custom RTC service. It implements token verification, WHIP resource creation and deletion, business-media testing, Echo testing, and graceful shutdown.

Set the verification values used during service registration:

bash
export TIRTC_CUSTOM_RTC_KEY_ID='<Custom RTC Key ID>'
export TIRTC_CUSTOM_RTC_KEY='<registered Ed25519 Custom RTC Key>'

Start the service:

bash
git clone https://github.com/tangeai/tirtc-service-sdk.git
cd tirtc-service-sdk/examples/quick-start

go run -tags tirtc_clib . \
  -listen :8080 \
  -service your_service_name \
  -custom-rtc-key-id "$TIRTC_CUSTOM_RTC_KEY_ID" \
  -custom-rtc-key "$TIRTC_CUSTOM_RTC_KEY" \
  -candidate 203.0.113.10

Set -candidate only when the service must advertise a public address that differs from a local interface. Route POST /whip, ordinary-session DELETE /whip/resource/{session_id}, and Echo-session DELETE /whip/echo/resource/{session_id} to the application.

Use examples/whip-token-signer in the same repository to generate Echo connection parameters for a test device:

bash
export TIRTC_PRIVATE_KEY='<Ed25519 private key matching the registered public key>'

cd tirtc-service-sdk/examples
go run ./whip-token-signer \
  -service your_service_name \
  -custom-rtc-key-id "$TIRTC_CUSTOM_RTC_KEY_ID" \
  -private-key "$TIRTC_PRIVATE_KEY" \
  -device-id your_device_id \
  -query '_tg_mode=echo'

Use the returned peer_id and token for device testing. The signer is for development and integration testing only. In production, the authorization service must authorize the request before issuing parameters.

Validate with tirn_probe_device

Get the source from tangeai/tirn-probe-device, build it with a matching TiRTC C SDK, and run:

bash
./build/linux-x86_64/tirn_probe_device media \
  --device-id your_device_id \
  --device-secret-key your_device_secret_key \
  --peer-id "$ECHO_PEER_ID" \
  --token "$CONNECT_TOKEN" \
  --audio-output /tmp/tirn-probe-echo.pcm \
  --duration-sec 10

The media command sends built-in test audio and frames while receiving Echo media. The test passes when all four audio and video send/receive counters are greater than zero. tirn_probe_device is an integration tool, not a production device SDK or part of the custom RTC service.

Deployment requirements

Set the Linux local port range on every custom RTC 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 and close and join normal sessions. If Echo is enabled, 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.

Security requirements

  • Keep the Ed25519 private key only in the authorization service's key-management system. Never send it to a device, custom RTC service, or frontend.
  • The custom RTC service needs only verification public keys. Never log full tokens, private keys, or random session paths, or put them in metric labels and error responses.
  • With HTTP, tokens and random session IDs are not protected by transport encryption. Enable HTTP only when a device requires it and the network path is controlled.
  • Validate query length, character set, and allowed values. Never treat the query as trusted input.

Troubleshooting

SymptomCheck
HTTP 401/403Access Key, public key, token expiry, service name, and complete query
HTTP 413SDP Offer limits in the ingress and application
HTTP 415Whether the proxy preserves Content-Type: application/sdp
SDP returned but connection failsUDP ports, firewall, NAT, and Candidate address
No Echo media_tg_mode=echo and continuous consumption of send/receive events
Events disappear over timeEvent buffers, Conn.EventStats(), and blocking business handlers

When reporting an issue, include the time, Region, service version, TiRTC SDK version, sanitized HTTP status and response, service logs, and EventStats(). Never provide full tokens, private keys, or device secrets.

TiRTC