本文将介绍腾讯云物联网(IoT)应用端 SDK 中云存回放(
TXIoTCloudStorage)的使用方法。完成快速接入并登录 SDK 后,您可以获取指定设备的云存对象,查询有云端录像的日期、拉取某一天的事件与录像列表。前提条件
在调用本文 API 前,请确保已完成以下准备工作:
参考 集成与登录 在工程中集成 IoT 应用端 SDK,完成登录,并将目标设备绑定到当前登录账户名下。
参考 开通云存服务,且设备已产生云端录像。
使用建议
调用顺序:获取云存对象 →
getDayList 查询有录像的日期 → 用户选中某天后 getEventList 拉取该天数据 → 循环翻页直到 nextPageToken 为空 → 选中某条录像交给播放器播放。一个云存对象对应一台设备:
getCloudStorage 需传入设备 ID,切换设备时需重新获取。分页务必拉完:
getEventList 单次调用不代表拿到当天全部数据,必须循环翻页。事件与录像分开渲染:事件列表用于展示“发生了什么”,录像文件用于“播放画面”,两者用途不同,建议在 UI 上区分。
获取云存对象
登录成功后,通过
TXIoTEngine 单例调用 getCloudStorage 并传入目标设备 ID,即可获取该设备的云存对象 TXIoTCloudStorage。注意:
云存对象与设备一一对应,不是全局单例。未登录时返回空值;切换设备时必须重新调用
getCloudStorage 获取新对象,不能复用其他设备的云存对象。TXIoTDeviceId deviceId = new TXIoTDeviceId();deviceId.productId = "your_product_id";deviceId.deviceName = "your_device_name";TXIoTCloudStorage cloudStorage = TXIoTEngine.getInstance(context).getCloudStorage(deviceId);if (cloudStorage == null) {// 未登录或参数异常,需先完成登录return;}
TXIoTDeviceId *deviceId = [[TXIoTDeviceId alloc] init];deviceId.productId = @"your_product_id";deviceId.deviceName = @"your_device_name";TXIoTCloudStorage *cloudStorage = [[TXIoTEngine sharedInstance] getCloudStorage:deviceId];if (cloudStorage == nil) {// 未登录或参数异常,需先完成登录return;}
查询有录像的日期
调用
getDayList 获取指定通道下有云端录像的日期列表,返回格式为 yyyy-MM-dd 的字符串列表,通常用于渲染日期选择器,避免用户选到没有录像的日期。参数 | 说明 |
channelId | 通道 ID,从 0 开始计数;单通道设备传 0。 |
timeZone | 按天统计所使用的时区。请与后续 getEventList 保持一致,否则会出现日期与录像对不上的问题。 |
注意:
传空指针时会自动使用系统默认时区,为保证多端行为一致,建议始终显式传值。
cloudStorage.getDayList(0, TimeZone.getDefault(), new TXIoTCallback<List<String>>() {@Overridepublic void onSuccess(List<String> dayList) {// dayList 形如 ["2026-08-18", "2026-08-19", "2026-08-20"]renderDatePicker(dayList);}@Overridepublic void onError(TXIoTErrorCode errorCode, String errorMessage) {// 统一错误处理}});
TXIoTCallback<NSArray<NSString *> *> *callback = [[TXIoTCallback alloc] init];callback.onSuccess = ^(NSArray<NSString *> *dayList) {// dayList 形如 @[@"2026-08-18", @"2026-08-19", @"2026-08-20"][self renderDatePicker:dayList];};callback.onError = ^(TXIoTErrorCode errorCode, NSString *errorMessage) {// 统一错误处理};[cloudStorage getDayList:0 timeZone:[NSTimeZone localTimeZone] callback:callback];
拉取某一天的事件与录像
用户选中某个日期后,调用
getEventList 拉取该天数据。由于数据分页返回,需要循环翻页直到 nextPageToken 为空,并在拉取过程中按 eventType 分流。参数 | 说明 |
channelId | 通道 ID,需与 getDayList 一致。 |
timeZone | 时区,需与 getDayList 一致。 |
date | 日期,格式 yyyy-MM-dd,取自 getDayList 的返回结果。不可为空字符串。 |
nextPageToken | 分页游标。首次传空字符串,翻页时原样回传上一次的返回值。 |
// recordSegments:录像段(时间轴);alarmEvents:事件段(事件列表)private void loadPage(TXIoTCloudStorage storage, String date, String pageToken) {storage.getEventList(0, TimeZone.getDefault(), date, pageToken, new TXIoTCallback<TXIoTPageResult<TXIoTEvent>>() {@Overridepublic void onSuccess(TXIoTPageResult<TXIoTEvent> result) {for (TXIoTEvent event : result.dataList) {// eventType 为空是录像段,非空是事件段if (TextUtils.isEmpty(event.eventType)) {recordSegments.add(event);} else {alarmEvents.add(event);}}if (!TextUtils.isEmpty(result.nextPageToken)) {loadPage(storage, date, result.nextPageToken); // 游标原样回传return;}refreshUI(recordSegments, alarmEvents); // 拉完再统一刷新}@Overridepublic void onError(TXIoTErrorCode errorCode, String errorMessage) {// 统一错误处理}});}// 首次拉取:清空缓存后从空游标开始loadPage(cloudStorage, date, "");
// recordSegments:录像段(时间轴);alarmEvents:事件段(事件列表)- (void)loadPage:(NSString *)datestorage:(TXIoTCloudStorage *)storagepageToken:(NSString *)pageToken {TXIoTCallback<TXIoTPageResult<TXIoTEvent *> *> *callback = [[TXIoTCallback alloc] init];callback.onSuccess = ^(TXIoTPageResult<TXIoTEvent *> *result) {for (TXIoTEvent *event in result.dataList) {// eventType 为空是录像段,非空是事件段if (event.eventType.length == 0) {[self.recordSegments addObject:event];} else {[self.alarmEvents addObject:event];}}if (result.nextPageToken.length > 0) {[self loadPage:date storage:storage pageToken:result.nextPageToken]; // 游标原样回传return;}[self refreshUI]; // 拉完再统一刷新};callback.onError = ^(TXIoTErrorCode errorCode, NSString *errorMessage) {// 统一错误处理};[storage getEventList:0timeZone:[NSTimeZone localTimeZone]date:datenextPageToken:pageTokencallback:callback];}// 首次拉取:清空缓存后传空游标// [self loadPage:date storage:cloudStorage pageToken:@""];
理解云存数据模型
云存查询返回的是事件列表,每个事件内部又包含录像文件列表,形成两级结构。理解这两级的分工是正确接入的前提。
两级结构说明
层级 | 含义与用途 |
TXIoTEvent | eventType 表示事件类型(如移动侦测、人形侦测),实际值需要 App 与设备端对齐。thumbnailUrl 为该事件的预览图,如果没有视频文件,说明该时间为抓图事件。eventTimeMs / durationMs 为事件的起始时间与时长,用于渲染事件列表、时间轴标记。 |
TXIoTVideoFile | 一个可播放的实体文件,包含播放所需的地址(未加密时为 HLS/m3u8 地址)或云点播三要素(开启云存加密时下发),以及该文件自身的起止时间。 |
一个事件可能对应0个、1个或多个录像文件:
1个:最常见,事件完整落在一个录像文件内。
多个:事件跨越了录像文件的切片边界(云端录像按固定时长切片存储),此时需按
startTimeMs 升序依次播放,才能覆盖完整事件时长。0个:该事件为抓图事件,云端没有关联录像(例如设备仅上报了告警但未录制)。
时间字段的口径
TXIoTEvent 与 TXIoTVideoFile 都有时间字段,但口径不同,请勿混用:字段 | 口径 |
TXIoTEvent.eventTimeMs | 事件开始时间(Unix 毫秒时间戳)。用于事件列表排序、时间轴打点。 |
TXIoTEvent.durationMs | 事件持续时长(毫秒)。可能大于单个录像文件的时长。 |
TXIoTVideoFile.startTimeMs | 该录像文件的起始时间(Unix 毫秒时间戳)。用于多文件排序、计算播放进度对应的绝对时刻。 |
TXIoTVideoFile.durationMs | 该录像文件的时长(毫秒)。 |
注意:
所有时间字段均为 Unix 毫秒级时间戳,若需展示为本地时间,请按传入
getEventList 的同一时区做格式化,避免跨日错位。如何播放一个录像文件
TXIoTVideoFile 的播放方式取决于设备的云存加密配置,两种方式互斥,服务端只会下发其中一种:1. URL 直播放(未开启云存加密,默认):
videoUrl 为 HLS(m3u8)地址,直接交给播放器播放即可。此时云点播三要素为空。2. 云点播播放(已开启云存加密):开启云存加密后,服务端会下发
vodAppId、vodFileId、vodPlaySign 三要素,需使用 云点播播放器 按 FileId 播放(IOT SDK 已包含点播播放器,不需要再次集成),由云点播完成解密与播放授权。注意:
两种播放方式由设备的云存加密配置决定,不需要业务侧切换套餐或改接口:未开启加密时只返回
videoUrl(m3u8),开启加密后才返回云点播三要素。private boolean startPlay(TXVodPlayer player, TXIoTVideoFile file) {// 已开启云存加密:走云点播播放if (!TextUtils.isEmpty(file.vodAppId) && !TextUtils.isEmpty(file.vodFileId)) {TXPlayInfoParams params = new TXPlayInfoParams(Integer.parseInt(file.vodAppId), file.vodFileId, file.vodPlaySign);player.startVodPlay(params);return true;}// 未开启云存加密:videoUrl 为 m3u8 地址,直接播放if (!TextUtils.isEmpty(file.videoUrl)) {return player.startVodPlay(file.videoUrl) == 0;}// 无可播放源return false;}
- (BOOL)startPlayWithPlayer:(TXVodPlayer *)player file:(TXIoTVideoFile *)file {// 已开启云存加密:走云点播播放if (file.vodAppId.length > 0 && file.vodFileId.length > 0) {TXPlayerAuthParams *params = [[TXPlayerAuthParams alloc] init];params.appId = [file.vodAppId intValue];params.fileId = file.vodFileId;params.sign = file.vodPlaySign;[player startVodPlayWithParams:params];return YES;}// 未开启云存加密:videoUrl 为 m3u8 地址,直接播放if (file.videoUrl.length > 0) {return [player startVodPlay:file.videoUrl] == 0;}// 无可播放源return NO;}
function resolvePlayUrl(file: TXIoTVideoFile): string {// 未开启云存加密时,videoUrl 为 m3u8 地址,可直接交给播放器,小程序不支持加密云存播放。return file.videoUrl || '';}
播放跨多个文件的事件
当事件跨越录像切片边界时,
videoFiles 会包含多个文件。此时需按 startTimeMs 升序排序,在前一个文件播放结束时依次切换到下一个,才能完整播完整个事件。private List<TXIoTVideoFile> playQueue = new ArrayList<>();private int playIndex = 0;private void playEvent(TXVodPlayer player, TXIoTEvent event) {if (event.videoFiles == null || event.videoFiles.isEmpty()) {// 纯告警事件,无关联录像,不可播放showToast("该事件无录像");return;}playQueue = new ArrayList<>(event.videoFiles);// 多文件必须按起始时间升序播放Collections.sort(playQueue, (a, b) -> Long.compare(a.startTimeMs, b.startTimeMs));playIndex = 0;startPlay(player, playQueue.get(playIndex));}// 在播放器的播放结束回调中切换下一段private void onPlayFinished(TXVodPlayer player) {playIndex++;if (playIndex < playQueue.size()) {startPlay(player, playQueue.get(playIndex));}}
@property(nonatomic, strong) NSArray<TXIoTVideoFile *> *playQueue;@property(nonatomic, assign) NSInteger playIndex;- (void)playEvent:(TXIoTEvent *)event player:(TXVodPlayer *)player {if (event.videoFiles.count == 0) {// 纯告警事件,无关联录像,不可播放[self showToast:@"该事件无录像"];return;}// 多文件必须按起始时间升序播放self.playQueue = [event.videoFiles sortedArrayUsingComparator:^NSComparisonResult(TXIoTVideoFile *a, TXIoTVideoFile *b) {return a.startTimeMs < b.startTimeMs ? NSOrderedAscending : NSOrderedDescending;}];self.playIndex = 0;[self startPlayWithPlayer:player file:self.playQueue[self.playIndex]];}// 在播放器的播放结束回调中切换下一段- (void)onPlayFinishedWithPlayer:(TXVodPlayer *)player {self.playIndex += 1;if (self.playIndex < self.playQueue.count) {[self startPlayWithPlayer:player file:self.playQueue[self.playIndex]];}}
说明:
若需实现“从事件中间某个时刻开始播放”,可用目标时刻减去当前文件的
startTimeMs,得到在该文件内的相对 seek 位置(毫秒)。常见问题
为什么前几页返回的事件 eventType 都是空字符串?
这是正常现象,
getEventList 是分页返回数据,请继续翻页直到 nextPageToken 为空,即可拿到事件段数据。为什么某个事件的 videoFiles 是空列表?
该事件为抓图事件,云端没有关联录像(例如设备仅上报告警但未录制)。此类事件只能展示缩略图与时间,请在 UI 上置为不可播放,切勿直接访问
videoFiles[0]。为什么事件的 durationMs 比 videoFiles 里单个文件的时长长?
云端录像按固定时长切片存储,事件跨越切片边界时会关联多个文件。事件时长是整段事件的时长,单个文件时长只是其中一片。请按
startTimeMs 升序依次播放全部文件。为什么日期选择器有数据,但拉取事件列表返回为空?
请检查
getDayList 与 getEventList 传入的 channelId 和 timeZone 是否完全一致。时区不一致会导致按天切分的边界错位,进而查不到数据。能否自己拼接 nextPageToken 以跳页?
不可以。
nextPageToken 是 SDK 内部的不透明游标,包含用于区分数据段的内部标识,自行构造会返回参数错误。请始终原样回传上一次的返回值。播放时应该用 videoUrl 还是云点播三要素?
取决于设备是否开启了云存加密,两者不是优先级关系而是互斥关系:未开启加密时(默认)服务端只返回
videoUrl,为 HLS(m3u8)地址,直接播放即可;开启加密后服务端才返回云点播三要素(vodAppId / vodFileId / vodPlaySign),需用云点播播放器按 FileId 播放,由云点播完成解密。接口参考
云存模块的完整接口定义、参数与数据结构说明,参见 API 文档: