音视频环形缓存

更新时间:2023-08-09 08:09:35下载pdf

涂鸦结合视频帧的编码特性(H.264/265 的 I 帧、P 帧)以及音频帧数据特点,基于环形缓冲区(Ring Buffer)的设计原理,实现了高效无锁、多进多出的 音视频环形缓冲区

背景信息

环形缓冲区也称为圆形缓冲区(Circular Buffer)或循环缓冲区(Cyclic Buffer),能够在固定大小的内存内,存放头尾循环读写的数据结构。

生产者将数据帧按序写入 Buffer 中,当到达内存尾部时跳转到头部覆盖老数据。

一个或者多个消费者按序读取数据帧,并确保其数据是正确未被覆盖,以及相互之间不会产生干扰。

功能特性

涂鸦音视频环形缓冲区具有以下特点:

  • 支持多个独立的 Ring Buffer,分别服务于高清视频通道、标清视频通道、音频通道等。
  • 支持单个数据通道的一路写、多路并发读,例如可以同时有多个业务模块同时读取一路高清视频数据,互不干扰。
  • 在业务模块发生卡顿时,Ring Buffer 内部自动实现了跳帧策略,有效防止数据异常覆盖、视频帧解码错误、音视频延迟等问题。

关联组件

svc_ring_buffer

数据结构

帧节点

音视频数据帧在 Ring Buffer 中统一按节点存储管理。

typedef struct
{
  UINT_T index;                   // 节点数据索引,循环使用
  MEDIA_FRAME_TYPE_E type;        // 数据帧类型
  UCHAR_T *raw_data;              // 原始数据指针
  UINT_T size;                    // 原始数据大小
  UINT64_T pts;                   // 呈现时间戳
  UINT64_T timestamp;             // 解码时间戳
  UINT_T seq_no;                  // 帧序列号,持续增加不循环
  UCHAR_T *extra_data;            // 附加数据,暂无使用
  UINT_T extra_size;              // 附加数据大小,暂无使
  UINT_T seq_sync;                // 用于同步音频/视频的全局序列号
} RING_BUFFER_NODE_T;

初始化参数

typedef struct
{
    UINT_T bitrate;            // 码率(上限)大小,单位 kb
    UINT_T fps;                // 帧率
    UINT_T max_buffer_seconds; // 最大缓存时长
    FUNC_REQUEST_KEY_FRAME_CB request_key_frame_cb; // 关键帧视频编回调接口,默认为空
} RING_BUFFER_INIT_PARAM_T;

参数说明

参数 说明
max_buffer_seconds 表示最大缓存时长,建议值为大于 1 个 GOP(Group Of Picture)长度,小于 10 秒。当设置为 0 时内部默认使用 10 秒。

回调参数

typedef VOID (*FUNC_REQUEST_KEY_FRAME_CB)(INT_T device, INT_T channel, IPC_STREAM_E stream);

该回调用于触发外部编码器快速产生一个视频编码关键帧(I 帧)。

参数说明

参数 说明
device 子设备号,仅用于 XVR 监视器等产品。对于不包含子设备的 IPC 单品,值为 0
channel 视频通道号,用于双目等多镜头多传感器产品,从 0 开始编号。
stream 码流通道号,用于区分同一视频通道输出的高清、标清等不同数据码流。

API 说明

初始化

针对特定设备、特定视频通道的特定码流,执行初始化。该操作会执行缓存空间的创建与节点的初始化。

/** @brief initialize one ring buffer for one channel(one device)
 * @param[in]      device           device number
 * @param[in]      channel          channel number in device
 * @param[in]      stream           stream number in channel
 * @param[in]      pparam           initialize parameter
 * @return error code
 * - OPRT_OK        init success
 * - Others         init failed
 */
OPERATE_RET tuya_ipc_ring_buffer_init(INT_T device, INT_T channel, IPC_STREAM_E stream, RING_BUFFER_INIT_PARAM_T* pparam);

反初始化

针对特定设备、特定视频通道的特定码流,执行反初始化,释放缓存空间与节点资源。

/** @brief uninitialize one ring buffer for one channel(one device)
 * @param[in]     device   device number
 * @param[in]     channel  channel number in device
 * @param[in]     stream   stream number in channel
 * @return error code
 * - OPRT_OK      uninit success
 * - Others       uninit failed
*/
OPERATE_RET tuya_ipc_ring_buffer_uninit(INT_T device, INT_T channel, IPC_STREAM_E stream);

开启缓存

以数据生产者(写模式)或数据消费者(读模式),创建特定码流缓存的用户句柄。该句柄将用于后续的数据操作。

/** @brief open a new session for read/write
 * @param[in]     device   device number
 * @param[in]     channel channel number in device
 * @param[in]     stream   stream number in channel
 * @param[in]     open_type open type
 * @return user handle
*/
RING_BUFFER_USER_HANDLE_T tuya_ipc_ring_buffer_open(INT_T device, INT_T channel, IPC_STREAM_E stream, RBUF_OPEN_TYPE_E open_type);

关闭缓存

关闭用户句柄对应的缓存。

/** @brief close session
 *  @warning open/close should be called in pair like file
 *  @param[in]   handle      user handle return by open
 *  @return error code
 * - OPRT_OK      success
 * - Others       failed
*/
OPERATE_RET tuya_ipc_ring_buffer_close(RING_BUFFER_USER_HANDLE_T handle);

