从 TRTC Demo 到独立音视频服务:React + TRTC Web SDK 服务化实践

1. 前言

实时音视频已经从会议、直播、在线教育等垂直场景,逐渐变成业务系统中的基础能力。一个后台系统可能需要远程协助,一个客户服务系统可能需要视频客服,一个邀请链路可能需要让双方直接进入同一个音视频房间。

传统接入方式通常是:每个业务系统自己集成 TRTC Web SDK,自己处理 SDKAppID、UserSig、进房、设备权限、摄像头采集、远端订阅、异常清理和浏览器兼容。这样做能跑通,但很快会暴露几个问题:

  • 业务代码与音视频 SDK 强耦合。
  • 多个业务系统重复实现同一套 WebRTC 生命周期。
  • SDK 升级、权限问题、浏览器策略变化需要多处维护。
  • 邀请参数、房间参数、用户身份的生成规则难以统一。

本文基于一个 React + TRTC Web SDK v5 项目实践,讨论一种更适合多业务复用的方案:把 TRTC Web SDK 能力封装成可以被外部业务系统通过 URL 和 iframe 快速调用的音视频服务。

当前项目来源于腾讯云 TRTC Web SDK v5 quick-demo-react,但已经扩展出三个独立服务入口:

服务 路由 职责
邀请参数生成服务 /#/invite-link 生成 ownerUserIdguestUserIdroomIdinviteToken 和邀请链接
音视频发布服务 /#/media-publisher 采集当前浏览器摄像头、麦克风,并进入 TRTC 房间发布本地音视频
远端观看服务 /#/media-viewer 进入同一 TRTC 房间,订阅指定 targetUserId 的远端视频

这三个服务不是 README 式的项目介绍,而是一次服务化拆分:业务系统不再直接操作 TRTC SDK,而是组合这些稳定的页面服务。

2. WebRTC 与 TRTC 基础概念

对业务系统来说,实时音视频的核心模型可以简化为:用户以某个身份进入一个房间,其中一部分用户发布音视频,另一部分用户订阅并观看音视频。

Room

Room 是音视频通信的隔离空间。当前项目使用字符串房间 ID,TRTC 进房参数名为 strRoomId,对外 URL 参数统一使用 roomId,并兼容 strRoomId

UserId

UserId 是用户进入 TRTC 房间的身份。当前服务化设计要求同一房间、同一时间内不同 iframe 使用不同的用户身份。例如发布者使用 host_1001,观看者 iframe 使用 viewer_guest_host_1001

Publisher

Publisher 是发布端。当前项目的 /#/media-publisher 会用 userId 进入房间,并调用 startLocalAudiostartLocalVideo 启动麦克风和摄像头。

Viewer

Viewer 是观看端。当前项目的 /#/media-viewer 使用 viewerUserId 进入房间,不申请本机摄像头和麦克风权限,只订阅 targetUserId 的远端视频。

Media Stream

音视频流来自浏览器摄像头和麦克风,通过 TRTC Web SDK 进入腾讯云 TRTC 房间,再由远端订阅播放。
#mermaid-svg-2ZzRHivq16pZUFRR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2ZzRHivq16pZUFRR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2ZzRHivq16pZUFRR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2ZzRHivq16pZUFRR .error-icon{fill:#552222;}#mermaid-svg-2ZzRHivq16pZUFRR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2ZzRHivq16pZUFRR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2ZzRHivq16pZUFRR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2ZzRHivq16pZUFRR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2ZzRHivq16pZUFRR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2ZzRHivq16pZUFRR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2ZzRHivq16pZUFRR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2ZzRHivq16pZUFRR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2ZzRHivq16pZUFRR .marker.cross{stroke:#333333;}#mermaid-svg-2ZzRHivq16pZUFRR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2ZzRHivq16pZUFRR p{margin:0;}#mermaid-svg-2ZzRHivq16pZUFRR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2ZzRHivq16pZUFRR .cluster-label text{fill:#333;}#mermaid-svg-2ZzRHivq16pZUFRR .cluster-label span{color:#333;}#mermaid-svg-2ZzRHivq16pZUFRR .cluster-label span p{background-color:transparent;}#mermaid-svg-2ZzRHivq16pZUFRR .label text,#mermaid-svg-2ZzRHivq16pZUFRR span{fill:#333;color:#333;}#mermaid-svg-2ZzRHivq16pZUFRR .node rect,#mermaid-svg-2ZzRHivq16pZUFRR .node circle,#mermaid-svg-2ZzRHivq16pZUFRR .node ellipse,#mermaid-svg-2ZzRHivq16pZUFRR .node polygon,#mermaid-svg-2ZzRHivq16pZUFRR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2ZzRHivq16pZUFRR .rough-node .label text,#mermaid-svg-2ZzRHivq16pZUFRR .node .label text,#mermaid-svg-2ZzRHivq16pZUFRR .image-shape .label,#mermaid-svg-2ZzRHivq16pZUFRR .icon-shape .label{text-anchor:middle;}#mermaid-svg-2ZzRHivq16pZUFRR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2ZzRHivq16pZUFRR .rough-node .label,#mermaid-svg-2ZzRHivq16pZUFRR .node .label,#mermaid-svg-2ZzRHivq16pZUFRR .image-shape .label,#mermaid-svg-2ZzRHivq16pZUFRR .icon-shape .label{text-align:center;}#mermaid-svg-2ZzRHivq16pZUFRR .node.clickable{cursor:pointer;}#mermaid-svg-2ZzRHivq16pZUFRR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2ZzRHivq16pZUFRR .arrowheadPath{fill:#333333;}#mermaid-svg-2ZzRHivq16pZUFRR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2ZzRHivq16pZUFRR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2ZzRHivq16pZUFRR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2ZzRHivq16pZUFRR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2ZzRHivq16pZUFRR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2ZzRHivq16pZUFRR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2ZzRHivq16pZUFRR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2ZzRHivq16pZUFRR .cluster text{fill:#333;}#mermaid-svg-2ZzRHivq16pZUFRR .cluster span{color:#333;}#mermaid-svg-2ZzRHivq16pZUFRR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2ZzRHivq16pZUFRR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2ZzRHivq16pZUFRR rect.text{fill:none;stroke-width:0;}#mermaid-svg-2ZzRHivq16pZUFRR .icon-shape,#mermaid-svg-2ZzRHivq16pZUFRR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2ZzRHivq16pZUFRR .icon-shape p,#mermaid-svg-2ZzRHivq16pZUFRR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2ZzRHivq16pZUFRR .icon-shape .label rect,#mermaid-svg-2ZzRHivq16pZUFRR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2ZzRHivq16pZUFRR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2ZzRHivq16pZUFRR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2ZzRHivq16pZUFRR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 摄像头
发布端 media-publisher
麦克风
TRTC 房间 strRoomId
观看端 media-viewer
远端画面

3. 整体架构设计

当前项目的服务化架构分成两层:

  • React 应用内部负责 TRTC SDK 生命周期。
  • 外部业务系统通过 iframe 和 postMessage 调用能力、接收状态。

#mermaid-svg-AseMuJc7bjs8jYbR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AseMuJc7bjs8jYbR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AseMuJc7bjs8jYbR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AseMuJc7bjs8jYbR .error-icon{fill:#552222;}#mermaid-svg-AseMuJc7bjs8jYbR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AseMuJc7bjs8jYbR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AseMuJc7bjs8jYbR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AseMuJc7bjs8jYbR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AseMuJc7bjs8jYbR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AseMuJc7bjs8jYbR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AseMuJc7bjs8jYbR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AseMuJc7bjs8jYbR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AseMuJc7bjs8jYbR .marker.cross{stroke:#333333;}#mermaid-svg-AseMuJc7bjs8jYbR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AseMuJc7bjs8jYbR p{margin:0;}#mermaid-svg-AseMuJc7bjs8jYbR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AseMuJc7bjs8jYbR .cluster-label text{fill:#333;}#mermaid-svg-AseMuJc7bjs8jYbR .cluster-label span{color:#333;}#mermaid-svg-AseMuJc7bjs8jYbR .cluster-label span p{background-color:transparent;}#mermaid-svg-AseMuJc7bjs8jYbR .label text,#mermaid-svg-AseMuJc7bjs8jYbR span{fill:#333;color:#333;}#mermaid-svg-AseMuJc7bjs8jYbR .node rect,#mermaid-svg-AseMuJc7bjs8jYbR .node circle,#mermaid-svg-AseMuJc7bjs8jYbR .node ellipse,#mermaid-svg-AseMuJc7bjs8jYbR .node polygon,#mermaid-svg-AseMuJc7bjs8jYbR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AseMuJc7bjs8jYbR .rough-node .label text,#mermaid-svg-AseMuJc7bjs8jYbR .node .label text,#mermaid-svg-AseMuJc7bjs8jYbR .image-shape .label,#mermaid-svg-AseMuJc7bjs8jYbR .icon-shape .label{text-anchor:middle;}#mermaid-svg-AseMuJc7bjs8jYbR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AseMuJc7bjs8jYbR .rough-node .label,#mermaid-svg-AseMuJc7bjs8jYbR .node .label,#mermaid-svg-AseMuJc7bjs8jYbR .image-shape .label,#mermaid-svg-AseMuJc7bjs8jYbR .icon-shape .label{text-align:center;}#mermaid-svg-AseMuJc7bjs8jYbR .node.clickable{cursor:pointer;}#mermaid-svg-AseMuJc7bjs8jYbR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AseMuJc7bjs8jYbR .arrowheadPath{fill:#333333;}#mermaid-svg-AseMuJc7bjs8jYbR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AseMuJc7bjs8jYbR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AseMuJc7bjs8jYbR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AseMuJc7bjs8jYbR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AseMuJc7bjs8jYbR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AseMuJc7bjs8jYbR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AseMuJc7bjs8jYbR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AseMuJc7bjs8jYbR .cluster text{fill:#333;}#mermaid-svg-AseMuJc7bjs8jYbR .cluster span{color:#333;}#mermaid-svg-AseMuJc7bjs8jYbR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AseMuJc7bjs8jYbR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AseMuJc7bjs8jYbR rect.text{fill:none;stroke-width:0;}#mermaid-svg-AseMuJc7bjs8jYbR .icon-shape,#mermaid-svg-AseMuJc7bjs8jYbR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AseMuJc7bjs8jYbR .icon-shape p,#mermaid-svg-AseMuJc7bjs8jYbR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AseMuJc7bjs8jYbR .icon-shape .label rect,#mermaid-svg-AseMuJc7bjs8jYbR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AseMuJc7bjs8jYbR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AseMuJc7bjs8jYbR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AseMuJc7bjs8jYbR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 业务系统
iframe
Hash 路由服务入口
React 页面
useMediaPublisher / useMediaViewer
trtc-sdk-v5
腾讯云 TRTC
postMessage 状态通知

