Skip to content

上传录像

设备取得身份和短期 device_access_token 后,通过 C SDK 创建上传请求并写入已经编码的音视频帧。请求正常结束时,on_result 回调返回唯一的最终结果。

音视频格式与帧要求

C SDK 接收已经编码的完整帧。写入前先确认格式是客户端能够播放的。

选择客户端能够播放的格式

类型支持的格式C SDK 媒体常量
视频H.264TISTORE_VIDEO_H264
视频H.265TISTORE_VIDEO_H265
视频JPEG 帧(作为 MJPEG 视频播放)TISTORE_VIDEO_JPEG
音频16 位 PCMTISTORE_AUDIO_PCM
音频G.711 A-lawTISTORE_AUDIO_ALAW
音频AACTISTORE_AUDIO_AAC
音频OpusTISTORE_AUDIO_OPUS

TISTORE_AUDIO_ULAWTISTORE_AUDIO_G726 虽然是 C SDK 已定义的媒体编号,但客户端 SDK 无法播放这两种音频。TiStoreQueueWriteFrame() 返回成功只表示队列接收了媒体编号和帧数据,不表示客户端能够解码。

组织视频帧

  • 每次调用 TiStoreQueueWriteFrame() 写入一帧完整的编码数据,不要把一帧拆成多次调用。
  • H.264 和 H.265 使用带起始码的 Annex B 码流。H.264 关键帧应包含 SPS、PPS 和 IDR;H.265 关键帧应包含 VPS、SPS、PPS 和可独立解码的帧。
  • H.264 和 H.265 的关键帧必须携带完整参数集,并把 flags 设为 TISTORE_FRAME_FLAG_KEY_FRAME。C SDK 会从关键帧开始上传,并自动完成后续分片;设备应用不需要管理分片边界。
  • JPEG 写入完整的 JPEG/JFIF 图片,从 FF D8 开始、以 FF D9 结束;每帧都设置 TISTORE_FRAME_FLAG_KEY_FRAME
  • 同一通道不要在录像过程中切换视频编码格式。

组织音频帧

格式每次写入的数据
PCMS16LE(16 位小端有符号整数)交错采样数据,不包含 WAV 文件头
G.711 A-law一包原始 A-law 数据,不包含 WAV 文件头
AAC一个不带封装头的 AAC 访问单元(Raw Access Unit),不包含 ADTS 头
Opus一包 Opus 编码数据,不包含 Ogg 封装

音频 flags 必须与数据实际采用的采样率和声道数一致。客户端 SDK 支持 8 kHz 或 16 kHz、16 位、单声道或双声道。

上传流程

C SDK 从创建 Service 到上传完成的调用与回调流程

上传请求是异步任务。TiStoreUploadRequest() 返回正数只表示请求已经受理。每当一段媒体上传成功或确认失败时,on_progress 返回这段范围的结果;一个请求可能收到多次。每个请求只有一次 on_result,它才是正常结束时的最终结果回调。TiStoreServiceStop() 会中止活动请求,不补发 on_result。当 duration_ms0 时,请求会一直保持开启;必须成功调用 TiStoreUploadSetEnd() 设置结束时间,它才会正常结束。

准备设备身份和 Token

  • 与设备绑定的 device_iddevice_secret_key
  • 业务服务端签发的短期 device_access_token
  • 与目标系统、CPU 架构和工具链匹配的 C SDK
  • 编码器输出的音视频帧,以及可靠的 UTC 毫秒时间戳。

设备应用先用产品已有的设备鉴权方式请求业务服务端,再取得 device_access_token。签发方式见云端签发 Token,设备侧的取得方式见 C SDK 接入AccessKeySecret 只保存在业务服务端,不能写入设备固件。

1. 初始化 SDK

c
#include <stdint.h>
#include <stdio.h>
#include <stdatomic.h>
#include <string.h>
#include "tistore.h"

static void on_log(int level, const char *message) {
    fprintf(stderr, "[tistore][%d] %s\n", level, message);
}

static int init_tistore(void) {
    struct TiStoreOptions options = TISTORE_OPTIONS_INITIALIZER;
    options.log_level = TISTORE_LOG_INFO;
    options.on_log = on_log;

    int rc = TiStoreInit(&options);
    if (rc != TISTORE_OK) {
        fprintf(stderr, "TiStoreInit failed: %s\n",
                TiStoreGetErrorString(rc));
    }
    return rc;
}

TiStoreInit() 是进程级初始化,只调用一次。上线前记录 TiStoreGetVersion()TiStoreGetBuildInfo(),便于定位二进制和头文件是否匹配。

2. 启动设备上传服务

设备每启动一条独立的媒体时间线,就创建一个 Service。创建 Service 后设置短期 Token,再调用 TiStoreServiceStart() 启动上传服务。回调由 SDK 分发线程执行,必须快速返回;不要在回调中调用 Stop、Destroy 或 Uninit。

c
struct UploadContext {
    atomic_int completed;
    int result;
    int error;
};

static void on_token_will_expire(
    int service_id, uint64_t expires_at_ms, void *user_data
) {
    // 通知业务线程向服务端续签。取得新 Token 后,
    // 由业务线程调用 TiStoreServiceUpdateToken(service_id, new_token)。
    (void)service_id;
    (void)expires_at_ms;
    (void)user_data;
}

static void on_token_expired(
    int service_id, uint64_t expires_at_ms, void *user_data
) {
    // Token 过期只暂停云端传输,不停止写队列或清空任务。
    (void)service_id;
    (void)expires_at_ms;
    (void)user_data;
}

static int create_started_service(
    const char *device_id,
    const char *device_secret_key,
    const char *device_access_token,
    struct UploadContext *ctx
) {
    struct TiStoreServiceOptions options =
        TISTORE_SERVICE_OPTIONS_INITIALIZER;
    options.device_secret_key = device_secret_key;
    options.buffer_size_bytes = 8 * 1024 * 1024;
    options.token_expire_warning_sec = 300;
    options.max_key_frame_interval_ms = 5000;
    options.on_token_will_expire = on_token_will_expire;
    options.on_token_expired = on_token_expired;
    options.user_data = ctx;

    int service_id = TiStoreServiceCreate(device_id, &options);
    if (service_id < 0) {
        fprintf(stderr, "create service failed: %s\n",
                TiStoreGetErrorString(service_id));
        return service_id;
    }

    int rc = TiStoreServiceUpdateToken(
        service_id, device_access_token);
    if (rc != TISTORE_OK) {
        TiStoreServiceDestroy(service_id);
        return rc;
    }

    rc = TiStoreServiceStart(service_id);
    if (rc != TISTORE_OK) {
        TiStoreServiceDestroy(service_id);
        return rc;
    }
    return service_id;
}

SDK 会复制配置字符串和回调函数指针,但不会管理 user_data。上例中的 ctx 必须保持有效,直到 Service 销毁完成。

3. 准备上传回调

先准备进度回调和最终结果回调。设备写帧与上传回调并发执行;回调只记录结果或投递事件,不要在回调线程中停止 Service。

c
static void on_upload_progress(
    int service_id,
    const struct TiStoreUploadRange *range,
    int error,
    void *user_data
) {
    printf("service=%d range=[%llu,%llu) bytes=%llu error=%d\n",
           service_id,
           (unsigned long long)range->start_time_ms,
           (unsigned long long)range->end_time_ms,
           (unsigned long long)range->size_bytes,
           error);
    (void)user_data;
}

static void on_upload_result(
    int service_id,
    const struct TiStoreUploadResult *result,
    void *user_data
) {
    struct UploadContext *ctx = (struct UploadContext *)user_data;
    ctx->result = result->result;
    ctx->error = result->error;

    printf("service=%d upload=%d result=%d error=%d "
           "range=[%llu,%llu) net=%llu bytes=%llu\n",
           service_id,
           result->upload_id,
           result->result,
           result->error,
           (unsigned long long)result->start_time_ms,
           (unsigned long long)result->end_time_ms,
           (unsigned long long)result->net_duration_ms,
           (unsigned long long)result->size_bytes);
    // 必须在回调内全部工作完成后再发布 completed。
    atomic_store_explicit(&ctx->completed, 1, memory_order_release);
}

4. 持续写入编码帧

单通道设备通常固定使用 channel_id = 0。音频和视频使用相同 Channel;同一 Channel、同一媒体类型的时间戳必须单调不减。

Service 启动后应立即启动送帧线程,并在没有上传请求时也持续写入。最近帧队列只有先收到事件前的媒体帧,事件请求才可能回溯到预录范围。下面的写帧函数在送帧线程中持续调用,与后续的请求创建和回调并发执行。

H.264 视频帧:

c
static int write_h264(
    int service_id,
    const uint8_t *data,
    uint32_t length,
    uint64_t utc_time_ms,
    int is_key_frame
) {
    struct TiStoreFrameInfo frame;
    memset(&frame, 0, sizeof(frame));
    frame.channel_id = 0;
    frame.media = TISTORE_VIDEO_H264;
    frame.flags = is_key_frame
        ? TISTORE_FRAME_FLAG_KEY_FRAME : 0;
    frame.timestamp_ms = utc_time_ms;
    frame.length = length;

    return TiStoreQueueWriteFrame(service_id, &frame, data);
}

AAC 音频帧:

c
static int write_aac(
    int service_id,
    const uint8_t *data,
    uint32_t length,
    uint64_t utc_time_ms
) {
    struct TiStoreFrameInfo frame;
    memset(&frame, 0, sizeof(frame));
    frame.channel_id = 0;
    frame.media = TISTORE_AUDIO_AAC;
    frame.flags = TISTORE_AUDIO_SAMPLE_16K16B1C;
    frame.timestamp_ms = utc_time_ms;
    frame.length = length;

    return TiStoreQueueWriteFrame(service_id, &frame, data);
}

SDK 在函数返回前复制 FrameInfo 和帧数据,因此调用返回后可以复用编码器缓冲区。返回 TISTORE_OK 只表示帧进入环形队列,不表示已经上传。

写帧失败时按具体错误码处理:

  • TISTORE_E_TIMESTAMP_OUT_OF_ORDER:修正同一 Channel、同一媒体类型的时间戳;
  • TISTORE_E_QUEUE_FULL:检查网络、鉴权和队列压力,不要无界重试;
  • TISTORE_E_UNSUPPORTED_MEDIAmedia 不在音频或视频的预留编号范围内,改用 TISTORE_AUDIO_*TISTORE_VIDEO_* 常量;
  • TISTORE_E_SERVICE_INVALID_STATE:确认 Service 已启动且尚未停止。

5. 创建录像请求

上传请求只选择时间范围,不拥有媒体帧。请求之间可以重叠;同一段底层媒体只上传一次。

事件录像

事件发生时,用“事件时刻减去预录时长”作为开始时间。把 duration_ms 设为 0,可以继续写入事件后的媒体;事件结束后再按下一节设置结束边界。

c
static int start_event_upload(
    int service_id,
    uint64_t event_time_ms,
    uint64_t preroll_ms,
    struct UploadContext *ctx
) {
    struct TiStoreUploadRequestOptions request =
        TISTORE_UPLOAD_REQUEST_OPTIONS_INITIALIZER;
    request.start_time_ms = event_time_ms >= preroll_ms
        ? event_time_ms - preroll_ms : TISTORE_TIME_EARLIEST;
    request.duration_ms = 0;
    request.on_progress = on_upload_progress;
    request.on_result = on_upload_result;
    request.user_data = ctx;
    return TiStoreUploadRequest(service_id, &request);
}

预录范围受最近帧队列容量和第一个合法视频关键帧限制。请求的实际起点可能晚于 start_time_ms;不要在事件触发后才开始送帧。

连续录像

连续录像同样使用开放请求。开始时间可以指向队列中的一段预录,也可以使用 TISTORE_TIME_EARLIEST 从当前可用的最早关键帧开始。录像计划结束或应用准备滚动到下一请求时,再设置结束边界。

c
struct UploadContext upload_context = {0};
struct TiStoreUploadRequestOptions request =
    TISTORE_UPLOAD_REQUEST_OPTIONS_INITIALIZER;
request.start_time_ms = TISTORE_TIME_EARLIEST;
request.duration_ms = 0;
request.on_progress = on_upload_progress;
request.on_result = on_upload_result;
request.user_data = &upload_context;

int upload_id = TiStoreUploadRequest(service_id, &request);
if (upload_id < 0) {
    fprintf(stderr, "request upload failed: %s\n",
            TiStoreGetErrorString(upload_id));
    return upload_id;
}

