视频流传输

更新时间:2022-11-24 09:20:26

背景介绍

Tuya IoT 平台支持加载流媒体组件,使产品具有视频传输控制的能力。此可视化产品方案采用的是流媒体组件的 RTSP 服务。RTSP 服务提供一种数据通信能力,用于处理客户端(手机 App 等)和设备端之间实时视频请求。
RTSP服务包括两个基本功能:

  1. 连接:用户可以在没有联网能力的情况下,客户端和设备端建立 TCP 连接,基于该连接可以传输视频数据。
  2. 流媒体播放:RTSP 以客户端方式工作,提供直播服务。

产品应用场景介绍

此方案使用 BK7231U 作为主芯片,该芯片内部集成了蓝牙双模 5.1 和 Wi-Fi 802.11n 两种通讯方式所需的硬件和软件资源。同时,流媒体组件也在芯片上通过RTSP协议与手机APP连接。

  • 适用于 Tuya 多代码开发方案中,美妆、个护、户外类的产品。
  • 此方案可以提供 OTA 升级能力、流媒体传输能力,和接入涂鸦后具备的OTA升级能力。
  • 此方案常见的使用场景为可视挖耳勺,可视黑头仪,可视冲牙器等。
  • 由于主控芯片的硬件限制,不适用于复杂功能的产品。

依赖项

  • 面板-挖耳勺,且需要联系涂鸦项目经理添加可视化白名单面板

  • App 尚未发布到 Tuya 公版,目前只有定制 OEM App 才支持 RTSP

  • SDK 暂只支持 BK7231U 芯片,SDK 需要联系 Tuya 项目经理获取

  • 检查 SDK 是否包含 tuya_svc_lan_rtsp.ctuya_svc_lan_rtsp.h文件。如无,联系 Tuya 项目经理获取支持可视化的 SDK

交互流程

应用与SDK交互流程

视频流传输

视频出图流程

视频流传输
缓冲区的大小根据帧格式来确定,VGA格式640*480需要30K+。

app控制流程

视频流传输

API 概览

视频流控制结构的回调注册

函数名 tuya_svc_lan_reg_rtsp_stream_src
函数说明 此函数通常在设备初始化完成之后调用(device_init完成),主要的功能是将 rtsp_stream_src_t 中的传输控制流的回调注册给SDK,使得RTSP server启动后调用这些逻辑实现实时传流功能。
函数参数 rtsp_stream_src_t 结构体指针
返回值 tuya_error_code.h
#include "tuya_svc_lan_rtsp.h"
/**
 * @brief register stream src to rtsp
 * 
 * @param[in] src stream info
 *
 * @return OPRT_OK when success. Others errors, please refer to tuya_error_code.h
 */
OPERATE_RET tuya_svc_lan_reg_rtsp_stream_src(rtsp_stream_src_t src);

视频流的控制结构

变量 说明
get_sample_rate 获取采样率
get_frame_fragment 传图像帧给sdk
get_codec 获取图像帧类型
start/stop 视频流开关
get_name 获取视频流的name,通常为“stream_0”
typedef struct {
    int (*get_sample_rate)(RTSP_MEDIA_TYPE_E type);
    /* get_frame 接口废弃,使用 get_frame_fragment 接口 */
    int (*get_frame)(int user_id, RTSP_MEDIA_TYPE_E type, char** buf, int *plen, uint64_t *pts); //成功返回0, 第一个视频帧需要I帧
    int (*get_frame_fragment)(int user_id, rtsp_frame_fragment_t* pframe);
    RTP_CODEC_E (*get_codec)(RTSP_MEDIA_TYPE_E type);
    int (*start)(); //返回user_id
    int (*stop)(int user_id);
    int (*get_name)(char* buf, int* buf_len);
} rtsp_stream_src_t;

RTSP用到的数据

RTSP 流媒体类型枚举:

typedef enum {
    RTSP_MEDIA_TYPE_INVALID = -1,
    RTSP_MEDIA_TYPE_VIDEO = 0,
    RTSP_MEDIA_TYPE_AUDIO,
    RTSP_MEDIA_TYPE_NUM,
} RTSP_MEDIA_TYPE_E;

流媒体帧格式

typedef struct {
    RTSP_MEDIA_TYPE_E         type;  
    char*                     buf;   
    int                       len;   
    uint64_t                  pts;
    uint8_t                   is_eof;
    uint8_t                   fragment_id; 
} rtsp_frame_fragment_t;

示例代码

此处为适配层的示例代码,RTSP 相关代码在 BK7231U 平台可以直接移植使用,其中的摄像头代码为表述功能完整性使用,实际开发以客户自己的设备为准。

//包含的库
#include "ty_video_lib.h"             //摄像头相关,可替换
#include "tuya_hal_network.h"         //链路层相关网络API
#include "tuya_hal_semaphore.h"       //信号量相关
#include "tuya_svc_lan_rtsp.h"        //包含RTSP INIT/UNINIT/REGISTER的接口
#include "rtsp_server.h"              /*包含了RTSP回调结构,帧结构,rtsp的API,可在使用VLC调试的时候本地建立server使用。
                                        在与app调试的时候不需要使用其中的API */

typedef struct {
    int pos[RTSP_MEDIA_TYPE_NUM];
    uint64_t last_pts[RTSP_MEDIA_TYPE_NUM];
} user_info_s;
user_info_s* users[5] = {0};           //RTSP组件最多可以保存5条流
SEM_HANDLE sem_handle;                 //摄像头读图信号量
SEM_HANDLE sem_handle_rtsp;            //RTSP传图信号量
struct video_pkg *vb_pkg;              //摄像头控制的数据结构
UCHAR_T test_tmp_buf[16384];           //目前整张图片保存,16k大小

struct video_pkg{
    BYTE_T *pkg_bufs;       /*数据*/
    UINT_T actual_len;      /*实际数据长度*/
    BYTE_T pkg_id;          /*第几张图*/
};

//获取图像文件格式
RTP_CODEC_E get_codec(RTSP_MEDIA_TYPE_E type)
{
    switch (type){
    case RTSP_MEDIA_TYPE_VIDEO:
        return RTP_CODEC_JPEG;
    case RTSP_MEDIA_TYPE_AUDIO:
        return RTP_CODEC_INVALID;
    default:
        return 0;
    }
}

//不同的视频类型有不同的取样率
int get_sample_rate(RTSP_MEDIA_TYPE_E type)
{
    switch (type){
    case RTSP_MEDIA_TYPE_VIDEO:
        return 90000;
    case RTSP_MEDIA_TYPE_AUDIO:
        return 8000;
    default:
        return 8000;
    }
}

//RTSP支持多流,初始化user_id
int start()
{
    int i;
    for(i = 0; i < 5; i++){
        if (NULL == users[i]){
            users[i] = malloc(sizeof(user_info_s));
            users[i]->last_pts[RTSP_MEDIA_TYPE_VIDEO] = 0;
            users[i]->last_pts[RTSP_MEDIA_TYPE_AUDIO] = 0;
            users[i]->pos[RTSP_MEDIA_TYPE_VIDEO] = 0;
            users[i]->pos[RTSP_MEDIA_TYPE_AUDIO] = 0;
            return i;
        }
    }
    return -1;
}
//释放user_id
int stop(int user_id)
{
    if (users[user_id]){
        free(users[user_id]);
        users[user_id] = NULL;
    }
    return 0;
}

获取stream name,RTSP根据stream name拉到流
int get_name(char* buf, int* buf_len)
{
    char name[] = "stream_0";
    memcpy(buf, name, sizeof(name));

    *buf_len = sizeof(name);

    return 0;
}

/*获取图像帧接口
 * IN:  user_id, pframe->type 
 * OUT: pframe->buf
 */
STATIC INT_T __get_frame_fragment_jpeg(INT_T user_id, rtsp_frame_fragment_t* pframe)
{
    user_info_s* pinfo = users[user_id];
	RTSP_MEDIA_TYPE_E type = pframe->type;
    INT_T pos = pinfo->pos[type];
    INT_T start = -1, end = -1;
    INT_T media_len;
    CONST INT_T frame_rate[2] = {28, 50};
	UINT_T time_now;

	time_now = rtos_get_time();//uni_time_get_posix_ms();

    if (time_now - pinfo->last_pts[type] < 1000 / frame_rate[type]) {
        return -1;
    }

	if (pframe == NULL || user_id >=5 || user_id < 0) {
		PR_ERR("param invalid");
		return -1;
	}
	
	tuya_os_adapt_semaphore_wait(sem_handle_rtsp);

	memset(test_tmp_buf, 0, sizeof(test_tmp_buf));
	memcpy(test_tmp_buf, vb_pkg->pkg_bufs, vb_pkg->actual_len - 5);
	media_len = vb_pkg->actual_len - 5;

	tuya_os_adapt_semaphore_post(sem_handle);
	

    if (type == RTSP_MEDIA_TYPE_VIDEO){
        PR_DEBUG("frame len:%d", media_len);
        pframe->buf = test_tmp_buf;
    }
    else {
    	return -1;      
    }

	pframe->len = media_len;
    pframe->pts = time_now;
    pframe->is_eof = 1;
    
    pinfo->last_pts[type] = time_now;    
    
    pinfo->pos[type] = media_len;

    if (pinfo->pos[type] > 0){
        pinfo->pos[type] = 0;
    }

    return 0;
}