写入数据帧

往特定的码流缓存中新增一个数据帧。

/** @brief append new frame into a ring buffer
 * @param[in]   handle       user handle return by open
 * @param[in]   addr         the address of the data to append
 * @param[in]   size         size of the data
 * @param[in]   type         media type
 * @param[in]   pts          time stamp in us
 * @return error code
 * - OPRT_OK      success
 * - Others       failed
*/
OPERATE_RET tuya_ipc_ring_buffer_append_data(RING_BUFFER_USER_HANDLE_T handle, UCHAR_T *addr, UINT_T size, MEDIA_FRAME_TYPE_E type, UINT64_T pts);

写入数据帧并附带时间戳

往特定的码流缓存中新增一个数据帧,并指定时间戳。

/** @brief append new frame into ring buffer with custom timestamp
 * @param[in]   handle       user handle return by open
 * @param[in]   addr         the address of the data to append
 * @param[in]   size         size of the data
 * @param[in]   type         media type
 * @param[in]   pts          time stamp in us
 * @param[in]   timestamp    time stamp in ms
 * @return error code
 * - OPRT_OK      success
 * - Others       failed
 */
OPERATE_RET tuya_ipc_ring_buffer_append_data_with_timestamp(RING_BUFFER_USER_HANDLE_T handle, UCHAR_T *addr, UINT_T size, MEDIA_FRAME_TYPE_E type, UINT64_T pts, UINT64_T timestamp);

读取数据帧

根据用户句柄,从特定设备、特定视频通道的特定码流缓存中获取一帧数据。接口内部会自动维护一个位置与状态信息,数据消费者只需根据业务需求重复调用该接口,即可按序获得新的数据。

当获取数据的速度较慢导致数据过老而被循环覆盖时,接口内部会自动跳转到最新的符合编码特性的数据帧。

/** @brief get next frame from ring buffer, will jump to the latest frame if delayed too much time
 * @param[in]     handle  user handle return by open
 * @param[in]     is_retry whether retry to get the last frame
 * @return the ring buffer node or NULL if there is no newer frame in ring buffer
 */
RING_BUFFER_NODE_T *tuya_ipc_ring_buffer_get_frame(RING_BUFFER_USER_HANDLE_T handle, BOOL_T is_retry);

查找预录帧

在音频码流通道中,从最新的数据帧往前查找第 frame_num 个帧。在视频码流通道中,会从第 frame_num 帧起继续往前查找直到数据帧类型为关键帧 I 帧。该接口仅用于获得对应的数据帧节点。

/** @brief start with the newest frame, find previous frame by count frame_num.
for video channel(device), it will keep find previous frame until it's I frame.
 * @param[in]    handle user handle returned by open
 * @param[in]    frame_num    count frame num backwards
 * @return the ring buffer node or NULL if there is no newer frame in ring buffer
*/
RING_BUFFER_NODE_T *tuya_ipc_ring_buffer_find_pre_by_frame(RING_BUFFER_USER_HANDLE_T handle, UINT_T frame_num);

查找并定位到预录帧

在音频码流通道中,从最新的数据帧往前查找第 frame_num 个帧。在视频码流通道中,会从第 frame_num 帧起继续往前查找直到数据帧类型为关键帧 I 帧。

该接口在获得对应的预录帧节点后,会将对应的用户句柄的状态更新到该节点,即此时再调用 tuya_ipc_ring_buffer_get_frame,获得该预录帧的后一帧。

/** @brief start with the newest frame, find previous frame by count frame_num, and update&anchor user(of userIndex) to this frame node.
for video channel(device), it will keep find previous frame until it's I frame.
 * @param[in]    handle user handle returned by open
 * @param[in]    frame_num    count frame num backwards
 * @return the ring buffer node or NULL if there is no newer frame in ring buffer
*/
RING_BUFFER_NODE_T *tuya_ipc_ring_buffer_get_pre_frame(RING_BUFFER_USER_HANDLE_T handle, UINT_T frame_num);

常见问题

内存在什么时候申请与释放,占用多大的内存?

Ring Buffer 在初始化时即申请连续的内存。这样设计的考虑,是为了减少在运行过程中,占用的内存动态变化导致的 OOM(Out of memory,内存溢出)等问题。每路码流占用的内存大小为,最大缓存时长 max_buffer_seconds+1 对应码率 bitrate 的乘积。

能不能将缓存时长设置地尽量小,节省内存占用?

在 IPC 的应用场景中,Ring Buffer 是复用于实时预览、本地录像存储、云存储等所有业务的。如果缓存时长较短,则在云存储上传录像、本地录像写磁盘、实时预览网络抖动的情况下,极易触发跳帧策略,影响用户体验。

  • 通常设置:10 秒。
  • 最小建议:不小于 6 秒。

单帧数据大小是否有限制?

为了防止异常场景出现,最大视频帧数据为根据设定的码率同比例变化,对应 1.5Mbps 数据,单帧上限为 280KB。

在什么情况下会出现取数据跳帧的情况?

当数据消费者落后生产者太多,以至于将发生数据被覆盖,包括节点信息覆盖和内存空间覆盖时,自动触发跳帧策略。

视频帧会跳到最新的关键 I 帧,音频会跳到最新的帧。