Skip to content

查询、播放与保存录像

先查询目标设备的录像,再播放、下载或截取需要的内容。下载可以直接处理指定时间段,也可以保存正在播放的片段。

确认录像格式可以播放

录像的音视频格式由设备上传的编码帧决定。客户端 SDK 只播放这些已经上传的格式。

类型客户端 SDK 支持的格式
视频H.264、H.265、JPEG(MJPEG)
音频G.711 A-law、AAC、16 位 PCM、Opus

G.711 μ-law 和 G.726 音频无法播放。H.264、H.265、JPEG 和音频帧还必须符合上传录像中的帧要求。格式不受支持或帧数据不完整时,录像时间段仍可能被查询到,但播放或下载会返回格式不支持或录像不可读。此时应修正设备的编码和送帧方式,再上传新的录像;重复请求同一段录像不会改变结果。

准备 AppId 和客户端 Token

初始化 SDK

Android 示例通过页面的 lifecycleScope 调用挂起 API,因此 Kotlin 调用处不写 await

dart
import 'package:tirtc_flutter/tirtc_flutter.dart';

final initCode = await TiStore.init(appId: appId);
if (initCode != kTiStoreErrorOk) return;
kotlin
import androidx.lifecycle.lifecycleScope
import com.tange.ai.tirtc.*
import kotlinx.coroutines.launch

val initCode = TiStore.init(applicationContext, appId)
if (initCode != TiStoreErrorCode.OK) return
swift
import Foundation
import TiRTC

guard TiStore.initialize(appId: appId) == TiStoreErrorCode.ok else { return }
typescript
import {
  TiStore,
  TiStoreAudioOutput,
  TiStoreReplaySpeed,
  TiStoreVideoFit,
  TiStoreVideoOutput,
  TiStoreVideoOutputView,
  TISTORE_ERROR_OK,
} from 'tirtc';

const initCode = await TiStore.init({context: uiAbilityContext, appId});
if (initCode !== TISTORE_ERROR_OK) return;
typescript
import type {ReactElement} from 'react';
import {
  TiStore,
  TiStoreAudioOutput,
  TiStoreReplaySpeed,
  TiStoreVideoOutput,
  TISTORE_ERROR_OK,
} from 'tirtc-react-native';

const initCode = await TiStore.init({appId});
if (initCode !== TISTORE_ERROR_OK) return;
typescript
import {
  TiStore,
  TiStoreAudioOutput,
  TiStoreReplaySpeed,
  TiStoreVideoOutput,
  TiVideoView,
  TISTORE_ERROR_OK,
} from '@tange-ai/tirtc-electron';

// Electron Runtime 对象只在主进程创建。
const initCode = TiStore.init({appId});
if (initCode !== TISTORE_ERROR_OK) return;

初始化失败时不要继续创建 store。记录当前平台、SDK 版本和初始化错误码。根据对应平台的 API 说明修正 AppId、运行环境或初始化顺序。

创建用于访问目标设备的 Store 实例

使用目标设备对应的 app_access_token 创建 TiStore 实例。这个实例固定访问 Token 所绑定的设备,后续查询、播放和导出都通过它完成。同时访问多台设备时,为每台设备分别创建实例。

dart
final store = TiStore(token: token);
kotlin
val store = TiStore(token)
swift
let store = TiStore(token: token)
typescript
const store = new TiStore(token);
typescript
const store = new TiStore(token);
typescript
const store = new TiStore(token);

同一设备的 Token 续签后调用 updateToken。已经开始的查询、播放或导出仍使用旧 Token;失败的操作需要重新发起。

查询哪些日期有录像

月历或日期列表不需要逐日查询录像时间段。使用 listRecordingDays 一次取得包含式日期范围内的完整逐日结果,再保留 hasRecordingtrue 的日期。

dart
final resp = await store.listRecordingDays(
  startDate: '2026-08-01',
  endDate: '2026-08-31',
  timeZoneId: 'Asia/Shanghai',
);
if (!resp.success || resp.data == null) return;

final availableDays = resp.data!
    .where((day) => day.hasRecording)
    .map((day) => day.date)
    .toList();
kotlin
lifecycleScope.launch {
    val result = store.listRecordingDays(
        startDate = "2026-08-01",
        endDate = "2026-08-31",
        timeZoneId = "Asia/Shanghai",
    )
    if (result.code != TiStoreErrorCode.OK) return@launch

    val availableDays = result.days
        .filter { it.hasRecording }
        .map { it.date }
}
swift
let result = await store.listRecordingDays(
    startDate: "2026-08-01",
    endDate: "2026-08-31",
    timeZoneId: "Asia/Shanghai"
)
guard result.code == TiStoreErrorCode.ok else { return }

let availableDays = result.days
    .filter(\.hasRecording)
    .map(\.date)
typescript
const result = await store.listRecordingDays(
  '2026-08-01',
  '2026-08-31',
  'Asia/Shanghai',
);
if (result.code !== TISTORE_ERROR_OK) return;

const availableDays = result.days
  .filter((day) => day.hasRecording)
  .map((day) => day.date);
typescript
const result = await store.listRecordingDays(
  '2026-08-01',
  '2026-08-31',
  'Asia/Shanghai',
);
if (result.code !== TISTORE_ERROR_OK) return;

const availableDays = result.days
  .filter((day) => day.hasRecording)
  .map((day) => day.date);
typescript
const result = await store.listRecordingDays(
  '2026-08-01',
  '2026-08-31',
  'Asia/Shanghai',
);
if (result.code !== TISTORE_ERROR_OK) return;

const availableDays = result.days
  .filter((day) => day.hasRecording)
  .map((day) => day.date);

startDateendDate 都使用严格的 YYYY-MM-DD 格式,并且均包含在结果中。timeZoneId 使用 IANA 时区 ID;省略时默认为 Asia/Shanghai。单次最多查询 31 天,成功结果按日期升序返回范围内每一天,包括 hasRecording == false 的日期。

选择某一天后,按下面的边界计算 listRecordings 参数:

  1. 在查询使用的 IANA 时区中,把所选日期当天 00:00 作为开始边界。
  2. 在同一时区中,把下一个自然日的 00:00 作为结束边界。
  3. 分别把两个边界转换为 UTC Unix 毫秒,得到左闭右开区间 [startTimeMs, endTimeMs)

不要用 startTimeMs + 86400000 推导结束边界。采用夏令时的地区可能存在 23 或 25 小时的自然日。

查询可回放的时间段

listRecordings 返回设备在查询范围内有录像的时间段。没有录像时,结果为空列表。

dart
final resp = await store.listRecordings(
  startTimeMs: startTimeMs,
  endTimeMs: endTimeMs,
);
if (!resp.success || resp.data == null) return;

final recordings = resp.data!;
if (recordings.isEmpty) return;
final selectedRecording = recordings.first;
// 使用 selectedRecording 打开播放页。
kotlin
lifecycleScope.launch {
    val result = store.listRecordings(
        startTimeMs,
        endTimeMs,
    )
    if (result.code != TiStoreErrorCode.OK) return@launch

    val selectedRecording = result.recordings.firstOrNull() ?: return@launch
    // 使用 selectedRecording 打开播放页。
}
swift
let result = await store.listRecordings(
    startTimeMs: startTimeMs,
    endTimeMs: endTimeMs
)
guard result.code == TiStoreErrorCode.ok,
      let selectedRecording = result.recordings.first else { return }

// 使用 selectedRecording 打开播放页。
typescript
const result = await store.listRecordings(
  startTimeMs,
  endTimeMs,
);
if (result.code !== TISTORE_ERROR_OK || result.recordings.length === 0) return;

const selectedRecording = result.recordings[0];
typescript
const result = await store.listRecordings(
  startTimeMs,
  endTimeMs,
);
if (result.code !== TISTORE_ERROR_OK || result.recordings.length === 0) return;

const selectedRecording = result.recordings[0];
typescript
const result = await store.listRecordings(
  startTimeMs,
  endTimeMs,
);
if (result.code !== TISTORE_ERROR_OK || result.recordings.length === 0) return;

const selectedRecording = result.recordings[0];

查询结果按开始时间排序,并裁剪到请求范围。单次查询最长 10 天。返回的是设备整体有录像的时间段,不保证每个 Channel 都有媒体。播放使用的 Channel ID 由设备配置或业务服务提供。

Token 过期时,从业务服务端取得同一设备的新 app_access_token。调用 updateToken 后重新查询。权限不足时停止重试,确认当前用户、device_id 和 Token 的授权关系。查询跨度超过 10 天时,缩短时间范围后再查。

开始播放

创建录像播放对象(Replay)和需要的音视频输出对象(Output),绑定 Channel 后调用 playinitialTimeMs 是播放页选定的起播时间;没有指定时省略它,Replay 会从录像范围开头播放。

dart
final replay = store.createReplay();
final videoOutput = TiStoreVideoOutput();
final audioOutput = TiStoreAudioOutput();

replay.onError = (code) { /* 处理来源错误 */ };
replay.onCompleted = () { /* 播放范围自然结束 */ };

videoOutput.attach(replay: replay, channelId: videoChannelId);
audioOutput.attach(replay: replay, channelId: audioChannelId);
replay.play(
  startTimeMs: selectedRecording.startTimeMs,
  endTimeMs: selectedRecording.endTimeMs,
  initialTimeMs: initialTimeMs,
);

final video = AspectRatio(child: videoOutput.view());
kotlin
val replay = store.createReplay()
val videoOutput = TiStoreVideoOutput()
val audioOutput = TiStoreAudioOutput()

replay.onError = TiStoreReplayErrorListener { /* 处理来源错误 */ }
replay.onCompleted = TiStoreReplayCompletedListener { /* 自然结束 */ }

videoOutput.attachView(videoContainer)
videoOutput.attach(replay, videoChannelId)
audioOutput.attach(replay, audioChannelId)
replay.play(
    startTimeMs = selectedRecording.startTimeMs,
    endTimeMs = selectedRecording.endTimeMs,
    initialTimeMs = initialTimeMs,
)
swift
let replay = store.createReplay()
let videoOutput = TiStoreVideoOutput()
let audioOutput = TiStoreAudioOutput()

replay.onError = { _ in /* 处理来源错误 */ }
replay.onCompleted = { /* 播放范围自然结束 */ }

videoOutput.attachView(videoView)
videoOutput.attach(replay: replay, channelId: videoChannelId)
audioOutput.attach(replay: replay, channelId: audioChannelId)
replay.play(
    startTimeMs: selectedRecording.startTimeMs,
    endTimeMs: selectedRecording.endTimeMs,
    initialTimeMs: NSNumber(value: initialTimeMs)
)
typescript
const replay = store.createReplay();
const videoOutput = new TiStoreVideoOutput();
const audioOutput = new TiStoreAudioOutput();

replay.onError = (code) => { /* 处理来源错误 */ };
replay.onCompleted = () => { /* 播放范围自然结束 */ };

videoOutput.attach(replay, videoChannelId);
audioOutput.attach(replay, audioChannelId);
replay.play(
  selectedRecording.startTimeMs,
  selectedRecording.endTimeMs,
  initialTimeMs,
);

@Builder
function StorePlaybackVideo(): void {
  TiStoreVideoOutputView({output: videoOutput, fit: TiStoreVideoFit.contain});
}
tsx
const replay = store.createReplay();
const videoOutput = new TiStoreVideoOutput();
const audioOutput = new TiStoreAudioOutput();

replay.onError = (code) => { /* 处理来源错误 */ };
replay.onCompleted = () => { /* 播放范围自然结束 */ };

videoOutput.attach(replay, videoChannelId);
audioOutput.attach(replay, audioChannelId);
replay.play(
  selectedRecording.startTimeMs,
  selectedRecording.endTimeMs,
  initialTimeMs,
);

function StorePlaybackVideo(): ReactElement {
  return videoOutput.view({style: styles.video});
}
typescript
// 主进程;browserWindow 和 bounds 由应用提供。
const replay = store.createReplay();
const videoOutput = new TiStoreVideoOutput();
const audioOutput = new TiStoreAudioOutput();
const videoView = new TiVideoView(browserWindow, bounds);

replay.onError = (code) => { /* 处理来源错误 */ };
replay.onCompleted = () => { /* 播放范围自然结束 */ };

videoOutput.mount(videoView);
videoOutput.attach(replay, videoChannelId);
audioOutput.attach(replay, audioChannelId);
replay.play(
  selectedRecording.startTimeMs,
  selectedRecording.endTimeMs,
  initialTimeMs,
);

播放范围自然结束时触发 Replay 的完成回调。Token、权限、网络或录像读取错误由 Replay 返回。解码、播放或渲染错误由对应 Output 返回。

attachplay 同步返回失败时,本次操作没有开始。先按错误码修正 Channel、时间范围或对象状态。已经开始播放后,如果 Replay 报告 Token 过期,更新 Token 并重新调用 play。录像不可读时不要持续重试同一段录像。网络或服务暂时不可用时,可以采用有上限的退避重试。

控制播放

Seek 使用当前播放范围内的绝对 UTC 时间。它是同步控制命令,不返回独立完成回调。定位生效后,Replay 的当前位置和时间变化通知会更新。

dart
replay.pause();
replay.resume();

replay.seek(targetTimeMs);
replay.setSpeed(TiStoreReplaySpeed.x2);
audioOutput.setVolume(0); // 静音

replay.stop();
kotlin
replay.pause()
replay.resume()

replay.seek(targetTimeMs)
replay.setSpeed(TiStoreReplaySpeed.X2)
audioOutput.setVolume(0) // 静音

replay.stop()
swift
replay.pause()
replay.resume()

replay.seek(toTimeMs: targetTimeMs)
replay.setSpeed(.x2)
audioOutput.setVolume(0) // 静音

replay.stop()
typescript
replay.pause();
replay.resume();

replay.seek(targetTimeMs);
replay.setSpeed(TiStoreReplaySpeed.x2);
audioOutput.setVolume(0); // 静音

replay.stop();
typescript
replay.pause();
replay.resume();

replay.seek(targetTimeMs);
replay.setSpeed(TiStoreReplaySpeed.x2);
audioOutput.setVolume(0); // 静音

replay.stop();
typescript
replay.pause();
replay.resume();

replay.seek(targetTimeMs);
replay.setSpeed(TiStoreReplaySpeed.x2);
audioOutput.setVolume(0); // 静音

replay.stop();

Seek 只能在当前播放仍在运行或暂停时调用,目标必须位于当前播放范围。越界时同步返回错误,当前播放不受影响。

目标落在录像空洞中时,Replay 会定位到其后的第一个可播放位置。直到范围末尾都没有录像时按自然结束处理。播放已经结束时,从指定位置重新调用 play

setVolume(0) 表示静音,恢复到 100 即可取消静音。支持 1x2x4x8x;非 1x 倍速下,Audio Output 自动静音。应用切换到另一段录像时,直接在同一个 Replay 上再次调用 play;已有 Output 绑定、音量和倍速会保留。

截取当前画面

Video Output 出画后,可以把当前画面保存为 JPEG。截图操作(Snapshot)不包含字幕、按钮等页面 UI。

dart
final result = await videoOutput.takeSnapshot();
if (!result.success || result.data == null) return;
final snapshot = result.data!;
kotlin
lifecycleScope.launch {
    val result = videoOutput.takeSnapshot()
    if (result.code != TiStoreErrorCode.OK || result.file == null) return@launch
    val snapshot = result.file
}
swift
let result = await videoOutput.takeSnapshot()
guard result.code == TiStoreErrorCode.ok,
      let snapshot = result.file else { return }
typescript
const result = await videoOutput.takeSnapshot();
if (!result.success || result.data === null) return;
const snapshot = result.data;
typescript
const result = await videoOutput.takeSnapshot();
if (!result.success || result.data === null) return;
const snapshot = result.data;
typescript
const result = await videoOutput.takeSnapshot();
if (!result.success || result.data === null) return;
const snapshot = result.data;

Video Output 尚未收到画面时,截图返回 no-frame;等待首帧后再试。截图文件写入失败时,先检查缓存目录和磁盘空间。

MP4 和 JPEG 都先生成在 SDK 私有 cache。Flutter 与 React Native 可以对返回的文件对象调用 moveToGallery(),并为保存的 MP4 或 JPEG 指定文件名。Android、iOS / macOS、HarmonyOS 与 Electron 由应用复制或移动普通文件,并在目标文件操作中设置名称。

直接读取、上传或分享完成后调用 delete()。需要长期保存时及时移动,不能把 cache 路径当作持久地址。

dart
Future<void> saveFiles(
  TiStoreRecordingFile recordingFile,
  TiStoreSnapshotFile snapshotFile,
) async {
  final recordingSave = await recordingFile.moveToGallery(
    fileName: '客厅摄像头-2026-08-20-14-30-05.mp4',
  );
  if (!recordingSave.success || recordingSave.data == null) return;
  final recordingAsset = recordingSave.data!;

  final snapshotSave = await snapshotFile.moveToGallery(
    fileName: '客厅摄像头-2026-08-20-14-30-05.jpg',
  );
  if (!snapshotSave.success || snapshotSave.data == null) return;
  final snapshotAsset = snapshotSave.data!;

  // 把 recordingAsset.uri 和 snapshotAsset.uri 交给应用展示或持久化。
}
typescript
import type {
  TiStoreRecordingFile,
  TiStoreSnapshotFile,
} from 'tirtc-react-native';

async function saveFiles(
  recordingFile: TiStoreRecordingFile,
  snapshotFile: TiStoreSnapshotFile,
): Promise<void> {
  const recordingSave = await recordingFile.moveToGallery(
    '客厅摄像头-2026-08-20-14-30-05.mp4',
  );
  if (!recordingSave.success || recordingSave.data === null) return;

  const snapshotSave = await snapshotFile.moveToGallery(
    '客厅摄像头-2026-08-20-14-30-05.jpg',
  );
  if (!snapshotSave.success || snapshotSave.data === null) return;

  // 把两个 data.uri 交给应用展示或持久化。
}

上例的两个参数分别来自录像任务和截图接口的成功结果。文件名可省略扩展名,SDK 会按文件类型补上 .mp4.jpg。如果传入扩展名,只接受对应类型;非法名称返回 6000,并保留 cache 源文件。省略文件名时,SDK 使用 Unix 毫秒时间戳生成名称。

下载录像

云存储支持直接下载指定时间段,也可以把正在播放的片段保存为 MP4。两种方式都会在 SDK 私有 cache 中生成本地文件。

下载指定时间段

不需要先播放时,通过同一个设备实例创建 ExportTask。exportStartTimeMsexportEndTimeMs 来自选中的录像时间段,也可以是其中一个更小的范围。调用时再传入需要保存的 Channel。

dart
final start = store.exportRecording(
  startTimeMs: exportStartTimeMs,
  endTimeMs: exportEndTimeMs,
  videoChannelId: videoChannelId,
  audioChannelId: audioChannelId,
  onProgress: (progress) => exportProgress = progress,
);
if (!start.success || start.data == null) return;
final task = start.data!;

final completed = await task.result;
if (!completed.success || completed.data == null) return;
final file = completed.data!;
showSavedFile(file.path);
kotlin
val start = store.exportRecording(
    TiStoreExportRequest(
        startTimeMs = exportStartTimeMs,
        endTimeMs = exportEndTimeMs,
        videoChannelId = videoChannelId,
        audioChannelId = audioChannelId,
    ),
    TiStoreExportProgressListener { exportProgress = it },
) { result ->
    if (result.code == TiStoreErrorCode.OK && result.file != null) {
        showSavedFile(result.file.path)
    }
}
if (start.code != TiStoreErrorCode.OK || start.task == null) return
val task = start.task
swift
let start = store.exportRecording(
    TiStoreExportRequest(
        startTimeMs: exportStartTimeMs,
        endTimeMs: exportEndTimeMs,
        videoChannelId: videoChannelId,
        audioChannelId: NSNumber(value: audioChannelId)
    ),
    progress: { exportProgress = $0 },
    completion: { result in
        guard result.code == TiStoreErrorCode.ok,
              let file = result.file else { return }
        showSavedFile(file.path)
    }
)
guard start.code == TiStoreErrorCode.ok, let task = start.task else { return }
typescript
const start = store.exportRecording({
  startTimeMs: exportStartTimeMs,
  endTimeMs: exportEndTimeMs,
  videoChannelId,
  audioChannelId,
}, (progress) => {
  exportProgress = progress;
});
if (!start.success || start.data === null) return;
const task = start.data;

const completed = await task.result;
if (!completed.success || completed.data === null) return;
const file = completed.data;
showSavedFile(file.path);
typescript
const start = store.exportRecording({
  startTimeMs: exportStartTimeMs,
  endTimeMs: exportEndTimeMs,
  videoChannelId,
  audioChannelId,
}, (progress) => {
  exportProgress = progress;
});
if (!start.success || start.data === null) return;
const task = start.data;

const completed = await task.result;
if (!completed.success || completed.data === null) return;
const file = completed.data;
showSavedFile(file.path);
typescript
const start = store.exportRecording({
  startTimeMs: exportStartTimeMs,
  endTimeMs: exportEndTimeMs,
  videoChannelId,
  audioChannelId,
}, (progress) => {
  exportProgress = progress;
});
if (!start.success || start.data === null) return;
const task = start.data;

const completed = await task.result;
if (!completed.success || completed.data === null) return;
const file = completed.data;
showSavedFile(file.path);

ExportTask 会自然处理完整个范围。需要提前结束下载时,可以调用 Task 的 stop。这会清理临时文件,不会把尚未完成的范围保存成部分 MP4。

任务创建成功只表示导出已经开始。必须等待 result、completion 或 Wait 返回成功结果后再使用文件。进度达到 100% 也不能替代最终结果。Token 过期时,更新同一设备的 Token 并重新创建任务。权限不足或录像不可读时不要持续重试原请求。

保存正在播放的片段

开始保存时,从当前 Replay 创建 RecordingTask。结束保存时调用 Task 的 stop。成功结果包含 MP4 路径和实际媒体时长。

dart
final start = replay.startRecording(
  videoChannelId: videoChannelId,
  audioChannelId: audioChannelId,
);
if (!start.success || start.data == null) return;
final task = start.data!;

final completed = await task.stop();
if (!completed.success || completed.data == null) return;
final file = completed.data!;
showSavedFile(file.path);
kotlin
val start = replay.startRecording(
    videoChannelId = videoChannelId,
    audioChannelId = audioChannelId,
)
if (start.code != TiStoreErrorCode.OK || start.task == null) return
val task = start.task

lifecycleScope.launch {
    val completed = task.stop()
    if (completed.code != TiStoreErrorCode.OK || completed.file == null) return@launch
    val file = completed.file
    showSavedFile(file.path)
}
swift
let start = replay.startRecording(
    videoChannelId: videoChannelId,
    audioChannelId: NSNumber(value: audioChannelId)
)
guard start.code == TiStoreErrorCode.ok, let task = start.task else { return }

