流媒体与音视频传输
本页介绍 AC79 AIoT SDK 中流媒体与音视频传输能力的完整实现:从底层流核心抽象(stream_core.h)到上层流协议封装(stream_protocol.c),涵盖设备注册机制、音视频分包缓冲、任务调度模型以及网络传输入口。
Purpose and Scope
本页覆盖以下内容:
- 流核心抽象层(
include_lib/net/server/stream_core.h):统一的流式设备接口struct net_video_stream_sub、注册宏REGISTER_NET_VIDEO_STREAM_SUDDEV以及stream_open/write/ctrl/close四个核心 API; - 流协议封装层(
apps/scan_box/stream_protocol.c):基于流核心构建的端到端音视频流协议实现,包括包头类型标记、JPEG 帧头剥离、音频/视频分包缓冲、信号量驱动的发送任务; - 数据包类型约定:视频(JPEG)与音频(PCM)类型码、JieLi 私有容器标记(
JL_ENDF、JL_000DC、JL_001WB)与网络字节序转换。
本页不涉及以下内容(由同目录其他页面覆盖):
- Wi-Fi/以太网等底层网络协议栈的配置与调优(见网络相关页面);
video_server摄像头/录像采集服务的完整实现细节(本页仅说明其作为流数据来源的调用关系);- 具体的播放端/云平台对接协议(如 RTSP、HTTP-FLV 等,若有独立页面则单独介绍)。
相关模块文件:
include_lib/net/server/rt_stream_pkg.h(实时流分包类型定义)、include_lib/net/streaming_media_server/stream_pkg.h、include_lib/net/streaming_media_server/stream_media_mem.h(流媒体服务器内存管理)。
Overview
流媒体传输在嵌入式 AIoT 场景中的核心矛盾是:采集端产生的是高吞吐、强实时的音视频裸数据,而网络侧要求的是可控节奏、可分包、可标记类型的传输单元。AC79 SDK 通过"设备抽象 + 协议封装 + 任务调度"三层结构解决这一矛盾:
设备抽象(stream_core.h):把"任何可读写、可控制的流式设备"(如网络 socket 流、文件流、自定义传输通道)统一抽象为
struct net_video_stream_sub,通过编译器段(section)注册机制实现插件式扩展——新增一种传输通道只需实现四个函数指针并调用注册宏,无需改动上层协议代码。这是典型的策略模式 + 段式注册表设计。协议封装(stream_protocol.c):在设备抽象之上实现音视频流协议。它定义了
u32长度字的高 4 位存放数据类型标记(视频/音频)、低 28 位存放真实长度的编码方式;处理 JieLi 私有容器头(00dc/01wb)与 JPEG SOI 标记的剥离;为视频和音频各分配一块独立的打包缓冲,并通过互斥标志copy与信号量sem实现"采集回调 → 发送任务"之间的生产者-消费者模型。任务调度(stream_protocol_task):独立的流协议发送任务负责从缓冲中取出已就绪的音视频包,调用底层
stream_write发送;采集回调(stream_packet_cb)只做轻量拷贝与信号量通知,保证采集侧不被网络阻塞(或网络不被采集侧饿死)。
关键概念
| 概念 | 说明 |
|---|---|
net_video_stream_sub | 流式设备的统一接口描述(open/write/ctrl/close 四函数指针 + 私有数据) |
REGISTER_NET_VIDEO_STREAM_SUDDEV | 把设备描述符静态放入 .net_video_stream 段,运行时由 begin/end 边界遍历 |
stream_open/write/ctrl/close | 流核心对外暴露的 API,内部根据 path 在设备注册表中查找匹配设备 |
| 类型码(type) | 长度字高 4 位,如 JPEG_TYPE_VIDEO、PCM_TYPE_AUDIO,接收端据此分发 |
stream_packet_cb | 采集端(video_server)回调入口,负责数据拷贝与信号量通知 |
stream_protocol_task | 流协议发送任务,消费缓冲并实际执行网络写入 |
Architecture
下图展示流媒体传输的整体分层架构与依赖关系:
flowchart TD
subgraph sg_App["应用层(apps/scan_box)"]
PTL["stream_protocol.c<br/>协议封装 + 发送任务"]
PKTCB["stream_packet_cb<br/>采集回调"]
end
subgraph sg_Core["流核心层(include_lib/net/server)"]
CORE["stream_core.h<br/>stream_open/write/ctrl/close"]
REG["net_video_stream_sub<br/>设备注册表(.net_video_stream 段)"]
PKGDEF["rt_stream_pkg.h / stream_pkg.h<br/>数据包类型定义"]
end
subgraph sg_Dev["流式设备实现"]
DEV1["网络流设备<br/>open/write/ctrl/close 实现"]
DEV2["文件/其他流设备<br/>open/write/ctrl/close 实现"]
end
subgraph sg_Src["数据源"]
VS["video_server<br/>视频/音频采集"]
end
VS -->|"数据回调"| PKTCB
PKTCB -->|"拷贝 + os_sem_post"| PTL
PTL -->|"stream_open/stream_write"| CORE
CORE -->|"按 path 查表"| REG
REG --> DEV1
REG --> DEV2
PTL -.->|"引用类型常量"| PKGDEF
架构说明:
- 数据源层:
video_server(采集服务)在产生一帧视频或一段音频时,通过stream_packet_cb(type, buf, size)回调进入流协议层。回调中携带的类型来自rt_stream_pkg.h(如VIDEO_REC_JPEG_TYPE_VIDEO、VIDEO_REC_PCM_TYPE_AUDIO)。 - 协议层:回调只做"拷贝到分包缓冲 +
os_sem_post"两个动作;真正的发送由stream_protocol_task完成。这样把中断/采集上下文与网络发送上下文解耦。 - 核心层:
stream_open(path, mode)用path在段注册表中定位net_video_stream_sub设备,之后stream_write/ctrl/close均通过函数指针转发到具体设备实现——新增传输通道不改协议层。 - 设备层:网络流设备(如 TCP/UDP socket 流)实现四个函数指针,是数据真正离开芯片的出口。
流核心抽象层(stream_core.h)
设备接口结构体
struct net_video_stream_sub 定义了流式设备必须实现的四个操作,外加名称与私有数据指针:
源码见 stream_core.h
struct net_video_stream_sub {
const char *name;
void *(*open)(const char *path, const char *mode);
int (*write)(void *file, void *buf, u32 len, u8 type);
int (*ctrl)(void *file, u32 cmd, u32 arg);
int (*close)(void *file);
void *private_data;
};
设计意图:
open返回一个不透明句柄(void *file),后续所有操作都围绕该句柄展开,这与标准文件系统 API 一致,便于复用现有抽象;write相比传统fwrite多了一个type参数,用于在写入时标记数据类型(视频/音频),网络对端可据此分流;name字段与path匹配(由stream_open内部使用),private_data供设备实现携带上下文(如 socket 句柄、锁等)。
段式注册机制
#define REGISTER_NET_VIDEO_STREAM_SUDDEV(dev) \
static const struct net_video_stream_sub dev SEC_USED(.net_video_stream)
extern struct net_video_stream_sub net_video_stream_sub_begin[];
extern struct net_video_stream_sub net_video_stream_sub_end[];
源码见 stream_core.h
该机制利用链接器的段(section)特性:所有通过宏注册的设备描述符被静态放置到 .net_video_stream 段中,运行时通过 begin/end 符号遍历。这种"零初始化开销、编译期收集"的注册方式非常适合资源受限的嵌入式环境——不需要动态数组、不需要注册 API 调用,设备的增删只发生在链接期。
核心 API 原型
void *stream_open(const char *path, const char *mode);
int stream_write(void *file, void *buf, u32 len);
int stream_ctrl(void *file, u32 cmd, u32 arg);
int stream_close(void *file);
源码见 stream_core.h
注意 stream_write 的公开原型不带 type 参数——类型信息由协议层通过长度字高 4 位编码后随数据一起传递(见下文),设备层写入时自行解析。stream_ctrl 是通用控制通道(如设置码率、查询状态、断开连接等),命令码由具体设备定义。
流协议封装层(stream_protocol.c)
全局状态与缓冲布局
协议层通过一个全局单例 strm_ptl(struct strm_ptl_info)管理所有运行状态:
struct strm_ptl_info {
u8 kill: 1; // 停止标志,任务退出信号
u8 vd_use: 1; // 视频缓冲已被占用(待发送)
u8 ad_use: 1; // 音频缓冲已被占用(待发送)
u8 err: 1; // 错误标志,出错后终止任务
u8 init: 1; // 协议已初始化(任务已运行)
u8 copy: 1; // 正在执行缓冲拷贝(用于退出时同步)
struct strm_path spath; // 打开的设备路径与模式
void *ptl_fd; // 底层流句柄(stream_open 返回值)
OS_SEM sem; // 数据就绪信号量
u8 *video_pkbuff; // 视频分包缓冲
u8 *audio_pkbuff; // 音频分包缓冲
u32 vd_len; // 视频包长度
u32 ad_len; // 音频包长度
int pid; // 任务 ID
};
struct strm_ptl_info strm_ptl_info_ = {0};
#define strm_ptl (&strm_ptl_info_)
缓冲大小由 video_buf_config.h 中的宏决定,默认取录像帧缓冲的一半:
#define VIDEO_PKBUFF_SIZE (NET_VREC0_FBUF_SIZE / 2)
#if NET_AUDIO_BUF_SIZE
#define AUDIO_PKBUFF_SIZE (NET_AUDIO_BUF_SIZE / 2)
#endif
设计意图:视频/音频各用独立缓冲,避免互相覆盖;vd_use/ad_use 标志位保证任一时刻只有"采集回调写入"或"任务读取"一方持有缓冲(生产者-消费者互斥);音频缓冲在 NET_AUDIO_BUF_SIZE 为 0 时整体编译裁掉,节省 RAM。
私有容器头与类型编码
协议层定义了若干 JieLi 私有标记与网络字节序工具:
#define jl_ntohl(x) (u32)((((u32)(x))>>24) | ((((u32)(x))>>8)&0xff00) | (((u32)(x))<<24) | ((((u32)(x))&0xff00)<<8))
#define JL_ENDF jl_ntohl(0x56185719) // 流结束标记(网络序)
#define JPEG_HEAD 0xE0FFD8FF // JPEG SOI 头(小端视图)
#define JPEG_HEAD1 0xC0FFD8FF // JPEG SOI 头(变体)
#define JL_000DC jl_ntohl(0x30306463) // "00dc" 容器标记(视频分片)
#define JL_001WB jl_ntohl(0x30317762) // "01wb" 容器标记(音频分片)
这些标记的作用:
JPEG_HEAD/JPEG_HEAD1是 JPEG 文件的 SOI 起始标记(FF D8 FF E0/FF D8 FF C0)在 32 位小端读取时的形态,用于识别一帧 JPEG 数据的真实起始位置;JL_000DC(ASCII"00dc")与JL_001WB(ASCII"01wb")是 JieLi 私有容器格式的分片标识,出现在数据前 8 字节;- 当检测到容器头 + JPEG 头共存时(
*head == JL_000DC && *(head+2) == JPEG_HEAD),数据前 8 字节为容器头,需剥离后才是纯 JPEG 载荷。
数据发送的封装与类型标记
stream_protocol_write 是任务调用底层写入口的封装,核心是把类型码编码进长度字的高 4 位:
static int stream_protocol_write(u8 type, void *buf, u32 len)
{
u32 *len_ptr = (u32 *)buf;
if (!strm_ptl->ptl_fd) {
return 0;
} else if (len <= 8) {
return len;
}
if (*(len_ptr + 2) == JPEG_HEAD || *(len_ptr + 2) == JPEG_HEAD1) {
buf += 8;
len -= 8;
}
len &= ~(0xf << 28);
len |= type << 28;
return stream_write(strm_ptl->ptl_fd, buf, len);
}
关键行为逐行解读:
- 句柄为空直接返回 0(静默失败,避免空指针);
len <= 8的包(纯容器头或空帧)不发送但按原长度"假装成功",保持调用方节奏;- 若负载第 3 个 32 位字是 JPEG SOI 头,说明存在 8 字节容器头,剥离之(
buf += 8; len -= 8;); len &= ~(0xf << 28)清掉长度字高 4 位,再len |= type << 28写入类型码;- 最终把"类型 + 长度"合一的
len与数据一起交给stream_write。
设计意图:用长度字高位携带类型,无需额外协议头、不增加带宽开销;对端(PC/App 工具)读取长度字后 len & 0x0fffffff 得到真实长度、len >> 28 得到类型即可分帧。这是典型的"带内元数据"设计。
采集回调与生产者-消费者模型
stream_packet_cb 是采集端(video_server)的入口回调,负责类型识别、缓冲校验、数据拷贝与信号量通知:
int stream_packet_cb(u8 type, u8 *buf, u32 size)
{
u32 *head = (u32 *)buf;
if (!strm_ptl->init || strm_ptl->err || strm_ptl->kill) {
return 0;
} else if (size <= 8 || *head == JL_ENDF) {
return size;
}
if (type == VIDEO_REC_JPEG_TYPE_VIDEO) {
if (size > VIDEO_PKBUFF_SIZE) {
printf("VDPKBUFF_SIZE no enough !!!\n");
return 0;
}
if ((*head == JL_000DC && *(head + 2) == JPEG_HEAD) || *(head + 2) == JPEG_HEAD ||
(*head == JL_000DC && *(head + 2) == JPEG_HEAD1) || *(head + 2) == JPEG_HEAD1) {
buf += 8;
size -= 8;
}
if (!strm_ptl->vd_use && !strm_ptl->kill) {
strm_ptl->copy = true;
strm_ptl->vd_use = true;
strm_ptl->vd_len = size;
memcpy(strm_ptl->video_pkbuff, buf, size);
strm_ptl->copy = false;
os_sem_post(&strm_ptl->sem);
}
} else if (type == VIDEO_REC_PCM_TYPE_AUDIO) {
#ifdef AUDIO_PKBUFF_SIZE
if (size > AUDIO_PKBUFF_SIZE) {
printf("ADPKBUFF_SIZE no enough !!!\n");
return 0;
}
if (!strm_ptl->ad_use && !strm_ptl->kill) {
strm_ptl->copy = true;
strm_ptl->ad_use = true;
strm_ptl->ad_len = size;
memcpy(strm_ptl->audio_pkbuff, buf, size);
strm_ptl->copy = false;
os_sem_post(&strm_ptl->sem);
}
#endif
}
return size;
}
设计意图与并发要点:
- 回调运行在采集线程上下文,必须"快进快出":只做一次
memcpy和一次os_sem_post,实际发送交给任务线程,避免网络慢速拖垮采集; copy标志用于退出同步:任务退出时会等待正在进行的拷贝完成(见下文exit段),防止"拷贝中释放缓冲"的竞态;- 当视频/音频缓冲均被占用(
vd_use && ad_use)时,后续帧被丢弃(if (!vd_use ...)条件不满足),这是有损但可控的背压策略——在网络慢于采集时主动丢帧保实时; - 检测到
JL_ENDF(流结束标记)时回调直接返回size而不进入拷贝,由上层处理收尾。
发送任务:生命周期与主循环
stream_protocol_task 是协议层的心脏,完成资源分配、设备打开、事件循环与清理:
static void stream_protocol_task(void *p)
{
int ret;
int to = 20;
os_sem_create(&strm_ptl->sem, 0);
strm_ptl->video_pkbuff = malloc(VIDEO_PKBUFF_SIZE);
if (!strm_ptl->video_pkbuff) {
printf("err : no mem for vd pkbuff !!!\n");
strm_ptl->err = true;
goto exit;
}
#ifdef AUDIO_PKBUFF_SIZE
u8 mic = 0;
if (syscfg_read(VM_MIC_INDEX, &mic, sizeof(mic)) > 0 && mic) {
strm_ptl->audio_pkbuff = malloc(AUDIO_PKBUFF_SIZE);
if (!strm_ptl->audio_pkbuff) {
printf("err : no mem for ad pkbuff !!!\n");
strm_ptl->err = true;
goto exit;
}
}
#endif
ret = stream_protocol_open(strm_ptl->spath.path, strm_ptl->spath.mode);
if (ret) {
printf("stream_protocol_open err, path : %s \n", strm_ptl->spath.path);
strm_ptl->err = true;
goto exit;
}
strm_ptl->init = true;
printf("stream_protocol run \n");
while (1) {
ret = os_sem_pend(&strm_ptl->sem, 10);
if (strm_ptl->kill) {
break;
}
if (!ret) {
if (strm_ptl->vd_use && strm_ptl->ad_use) {
os_sem_set(&strm_ptl->sem, 0);
}
if (strm_ptl->vd_use && strm_ptl->vd_len) {
ret = stream_protocol_write(JPEG_TYPE_VIDEO, strm_ptl->video_pkbuff, strm_ptl->vd_len);
if (!ret) {
strm_ptl->err = true;
printf("err : video stream_protocol_write, kill task !!!\n");
break;
}
}
strm_ptl->vd_use = false;
#ifdef AUDIO_PKBUFF_SIZE
if (strm_ptl->ad_use && strm_ptl->ad_len) {
ret = stream_protocol_write(PCM_TYPE_AUDIO, strm_ptl->audio_pkbuff, strm_ptl->ad_len);
if (!ret) {
strm_ptl->err = true;
printf("err : audio stream_protocol_write, kill task !!!\n");
break;
}
}
strm_ptl->ad_use = false;
#endif
}
}
exit:
strm_ptl->kill = true;
while (strm_ptl->copy && to--) {
os_time_dly(1);
}
stream_protocol_close();
if (strm_ptl->video_pkbuff) {
free(strm_ptl->video_pkbuff);
strm_ptl->video_pkbuff = NULL;
}
if (strm_ptl->audio_pkbuff) {
free(strm_ptl->audio_pkbuff);
strm_ptl->audio_pkbuff = NULL;
}
if (os_sem_valid(&strm_ptl->sem)) {
os_sem_del(&strm_ptl->sem, 0);
}
}
控制流解读:
- 初始化:创建计数信号量(初值 0)→ 分配视频缓冲(必需)→ 读取
VM_MIC_INDEX系统配置决定是否启用麦克风音频缓冲(可选,按需分配省 RAM)→ 打开底层流设备; - 主循环:以 10ms 超时等待信号量;被唤醒后若视频/音频缓冲都就绪则清空信号量计数(
os_sem_set(&sem, 0),防止一次唤醒处理多帧造成积压);随后先发视频、再发音频,每次发送后立即释放对应*_use标志,让回调可以写入下一帧; - 错误处理:
stream_write返回 0 视为致命错误(网络断开、设备异常),置err并跳出循环; - 退出清理:置
kill→ 等待至多 20ms 让正在进行的拷贝完成(while (copy && to--))→ 关闭设备句柄 → 释放缓冲 → 删除信号量。清理顺序保证不会出现"缓冲已释放但回调仍在 memcpy"的悬挂访问。
任务创建入口
stream_protocol_task_create 是协议层的启动入口,负责参数传递、重复启动防护与线程创建:
int stream_protocol_task_create(char *path, char *mode)
{
int ret;
int to = 100;
if (strm_ptl->init) {
printf("stream_protocol_task already open \n");
return 0;
}
strm_ptl->spath.path = path;
strm_ptl->spath.mode = mode;
strm_ptl->ptl_fd = NULL;
strm_ptl->kill = 0;
strm_ptl->err = 0;
...
设计要点:
init标志提供幂等性:任务已运行时重复调用直接返回 0,防止上层(如 App 命令处理)误触发多次启动导致资源泄漏;path/mode通过spath结构传递给任务线程,path形如流设备名(与net_video_stream_sub.name匹配),mode为打开模式;- 新任务创建前会等待上一任务完全退出(
to = 100次轮询),保证设备句柄与缓冲的排他使用。
Core Flow
端到端数据流时序
sequenceDiagram
participant VS as video_server 采集
participant CB as stream_packet_cb
participant TSK as stream_protocol_task
participant CORE as stream_write(核心层)
participant DEV as net_video_stream_sub 设备
VS->>CB: 视频帧/音频包 (type, buf, size)
CB->>CB: 校验 init/err/kill、大小、剥离容器头
CB->>CB: memcpy 到 video/audio_pkbuff
CB->>TSK: os_sem_post(sem)
TSK->>TSK: os_sem_pend 被唤醒
TSK->>TSK: 类型码写入长度字高4位 (type<<28 | len)
TSK->>CORE: stream_write(fd, buf, len|type)
CORE->>DEV: dev->write(file, buf, len, type)
DEV-->>TSK: 写入字节数/0
TSK->>TSK: 失败则 err=true 退出清理
任务生命周期状态机
stateDiagram-v2
[*] --> IDLE: task_create 前
IDLE --> INIT: stream_protocol_task_create
INIT --> RUNNING: 缓冲分配成功 + 设备打开成功
INIT --> FAILED: malloc/open 失败
RUNNING --> RUNNING: 收到数据帧并发送
RUNNING --> FAILED: stream_write 返回 0
RUNNING --> STOPPING: kill 置位
STOPPING --> IDLE: 等待拷贝完成 + 释放资源
FAILED --> IDLE: 清理后回到空闲
关键路径耗时分析
单帧端到端路径为:回调 memcpy(内存拷贝)→ 信号量唤醒(调度延迟)→ 长度字编码(几条位运算)→ 设备写(网络发送)。其中内存拷贝与网络发送是主要耗时点:视频缓冲大小为 NET_VREC0_FBUF_SIZE/2,音频为 NET_AUDIO_BUF_SIZE/2,分包尺寸被限制在缓冲以内,超出部分直接丢弃并打印告警(VDPKBUFF_SIZE no enough)。
Usage Examples
基本用法:启动流协议任务并打开网络流设备
以下代码展示了上层如何启动整个流媒体传输链路。path 为流设备名(与 net_video_stream_sub.name 匹配),mode 为打开模式:
源码见 stream_protocol.c(节选,
stream_protocol_task_create开头部分)
int stream_protocol_task_create(char *path, char *mode)
{
int ret;
int to = 100;
if (strm_ptl->init) {
printf("stream_protocol_task already open \n");
return 0;
}
strm_ptl->spath.path = path;
strm_ptl->spath.mode = mode;
strm_ptl->ptl_fd = NULL;
strm_ptl->kill = 0;
strm_ptl->err = 0;
// ... 创建 stream_protocol_task 线程
}
调用方式(示意,来自 scan_box 应用):stream_protocol_task_create("net", "w+") 之类——具体设备名由该应用传入,例如网络流设备注册名。启动后,video_server 采集到的音视频帧会经 stream_packet_cb 自动进入发送流水线。
底层设备接入:注册一个流式设备
新增一种传输通道(如自定义 TCP 流、文件流)时,实现 struct net_video_stream_sub 的四个函数指针并用注册宏挂载:
源码见 stream_core.h
struct net_video_stream_sub {
const char *name;
void *(*open)(const char *path, const char *mode);
int (*write)(void *file, void *buf, u32 len, u8 type);
int (*ctrl)(void *file, u32 cmd, u32 arg);
int (*close)(void *file);
void *private_data;
};
#define REGISTER_NET_VIDEO_STREAM_SUDDEV(dev) \
static const struct net_video_stream_sub dev SEC_USED(.net_video_stream)
设备实现示例(示意骨架,函数体由具体传输实现):
static void *my_stream_open(const char *path, const char *mode) { /* 建立连接 */ }
static int my_stream_write(void *file, void *buf, u32 len, u8 type) { /* 发送数据 */ }
static int my_stream_ctrl(void *file, u32 cmd, u32 arg) { /* 控制命令 */ }
static int my_stream_close(void *file) { /* 断开连接 */ }
REGISTER_NET_VIDEO_STREAM_SUDDEV(my_stream_dev) = {
.name = "mystream",
.open = my_stream_open,
.write = my_stream_write,
.ctrl = my_stream_ctrl,
.close = my_stream_close,
.private_data = NULL,
};
注册后,上层即可用 stream_open("mystream", "w+") 打开该设备——无需修改任何协议层代码,这正是段式注册表的设计价值。
发送路径中的类型编码
协议层在把视频帧交给核心层前,把类型码写入长度字高 4 位:
static int stream_protocol_write(u8 type, void *buf, u32 len)
{
u32 *len_ptr = (u32 *)buf;
if (!strm_ptl->ptl_fd) {
return 0;
} else if (len <= 8) {
return len;
}
if (*(len_ptr + 2) == JPEG_HEAD || *(len_ptr + 2) == JPEG_HEAD1) {
buf += 8;
len -= 8;
}
len &= ~(0xf << 28);
len |= type << 28;
return stream_write(strm_ptl->ptl_fd, buf, len);
}
对端解析约定:len & 0x0fffffff 得到净荷长度,len >> 28 得到类型(JPEG_TYPE_VIDEO / PCM_TYPE_AUDIO)。
Configuration Options
| 配置项 | 类型 | 默认来源 | 说明 |
|---|---|---|---|
VIDEO_PKBUFF_SIZE | 宏(字节) | NET_VREC0_FBUF_SIZE / 2 | 视频分包缓冲大小,定义于 stream_protocol.c;超出该大小的视频帧被丢弃 |
AUDIO_PKBUFF_SIZE | 宏(字节) | NET_AUDIO_BUF_SIZE / 2 | 音频分包缓冲大小;NET_AUDIO_BUF_SIZE 为 0 时整段音频逻辑编译裁掉 |
NET_AUDIO_BUF_SIZE | 宏 | video_buf_config.h | 音频采集缓冲,决定是否启用音频流 |
VM_MIC_INDEX | 系统配置项 | syscfg | 麦克风开关;为真时任务才分配音频分包缓冲 |
strm_ptl->sem 超时 | 常量(ms) | 10 | 主循环 os_sem_pend 超时,保证 kill 能被及时响应 |
退出等待 to | 常量 | 20(拷贝)/ 100(任务退出) | 退出时等待拷贝完成、等待旧任务结束的轮询次数 |
缓冲相关宏见 stream_protocol.c,缓冲区大小来自
video_buf_config.h(NET_VREC0_FBUF_SIZE、NET_AUDIO_BUF_SIZE)。
API Reference
流核心层(stream_core.h)
void *stream_open(const char *path, const char *mode)
按名称打开流式设备。
- 参数:
path— 设备名,须与某个已注册net_video_stream_sub.name匹配;mode— 打开模式(如"w+")。 - 返回:不透明句柄;失败返回
NULL。协议层在句柄为 NULL 时打印stream_protocol_open err !!!并返回 -1。
int stream_write(void *file, void *buf, u32 len)
向流设备写入数据。
- 参数:
file—stream_open返回的句柄;buf— 数据指针;len— 长度(协议层把类型码编码进高 4 位)。 - 返回:写入的字节数;返回 0 表示致命错误,协议层会置
err并终止任务。
int stream_ctrl(void *file, u32 cmd, u32 arg)
向流设备发送控制命令。
- 参数:
cmd— 设备自定义命令码;arg— 命令参数。 - 返回:命令执行结果(0 或设备自定义值)。协议层在句柄为空时直接返回 0。
int stream_close(void *file)
关闭流设备并释放句柄。
- 参数:
file— 要关闭的句柄。 - 返回:0 成功;协议层关闭后会将
ptl_fd置 NULL 防止悬挂。
协议层(stream_protocol.c)
int stream_protocol_task_create(char *path, char *mode)
创建并启动流协议发送任务(内部线程)。
- 参数:
path— 目标流设备名;mode— 打开模式。 - 返回:0 成功;任务已运行时重复调用返回 0(幂等);启动失败返回负值。
- 并发约束:同一时刻仅允许一个实例,通过
init标志保证;重启前必须等待旧任务完全退出。
int stream_packet_cb(u8 type, u8 *buf, u32 size)
采集数据回调(由 video_server 调用,运行在采集线程上下文)。
- 参数:
type— 数据类型(VIDEO_REC_JPEG_TYPE_VIDEO/VIDEO_REC_PCM_TYPE_AUDIO);buf— 帧数据;size— 帧长度。 - 返回:消费的字节数;未初始化/出错/停止时返回 0;空包或
JL_ENDF结束标记返回size。 - 行为:视频/音频包拷贝进各自分包缓冲并
os_sem_post;缓冲被占用或尺寸超限时丢弃。
失败模式、边界情况与并发
失败模式
| 失败场景 | 检测方式 | 处理行为 | 源码位置 |
|---|---|---|---|
| 设备打开失败 | stream_open 返回 NULL | 打印 stream_protocol_open err,置 err,任务跳转 exit 清理 | stream_protocol.c#L50-L58 |
| 视频缓冲分配失败 | malloc 返回 NULL | 打印 no mem for vd pkbuff,置 err 退出 | stream_protocol.c#L149-L154 |
| 音频缓冲分配失败 | malloc 返回 NULL(仅在麦克风开启时) | 打印告警并退出;麦克风关闭时根本不会分配 | stream_protocol.c#L155-L165 |
| 网络写入失败 | stream_write 返回 0 | 打印 video/audio stream_protocol_write, kill task,置 err 并 break 退出 | stream_protocol.c#L185-L204 |
| 帧超长 | size > VIDEO_PKBUFF_SIZE 等 | 打印 VDPKBUFF_SIZE no enough,丢弃该帧(返回 0) | stream_protocol.c#L107-L110 |
| 重复启动 | init 标志为真 | 打印 already open,直接返回 0(幂等) | stream_protocol.c#L231-L234 |
边界情况
- 空包与小包:
len <= 8(纯容器头/空帧)不发送但按原长度返回"成功",避免破坏发送节奏;回调侧对size <= 8的包同样直接返回。 JL_ENDF结束标记:收到该标记时回调不拷贝、不通知,直接返回size,由上层负责收尾——它是"流结束"的带内信号。- JPEG 容器头共存:
00dc容器头 + JPEG SOI 头同时存在时,前 8 字节是容器头,发送与拷贝前均剥离,保证对端拿到的是纯净 JPEG 载荷。 - 音频未开启:
NET_AUDIO_BUF_SIZE为 0 或VM_MIC_INDEX为假时,音频缓冲不分配、音频分支编译裁掉,stream_packet_cb对音频包不做任何处理。
并发与一致性分析
协议层存在三个并发域:
- 采集回调线程 vs 发送任务线程:通过
vd_use/ad_use标志实现"双缓冲握手"。回调在!vd_use时才拷贝并置vd_use = true;任务发送完成后置回false。由于标志位是单字节原子操作且运行在单核 RTOS 环境中,此模式无需额外加锁。 - 退出竞态:任务退出时可能恰逢回调正在
memcpy。解决方式是用copy标志标记拷贝区间,exit段最多轮询等待 20ms(while (copy && to--) os_time_dly(1))后再释放缓冲——保证"拷贝结束之后才 free"。 - 背压丢帧:当网络发送慢于采集时,
vd_use/ad_use长期为真,回调直接丢弃新帧。这是有损但实时的设计取舍:流媒体场景宁可丢帧也不阻塞采集,从而维持最低时延。
性能与运维注意事项
- 内存预算:视频分包缓冲
NET_VREC0_FBUF_SIZE/2+ 音频NET_AUDIO_BUF_SIZE/2,均在任务启动时malloc、退出时free。RAM 紧张的板卡应优先调小NET_AUDIO_BUF_SIZE或关闭麦克风(不分配音频缓冲)。 - 发送优先级:主循环固定"先视频后音频",且一次唤醒只处理一帧(
os_sem_set(&sem, 0)清计数)。该策略保证视频帧优先占用带宽,音频为视频让路——与监控类场景"画面优先"的需求一致。 - 超时与响应性:
os_sem_pend超时 10ms,即使长时间无数据,任务也能周期性检查kill,保证停止指令的响应延迟 ≤ 10ms。 - 调试输出:任务运行、打开失败、缓冲不足、写入失败均有
printf关键日志(stream_protocol run、VDPKBUFF_SIZE no enough等),现场排查可据此快速定位是资源问题还是网络问题。 - 对端分帧约定:接收端必须按"高 4 位类型 + 低 28 位长度"解析长度字,先取
len >> 28判断类型、len & 0x0fffffff得到净荷长度,再按类型分别重组视频/音频流。
扩展点
- 新增流式传输设备:实现
struct net_video_stream_sub的open/write/ctrl/close并用REGISTER_NET_VIDEO_STREAM_SUDDEV注册,即可被stream_open(path, ...)按名访问。设备名通过path参数对上,协议层零改动。 - 新增数据类型:在长度字高 4 位中扩展类型码(当前使用
JPEG_TYPE_VIDEO、PCM_TYPE_AUDIO),并在stream_packet_cb中增加分支与对应分包缓冲;需同步更新对端解析逻辑。 - 控制命令:通过
stream_ctrl传递设备自定义命令(如码率调整、连接断开、统计查询),命令码由具体设备定义——协议层只做透传。 - 采集源替换:
stream_packet_cb是唯一采集入口,任何能产出(type, buf, size)的模块(当前为 video_server)都可以接入本协议,无需改动任务主体。
测试情况
本次源码勘察中,仓库内未发现针对 stream_protocol.c / stream_core.h 的独立单元测试文件;该模块的验证主要依赖集成运行路径:apps/scan_box(扫描盒应用)通过 stream_protocol_task_create 启动任务、video_server 作为采集源驱动回调,配合对端 PC 工具解析长度字完成端到端联调。协议层的健壮性(重复启动幂等、退出竞态等待、写失败自终止)设计上即为在真实网络环境下运行的防御性实现。
Related Links
- stream_core.h(流核心接口)
- stream_protocol.c(流协议封装示例)
- rt_stream_pkg.h(实时流分包类型定义)
- stream_pkg.h(流分包定义)
- stream_media_mem.h(流媒体服务器内存管理)
- stream_pkg.h(流媒体服务器分包)
- 相关目录:Wi-Fi/网络协议栈配置见网络相关页面;视频采集服务见 video_server 相关页面。