应用入口关系如下:
#mermaid-svg-m5bHu9IQMLUubs2t{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-m5bHu9IQMLUubs2t .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-m5bHu9IQMLUubs2t .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-m5bHu9IQMLUubs2t .error-icon{fill:#552222;}#mermaid-svg-m5bHu9IQMLUubs2t .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-m5bHu9IQMLUubs2t .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-m5bHu9IQMLUubs2t .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-m5bHu9IQMLUubs2t .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-m5bHu9IQMLUubs2t .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-m5bHu9IQMLUubs2t .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-m5bHu9IQMLUubs2t .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-m5bHu9IQMLUubs2t .marker{fill:#333333;stroke:#333333;}#mermaid-svg-m5bHu9IQMLUubs2t .marker.cross{stroke:#333333;}#mermaid-svg-m5bHu9IQMLUubs2t svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-m5bHu9IQMLUubs2t p{margin:0;}#mermaid-svg-m5bHu9IQMLUubs2t .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-m5bHu9IQMLUubs2t .cluster-label text{fill:#333;}#mermaid-svg-m5bHu9IQMLUubs2t .cluster-label span{color:#333;}#mermaid-svg-m5bHu9IQMLUubs2t .cluster-label span p{background-color:transparent;}#mermaid-svg-m5bHu9IQMLUubs2t .label text,#mermaid-svg-m5bHu9IQMLUubs2t span{fill:#333;color:#333;}#mermaid-svg-m5bHu9IQMLUubs2t .node rect,#mermaid-svg-m5bHu9IQMLUubs2t .node circle,#mermaid-svg-m5bHu9IQMLUubs2t .node ellipse,#mermaid-svg-m5bHu9IQMLUubs2t .node polygon,#mermaid-svg-m5bHu9IQMLUubs2t .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-m5bHu9IQMLUubs2t .rough-node .label text,#mermaid-svg-m5bHu9IQMLUubs2t .node .label text,#mermaid-svg-m5bHu9IQMLUubs2t .image-shape .label,#mermaid-svg-m5bHu9IQMLUubs2t .icon-shape .label{text-anchor:middle;}#mermaid-svg-m5bHu9IQMLUubs2t .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-m5bHu9IQMLUubs2t .rough-node .label,#mermaid-svg-m5bHu9IQMLUubs2t .node .label,#mermaid-svg-m5bHu9IQMLUubs2t .image-shape .label,#mermaid-svg-m5bHu9IQMLUubs2t .icon-shape .label{text-align:center;}#mermaid-svg-m5bHu9IQMLUubs2t .node.clickable{cursor:pointer;}#mermaid-svg-m5bHu9IQMLUubs2t .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-m5bHu9IQMLUubs2t .arrowheadPath{fill:#333333;}#mermaid-svg-m5bHu9IQMLUubs2t .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-m5bHu9IQMLUubs2t .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-m5bHu9IQMLUubs2t .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-m5bHu9IQMLUubs2t .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-m5bHu9IQMLUubs2t .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-m5bHu9IQMLUubs2t .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-m5bHu9IQMLUubs2t .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-m5bHu9IQMLUubs2t .cluster text{fill:#333;}#mermaid-svg-m5bHu9IQMLUubs2t .cluster span{color:#333;}#mermaid-svg-m5bHu9IQMLUubs2t div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-m5bHu9IQMLUubs2t .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-m5bHu9IQMLUubs2t rect.text{fill:none;stroke-width:0;}#mermaid-svg-m5bHu9IQMLUubs2t .icon-shape,#mermaid-svg-m5bHu9IQMLUubs2t .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-m5bHu9IQMLUubs2t .icon-shape p,#mermaid-svg-m5bHu9IQMLUubs2t .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-m5bHu9IQMLUubs2t .icon-shape .label rect,#mermaid-svg-m5bHu9IQMLUubs2t .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-m5bHu9IQMLUubs2t .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-m5bHu9IQMLUubs2t .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-m5bHu9IQMLUubs2t :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} index.html
src/main.tsx
initAegis()
src/App.tsx
HashRouter
/#/invite-link
/#/media-publisher
/#/media-viewer
/#/media-module-demo
useMediaPublisher
useMediaViewer
TRTC.create()

src/App.tsx 中通过 SILENT_ROUTES 隐藏导航栏,/invite-link/media-publisher/media-viewer 都属于静默服务路由。这一点非常关键:服务入口不是给用户操作 Demo 控制台,而是给业务系统嵌入使用。

4. 为什么选择 iframe 服务化模式

把 TRTC 能力封装为 iframe 页面,本质上是在业务系统和 SDK 之间增加一层可复用的音视频服务边界。

它的优势是:

  • 解耦业务系统和 TRTC SDK。业务系统只拼 URL,不直接引入 trtc-sdk-v5
  • 降低接入成本。外部系统不需要理解完整 WebRTC 生命周期。
  • 统一维护。设备采集、进房、订阅、销毁、SDK 升级集中在一个模块里。
  • 多业务复用。不同业务只传不同的 userIdroomIdmode
  • 页面隔离。iframe 内部状态和业务系统的 React/Vue/原生页面互不污染。

它也带来限制:

  • iframe 使用摄像头和麦克风必须配置 allow
  • 父子页面通信依赖 postMessage,生产环境必须校验 originsource
  • 跨域部署时需要统一 HTTPS、权限策略和自动播放策略。
  • 每个 iframe 都是独立 TRTC 用户,不能复用同一个 userId

在当前项目里,iframe 不是简单展示页面,而是对外服务 API 的载体。

5. 三个 TRTC 服务设计

5.1 邀请参数生成服务

服务作用

/#/invite-link 不进入 TRTC 房间,只负责生成邀请链路所需的参数和网址。它解决的是"业务系统如何统一生成房间、房主、访客和 token 参数"的问题。

URL

text 复制代码
https://trtc.example.com/#/invite-link?ownerUserId=host_1001&roomId=room_001&mode=business_a

参数

参数 必填 说明
ownerUserId 房主用户 ID,兼容读取 userId
roomId TRTC 字符串房间 ID,兼容读取 strRoomId
mode 业务透传参数,默认 rtc,当前实现会转为小写

内部流程

#mermaid-svg-slaVYMOiR2NoyAiE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-slaVYMOiR2NoyAiE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-slaVYMOiR2NoyAiE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-slaVYMOiR2NoyAiE .error-icon{fill:#552222;}#mermaid-svg-slaVYMOiR2NoyAiE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-slaVYMOiR2NoyAiE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-slaVYMOiR2NoyAiE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-slaVYMOiR2NoyAiE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-slaVYMOiR2NoyAiE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-slaVYMOiR2NoyAiE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-slaVYMOiR2NoyAiE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-slaVYMOiR2NoyAiE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-slaVYMOiR2NoyAiE .marker.cross{stroke:#333333;}#mermaid-svg-slaVYMOiR2NoyAiE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-slaVYMOiR2NoyAiE p{margin:0;}#mermaid-svg-slaVYMOiR2NoyAiE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-slaVYMOiR2NoyAiE .cluster-label text{fill:#333;}#mermaid-svg-slaVYMOiR2NoyAiE .cluster-label span{color:#333;}#mermaid-svg-slaVYMOiR2NoyAiE .cluster-label span p{background-color:transparent;}#mermaid-svg-slaVYMOiR2NoyAiE .label text,#mermaid-svg-slaVYMOiR2NoyAiE span{fill:#333;color:#333;}#mermaid-svg-slaVYMOiR2NoyAiE .node rect,#mermaid-svg-slaVYMOiR2NoyAiE .node circle,#mermaid-svg-slaVYMOiR2NoyAiE .node ellipse,#mermaid-svg-slaVYMOiR2NoyAiE .node polygon,#mermaid-svg-slaVYMOiR2NoyAiE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-slaVYMOiR2NoyAiE .rough-node .label text,#mermaid-svg-slaVYMOiR2NoyAiE .node .label text,#mermaid-svg-slaVYMOiR2NoyAiE .image-shape .label,#mermaid-svg-slaVYMOiR2NoyAiE .icon-shape .label{text-anchor:middle;}#mermaid-svg-slaVYMOiR2NoyAiE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-slaVYMOiR2NoyAiE .rough-node .label,#mermaid-svg-slaVYMOiR2NoyAiE .node .label,#mermaid-svg-slaVYMOiR2NoyAiE .image-shape .label,#mermaid-svg-slaVYMOiR2NoyAiE .icon-shape .label{text-align:center;}#mermaid-svg-slaVYMOiR2NoyAiE .node.clickable{cursor:pointer;}#mermaid-svg-slaVYMOiR2NoyAiE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-slaVYMOiR2NoyAiE .arrowheadPath{fill:#333333;}#mermaid-svg-slaVYMOiR2NoyAiE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-slaVYMOiR2NoyAiE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-slaVYMOiR2NoyAiE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-slaVYMOiR2NoyAiE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-slaVYMOiR2NoyAiE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-slaVYMOiR2NoyAiE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-slaVYMOiR2NoyAiE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-slaVYMOiR2NoyAiE .cluster text{fill:#333;}#mermaid-svg-slaVYMOiR2NoyAiE .cluster span{color:#333;}#mermaid-svg-slaVYMOiR2NoyAiE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-slaVYMOiR2NoyAiE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-slaVYMOiR2NoyAiE rect.text{fill:none;stroke-width:0;}#mermaid-svg-slaVYMOiR2NoyAiE .icon-shape,#mermaid-svg-slaVYMOiR2NoyAiE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-slaVYMOiR2NoyAiE .icon-shape p,#mermaid-svg-slaVYMOiR2NoyAiE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-slaVYMOiR2NoyAiE .icon-shape .label rect,#mermaid-svg-slaVYMOiR2NoyAiE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-slaVYMOiR2NoyAiE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-slaVYMOiR2NoyAiE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-slaVYMOiR2NoyAiE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 打开 /#/invite-link
getInviteLinkParams() 解析 URL
getMissingInviteLinkParams() 校验 ownerUserId / roomId
createInviteLinkPayload()
生成 guestUserId
生成 hostViewerUserId / guestViewerUserId
生成 inviteToken
createHangwuInviteLink('/hangwu-family-invite')
页面展示并支持复制
iframe 内 postMessage: TRTC_INVITE_LINK_READY

接入示例

html 复制代码
<iframe
  src="https://trtc.example.com/#/invite-link?ownerUserId=host_1001&roomId=room_001&mode=business_a"
  allow="clipboard-read; clipboard-write"
  style="width: 100%; height: 260px; border: 0;"
></iframe>

注意事项

当前实现中 inviteToken 由前端使用 window.btoa(encodeURIComponent(JSON.stringify(tokenPayload))) 生成,有效期一小时。它适合联调和前端闭环验证;生产环境应改为后端生成并签名。

5.2 音视频发布服务

服务作用

/#/media-publisher 负责把当前浏览器设备上的摄像头和麦克风发布到 TRTC 房间。它不会订阅远端,也不会展示复杂操作 UI。

URL

text 复制代码
https://trtc.example.com/#/media-publisher?userId=host_1001&roomId=room_001&mode=business_a

参数

参数 必填 说明
userId 当前发布者进入 TRTC 房间使用的用户 ID
roomId TRTC 字符串房间 ID,兼容 strRoomId
mode 业务透传参数,默认 rtc
cameraId 指定摄像头设备 ID
microphoneId 指定麦克风设备 ID
videoProfile 视频规格,默认 1080p

内部流程

#mermaid-svg-tvrJ5uJpBrniGjSP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tvrJ5uJpBrniGjSP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tvrJ5uJpBrniGjSP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tvrJ5uJpBrniGjSP .error-icon{fill:#552222;}#mermaid-svg-tvrJ5uJpBrniGjSP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tvrJ5uJpBrniGjSP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tvrJ5uJpBrniGjSP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tvrJ5uJpBrniGjSP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tvrJ5uJpBrniGjSP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tvrJ5uJpBrniGjSP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tvrJ5uJpBrniGjSP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tvrJ5uJpBrniGjSP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tvrJ5uJpBrniGjSP .marker.cross{stroke:#333333;}#mermaid-svg-tvrJ5uJpBrniGjSP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tvrJ5uJpBrniGjSP p{margin:0;}#mermaid-svg-tvrJ5uJpBrniGjSP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tvrJ5uJpBrniGjSP .cluster-label text{fill:#333;}#mermaid-svg-tvrJ5uJpBrniGjSP .cluster-label span{color:#333;}#mermaid-svg-tvrJ5uJpBrniGjSP .cluster-label span p{background-color:transparent;}#mermaid-svg-tvrJ5uJpBrniGjSP .label text,#mermaid-svg-tvrJ5uJpBrniGjSP span{fill:#333;color:#333;}#mermaid-svg-tvrJ5uJpBrniGjSP .node rect,#mermaid-svg-tvrJ5uJpBrniGjSP .node circle,#mermaid-svg-tvrJ5uJpBrniGjSP .node ellipse,#mermaid-svg-tvrJ5uJpBrniGjSP .node polygon,#mermaid-svg-tvrJ5uJpBrniGjSP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tvrJ5uJpBrniGjSP .rough-node .label text,#mermaid-svg-tvrJ5uJpBrniGjSP .node .label text,#mermaid-svg-tvrJ5uJpBrniGjSP .image-shape .label,#mermaid-svg-tvrJ5uJpBrniGjSP .icon-shape .label{text-anchor:middle;}#mermaid-svg-tvrJ5uJpBrniGjSP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tvrJ5uJpBrniGjSP .rough-node .label,#mermaid-svg-tvrJ5uJpBrniGjSP .node .label,#mermaid-svg-tvrJ5uJpBrniGjSP .image-shape .label,#mermaid-svg-tvrJ5uJpBrniGjSP .icon-shape .label{text-align:center;}#mermaid-svg-tvrJ5uJpBrniGjSP .node.clickable{cursor:pointer;}#mermaid-svg-tvrJ5uJpBrniGjSP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tvrJ5uJpBrniGjSP .arrowheadPath{fill:#333333;}#mermaid-svg-tvrJ5uJpBrniGjSP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tvrJ5uJpBrniGjSP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tvrJ5uJpBrniGjSP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tvrJ5uJpBrniGjSP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tvrJ5uJpBrniGjSP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tvrJ5uJpBrniGjSP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tvrJ5uJpBrniGjSP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tvrJ5uJpBrniGjSP .cluster text{fill:#333;}#mermaid-svg-tvrJ5uJpBrniGjSP .cluster span{color:#333;}#mermaid-svg-tvrJ5uJpBrniGjSP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tvrJ5uJpBrniGjSP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tvrJ5uJpBrniGjSP rect.text{fill:none;stroke-width:0;}#mermaid-svg-tvrJ5uJpBrniGjSP .icon-shape,#mermaid-svg-tvrJ5uJpBrniGjSP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tvrJ5uJpBrniGjSP .icon-shape p,#mermaid-svg-tvrJ5uJpBrniGjSP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tvrJ5uJpBrniGjSP .icon-shape .label rect,#mermaid-svg-tvrJ5uJpBrniGjSP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tvrJ5uJpBrniGjSP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tvrJ5uJpBrniGjSP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tvrJ5uJpBrniGjSP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 打开 /#/media-publisher
getMediaPublisherParams()
校验 SDKAppID / SecretKey / userId / roomId
TRTC.create()
绑定 ERROR / CONNECTION_STATE_CHANGED / PUBLISH_STATE_CHANGED
genTestUserSig()
enterRoom({ sdkAppId, userId, userSig, strRoomId })
startLocalAudio({ microphoneId })
startLocalVideo({ view, cameraId, profile })
状态 publishing

接入示例

html 复制代码
<iframe
  src="https://trtc.example.com/#/media-publisher?userId=host_1001&roomId=room_001&mode=business_a"
  allow="camera; microphone; autoplay; fullscreen"
  style="width: 100%; height: 420px; border: 0; background: #000;"
></iframe>

注意事项

allow="camera; microphone; autoplay; fullscreen" 必须配置。否则浏览器可能禁止 iframe 访问摄像头、麦克风或自动播放媒体。

当前 media-publisher 没有暴露设备选择 UI,设备参数只能通过 URL 传入。

5.3 远端观看服务

服务作用

/#/media-viewer 负责使用观看者身份进入房间,等待并播放指定发布者的远端视频。

URL

text 复制代码
https://trtc.example.com/#/media-viewer?viewerUserId=viewer_guest_host_1001&targetUserId=host_1001&roomId=room_001&mode=business_a

参数

参数 必填 说明
viewerUserId 观看者进入 TRTC 房间使用的用户 ID,兼容读取 userId
targetUserId 需要观看的远端发布者 ID
roomId TRTC 字符串房间 ID,兼容 strRoomId
mode 业务透传参数,默认 rtc

内部流程

