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
| Concept | Meaning |
|---|---|
| Custom RTC service | A service deployed and operated in your own cloud that uses TiRTC to provide real-time media, commands, or media workloads to devices |
| WHIP | Short 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 |
| Region | A TiRTC service region. Tange Platform selects the registered custom RTC service endpoint for the device's Region |
peer_id | A service identifier and connection parameters in the form whips://{service_name}?{query} |
| Connection token | A 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.
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.
The server SDK uses the Go module github.com/tangeai/tirtc-service-sdk/v2. This guide uses the following three packages under pkg/:
| Go package | Responsibility | Usage |
|---|---|---|
tirtcx | Initialize the TiRTC engine, accept WHIP sessions, and handle connections, media, and commands | Required core package for a custom RTC service |
tirtcxauth | Issue and verify connection tokens and provide HTTP bearer authentication | The authorization service can issue tokens with it; the custom RTC service uses it for verification |
whipecho | Embed Echo diagnostics in an existing HTTP handler | Optional; 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.
| Region | Area |
|---|---|
cn01 | Mainland China |
na01 | Americas |
ea01 | Asia |
we01 | Europe |
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:
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:
# 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.
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.
| Capability | Status | Implementation |
|---|---|---|
| SDP Offer/Answer over HTTP POST | Supported | The custom RTC service receives the Offer and returns the Answer |
Location session resource | Supported | The custom RTC service returns and maintains the session resource |
End a session with DELETE | Supported | Implemented by the custom RTC service |
Trickle ICE / PATCH | Not currently supported | — |
| ICE, DTLS, and SRTP | Supported | Handled 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.
| Status | Meaning |
|---|---|
400 Bad Request | Invalid SDP or request parameters |
401 Unauthorized | Missing Bearer token |
403 Forbidden | Token verification failed |
405 Method Not Allowed | Method is not supported by the endpoint |
415 Unsupported Media Type | Content-Type is not application/sdp |
500 Internal Server Error | Internal service failure |
Initialize and start tirtc-service-sdk
The initialization values come from service registration and the deployment environment:
| Value | Meaning |
|---|---|
serviceName | The confirmed registered service_name |
customRtcKeyId | The registered Custom RTC Key ID used to select the verification key |
customRtcKey | The registered Ed25519 public key paired with the authorization service's signing key |
candidateIP | Optional 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:
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:
mux.HandleFunc("POST /whip", auth(postWHIP))
mux.HandleFunc("DELETE /whip/resource/{session_id}", deleteResource)After authentication, read the device identity from the request context:
claims, ok := tirtcxauth.ClaimsFromContext(r.Context())Accept a WHIP session
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
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:
export TIRTC_CUSTOM_RTC_KEY_ID='<Custom RTC Key ID>'
export TIRTC_CUSTOM_RTC_KEY='<registered Ed25519 Custom RTC Key>'Start the service:
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.10Set -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:
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:
./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 10The 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:
sudo sysctl -w net.ipv4.ip_local_port_range="12768 63999"For a persistent setting, add the following under /etc/sysctl.d/:
net.ipv4.ip_local_port_range = 12768 63999Then 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
| Symptom | Check |
|---|---|
| HTTP 401/403 | Access Key, public key, token expiry, service name, and complete query |
| HTTP 413 | SDP Offer limits in the ingress and application |
| HTTP 415 | Whether the proxy preserves Content-Type: application/sdp |
| SDP returned but connection fails | UDP ports, firewall, NAT, and Candidate address |
| No Echo media | _tg_mode=echo and continuous consumption of send/receive events |
| Events disappear over time | Event 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.