编码器 OSD 模块

Supported ICs: [RTL8735C]

概述

编码器 OSD 组件(video_osd)让 AP 应用可以直接在 H.264/HEVC 编码输出上叠加时间戳(Timestamp)、ARGB Logo 与多边形隐私遮罩(Privacy Mask),并让 NN 检测结果共用同一套叠加层硬件而不与之冲突。公开的 header 有:

  • video_osd.h —— 时间戳与 Logo 的配置、位置,以及 start/stop/show/hide API。

  • video_osd_stream.h —— 自适应 stream manager:init、status、deinit、记忆体预算与错误码。

  • video_osd_privacy.h —— 多边形隐私遮罩场景(scene)、能力查询,以及 apply/get/status/deinit API。

  • nn_osd.h (位于 video_nn_utils)—— NN 框、标签、特征点绘制与画布群组(canvas group)辅助函数。

这四个 header 是应用程式唯一需要使用的 API。video_osd_internal.h、video_osd_plan.* 以及 compositor/polygon rasterizer 原始码属于内部实作细节,用于实现 共存规则与区域规划 中描述的区域规划与像素混合逻辑;请勿直接呼叫这些内部函式。

以上四种功能都是绘制在同一条编码串流(encoded stream)(H.264 或 HEVC)上。快照/JPEG 输出、raw NV12/RGB 串流(V4/V5),以及任何编码前的录影路径都不在此范围内——详见 限制与安全。

../../_images/multimedia_pro_mmf_video_pipeline_ap_vp.drawio.svg

AP / VP 视频管线。OSD 合成发生在 VP 编码器端;AP 端只负责建立区域清单并提交。

快速开始

参考范例为 mmf2_video_example_v1_osd_init.c,它汇出两个 VIDEO app table 项目:

  • mmf2_video_example_v1_osd_uvc_init —— 720p H.264,透过 UVC 输出(需要 CONFIG_USBD_UVC)。

  • mmf2_video_example_v1_osd_rtsp_init —— 相同场景,改用 RTSP 输出(需要网路功能)。

它同时提供一个 console 命令 VAPP_OSD,含两个子命令:

  • VAPP_OSD update —— 在执行期移动凹多边形隐私遮罩。

  • VAPP_OSD stop —— 阻挡输出并释放所有 OSD 资源。

范例最上方有三个功能开关,可独立开启/关闭时间戳、Logo 与隐私遮罩:

#define V1_PRIVACY_ENA           1
#define V1_TIMESTAMP_ENA         1
#define V1_LOGO_ENA              1

这三者可以同时全部关闭;manager 本身不要求任何 client 存在。NN 并不是这三个开关之一——它是一个独立的产生端(透过 nn_osd),一旦 stream 初始化完成后即可共用同一个 manager;详见 NN 共存。

完整的启动/停止流程

#include "video_osd.h"
#include "video_osd_privacy.h"
#include "video_osd_stream.h"

/* 1. 开启 video_module,先阻挡输出,然后呼叫 CMD_VIDEO_APPLY。
 *    编码器必须先运作起来才能设定 OSD,但在 gate 关闭期间,
 *    不能让任何未遮罩的画面流向 sink。 */
video_output_gate_t output_gate = { .blocked = 1 };
mm_context_t *video_v1_ctx = mm_module_open(&video_module);
mm_module_ctrl(video_v1_ctx, CMD_VIDEO_SET_OUTPUT_GATE, (int)&output_gate);
mm_module_ctrl(video_v1_ctx, CMD_VIDEO_SET_PARAMS, (int)&video_v1_params);
mm_module_ctrl(video_v1_ctx, MM_CMD_SET_QUEUE_LEN, 6);
mm_module_ctrl(video_v1_ctx, MM_CMD_INIT_QUEUE_ITEMS, MMQI_FLAG_DYNAMIC);
mm_module_ctrl(video_v1_ctx, CMD_VIDEO_APPLY, 0);

/* 2. 在任何 client 之前,先初始化共用的 stream manager。 */
video_osd_stream_t osd_stream = {0};
video_osd_stream_config_t stream_config = {
    .target         = v1_osd_target,   /* stream_id/codec/width/height,须与 video_v1_params 相符 */
    .memory_budget  = 8U * 1024U * 1024U, /* 0 表示不限制;详见记忆体模型 */
};
video_osd_stream_init(&osd_stream, &stream_config);

/* 3. 依需求加入 client:privacy、logo、timestamp(任意子集皆可)。
 *    video_osd_privacy_apply_sync() 会在接受前重试 EBUSY,接受后
 *    等待编码器套用该确切 revision——不需要手动轮询。 */
video_osd_privacy_t privacy = {0};
video_osd_privacy_config_t privacy_config = { .target = v1_osd_target };
video_osd_privacy_init(&privacy, &privacy_config);
video_osd_privacy_scene_t scene;
video_osd_privacy_scene_default(&scene);
/* ... 填入 scene.polygons[] ... */
uint32_t revision = 0;
int ret = video_osd_privacy_apply_sync(&privacy, &scene, &revision, 5000);
if (ret != 0) {
    /* 不可回报此次更新已成功;保留 context 并处理错误
     * (ETIMEDOUT 的情况见「错误处理与关闭流程」章节)。 */
}

/* Logo/timestamp 改用对应的 *_sync 版本:各自已经等待了自己
 * 那一次初始 revision,因此这里不需要再额外呼叫一次等待。 */
video_osd_logo_t logo = {0};
video_osd_logo_config_t logo_config;
video_osd_logo_config_default(&logo_config);
logo_config.target = v1_osd_target;
logo_config.argb8888 = my_logo_argb;
logo_config.width = MY_LOGO_WIDTH;
logo_config.height = MY_LOGO_HEIGHT;
logo_config.stride_bytes = MY_LOGO_WIDTH * sizeof(uint32_t);
video_osd_logo_show_sync(&logo, &logo_config, 5000);

video_osd_timestamp_t timestamp = {0};
video_osd_timestamp_config_t ts_config;
video_osd_timestamp_config_default(&ts_config);
ts_config.target = v1_osd_target;
video_osd_timestamp_start_sync(&timestamp, &ts_config, 5000);

