杰理 SDK 文档中心
首页
首页
  • SDK 概述与快速开始

    • SDK 概览与 AC791N 芯片平台
    • 环境搭建与编译指南
    • 烧录与固件升级
    • 工程结构导览
  • 产品方案应用

    • WiFi 摄像头方案
    • WiFi IPC 可视对讲方案
    • WiFi 故事机方案
    • 扫码枪 HID 方案
    • 开发板示例工程
  • 公共应用组件

    • 语音识别 ASR 引擎
    • LLM 与 AI 语音助手接入
    • 摄像头传感器驱动
    • UI 显示框架与驱动
    • USB 主机与设备栈
    • 文件系统与存储管理
    • 系统服务与外设管理
    • 生产测试与射频工具
  • 蓝牙协议栈

    • 经典蓝牙 BR/EDR
    • BLE 低功耗蓝牙
    • 蓝牙 Mesh 网络
    • 蓝牙扩展协议(RCSP/广播/无线麦克风)
  • WiFi 与网络协议栈

    • WiFi 驱动与网络模式
    • lwIP TCP/IP 协议栈
    • 网络安全与加密库
    • 应用层网络协议
    • 流媒体与音视频传输
    • 云平台接入 SDK
    • P2P 远程访问与设备互联
  • 芯片平台与驱动

    • wl82 平台与硬件加速
    • 外设驱动框架
    • 平台配置与固件打包工具
  • 媒体与音频引擎

    • 音频编解码与音源
    • 音效处理引擎
    • 视频与图像处理
  • 操作系统与运行时

    • 实时操作系统与 POSIX 层
    • C/C++ 运行时库
  • 开发资源与文档

    • 文档与规格书
    • 公共示例工程
    • UI 资源工程与打包
    • SDK 辅助工具与脚本

流媒体与音视频传输

本页介绍 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 通过"设备抽象 + 协议封装 + 任务调度"三层结构解决这一矛盾:

  1. 设备抽象(stream_core.h):把"任何可读写、可控制的流式设备"(如网络 socket 流、文件流、自定义传输通道)统一抽象为 struct net_video_stream_sub,通过编译器段(section)注册机制实现插件式扩展——新增一种传输通道只需实现四个函数指针并调用注册宏,无需改动上层协议代码。这是典型的策略模式 + 段式注册表设计。

  2. 协议封装(stream_protocol.c):在设备抽象之上实现音视频流协议。它定义了 u32 长度字的高 4 位存放数据类型标记(视频/音频)、低 28 位存放真实长度的编码方式;处理 JieLi 私有容器头(00dc/01wb)与 JPEG SOI 标记的剥离;为视频和音频各分配一块独立的打包缓冲,并通过互斥标志 copy 与信号量 sem 实现"采集回调 → 发送任务"之间的生产者-消费者模型。

  3. 任务调度(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)管理所有运行状态:

源码见 stream_protocol.c

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

源码见 stream_protocol.c

设计意图:视频/音频各用独立缓冲,避免互相覆盖;vd_use/ad_use 标志位保证任一时刻只有"采集回调写入"或"任务读取"一方持有缓冲(生产者-消费者互斥);音频缓冲在 NET_AUDIO_BUF_SIZE 为 0 时整体编译裁掉,节省 RAM。

私有容器头与类型编码

协议层定义了若干 JieLi 私有标记与网络字节序工具:

源码见 stream_protocol.c

#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 位:

源码见 stream_protocol.c

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

关键行为逐行解读:

  1. 句柄为空直接返回 0(静默失败,避免空指针);
  2. len <= 8 的包(纯容器头或空帧)不发送但按原长度"假装成功",保持调用方节奏;
  3. 若负载第 3 个 32 位字是 JPEG SOI 头,说明存在 8 字节容器头,剥离之(buf += 8; len -= 8;);
  4. len &= ~(0xf << 28) 清掉长度字高 4 位,再 len |= type << 28 写入类型码;
  5. 最终把"类型 + 长度"合一的 len 与数据一起交给 stream_write。

设计意图:用长度字高位携带类型,无需额外协议头、不增加带宽开销;对端(PC/App 工具)读取长度字后 len & 0x0fffffff 得到真实长度、len >> 28 得到类型即可分帧。这是典型的"带内元数据"设计。

采集回调与生产者-消费者模型

stream_packet_cb 是采集端(video_server)的入口回调,负责类型识别、缓冲校验、数据拷贝与信号量通知:

源码见 stream_protocol.c

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 是协议层的心脏,完成资源分配、设备打开、事件循环与清理:

源码见 stream_protocol.c

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

控制流解读:

  1. 初始化:创建计数信号量(初值 0)→ 分配视频缓冲(必需)→ 读取 VM_MIC_INDEX 系统配置决定是否启用麦克风音频缓冲(可选,按需分配省 RAM)→ 打开底层流设备;
  2. 主循环:以 10ms 超时等待信号量;被唤醒后若视频/音频缓冲都就绪则清空信号量计数(os_sem_set(&sem, 0),防止一次唤醒处理多帧造成积压);随后先发视频、再发音频,每次发送后立即释放对应 *_use 标志,让回调可以写入下一帧;
  3. 错误处理:stream_write 返回 0 视为致命错误(网络断开、设备异常),置 err 并跳出循环;
  4. 退出清理:置 kill → 等待至多 20ms 让正在进行的拷贝完成(while (copy && to--))→ 关闭设备句柄 → 释放缓冲 → 删除信号量。清理顺序保证不会出现"缓冲已释放但回调仍在 memcpy"的悬挂访问。

任务创建入口

stream_protocol_task_create 是协议层的启动入口,负责参数传递、重复启动防护与线程创建:

源码见 stream_protocol.c

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 位:

源码见 stream_protocol.c

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。

源码见 stream_core.h、stream_protocol.c

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。

源码见 stream_protocol.c

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 标志保证;重启前必须等待旧任务完全退出。

源码见 stream_protocol.c

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_protocol.c

失败模式、边界情况与并发

失败模式

失败场景检测方式处理行为源码位置
设备打开失败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 对音频包不做任何处理。

并发与一致性分析

协议层存在三个并发域:

  1. 采集回调线程 vs 发送任务线程:通过 vd_use/ad_use 标志实现"双缓冲握手"。回调在 !vd_use 时才拷贝并置 vd_use = true;任务发送完成后置回 false。由于标志位是单字节原子操作且运行在单核 RTOS 环境中,此模式无需额外加锁。
  2. 退出竞态:任务退出时可能恰逢回调正在 memcpy。解决方式是用 copy 标志标记拷贝区间,exit 段最多轮询等待 20ms(while (copy && to--) os_time_dly(1))后再释放缓冲——保证"拷贝结束之后才 free"。
  3. 背压丢帧:当网络发送慢于采集时,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 相关页面。
Prev
应用层网络协议
Next
云平台接入 SDK