问题排查
接入异常时,先找出最早没有完成的阶段,再检查这一阶段的输入、返回值和回调。不要只凭“接口返回成功”判断录像已经上传、可以查询、已经播放或下载完成。
先从流程概览确认问题位于服务端签名、设备上传、客户端查询、Replay 播放还是本地文件生成,再依次检查各阶段。
开发阶段接入客户端日志上传(推荐)
建议在开发阶段就为客户端 App 预留日志上传入口。遇到问题并需要平台协助诊断时,可以直接上传同一次复现的日志。日志上传主要用于排查问题,不建议把入口作为面向所有用户的常规功能直接展示。常见做法是在“关于”页、版本号或其他约定位置连续点击、双击或长按,打开一个诊断页面;企业内部版本也可以放在仅测试人员可见的设置项中。
诊断页面应显示“上传中”状态;上传成功后展示并允许复制 logId,上传失败时展示错误码并允许重试。隐藏入口只是一种交互设计,不是安全控制;应用仍需按照隐私政策告知数据用途,并限制能够触发诊断操作的用户范围。
完成客户端 SDK 初始化或 Client 创建后,按本页的上传客户端日志接入当前平台的日志上传接口。需要定位云录像的解码、播放或音画同步问题时,可以再按需接入原始音视频采集。普通日志不包含原始音视频;只有应用显式完成一次采集后,下一次日志上传才会附带采集文件。
这些诊断接口用于客户端 SDK。设备端日志需要由设备保存或回传,具体见获取设备 C SDK 日志。
先确认问题停在哪个阶段
1. 服务端签名或 Token 签发失败
- 先用固定测试向量验证签名器,不要直接拿真实凭证试错。
- 检查 UTC 请求时间、最终发送的原始 Body、Canonical Query、Credential Scope 和 SignedHeaders。
- 确认
AppId与AccessKeyId属于同一应用,两类 Token 的目标和用途没有混用。 - 保留 HTTP 状态、业务错误码和
request_id,不要记录AccessKeySecret、Authorization 或完整 Token。
2. 设备 SDK 没有启动或上传任务没有完成
- 检查
TiCloudStorageInit()、Service 创建和启动结果,以及device_access_token是否有效。 TiCloudStorageQueueWriteFrame()返回成功只表示 SDK 接收当前帧;继续检查帧时间戳、关键帧、队列状态和服务回调。- 结束持续录像时调用
TiCloudStorageUploadSetEnd(),并等待该upload_id的on_result最终结果。 TiCloudStorageServiceStop()会中止活动任务并丢弃尚未分发的回调,不能用它等待上传完成。
3. 客户端查询不到录像
- 确认
app_access_token绑定目标设备,而且查询范围使用 UTC Unix 毫秒。 - 查询范围应覆盖已经完成上传的实际时间,不要使用本地时区字符串。
- 确认目标区间没有被删除;删除成功后,客户端录像时间段查询和播放会精确扣除该区间。
- 设备写帧成功但上传尚未完成时,查询为空不等于 SDK 丢帧。
4. 已查询到录像,但播放没有声音或画面
- 使用查询返回的可用时间段创建 Replay,不要跨越录像空洞。
- 确认音频和视频 Output 已绑定当前 Replay。
- 分别核对设备上传音频、视频时使用的
channel_id与客户端 Output 的绑定值;查询结果只表示设备整体在该时间段有录像。 - 同时记录 Replay 和每个 Output 的错误回调,并根据下方“播放没有声音或画面”继续判断。
- 检查 Replay、Output 和
TiCloudStorage实例是否被应用提前释放。 - 按当前平台 API 的状态、回调或 Promise 判断播放是否真正开始。
5. 下载录像失败
- 确认时间范围来自查询结果,SDK 缓存目录可写且磁盘空间充足。
- 保持 Export Task 或 Recording Task 及其依赖对象有效。
- 进度回调不是完成标志,以 Task 的最终结果为准。
- 区分 SDK 缓存文件与应用持久文件;需要长期保留时显式复制或移动。
- 同时保留 Replay 或 Export Task 的错误码,并按下表区分录像文件不存在、文件下载失败,以及文件已经下载但无法解析这三类问题。
6. 根据录像访问错误码继续排查
| 错误码 | 你看到这个错误时,已经发生了什么 | 怎么处理 |
|---|---|---|
| 6119 | Export 扫描了请求范围,但所选视频 Channel 没有可写入 MP4 的媒体;常见原因是下载使用的 Channel ID 与设备上传配置不同,或该 Channel 在所选范围内没有视频 | 从设备上传配置、设备日志或业务服务取得实际视频 Channel ID,并确认该 Channel 在所选范围内有支持的媒体。listRecordings 是设备级查询,重复查询不能确认 Channel 是否存在 |
| 6122 | 录像文件已经下载,但 SDK 无法解密或解析出有效的音视频帧,当前任务无法继续。常见原因包括文件损坏、封装格式错误,或 AAC ADTS 头内的参数与实际音频不一致 | 不要持续重试同一文件。如果设备上传 AAC,确认每次写入都保留完整 ADTS 头;ADTS 中的采样率和声道数必须与实际音频及 flags 一致,ADTS 声明的帧长度必须与本次写入字节数一致。修正后重新上传录像,并保留本次失败日志 |
| 6123 | SDK 在查询录像日期、录像时间段或取得下载信息时,没有完成云端请求。通常是网络中断、Ti 云存服务暂时不可用,或服务返回了 SDK 无法识别的数据 | 稍后重新查询。持续失败时保留 HTTP 状态、响应和同一次请求的 SDK 日志 |
| 6134 | SDK 请求一份具体录像文件时,云端明确返回文件不存在。查询结果可能已过期,或录像已经被删除 | 刷新录像列表后重新选择,不持续重试同一文件 |
| 6135 | SDK 未能成功下载一份具体录像文件。常见原因包括请求签名失败、连接超时、HTTP 错误、返回的数据范围不完整,或有限重试耗尽 | 稍后重试一次。持续失败时上传日志,并保留同一次请求的 HTTP 状态和响应摘要 |
| 6136 | 当前网络不可用,SDK 无法开始或继续访问录像服务 | 检查系统网络状态和网络权限;网络恢复后重新发起操作 |
| 6137 | SDK 无法解析当前录像服务 endpoint 的域名 | 检查 endpoint 拼写和设备 DNS 配置;使用默认 endpoint 时保留日志并联系技术支持 |
不要把 6122 当作“录像不存在”或通用网络错误。代码中使用各平台公开的错误常量判断类型,不要解析错误文本。
常见问题
播放没有声音或画面
先确认查询结果不为空,再看 Replay 和对应 Output 的错误回调。Replay 的 Token、网络或录像读取错误会影响整次播放;只有一个 Output 没有媒体时,重点检查它的 Channel ID、绑定和状态。
如果解码型 Audio 或 Video Output 返回 6130,说明它持续收到了同类媒体,但播放未暂停时连续 5 秒没有帧的 Channel ID 与绑定值匹配。通常是设备上传与客户端绑定的值不同;这个错误只终止当前 Output,Replay 和其他 Output 可以继续工作。
以 Flutter 为例,发起播放前把绑定值和错误码记在同一次日志中:
replay.onError = (code) => debugPrint('replay error=$code');
videoOutput.onError = (code) =>
debugPrint('video channel=$videoChannelId, error=$code');
videoOutput.attach(replay: replay, channelId: videoChannelId);然后按以下顺序处理:
- 从设备上传配置或设备日志取得同类媒体实际使用的
channel_id,与attach的值比较。音频和视频需要分别核对。 - 值不一致时,先 detach 对应 Output,再用正确值 attach,并重新播放同一时间范围。
- 值一致或没有明确错误时,检查该范围是否包含同类媒体、Video Output 是否绑定渲染视图,以及相关对象是否仍然存活。仍无法定位时,保留 SDK 版本、播放范围、Output 类型、状态、错误码、绑定值和同一次复现的双端日志。
所选范围内完全没有某种媒体类型时,对应 Output 可能在未输出媒体的情况下进入完成状态。因此,completed 不能证明 Channel ID 正确,仅凭没有声音或画面也不能判断是 6130。
录像只播放一部分,Replay 就回调完成
Replay 的完成回调表示本次 play 的来源范围已经耗尽,不表示最后一帧已经显示或最后一段音频已经播放。Replay 可以领先于实际输出,领先时间不是固定值。
如果应用在 Replay onCompleted / OnCompleted 中停止播放、解除 Output 绑定、移除渲染 View 或切换录像,已经接收但尚未输出的声音或画面无法继续在当前播放页呈现,因此可能只播放录像的一部分。自然播放到结尾时,继续保持 Replay、Output 和渲染 View 有效,并按业务目标等待对应 Output:
- 只需要画面时,先确认 Video Output 已经开始出画且没有失败,再等待 Video Output 进入完成状态;
- 同时需要声音和画面时,分别确认业务需要的 Audio、Video Output 已经开始输出且没有失败,再等待相应 Output 都进入完成状态。
Output 可能在没有对应媒体的情况下直接完成,所以不能只凭 completed 判断播放成功。Output 从未开始输出或进入 failed 时,按上方“播放没有声音或画面”继续检查 Channel ID、媒体范围、绑定和错误回调。各阶段的判断方法和状态名称见查询与播放录像。
问题仍未解决时收集证据
问题复现后尽快上传客户端日志,并保存同一次复现的服务端和设备端材料,避免日志滚动覆盖。至少包括:
- 问题发生时间和时区,最好同时提供 UTC Unix 毫秒。
- 平台、操作系统、CPU 架构、SDK 版本和 Build Info。
- 脱敏
AppId、device_id,以及服务端接口名、HTTP 状态、错误码和request_id。 - 关键调用的返回值、状态、最终结果回调和前后日志。
- 查询、播放或下载的开始与结束时间,以及设备实际写帧时间范围。
获取设备 C SDK 日志
在设备投入使用前配置可持久化的日志输出。问题复现后,取回覆盖 Service 启动、上传任务创建、写帧、设置结束时间和最终结果回调的连续记录。
如果设备只把日志输出到控制台且当时没有保存,历史记录无法补回,需要先增加文件、环形缓冲区或应用日志回调,再次复现。
检查客户端 HTTP 请求与响应
启用控制台日志后,每次 Ti 云存 HTTP 请求还会输出以下 Debug 诊断信息:
- 请求方法、URL、非敏感 Header 和可安全记录的请求 Body;
- HTTP 状态、耗时、传输阶段、传输错误和可安全记录的响应 Header、Body;
- 与该请求对应的 cURL 模板,用于复核 URL、Header 和 Body 是否符合预期。
Authorization、Token、Cookie、临时凭证、签名、Secret 和加密 Key 会显示为 <redacted>;敏感 JSON 整体脱敏,二进制响应只记录字节数,过长内容会截断。因此日志里的 cURL 是诊断模板,不是可以直接执行的完整凭证。确需重放请求时,只能在受控本机环境中补入有效凭证,不要把补全后的命令写入日志、工单或聊天记录。
排查时把同一次请求的 request、response 和 curl_template 日志放在一起查看。先检查 URL、方法和非敏感 Header,再检查 HTTP 状态、响应内容、transport_stage 和 transport_detail;这可以区分请求没有发出、远端拒绝、响应不可用和对象内容解析失败。
采集原始音视频数据
这里的“原始音视频”是 Replay 读取到、尚未由 SDK 转码或封装成 MP4 的音视频数据,并保留格式、时间等诊断信息。它可能仍是 H.264、H.265、AAC 等编码数据,不等同于未压缩的 PCM 或 YUV。采集结果是随日志上传的诊断压缩包,不是可以直接播放或交付的业务录像;需要可播放的 MP4 时,使用下载录像。
普通日志包不包含原始音视频。需要进一步定位解码、播放、音画同步或录像内容异常时,可以在 Replay 上按 Channel ID 开始一次原始音视频诊断采集。填写的 Channel ID 必须与设备上传和 Output 绑定值一致。
建议把原始音视频采集作为客户端诊断页面中的增强操作,不要在应用启动后默认采集。下面给出问题复现时的最小调用示例。
final started = await replay.startRawDump(
TiCloudStorageRawDumpOptions(
audioChannelIds: <int>[audioChannelId],
videoChannelIds: <int>[videoChannelId],
),
);
if (!started.success || started.data == null) return;
// 复现问题后停止采集。
final stopped = await started.data!.stop();
if (!stopped.success || stopped.data == null) return;
if (!stopped.data!.captureComplete) {
debugPrint('media capture incomplete: ${stopped.data!.stopReason}');
}
final upload = await TiRtcLogging.upload();
// upload.code == 0 且 logId 非空时提交给技术支持。const started = await replay.startRawDump({
audioChannelIds: [audioChannelId],
videoChannelIds: [videoChannelId],
});
if (!started.success || started.data === null) return;
// 复现问题后停止采集。
const stopped = await started.data.stop();
if (!stopped.success || stopped.data === null) return;
if (!stopped.data.captureComplete) {
console.warn(`media capture incomplete: ${stopped.data.stopReason}`);
}
const upload = await TiRtcLogging.upload();
// upload.code === 0 且 logId 非空时提交给技术支持。val started = replay.startRawDump(
TiCloudStorageRawDumpOptions(
audioChannelIds = intArrayOf(audioChannelId),
videoChannelIds = intArrayOf(videoChannelId),
),
)
val capture = started.dump ?: return
// 复现问题后停止采集。
val stopped = capture.stop()
val captureFile = stopped.archive ?: return
if (!captureFile.captureComplete) {
Log.w("TiCloudStorage", "media capture incomplete: ${captureFile.stopReason}")
}
TiRtcLogging.upload { code, logId ->
// code == 0 且 logId 非空时提交给技术支持。
}let started = await replay.startRawDump(
options: TiCloudStorageRawDumpOptions(
audioChannelIds: [NSNumber(value: audioChannelId)],
videoChannelIds: [NSNumber(value: videoChannelId)]
)
)
guard let capture = started.dump else { return }
// 复现问题后停止采集。
let stopped = await capture.stop()
guard let captureFile = stopped.archive else { return }
if !captureFile.captureComplete {
print("media capture incomplete: \(captureFile.stopReason)")
}
TiRtcLogging.upload { result in
// result.succeeded 时提交 result.logId。
}const started = await replay.startRawDump({
audioChannelIds: [audioChannelId],
videoChannelIds: [videoChannelId],
});
if (!started.success || started.data === null) return;
// 复现问题后停止采集。
const stopped = await started.data.stop();
if (!stopped.success || stopped.data === null) return;
if (!stopped.data.captureComplete) {
console.warn(`media capture incomplete: ${stopped.data.stopReason}`);
}
const upload = await TiRtcLogging.upload();
// upload.code === 0 且 logId 非空时提交给技术支持。capture, err := replay.StartRawDump(storage.RawDumpOptions{
AudioChannelIDs: []uint8{audioChannelID},
VideoChannelIDs: []uint8{videoChannelID},
})
if err != nil {
return err
}
defer capture.Close()
// 复现问题后停止采集。
archive, err := capture.Stop()
if err != nil {
return err
}
if !archive.CaptureComplete {
log.Printf("media capture incomplete: %v", archive.StopReason)
}
_, err = cloudStorage.UploadLogs()try (RawDump capture = replay.startRawDump(
new RawDumpOptions(
new int[] {audioChannelId},
new int[] {videoChannelId}))) {
// 复现问题后停止采集。
RawDumpArchive archive = capture.stop();
if (!archive.captureComplete()) {
System.out.println("media capture incomplete: " + archive.stopReason());
}
String logId = cloudStorage.uploadLogs();
}options = storage.RawDumpOptions(
audio_channel_ids=(audio_channel_id,),
video_channel_ids=(video_channel_id,),
)
with replay.start_raw_dump(options) as capture:
# 复现问题后停止采集。
archive = capture.stop()
if not archive.capture_complete:
print(f"media capture incomplete: {archive.stop_reason}")
log_id = cloud_client.upload_logs()单次采集最长 5 分钟,采集文件上限为 1 GiB;达到限制、来源关闭、资源不足或写入失败时会自动停止。停止后检查 captureComplete、stopReason、unsavedPacketCount、unsavedByteCount 和 empty,不能只凭生成了文件判断数据完整。
TiRTC 和 Ti 云存共用同一套诊断采集资源,同一进程同时只能进行一个原始音视频采集任务。先停止采集,再上传日志;采集和上传并发时会返回资源正在使用。上传失败时,采集文件保留在 SDK 缓存目录,可以在网络恢复后重试;成功上传后 SDK 会删除该文件。下一次成功采集或上传可能清理旧采集文件,需要自行留存时应提前复制到应用目录。
诊断压缩包可能包含真实音视频内容。采集和上传前必须取得必要授权并遵守应用隐私政策,避免采集与问题无关的敏感内容。
上传客户端日志
客户端应在开发阶段预留日志上传入口。完成对应 SDK 的初始化或 Client 创建后即可上传日志;问题复现后尽快使用该入口上传:
final ({int code, String? logId}) result = await TiRtcLogging.upload();
if (result.code == 0 && (result.logId?.isNotEmpty ?? false)) {
debugPrint('Ti Cloud Storage logId: ${result.logId}');
} else {
debugPrint('Ti Cloud Storage log upload failed, code=${result.code}');
}import {TiRtcLogging} from 'tirtc-react-native';
const result = await TiRtcLogging.upload();
if (result.code === 0 && result.logId !== null && result.logId.length > 0) {
console.info(`Ti Cloud Storage logId: ${result.logId}`);
} else {
console.warn(`Ti Cloud Storage log upload failed, code=${result.code}`);
}val accepted = TiRtcLogging.upload { code, logId ->
if (code == 0 && !logId.isNullOrEmpty()) {
Log.i("TiCloudStorage", "logId=$logId")
} else {
Log.w("TiCloudStorage", "upload failed, code=$code")
}
}
if (accepted != 0) {
Log.w("TiCloudStorage", "upload request not accepted, code=$accepted")
}import { TiRtcLogging } from 'tirtc/Index';
const result = await TiRtcLogging.upload();
if (result.code === 0 && result.logId !== undefined && result.logId.length > 0) {
console.info(`Ti Cloud Storage logId: ${result.logId}`);
} else {
console.warn(`Ti Cloud Storage log upload failed, code=${result.code}`);
}let accepted = TiRtcLogging.upload { result in
if result.succeeded, let logId = result.logId {
print("Ti Cloud Storage logId: \(logId)")
} else {
print("Ti Cloud Storage log upload failed, code=\(result.code)")
}
}
if accepted != 0 {
print("Ti Cloud Storage log upload request not accepted, code=\(accepted)")
}logID, err := cloudStorage.UploadLogs()
if err != nil {
log.Printf("Ti Cloud Storage log upload failed: %v", err)
} else {
log.Printf("Ti Cloud Storage logId: %s", logID)
}try {
String logId = cloudStorage.uploadLogs();
System.out.println("Ti Cloud Storage logId: " + logId);
} catch (TiRtcException error) {
System.err.println("Ti Cloud Storage log upload failed: " + error.code());
}try:
log_id = cloud_client.upload_logs()
print(f"Ti Cloud Storage logId: {log_id}")
except tirtc.TiRTCError as error:
print(f"Ti Cloud Storage log upload failed: {error}")上传成功后,记录返回的 logId。Android 和 iOS 的同步返回值只表示上传请求是否已受理,最终结果以回调为准;Flutter、HarmonyOS、Go、Java 和 Python 等待调用返回。React Native 成功返回表示上传已完成;Android 端如果因等待超时返回 code == 1001 且 logId == null,底层上传可能仍在收尾。此时调用 TiCloudStorage.shutdown() 可能返回 in-use (6026),等待后重试关闭。其他平台也要等上传完成后再关闭日志所属的服务或 Client。
调用上传接口会整理日志并产生网络请求。调用前确认已经满足应用的隐私政策和必要授权。正常日志不包含原始音视频;只有完成一次原始音视频诊断采集后,下一次上传才会附带采集文件。
如果上传失败,记录同步返回值、回调错误码、Go error、Java TiRtcException 或 Python TiRTCError,并按目标平台的应用日志机制导出同一次复现的原始记录。原始记录应覆盖 Client 或 TiCloudStorage 实例创建、录像查询、Replay、Output 和导出任务的返回值与状态变化。
提交排查材料
把以下材料发给开通时的联系人,或发送到 business@tange.ai。
- 最短复现步骤,以及问题停在哪个阶段。
- 客户端日志上传返回的
logId;上传失败时提供错误码和同一次复现的原始客户端日志。 - 同一次复现的服务端
request_id和设备端日志。 - 使用的平台、SDK 版本、时间范围、返回码和关键状态。
- 预期结果与实际结果。
提交前移除 Token、Authorization、AccessKeySecret、device_secret_key、用户信息和音视频内容中的敏感数据。