/* 4. 这次等待并不是上面三次呼叫的第二重确认——它只是为了取得
 * 一个共用的 applied_timestamp,供输出 gate 使用,同时也涵盖
 * 期间可能夹带进来的 clock/NN 更新。 */
video_osd_stream_status_t status;
video_osd_stream_wait_applied(&osd_stream, 0, 5000, &status);
/* 预期 status.last_error == 0,且上面已达到 status.applied_revision */

/* 5. 开启 sink,然后使用已套用的 timestamp 开启输出 gate,
 *    以捨弃遮罩/Logo/时间戳生效之前产生的画面。 */
output_gate.blocked = 0;
output_gate.filter_timestamp = status.applied_revision != 0;
output_gate.minimum_timestamp = status.applied_timestamp;
mm_module_ctrl(video_v1_ctx, CMD_VIDEO_SET_OUTPUT_GATE, (int)&output_gate);
mm_module_ctrl(video_v1_ctx, CMD_VIDEO_FORCE_IFRAME, 0);

/* 6. 关闭流程:先阻挡输出、停止下游 sink/linker,然后依相反顺序
 *    呼叫四个 *_sync 移除 API 并逐一检查回传值,最后才 deinit
 *    stream manager。详细顺序请见「错误处理与关闭流程」章节。 */

完整的分段式设定流程(mmf2_video_example_v1_osd_start())请参考 mmf2_video_example_v1_osd_init.c,其中包含 UVC/RTSP sink 的选择与串接的 SISO linker。请复制其分段结构——输出 gate、stream manager、各 client、sink、重新开启 gate——而不是只抄个别 API 呼叫。

公开 API

以下是唯一供客户使用的 API。video_osd_internal.h、video_osd_plan.* 以及 video_osd_compositor.c 中的其余内容,都是区域规划器(region planner)与像素合成器(compositor)的内部实作细节。

Stream manager (video_osd_stream.h)

函式

说明

video_osd_stream_init(ctx, config)

为一条 H.264/HEVC 串流建立共用 manager。必须在该串流上的任何 logo/timestamp/privacy/NN client 之前呼叫。

video_osd_stream_get_status(ctx, status)

读取 revision、pending 状态、最后错误码、记忆体计数,以及区域/索引数量。

video_osd_stream_wait_applied(ctx, revision, timeout_ms, status)

等待 applied_revision 达到 revision,且没有回报编码器错误。revision = 0 会在第一次成功读取时,将当时的 requested_revision 设为固定等待目标。建议以此取代手动轮询 get_status();详见 同步便利 API。

video_osd_stream_deinit(ctx)

释放 manager。若编码器仍在回收缓冲区,会回传 VIDEO_OSD_EBUSY;请保留 context 并重试。

video_osd_stream_deinit_sync(ctx, timeout_ms)

内部重试 deinit() 直到成功或超时。其他所有 client(logo/timestamp/privacy/NN)必须已先移除。详见 同步便利 API。

以下 API 共用的错误码(同样适用于 privacy 系列 API):

常数

意义

VIDEO_OSD_EINVAL (-1)

参数或几何资料无效、context 为 NULL,或在未受管理的 direct-overlay client 上呼叫了同步移除 API;先前已被接受的状态维持不变。

VIDEO_OSD_ENOMEM (-2)

配置(allocation)失败。

VIDEO_OSD_EBUSY (-3)

本次请求未被提交;前一笔交易仍在处理中。非同步 API 仍会直接回传此值;_sync helper 会在其 timeout 预算内自行重试。

VIDEO_OSD_EENCODER (-4)

VP 编码器在执行期拒绝了该区域清单,或完成通知回报了错误。

VIDEO_OSD_ENOSPC (-5)

硬体叠加层索引或记忆体预算不足,无法容纳此次请求。

VIDEO_OSD_ETIMEDOUT (-6)

_sync/wait_applied 呼叫未在 timeout_ms 内完成。这不代表已取消、回复或释放底层操作——详见 同步便利 API。

所有错误码定义都在 video_osd.h;include video_osd_stream.h 也能看到相同定义。video_osd_privacy.h 提供对应的 VIDEO_OSD_PRIVACY_* 别名,包含 VIDEO_OSD_PRIVACY_ETIMEDOUT。

时间戳与 Logo (video_osd.h)

函式

说明

video_osd_timestamp_config_default(config)

填入相机预设值:本地时间、YYYY-MM-DD HH:mm:ss、3 倍放大的白色 bitmap 字型、右下角 16 像素边距。

video_osd_timestamp_start_sync(ctx, config, timeout_ms)

ctx 必须是空的:启动 clock 并等待其初始 revision 被编码器套用后才回传 0。详见 同步便利 API。

video_osd_timestamp_start(ctx, config)

启动(或重新启动)时间戳叠加层,不等待编码器确认;一旦请求被接受即回传 0。

video_osd_timestamp_stop_sync(ctx, timeout_ms)

停止 clock task、移除叠加层,并等待编码器确认移除完成后才回传 0。详见 同步便利 API。

video_osd_timestamp_stop(ctx)

停止时间戳叠加层。回传型态为 void——详见 错误处理与关闭流程。

video_osd_logo_show_sync(ctx, config, timeout_ms)

ctx 必须是空的:与 timestamp_start_sync() 相同的启动/完成/超时契约。详见 同步便利 API。

video_osd_logo_show(ctx, config)

显示(或替换)Logo 叠加层,不等待编码器确认。会复制每一列像素资料;呼叫返回后,呼叫端的 ARGB 缓冲区即可释放。

video_osd_logo_hide_sync(ctx, timeout_ms)

与 timestamp_stop_sync() 相同的完成/超时契约。详见 同步便利 API。

video_osd_logo_hide(ctx)

隐藏 Logo 叠加层。回传型态为 void——详见 错误处理与关闭流程。

Privacy (video_osd_privacy.h)

函式

说明

video_osd_privacy_get_caps(target, caps)

查询多边形/顶点数上限,以及某个 target 的最差情况 ARGB 画布大小估计值。

video_osd_privacy_scene_default(scene)

将 scene 归零初始化,设定 version 并令 enabled = 1。

video_osd_privacy_init(ctx, config)

在某条串流上建立 privacy client。若尚无 stream manager,会自动建立一个以保持相容性;建议改为明确先呼叫 video_osd_stream_init() 以取得所有权。

video_osd_privacy_apply_sync(ctx, scene, revision, timeout_ms)

替换整个场景,接受前重试 EBUSY,接受后等待该确切 revision 被编码器套用。只有真正套用后才回传 0。详见 同步便利 API。

video_osd_privacy_apply(ctx, scene, revision)

替换整个场景,不等待编码器确认。回传 0 代表已被接受(accepted),尚非已套用——请轮询状态以取得 applied_revision。

video_osd_privacy_get(ctx, scene)

读回最后一次被接受(不一定已套用)的场景。

video_osd_privacy_get_status(ctx, status)

与 video_osd_stream_status_t 结构相同(video_osd_privacy_status_t 即为其 typedef)。

video_osd_privacy_deinit_sync(ctx, timeout_ms)

解除遮罩并等待安全释放,若 privacy 拥有自动建立的 manager 也会一并处理。详见 同步便利 API。

video_osd_privacy_deinit(ctx)

解除遮罩。可能回传 VIDEO_OSD_EBUSY;请保留 context 并重试。

NN 叠加层 (nn_osd.h,位于 video_nn_utils)

函式

说明

nn_osd_group_init(group, width, height, layer[], layer_count, enable_watchdog)

为每个颜色图层配置一个 1bpp 画布,并分配 OSD 硬体索引。

nn_osd_group_lock / nn_osd_group_unlock

在侦测 callback 重绘画布期间进行保护。

nn_osd_group_clear / nn_osd_canvas_* 系列绘图函式

清除并在某个图层的画布上绘制矩形、点、文字。

nn_osd_group_flush(group)

将本帧所有图层画布一次提交。

nn_osd_group_kick(group, active)

提供可选的「过期叠加层」看门狗(watchdog)心跳;仅在 enable_watchdog 为真时才需呼叫。

nn_osd_group_deinit(group)

释放画布、watchdog 与 OSD 索引。

时间戳(Timestamp)

video_osd_timestamp_config_t 栏位说明:

栏位

型态

说明

target

video_osd_target_t

目前正在运作的编码器的 stream_id / codec / frame_width / frame_height。

placement

video_osd_placement_t

角落位置(VIDEO_OSD_POSITION_UPPER_LEFT / UPPER_RIGHT / LOWER_LEFT / LOWER_RIGHT / CUSTOM)加上 margin_x/margin_y;若为 CUSTOM 则改用明确的 x/y。

format

const char *

strftime() 格式字串。可使用任一 VIDEO_OSD_TIMESTAMP_FORMAT_* 巨集,或自订字串。

time_zone

video_osd_time_zone_t

VIDEO_OSD_TIME_LOCAL 或 VIDEO_OSD_TIME_UTC。

valid_after

uint32_t

低于此 epoch 秒数时,视为时钟尚未校时;详见 时间来源。

max_chars

uint16_t

渲染文字缓冲区容量。预设为 VIDEO_OSD_TIMESTAMP_DEFAULT_MAX_CHARS (19);上限为 VIDEO_OSD_TIMESTAMP_MAX_CHARS (64)。

font_scale

uint8_t

1–3。bitmap 字型尺寸固定,此值为像素尺寸的放大倍率。

alpha / y / u / v

uint8_t

文字颜色与不透明度,以 YUV 表示。预设为不透明白色(alpha=255、y=255、u=128、v=128)。

格式巨集:

巨集

输出范例

VIDEO_OSD_TIMESTAMP_FORMAT_YMD_DASH_24

2026-09-16 14:05:30

VIDEO_OSD_TIMESTAMP_FORMAT_YMD_SLASH_24

2026/09/16 14:05:30

VIDEO_OSD_TIMESTAMP_FORMAT_MDY_SLASH_24

09/16/2026 14:05:30

VIDEO_OSD_TIMESTAMP_FORMAT_DMY_SLASH_24

16/09/2026 14:05:30

VIDEO_OSD_TIMESTAMP_FORMAT_YMD_DASH_12

2026-09-16 02:05:30 PM

VIDEO_OSD_TIMESTAMP_FORMAT_YMD_SLASH_12

2026/09/16 02:05:30 PM

VIDEO_OSD_TIMESTAMP_FORMAT_MDY_SLASH_12

09/16/2026 02:05:30 PM

VIDEO_OSD_TIMESTAMP_FORMAT_DMY_SLASH_12

16/09/2026 02:05:30 PM

重要

预设的 max_chars (19)恰好符合 24 小时制格式所需长度。任何 12 小时制格式(_12 结尾)会多出 `` AM``/`` PM``,至少需要 22 个字元;范例中因此明确设定了 V1_TIMESTAMP_MAX_CHARS。video_osd_timestamp_start_sync() 会拒绝无法容纳在 max_chars 内的格式字串。

只要格式字串能容纳于 max_chars 内,且长度短于内部格式字串容量上限,即可使用自订格式字串——例如可用 "%H:%M" 这样简单的 strftime() 格式来显示精简时钟。

video_osd_timestamp_config_t config;
video_osd_timestamp_config_default(&config);
config.target = v1_osd_target;
config.placement.position = VIDEO_OSD_POSITION_LOWER_RIGHT;
config.placement.margin_x = 16;
config.placement.margin_y = 16;
config.format = VIDEO_OSD_TIMESTAMP_FORMAT_YMD_DASH_24;
config.time_zone = VIDEO_OSD_TIME_LOCAL;
config.max_chars = 19;      /* 若使用 _12(AM/PM)格式,请设为 >= 22 */
config.font_scale = 3;
video_osd_timestamp_start_sync(&timestamp, &config, 5000);

时间来源

video_osd_timestamp_start_sync() 透过 time() 读取系统时间。在此平台上,time() 在 NTP(或其他时间来源)校时之前会固定停留在 Unix epoch。校时之前,时间戳叠加层会改用系统开机时间(uptime),从 1970-01-01 00:00:00 起算,而不是显示占位符字串。系统校时由应用程式自行负责;本组件本身不执行 NTP 校时。

Privacy(隐私遮罩)

一个 privacy 场景(scene)最多可遮罩 VIDEO_OSD_PRIVACY_MAX_POLYGONS (8)个多边形,每个多边形有 3 到 VIDEO_OSD_PRIVACY_MAX_VERTICES (16)个顶点。凸形与凹形皆支援,且不限绕行方向(winding order);多边形须沿周界排列,不可自我相交或有孔洞。

video_osd_privacy_scene_t 栏位说明:

栏位

型态

说明

version

uint32_t

由 video_osd_privacy_scene_default() 设定为 VIDEO_OSD_PRIVACY_VERSION。

enabled

uint32_t

0 会隐藏所有遮罩,但保留其定义;1 (预设)则显示它们。

coordinates

video_osd_privacy_coordinates_t

VIDEO_OSD_PRIVACY_NORMALIZED (预设)或 VIDEO_OSD_PRIVACY_REFERENCE_PIXELS。

reference_width / reference_height

uint32_t

仅当 coordinates == VIDEO_OSD_PRIVACY_REFERENCE_PIXELS 时使用;必须落在 [1, 1000000] 范围内。

fill_rgb

uint32_t

整个场景共用的一个 0x00RRGGBB 不透明填色。预设为黑色(0x000000)。不支援每个多边形各自设色,也没有 alpha 选项。

polygon_count

uint32_t

polygons[] 中的多边形数量,最多 8 个。

polygons[]

video_osd_privacy_polygon_t[8]

每个元素包含 id、enabled、vertex_count,以及一个 {x, y} 组成的 vertices[] 阵列。

坐标系统

坐标一律以影像边缘为基准:原点在左上角,x 向右递增,y 向下递增。

  • VIDEO_OSD_PRIVACY_NORMALIZED (预设):每个轴为 [0, 10000] (VIDEO_OSD_PRIVACY_NORMALIZED_MAX)范围内的整数;(0,0) 为左上角,(10000,10000) 在任何串流解析度下都对应到右下角。

  • VIDEO_OSD_PRIVACY_REFERENCE_PIXELS:每个轴为 [0, reference_width] / [0, reference_height] 范围内的像素坐标。当应用程式已经以某个参考影像(例如让使用者在快照上标注)的像素空间作业时,可使用此模式取代标准化单位。

在两种坐标系统下,右/下边缘都会对应到编码器目前的输出宽/高;本组件除此单一映射之外不做任何额外缩放。letterbox、裁切、镜射、旋转,以及 PTZ/世界坐标转换,都必须由应用程式在建立场景之前自行处理。

范例:标准化坐标多边形

static const video_osd_privacy_polygon_t polygons[] = {
    {
        .id = 1,
        .enabled = 1,
        .vertex_count = 4,
        .vertices = {
            {1000, 1000}, {4000, 1000}, {4000, 4000}, {1000, 4000},
        },
    },
};

video_osd_privacy_scene_t scene;
video_osd_privacy_scene_default(&scene);
scene.fill_rgb = 0x000000U;
scene.polygon_count = 1;
memcpy(scene.polygons, polygons, sizeof(polygons));
uint32_t revision = 0;
video_osd_privacy_apply_sync(&privacy, &scene, &revision, 5000);

范例:参考像素坐标多边形

video_osd_privacy_scene_t scene;
video_osd_privacy_scene_default(&scene);
scene.coordinates = VIDEO_OSD_PRIVACY_REFERENCE_PIXELS;
scene.reference_width = 1920;   /* 应用程式绘制顶点时所使用的像素空间 */
scene.reference_height = 1080;
scene.fill_rgb = 0x000000U;
scene.polygon_count = 1;
scene.polygons[0] = (video_osd_privacy_polygon_t) {
    .id = 1, .enabled = 1, .vertex_count = 4,
    .vertices = { {200, 150}, {800, 150}, {800, 600}, {200, 600} },
};
uint32_t revision = 0;
video_osd_privacy_apply_sync(&privacy, &scene, &revision, 5000);

新增、移动、删除

video_osd_privacy_apply() 一律会替换整个场景;没有单独的新增/移除 API。请以应用程式自己维护的多边形清单(或以 video_osd_privacy_get() 取得的结果)为基础,建立下一个场景:

  • 新增一个多边形:将其附加到 polygons[] 并将 polygon_count 加 1。

  • 移动一个多边形:保留其 id,修改其 vertices[],然后重新提交——范例中的 update 项目就是这样运作的。

  • 停用一个多边形但保留其定义:将其 enabled 设为 0。

  • 删除一个多边形:在下一个场景中省略其 id。

  • 移除全部遮罩:提交一个 polygon_count = 0 的场景;或将 scene.enabled 设为 0,隐藏所有多边形但保留其定义,供之后重新启用。

这些都是刻意设计的明确解除遮罩操作,而不是错误时的 fallback——本组件绝不会在部分失败时悄悄丢弃某个遮罩;被拒绝的 apply() 会让先前已被接受的场景继续维持生效,而不是套用新的(不完整的)内容。

每次呼叫 apply() 都会复制传入的场景资料,因此呼叫端本地的 scene 变数在呼叫返回后即可立即重用或超出生命周期。video_osd_privacy_get() 回传的是最后一次**被接受**的场景,未必是已经**套用**的场景——请检查 video_osd_privacy_get_status() (或共用的 stream status)以区分两者。

共存规则与区域规划

合成顺序(由后到前)固定为:privacy → NN → logo → timestamp。Logo 与 timestamp 彼此之间,或与 privacy 之间都可能重叠;半透明的 Logo 叠在隐私遮罩上方时,会与该遮罩的不透明填色混合,而不是与遮罩正在覆盖的相机画面像素混合。

区域格式选择

manager 只会光栅化(rasterize)对齐后的边界区域,预设并非全画面叠加。在进行任何合并之前,四种功能各自会在规划器的原始表格中贡献一种不同的输出格式选择:

已启用

初始输出选择

无

不使用任何像素缓冲区或硬体叠加层索引。

Logo (L)

Cropped ARGB 区域(Logo 固定使用此格式)。

Timestamp (T)

Cropped bitmap 区域。

NN (N)

每个 NN 颜色图层各自的 cropped bitmap 区域。

Privacy (P)

每个多边形对应的 cropped bitmap 区域。

规划器接着会合并共用同一个编码器区块(block)的区域——H.264 为 64×16,HEVC 为 64×64——并重复此合并,直到没有任何两个输出区域再共用同一个区块。合并后,若两个不透明 bitmap 区域的 YUV 颜色**相同**,仍会维持 bitmap 格式;混色、任何半透明重叠,或该合并群组中含有 ARGB 来源(Logo),则该合并群组需要一个局部的 ARGB8888 区域。不存在无条件的全画面 ARGB 配置,但单一个很大的连通重叠群组,仍可能扩大到覆盖整个画面——规划器并不保证在任何排列组合下都能得到小范围区域。

完整的 16 种组合表(L/T/N/P 各自可选是否启用)都会收敛为上述的区块不相交(block-disjoint)结果;每一列组合都适用相同的合并/仲裁规则。这是一个有界限(bounded)的贪婪(greedy)规划器:当合并可降低总像素负载(payload),或硬体叠加层索引即将不足时,规划器就会进行合并,并选择造成最小 payload 增加量的方案,每次合并后重新检查是否产生新的区块冲突。这并非逐区块拼贴(tiling)方案,也不是全域最佳打包(packing)。此 manager 永不会选用 NV12 作为 OSD 区域格式,即便编码器驱动原生支援的叠加层格式包含 ARGB8888、NV12 与 bitmap。

NN 仲裁

NN 与 privacy/logo/timestamp 区域是以编码器区块为单位进行仲裁的。若某个 NN group 中的任何一个区域,与受保护的 logo/timestamp 区域,或与某个 privacy 多边形的保守边界矩形共用同一个区块,则该次 callback 的**整个** NN group 都会被省略(omit)——而不只是发生冲突的那个方框。下一次不冲突的 NN callback 会自动将其还原。这个保守判断同样适用于凹多边形的空缺凹口(notch)区域;仲裁是基于多边形的边界矩形,而非逐像素精确覆盖,因此一个只与凹口重叠的方框,仍可能被判定为冲突并遭到抑制。

记忆体模型

每个区域的成本

  • 一个 bitmap 区域的成本为 aligned_width * height / 8 个 payload bytes。

  • 一个 ARGB 区域的成本为 width * height * 4 bytes。

输出区域的生命周期

输出区域在编码器读取期间是不可变的(immutable)。内容未变动的区域会重用现有缓冲区。内容变动的区域,只会在该次更新的 frame-boundary 完成确认(completion)之前同时保留新旧两份缓冲区,完成后旧的配置即会被释放。因此一个静态、未变动的区域绝不会同时持有第二份输出画布。

输入/来源端储存空间 (此为输出区域之外的额外开销)

  • Logo 会保留其自身持有的 ARGB 来源复本。

  • Timestamp 会保留两个小型 bitmap 来源(双缓冲,用于文字的原子式更新)。

  • NN 会保留其原始的全画面 1-bit 绘图画布;受管理路径会额外将 strided view 复制到一个紧凑、由 manager 持有的影子缓冲区(shadow buffer),取代中间的 cropped 缓冲区,但这并不会消除原始画布本身的占用。

状态栏位 (video_osd_stream_get_status() / video_osd_privacy_get_status() 皆回传相同的 video_osd_stream_status_t 结构)

栏位

意义

requested_revision / applied_revision

该串流上所有 client 共用的 revision 计数器;一旦编码器确认更新完成,applied_revision 会追上 requested_revision。

pending

非零表示目前有一笔交易正在处理中。同一时间只能有一笔交易处于 pending 状态。

applied_timestamp

已套用的 revision 生效时,编码器端所对应的画面时间戳;开启输出 gate 时可作为 minimum_timestamp 使用。

last_error

0 表示成功;否则为最近一次交易所对应的某个 VIDEO_OSD_E* 错误码。

allocated_bytes

manager、descriptor,以及目前生效中/暂存(staged)输出区域的配置量。

source_bytes

已注册 client 来源储存空间的抽样值,包含 NN 的工作画布。

peak_bytes

allocated_bytes + source_bytes 曾观察到的最大总和。

memory_budget

传入 video_osd_stream_init() 的预算值(或透过 privacy 自动 init 相容路径设定的值);0 表示不限制。

index_count

已保留的实体硬体叠加层索引数,包含一笔待处理(pending)的更新;与逻辑 client handle 数量并不相同。

bitmap_regions / argb_regions

目前已套用(applied)的输出区域数量,依格式分类。

预算语意

memory_budget = 0 会完全停用此限制。设定非零预算时,init 与每次 commit 都会检查:若某次更新(包含新旧输出缓冲区同时存在期间)会使 allocated_bytes + source_bytes 超出预算,该次更新会被拒绝。这**并非**一种预先配置(pre-allocation),也**不是**对整个 process heap 的硬性上限:

  • client 的来源配置(例如 Logo 自身持有的 ARGB 复本)可能发生在该 client 向 manager 注册之前,在注册/commit 之前不会被计入。

  • RTOS 的 task/semaphore 堆叠、配置器(allocator)中介资料,以及不相关的缓冲区,皆不包含在内。

  • 范例中的 V1_OSD_MEMORY_BUDGET (8 MiB)是一个 manager 不会超过的上限,并不代表一开始就预留了 8 MiB 记忆体。

video_osd_privacy_get_caps().canvas_bytes 是另外一个独立的、仅供相容性使用的最差情况估计值——相当于两份全画面 ARGB 画布的字节数——它**不是** video_osd_privacy_init() 实际配置的数量。请查询 video_osd_stream_get_status() 以取得实际使用量。

硬体索引池

OSD 与 mosaic 共用每个编码器 channel 的同一组 12 个硬体叠加层索引池。stream manager 是唯一会从此索引池中保留**实体**索引的一方;每个受管理的 helper(logo、timestamp、privacy,以及透过 nn_osd 使用的 NN)内部都是使用逻辑 handle,不会额外做实体保留。在某个索引上启用 mosaic 会停用该索引上的 OSD,反之亦然,因此若应用程式在同一个 channel 上另外直接使用 video_set_mosaic()/video_disable_mosaic(),就是与受管理的 OSD stream 共用同一组 12 个 slot。