正数 upload_id 只表示请求已受理,不表示已经找到起始关键帧或上传成功。应用必须等待这个请求的 on_result

回调与业务线程之间必须使用互斥量、条件变量或 C11 原子操作同步。上例通过 atomic_load_explicit(&upload_context.completed, memory_order_acquire) 判断回调是否完成。不要用普通变量或 volatile 变量跨线程传递完成状态。

6. 设置录像结束时间并等待结果

录像事件结束时,先把事件结束前的最后一帧写入队列,再设置录像结束时间。end_time_ms 是 UTC 毫秒时间戳,也是录像范围的右开边界:

c
int rc = TiStoreUploadSetEnd(
    service_id, upload_id, event_end_time_ms);
if (rc != TISTORE_OK) {
    fprintf(stderr, "set end failed: %s\n",
            TiStoreGetErrorString(rc));
    // 请求尚未取得结束时间,不能等待 on_result。
    // 修正 end_time_ms 后可以重试;这里选择放弃整个 Service。
    TiStoreServiceStop(service_id);
    TiStoreServiceDestroy(service_id);
    TiStoreUninit();
    return rc;
}

只有设置成功后才能等待最终结果。设置失败不会改变请求边界:先修正 end_time_ms 并重试;如果无法恢复,必须在业务线程停止并销毁 Service 来放弃请求,不能继续等待 on_result。设置成功只表示结束边界被接受,仍要保持 Service、请求上下文和回调有效,直到 on_result 恰好返回一次。

on_result 的结果分为三种:

  • TISTORE_UPLOAD_COMPLETE:所选媒体范围连续、完整地上传;
  • TISTORE_UPLOAD_PARTIAL:至少一段媒体范围上传成功,但成功范围存在缺口;
  • TISTORE_UPLOAD_FAILED:没有媒体范围上传成功,结合 error 处理。

只有收到 COMPLETE 或业务明确接受的 PARTIAL 后,再由客户端查询并验证录像。写帧成功、进度回调成功或结束边界设置成功,都不能单独作为上传完成信号。

7. 检查上传队列

c
struct TiStoreQueueInfo info;
int rc = TiStoreQueueGetInfo(service_id, &info);
if (rc == TISTORE_OK) {
    printf("queue=%llu/%llu inflight=%llu frames=%llu\n",
           (unsigned long long)info.used_bytes,
           (unsigned long long)info.capacity_bytes,
           (unsigned long long)info.inflight_bytes,
           (unsigned long long)info.frame_count);
}

inflight_bytes 持续升高表示上传处理落后于写入速度,常见原因是网络不可用、Token 失效或云端重试。结合错误码和 SDK 日志处理,不要只扩大队列掩盖问题。

8. 停止并释放资源

先停止所有送帧线程,并确保需要保留结果的请求都已经收到 on_result,再释放资源:

c
int rc = TiStoreServiceStop(service_id);
if (rc == TISTORE_OK) {
    rc = TiStoreServiceDestroy(service_id);
}
if (rc == TISTORE_OK) {
    rc = TiStoreUninit();
}

TiStoreServiceStop() 会立即中止活动任务并丢弃尚未分发的回调。它不是等待上传完成的接口。

确认录像已经上传

完成接入后,应能从日志和回调确认:

  1. SDK 与 Service 初始化成功;
  2. 上传请求取得正数 upload_id
  3. 第一个合法视频关键帧进入后产生上传进度;
  4. 设置结束边界后收到唯一 on_result
  5. 客户端用对应设备的 app_access_token 查询到该时间段并能播放。

COMPLETE 和 PARTIAL 说明云端已经保存相应时间范围,不代表音视频数据已经通过客户端解码验证。若客户端能够查到录像,但播放或下载返回“录像不可读”,回到前面的格式、关键帧和音频帧要求,修正设备编码后重新上传。

若请求以 TISTORE_E_UPLOAD_START_TIMEOUT 结束,检查是否持续写入合法视频关键帧。也要确认 max_key_frame_interval_ms 覆盖编码器真实关键帧间隔。

收到 TISTORE_UPLOAD_COMPLETE,或确认业务可以接受 TISTORE_UPLOAD_PARTIAL 后,客户端可以查询、播放或下载这段录像

TiStore 开发文档