跳到主要内容

存储卡

更新时间:2026-09-03 13:57:01
// 导入头文件
import 'package:airtc_engine/src/sd_storage/rtcx_sd_storage.dart';

SD卡存储录像调用接口

class RTCXFluSDCardApi {
// 查询SD卡录像列表
@async
RTCXFluSDStorageRecordListRsp querySDRecordList(RTCXFluSDStorageRecordReq req);

// 查询SD卡事件列表
@async
RTCXFluSDStorageRecordEventListRsp querySDRecordEventList(
RTCXFluSDStorageRecordEventReq req,
);

// 查询录像文件列表
@async
RTCXFluSDStorageRecordFileListRsp querySDRecordFileList(
RTCXFluSDStorageRecordFileReq req,
);

// 查询SD卡月录像
@async
RTCXFluSDStorageMonthRecordRsp querySDMonthRecord(
RTCXFluSDStorageMonthRecordReq req,
);
}

创建api实例

// 创建api实例
final RTCXFluSDCardApi sdCardApi = RTCXFluSDCardApi();

SD卡录制列表

时间跨度不能超过7天,开始时间不能大于结束时间

分页逻辑是查询beginTime至endTime时间段内按时间降序数据 前pageSize条数据。下 一页数据将上次返回的nextEndTime 作为下次请求的endTime

void querySDRecordList() {
RTCXFluSDStorageRecordReq req = RTCXFluSDStorageRecordReq(
iotId: "",
productKey: '',
deviceName: '',
beginTime: beginTime,
endTime: endTime,
);
sdCardApi
.querySDRecordList(req)
.then((data) {
RTCXFluSDStorageRecordListRsp rsp = data;
List<RTCXFluSDStorageRecordRsp>? recordList = rsp.recordList;
})
.catchError((error) {
print("querySDRecordList error:${error.toString()}");
});
}

SD卡事件列表

时间跨度不能超过7天,开始时间不能大于结束时间

分页逻辑是查询beginTime至endTime时间段内按时间降序数据 前pageSize条数据。下 一页数据将上次返回的nextEndTime 作为下次请求的endTime

void querySDRecordEventList() {
RTCXFluSDStorageRecordEventReq req = RTCXFluSDStorageRecordEventReq(
iotId: "",
productKey: '',
deviceName: '',
beginTime: beginTime,
endTime: endTime,
);
sdCardApi
.querySDRecordEventList(req)
.then((data) {
RTCXFluSDStorageRecordEventListRsp rsp = data;
List<RTCXFluSDStorageRecordEventRsp>? recordEventList =
rsp.recordEventList;
})
.catchError((error) {
print("querySDRecordEventList error:${error.toString()}");
});
}

SD卡文件列表

时间跨度不能超过7天,开始时间不能大于结束时间

分页逻辑是查询beginTime至endTime时间段内按时间降序数据 前pageSize条数据。下 一页数据将上次返回的nextEndTime 作为下次请求的endTime

void querySDRecordFileList() {
RTCXFluSDStorageRecordFileReq req = RTCXFluSDStorageRecordFileReq(
iotId: "",
productKey: '',
deviceName: '',
beginTime: beginTime,
endTime: endTime,
);
sdCardApi
.querySDRecordFileList(req)
.then((data) {
RTCXFluSDStorageRecordFileListRsp rsp = data;
List<RTCXFluSDStorageRecordFileRsp>? recordFileList =
rsp.recordFileList;
})
.catchError((error) {
print("querySDRecordFileList error:${error.toString()}");
});
}

SD卡月录像列表

void querySDMonthRecord() {
RTCXFluSDStorageMonthRecordReq req = RTCXFluSDStorageMonthRecordReq(
iotId: "",
productKey: '',
deviceName: '',
year: year,
month: month,
);
sdCardApi
.querySDMonthRecord(req)
.then((data) {
RTCXFluSDStorageMonthRecordRsp rsp = data;
List<RTCXFluSDStorageDayRecordRsp>? monthRecordList =
rsp.monthRecordList;
})
.catchError((error) {
print("querySDMonthRecord error:${error.toString()}");
});
}

SD卡录制播放

// SD卡播放使用PlayerViewController
final PlayerViewController _controller = PlayerViewController();