#mermaid-svg-AwWckwqt3lGmKCVR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AwWckwqt3lGmKCVR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AwWckwqt3lGmKCVR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AwWckwqt3lGmKCVR .error-icon{fill:#552222;}#mermaid-svg-AwWckwqt3lGmKCVR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AwWckwqt3lGmKCVR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AwWckwqt3lGmKCVR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AwWckwqt3lGmKCVR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AwWckwqt3lGmKCVR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AwWckwqt3lGmKCVR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AwWckwqt3lGmKCVR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AwWckwqt3lGmKCVR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AwWckwqt3lGmKCVR .marker.cross{stroke:#333333;}#mermaid-svg-AwWckwqt3lGmKCVR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AwWckwqt3lGmKCVR p{margin:0;}#mermaid-svg-AwWckwqt3lGmKCVR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AwWckwqt3lGmKCVR .cluster-label text{fill:#333;}#mermaid-svg-AwWckwqt3lGmKCVR .cluster-label span{color:#333;}#mermaid-svg-AwWckwqt3lGmKCVR .cluster-label span p{background-color:transparent;}#mermaid-svg-AwWckwqt3lGmKCVR .label text,#mermaid-svg-AwWckwqt3lGmKCVR span{fill:#333;color:#333;}#mermaid-svg-AwWckwqt3lGmKCVR .node rect,#mermaid-svg-AwWckwqt3lGmKCVR .node circle,#mermaid-svg-AwWckwqt3lGmKCVR .node ellipse,#mermaid-svg-AwWckwqt3lGmKCVR .node polygon,#mermaid-svg-AwWckwqt3lGmKCVR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AwWckwqt3lGmKCVR .rough-node .label text,#mermaid-svg-AwWckwqt3lGmKCVR .node .label text,#mermaid-svg-AwWckwqt3lGmKCVR .image-shape .label,#mermaid-svg-AwWckwqt3lGmKCVR .icon-shape .label{text-anchor:middle;}#mermaid-svg-AwWckwqt3lGmKCVR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AwWckwqt3lGmKCVR .rough-node .label,#mermaid-svg-AwWckwqt3lGmKCVR .node .label,#mermaid-svg-AwWckwqt3lGmKCVR .image-shape .label,#mermaid-svg-AwWckwqt3lGmKCVR .icon-shape .label{text-align:center;}#mermaid-svg-AwWckwqt3lGmKCVR .node.clickable{cursor:pointer;}#mermaid-svg-AwWckwqt3lGmKCVR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AwWckwqt3lGmKCVR .arrowheadPath{fill:#333333;}#mermaid-svg-AwWckwqt3lGmKCVR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AwWckwqt3lGmKCVR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AwWckwqt3lGmKCVR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AwWckwqt3lGmKCVR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AwWckwqt3lGmKCVR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AwWckwqt3lGmKCVR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AwWckwqt3lGmKCVR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AwWckwqt3lGmKCVR .cluster text{fill:#333;}#mermaid-svg-AwWckwqt3lGmKCVR .cluster span{color:#333;}#mermaid-svg-AwWckwqt3lGmKCVR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AwWckwqt3lGmKCVR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AwWckwqt3lGmKCVR rect.text{fill:none;stroke-width:0;}#mermaid-svg-AwWckwqt3lGmKCVR .icon-shape,#mermaid-svg-AwWckwqt3lGmKCVR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AwWckwqt3lGmKCVR .icon-shape p,#mermaid-svg-AwWckwqt3lGmKCVR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AwWckwqt3lGmKCVR .icon-shape .label rect,#mermaid-svg-AwWckwqt3lGmKCVR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AwWckwqt3lGmKCVR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AwWckwqt3lGmKCVR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AwWckwqt3lGmKCVR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 打开 /#/media-viewer
getMediaViewerParams()
校验 viewerUserId / targetUserId / roomId
TRTC.create()
监听 REMOTE_VIDEO_AVAILABLE / REMOTE_VIDEO_UNAVAILABLE / ERROR
genTestUserSig(viewerUserId)
enterRoom()
状态 waitingTarget
收到 targetUserId 的 REMOTE_VIDEO_AVAILABLE
startRemoteVideo({ userId, streamType, view })
状态 viewing

接入示例

html 复制代码
<iframe
  src="https://trtc.example.com/#/media-viewer?viewerUserId=viewer_guest_host_1001&targetUserId=host_1001&roomId=room_001&mode=business_a"
  allow="autoplay; fullscreen"
  style="width: 100%; height: 420px; border: 0; background: #000;"
></iframe>

注意事项

viewer 不采集本机设备,因此不需要 cameramicrophone 权限。当前实现会校验 viewerUserId !== targetUserId,同一个 iframe 不能既是观看者又是被观看目标。

6. 邀请参数服务详解

邀请参数服务的价值在于统一生成一组可流转的通话身份,而不是让业务系统临时拼接用户 ID。

src/utils/silentRouteParams.tscreateInviteLinkPayload() 会生成:

  • guestUserId:被邀请者发布音视频时使用。
  • hostViewerUserId:房主观看被邀请者时使用。
  • guestViewerUserId:被邀请者观看房主时使用。
  • inviteToken:包含 ownerUserIdguestUserIdroomIdmodegeneratedAtexpiresAt
  • moduleParams:分别给房主 publisher、房主 viewer、访客 publisher、访客 viewer 使用的参数。

#mermaid-svg-ZozYDKyldz28M12i{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZozYDKyldz28M12i .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZozYDKyldz28M12i .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZozYDKyldz28M12i .error-icon{fill:#552222;}#mermaid-svg-ZozYDKyldz28M12i .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZozYDKyldz28M12i .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZozYDKyldz28M12i .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZozYDKyldz28M12i .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZozYDKyldz28M12i .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZozYDKyldz28M12i .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZozYDKyldz28M12i .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZozYDKyldz28M12i .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZozYDKyldz28M12i .marker.cross{stroke:#333333;}#mermaid-svg-ZozYDKyldz28M12i svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZozYDKyldz28M12i p{margin:0;}#mermaid-svg-ZozYDKyldz28M12i .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZozYDKyldz28M12i .cluster-label text{fill:#333;}#mermaid-svg-ZozYDKyldz28M12i .cluster-label span{color:#333;}#mermaid-svg-ZozYDKyldz28M12i .cluster-label span p{background-color:transparent;}#mermaid-svg-ZozYDKyldz28M12i .label text,#mermaid-svg-ZozYDKyldz28M12i span{fill:#333;color:#333;}#mermaid-svg-ZozYDKyldz28M12i .node rect,#mermaid-svg-ZozYDKyldz28M12i .node circle,#mermaid-svg-ZozYDKyldz28M12i .node ellipse,#mermaid-svg-ZozYDKyldz28M12i .node polygon,#mermaid-svg-ZozYDKyldz28M12i .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZozYDKyldz28M12i .rough-node .label text,#mermaid-svg-ZozYDKyldz28M12i .node .label text,#mermaid-svg-ZozYDKyldz28M12i .image-shape .label,#mermaid-svg-ZozYDKyldz28M12i .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZozYDKyldz28M12i .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZozYDKyldz28M12i .rough-node .label,#mermaid-svg-ZozYDKyldz28M12i .node .label,#mermaid-svg-ZozYDKyldz28M12i .image-shape .label,#mermaid-svg-ZozYDKyldz28M12i .icon-shape .label{text-align:center;}#mermaid-svg-ZozYDKyldz28M12i .node.clickable{cursor:pointer;}#mermaid-svg-ZozYDKyldz28M12i .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZozYDKyldz28M12i .arrowheadPath{fill:#333333;}#mermaid-svg-ZozYDKyldz28M12i .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZozYDKyldz28M12i .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZozYDKyldz28M12i .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZozYDKyldz28M12i .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZozYDKyldz28M12i .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZozYDKyldz28M12i .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZozYDKyldz28M12i .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZozYDKyldz28M12i .cluster text{fill:#333;}#mermaid-svg-ZozYDKyldz28M12i .cluster span{color:#333;}#mermaid-svg-ZozYDKyldz28M12i div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZozYDKyldz28M12i .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZozYDKyldz28M12i rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZozYDKyldz28M12i .icon-shape,#mermaid-svg-ZozYDKyldz28M12i .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZozYDKyldz28M12i .icon-shape p,#mermaid-svg-ZozYDKyldz28M12i .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZozYDKyldz28M12i .icon-shape .label rect,#mermaid-svg-ZozYDKyldz28M12i .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZozYDKyldz28M12i .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZozYDKyldz28M12i .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZozYDKyldz28M12i :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ownerUserId
邀请参数 payload
roomId
mode
guestUserId = guest_xxx
hostViewsGuest.viewerUserId
guestViewsHost.viewerUserId
inviteToken
网址 A: /#/hangwu-family-invite
网址 B: /#/hangwu-invite
/#/hangwu-video-call

token 校验逻辑在 src/utils/hangwuInvite.ts

ts 复制代码
export function getHangwuInviteErrors(params: HangwuInviteParams) {
  const errors = getMissingHangwuInviteParams(params);
  const token = decodeHangwuInviteToken(params.inviteToken);
  if (params.inviteToken && !token) errors.push('inviteToken 无效');
  if (token) {
    if (token.ownerUserId !== params.ownerUserId || token.guestUserId !== params.guestUserId || token.roomId !== params.roomId || token.mode !== params.mode) {
      errors.push('邀请参数与 inviteToken 不匹配');
    }
    if (!Number.isFinite(token.generatedAt) || !Number.isFinite(token.expiresAt) || token.expiresAt <= token.generatedAt) {
      errors.push('inviteToken 有效期无效');
    } else {
      if (token.generatedAt > Date.now() + 60 * 1000) errors.push('inviteToken 生成时间无效');
      if (token.expiresAt < Date.now()) errors.push('邀请链接已过期');
    }
  }
  if (params.ownerUserId && params.ownerUserId === params.guestUserId) errors.push('ownerUserId 不能等于 guestUserId');
  return errors;
}

这段逻辑说明邀请链路至少解决了四类问题:缺参、token 无效、参数被篡改、链接过期。

7. 音视频发布服务详解

发布服务的目标是把浏览器设备采集和 TRTC 发布链路封装成一个 URL。

发布端页面只有两个核心文件:

  • src/pages/MediaPublisherPage.tsx:解析参数,渲染本地视频容器和状态浮层。
  • src/hooks/useMediaPublisher.ts:创建 TRTC 实例,进入房间,启动音频和视频。