错误处理与关闭流程

同步便利 API

下面八个 API 把呼叫端原本要手写的 status 轮询与 EBUSY 重试迴圈都收纳起来。它们不会新增 session 物件、worker task、queue,或新的 AP/VP 协定;内部仍是呼叫本节稍后描述的既有非同步 API。在任何受管理(managed)的串流上,建议优先使用它们而不是手动轮询。

API

回传 0(成功)的意义

video_osd_logo_show_sync(ctx, config, timeout_ms)

初始 Logo 更新已被接受,且已由编码器套用。

video_osd_timestamp_start_sync(ctx, config, timeout_ms)

clock 已启动,且其初始更新已被接受并由编码器套用。

video_osd_stream_wait_applied(ctx, revision, timeout_ms, status)

已套用的 revision 达到指定目标,且没有回报编码器错误。

video_osd_privacy_apply_sync(ctx, scene, revision, timeout_ms)

一份完整的场景替换已被接受,且已由编码器套用。

video_osd_timestamp_stop_sync(ctx, timeout_ms)

clock task 已停止,移除已被确认,context 已释放。

video_osd_logo_hide_sync(ctx, timeout_ms)

移除已被确认,context 已释放。

video_osd_privacy_deinit_sync(ctx, timeout_ms)

privacy 已 deinit;若 privacy 自动建立了 manager,也一并释放。

video_osd_stream_deinit_sync(ctx, timeout_ms)

manager 已释放,前提是其所有 client 都已先被移除。

show_sync / start_sync 的初始启用语意

  • 必须已经存在相符的 stream manager——stream_id、codec,以及帧宽/高都要与传给 video_osd_stream_init() 的 target 一致。

  • ctx 必须是零初始化的,或已经完全清理完成。若 context 处于非空状态(正在使用中,或移除到一半),会回传 VIDEO_OSD_EINVAL——这点与移除类 API 不同,不能靠再呼叫一次就自动视为成功。

  • 接受前会自行重试 EBUSY。一旦被接受,只会等待那一次初始 revision;该 revision 是在 manager lock 内撷取的,因此之后的 timestamp/NN 更新不会改变这次呼叫在等待的目标。

  • 回传 0 代表初始内容已经由编码器套用——不需要再额外呼叫一次 video_osd_stream_wait_applied() 来确认这个特定的 client 是否启动成功。

  • 呼叫期间,config、其格式字串(timestamp)以及 Logo 的来源像素都必须保持有效且不可变。API 内部会复制格式字串与像素资料,因此呼叫返回后即可释放呼叫端的输入——即使返回的是超时结果,也一样可以释放。

  • timeout_ms 同时涵盖接受前重试与接受后等待,是单一预算,且仍不是硬性即时上限。

  • 超时或编码器错误都可能发生在「已被接受之后」:此时 context 可能仍存活、Logo 可能已经显示、clock 可能已经在持续刷新。这些都不会自动 rollback——请不要对 context 做 memset()/free,也不要在同一个 context 上再呼叫一次 show()/start()。

  • 复原方式一律相同:呼叫对应的 stop_sync/hide_sync``(即使 context 已经是空的也能安全呼叫),成功之后再重新尝试 ``show_sync/start_sync。

  • 呼叫签名只有三个逻辑参数(context、config、timeout),没有额外的输出用 revision 参数。若超时发生在「被接受之前」,之后某次针对其他 revision 的 video_osd_stream_wait_applied() 成功,并不能证明这个特定的 Logo/Clock 已经被接受;请改用上面「先 stop_sync/hide_sync 再重试」的模式,而不要从不相关的等待结果去推论。

  • 失败时可能回传 VIDEO_OSD_EINVAL、VIDEO_OSD_ENOMEM、VIDEO_OSD_ENOSPC、VIDEO_OSD_EENCODER、VIDEO_OSD_EBUSY 或 VIDEO_OSD_ETIMEDOUT,依失败发生的位置而定。

wait_applied 的 revision 语意

  • revision > 0:等待 applied_revision >= revision。这是一个具体目标,不是「等到整条串流变成 idle」。

  • revision = 0:第一次成功读取 status 时,会将当时的 requested_revision 拍下作为固定等待目标。之后的 timestamp 或 NN 更新不会把这个目标往后推。

  • 正因为目标是固定的,成功回传时 status 里的 pending 仍可能是 1——原先要等的那笔 revision 已完成,但另一笔较新、不相关的更新正在处理中。

  • 若传入的 revision 是这个 manager 尚未请求过的未来值,会回传 VIDEO_OSD_EINVAL。一个 revision 数值只在其所属的那个 video_osd_stream_t 生命周期内有意义。

  • status 可传 NULL。不为 NULL 时,会收到最后一次成功读取到的 status,包含 applied_timestamp。若整个呼叫过程中没有任何一次 status 读取成功,caller 的输出不会被写入——呼叫失败时不要读取这个欄位。

  • 若 status 中已设定 last_error,且目前没有復原交易在 pending,会直接回传该错误;若有復原交易正在 pending,则会继续等待而不是立刻失败。

  • 这里的成功仍然只代表「编码器已套用」,不代表任何网路客户端或播放器已经显示该画面。

apply_sync 的场景替换语意

  • 一次呼叫即替换整个场景;没有单独的新增/移除变体。

  • 在被接受之前,此呼叫会在 timeout_ms 预算内自行重试 EBUSY。

  • 一旦被接受,只会等待那一笔 revision,绝不会重新提交同一个 scene。

  • 回传 0 代表该场景已被编码器套用,而不只是命令被接受。

  • revision 是可选的输出参数;一般用法可传 NULL。不为 NULL 时,呼叫一开始会先清成 0,接受后立即填入该值——即使之后等待超时,这个值仍会保留,方便 caller 继续针对该 revision 等待(见下方的重试范例)。

  • 呼叫期间 scene 参数必须保持有效,且不能被另一个 task 修改。

  • timeout_ms 是涵盖「接受前重试」与「接受后等待」两个阶段的单一预算,两阶段之间不会重新起算。

