帮你快速理解、总结文档立即下载

应用端接入

最近更新时间:2026-10-10 14:07:32
我的收藏
本文将介绍腾讯云物联网(IoT)应用端 SDK 中云存回放(TXIoTCloudStorage)的使用方法。完成快速接入并登录 SDK 后,您可以获取指定设备的云存对象,查询有云端录像的日期、拉取某一天的事件与录像列表。

前提条件

在调用本文 API 前,请确保已完成以下准备工作:
参考 集成与登录 在工程中集成 IoT 应用端 SDK,完成登录,并将目标设备绑定到当前登录账户名下。
参考 开通云存服务,且设备已产生云端录像。

使用建议

调用顺序:获取云存对象 → getDayList 查询有录像的日期 → 用户选中某天后 getEventList 拉取该天数据 → 循环翻页直到 nextPageToken 为空 → 选中某条录像交给播放器播放。
一个云存对象对应一台设备:getCloudStorage 需传入设备 ID,切换设备时需重新获取。
分页务必拉完:getEventList 单次调用不代表拿到当天全部数据,必须循环翻页。
事件与录像分开渲染:事件列表用于展示“发生了什么”,录像文件用于“播放画面”,两者用途不同,建议在 UI 上区分。

获取云存对象

登录成功后,通过 TXIoTEngine 单例调用 getCloudStorage 并传入目标设备 ID,即可获取该设备的云存对象 TXIoTCloudStorage。
注意:
云存对象与设备一一对应,不是全局单例。未登录时返回空值;切换设备时必须重新调用 getCloudStorage 获取新对象,不能复用其他设备的云存对象。
Android
iOS
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 保持一致,否则会出现日期与录像对不上的问题。
注意:
传空指针时会自动使用系统默认时区,为保证多端行为一致,建议始终显式传值。
Android
iOS
cloudStorage.getDayList(0, TimeZone.getDefault(), new TXIoTCallback<List<String>>() {
@Override
public void onSuccess(List<String> dayList) {
// dayList 形如 ["2026-08-18", "2026-08-19", "2026-08-20"]
renderDatePicker(dayList);
}

@Override
public 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
分页游标。首次传空字符串,翻页时原样回传上一次的返回值。
Android
iOS
// recordSegments:录像段(时间轴);alarmEvents:事件段(事件列表)
private void loadPage(TXIoTCloudStorage storage, String date, String pageToken) {
storage.getEventList(0, TimeZone.getDefault(), date, pageToken, new TXIoTCallback<TXIoTPageResult<TXIoTEvent>>() {
@Override
public 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); // 拉完再统一刷新
}

@Override
public void onError(TXIoTErrorCode errorCode, String errorMessage) {
// 统一错误处理
}
});
}

// 首次拉取:清空缓存后从空游标开始
loadPage(cloudStorage, date, "");
// recordSegments:录像段(时间轴);alarmEvents:事件段(事件列表)
- (void)loadPage:(NSString *)date
storage:(TXIoTCloudStorage *)storage
pageToken:(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:0
timeZone:[NSTimeZone localTimeZone]
date:date
nextPageToken:pageToken
callback: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),开启加密后才返回云点播三要素。
Android
iOS
小程序
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 升序排序,在前一个文件播放结束时依次切换到下一个,才能完整播完整个事件。
Android
iOS
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 文档: