Skip to content

接入自定义 RTC 服务

自定义 RTC 服务需自行部署和运维,通过 TiRTC 为设备提供实时音视频或媒体业务。

只有需要自行部署和运维 RTC 服务时,才需要阅读本页。客户端直接连接设备端时,参考连接设备。完整的接入步骤见概览

核心概念

概念含义
自定义 RTC 服务自行部署和运维,通过 TiRTC 为设备提供实时音视频、命令或媒体业务的服务
WHIPWebRTC-HTTP Ingestion Protocol(WebRTC-HTTP 摄取协议)的缩写,是 IETF RFC 9725 定义的标准协议。设备端通过探鸽平台使用 WHIP 协议连接自定义 RTC 服务
RegionTiRTC 服务区域。探鸽平台按设备所在 Region 选择已注册的自定义 RTC 服务地址
peer_id格式为 whips://{service_name}?{query} 的服务标识和连接参数
连接 Token绑定设备 ID、服务名和完整 Query 的签名凭证,用于认证创建会话的请求

架构

自定义 RTC 服务建连时序

下图展示从业务授权、设备发起连接,到自定义 RTC 服务接受 WHIP 建连请求的完整时序。业务授权服务和自定义 RTC 服务均需自行部署和运维。

自定义 RTC 服务业务授权与设备建连时序

服务端 SDK 架构

本指南使用服务端 SDK构建自定义 RTC 服务。下图展示自定义 RTC 服务与服务端 SDK 各 Go 包的关系。

服务端 SDK 分层

服务端 SDK 的 Go Module 为 github.com/tangeai/tirtc-service-sdk/v2。本指南使用其 pkg/ 下的三个 Go 包:

Go 包职责使用方式
tirtcx初始化 TiRTC 引擎,接受 WHIP 会话,处理连接、媒体与命令自定义 RTC 服务的核心必选包
tirtcxauth签发和验证连接 Token,提供 HTTP Bearer 认证业务授权服务可用于签发 Token;自定义 RTC 服务用于验签
whipecho在现有 HTTP Handler 中嵌入 Echo 诊断能力可选,仅在联调和链路诊断时使用

开通与准备

部署前,请向探鸽智能提交以下信息以注册服务:

  • 全局唯一的 service_name
  • 至少一对 Custom RTC Key ID 和 Custom RTC Key。Custom RTC Key 是 Ed25519 公钥;配对私钥需自行生成,并且只保存在业务授权服务中。
  • 每个目标 Region 的公网 WHIP 地址。按设备能力使用 HTTP 或 HTTPS。
Region区域
cn01中国大陆
na01美洲
ea01亚洲
we01欧洲

设备联调还需要测试设备的 device_id(设备标识)和 device_secret_key(设备密钥)。尚未取得服务注册信息或这两项设备身份信息时,请联系探鸽智能技术支持。

下载服务端 SDK获取 tirtc-service-sdk/v2,并按该页面标注的版本准备 TiRTC C 库。

实现业务授权服务

业务授权服务用于校验设备的业务访问权限,生成 peer_id,并向设备签发连接 Token。设备将这两个参数传给 TiRtcWhipConnect,向目标服务发起连接。

签发设备连接参数

业务授权服务校验设备的访问权限后,为设备生成以下格式的 peer_id,并签发对应的连接 Token:

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

Query 必须使用标准 URL 编码,总长度不得超过 2048 字节。不要在 Query 中放置密码、私钥或长期凭证。

连接 Token 使用 Ed25519 签名,签名算法如下:

text
# CanonicalizeQuery 按 application/x-www-form-urlencoded 解析参数,将参数名按字节序升序排列,
# 保留同名参数值的顺序,并使用标准 URL Query 规则重新编码。
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)

可以使用 SDK 的 tirtcxauth 包完成上述 Token 签发。完整算法见 tirtcxauth 鉴权说明

Token 在 exp 到期前可以重复使用,tirtcxauth 本身不执行防重放检查。设备 SDK 会自动保护 TiRtcWhipConnect 调用整体,避免该调用被其他设备重放。如需阻止对自定义 RTC 服务请求的直接重放,建议在 peer_id Query 中加入一次性业务参数,并由自定义 RTC 服务在首次使用后核销。

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
}

业务授权服务只向已授权设备返回匹配的 peer_id 和 Token。设备将二者传给 TiRtcWhipConnect(peer_id, token, ...);设备不应自行拼接 peer_id。设备 SDK 的初始化与回调处理见连接设备

实现自定义 RTC 服务

自定义 RTC 服务需要支持 WHIP 协议,用于接收探鸽平台转发的 SDP Offer、创建会话资源并返回 SDP Answer。实现服务前,请先了解 TiRTC 的 WHIP 协议约束。

WHIP 协议约束

TiRTC 基于 IETF RFC 9725,当前使用非 Trickle ICE:探鸽平台根据 Region,将设备的完整 SDP Offer 和 Bearer Token 通过一次 HTTP POST 转发到自定义 RTC 服务;服务返回 SDP Answer 后,探鸽平台完成设备建连,媒体随后通过 TiRTC 传输。

能力当前状态实现说明
SDP Offer/Answer over HTTP POST支持自定义 RTC 服务接收 Offer 并返回 Answer
Location 会话资源支持自定义 RTC 服务返回并维护会话资源
DELETE 结束会话支持自定义 RTC 服务实现
Trickle ICE / PATCH暂不支持
ICE、DTLS、SRTP支持tirtc-service-sdk 处理

成功创建资源时返回 201 CreatedContent-Type: application/sdp、SDP Answer 和唯一的相对 LocationLocation 中的会话 ID 应使用密码学安全随机数生成,并至少包含 128 bit 随机熵。DELETE <Location> 不要求 Authorization;无论资源刚被删除还是已经不存在,都返回 204 No Content

状态码含义
400 Bad RequestSDP 或请求参数无效
401 Unauthorized缺少 Bearer Token
403 ForbiddenToken 验证失败
405 Method Not Allowed端点不支持该方法
415 Unsupported Media TypeContent-Type 不是 application/sdp
500 Internal Server Error服务内部错误

初始化及启动 tirtc-service-sdk

初始化参数来自服务注册信息和部署环境:

参数含义
serviceName服务注册确认的 service_name
customRtcKeyId服务注册确认的 Custom RTC Key ID,用于选择验签公钥
customRtcKey与业务授权服务签发私钥配对、并已注册的 Ed25519 公钥
candidateIP可选。服务的公网 IP。服务在 NAT 后时需要配置

进程级 TiRTC 引擎只初始化和启动一次。tirtcx.Init 必须在调用 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 是一个 http 拦截器,用于验证 Bearer Token,并将 Claims 写入请求 Context。
auth := tirtcxauth.BearerInterceptor(serviceName, verifier)
// acceptor 用于接受 SDP Offer 并创建 WHIP Session。
acceptor := tirtcx.NewWhipAcceptor(candidateIP)

// 其他初始化代码
...

// 启动
if err := tirtcx.Start(context.Background()); err != nil {
    return err
}
defer tirtcx.Stop()

认证创建请求

只在创建会话的 POST Handler 外应用 BearerInterceptor

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

认证成功后,可从 Context 读取设备身份:

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

接受 WHIP 会话

go
func (s *mediaService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // 只接受 SDP Offer。
    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
    }

    // 限制 SDP Offer 大小为 1 MiB。
    offer, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    // 使用 16 字节密码学安全随机数生成不可预测的会话 ID。
    sessionID, err := newSessionID()
    if err != nil {
        http.Error(w, "create session ID: "+err.Error(), http.StatusInternalServerError)
        return
    }

    // 在 30 秒内接受 SDP Offer 并生成 SDP Answer。
    acceptCtx, cancelAccept := context.WithTimeout(r.Context(), 30*time.Second)

    // acceptor 在初始化时创建
    whipSession, err := acceptor.WhipAccept(
        acceptCtx,
        offer,
        tirtcx.ConnEventOptions{},
    )
    cancelAccept()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // 保存会话,并在独立 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)

    // 返回 201、SDP Answer 和用于删除会话的 Location。
    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
}

管理连接生命周期

在独立 Goroutine 中等待连接:

go
connectCtx, cancelConnect := context.WithTimeout(sessionCtx, 30*time.Second)
defer cancelConnect()
defer session.whipSession.Close()

conn, err := session.whipSession.WaitConn(connectCtx)
if err != nil {
    return
}
select {
case <-conn.Done():
    if err := conn.Err(); err != nil {
        // 异常终止
    }
case <-sessionCtx.Done():
}

WaitConn 可并发、重复调用。单个等待者超时不会关闭会话,因此应在等待前注册 WhipSession.Close(),确保建连超时、失败和正常断开都会释放会话。该方法可重复调用。

应用负责普通会话的注册表和 DELETEDELETE 不做 Token 鉴权,随机会话 ID 即访问凭据。收到 DELETE <Location> 时,应先从注册表移除资源,再取消业务 Context 并关闭 WhipSession;未知或已删除的 ID 也返回 204 No Content。入口按设备能力支持 HTTP 或 HTTPS,并应限制请求速率、避免记录完整资源路径。