stop_sync / hide_sync / deinit_sync 的移除语意

  • 回传 0 代表该资源已经清理完成。对已经移除、或零初始化的空 context 再呼叫一次,同样会回传 0。

  • NULL context 指标会回传 VIDEO_OSD_EINVAL——这是呼叫端的错误,不是成功。

  • 这些同步移除 API 只适用于受管理(managed)的串流。若在一个没有 stream manager 的未受管理 direct-overlay client 上呼叫,会回传 VIDEO_OSD_EINVAL,因为该路径没有对应的编码器完成确认协定——它绝不会假装已经取得硬体确认。

  • 针对 timestamp 的移除,一旦开始移除,更新 task 就会立即停止;即使呼叫超时,clock 也不会自动重新启动。

  • 遇到错误或超时时,请保留该 context 并重试同一个 _sync API。不要改呼叫 void 的 stop()/hide(),不要手动清空 context,不要 free 它,也不要拿同一个尚未移除完成的 context 再次 start()/show()。

  • video_osd_stream_deinit_sync() 不会自动销毁其他 client——logo、timestamp、privacy 与 NN 必须先被移除。

  • 若 privacy 自动建立了它自己的 stream manager(见 公开 API),video_osd_privacy_deinit_sync() 也会一并释放该 manager,但前提是该串流上其他所有 client 都已先被移除。

非同步 API 的已接受(accepted)与已套用(applied)

video_osd_privacy_apply()、video_osd_logo_show() 与 video_osd_timestamp_start() 回传 0,代表该请求已被接受(accepted)进入 pending 交易,尚不代表已经在编码输出上可见。请轮询 video_osd_stream_get_status() (或 privacy 专用的别名函式),直到 pending == 0;然后再检查 applied_revision >= revision 且 last_error == 0,才能视为该变更已生效——或者直接呼叫 video_osd_stream_wait_applied(),不必自己手写这段轮询迴圈。所谓「已套用(applied)」的完成通知,其意义仅是 VP 编码器的 preprocessing 步骤已针对某一个输入 frame 选定完整的区域清单,并释放了先前的缓冲区——它并不是任何下游客户端的网路/显示层级确认(ACK)。

非同步与同步 API 的错误码

  • apply()/show()/start() 回传非零值(VIDEO_OSD_EBUSY 除外),代表本次请求完全未被提交,或已被直接拒绝;先前已被接受的状态维持不变。

  • VIDEO_OSD_EBUSY 代表前一笔交易仍处于 pending 状态——本次请求未被提交。非同步 API 会将此值直接回传给 caller;_sync helper 则会在其 timeout 预算内自行重试。请将连续快速的更新(例如来自 UI 拖曳操作)合并成一次替换提交,而不是把每一个中间状态都送出。

  • VP 编码器在执行期的拒绝,会透过 status 中的 last_error 呈现(或由 _sync/wait_applied 呼叫直接回传 VIDEO_OSD_EENCODER),而不是透过最初那次回传值。当该错误存在期间,编码输出会被刻意停止,而不是退回成未遮罩的画面;请提交一个新的合法 scene/logo/timestamp 以復原。切勿在 status 回报错误期间开启输出 gate。

超时(Timeout)

  • timeout_ms 是单次呼叫的轮询/重试预算,以 64-bit 的 monotonic RTOS 时钟计算经过时间,因此不受摄影机校时影响。

  • EBUSY 会以最多 20 毫秒的间隔重试;重试期间绝不会在持有 manager mutex 时 sleep。

  • timeout_ms = 0 仍会尝试一次——若该次尝试遇到 busy,会立即回传 VIDEO_OSD_ETIMEDOUT,而不是阻塞等待。

  • UINT32_MAX 只是一个很大但有限的毫秒数,并不代表「永远等待」。

  • 已在进行中的 IPC 呼叫、mutex 等待或排程延迟,都可能让实际回传时间超过 timeout_ms——这些 API 都不是硬性即时(hard real-time)。

  • VIDEO_OSD_ETIMEDOUT 不会取消已提交的命令,不会回复到先前的场景,也不会释放尚未确认完成的缓冲区。此时应保留该 context 并持续重试/等待——绝不可在超时时释放该 context 或视为可以安全释放。若 VP 端完全停止回应,应维持输出 gate 关闭,并在应用程式层级主动復原或重启该 pipeline;不要假设解除遮罩或缓冲区释放已经悄悄成功。

  • 每一次 API 呼叫都有各自独立的 timeout 预算。下方关闭流程中依序呼叫的四个移除 API,最多可能各自用满一份预算(即总共四份),而不是整个 stop 流程共用同一份预算。

直接呼叫非同步 API 的应用程式,需要自行负责如上所述的状态轮询。请检查 _sync API 的 int 回传值来判断操作是否成功;不要检查 ctx->priv。

apply_sync 接受后超时的復原方式

uint32_t revision = 0;
int ret = video_osd_privacy_apply_sync(&privacy, &scene, &revision, 5000);

if (ret == VIDEO_OSD_ETIMEDOUT && revision != 0) {
    /* 已被接受,只是完成确认尚未收到:继续等待同一笔 revision,
     * 不需要重新提交该场景。 */
    ret = video_osd_stream_wait_applied(&stream, revision, 5000, NULL);
}
/* ret != 0:不可回报此次更新已成功;保留 context 并处理错误。
 * 重新提交场景是另一笔全新的替换,不是取消正在进行中的这一笔。 */

若呼叫端不需要区分「尚未被接受」与「已接受但完成确认未到」,可以传入 NULL 的 revision,之后再查询 video_osd_privacy_get_status()/video_osd_stream_get_status() 即可。

启动确认与输出 gate

video_osd_stream_status_t status;
int ret = video_osd_stream_wait_applied(&stream, 0, 5000, &status);
if (ret != 0) {
    /* 保持输出 gate 关闭,进入错误处理。 */
    return ret;
}
/* 用 status.applied_timestamp 配置 video_module 的 output gate;
 * 是否启用 filter_timestamp 依 applied_revision != 0 判断。 */