void sdCardRecordPlay() {
final now = DateTime.now();
final yesterday = now.subtract(const Duration(hours: 24));
int beginTime = (yesterday.millisecondsSinceEpoch/1000).toInt();
int endTime = (now.millisecondsSinceEpoch/1000).toInt();
// beginTime与endTime根据获取到的SD卡录像RTCXFluSDStorageRecordRsp确定

RTCXFluPlayerSeekParam seekParam = RTCXFluPlayerSeekParam(
startTime: beginTime,
endTime: endTime,
playbackType: RTCXFluIOTPlaybackType.RTCXFluIOTPlaybackTypeSDCardRecord,
);
_controller.seekTo(seekParam);
}

SD卡文件录制播放

// 可以在获取SD卡文件列表后,根据返RTCXFluSDStorageRecordFileRsp进行播放SD卡文件录制播放
// SD卡播放使用PlayerViewController
final PlayerViewController _controller = PlayerViewController();

void sdCardRecordFilePlay() {
final now = DateTime.now();
final yesterday = now.subtract(const Duration(hours: 24));
int beginTime = (yesterday.millisecondsSinceEpoch/1000).toInt();
int endTime = (now.millisecondsSinceEpoch/1000).toInt();
// beginTime与endTime根据获取到的RTCXFluSDStorageRecordFileRsp确定

RTCXFluPlayerSeekParam seekParam = RTCXFluPlayerSeekParam(
startTime: beginTime,
endTime: endTime,
sdFileName: fileRsp.fileName,
sdFileStartTime: fileRsp.sdFileStartTime,
playbackType: RTCXFluIOTPlaybackType.RTCXFluIOTPlaybackTypeSDCardRecord,
);
_controller.seekTo(seekParam);
}

SD卡事件播放

// 可以在获取SD事件后,根据返回的事件的beginTime与endTime进行播放
// SD卡播放使用PlayerViewController
final PlayerViewController _controller = PlayerViewController();

void sdCardEventPlay() {
final now = DateTime.now();
final yesterday = now.subtract(const Duration(hours: 24));
int beginTime = (yesterday.millisecondsSinceEpoch/1000).toInt();
int endTime = (now.millisecondsSinceEpoch/1000).toInt();
// beginTime与endTime根据获取到的SD卡录像RTCXFluSDStorageRecordEventRsp确定

RTCXFluPlayerSeekParam seekParam = RTCXFluPlayerSeekParam(
startTime: beginTime,
endTime: endTime,
playbackType: RTCXFluIOTPlaybackType.RTCXFluIOTPlaybackTypeSDCardEvent,
);
_controller.seekTo(seekParam);
}

SD卡事件缩略图

通过图片ID获取SD卡存储的图片

注意
  1. 请严格控制请求频率, 大量获取图片会影响设备性能,导致请求失败
  2. 请不要重复发起相同图片的获取请求,并且缓存获已取到的图片
  3. 不再使用请及时销毁实例
/// 获取SD卡事件列表后,如果设备支持DataChannel并且在线,可以通过querySDCardEventPicture查询事件的缩略图
class RTCXFluDataChannelController {

/// 获取存储卡图片
Future<Uint8List> querySDCardEventPicture(RTCXFluDCSDCardEventPictureReq req) {
return _api.querySDCardEventPicture(uuid, req);
}
}

相关API接口对应如下:

// RTCXFluDataChannelController初始化
late RTCXFluDataChannelController _datachannelController;
String contentText = "";
Uint8List? _thumbImage;
final RTCXFluSDCardApi sdCardApi = RTCXFluSDCardApi();

@override
void initState() {
super.initState();
// datachannel通道
_datachannelController = RTCXFluDataChannelController();
_datachannelController.createDataChannel(
RTCXFluDataChannelModel(
productKey: widget.productKey,
deviceName: widget.deviceName,
iotId: widget.iotId,
deviceType: widget.deviceType,
),
);
_datachannelController.onData.listen((RTCXFluDCCustomDataTransfer event) {
if (event.customBinary != null) {
String content = utf8.decode(event.customBinary!);
setState(() {
contentText = "收到消息: $content";
});
}
});
_datachannelController.onStatusChange.listen((Map<String, Object> p1) {
String jsonString = json.encode(p1);
Fluttertoast.showToast(msg: "datachannel onStatus:${jsonString}");
});
}