let completed = await task.stop()
guard completed.code == TiStoreErrorCode.ok,
      let file = completed.file else { return }
showSavedFile(file.path)
typescript
const start = replay.startRecording({
  videoChannelId,
  audioChannelId,
});
if (!start.success || start.data === null) return;
const task = start.data;

const completed = await task.stop();
if (!completed.success || completed.data === null) return;
const file = completed.data;
showSavedFile(file.path);
typescript
const start = replay.startRecording({
  videoChannelId,
  audioChannelId,
});
if (!start.success || start.data === null) return;
const task = start.data;

const completed = await task.stop();
if (!completed.success || completed.data === null) return;
const file = completed.data;
showSavedFile(file.path);
typescript
const start = replay.startRecording({
  videoChannelId,
  audioChannelId,
});
if (!start.success || start.data === null) return;
const task = start.data;

const completed = await task.stop();
if (!completed.success || completed.data === null) return;
const file = completed.data;
showSavedFile(file.path);

RecordingTask 保存 Replay 交付的媒体,不包含页面 UI。Replay 自然结束时 Task 会自动完成;之后调用 stop 仍会返回同一文件结果。

startRecording 失败时不会创建 Task。stop 返回成功并交付文件后,MP4 才可以读取或展示。文件写入失败时,检查缓存目录权限、磁盘剩余空间和本地写入性能,再重新保存。

一个 Task 只保存一个视频 Channel。两个摄像头分别启动两个 Task,SDK 会生成两个独立文件;需要时可以给两个 Task 传入同一个音频 Channel。

确认播放和下载结果

释放页面资源前,确认本次操作已经得到预期结果:

  • 查询返回至少一个录像时间段;
  • 开始播放后收到音频帧或看到视频首帧;
  • 暂停、继续和 Seek 后,状态与画面符合预期;
  • 下载任务返回可读取的 MP4,截图返回可读取的 JPEG。

若任一步失败,记录平台、SDK 版本、目标 device_id、时间范围和公开错误码,不要记录 Token。

释放资源

离开播放页面并准备释放页面资源时,先结束页面持有且不再继续的 RecordingTask 或 ExportTask。等待已经接受的 Snapshot 后停止播放。使用 Video Output View 的平台先移除 View,然后解除并释放 Output。

播放页持有 store 且离开后不再访问这台设备时,一并释放它。如果应用在更高层复用同一个 store,等不再访问该设备时再释放。

dart
replay.stop();
videoOutput.detach();
audioOutput.detach();
videoOutput.dispose();
audioOutput.dispose();
replay.dispose();
store.dispose();
kotlin
replay.stop()
videoOutput.detachView()
videoOutput.detach()
audioOutput.detach()
videoOutput.dispose()
audioOutput.dispose()
replay.dispose()
store.dispose()
swift
replay.stop()
videoOutput.detachView()
videoOutput.detach()
audioOutput.detach()
videoOutput.dispose()
audioOutput.dispose()
replay.dispose()
store.dispose()
typescript
replay.stop();
videoOutput.detach();
audioOutput.detach();
videoOutput.dispose();
audioOutput.dispose();
replay.dispose();
store.dispose();
typescript
replay.stop();
videoOutput.detach();
audioOutput.detach();
videoOutput.dispose();
audioOutput.dispose();
replay.dispose();
store.dispose();
typescript
replay.stop();
videoOutput.unmount();
videoOutput.detach();
audioOutput.detach();
videoOutput.dispose();
audioOutput.dispose();
replay.dispose();
videoView.dispose();
store.dispose();

释放失败时,对象仍然有效。等待任务和回调结束、解除绑定或移除 View 后,再次释放即可。离开页面后,确认没有仍被占用的 Replay、Output、Task 或 Store。

TiStore 开发文档