Output gate 仍留在 MMF 范例中,不属于 video_osd 核心——核心本身不依赖 UVC/RTSP。上面的 Logo/Timestamp 同步 helper 并不需要这次呼叫来确认自己的启用状态;这里的等待只是为了取得一个「共用」的状态(同时涵盖启动期间可能夹带进来的 clock/NN 更新),不是每个功能各自需要的第二次等待。

三种功能一致的启用用法

/* 编码器已启动、输出 gate 已关闭,且 stream manager / privacy
 * context 都已初始化完成。 */
int ret = video_osd_privacy_apply_sync(&privacy, &scene, NULL, 5000);
if (!ret) {
    ret = video_osd_logo_show_sync(&logo, &logo_config, 5000);
}
if (!ret) {
    ret = video_osd_timestamp_start_sync(&clock, &clock_config, 5000);
}
if (ret != 0) {
    /* 走同步清理流程;不能因为这里失败就假设完全没有任何
     * OSD 内容已被接受。 */
    return ret;
}
/* 三者皆已套用;取得共用的 status.applied_timestamp 后
 * 即可安全开放输出。 */

三种 client 之间没有强制的启用顺序。范例依序先 Privacy、再 Logo、最后 Timestamp,是为了先建立好遮罩,最后才启动周期性的 clock 刷新;但实际的合成顺序是由 manager 决定的,与呼叫顺序无关(见 共存规则与区域规划)。

关闭顺序

与启动顺序相反,改用同步移除 API 并逐一检查回传值:

/* 先阻挡输出 gate、停止下游的 sink/linker。以下四个移除步骤
 * 期间编码器都必须持续运作。 */
int ret = video_osd_timestamp_stop_sync(&clock, 5000);
if (!ret) {
    ret = video_osd_logo_hide_sync(&logo, 5000);
}
if (!ret) {
    ret = video_osd_privacy_deinit_sync(&privacy, 5000);
}
if (!ret) {
    ret = video_osd_stream_deinit_sync(&stream, 5000);
}

if (ret != 0) {
    /* 保持输出阻挡、编码器持续运作;保留所有 context,
     * 稍后再重试整段 stop 流程。 */
    return ret;
}
/* 只有在上面每一步都回传 0 之后,才能停止编码器本身。 */
  1. 先阻挡输出 gate(CMD_VIDEO_SET_OUTPUT_GATE 并设 blocked = 1),并停止下游的 linker/sink。

  2. 呼叫 video_osd_timestamp_stop_sync(),再呼叫 video_osd_logo_hide_sync()。若有使用 NN,需先停止 NN 产生端,并透过 nn_osd API deinit 其 NN group,然后才能继续下一步。

  3. 呼叫 video_osd_privacy_deinit_sync()。

  4. 呼叫 video_osd_stream_deinit_sync()。步骤 2–4 期间,编码器必须仍在运作,才能确认每一次资源回收。

  5. 只有在上面每一步都回传 0 之后,才可以停止/关闭编码器本身。

透过 privacy 相容性自动 init 路径建立的 manager,会在该串流上其他所有 client 都被移除之后,由 privacy 自身的 deinit_sync() 一并释放——不要再另外对它呼叫一次 video_osd_stream_deinit_sync()。在任何 OSD client 仍在进行关闭流程期间,切勿从外部停止或重新设定编码器。这些同步 API 都不可在 ISR、编码器/IPC callback 中呼叫,也不可在持有某个背景 callback 所需的 NN group lock 时呼叫。

NN 共存

NN 侦测叠加层使用 video_nn_utils 中的 nn_osd_group_init()/nn_osd_group_flush(),而不是直接调用本组件的 client API;但一旦 video_osd_stream_init() 已经执行过,它们即会绘制到同一个共用的 stream manager 上。每个 nn_osd 图层使用一个硬体 OSD 索引;当方框、标签与特征点可以共用同一种颜色时,使用单一图层即可;只有在需要不同颜色时才使用多个图层,并请留意不同索引的 bitmap 区域彼此可能重叠。

当侦测 callback 会持续每帧刷新叠加层时,预设不要启用 nn_osd 的过期叠加层看门狗(watchdog)——将 enable_watchdog 传入 0,并且在此情况下不要呼叫 nn_osd_group_kick()。只有在 callback 可能在物体仍然存在的情况下停止到达(导致过期方框持续残留画面上)时,才应启用该 watchdog;若启用,请以注解说明原因。

关于某个 NN group 在与受保护的 logo/timestamp 区域,或某个 privacy 多边形的边界矩形发生冲突时如何被抑制与还原,请参见前述的 共存规则与区域规划。

限制与安全

  • 本组件只保护它所初始化的那一条编码串流(encoded stream)。JPEG 快照、用于 NN 输入的 raw NV12/RGB 串流(V4/V5)、其他同时运作的编码串流,以及任何编码前的录影路径,都**不会**被自动遮罩。正式产品的设计必须对其对外公开的每一个输出路径,都套用遮罩(或等效的判断机制),包括启动时、快照拍摄时,以及错误復原过程中,而不能只处理本范例所设定的这一条串流。

  • 场景更新的应用程式端传输方式(HTTP/MQTT/P2P 或任何其他通道)、授权/存取控制、UI 坐标系统的转换,以及跨重启后持久化最后套用的场景,都是呼叫端自行负责的事项;video_osd/video_osd_privacy 皆不提供这些功能。

  • 低延迟/slice 模式、look-ahead、multi-pass 编码,以及 H.264/HEVC + JPEG 混合双重编码,此受管理交易机制皆不支援;本实作是针对 SDK 的单核心、单一 pass 同步编码路径设计的。若需要将上述任一功能与受管理 OSD 一并使用,请先向 VP 编码器驱动确认。

  • 请勿在已经拥有 stream manager 的串流上,另外开启直接呼叫 video_set_osd_bitmap()/video_set_mosaic() (或对应的停用函式)的 client;一旦场景所有权(scene ownership)生效,VP 会拒绝该串流上的直接叠加写入。Mosaic 仍是与其共用同一组 12 索引硬体池的独立功能,并不是 privacy 多边形的后端实作。

  • AP 与 VP 韧体——包括预先建置(prebuilt)的多媒体函式库——必须一起更新,因为 OSD 场景协定(video_osd_scene_t)是 AP/VP 共用的结构。