页面组件非常薄:

tsx 复制代码
export default function MediaPublisherPage() {
  const params = useMemo(() => getMediaPublisherParams(), []);
  const { status, error, localVideoRef } = useMediaPublisher(params);
  const pending = status !== 'publishing' && status !== 'failed' && status !== 'destroyed';

  return (
    <div className="silent-media-page">
      <div className="silent-media-video" ref={localVideoRef} />
      {pending && <div className="silent-media-overlay">连接中...</div>}
      {status === 'destroyed' && <div className="silent-media-overlay">已退出</div>}
      {error && <div className="silent-media-error">{error}</div>}
    </div>
  );
}

真正的生命周期在 hook 中:

ts 复制代码
const trtc = TRTC.create();

await trtc.enterRoom({
  sdkAppId: currentParams.sdkAppId,
  userId: currentParams.userId,
  userSig,
  strRoomId: currentParams.roomId,
});

await trtc.startLocalAudio({
  option: {
    microphoneId: currentParams.microphoneId || undefined,
  },
});

await trtc.startLocalVideo({
  view: localVideoRef.current,
  option: {
    cameraId: currentParams.cameraId || undefined,
    profile: (currentParams.videoProfile || '1080p') as any,
  },
});

这里有一个实践细节:当前 TRTC Web SDK v5 封装中,发布端没有单独暴露 publish() 代码路径,而是在进房后启动本地音频和视频。博客或接入文档中如果泛泛写"创建 LocalStream 后 publish",就和这个实现不一致。

iframe 示例:

html 复制代码
<iframe
  src="https://trtc.example.com/#/media-publisher?userId=host_1001&roomId=room_001&mode=business_a&videoProfile=1080p"
  allow="camera; microphone; autoplay; fullscreen"
  style="width: 100%; height: 420px; border: 0; background: #000;"
></iframe>

allow 的意义不是样式配置,而是浏览器权限声明。发布端需要摄像头和麦克风,所以必须包含 cameramicrophone

8. 远端观看服务详解

观看服务的关键是 targetUserId。它把"我是谁"和"我要看谁"分离开。
#mermaid-svg-AOTS3WrHo2rCCOgs{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AOTS3WrHo2rCCOgs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AOTS3WrHo2rCCOgs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AOTS3WrHo2rCCOgs .error-icon{fill:#552222;}#mermaid-svg-AOTS3WrHo2rCCOgs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AOTS3WrHo2rCCOgs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AOTS3WrHo2rCCOgs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AOTS3WrHo2rCCOgs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AOTS3WrHo2rCCOgs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AOTS3WrHo2rCCOgs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AOTS3WrHo2rCCOgs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AOTS3WrHo2rCCOgs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AOTS3WrHo2rCCOgs .marker.cross{stroke:#333333;}#mermaid-svg-AOTS3WrHo2rCCOgs svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AOTS3WrHo2rCCOgs p{margin:0;}#mermaid-svg-AOTS3WrHo2rCCOgs .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AOTS3WrHo2rCCOgs .cluster-label text{fill:#333;}#mermaid-svg-AOTS3WrHo2rCCOgs .cluster-label span{color:#333;}#mermaid-svg-AOTS3WrHo2rCCOgs .cluster-label span p{background-color:transparent;}#mermaid-svg-AOTS3WrHo2rCCOgs .label text,#mermaid-svg-AOTS3WrHo2rCCOgs span{fill:#333;color:#333;}#mermaid-svg-AOTS3WrHo2rCCOgs .node rect,#mermaid-svg-AOTS3WrHo2rCCOgs .node circle,#mermaid-svg-AOTS3WrHo2rCCOgs .node ellipse,#mermaid-svg-AOTS3WrHo2rCCOgs .node polygon,#mermaid-svg-AOTS3WrHo2rCCOgs .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AOTS3WrHo2rCCOgs .rough-node .label text,#mermaid-svg-AOTS3WrHo2rCCOgs .node .label text,#mermaid-svg-AOTS3WrHo2rCCOgs .image-shape .label,#mermaid-svg-AOTS3WrHo2rCCOgs .icon-shape .label{text-anchor:middle;}#mermaid-svg-AOTS3WrHo2rCCOgs .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AOTS3WrHo2rCCOgs .rough-node .label,#mermaid-svg-AOTS3WrHo2rCCOgs .node .label,#mermaid-svg-AOTS3WrHo2rCCOgs .image-shape .label,#mermaid-svg-AOTS3WrHo2rCCOgs .icon-shape .label{text-align:center;}#mermaid-svg-AOTS3WrHo2rCCOgs .node.clickable{cursor:pointer;}#mermaid-svg-AOTS3WrHo2rCCOgs .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AOTS3WrHo2rCCOgs .arrowheadPath{fill:#333333;}#mermaid-svg-AOTS3WrHo2rCCOgs .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AOTS3WrHo2rCCOgs .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AOTS3WrHo2rCCOgs .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AOTS3WrHo2rCCOgs .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AOTS3WrHo2rCCOgs .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AOTS3WrHo2rCCOgs .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AOTS3WrHo2rCCOgs .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AOTS3WrHo2rCCOgs .cluster text{fill:#333;}#mermaid-svg-AOTS3WrHo2rCCOgs .cluster span{color:#333;}#mermaid-svg-AOTS3WrHo2rCCOgs div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AOTS3WrHo2rCCOgs .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AOTS3WrHo2rCCOgs rect.text{fill:none;stroke-width:0;}#mermaid-svg-AOTS3WrHo2rCCOgs .icon-shape,#mermaid-svg-AOTS3WrHo2rCCOgs .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AOTS3WrHo2rCCOgs .icon-shape p,#mermaid-svg-AOTS3WrHo2rCCOgs .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AOTS3WrHo2rCCOgs .icon-shape .label rect,#mermaid-svg-AOTS3WrHo2rCCOgs .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AOTS3WrHo2rCCOgs .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AOTS3WrHo2rCCOgs .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AOTS3WrHo2rCCOgs :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} host_1001 media-publisher
room_001
viewer_guest_host_1001 media-viewer
targetUserId = host_1001

src/hooks/useMediaViewer.ts 的过滤逻辑非常明确:

ts 复制代码
trtc.on(TRTC.EVENT.REMOTE_VIDEO_AVAILABLE, ({ userId, streamType }: any) => {
  if (String(userId) !== String(paramsRef.current.targetUserId)) {
    return;
  }
  window.requestAnimationFrame(() => {
    startTargetRemoteVideo(userId, streamType);
  });
});

也就是说,即使房间里有多个远端用户,当前 viewer 也只会播放 targetUserId 对应的远端视频。

当目标用户下线或停止视频时,观看端会处理 REMOTE_VIDEO_UNAVAILABLE

ts 复制代码
trtc.on(TRTC.EVENT.REMOTE_VIDEO_UNAVAILABLE, ({ userId, streamType }: any) => {
  if (String(userId) !== String(paramsRef.current.targetUserId)) {
    return;
  }
  startedRemoteVideoRef.current = false;
  clearRemoteVideoContainer();
  trtc.stopRemoteVideo({ userId, streamType });
  notifyStatus('targetOffline');
});

这使 viewer 可以先进入房间并等待发布端上线。页面状态为 waitingTargettargetOffline 时,会显示"等待远端画面"。

9. 外部业务完整接入流程

假设第三方业务系统要做一条双方音视频通话链路,可以这样组合三个服务:
腾讯云 TRTC 房间 访客 / 访客 / 房主 / 房主 / / 业务系统 腾讯云 TRTC 房间 访客 / 访客 / 房主 / 房主 / / 业务系统 #mermaid-svg-C36V71EiM9xujbEF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-C36V71EiM9xujbEF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-C36V71EiM9xujbEF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-C36V71EiM9xujbEF .error-icon{fill:#552222;}#mermaid-svg-C36V71EiM9xujbEF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-C36V71EiM9xujbEF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-C36V71EiM9xujbEF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-C36V71EiM9xujbEF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-C36V71EiM9xujbEF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-C36V71EiM9xujbEF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-C36V71EiM9xujbEF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-C36V71EiM9xujbEF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-C36V71EiM9xujbEF .marker.cross{stroke:#333333;}#mermaid-svg-C36V71EiM9xujbEF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-C36V71EiM9xujbEF p{margin:0;}#mermaid-svg-C36V71EiM9xujbEF .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-C36V71EiM9xujbEF text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-C36V71EiM9xujbEF .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-C36V71EiM9xujbEF .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-C36V71EiM9xujbEF .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-C36V71EiM9xujbEF .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-C36V71EiM9xujbEF #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-C36V71EiM9xujbEF .sequenceNumber{fill:white;}#mermaid-svg-C36V71EiM9xujbEF #sequencenumber{fill:#333;}#mermaid-svg-C36V71EiM9xujbEF #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-C36V71EiM9xujbEF .messageText{fill:#333;stroke:none;}#mermaid-svg-C36V71EiM9xujbEF .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-C36V71EiM9xujbEF .labelText,#mermaid-svg-C36V71EiM9xujbEF .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-C36V71EiM9xujbEF .loopText,#mermaid-svg-C36V71EiM9xujbEF .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-C36V71EiM9xujbEF .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-C36V71EiM9xujbEF .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-C36V71EiM9xujbEF .noteText,#mermaid-svg-C36V71EiM9xujbEF .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-C36V71EiM9xujbEF .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-C36V71EiM9xujbEF .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-C36V71EiM9xujbEF .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-C36V71EiM9xujbEF .actorPopupMenu{position:absolute;}#mermaid-svg-C36V71EiM9xujbEF .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-C36V71EiM9xujbEF .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-C36V71EiM9xujbEF .actor-man circle,#mermaid-svg-C36V71EiM9xujbEF line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-C36V71EiM9xujbEF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ownerUserId + roomId + modeTRTC_INVITE_LINK_READY(inviteUrl, guestUserId, inviteToken)userId=ownerUserId, roomIdenterRoom + startLocalAudio + startLocalVideoviewerUserId=viewer_host_xxx, targetUserId=guestUserIdenterRoom, wait target打开邀请链接,访客进入业务页viewerUserId=viewer_guest_xxx, targetUserId=ownerUserIduserId=guestUserId, enterRoom + startLocalAudio + startLocalVideoREMOTE_VIDEO_AVAILABLE guestUserIdREMOTE_VIDEO_AVAILABLE ownerUserId

