# RTC SDK 录制与会议回放 会议录制由当前主持人决定,具备平台权限的业务管理 API 也可控制。房间必须先允许录制;“允许录制”不等于已经开始录制。普通成员可以知道当前是否正在录制,但不能因为看到了录制按钮就获得启停权限。 ![会议摄像头、音频、共享屏幕与协作事件共用时间线](../../media/docs-media/sdk/recording.svg) **版本边界:**持续录制、成员合成、独立档案票据和交互回放已进入 匹配目标版本 本地发布包;旧 1.0.0.157 App 不会因服务升级自动出现新入口。接入时必须核对同批 SDK 与实际安装包,UniApp 自定义基座和 iPhone 新宿主录制按钮仍按[验收边界](acceptance.md)单独核实。 ```mermaid flowchart LR A["会议允许录制"] --> B["主持人启动一次持续录制"] B --> C["独立成员/屏幕音视频轨"] B --> D["聊天、白板和主持事件"] C --> E["会后兼容副本与分片索引"] D --> F["统一时间线"] E --> F F --> G["交互回放或按需整场导出"] ``` ## 会中控制和状态 Web/H5、Android 和 iOS 的 SDK 提供 `startContinuousRecording`、`stopContinuousRecording(recordingId)`、`getContinuousRecording`。UniApp 使用同名门面。SDK 快照提供 `recordingEnabled`、`canControlRecording`;Web/Android 的 `onContinuousRecordingChanged`、iOS 的 `didChangeContinuousRecording`、UniApp 的 `recordingState` 供宿主显示“录制中”。没有活动任务返回 `null`。返回 `Stopping` 时文件仍在收尾,不应立即显示“下载完成”。 主持人转交后,旧主持人不能继续启停。会中音视频分路录制,成员/屏幕合成在会后处理;静音或断流区间不能凭空伪造真人媒体。固定布局 MP4 与交互回放用途不同,白板和发言绿框按事件时间线由宿主回放。 主持人的网页可按以下方式接入按钮。`client` 是当前已入会的 `AkRtcWebClient`,SDK 返回的 `recordingId` 必须保存到本次会议页面状态,不要用旧的逐流 `startRecording` 来代替: ```js const started = await client.startContinuousRecording() const recordingId = started.recordingId // 页面显示“启动中”;等状态回调变为 Recording 后显示“录制中”。 const current = await client.getContinuousRecording() if (current?.recordingId === recordingId) { await client.stopContinuousRecording(recordingId) } // 返回 Stopping 后仍要等任务收尾,不能立即显示下载按钮。 ``` 主持转交、房间录制策略关闭、登录权限不足或媒体节点异常都可能使启停失败。按钮要保留错误状态与重新查询入口;403 不能无限重试。普通成员只调用查询和状态订阅,不提供可点击的启停控件。 ## 会后查询、播放和下载 入会票据与档案票据是两种凭据。业务后端需按会议号为有权限的用户签发短时档案票据;`view` 用于目录/事件,`play` 用于媒体,`download` 用于文件和 ZIP。权限还受会议范围、当前账号和原签发凭据约束。客户端通过无 UI 的档案 SDK 分页查询 `recordingId`,再读取该任务的清单、成员和设备轨道及资源 URL;同一个人用多台设备入会时按 `participantId + deviceSessionId` 区分。 群聊附件和白板图片属于本次公开事件且通过授权时才可查看或下载;私聊附件不因主持身份自动公开。票据过期后由业务后端重新签发。精确字段以同批交付的 RTC 录制 API 契约和服务端 OpenAPI 为准;页面布局、播放器和权限提示由宿主实现。 ## 用无 UI 档案 SDK 取得回放素材 业务后端先调用 `POST /api/v2/rtc/rooms/{roomId}/recordings/access`,为当前业务用户按需申请 `view`、`play`、`download`。Web 宿主把取票函数交给 SDK。这个函数要返回 `{ token, roomId, actions, expiresAtUtc }`,并在下一次调用时能重新向业务后端取票;不要把一张过期票据永久缓存在页面: ```js import { AkRtcArchiveClient, rtcArchiveSpeakers } from '@akstream/rtc-web-sdk' const archive = new AkRtcArchiveClient({ baseUrl: serverOrigin, accessProvider: (roomId, action) => businessBackend.getRtcArchiveTicket(roomId, action) }) const page = await archive.queryRecordings(roomId, { page: 1, pageSize: 20 }) const selected = page.items[0] if (selected) { let cursor = 0 const events = [] let manifest do { manifest = await archive.getManifest(roomId, selected.recordingId, { afterSequence: cursor }) events.push(...manifest.events) cursor = manifest.nextSequence } while (manifest.hasMoreEvents) const speaking = rtcArchiveSpeakers(events, 20000) console.log('20 秒处的发言设备', speaking) } ``` 这里的 `serverOrigin` 是 AKStream.Next 的 HTTPS 地址,`businessBackend.getRtcArchiveTicket` 是宿主自己实现的授权调用;SDK 不替业务系统决定谁能看哪间会议。会议列表按 `page/pageSize` 分页;单次录制的事件按 `nextSequence` 继续读取,上例把同一次录制的事件读完后才计算发言设备。大量事件时,页面可以边加载边显示;不要因为第一页没有发言事件就判定整场没人说话。 | 想做的事 | SDK 调用 | 所需动作 | |---|---|---| | 找到会议录制 | `queryRecordings(roomId, { page, pageSize, search, deviceSessionId })` | `view` | | 按时间线显示聊天、白板和发言 | `getManifest(roomId, recordingId, { afterSequence })` | `view` | | 在播放器中打开一段媒体 | 用清单中的 `fileId` 调用 `getMediaResource` | `play` | | 下载原始片段或 ZIP | `getDownloadResource` 或 `getPackageResource` | `download` | 资源方法返回短时 URL,交给宿主播放器或下载器即可;URL 不要保存成永久业务数据。搜索和设备筛选是录制列表条件,不会改变一段录像内原有的成员和时间线。 ## 回放时应该还原什么 | 素材 | 页面怎么用 | 没有素材时 | |---|---|---| | 成员摄像头与麦克风 | 按 `participantId + deviceSessionId` 区分当时的设备,并沿录制时间轴播放 | 显示该时段无媒体,不冻结上一帧冒充直播 | | 共享屏幕 | 作为独立发布与成员画面同步 | 保留其他成员画面 | | 群聊和白板 | 按事件时间显示当时内容 | 不把会议最终白板状态放到任意过去时刻 | | 发言与主持变化 | 用当时设备 ID 显示绿框和主持状态 | 旧录像缺事件时写“未知”,不猜是谁在讲话 | 整场 MP4 是会后按需导出,并非在开会时额外开一条持续转码流。原轨缺口、静音和摄像头关闭应保持真实时间关系;宿主可在交互回放中显示“此时段无媒体”。 逐个档案函数的参数、返回和错误见[档案 SDK 函数参考](archive-api.md);会中主持控制 payload 见[会议控制动作](room-actions.md),服务端签票和资源路径见[RTC 档案 API](../development/api-rtc-archive.md)。