Skip to content

Connection

Connection spans three roles: the device starts and waits for inbound access, your backend issues the connect token, and the client uses that token to connect the target device. This page covers that end-to-end path.

Prepare These Inputs

ItemPurpose
device_idIdentifies the target device
device_secret_keyDevice secret used by device startup and backend-side token signing
access_idApplication credential identifier
secret_keyApplication secret used by your backend
remote_idThe target identifier used by the client connect call
subThe subject identity for this request, such as your user_id

In the common case, the client-side remote_id is the target device device_id.

Overall Flow

  1. The device starts the SDK with device_id and device_secret_key.
  2. Your backend checks whether the current subject is allowed to access the target device, then issues the connect token with the public custom signing algorithm.
  3. The client uses the returned token to connect the target remote_id.

Step 1: Start the Device and Wait for Connections

The device-side C SDK call order is: TiRtcSetOption(...) -> TiRtcInit() -> TiRtcStart().

  • If you use a custom service entry, set TIRTC_OPT_SERVICE_ENDPOINT first.
  • TiRtcStart() can take "<device_id>,<device_secret_key>" directly.
  • If you already set device_secret_key through TIRTC_OPT_SECRET_KEY, TiRtcStart() can also take only device_id.
c
#include <stdio.h>

#include "tiRTC.h"

static void on_event(int event, const void *data, int len) {
  (void)data;
  (void)len;

  if (event == TiEVENT_SYS_STARTED) {
    puts("device started");
  }
}

static TIRTCCALLBACKS cbs = {
  .on_event = on_event,
};

int start_device(void) {
  int code = TiRtcInit();
  if (code != 0) {
    return code;
  }

  return TiRtcStart("your-device-id,your-device-secret-key", &cbs);
}

A 0 return from TiRtcStart() only means the initial argument check passed. Startup is complete when TiEVENT_SYS_STARTED arrives. After that, the device is waiting for inbound connections.

Step 2: Issue the Token on Your Backend

Before issuing the token, do your own authorization check: can this subject access this target device? If yes, issue the short-lived connect token.

Required Claims

FieldMeaning
subSubject identity for this request
scopeAuthorized target scope; a common form is connect:<remote_id>
issYour application access_id
iatIssued-at timestamp
expExpiration timestamp
nonceA fresh random value for each issuance

Public Custom Signing Algorithm

The final token string sent to the client has this form:

text
token = "v1.<payload_b64>.<app_sig>"

Compute it like this:

text
payload_json = {
  "sub": <subject>,
  "scope": "connect:" + <remote_id>,
  "iss": <access_id>,
  "iat": <issued_at>,
  "exp": <expires_at>,
  "nonce": <random_nonce>
}

payload_b64 = base64url(payload_json)
device_sig = HMAC_SHA256(device_secret_key, payload_b64)
app_sig = HMAC_SHA256(secret_key, payload_b64 + "." + device_sig)
token = "v1." + payload_b64 + "." + app_sig

Keep two boundaries clear:

  • secret_key and device_secret_key stay in controlled backend or device environments. Do not send them to the client.
  • The client only consumes the final token. It does not participate in signing.

Go Example

go
package tirtc

import (
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"time"
)

type ConnectTokenClaims struct {
	Subject   string `json:"sub"`
	Scope     string `json:"scope"`
	Issuer    string `json:"iss"`
	IssuedAt  int64  `json:"iat"`
	ExpiresAt int64  `json:"exp"`
	Nonce     string `json:"nonce"`
}

func hmacB64(key, content string) string {
	h := hmac.New(sha256.New, []byte(key))
	_, _ = h.Write([]byte(content))
	return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
}

func randomNonce() (string, error) {
	buf := make([]byte, 16)
	if _, err := rand.Read(buf); err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(buf), nil
}

func GenerateConnectToken(accessID, secretKey, remoteID, deviceSecretKey, subject string, ttlSeconds int64) (string, error) {
	now := time.Now().Unix()
	nonce, err := randomNonce()
	if err != nil {
		return "", err
	}

	claims := ConnectTokenClaims{
		Subject:   subject,
		Scope:     "connect:" + remoteID,
		Issuer:    accessID,
		IssuedAt:  now,
		ExpiresAt: now + ttlSeconds,
		Nonce:     nonce,
	}

	payloadJSON, err := json.Marshal(claims)
	if err != nil {
		return "", err
	}

	payloadB64 := base64.RawURLEncoding.EncodeToString(payloadJSON)
	deviceSig := hmacB64(deviceSecretKey, payloadB64)
	appSig := hmacB64(secretKey, payloadB64+"."+deviceSig)
	return "v1." + payloadB64 + "." + appSig, nil
}

Step 3: Connect from the Client

The remote_id used here must match the target bound during token issuance. In the common case, this is the target device device_id.

kotlin
val conn = TiRtcConn()
val code = conn.connect(
  remoteId = remoteId,
  token = token,
)
if (code != 0) {
  Log.e("TiRTC", "connect request failed code=$code")
}
swift
let conn = TiRtcConn(delegate: nil)
let code = conn.connect(to: remoteId, token: token)
if code != 0 {
    print("connect request failed: \(code)")
}
dart
final TiRtcConn conn = TiRtcConn();
final int code = conn.connect(remoteId: remoteId, token: token);
if (code != 0) {
  debugPrint('connect request failed: $code');
}

Playback, command messaging, and stream messaging all build on this connection.

Alignment Checks

  • Treat TiEVENT_SYS_STARTED as the device-started signal.
  • The target bound during backend signing and the client-side remote_id must identify the same target.
  • If your current backend still uses the field name peer_id, keep it equal to the same target string used as remote_id here.
  • Treat token as an opaque string on the client side. Do not assemble, parse, or rewrite it there.

TiRTC