通用双 iframe 组合方式如下。

房主端:

html 复制代码
<iframe
  src="https://trtc.example.com/#/media-publisher?userId=host_1001&roomId=room_001&mode=business_a"
  allow="camera; microphone; autoplay; fullscreen"
></iframe>

<iframe
  src="https://trtc.example.com/#/media-viewer?viewerUserId=viewer_host_guest_2001&targetUserId=guest_2001&roomId=room_001&mode=business_a"
  allow="autoplay; fullscreen"
></iframe>

被邀请者端:

html 复制代码
<iframe
  src="https://trtc.example.com/#/media-viewer?viewerUserId=viewer_guest_host_1001&targetUserId=host_1001&roomId=room_001&mode=business_a"
  allow="autoplay; fullscreen"
></iframe>

<iframe
  src="https://trtc.example.com/#/media-publisher?userId=guest_2001&roomId=room_001&mode=business_a"
  allow="camera; microphone; autoplay; fullscreen"
></iframe>

当前项目还提供了 /#/media-module-demo,用于测试邀请参数、房主端组合和被邀请者端组合。

10. iframe 跨页面通信设计

iframe 接入后,父页面需要知道子模块是否连接成功、是否正在发布、是否失败、是否已经退出。当前项目用 postMessage 实现跨页面通信。

状态通知

状态通知函数在 src/utils/postMessage.ts

ts 复制代码
export function postTrtcModuleStatus(payload: Omit<TrtcModuleStatusPayload, 'type'>) {
  if (window.parent && window.parent !== window) {
    window.parent.postMessage({ type: 'TRTC_MODULE_STATUS', ...payload }, '*');
  }
}

publisher 状态示例:

json 复制代码
{
  "type": "TRTC_MODULE_STATUS",
  "page": "media-publisher",
  "status": "publishing",
  "userId": "host_1001",
  "roomId": "room_001",
  "mode": "business_a"
}

viewer 状态示例:

json 复制代码
{
  "type": "TRTC_MODULE_STATUS",
  "page": "media-viewer",
  "status": "viewing",
  "userId": "viewer_guest_host_1001",
  "roomId": "room_001",
  "mode": "business_a",
  "targetUserId": "host_1001"
}

publisher 状态机来自 src/types/silentRoom.ts

text 复制代码
idle -> resolvingParams -> creating -> entering -> startingDevice -> publishing
failed
leaving -> destroyed

viewer 状态机:

text 复制代码
idle -> resolvingParams -> creating -> entering -> waitingTarget -> viewing
targetOffline
failed
destroyed

命令控制

父页面可以向 iframe 发送命令:

js 复制代码
const iframe = document.querySelector('#hostPublisherIframe');

iframe.contentWindow.postMessage({
  type: 'TRTC_MODULE_COMMAND',
  command: 'leave'
}, '*');

命令订阅逻辑如下:

ts 复制代码
export function subscribeTrtcModuleCommand(onCommand: (payload: TrtcModuleCommandPayload) => void) {
  const handleMessage = (event: MessageEvent) => {
    const data = event.data;
    if (!data || typeof data !== 'object') return;
    if (data.type !== 'TRTC_MODULE_COMMAND') return;
    onCommand(data as TrtcModuleCommandPayload);
  };

  window.addEventListener('message', handleMessage);
  return () => window.removeEventListener('message', handleMessage);
}

src/types/silentRoom.ts 预定义了 restartmuteAudiounmuteAudiomuteVideounmuteVideoswitchCameraswitchMicrophone 等命令类型,但当前 useMediaPublisheruseMediaViewer 实际只处理 leave

生产环境中,父页面监听消息时不应直接信任 event.data,还要校验:

  • event.origin 是否为 TRTC 模块服务域名。
  • event.source 是否为当前 iframe 的 contentWindow
  • 消息里的 userIdroomIdtargetUserId 是否与业务系统记录一致。

11. React 内部架构映射

当前项目没有 services/ 目录,也没有额外的 React Context / Provider。服务化能力主要分布在:

层级 文件 职责
入口 src/main.tsx 初始化 Aegis、加载 i18n、渲染 App
路由 src/App.tsx 使用 HashRouter 注册页面路由,静默服务路由隐藏 NavBar
页面 src/pages/InviteLinkPage.tsx 生成邀请链接和 TRTC_INVITE_LINK_READY
页面 src/pages/MediaPublisherPage.tsx 渲染本地视频容器、连接状态和错误
页面 src/pages/MediaViewerPage.tsx 渲染远端视频容器、等待状态和错误
Hook src/hooks/useMediaPublisher.ts 发布端 TRTC 生命周期
Hook src/hooks/useMediaViewer.ts 观看端 TRTC 生命周期
Utils src/utils/silentRouteParams.ts 服务路由 URL 参数解析、缺参校验、邀请 payload 生成
Utils src/utils/postMessage.ts iframe 状态通知和命令订阅
Utils src/utils/generateTestUserSig.ts 浏览器端测试 UserSig 生成
Types src/types/silentRoom.ts 服务页面、状态、命令和参数类型

主 Demo 页仍保留在 HomePage + useTRTC + Zustand 架构中,支持手动进房、设备选择、屏幕共享、日志和邀请链接。服务化页面没有复用主页面 UI,而是用更薄的页面加专用 hook,降低 iframe 使用时的干扰。
#mermaid-svg-jp8bOBcVmwhqmSqr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jp8bOBcVmwhqmSqr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jp8bOBcVmwhqmSqr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jp8bOBcVmwhqmSqr .error-icon{fill:#552222;}#mermaid-svg-jp8bOBcVmwhqmSqr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jp8bOBcVmwhqmSqr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jp8bOBcVmwhqmSqr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jp8bOBcVmwhqmSqr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jp8bOBcVmwhqmSqr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jp8bOBcVmwhqmSqr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jp8bOBcVmwhqmSqr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jp8bOBcVmwhqmSqr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jp8bOBcVmwhqmSqr .marker.cross{stroke:#333333;}#mermaid-svg-jp8bOBcVmwhqmSqr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jp8bOBcVmwhqmSqr p{margin:0;}#mermaid-svg-jp8bOBcVmwhqmSqr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jp8bOBcVmwhqmSqr .cluster-label text{fill:#333;}#mermaid-svg-jp8bOBcVmwhqmSqr .cluster-label span{color:#333;}#mermaid-svg-jp8bOBcVmwhqmSqr .cluster-label span p{background-color:transparent;}#mermaid-svg-jp8bOBcVmwhqmSqr .label text,#mermaid-svg-jp8bOBcVmwhqmSqr span{fill:#333;color:#333;}#mermaid-svg-jp8bOBcVmwhqmSqr .node rect,#mermaid-svg-jp8bOBcVmwhqmSqr .node circle,#mermaid-svg-jp8bOBcVmwhqmSqr .node ellipse,#mermaid-svg-jp8bOBcVmwhqmSqr .node polygon,#mermaid-svg-jp8bOBcVmwhqmSqr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jp8bOBcVmwhqmSqr .rough-node .label text,#mermaid-svg-jp8bOBcVmwhqmSqr .node .label text,#mermaid-svg-jp8bOBcVmwhqmSqr .image-shape .label,#mermaid-svg-jp8bOBcVmwhqmSqr .icon-shape .label{text-anchor:middle;}#mermaid-svg-jp8bOBcVmwhqmSqr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jp8bOBcVmwhqmSqr .rough-node .label,#mermaid-svg-jp8bOBcVmwhqmSqr .node .label,#mermaid-svg-jp8bOBcVmwhqmSqr .image-shape .label,#mermaid-svg-jp8bOBcVmwhqmSqr .icon-shape .label{text-align:center;}#mermaid-svg-jp8bOBcVmwhqmSqr .node.clickable{cursor:pointer;}#mermaid-svg-jp8bOBcVmwhqmSqr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jp8bOBcVmwhqmSqr .arrowheadPath{fill:#333333;}#mermaid-svg-jp8bOBcVmwhqmSqr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jp8bOBcVmwhqmSqr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jp8bOBcVmwhqmSqr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jp8bOBcVmwhqmSqr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jp8bOBcVmwhqmSqr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jp8bOBcVmwhqmSqr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jp8bOBcVmwhqmSqr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jp8bOBcVmwhqmSqr .cluster text{fill:#333;}#mermaid-svg-jp8bOBcVmwhqmSqr .cluster span{color:#333;}#mermaid-svg-jp8bOBcVmwhqmSqr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jp8bOBcVmwhqmSqr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jp8bOBcVmwhqmSqr rect.text{fill:none;stroke-width:0;}#mermaid-svg-jp8bOBcVmwhqmSqr .icon-shape,#mermaid-svg-jp8bOBcVmwhqmSqr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jp8bOBcVmwhqmSqr .icon-shape p,#mermaid-svg-jp8bOBcVmwhqmSqr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jp8bOBcVmwhqmSqr .icon-shape .label rect,#mermaid-svg-jp8bOBcVmwhqmSqr .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jp8bOBcVmwhqmSqr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jp8bOBcVmwhqmSqr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jp8bOBcVmwhqmSqr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MediaPublisherPage / MediaViewerPage
silentRouteParams
useMediaPublisher / useMediaViewer
React useState 状态机
useRef: TRTC 实例和 video 容器
trtc-sdk-v5
postMessage