处理事件

只为需要消费的事件配置缓冲区。启用后必须持续读取,否则缓冲区满时事件会被丢弃。

订阅决策不再通过事件通道异步返回,而是由同步 Handler 决定:

go
options := tirtcx.ConnEventOptions{
    AudioBuffer: 16,
    VideoBuffer: 16,
    SubscribeVideoHandler: func(streamID uint8) int {
        return 0
    },
}

订阅 Handler 运行在原生回调中,必须快速返回且不能阻塞。使用 Conn.EventStats() 监控事件丢弃。

可选:嵌入 Echo 诊断能力

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 自动根据 _tg_mode=echo 参数拦截并管理 Echo 诊断会话。_tg_* 参数为探鸽保留参数,业务不应使用。 普通请求不会被 whipecho 拦截。

运行 Echo 诊断示例

tangeai/tirtc-service-sdk 仓库中的 examples/quick-start 是可直接运行的自定义 RTC 服务示例。它实现 Token 验证、WHIP 会话创建与删除、业务媒体测试、Echo 测试和退出清理。

设置服务注册时使用的验证参数:

bash
export TIRTC_CUSTOM_RTC_KEY_ID='<Custom RTC Key ID>'
export TIRTC_CUSTOM_RTC_KEY='<已注册的 Ed25519 Custom RTC Key>'

启动服务:

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

只有服务需要公布不同于本机网卡的公网地址时,才设置 -candidate。入口需要把 POST /whip、普通会话的 DELETE /whip/resource/{session_id} 和 Echo 会话的 DELETE /whip/echo/resource/{session_id} 转发到应用。

使用同一仓库中的 examples/whip-token-signer 为测试设备生成 Echo 连接参数:

bash
export TIRTC_PRIVATE_KEY='<与已注册公钥配对的 Ed25519 私钥>'

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'

将输出的 peer_idtoken 用于设备联调。签发工具只用于开发和联调;生产环境必须由业务授权服务先执行授权校验。

使用 tirn_probe_device 验证

tangeai/tirn-probe-device 获取工具源码,使用匹配的 TiRTC C SDK 构建后运行:

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

media 子命令发送内置测试音频和测试帧,同时接收 Echo 返回的媒体。发送和接收的音视频四项计数均大于零时,工具输出“媒体联调通过”。tirn_probe_device 是联调工具,不是量产设备 SDK 或自定义 RTC 服务的组成部分。

部署要求

部署自定义 RTC 服务的 Linux 主机必须将本地端口范围设置为 12768 63999

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

需要持久生效时,在 /etc/sysctl.d/ 下增加配置:

text
net.ipv4.ip_local_port_range = 12768 63999

然后执行 sudo sysctl --system。主机防火墙、云安全组及上游网络设备必须开放 12768-63999 范围内的全部 UDP 端口。

优雅退出

退出顺序:

  1. 停止接受新的 HTTP 请求。
  2. 关闭普通业务会话并等待 Goroutine。
  3. 如启用了 Echo,调用 echo.Shutdown(ctx)
  4. 调用 tirtcx.Stop()tirtcx.Uninit()

上线时监控建连成功率、建连耗时、活跃连接、异常断开、发送失败和事件丢弃。Echo 可通过 Stats() 和 Observer 接入现有监控系统。

安全要求

  • Ed25519 私钥只保存在业务授权服务的密钥管理系统中,不得下发到设备、自定义 RTC 服务或前端。
  • 自定义 RTC 服务只需要验签公钥。完整 Token、私钥和随机会话路径不得写入日志、指标标签或错误响应。
  • 使用 HTTP 时,Token 和随机会话 ID 不受传输加密保护,只能在设备确有需要且网络路径受控时启用。
  • 对 Query 的长度、字符集和允许值进行校验;不要把 Query 当作可信输入。

常见问题

现象检查项
HTTP 401/403Access Key、公钥、Token 有效期、服务名和完整 Query
HTTP 413SDP Offer 是否超过入口或应用的大小限制
HTTP 415代理是否保留 Content-Type: application/sdp
已返回 SDP 但连接失败UDP 端口、防火墙、NAT 和 Candidate 地址
收不到 Echo 媒体是否使用 _tg_mode=echo,发送和接收事件是否持续消费
运行一段时间后事件缺失事件缓冲区、Conn.EventStats() 和业务 Handler 是否阻塞

提交问题时请提供发生时间、Region、服务版本、TiRTC SDK 版本、脱敏后的 HTTP 状态与响应、服务日志以及 EventStats()。不要提供完整 Token、私钥或设备密钥。

TiRTC 开发文档