// 释放资源
@override
void dispose() {
_datachannelController.destroy();
super.dispose();
}

获取存储卡图片接口调用如下:

// picId从SD卡事件获取到的picId
void _querySDCardPicture(String picId) {
RTCXFluDCSDCardEventPictureReq req = RTCXFluDCSDCardEventPictureReq(
picId: picId,
);
_datachannelController
.querySDCardEventPicture(req)
.then((Uint8List? data) {
Fluttertoast.showToast(msg: "查询SD卡事件缩略图成功");
if (data != null) {
// 获取到图片,可以通过Image.memory进行展示
setState(() {
_thumbImage = data;
});
}
})
.catchError((error) {
Fluttertoast.showToast(msg: "查询SD卡事件缩略图失败 error:${error}");
});
}

SD卡视频下载

SD卡视频下载请先申请文件写入权限

// 导入头文件
import 'package:airtc_engine/src/sd_storage/rtcx_sd_media_download.dart';
import 'package:airtc_engine/src/sd_storage/rtcx_sd_media_download_controller.dart';
import 'package:airtc_engine/src/utils/rtcx_toolkit.dart';

SD卡视频下载接口如下:


/// SD卡视频下载错误码
class RTCXFluSDMediaDownloadErrorCode {
static const int errorNotExist = -10064; // 下载任务不存在
static const int errorOnGoing = -10065; // 下载任务正在进行中
static const int errorTaskFull = -10066; // 下载任务数已达上限
static const int errorDownloadFail = -103; // 下载失败
static const int errorChannelRemoved = -102;// 下载通道已移除(P2P)
static const int errorChannelNonOpened = -100; //下载通道连接失败或断开(datachannel)
static const int errorChannelUnreliable = -5; //下载通道连接异常
static const int errorChannelFailed = -2; //下载通道连接失败或断开
static const int errorChannelTimeout = -1; //下载通道连接超时
}

class RTCXFluSDMediaDownloadController {
/// 下载进度回调
Stream<RTCXFluSDMediaDownloadProgress> get onDownloadProgress =>
_onDownloadProgressController.stream;

/// 下载文件回调
Stream<String> get onDownloadFilePath =>
_onDownloadFilePathController.stream;

/// 下载出现异常回调
Stream<RTCXFluSDMediaDownloadError> get onDownloadFileError =>
_onDownloadFileErrorController.stream;

/// 设置下载的额外配置
Future<void> createDownloader(RTCXFluSDMediaDownloadConfig config);

/// 开始下载
Future<void> startDownload(RTCXFluSDMediaDownloadReq req);

/// 结束下载,下载完成时,必须调用此接口,保存的视频才能正常播放,可通过下载进度回调判断是否完成
Future<void> stopDownload();

/// 销毁
Future<void> destroy();
}

调用示例如下:

  • 设置下载回调
// 初始化
final RTCXFluSDMediaDownloadController _downloadController =
RTCXFluSDMediaDownloadController();


/// 下载状态:0-未下载/空闲,1-下载中,2-下载完成,3-下载失败
int _downloadState = 0;

/// 下载进度(0.0 ~ 1.0)
double _downloadProgress = 0.0;

/// 下载完成的文件路径
String _downloadFilePath = '';

/// 下载错误信息
String _downloadErrorMsg = '';