清理逻辑也集中在 hook 中。发布端卸载时会:

text 复制代码
stopLocalVideo
stopLocalAudio
exitRoom
off('*')
destroy
清空本地视频容器

观看端卸载时会:

text 复制代码
stopRemoteVideo({ userId: targetUserId })
exitRoom
off('*')
destroy
清空远端视频容器

这类清理对 iframe 模式尤其重要,因为父页面移除 iframe 时,子页面必须释放摄像头、麦克风和 TRTC 连接。

12. 浏览器安全限制

HTTPS

摄像头、麦克风和 WebRTC 依赖浏览器安全上下文。当前 README 和部署文档都明确提醒:生产环境或跨设备访问应使用 HTTPS。否则可能出现 navigator.mediaDevices 不可用、摄像头无法打开或 iframe 权限被拦截。

localhost

本地开发时,localhost127.0.0.1 通常被浏览器视为可用于调试的安全环境。当前 vite.config.ts 会从 5173 开始寻找可用端口,并监听 0.0.0.0,便于本机和局域网调试。

需要注意:局域网 IP + HTTP 通常不是安全上下文,不能等同于 localhost

iframe 权限

发布端 iframe 必须包含:

text 复制代码
camera; microphone; autoplay; fullscreen

观看端 iframe 至少应包含:

text 复制代码
autoplay; fullscreen

发布端需要摄像头和麦克风;观看端不采集本机设备,但仍可能受自动播放策略影响。

浏览器兼容

当前 HomePage 调用了 TRTC.isSupported(),如果不支持会提示使用最新版本 Chrome。静默路由没有单独做这层拦截,因此外部业务系统接入时建议在父页面或测试流程中补充浏览器检查。

实际生产中至少要覆盖:

  • Chrome:当前项目明确提示的推荐浏览器。
  • Edge:基于 Chromium 的 Edge 通常与 Chrome 接近,但仍应实际验证设备权限和自动播放。
  • Safari:需要重点验证 getUserMedia、iframe 权限和自动播放策略。

13. 部署注意事项

当前项目使用 Vite 构建,package.json 中脚本为:

json 复制代码
{
  "dev": "vite",
  "build": "tsc && vite build",
  "preview": "vite preview"
}

Dockerfile 采用两阶段构建:
#mermaid-svg-88TUbTCC8snvsu8n{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-88TUbTCC8snvsu8n .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-88TUbTCC8snvsu8n .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-88TUbTCC8snvsu8n .error-icon{fill:#552222;}#mermaid-svg-88TUbTCC8snvsu8n .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-88TUbTCC8snvsu8n .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-88TUbTCC8snvsu8n .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-88TUbTCC8snvsu8n .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-88TUbTCC8snvsu8n .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-88TUbTCC8snvsu8n .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-88TUbTCC8snvsu8n .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-88TUbTCC8snvsu8n .marker{fill:#333333;stroke:#333333;}#mermaid-svg-88TUbTCC8snvsu8n .marker.cross{stroke:#333333;}#mermaid-svg-88TUbTCC8snvsu8n svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-88TUbTCC8snvsu8n p{margin:0;}#mermaid-svg-88TUbTCC8snvsu8n .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-88TUbTCC8snvsu8n .cluster-label text{fill:#333;}#mermaid-svg-88TUbTCC8snvsu8n .cluster-label span{color:#333;}#mermaid-svg-88TUbTCC8snvsu8n .cluster-label span p{background-color:transparent;}#mermaid-svg-88TUbTCC8snvsu8n .label text,#mermaid-svg-88TUbTCC8snvsu8n span{fill:#333;color:#333;}#mermaid-svg-88TUbTCC8snvsu8n .node rect,#mermaid-svg-88TUbTCC8snvsu8n .node circle,#mermaid-svg-88TUbTCC8snvsu8n .node ellipse,#mermaid-svg-88TUbTCC8snvsu8n .node polygon,#mermaid-svg-88TUbTCC8snvsu8n .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-88TUbTCC8snvsu8n .rough-node .label text,#mermaid-svg-88TUbTCC8snvsu8n .node .label text,#mermaid-svg-88TUbTCC8snvsu8n .image-shape .label,#mermaid-svg-88TUbTCC8snvsu8n .icon-shape .label{text-anchor:middle;}#mermaid-svg-88TUbTCC8snvsu8n .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-88TUbTCC8snvsu8n .rough-node .label,#mermaid-svg-88TUbTCC8snvsu8n .node .label,#mermaid-svg-88TUbTCC8snvsu8n .image-shape .label,#mermaid-svg-88TUbTCC8snvsu8n .icon-shape .label{text-align:center;}#mermaid-svg-88TUbTCC8snvsu8n .node.clickable{cursor:pointer;}#mermaid-svg-88TUbTCC8snvsu8n .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-88TUbTCC8snvsu8n .arrowheadPath{fill:#333333;}#mermaid-svg-88TUbTCC8snvsu8n .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-88TUbTCC8snvsu8n .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-88TUbTCC8snvsu8n .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-88TUbTCC8snvsu8n .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-88TUbTCC8snvsu8n .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-88TUbTCC8snvsu8n .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-88TUbTCC8snvsu8n .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-88TUbTCC8snvsu8n .cluster text{fill:#333;}#mermaid-svg-88TUbTCC8snvsu8n .cluster span{color:#333;}#mermaid-svg-88TUbTCC8snvsu8n div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-88TUbTCC8snvsu8n .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-88TUbTCC8snvsu8n rect.text{fill:none;stroke-width:0;}#mermaid-svg-88TUbTCC8snvsu8n .icon-shape,#mermaid-svg-88TUbTCC8snvsu8n .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-88TUbTCC8snvsu8n .icon-shape p,#mermaid-svg-88TUbTCC8snvsu8n .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-88TUbTCC8snvsu8n .icon-shape .label rect,#mermaid-svg-88TUbTCC8snvsu8n .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-88TUbTCC8snvsu8n .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-88TUbTCC8snvsu8n .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-88TUbTCC8snvsu8n :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} node:20-alpine build
npm ci
npm run build
/app/dist
nginx:1.27-alpine runtime
/usr/share/nginx/html

部署时需要注意:

  • VITE_TRTC_SDK_APP_IDVITE_TRTC_SDK_SECRET_KEY 是构建期注入。
  • 修改 .env 后,开发环境要重启 Vite,Docker 生产环境要重新构建镜像。
  • nginx.conf 使用 try_files $uri $uri/ /index.html;,适配前端路由。
  • 当前 Docker Compose 将容器 80 端口映射到宿主机 18080
  • 生产环境必须使用 HTTPS 域名暴露服务,例如 https://trtc.example.com/

14. TRTC 安全设计

当前项目的 UserSig 生成逻辑在浏览器端:

ts 复制代码
const generator = new (window as any).LibGenerateTestUserSig(sdkAppId, sdkSecretKey, EXPIRETIME);
const userSig = generator.genTestUserSig(userId);

public/lib-generate-test-usersig.min.js 提供了测试用 UserSig 生成能力。README 中也明确说明:这只适合调试,生产环境必须迁移到后端。

