Encoder OSD Module

Supported ICs: [RTL8735C]

Overview

The encoder OSD component (video_osd) lets an AP application draw a timestamp, an ARGB logo, and polygon privacy masks directly on the H.264/HEVC encoded output, and lets NN detection results share the same overlay hardware without colliding with them. The public headers are:

  • video_osd.h — timestamp and logo configuration, placement, and start/stop/show/hide APIs.

  • video_osd_stream.h — the adaptive stream manager: init, status, deinit, memory budget, and error codes.

  • video_osd_privacy.h — polygon privacy mask scene, capabilities, and apply/get/status/deinit APIs.

  • nn_osd.h (in video_nn_utils) — NN box/label/landmark drawing and canvas-group helpers.

These four headers are the only APIs an application needs. Internal headers such as video_osd_internal.h, video_osd_plan.h, and the compositor/polygon rasterizer sources implement the region planner and pixel blending described in Coexistence and Region Planning; do not call them directly.

All four features draw onto one encoded stream (H.264 or HEVC). Snapshot/JPEG output, raw NV12/RGB streams (V4/V5), and any pre-encoder recording path are not covered — see Limitations and Security.

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

AP / VP video pipeline. OSD composition happens on the VP encoder side; the AP side only builds the region list and submits it.

Quick Start

The reference example is mmf2_video_example_v1_osd_init.c, which exports two VIDEO app table entries:

  • mmf2_video_example_v1_osd_uvc_init — 720p H.264 over UVC (requires CONFIG_USBD_UVC).

  • mmf2_video_example_v1_osd_rtsp_init — the same scene over RTSP (requires networking).

It also exposes a console command, VAPP_OSD, with two sub-commands:

  • VAPP_OSD update — moves the concave privacy polygon at runtime.

  • VAPP_OSD stop — blocks output and releases every OSD resource.

Three feature switches near the top of the example turn timestamp, logo, and privacy on or off independently:

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

All three can be disabled at once; the manager itself does not require any client. NN is not one of these three switches — it is a separate producer (nn_osd) that shares the same manager once a stream is initialized; see NN Coexistence.

Complete start/stop flow

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

/* 1. Open video_module with output blocked, then CMD_VIDEO_APPLY.
 *    The encoder must be running before OSD can be configured, but no
 *    unmasked frame may reach the sink while the gate is still closed. */
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. Initialize the shared stream manager BEFORE any client. */
video_osd_stream_t osd_stream = {0};
video_osd_stream_config_t stream_config = {
    .target         = v1_osd_target,   /* stream_id/codec/width/height, must match video_v1_params */
    .memory_budget  = 8U * 1024U * 1024U, /* 0 = unlimited; see Memory Model */
};
video_osd_stream_init(&osd_stream, &stream_config);

/* 3. Add whichever clients are needed: privacy, logo, timestamp (any subset).
 *    video_osd_privacy_apply_sync() retries EBUSY before acceptance, then
 *    waits for the encoder to apply that exact revision -- no manual polling. */
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);
/* ... fill scene.polygons[] ... */
uint32_t revision = 0;
int ret = video_osd_privacy_apply_sync(&privacy, &scene, &revision, 5000);
if (ret != 0) {
    /* Do not report the update as successful; keep the context and handle
     * the error (see Error Handling and Shutdown for the ETIMEDOUT case). */
}

/* Logo/timestamp use the matching *_sync entries: each already waits for
 * its own initial revision, so no extra per-feature wait is needed here. */
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. This final wait is not a required second confirmation for the three
 * calls above -- it only obtains a common applied_timestamp for the
 * output gate, also covering any clock/NN update that raced in between. */
video_osd_stream_status_t status;
video_osd_stream_wait_applied(&osd_stream, 0, 5000, &status);
/* status.last_error == 0 and status.applied_revision reached above expected */

/* 5. Open the sink, then open the output gate using the applied timestamp
 *    to drop any frame produced before the mask/logo/timestamp were live. */
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. Shutdown: block output, stop the downstream sink/linker, then call the
 *    four *_sync removal APIs in reverse order, checking each return value.
 *    See Error Handling and Shutdown for the exact order and retry rules. */

Read mmf2_video_example_v1_osd_init.c for the full sectioned setup (mmf2_video_example_v1_osd_start()), including the UVC/RTSP sink selection and the SISO link to it. Copy the sectioned structure — output gate, stream manager, clients, sink, gate reopen — rather than only the individual API calls.

Public APIs

Only these APIs are customer-facing. Everything else in video_osd_internal.h, video_osd_plan.*, and video_osd_compositor.c is implementation detail for the region planner and pixel compositor.

Stream manager (video_osd_stream.h)

Function

Description

video_osd_stream_init(ctx, config)

Create the shared manager for one H.264/HEVC stream. Call before any logo/timestamp/privacy/NN client on that stream.

video_osd_stream_get_status(ctx, status)

Read revisions, pending state, last error, memory counters, and region/index counts.

video_osd_stream_wait_applied(ctx, revision, timeout_ms, status)

Wait until applied_revision reaches revision with no reported encoder error. revision = 0 snapshots the current requested_revision on the first successful read as the wait target. Recommended over manually polling get_status(); see Synchronous Convenience APIs.

video_osd_stream_deinit(ctx)

Release the manager. Returns VIDEO_OSD_EBUSY while the encoder is still retiring a buffer; retry with the context intact.

video_osd_stream_deinit_sync(ctx, timeout_ms)

Retry deinit() internally until it succeeds or times out. All other clients (logo/timestamp/privacy/NN) must already be removed. See Synchronous Convenience APIs.

Error codes returned by these and the privacy APIs:

Constant

Meaning

VIDEO_OSD_EINVAL (-1)

Invalid argument or geometry, a NULL context, or a sync removal called on an unmanaged direct-overlay client; the previous accepted state is unchanged.

VIDEO_OSD_ENOMEM (-2)

Allocation failed.

VIDEO_OSD_EBUSY (-3)

Nothing was submitted; a previous transaction is still pending. Still returned by the asynchronous APIs; the _sync helpers retry it internally within their timeout budget.

VIDEO_OSD_EENCODER (-4)

The VP encoder rejected the region list, or a completion reports an error, at runtime.

VIDEO_OSD_ENOSPC (-5)

Not enough hardware overlay indices or memory budget for this request.

VIDEO_OSD_ETIMEDOUT (-6)

A _sync/wait_applied call did not finish inside timeout_ms. This does not cancel, roll back, or free the underlying operation — see Synchronous Convenience APIs.

All error values are declared in video_osd.h; including video_osd_stream.h also makes them visible. video_osd_privacy.h provides matching VIDEO_OSD_PRIVACY_* aliases for every value, including VIDEO_OSD_PRIVACY_ETIMEDOUT.

Timestamp and logo (video_osd.h)

Function

Description

video_osd_timestamp_config_default(config)

Fill camera defaults: local time, YYYY-MM-DD HH:mm:ss, 3x white bitmap font, lower-right with 16-pixel margins.

video_osd_timestamp_start_sync(ctx, config, timeout_ms)

ctx must be empty: start the clock and wait for its initial revision to be encoder-applied before returning 0. See Synchronous Convenience APIs.

video_osd_timestamp_start(ctx, config)

Start (or restart) the timestamp overlay without waiting for encoder confirmation; returns 0 once the request is accepted.

video_osd_timestamp_stop_sync(ctx, timeout_ms)

Stop the clock task, remove the overlay, and wait for encoder-confirmed removal before returning 0. See Synchronous Convenience APIs.

video_osd_timestamp_stop(ctx)

Stop the timestamp overlay. void — see Error Handling and Shutdown.

video_osd_logo_show_sync(ctx, config, timeout_ms)

ctx must be empty: same start/completion/timeout contract as timestamp_start_sync(). See Synchronous Convenience APIs.

video_osd_logo_show(ctx, config)

Show (or replace) the logo overlay without waiting for encoder confirmation. Copies every row; the caller’s ARGB buffer may be released once this call returns.

video_osd_logo_hide_sync(ctx, timeout_ms)

Same completion/timeout contract as timestamp_stop_sync(). See Synchronous Convenience APIs.

video_osd_logo_hide(ctx)

Hide the logo overlay. void — see Error Handling and Shutdown.

Privacy (video_osd_privacy.h)

Function

Description

video_osd_privacy_get_caps(target, caps)

Query polygon/vertex limits and a worst-case ARGB canvas size estimate for a target.

video_osd_privacy_scene_default(scene)

Zero-initialize a scene with version set and enabled = 1.

video_osd_privacy_init(ctx, config)

Create a privacy client on a stream. Auto-creates a stream manager if none exists yet, for compatibility; explicit stream ownership through video_osd_stream_init() is preferred.

video_osd_privacy_apply_sync(ctx, scene, revision, timeout_ms)

Replace the entire scene, retrying EBUSY before acceptance and then waiting for that exact revision to be encoder-applied. Returns 0 only once applied. See Synchronous Convenience APIs.

video_osd_privacy_apply(ctx, scene, revision)

Replace the entire scene without waiting for encoder confirmation. Returns 0 for accepted, not applied — poll status for applied_revision.

video_osd_privacy_get(ctx, scene)

Read back the last accepted (not necessarily applied) scene.

video_osd_privacy_get_status(ctx, status)

Same fields as video_osd_stream_status_t (video_osd_privacy_status_t is a typedef of it).

video_osd_privacy_deinit_sync(ctx, timeout_ms)

Remove masking and wait for safe retirement, including an auto-created manager if privacy owns it. See Synchronous Convenience APIs.

video_osd_privacy_deinit(ctx)

Remove masking. May return VIDEO_OSD_EBUSY; retry with the context intact.

NN overlay (nn_osd.h, in video_nn_utils)

Function

Description

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

Allocate one 1bpp canvas per color layer and assign OSD hardware indices.

nn_osd_group_lock / nn_osd_group_unlock

Protect canvases while a detection callback redraws them.

nn_osd_group_clear / nn_osd_canvas_* drawing calls

Clear and draw rectangles, points, and text on a layer’s canvas.

nn_osd_group_flush(group)

Submit all layer canvases for this frame.

nn_osd_group_kick(group, active)

Feed the optional stale-overlay watchdog; call only when enable_watchdog was set.

nn_osd_group_deinit(group)

Release canvases, watchdog, and OSD indices.

Timestamp

video_osd_timestamp_config_t fields:

Field

Type

Description

target

video_osd_target_t

stream_id / codec / frame_width / frame_height of the running encoder.

placement

video_osd_placement_t

Corner (VIDEO_OSD_POSITION_UPPER_LEFT / UPPER_RIGHT / LOWER_LEFT / LOWER_RIGHT / CUSTOM) plus margin_x/margin_y, or explicit x/y for CUSTOM.

format

const char *

A strftime() format string. Use one of the VIDEO_OSD_TIMESTAMP_FORMAT_* macros or a custom string.

time_zone

video_osd_time_zone_t

VIDEO_OSD_TIME_LOCAL or VIDEO_OSD_TIME_UTC.

valid_after

uint32_t

Epoch seconds below which the clock is treated as unsynchronized; see Time Source.

max_chars

uint16_t

Rendered text buffer capacity. Default is VIDEO_OSD_TIMESTAMP_DEFAULT_MAX_CHARS (19); max is VIDEO_OSD_TIMESTAMP_MAX_CHARS (64).

font_scale

uint8_t

1–3. The bitmap font is fixed-size; scale multiplies pixel dimensions.

alpha / y / u / v

uint8_t

Text color and opacity, in YUV. Default is opaque white (alpha=255, y=255, u=128, v=128).

Format macros:

Macro

Example output

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

Important

The default max_chars (19) fits the 24-hour formats exactly. Any 12-hour format (_12 suffix) adds `` AM``/`` PM`` and needs at least 22 characters; the example sets V1_TIMESTAMP_MAX_CHARS explicitly for this reason. video_osd_timestamp_start_sync() rejects a format that does not fit max_chars.

Custom format strings are accepted as long as they fit within max_chars and are shorter than the internal format-string capacity; use a plain strftime() pattern such as "%H:%M" for a compact clock.

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;      /* use >= 22 for a _12 (AM/PM) format */
config.font_scale = 3;
video_osd_timestamp_start_sync(&timestamp, &config, 5000);

Time source

video_osd_timestamp_start_sync() reads the system clock with time(). On this platform time() stays pinned at the Unix epoch until NTP (or another time source) sets it. Before synchronization, the timestamp overlay falls back to system uptime counted from 1970-01-01 00:00:00 rather than displaying a placeholder string. Synchronizing the clock is the application’s responsibility; the OSD component does not perform NTP itself.

Privacy

A privacy scene masks up to VIDEO_OSD_PRIVACY_MAX_POLYGONS (8) polygons, each with 3 to VIDEO_OSD_PRIVACY_MAX_VERTICES (16) vertices. Convex and concave shapes are both supported in either winding order; a polygon must follow its perimeter without self-intersections or holes.

video_osd_privacy_scene_t fields:

Field

Type

Description

version

uint32_t

Set by video_osd_privacy_scene_default() to VIDEO_OSD_PRIVACY_VERSION.

enabled

uint32_t

0 hides every mask while keeping their definitions; 1 (the default) shows them.

coordinates

video_osd_privacy_coordinates_t

VIDEO_OSD_PRIVACY_NORMALIZED (default) or VIDEO_OSD_PRIVACY_REFERENCE_PIXELS.

reference_width / reference_height

uint32_t

Only used when coordinates == VIDEO_OSD_PRIVACY_REFERENCE_PIXELS; must be in [1, 1000000].

fill_rgb

uint32_t

One 0x00RRGGBB opaque fill color for the whole scene. Black (0x000000) by default. There is no per-polygon color and no alpha option.

polygon_count

uint32_t

Number of entries in polygons[], up to 8.

polygons[]

video_osd_privacy_polygon_t[8]

Each has id, enabled, vertex_count and a vertices[] array of {x, y}.

Coordinate systems

Coordinates always refer to image edges: origin at the top-left, x increasing right, y increasing down.

  • VIDEO_OSD_PRIVACY_NORMALIZED (default): each axis is an integer in [0, 10000] (VIDEO_OSD_PRIVACY_NORMALIZED_MAX); (0,0) is the top-left corner and (10000,10000) is the bottom-right corner at any stream resolution.

  • VIDEO_OSD_PRIVACY_REFERENCE_PIXELS: each axis is a pixel coordinate in [0, reference_width] / [0, reference_height]. Use this when the application already works in the pixel space of a reference image (e.g. a snapshot the user drew on) instead of normalized units.

In both systems the right/bottom edge always maps to the encoder’s current output width/height; the component performs no scaling beyond that single mapping. Letterboxing, cropping, mirroring, rotation, and PTZ/world-coordinate transforms are the application’s responsibility before it builds the scene.

Example: normalized polygon

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);

Example: reference-pixel polygon

video_osd_privacy_scene_t scene;
video_osd_privacy_scene_default(&scene);
scene.coordinates = VIDEO_OSD_PRIVACY_REFERENCE_PIXELS;
scene.reference_width = 1920;   /* pixel space the app's vertices were drawn in */
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);

Add, move, delete

video_osd_privacy_apply() always replaces the entire scene; there is no incremental add/remove call. Build the next scene from the application’s own polygon list (or from video_osd_privacy_get()) and:

  • Add a polygon: append it to polygons[] and increment polygon_count.

  • Move a polygon: keep its id, change its vertices[], and re-submit — this is how the example’s update entry works.

  • Disable one polygon without forgetting it: set its enabled = 0.

  • Delete one polygon: omit its id from the next scene.

  • Remove all masks: submit a scene with polygon_count = 0, or set scene.enabled = 0 to hide every polygon while keeping their definitions for a later re-enable.

These are explicit unmasking operations by design, not error fallbacks — the component never silently drops a mask on a partial failure; a rejected apply() leaves the previous accepted scene active instead.

Every apply() call copies the scene it is given, so the caller’s local scene variable can be reused or go out of scope immediately after the call returns. video_osd_privacy_get() returns the last accepted scene, which may not yet be the applied one — check video_osd_privacy_get_status() (or the shared stream status) to distinguish the two.

Coexistence and Region Planning

Composition order, back to front, is fixed: privacy → NN → logo → timestamp. Logo and timestamp may overlap each other or a privacy mask; a translucent logo drawn over a privacy mask blends with that mask’s opaque fill color, not with the camera pixels the mask is covering.

Region format selection

The manager rasterizes only aligned bounding regions, not full-frame overlays by default. Each of the four features contributes a distinct format choice into the planner’s raw table before any merging happens:

Enabled

Initial output choice

None

No pixel buffer or hardware overlay index is used.

Logo (L)

Cropped ARGB region (fixed format for logo).

Timestamp (T)

Cropped bitmap region.

NN (N)

One cropped bitmap region per NN color layer.

Privacy (P)

Cropped bitmap region(s) per polygon.

The planner then merges regions that share an encoder coding block — 64×16 for H.264, 64×64 for HEVC — repeating the merge until no two output regions share a block. Two opaque bitmap regions of the same YUV color remain bitmap after merging; mixed colors, any translucent overlap, or a region that includes an ARGB source (logo) require a localized ARGB8888 region for the merged group. There is no unconditional full-frame ARGB allocation, but a single large connected overlap group can still grow to cover the whole frame — the planner does not guarantee a small region in every layout.

The full 16-combination table (L/T/N/P, each optionally present) collapses to the same block-disjoint pattern above; every combination row is subject to the same merge/arbitration rules. This is a bounded greedy planner: it merges when doing so reduces total pixel payload, or when hardware overlay indices are running low, choosing the option with the smallest payload increase and re-checking for new block conflicts after each merge. It is not a per-block tiling scheme and not a globally optimal packing. NV12 is never selected as an OSD region format by this manager, even though the encoder driver’s native overlay formats include ARGB8888, NV12, and bitmap.

NN arbitration

NN and privacy/logo/timestamp regions are arbitrated per encoder coding block. If any region belonging to one NN group shares a block with a protected logo/timestamp region, or with a privacy polygon’s conservative bounding rectangle, the entire NN group for that callback is omitted — not just the conflicting box. The next NN callback that does not conflict restores the group automatically. This conservative check also applies to the empty notch of a concave polygon; arbitration works on the polygon’s bounding rectangle, not pixel-exact coverage, so a box that only overlaps a concave notch can still be suppressed.

Memory Model

Per-region cost

  • A bitmap region costs aligned_width * height / 8 payload bytes.

  • An ARGB region costs width * height * 4 bytes.

Output region lifetime

Output regions are immutable while the encoder is reading them. A region whose content did not change reuses its existing buffer. A region that did change keeps both its old and new buffers only until the frame-boundary completion for that update is acknowledged, then the old allocation is retired. A static, unchanged region therefore never holds a second output canvas.

Input/source storage (in addition to the output regions above)

  • Logo keeps an owned copy of its ARGB source.

  • Timestamp keeps two small bitmap sources (double-buffered for atomic text updates).

  • NN keeps its original full-frame 1-bit drawing canvas; the managed path additionally copies a strided view into one tight manager-owned shadow buffer rather than an intermediate cropped buffer, but this does not eliminate the original canvas.

Status fields (video_osd_stream_get_status() / video_osd_privacy_get_status(), both return the same video_osd_stream_status_t layout)

Field

Meaning

requested_revision / applied_revision

Revision counters shared by every client on the stream; applied_revision reaches requested_revision once the encoder has confirmed the update.

pending

Non-zero while one transaction is in flight. Only one transaction may be pending at a time.

applied_timestamp

The encoder-side frame timestamp at which the applied revision took effect; use it as minimum_timestamp when opening the output gate.

last_error

0 on success; otherwise one of the VIDEO_OSD_E* codes from the most recent transaction.

allocated_bytes

Manager, descriptor, and live/staged output region allocations.

source_bytes

Sampled registered client source storage, including NN working canvases.

peak_bytes

Largest observed sum of allocated_bytes + source_bytes.

memory_budget

The budget passed to video_osd_stream_init() (or the privacy auto-init compatibility path); 0 means unlimited.

index_count

Reserved physical hardware overlay indices, including one pending update; not the same as the number of logical client handles.

bitmap_regions / argb_regions

Currently applied output region counts by format.

Budget semantics

memory_budget = 0 disables the limit entirely. A non-zero budget is checked at init and at every commit: the manager rejects an update that would push allocated_bytes + source_bytes (including in-flight old/new output buffers) over the budget. It is not a pre-allocation and not a hard cap on the whole process heap:

  • Client source allocations (e.g. a logo’s owned ARGB copy) can happen before the client registers with the manager, and are not counted until registration/commit.

  • RTOS task/semaphore stacks, allocator metadata, and unrelated buffers are excluded.

  • The demo’s V1_OSD_MEMORY_BUDGET of 8 MiB is a ceiling the manager will refuse to exceed, not 8 MiB of memory reserved up front.

video_osd_privacy_get_caps().canvas_bytes is a separate, compatibility-only worst-case estimate — two full-frame ARGB canvases’ worth of bytes — and is not what video_osd_privacy_init() actually allocates. Query video_osd_stream_get_status() for real usage.

Hardware index pool

OSD and mosaic share one 12-index-per-encoder-channel hardware overlay pool. The stream manager is the only party that reserves physical indices from this pool; every managed helper (logo, timestamp, privacy, NN through nn_osd) uses a logical handle internally and does not make its own physical reservation. Enabling mosaic on an index disables OSD on that index and vice versa, so an application that also uses video_set_mosaic()/video_disable_mosaic() directly on the same channel is sharing the same 12 slots with the managed OSD stream.

Error Handling and Shutdown

Synchronous convenience APIs

The eight APIs below hide the status-polling and EBUSY-retry loops that a caller would otherwise write by hand. They do not add a session object, worker task, queue, or new AP/VP protocol; they call the underlying plain APIs described later in this section. Prefer them over manual polling on any managed stream.

API

Success (return 0) means

video_osd_logo_show_sync(ctx, config, timeout_ms)

The initial logo update was accepted and then encoder-applied.

video_osd_timestamp_start_sync(ctx, config, timeout_ms)

The clock started and its initial update was accepted and then encoder-applied.

video_osd_stream_wait_applied(ctx, revision, timeout_ms, status)

The applied revision reached the fixed target, with no reported encoder error.

video_osd_privacy_apply_sync(ctx, scene, revision, timeout_ms)

A complete scene replacement was accepted and then encoder-applied.

video_osd_timestamp_stop_sync(ctx, timeout_ms)

The clock task stopped, removal was confirmed, and the context was released.

video_osd_logo_hide_sync(ctx, timeout_ms)

Removal was confirmed and the context was released.

video_osd_privacy_deinit_sync(ctx, timeout_ms)

Privacy was deinitialized, including an auto-owned stream manager if privacy created one.

video_osd_stream_deinit_sync(ctx, timeout_ms)

The manager was released, after all of its clients were already removed.

show_sync / start_sync initial-activation semantics

  • A matching stream manager must already exist — same stream_id, codec, and frame width/height as the target passed to video_osd_stream_init().

  • ctx must be zero-initialized or fully cleaned up already. A non-empty context (one that is live, or partway through removal) returns VIDEO_OSD_EINVAL — calling this again on the same context is not automatically treated as success the way the removal APIs are.

  • Before acceptance, the call retries EBUSY internally. After acceptance, it waits only for that one initial revision, which is captured under the manager lock so a later timestamp/NN update cannot change what this call is waiting for.

  • Returning 0 means the initial content was already encoder-applied — there is no need to call video_osd_stream_wait_applied() a second time just to confirm this particular client came up.

  • config, its format string (timestamp), and the logo’s source pixels must remain valid and unmodified for the duration of the call. The API copies the format string and pixel rows internally, so the caller’s inputs can be released after the call returns — even if that return is a timeout.

  • timeout_ms covers both the pre-acceptance retry and the post-acceptance wait as one budget, and is still not a hard real-time deadline.

  • A timeout or encoder error can happen after acceptance: the context may still be live, the logo may already be visible, or the clock may already be refreshing. None of this rolls back automatically — do not memset()/free the context, and do not call show()/start() again on the same context.

  • Recovery is the same in every case: call the matching stop_sync/hide_sync (safe even on an already-empty context) and only then attempt show_sync/start_sync again.

  • The call takes three logical arguments (context, config, timeout) and has no output revision parameter. If the timeout happened before acceptance, a later successful video_osd_stream_wait_applied() on some other revision does not prove this particular logo/clock was accepted; use the stop_sync-then-retry pattern above instead of trying to infer acceptance from an unrelated wait.

  • A failure can be VIDEO_OSD_EINVAL, VIDEO_OSD_ENOMEM, VIDEO_OSD_ENOSPC, VIDEO_OSD_EENCODER, VIDEO_OSD_EBUSY, or VIDEO_OSD_ETIMEDOUT depending on where the call failed.

wait_applied revision semantics

  • revision > 0: waits for applied_revision >= revision. This is a specific target, not “wait for the whole stream to go idle.”

  • revision = 0: on the first successful status read, the call snapshots the current requested_revision and uses that as a fixed target. A later timestamp or NN update does not move this target forward.

  • Because of that fixed target, a successful return can still leave pending == 1 in the returned status — the originally targeted revision completed, but a newer, unrelated update is now in flight.

  • Passing a revision that has not been requested yet on this manager returns VIDEO_OSD_EINVAL. A revision value is only meaningful for the lifetime of the video_osd_stream_t it came from.

  • status may be NULL. When it is not, it receives the last successfully read status, including applied_timestamp. If no status read ever succeeded, the caller’s output is left untouched — do not read it after a failed call.

  • If last_error is set in status and no recovery transaction is currently pending, that error is returned. If a recovery transaction is pending, the call keeps waiting instead of failing immediately.

  • Success here still only means encoder-applied, not that any network client or player has displayed the frame.

apply_sync scene-replacement semantics

  • Replaces the entire scene in one call; there is no incremental add/remove variant.

  • Before acceptance, the call retries an EBUSY result internally within the timeout_ms budget.

  • After acceptance, it waits only for that one revision; it never resubmits the scene.

  • Returning 0 means the scene was encoder-applied, not merely that the command was accepted.

  • revision is an optional output; pass NULL for the common case. When non-NULL, it is cleared to 0 at the start of the call and filled in as soon as the scene is accepted — that value is retained even if the call later times out, so the caller can keep waiting on it (see the retry pattern below).

  • The scene argument must remain valid and must not be modified by another task for the entire duration of the call.

  • timeout_ms is one budget covering both the pre-acceptance retry and the post-acceptance wait; it is not restarted between the two phases.

stop_sync / hide_sync / deinit_sync removal semantics

  • Returning 0 means that resource has finished being cleaned up. Calling one of these again on an already-removed or zero-initialized context also returns 0.

  • A NULL context pointer returns VIDEO_OSD_EINVAL — this is a caller error, not success.

  • These sync removals apply only to a managed stream. Calling one on an unmanaged direct-overlay client (no stream manager) returns VIDEO_OSD_EINVAL, because that path has no encoder-completion protocol to wait on — it never fakes a hardware confirmation.

  • For timestamp removal, the update task is stopped as soon as removal begins; the clock does not resume automatically even if the call times out.

  • On error or timeout, keep the context and retry the same _sync API. Do not call the void stop()/hide() instead, do not zero out or manually clear the context, do not free it, and do not reuse the same not-yet-removed context to start()/show() again.

  • video_osd_stream_deinit_sync() does not destroy other clients automatically — logo, timestamp, privacy, and NN must already be removed first.

  • If privacy auto-created its stream manager (see Public APIs), video_osd_privacy_deinit_sync() also releases that manager, but only after every other client on the stream has been removed.

Accepted vs. applied on the plain APIs

video_osd_privacy_apply(), video_osd_logo_show(), and video_osd_timestamp_start() returning 0 means the request was accepted into the pending transaction, not that it is visible on encoded output yet. Poll video_osd_stream_get_status() (or the privacy-specific alias) until pending == 0; then check applied_revision >= revision and last_error == 0 before treating the change as live — or call video_osd_stream_wait_applied() instead of writing this loop by hand. The “applied” completion itself only means the VP encoder’s preprocessing step selected the full region list for one input frame and retired the previous buffers — it is not a network/display acknowledgment to any downstream client.

Errors returned by the plain and sync APIs

  • A non-zero return from apply()/show()/start() (other than VIDEO_OSD_EBUSY) means nothing was submitted or the request was rejected outright; the previous accepted state is unchanged.

  • VIDEO_OSD_EBUSY means a previous transaction is still pending — nothing new was submitted. The plain APIs return it directly to the caller; the _sync helpers retry it internally within their timeout budget instead. Coalesce rapid successive updates (e.g. from a dragging UI) into one replacement rather than submitting every intermediate one.

  • A runtime rejection at the VP encoder surfaces through last_error in status (or as VIDEO_OSD_EENCODER from a _sync/wait_applied call), not through the initial return value. While that error is set, encoded output is intentionally stopped rather than falling back to an unmasked frame; submit a new valid scene/logo/timestamp to recover. Do not open the output gate while status reports an error.

Timeouts

  • timeout_ms bounds one polling/retry budget for that single call, measured against a 64-bit monotonic RTOS clock so it is unaffected by camera time synchronization.

  • EBUSY is retried at intervals of up to 20 ms; the manager mutex is never held across a sleep.

  • timeout_ms = 0 still makes one attempt — if that attempt is busy, the call returns VIDEO_OSD_ETIMEDOUT immediately rather than blocking.

  • UINT32_MAX is a large but finite number of milliseconds, not “wait forever.”

  • An in-flight IPC call, mutex wait, or scheduling delay can make the call return later than timeout_ms — none of these APIs are hard real-time.

  • VIDEO_OSD_ETIMEDOUT does not cancel a submitted command, does not roll back to the previous scene, and does not free a buffer whose completion is still unconfirmed. Keep the context alive and keep retrying/waiting — never free a context or treat it as safe to release on a timeout. If the VP side stops responding entirely, keep the output gate blocked and recover or restart the pipeline under application control; do not assume that unmasking or buffer retirement quietly succeeded.

  • Each API call has its own independent timeout budget. The four sequential removal calls in the shutdown sequence below can therefore consume up to four separate timeout budgets, not one shared budget for the whole stop sequence.

An application that calls the plain APIs directly is responsible for its own status polling, as described above. Check the int return value of the _sync APIs to decide whether an operation succeeded; do not inspect ctx->priv.

Recovering from apply_sync timing out after acceptance

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

if (ret == VIDEO_OSD_ETIMEDOUT && revision != 0) {
    /* Already accepted, completion not yet confirmed: keep waiting on the
     * same revision instead of resubmitting the scene. */
    ret = video_osd_stream_wait_applied(&stream, revision, 5000, NULL);
}
/* ret != 0: do not report the update as successful; keep the context and
 * handle the error. Submitting the scene again is a new replacement, not
 * a way to cancel the one still in flight. */

If the caller does not need to distinguish “not yet accepted” from “accepted but not yet confirmed,” pass NULL for revision and query video_osd_privacy_get_status()/video_osd_stream_get_status() afterward instead.

Startup confirmation and the output gate

video_osd_stream_status_t status;
int ret = video_osd_stream_wait_applied(&stream, 0, 5000, &status);
if (ret != 0) {
    /* Keep the output gate blocked and go to error handling. */
    return ret;
}
/* Use status.applied_timestamp to configure the video_module output gate;
 * enable filter_timestamp based on applied_revision != 0. */