/* RTSP应用初始化,可以将此接口在device_init之后调用 */
VOID user_rtsp_init()
{
    CHAR_T url[256] = {0};
    int url_len = 256;
    rtsp_stream_src_t src = {0};	
    
    /* 将RTSP控制接口注册给SDK */
    src.get_codec = __get_codec;
    //src.get_frame = __get_frame_jpeg;    //该接口已废弃
    src.get_frame_fragment = __get_frame_fragment_jpeg;
    src.get_sample_rate = __get_sample_rate;
    src.start = __start;
    src.stop = __stop;
    src.get_name = __get_name;
    tuya_svc_lan_reg_rtsp_stream_src(src);       
  	
    /* 摄像头相关的接口,可按需适配实现 */
    video_device *soc_video_device = NULL;
    struct video_pix_format v_format;
    struct vbq_config video_buff_q_config;
    
    /* 配置tuya_config.h文件,然后调用tuya_soc_camera_init构建系统,建立设备节点 */
    tuya_soc_camera_init();
    soc_video_device = tuya_video_dev_find("ty_video");
    if(soc_video_device == NULL){
        PR_DEBUG("soc_video_device is null");
        return OPRT_COM_ERROR;
    }

    /*初始化video_device*/
    tuya_video_dev_open(soc_video_device);

    /*设置格式*/
    v_format.p_type = QVGA_320_240;
    v_format.pixelformat = V4L2_PIX_FMT_JPEG;
    v_format.field = V4L2_FIELD_ANY;
    v_format.colorspace = V4L2_COLORSPACE_JPEG;
    tuya_video_dev_ctl(soc_video_device,CMD_SET_FMT,&v_format);
    
    /* 设置缓存 2个帧缓存 一帧的大小为 16*1024 */
    video_buff_q_config.video_pkg_buffer_len = 16 * 1024;
    video_buff_q_config.video_pkg_num = 2;
    tuya_video_dev_ctl(soc_video_device,CMD_STREAM_SET_BUFF,&video_buff_q_config);
    
    /* 启动 */
    tuya_video_dev_ctl(soc_video_device,CMD_STREAM_START,NULL);

	tuya_os_adapt_semaphore_create_init(&sem_handle, 0, 1);
	tuya_os_adapt_semaphore_create_init(&sem_handle_rtsp, 0, 1);

    while(1)
    {    
        /* 读图后等待RTSP来取 */
        tuya_video_dev_read(soc_video_device,&vb_pkg);        
		tuya_os_adapt_semaphore_post(sem_handle_rtsp);

        /* RTSP已取,释放读取到的buf */  
        tuya_os_adapt_semaphore_wait(sem_handle);        
        tuya_video_dev_ctl(soc_video_device, CMD_STREAM_DEQUEEU_BUFF,NULL);
    }
}

SDK 下载

该 SDK 暂未发布,请联系涂鸦项目经理获取。

SDK 目录结构说明

+-- software
¦   +-- IoTOS2.3.4_ty_iot_sdk_2.3.4_bk7231u_1.1.0    # 涂鸦IoTOS IoT SDK包
¦   ¦   +-- apps					# 示例程序,每个示例程序都包含了代码和对应的文档
¦   ¦   +-- sdk 					# 头文件和库文件
¦   ¦   +-- platforms				# 原厂库和工具
¦   ¦   +-- CHANGELOG.md 			# 版本修改记录
¦   ¦   +-- README.md 				# 使用介绍
¦   ¦   +-- build_app.sh 			# 编译脚本
¦   +-- Tuya IoTOS IoT SDK x.x.x版本说明.pdf 		# 涂鸦IoTOS IoT SDK介绍文档
+-- pc
¦   +-- tools 						# 开发使用到的工具
+-- hardware
¦   +-- board 						# 模组、开源硬件资料
¦   +-- chip 						# 芯片资料

应用开发快速开始

初始化流程device_init之后调用user_rtsp_init(),将RTSP组件的应用层回调注册给SDK。

FAQ

问题描述 解决办法
platforms\bk7231t\bk7231t_os\beken378\driver\uart\uart.bk.c\bk_printf缓冲区偏小且字符串拷贝不检查长度导致越界 缓冲区改为256或1024 字符串拷贝改为vsnprintf(string, sizeof(string) - 1, fmt, ap);
更换 platform 的时候,特别是更新了编译工具链之后,需要客户将编译的CFLAGS同步给涂鸦,避免出现编译出固件后无法创建线程的问题。 更新了CFLAGS
bk7231u 适配层可能会出现 ssid 无法转换成bssid的情况导致ez配网失败 适配层注意信道的合法性(1-13,0信道是非法信道),bssid不能为空
bk7231u的适配层的tuya_adapter_wifi_station_get_status接口用来表述联网状态的枚举和原厂用来表述联网状态的枚举可能不同步,在传参过程中可能发生参数含义改变。 需将获取到IP的状态前后确认一下。
客户创建产品需要选择自研模组,并将固件上传,不然无法正常配网成功 配网会检查OTA信息。需更新
KV数据库被写坏,导致kv操作失败 原厂初始化蓝牙的时候会写1f4000这个地址,但是这个是分配给涂鸦kv数据库的地址段。把原厂蓝牙模块关掉问题解决。