推荐生产架构如下:
#mermaid-svg-DwJ0AwammaRoNryW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DwJ0AwammaRoNryW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DwJ0AwammaRoNryW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DwJ0AwammaRoNryW .error-icon{fill:#552222;}#mermaid-svg-DwJ0AwammaRoNryW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DwJ0AwammaRoNryW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DwJ0AwammaRoNryW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DwJ0AwammaRoNryW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DwJ0AwammaRoNryW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DwJ0AwammaRoNryW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DwJ0AwammaRoNryW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DwJ0AwammaRoNryW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DwJ0AwammaRoNryW .marker.cross{stroke:#333333;}#mermaid-svg-DwJ0AwammaRoNryW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DwJ0AwammaRoNryW p{margin:0;}#mermaid-svg-DwJ0AwammaRoNryW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DwJ0AwammaRoNryW .cluster-label text{fill:#333;}#mermaid-svg-DwJ0AwammaRoNryW .cluster-label span{color:#333;}#mermaid-svg-DwJ0AwammaRoNryW .cluster-label span p{background-color:transparent;}#mermaid-svg-DwJ0AwammaRoNryW .label text,#mermaid-svg-DwJ0AwammaRoNryW span{fill:#333;color:#333;}#mermaid-svg-DwJ0AwammaRoNryW .node rect,#mermaid-svg-DwJ0AwammaRoNryW .node circle,#mermaid-svg-DwJ0AwammaRoNryW .node ellipse,#mermaid-svg-DwJ0AwammaRoNryW .node polygon,#mermaid-svg-DwJ0AwammaRoNryW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DwJ0AwammaRoNryW .rough-node .label text,#mermaid-svg-DwJ0AwammaRoNryW .node .label text,#mermaid-svg-DwJ0AwammaRoNryW .image-shape .label,#mermaid-svg-DwJ0AwammaRoNryW .icon-shape .label{text-anchor:middle;}#mermaid-svg-DwJ0AwammaRoNryW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DwJ0AwammaRoNryW .rough-node .label,#mermaid-svg-DwJ0AwammaRoNryW .node .label,#mermaid-svg-DwJ0AwammaRoNryW .image-shape .label,#mermaid-svg-DwJ0AwammaRoNryW .icon-shape .label{text-align:center;}#mermaid-svg-DwJ0AwammaRoNryW .node.clickable{cursor:pointer;}#mermaid-svg-DwJ0AwammaRoNryW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DwJ0AwammaRoNryW .arrowheadPath{fill:#333333;}#mermaid-svg-DwJ0AwammaRoNryW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DwJ0AwammaRoNryW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DwJ0AwammaRoNryW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DwJ0AwammaRoNryW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DwJ0AwammaRoNryW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DwJ0AwammaRoNryW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DwJ0AwammaRoNryW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DwJ0AwammaRoNryW .cluster text{fill:#333;}#mermaid-svg-DwJ0AwammaRoNryW .cluster span{color:#333;}#mermaid-svg-DwJ0AwammaRoNryW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DwJ0AwammaRoNryW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DwJ0AwammaRoNryW rect.text{fill:none;stroke-width:0;}#mermaid-svg-DwJ0AwammaRoNryW .icon-shape,#mermaid-svg-DwJ0AwammaRoNryW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DwJ0AwammaRoNryW .icon-shape p,#mermaid-svg-DwJ0AwammaRoNryW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DwJ0AwammaRoNryW .icon-shape .label rect,#mermaid-svg-DwJ0AwammaRoNryW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DwJ0AwammaRoNryW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DwJ0AwammaRoNryW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DwJ0AwammaRoNryW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 业务前端 / TRTC 服务页面
业务后端 UserSig 服务
SDKSecretKey 只保存在服务端
腾讯云 TRTC
trtc-sdk-v5 enterRoom

生产化改造建议:

  • 前端不保存 SDKSecretKey
  • inviteToken 由后端签发,绑定 roomIdownerUserIdguestUserId、有效期和权限。
  • UserSig 由后端按用户身份按需签发。
  • postMessage 的 originsource 和参数一致性必须校验。
  • 邀请链接中的敏感参数应控制有效期,并支持服务端失效。

15. 常见问题

摄像头打不开

优先检查:

  • 当前页面是否是 HTTPS、localhost127.0.0.1
  • iframe 是否配置 allow="camera; microphone; autoplay; fullscreen"
  • 浏览器是否已授权摄像头和麦克风。
  • 摄像头是否被其他程序占用。
  • VITE_TRTC_SDK_APP_IDVITE_TRTC_SDK_SECRET_KEY 是否在构建产物中正确注入。

viewer 黑屏

优先检查:

  • publisher 和 viewer 是否使用相同 roomId
  • targetUserId 是否等于发布端的 userId
  • 发布端是否已经进入 publishing 状态。
  • viewer 是否一直处于 waitingTargettargetOffline
  • viewerUserId 是否与 targetUserId 重复。

iframe 无声音

优先检查:

  • iframe 是否包含 allow="autoplay; fullscreen"
  • 浏览器是否因为自动播放策略阻止了音频。
  • 观看端是否真正收到 REMOTE_VIDEO_AVAILABLE 并进入 viewing
  • 发布端麦克风权限是否成功授权。

多窗口冲突

TRTC 用户身份必须唯一。同一个页面中每个 iframe 都应使用不同的 userIdviewerUserId

text 复制代码
hostUserId != guestUserId
viewerUserId != targetUserId
同一页面中的每个 iframe 都使用不同身份
双方使用同一个 roomId

缺少 VITE_TRTC_SDK_APP_ID / VITE_TRTC_SDK_SECRET_KEY

这不是外部业务 URL 参数缺失,而是 TRTC 模块自身构建产物缺少 SDK 凭据。开发环境检查 .env 并重启 Vite;Docker 环境通过 build args 重新构建镜像。

16. 总结

这次实践的核心不是"写了三个页面",而是把 TRTC Web SDK 的复杂生命周期转换成三个稳定的服务入口:

  • /#/invite-link 负责邀请参数和链路生成。
  • /#/media-publisher 负责本地设备采集和发布。
  • /#/media-viewer 负责指定远端用户订阅和播放。

业务系统通过 iframe 组合这些入口,就能快速获得音视频能力,而不用在每个系统里重复实现 TRTC 初始化、进房、设备启动、事件监听和资源释放。
#mermaid-svg-b6usynPyzBKfceXV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-b6usynPyzBKfceXV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-b6usynPyzBKfceXV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-b6usynPyzBKfceXV .error-icon{fill:#552222;}#mermaid-svg-b6usynPyzBKfceXV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-b6usynPyzBKfceXV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-b6usynPyzBKfceXV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-b6usynPyzBKfceXV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-b6usynPyzBKfceXV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-b6usynPyzBKfceXV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-b6usynPyzBKfceXV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-b6usynPyzBKfceXV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-b6usynPyzBKfceXV .marker.cross{stroke:#333333;}#mermaid-svg-b6usynPyzBKfceXV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-b6usynPyzBKfceXV p{margin:0;}#mermaid-svg-b6usynPyzBKfceXV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-b6usynPyzBKfceXV .cluster-label text{fill:#333;}#mermaid-svg-b6usynPyzBKfceXV .cluster-label span{color:#333;}#mermaid-svg-b6usynPyzBKfceXV .cluster-label span p{background-color:transparent;}#mermaid-svg-b6usynPyzBKfceXV .label text,#mermaid-svg-b6usynPyzBKfceXV span{fill:#333;color:#333;}#mermaid-svg-b6usynPyzBKfceXV .node rect,#mermaid-svg-b6usynPyzBKfceXV .node circle,#mermaid-svg-b6usynPyzBKfceXV .node ellipse,#mermaid-svg-b6usynPyzBKfceXV .node polygon,#mermaid-svg-b6usynPyzBKfceXV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-b6usynPyzBKfceXV .rough-node .label text,#mermaid-svg-b6usynPyzBKfceXV .node .label text,#mermaid-svg-b6usynPyzBKfceXV .image-shape .label,#mermaid-svg-b6usynPyzBKfceXV .icon-shape .label{text-anchor:middle;}#mermaid-svg-b6usynPyzBKfceXV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-b6usynPyzBKfceXV .rough-node .label,#mermaid-svg-b6usynPyzBKfceXV .node .label,#mermaid-svg-b6usynPyzBKfceXV .image-shape .label,#mermaid-svg-b6usynPyzBKfceXV .icon-shape .label{text-align:center;}#mermaid-svg-b6usynPyzBKfceXV .node.clickable{cursor:pointer;}#mermaid-svg-b6usynPyzBKfceXV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-b6usynPyzBKfceXV .arrowheadPath{fill:#333333;}#mermaid-svg-b6usynPyzBKfceXV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-b6usynPyzBKfceXV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-b6usynPyzBKfceXV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-b6usynPyzBKfceXV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-b6usynPyzBKfceXV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-b6usynPyzBKfceXV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-b6usynPyzBKfceXV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-b6usynPyzBKfceXV .cluster text{fill:#333;}#mermaid-svg-b6usynPyzBKfceXV .cluster span{color:#333;}#mermaid-svg-b6usynPyzBKfceXV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-b6usynPyzBKfceXV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-b6usynPyzBKfceXV rect.text{fill:none;stroke-width:0;}#mermaid-svg-b6usynPyzBKfceXV .icon-shape,#mermaid-svg-b6usynPyzBKfceXV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-b6usynPyzBKfceXV .icon-shape p,#mermaid-svg-b6usynPyzBKfceXV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-b6usynPyzBKfceXV .icon-shape .label rect,#mermaid-svg-b6usynPyzBKfceXV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-b6usynPyzBKfceXV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-b6usynPyzBKfceXV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-b6usynPyzBKfceXV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} TRTC Web SDK 复杂生命周期
React 静默服务路由
iframe 接入
多业务复用
降低接入和维护成本

这类服务化封装适合被多个业务系统复用:音视频模块集中演进,业务系统只关注用户、房间、邀请链路和页面编排。对于企业内部多业务接入实时音视频,这是比"每个系统集成一次 SDK"更可维护的架构。

相关推荐
索西引擎1 小时前
【React】Redux 中间件机制:副作用处理与数据流增强的形式化分析
前端·react.js·中间件
寒草2 小时前
「寒草呈献」工作六年,是否仍有创造未来的勇气 ✨
前端·后端
HackTwoHub2 小时前
解锁 AI 红队全新玩法!Claude-Red 攻防 Skill 库,内置 SQLi、XXE、文件上传等 Web 专项 Skill,一键导入快速落地渗透实战
前端·人工智能·web安全·网络安全·自动化·系统安全
石小石Orz2 小时前
如何设计一个优秀的 Skills
前端·人工智能
程序员爱钓鱼3 小时前
Rust 切片 Slice 详解:安全访问连续数据
前端·后端·rust
EasyGBS5 小时前
政务视频资源汇聚:国标GB28181视频平台EasyGBS在“一网统管”中的定位和落地路径
网络·音视频·政务
alexander0685 小时前
CSS 类选择器组合
前端·css
寅时码10 小时前
React 之死·终章:一个 useRef,把闭包陷阱、依赖数组、漫天 rerender 全送走
前端·react.js·ai编程
不好听61310 小时前
HTML 事件监听机制:从 DOM Level 0 到 React 合成事件
前端·react.js·html