// 在页面的Widget中initState方法初始化设置,设置下载回调
final RTCXFluSDMediaDownloadController _downloadController =
RTCXFluSDMediaDownloadController();
@override
void initState() {
super.initState();

RTCXFluSDMediaDownloadConfig downloadConfig = RTCXFluSDMediaDownloadConfig(
productKey: '${widget.productKey}',
deviceName: '${widget.deviceName}',
iotId: '${widget.iotId}',
);
_downloadController.createDownloader(downloadConfig);

// 下载完成回调:更新状态为完成,显示文件路径并提示
_downloadController.onDownloadFilePath.listen((String filePath) {
print("DeviceSdCardEventPlayPage filePath:${filePath}");
if (!mounted) return;
setState(() {
_downloadState = 2;
_downloadProgress = 1.0;
_downloadFilePath = filePath;
});
Fluttertoast.showToast(msg: "下载完成:$filePath");
if (filePath.isNotEmpty) {
_saveFile(filePath);
}
});

// 下载出错回调:更新状态为失败,显示错误信息并提示
_downloadController.onDownloadFileError.listen((
RTCXFluSDMediaDownloadError error,
) {
print("error:${error.code}, message:${error.message}");
if (!mounted) return;
setState(() {
_downloadState = 3;
_downloadErrorMsg = error.message ?? '';
});
Fluttertoast.showToast(msg: "下载出现错误:${error.message}");
});

// 下载进度回调:progress 单位为毫秒,用事件总时长换算成百分比
_downloadController.onDownloadProgress.listen((
RTCXFluSDMediaDownloadProgress progress,
) {
print(
"progress:${progress.progress}, completed:${progress.completed}",
);
if (!mounted) return;
final int totalDuration =
(widget.rsp.endTime ?? 0) - (widget.rsp.beginTime ?? 0);
double percent = 0.0;
if (totalDuration > 0) {
percent = ((progress.progress ?? 0) / totalDuration).clamp(0.0, 1.0);
}
final bool completed = progress.completed ?? false;
if (completed) percent = 1.0;
// 结束下载,下载完成时,必须调用此接口,保存的视频才能正常播放,可通过下载进度回调判断是否完成
if (completed || percent == 1.0) {
_stopDownload();
}

setState(() {
_downloadProgress = percent;
// 进度完成但未收到文件路径时保持下载中状态,等待 filePath 回调
if (completed && _downloadState == 1) {
_downloadProgress = 1.0;
}
});
});
}

  • 开始下载
void _onDownload() async {
if (_downloadState == 1) {
Fluttertoast.showToast(msg: '正在下载中,请稍候');
return;
}
try {
Directory fileDir = await RTCXPathUtil.createSubDir("download");

RTCXFluSDMediaDownloadReq req = RTCXFluSDMediaDownloadReq(
beginTime: widget.rsp.beginTime!,
endTime: widget.rsp.endTime!,
savePath: '${fileDir.path}',
deviceType: widget.deviceType,
productKey: '${widget.productKey}',
deviceName: '${widget.deviceName}',
);
setState(() {
_downloadState = 1;
_downloadProgress = 0.0;
_downloadFilePath = '';
_downloadErrorMsg = '';
});
_downloadController.startDownload(req);
} catch (e) {
setState(() {
_downloadState = 3;
_downloadErrorMsg = e.toString();
});
Fluttertoast.showToast(msg: "下载出现错误:${e.toString()}");
}
}

  • 结束下载

结束下载,下载完成时,必须调用此接口,保存的视频才能正常播放,可通过下载进度回调判断是否完成

// 结束下载,下载完成时,必须调用此接口,保存的视频才能正常播放,可通过下载进度回调判断是否完成
void _stopDownload() {
try {
_downloadController.stopDownload();
} catch (e) {
print("结束下载 e:${e.toString()}");
}
}

  • 保存视频
// 保存视频到相册,
Future<void> _saveFile(String filePath) async {
try {
bool isGranted = await RTCXPermissionUtil.requestStoragePermission();
if (isGranted) {
final result = await RTCXGallerySaverUtil.saveFileToGallery(filePath);
print("_保存录像 result:${result}");
Fluttertoast.showToast(msg: "保存录像result:${result}");
} else {
// 可根据iOS、Android进行引导打开存储到相册权限
Fluttertoast.showToast(msg: "权限未开通,请在设置->app->权限中开启权限");
}
} catch (e) {
Fluttertoast.showToast(msg: "保存录像出现错误:${e.toString()}");
}
}

  • 释放资源
// 当界面返回或Widget不在需要的时候,需要在dispose中进行释放
@override
void dispose() {
// 取消生命周期监听与所有回调订阅
_downloadController.destroy();
super.dispose();
}
注意

结束下载,下载完成时,必须调用此stopDownload,保存的视频才能正常播放,可以在onDownloadProgress回调中判断completed、progress下载进度否完成再调用stopDownload。