Skip to content

Electron API 说明

Electron 通过 @tange-ai/tirtc-electron 查询、回放、截图和导出云录像。

安装和初始化见 Electron SDK 接入。查询、播放、下载和截图见播放录像

运行环境与进程边界

支持 darwin/arm64win32/x64。两个 native payload 随同一个 @tange-ai/tirtc-electron npm 包交付,安装和运行时不再下载 Runtime。公开对象只在 Electron 主进程创建;Renderer 通过应用自己定义的窄 preload bridge 调用,不直接 import addon,也不启用 nodeIntegration

package 默认使用以下 Core 目录:

  • cache:app.getPath('userData')/tirtc/cache
  • 日志:app.getPath('logs')/tirtc

应用若通过 Electron 自身入口修改根目录,必须在调用 TiStore.init 前完成。目录不可创建或写入时,init 返回 TISTORE_ERROR_IO_FAILED,不会回退到工作目录或临时目录。

同步命令返回 number 错误码,TISTORE_ERROR_OK 表示成功。List、Snapshot 和文件 Task 通过 Promise 返回一次最终结果。Promise resolve SDK 结果,不以 reject 表示网络、Token、媒体、文件或底层资源创建错误。

公开 API 不导出 SDK 专用 Exception。JavaScript 类型或对象结构不符合声明时抛 TypeError;类型正确但内容为空、数值越界、状态不允许或 SDK 执行失败时,返回对应错误码或结果。package 或 addon 无法加载时,模块加载抛普通 Error,错误信息包含平台、架构和无法加载的 payload。

package 成功加载后,TiStore、Replay、Output 和 TiVideoView 的构造器只校验 JavaScript 类型和对象结构,并创建对应对象。底层资源由首次需要它的具体操作创建。参数无效或资源创建失败时,由该操作返回错误码或失败结果。应用 callback 自己抛出的异常沿 JavaScript 调用链传播,不转换为 SDK 错误。

安装与导入

bash
npm install @tange-ai/tirtc-electron@2.4.0-alpha.2
typescript
import {
  TiStore,
  TiStoreReplay,
  TiStoreAudioOutput,
  TiStoreVideoOutput,
  TiVideoView,
  TISTORE_ERROR_OK,
} from '@tange-ai/tirtc-electron';

基础类型

typescript
export type Resp<T> = Readonly<{
  success: boolean;
  code: number | null;
  message: string | null;
  data: T | null;
}>;

export type TiStoreRecordingRange = Readonly<{
  startTimeMs: number;
  endTimeMs: number;
}>;

export type TiStoreRecordingRangesResult = Readonly<{
  code: number;
  recordings: ReadonlyArray<TiStoreRecordingRange>;
}>;

export type TiStoreRecordingDay = Readonly<{
  date: string;
  hasRecording: boolean;
}>;

export type TiStoreRecordingDaysResult = Readonly<{
  code: number;
  days: ReadonlyArray<TiStoreRecordingDay>;
}>;

export const TiStoreReplaySpeed = {
  x1: 'x1',
  x2: 'x2',
  x4: 'x4',
  x8: 'x8',
} as const;
export type TiStoreReplaySpeed =
  (typeof TiStoreReplaySpeed)[keyof typeof TiStoreReplaySpeed];

最小回放示例

这段代码运行在 Electron 主进程。selectedRecording 来自 listRecordings;应用可以经由自己的 preload bridge 把必要的只读状态投影给 Renderer。

typescript
const initCode = TiStore.init({appId});
if (initCode !== TISTORE_ERROR_OK) return;

const store = new TiStore(token);
const replay = store.createReplay();
const videoOutput = new TiStoreVideoOutput();
const audioOutput = new TiStoreAudioOutput();
const videoView = new TiVideoView(browserWindow, bounds);
replay.onCompleted = showReplayCompleted;
replay.onError = showReplayError;
videoOutput.onError = showVideoOutputError;
audioOutput.onError = showAudioOutputError;

function requireOk(code: number): void {
  if (code !== TISTORE_ERROR_OK) {
    throw new Error(TiStore.errorToString(code));
  }
}

requireOk(videoOutput.mount(videoView));
requireOk(videoOutput.attach(replay, videoChannelId));
requireOk(audioOutput.attach(replay, audioChannelId));

requireOk(replay.play(
  selectedRecording.startTimeMs,
  selectedRecording.endTimeMs,
));

mountattachplay 的同步返回值表示当前调用是否被接受。调用被接受后,Token、网络、权限或录像读取错误由 Replay 的 onError 返回;解码、播放或渲染错误由对应 Output 的 onError 返回。

页面或窗口退出时先 stop Replay,再 detach、unmount 和 dispose Output,随后 dispose Replay 与 View。不再访问这台设备时 dispose store

TiStore

typescript
export class TiStore {
  constructor(token: string);

  static init(options: Readonly<{
    appId: string;
    endpoint?: string;
    consoleLogEnabled?: boolean;
  }>): number;

  static errorToString(code: number): string;

  onTokenExpired: (() => void) | null;

  updateToken(token: string): number;
  listRecordingDays(
    startDate: string,
    endDate: string,
    timeZoneId?: string,
  ): Promise<TiStoreRecordingDaysResult>;
  listRecordings(
    startTimeMs: number,
    endTimeMs: number,
  ): Promise<TiStoreRecordingRangesResult>;
  createReplay(): TiStoreReplay;
  exportRecording(
    options: TiStoreExportOptions,
    onProgress?: (progress: number) => void,
  ): Resp<TiStoreExportTask>;
  dispose(): number;
}

使用相同配置重复 init 成功,使用不同配置返回 already-initializedendpoint 只用于显式自定义服务地址;完整 Token 不属于初始化配置。初始化后用一台设备的非空 Token 创建 TiStore 实例;构造不发起网络请求。同时访问多台设备时创建多个实例。

listRecordingDays 的起止日期都使用严格的 YYYY-MM-DD 格式并包含在结果中。timeZoneId 使用 IANA ID,省略时默认为 Asia/Shanghai。单次最多查询 31 天;成功结果按日期升序返回范围内每一天,包括 hasRecording == false 的日期。范围超过上限时 resolve range-too-large

listRecordings 的时间参数使用 UTC Unix 毫秒,并且必须是 JavaScript 安全整数。结果按 startTimeMs 升序排列并裁剪在请求的 [startTimeMs, endTimeMs) 内;重叠或首尾相接的时间段会合并,任何大于 0 的空洞都保留为不同项。结果是设备级完整快照,不保证每个 Channel 都有媒体,也不分页。单次跨度最长 10 天,合并后最多返回 10000 项;超过任一上限时整体 resolve range-too-large,不返回部分结果;成功但没有录像时返回空数组。

onTokenExpired 只在实例当前 Token 被云端明确判定为过期时通知,同一个 Token 至多一次。触发过期的 List、Play 或 Export 先通过自身结果/回调报告错误;Token 无效或权限不足不触发该回调。当前 Token 已经确定过期时,新操作直接返回 token-expiredupdateToken 保存不同的非空 Token,之后启动的操作使用新 Token;相同 Token 不清除过期状态,已经开始的操作也不自动换证或重试。它只用于同一设备续签,访问另一台设备时创建新的 TiStore。仍有查询、Replay、ExportTask 或回调正在执行时,dispose() 返回 in-use

TiStoreReplay

typescript
export class TiStoreReplay {
  readonly speed: TiStoreReplaySpeed;
  readonly currentTimeMs: number | null;
  onTimeChanged: ((timeMs: number) => void) | null;
  onError: ((code: number) => void) | null;
  onCompleted: (() => void) | null;

  private constructor();

  play(
    startTimeMs: number,
    endTimeMs: number,
    initialTimeMs?: number,
  ): number;
  pause(): number;
  resume(): number;
  seek(timeMs: number): number;
  setSpeed(speed: TiStoreReplaySpeed): number;
  startRecording(
    options: TiStoreStartRecordingOptions,
  ): Resp<TiStoreRecordingTask>;
  stop(): number;
  dispose(): number;
}

Replay 只能由 store.createReplay() 创建,并固定属于该设备实例。playseekcurrentTimeMs 都使用 UTC Unix 毫秒,传入值必须是 JavaScript 安全整数。play 前至少绑定一个 Output。接受 Play 时固定使用实例当前的 Token,之后更新 Token 不改变本次回放。initialTimeMs 省略时从 startTimeMs 开始;传入时必须位于 [startTimeMs, endTimeMs)。播放范围或初始位置无效时同步返回 invalid-argument,原回放不受影响。成功接受后从初始位置建立新回放,不会先输出范围开头的媒体;初始位置落在空洞中时从其后的第一段录像开始,其后没有录像时按自然结束处理。非 x1 倍速时 Audio Output 静音。

切换录像时在同一个 Replay 上再次调用 play。新请求被接受后替换旧回放,并保留 Output binding、Audio Output 音量和倍速;同步拒绝时原回放不受影响。

seek 只能在当前 play 仍在运行或暂停时调用,目标必须位于当前播放范围;回放未活动或目标越界时,分别同步返回 not-startedinvalid-argument。自然完成后要从指定位置重新播放时,调用带 initialTimeMsplay。连续 Seek 采用 latest-wins,并保留倍速和暂停意图。

定位生效后,currentTimeMsonTimeChanged 更新为第一个不早于目标的可播放位置。目标合法但其后没有录像时,位置推进到 endTimeMs,并按自然结束触发 onCompletedonError 只承接已接受回放中的 Token、网络、权限或录像读取错误,并且一次 play 最多通知一次。

每个被接受的 play 仅在其时间范围自然耗尽时调用一次 onCompleted;同步拒绝、主动 stop、被新 play 替换或错误终止均不触发,晚设置也不补发。onCompleted 不取代 Output 的 completed,后者只表示对应 Output 已排空。自然完成会自动结束活动 RecordingTask。

最后一个 Output 离开且没有活动 RecordingTask 时,Replay 停止读取来源,但不触发 onCompleted

Audio/Video Output

typescript
import type {BrowserWindow, Rectangle} from 'electron';

export type TiStoreAudioOutputState =
  | 'idle' | 'buffering' | 'playing' | 'paused' | 'completed' | 'failed';
export type TiStoreVideoOutputState =
  | 'idle' | 'buffering' | 'rendering' | 'paused' | 'completed' | 'failed';

export class TiVideoView {
  constructor(window: BrowserWindow, bounds: Rectangle);
  setBounds(bounds: Rectangle): number;
  dispose(): number;
}

export class TiStoreAudioOutput {
  readonly state: TiStoreAudioOutputState;
  onStateChanged: ((state: TiStoreAudioOutputState) => void) | null;
  onError: ((code: number) => void) | null;

  attach(replay: TiStoreReplay, channelId: number): number;
  setVolume(volumePercent: number): number;
  detach(): number;
  dispose(): number;
}

export class TiStoreVideoOutput {
  readonly state: TiStoreVideoOutputState;
  onStateChanged: ((state: TiStoreVideoOutputState) => void) | null;
  onError: ((code: number) => void) | null;

  mount(view: TiVideoView): number;
  unmount(): number;
  attach(replay: TiStoreReplay, channelId: number): number;
  takeSnapshot(): Promise<Resp<TiStoreSnapshotFile>>;
  detach(): number;
  dispose(): number;
}

attach 使用设备写入录像时的 channelId,取值为 0..255;音频和视频可以使用相同 ID。setVolume 接收 0..100 的安全整数;0 表示静音。它只调整当前 Audio Output,不修改系统全局音量。新一次 play 会保留该值。

TiVideoView 是跨平台视频承载对象;HWNDNSView、native pointer 和 addon object ID 不属于公开 API。构造后、第一次 mount 前调用 setBounds,只会更新待使用的 bounds 并返回成功;第一次 mount 才创建平台 host。此时窗口已经销毁则 mount 返回 invalid-argument,不留下部分绑定。

View、BrowserWindow 或关联 WebContents 销毁时,必须确定性解除 mount。仍被 Output mount 时,View 的 dispose 返回 in-use,可在 unmount 后重试。成功 dispose 后重复调用返回 TISTORE_ERROR_OKsetBounds 返回 TISTORE_ERROR_NOT_INITIALIZED,不会重新创建平台 host。

Snapshot 保存当前绑定和本次播放中最近一次成功呈现、且仍可读取的画面,不包含字幕、按钮等页面 UI。成功时 data.path 是 SDK 私有 cache 中的临时 JPEG,直接使用完成后调用文件对象的 delete()。当前绑定和本次播放尚未成功出画时返回 no-frame;暂停保留当前画面,自然播放完成后只要最终画面仍在就可以截图。重新 attach、开始新一次 play、detach、unmount 或清空画面后,必须等新画面成功呈现才能再次截图。同一个 Video Output 同时只接受一个 Snapshot;不同 Output 仍受全局资源上限约束。Snapshot 完成前,dispose() 返回 in-use,Output 及其绑定关系保持不变。

Output attach、mount 与 detach 相互独立。重复 attach 同一个 Replay 和 channelId 会成功;已经绑定时传入不同 Replay 或 channelId 返回 in-use 并保留原关系,必须先显式 detach。dispose 不隐式解除关系,Replay 绑定、渲染面或活动 Snapshot 仍存在时返回 in-use。某个 Output 的解码或渲染错误只终止该 Output;Replay 的 Token、网络或录像读取错误使本次回放的全部 Output 进入 failed

本地媒体文件

typescript
export interface TiStoreRecordingFile {
  readonly path: string;
  readonly durationMs: number;
  delete(): Promise<TiRtcError>;
}

export interface TiStoreSnapshotFile {
  readonly path: string;
  delete(): Promise<TiRtcError>;
}

两个文件对象的 path 在成功返回时指向 SDK 私有 cache。桌面应用可以用 Node fs 复制或移动到持久位置,直接使用完成后调用受限、幂等的 delete()

TiStoreRecordingTask

typescript
export type TiStoreStartRecordingOptions = Readonly<{
  videoChannelId: number;
  audioChannelId?: number;
}>;

export class TiStoreRecordingTask {
  stop(): Promise<Resp<TiStoreRecordingFile>>;
}

TiStoreReplay.startRecording(options) 同步返回 Resp<TiStoreRecordingTask>。Replay 必须正在回放;videoChannelId 必填,audioChannelId 可空,两者都必须是 0..255 的安全整数,也可以使用相同值。同步失败不创建 Task。

stop() 停止接收新媒体,排空已接收数据并完成 MP4。第一次调用开始终结;重复调用返回同一个 Promise。Replay 自然结束时 Task 自动完成,随后调用 stop() 返回缓存结果。

Task 不公开 state、实时 duration、cancel 或 dispose。相同 Channel 可以创建多个 Task,SDK 为每个 Task 生成不同文件;两个视频 Channel 使用两个 Task。

TiStoreExportTask

typescript
export type TiStoreExportOptions = Readonly<{
  startTimeMs: number;
  endTimeMs: number;
  videoChannelId: number;
  audioChannelId?: number;
}>;

export class TiStoreExportTask {
  readonly progress: number;
  readonly result: Promise<Resp<TiStoreRecordingFile>>;

  stop(): Promise<Resp<TiStoreRecordingFile>>;
}

store.exportRecording(options, onProgress) 在启动 Task 前固定实例当前的 Token,并保存可选的进度回调,然后同步返回 Resp<TiStoreExportTask>。成功只表示 Task 已经开始。

startTimeMsendTimeMs 使用 UTC Unix 毫秒安全整数,查询范围为 [startTimeMs, endTimeMs)startTimeMs 必须早于 endTimeMsvideoChannelId 必填,audioChannelId 可空;两者都必须是 0..255 的安全整数,也可以使用相同值。

同步失败不创建 Task,也不调用回调。回调不会在 exportRecording 返回前触发。

progress 位于 0.0..1.0 且单调不回退,只有 MP4 完整生成后才到达 1.0。中间通知可以合并,progress 始终返回当前快照。活动 Task 调用 stop() 会清理临时文件并以 stopped 返回,不生成部分 MP4。自然完成或失败后调用 stop() 返回缓存的同一结果。重复调用返回同一个 Promise。

Task 完成后自动释放任务使用的底层资源,不提供 dispose。不同 Task 相互独立并生成不同 cache 文件;资源达到上限返回 resource-exhausted

错误与生命周期

业务通常需要直接判断这些公开常量:

typescript
export const TISTORE_ERROR_OK: 0;
export const TISTORE_ERROR_INVALID_ARGUMENT: 6000;
export const TISTORE_ERROR_NOT_INITIALIZED: 6001;
export const TISTORE_ERROR_TOKEN_EXPIRED: 6014;
export const TISTORE_ERROR_ALREADY_INITIALIZED: 6022;
export const TISTORE_ERROR_PERMISSION_DENIED: 6024;
export const TISTORE_ERROR_IN_USE: 6026;
export const TISTORE_ERROR_NOT_STARTED: 6027;
export const TISTORE_ERROR_NOT_BOUND: 6029;
export const TISTORE_ERROR_NOT_CONFIGURED: 6030;
export const TISTORE_ERROR_RESOURCE_EXHAUSTED: 6043;
export const TISTORE_ERROR_FILE_WRITE_FAILED: 6046;
export const TISTORE_ERROR_UNSUPPORTED_FORMAT: 6113;
export const TISTORE_ERROR_IO_FAILED: 6114;
export const TISTORE_ERROR_CANCELLED: 6115;
export const TISTORE_ERROR_RANGE_TOO_LARGE: 6117;
export const TISTORE_ERROR_NO_FRAME: 6118;
export const TISTORE_ERROR_NO_RECORDABLE_MEDIA: 6119;
export const TISTORE_ERROR_RECORDING_OVERRUN: 6120;
export const TISTORE_ERROR_RECORDING_UNREADABLE: 6122;
export const TISTORE_ERROR_STORE_UNAVAILABLE: 6123;
export const TISTORE_ERROR_STOPPED: 6124;
错误码常量建议处理
6000TISTORE_ERROR_INVALID_ARGUMENT修正缺失、越界或无效时间范围后重试
6001TISTORE_ERROR_NOT_INITIALIZED先调用 TiStore.init
6014TISTORE_ERROR_TOKEN_EXPIRED为同一设备更新 Token 后重新发起操作
6022TISTORE_ERROR_ALREADY_INITIALIZED保持初始化配置一致;需要更换配置时,重启应用后再初始化
6024TISTORE_ERROR_PERMISSION_DENIED检查当前用户、设备和 Token 的授权关系
6026TISTORE_ERROR_IN_USE完成活动操作或显式解除绑定后重试
6027TISTORE_ERROR_NOT_STARTED先开始对应 Replay
6029TISTORE_ERROR_NOT_BOUND需要绑定的操作先完成绑定;解除未绑定的对象时,按当前状态结束清理
6030TISTORE_ERROR_NOT_CONFIGURED补齐目标或媒体通道配置
6043TISTORE_ERROR_RESOURCE_EXHAUSTED释放不再使用的任务和媒体资源后重试
6046TISTORE_ERROR_FILE_WRITE_FAILED检查缓存目录权限和磁盘空间
6113TISTORE_ERROR_UNSUPPORTED_FORMAT提示当前录像无法播放或导出
6114TISTORE_ERROR_IO_FAILED检查缓存目录和日志目录是否可创建、可写
6115TISTORE_ERROR_CANCELLED结束对应 List 等待
6117TISTORE_ERROR_RANGE_TOO_LARGE日期查询超过 31 天,或时间段查询超过 10 天或合并结果超过 10000 项;缩短查询范围后重试
6118TISTORE_ERROR_NO_FRAME等待 Video Output 出画后重新截图
6119TISTORE_ERROR_NO_RECORDABLE_MEDIA重新选择录像范围或 Channel ID
6120TISTORE_ERROR_RECORDING_OVERRUN检查本地写入性能并重新保存
6122TISTORE_ERROR_RECORDING_UNREADABLE文件缺失或损坏时停止重试;其他情况检查设备上传的格式与帧要求
6123TISTORE_ERROR_STORE_UNAVAILABLE网络或服务暂时不可用,稍后重试
6124TISTORE_ERROR_STOPPED活动 ExportTask 已主动停止,不使用输出文件

range-too-large 只用于 List:日期查询超过 31 天,或时间段查询超过 10 天或合并结果超过 10000 项。Replay 和 Export 对合法范围在内部自动分窗。

不要解析 errorToString、异常文本或 Runtime 日志驱动业务逻辑。对象的事件按产生顺序在 Electron 主线程串行投递。

dispose 会检查对象是否仍在使用,并允许在清理后重试。Replay 仍在播放、仍有 Output 绑定或活动 RecordingTask 时返回 in-use。Output 仍绑定 Replay、仍 mount View 或仍有活动 Snapshot 时也返回 in-use。失败时对象及关系保持不变。资源真正释放后才返回 TISTORE_ERROR_OK,重复调用也返回成功。MP4 Task 完成后自动释放任务使用的底层资源,不需要额外 dispose。

Renderer bridge 只转发页面实际需要的命令和结构化状态,不暴露通用 invoke、任意 native method、完整 Token、文件系统任意访问或全局事件总线。

TiStore 开发文档