The output gate stays in the MMF example, not in the video_osd core — the core has no dependency on UVC/RTSP. The logo/timestamp sync helpers above do not need this call to confirm their own activation; this one wait obtains a shared status (including any clock/NN update that raced in during startup), not a required second wait per feature.

Consistent activation across the three features

/* Encoder already started, output gate already closed, and the stream
 * manager / privacy context already initialized. */
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) {
    /* Follow the synchronous cleanup path; do not assume a failure here
     * means nothing was accepted. */
    return ret;
}
/* All three are now applied; fetch the shared status.applied_timestamp
 * and open the output gate safely. */

There is no required activation order between the three clients. The example starts privacy first, then logo, then timestamp — so masking is in place before the periodic clock refresh begins — but composition order is decided by the manager, not by call order (see Coexistence and Region Planning).

Shutdown order

Reverse of startup, using the sync removal APIs and checking each return value:

/* Block the output gate and stop the downstream sink/linker first.
 * Keep the encoder running through all four removals below. */
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) {
    /* Keep output blocked and the encoder running; keep every context
     * intact and retry this whole stop sequence later. */
    return ret;
}
/* Only stop the encoder once every removal above returned 0. */
  1. Block the output gate (CMD_VIDEO_SET_OUTPUT_GATE with blocked = 1) and stop the downstream linker/sink first.

  2. Call video_osd_timestamp_stop_sync(), then video_osd_logo_hide_sync(). If NN is in use, stop the NN producer and deinit its NN group(s) through the nn_osd API before continuing.

  3. Call video_osd_privacy_deinit_sync().

  4. Call video_osd_stream_deinit_sync(). The encoder must still be running through steps 2–4 so it can acknowledge each retirement.

  5. Only after every removal above returns 0 should the encoder itself be stopped/closed.

An auto-created manager (from privacy’s compatibility auto-init path) is released by privacy’s own deinit_sync() once every other client on that stream is gone — do not call video_osd_stream_deinit_sync() a second time for it. Do not stop or reconfigure the encoder externally while any of its OSD clients are still being torn down. None of these synchronous APIs may be called from an ISR, from an encoder/IPC callback, or while holding an NN group lock that a background callback needs.

NN Coexistence

NN detection overlays use nn_osd_group_init()/nn_osd_group_flush() from video_nn_utils, not this component’s client APIs directly, but they draw onto the same shared stream manager once video_osd_stream_init() has run. Each nn_osd layer uses one hardware OSD index; use one layer when boxes, labels, and landmarks can share a single color, and multiple layers only when different colors are required, since different-index bitmap regions can overlap.

Do not enable the nn_osd stale-overlay watchdog by default when a detection callback keeps refreshing the overlay every frame — pass 0 for enable_watchdog and skip nn_osd_group_kick() in that case. Enable the watchdog only when a callback may stop arriving while objects are still notionally present (so stale boxes would otherwise linger on screen); if enabled, comment why.

See Coexistence and Region Planning above for how an NN group is suppressed and restored when it conflicts with a protected logo/timestamp region or a privacy polygon’s bounding rectangle.

Limitations and Security

  • This component protects only the one configured encoded stream it was initialized on. JPEG snapshots, raw NV12/RGB streams used for NN input (V4/V5), other simultaneously running encoded streams, and any pre-encoder recording path are not automatically masked. A production design must apply masking — or an equivalent decision — to every output path it exposes, including at startup, on snapshot capture, and during error recovery, not only to the one stream this example configures.

  • Application-side transport of scene updates (HTTP/MQTT/P2P or any other channel), authorization/access control, coordinate-system translation from a UI, and persistence of the last-applied scene across reboots are all the caller’s responsibility; none of it is provided by video_osd/video_osd_privacy.

  • Low-latency/slice mode, look-ahead, multi-pass encoding, and combined H.264/HEVC + JPEG dual encoding are not supported by the managed transaction; this implementation targets the SDK’s single-core, single-pass synchronous encode path. Confirm against the VP encoder driver before relying on any of these together with managed OSD.

  • Do not open direct video_set_osd_bitmap()/video_set_mosaic() (or matching disable) clients on a stream that already has a stream manager; once scene ownership is active, VP rejects direct-overlay writes on that stream. Mosaic remains a separate feature that shares the same 12-index hardware pool, not a backend for privacy polygons.

  • AP and VP firmware — including prebuilt multimedia libraries — must be updated together, since the OSD scene protocol is a shared AP/VP structure (video_osd_scene_t).