ffmpeg ffplay 基于h264中SEI信息进行双摄画面拆分播放实践

1.背景

        工作中用到IPCamera支持双摄(即一个IPCamera带两个摄像头),IPC端将两个摄像头的画面上下拼接成了一个画面发布dash到云端,并且携带SEI信息。SEI信息中带两个frame(x, y, width, height),app端(iOS、安卓)根据这个信息拆分画面通过opengl展示到两个view上,以便以不同的排列方式展示双摄画面。

 2. 使用ffplay播放单个画面,请参考:

ffplay+SDL2+opengles在iOS中使用(参考ijkplayer)_ffplay swift ios-CSDN博客

3. ffp_handleSEI方法用于将AVPacket中的SEI信息读取到multi成员变量中。

//根据视频获取的AVPacket获取SEI中的双摄画面信息,以便根据这些信息拆分显示双摄。
void FSPlay::ffp_handleSEI(AVPacket *pkt) {
    //如果不需要查询SEI信息(单摄不需要查询)或multi数据已经获取过了(仅获取一次即可),则直接返回。
    if (!needSearchSEI || multi.acquired) {
        return;
    }
    
    //将AVPacket的data、size数据传入sei_saas_data_without_nal_unit方法获取实际的SEI buf,内部通过查询前后256byte的数据查找对应的uuid,找到uuid后其后边就是SEI对应的数据。
    //NAL引导码 + NAL帧类型(SEI) + SEI帧类型(用户自定义类型) + 数据长度 + UUID + 净荷数据 + 0X80
    //引导码为0x00 0x00 0x00 0x01或者 0x00 0x00 0x01,NAL帧类型为6,SEI帧类型为5,数据长度为UUID长度+ 净荷数据长度 0x80为尾部固定。
    fsbase::ByteBuf buf = fsbase::sei_saas_data_without_nal_unit(pkt->data, pkt->size);
    if (buf.size() > 0) {
        fsbase::sei_frame_t f;
        
        //通过SEI的buf data创建sei_frame结构体。
        fsbase::sei_frame_make(buf.data(), 0, &f);
        
        //如果saas_data有效时执行if代码。
        if (f.saas_data != NULL && f.saas_len > 0) {
            
            //根据saas_data构建multi结构体。
            std::shared_ptr<fsbase::multi_rect_t> multi_cpp = fsbase::sei_query_multi_rect(f.saas_data, f.saas_len);
            
            //如果multi_cpp有效时执行if代码。
            if (multi_cpp != NULL) {
                
                //如果count数量为2的时候说明是对的,继续执行if
                if (multi_cpp->count + 1 >= 2) {
                    
                    //将acquired设置为1表示已经获取过multi数据了,后续可以直接使用,不需要重复获取了。
                    multi.acquired = 1;
                    
                    //从multi_cpp中读取rects[0]的x,y,width,height数据保存到成员变量multi中。
                    multi.primary = FSFrameRange();
                    multi.primary.x = multi_cpp->rects[0].x;
                    multi.primary.y = multi_cpp->rects[0].y;
                    multi.primary.width = multi_cpp->rects[0].w;
                    multi.primary.height = multi_cpp->rects[0].h;
                    
                    //从multi_cpp中读取rects[1]的x,y,width,height数据保存到成员变量multi中。
                    multi.secondary = FSFrameRange();
                    multi.secondary.x = multi_cpp->rects[1].x;
                    multi.secondary.y = multi_cpp->rects[1].y;
                    multi.secondary.width = multi_cpp->rects[1].w;
                    multi.secondary.height = multi_cpp->rects[1].h;
                }
            }
        }
    }
}

4. 在获取到视频的AVPacket时调用ffp_handleSEI方法读取SDI信息

int FSPlay::read_thread(void *arg) {
    //...省略代码
    if (pkt->stream_index == is->video_stream && pkt_in_play_range
                   && !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) {
        packet_queue_put(&is->videoq, pkt);

        //调用ffp_handleSEI读取SEI信息到multi
        ffp_handleSEI(pkt);
    }
    //...省略代码
}

5. video_image_display方法中根据multi的primary和secondary将原rgb数据拆分成两个画面分别回调给opengl端显示。

void FSPlay::video_image_display(VideoState *is)
{
    Frame *vp;

    vp = frame_queue_peek_last(&is->pictq);
    
    if (rgbFrame == NULL) {
        rgbFrame = av_frame_alloc();
    }
    av_image_alloc(rgbFrame->data, rgbFrame->linesize, vp->width, vp->height, AV_PIX_FMT_RGB24, 1);

    enum AVPixelFormat sw_pix_fmt = (enum AVPixelFormat)(vp->format);

    swsContext = sws_getContext(vp->width, vp->height, sw_pix_fmt, vp->width, vp->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);

    SDL_LockMutex(is->pictq.mutex);
    sws_scale(swsContext, vp->frame->data, vp->frame->linesize, 0, vp->frame->height, rgbFrame->data, rgbFrame->linesize);
    SDL_UnlockMutex(is->pictq.mutex);
    
    //如果不需要查询SEI信息(单摄不需要查询,或者multi没有获得时走单摄的显示逻辑。
    if (!needSearchSEI || !multi.acquired) {
        VideoFrame *videoFrame = (VideoFrame *)malloc(sizeof(VideoFrame));
        videoFrame->width = vp->width;
        videoFrame->height = vp->height;
        videoFrame->planar = 1;
        videoFrame->pixels[0] = (uint8_t *)malloc(vp->width * vp->height * 3);
        
        videoFrame->format = AV_PIX_FMT_RGB24;
        copyFrameData(videoFrame, rgbFrame);
        if (renderCallback != NULL && openglesView != NULL) {
            renderCallback(openglesView, videoFrame);
        }
        free(videoFrame->pixels[0]);
        free(videoFrame);
    } else {
        //创建destination1用来放主摄的rgb数据。
        VideoFrame *destination1 = (VideoFrame *)malloc(sizeof(VideoFrame));
        destination1->width = multi.primary.width;
        destination1->height = multi.primary.height;
        destination1->planar = 1;
        
        //根据目标主摄尺寸分配内存buf。
        destination1->pixels[0] = (uint8_t *)malloc(destination1->width * destination1->height * 3);
        destination1->format = AV_PIX_FMT_RGB24;
        
        //根据multi.primary的x,y,width,height拷贝数据从rgbFrame到destination1->pixels[0]中。
        copyFrameData(destination1, rgbFrame, &(multi.primary));
        
        //通过renderCallback将主摄的显示view和画面数据回调给opengl端进行绘制。
        if (renderCallback != NULL && openglesView != NULL) {
            renderCallback(openglesView, destination1);
        }
        
        //释放资源。
        free(destination1->pixels[0]);
        free(destination1);
        
        //创建destination2用来放次摄的rgb数据。
        VideoFrame *destination2 = (VideoFrame *)malloc(sizeof(VideoFrame));
        destination2->width = multi.secondary.width;
        destination2->height = multi.secondary.height;
        destination2->planar = 1;
        
        //根据目标次摄尺寸分配内存buf。
        destination2->pixels[0] = (uint8_t *)malloc(destination2->width * destination2->height * 3);
        destination2->format = AV_PIX_FMT_RGB24;
        
        //根据multi.secondary的x,y,width,height拷贝数据从rgbFrame到destination2->pixels[0]中。
        copyFrameData(destination2, rgbFrame, &(multi.secondary));
        
        //通过renderSecondCallback将次摄的显示view和画面数据回调给opengl端进行绘制。
        if (renderSecondCallback != NULL && renderSecondView != NULL) {
            renderSecondCallback(renderSecondView, destination2);
        }
        
        //释放资源。
        free(destination2->pixels[0]);
        free(destination2);
    }
    
    av_freep(&rgbFrame->data[0]);
    sws_freeContext(swsContext);
    swsContext = NULL;
}

6. copyFrameData方法用于将原rgb数据以指定的range拷贝到目标Frame中。

//将source中的数据根据range标识的x,y,width,height拷贝到destination中
void FSPlay::copyFrameData(VideoFrame *destination, AVFrame *source, FSFrameRange *range) {
    //获取原始数据指针
    uint8_t *src = source->data[0];
    
    //获取目标数据指针
    uint8_t *dst = destination->pixels[0];
    
    //获取linesize,src每次换行时通过linesize进行偏移。
    int linesize = source->linesize[0];
    
    //拿到目标的宽高,width为byte数,height为循环次数
    int width = destination->width * 3;
    int height = destination->height;
    
    //重置内存为0
    memset(dst, 0, width * height);
    
    //将src指针偏移到需要拷贝的首行
    src += linesize * range->y;
    
    //遍历height次。
    for (int i = 0; i < height; ++i) {
        //拷贝单行数据,从src偏移x * 3开始拷贝,共拷贝width长度。
        memcpy(dst, src + range->x * 3, width);
        
        //目标指针偏移一行
        dst += width;
        
        //src指针偏移一行
        src += linesize;
    }
}

7. sei_saas_data_without_nal_unit方法用于根据uuid去搜索SEI信息,搜索前256字节和后256字节。

ByteBuf sei_saas_data_without_nal_unit(const uint8_t *buf, int size) {
    /* 先搜索前IV_SEI_PROBE_SIZE字节 */
    int index = 0;
    int end = MIN(IV_SEI_PROBE_SIZE, size);
    
__SEARCH__:
    while (index < end) {
        auto byteBuf = search_sei_data_by_uuid(buf + index, size - index);
        if (byteBuf.size() == 0) {
            index = end;
            break;
        }
        return byteBuf;
    }
    
    /* 若后面还有数据,再搜索后IV_SEI_PROBE_SIZE字节 */
    if (index < size) {
        index = MAX(index, size - IV_SEI_PROBE_SIZE);
        end   = size;
        goto __SEARCH__;
    }
    
    return ByteBuf();
}

/**
 跟据UUID搜索自定义SEIData数据位置
 
 - Parameters:
 - p: 搜索起始地址
 - size: 搜索区间长度
 - sei_buf: 查找到的SEIData数据位置, IV_SEI_UUID[]开头
 @return 返回SEIData数据长度
 */
static ByteBuf search_sei_data_by_uuid(const uint8_t *p, int size) {
    int i = 2;
    
    while (size - i > IV_SEI_UUID_LEN) {
        // 检查是哪个版本的SEI协议
        for (int v = 0; v < sizeof(SEI_UUIDs) / sizeof(SEI_UUIDs[0]); v++) {
            auto &uuid = SEI_UUIDs[v];
            // a.(低成本)初步匹配前4字节
            // b.(高成本)校验UUID是否全匹配T平台协议
            if (CHECK_FIRST_4BYTES_EQUAL(p+i, uuid) && memcmp(p + i + 4, &uuid[4], IV_SEI_UUID_LEN-4) == 0){
                int k = i - 1;
                // c. 获取净荷长度, 往后累加数值直到不是0xFF后为止,累加的数值作为数据长度
                int payloadLen = p[k];
                while (k > 0 && p[--k] == 0xFF) {
                    payloadLen += 0xFF;
                }
                
                ByteBuf res;
                if (v == 0) {
                    res = ByteBuf(p + i, p + i + payloadLen);
                } else {
                    res = remove_redundant_bytes(p + i, payloadLen);
                }
                
                // d. SEI类型值是用户自定义的固定为0x05
                // e. 校验SEI帧结尾是否为0x80
                if (p[k             ] == IV_SEI_USER_DATA &&
                    p[i + payloadLen] == IV_SEI_DATA_END) {
                    return res;
                }
            }
        }
        
        i++;
    }
    return ByteBuf();
}

8. 通过renderCallback和renderSecondCallback回调的画面数据,用opengles进行渲染,显示到两个对应的view上。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/572818.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Meta-Llama-3-8B-Instruct本地推理

Meta-Llama-3-8B-Instruct 本地推理 基础环境信息&#xff08;wsl2安装Ubuntu22.04 miniconda&#xff09; 使用miniconda搭建环境 (base) :~$ conda create --name pytorch212 python3.10 Retrieving notices: ...working... done Channels:- defaults Platform: linux-64 C…

EasyRecovery数据恢复软件2025破解版安装包下载

EasyRecovery数据恢复软件的主要功能及使用教程。coco玛奇朵可以提供一个概要和简化的教程&#xff0c;以便你了解其基本内容和操作步骤。 EasyRecovery绿色破解下载网盘链接: https://pan.baidu.com/s/1_6NmcOh_Jmc-DGc4TJD-Mg?pwddq4w 提取码: dq4w 复制这段内容后打开百度…

ABAP 第三代增强(BADI)--BADI旧方法

文章目录 第三代增强&#xff08;BADI&#xff09;--BADI旧方法需求分析确定BADI使用SE18查看BADIBADI的创建实施逻辑代码编写测试注意事项 第三代增强&#xff08;BADI&#xff09;–BADI旧方法 第三代增强BADI&#xff1a;全称是&#xff08;Business Add-Ins&#xff09; …

[卷积神经网络]YoloV9

一、概述 代码路径为&#xff1a; YoloV9https://github.com/WongKinYiu/yolov9 YoloV9的作者在论文中指出&#xff1a;现在的深度学习方法大多都在寻找一个合适的目标函数&#xff0c;但实际上输入数据在进行特征提取和空间变换的时候会丢失大量信息。针对这个问题&#xff…

MySQL数据类型:字符串类型详解

MySQL数据类型&#xff1a;字符串类型详解 在MySQL数据库中&#xff0c;字符串数据类型用于存储各种文本信息。这些数据类型主要包括CHAR、VARCHAR、TEXT和BLOB等。 CHAR与VARCHAR CHAR CHAR类型用于存储固定长度的字符串。它的长度在创建表时就已确定&#xff0c;长度范围…

书生·浦语大模型实战营之Llama 3 高效部署实践(LMDeploy 版)

书生浦语大模型实战营之Llama 3 高效部署实践&#xff08;LMDeploy 版&#xff09; 环境&#xff0c;模型准备LMDeploy chatTurmind和Transformer的速度对比LMDeploy模型量化(lite)LMDeploy服务(serve) 环境&#xff0c;模型准备 InternStudio 可以直接使用 studio-conda -t …

查找总价格为目标值的两个商品 ---- 双指针

题目链接 题目: 分析: 解法一: 暴力解法, 将每两个的和都算出来, 判断是否为目标值解法二: 数组中的数是按升序排序的, 我们可以定义左右指针 如果和小于目标值, 则应该让和变大, 所以左指针右移如果和大于目标值, 则应该让和变小, 所以右指针左移 思路: 定义left 0, righ…

使用Krukal算法解决图的最小生成树问题

Kruskal 算法 Kruskal算法是一种用于寻找连通图中最小生成树的算法。最小生成树是一个包含图中所有顶点的树&#xff0c;且边权重之和最小。Kruskal算法是一种贪心算法&#xff0c;它的基本思想是&#xff1a;每次选择边权重最小的边来扩展树&#xff0c;直到树包含所有的顶点…

一周学会Django5 Python Web开发-Django5 ORM执行SQL语句

锋哥原创的Python Web开发 Django5视频教程&#xff1a; 2024版 Django5 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 Django5 Python web开发 视频教程(无废话版) 玩命更新中~共计49条视频&#xff0c;包括&#xff1a;2024版 Django5 Python we…

Spring 注解开发详解

1. 注解驱动入门案例介绍 1.1 需求描述 1.需求&#xff1a;实现保存一条数据到数据库。 2.表结构&#xff1a;create table account(id int primary key auto_increment,name varchar(50),money double(7,2)); 3.要求&#xff1a;使用spring框架中的JdbcTemplate和DriverMana…

Python 使用相对路径读取文件失败

python open一个问及那时使用绝对路径可以&#xff0c;但是使用相对路径时报错&#xff0c;找不到指定文件 解决步骤如下&#xff1a; 添加Python配置 在新增的配置Json文件添加下图红框这一行

阿里云OSS

进入阿里云官网&#xff0c;手机号短信登录

Ansible 中的copy 复制模块应用详解

作者主页&#xff1a;点击&#xff01; Ansible专栏&#xff1a;点击&#xff01; 创作时间&#xff1a;2024年4月25日13点40分 Ansible 中的 copy 模块用于将文件或目录从本地计算机或远程主机复制到远程主机上的特定位置。它是一个功能强大的模块&#xff0c;可用于各种文…

prometheus helm install 如何配置告警模版

对接企业微信 获取企业id 注册完成之后&#xff0c;通过企业微信官网登录后台管理&#xff0c;在【我的企业】的企业信息里面&#xff0c;获取到Alertmanager服务配置需用到的第一个配置&#xff1a;企业ID 获取部门id 部门ID 在【通讯录】中&#xff0c;添加一个子部门&a…

无人机+自组网:2U机架车载式自组网电台技术详解

自组网的特点包括自发现、自动配置、自组织和自愈等。由于网络中的节点可以随时加入或离开&#xff0c;自组网需要能够自动感知拓扑结构的变化&#xff0c;并快速调整路由策略以适应新的网络环境。此外&#xff0c;自组网中的节点还需要具备节能、安全和分布式管理等特性&#…

maixcam如何无脑运行运行别人的模型(以安全帽模型为例)

maixcam如何无脑运行运行别人的模型&#xff08;以安全帽模型为例&#xff09; 本文章主要讲如何部署上传的模型文件&#xff0c;以及如果你要把你模型按照该流程应该怎么修改&#xff0c;你可以通过该文章得到你想要的应该&#xff0c;该应用也包含的退出按钮&#xff0c;是屏…

质量管理系统( QMS):一文扫盲,质量重于泰山。

一、什么是QMS系统 QMS系统是质量管理系统&#xff08;Quality Management System&#xff09;的缩写。它是一种组织内部用于管理和控制质量相关活动的体系&#xff0c;旨在确保产品或服务符合质量标准和客户要求。 QMS系统通常包括一系列文件、程序和流程&#xff0c;用于规…

Linux常用命令总结(四):文件权限及相关命令介绍

1. 文件属性信息解读 1. 文件类型和权限的表示 0首位表示类型。在Linux中第一个字符代表这个文件是目录、文件或链接文件 符号对应文件类型-代表文件dd 代表目录l链接文档(link file)&#xff1b; 1-3位确定属主&#xff08;该文件的所有者&#xff09;拥有该文件的权限。 4-6…

【信息收集】端口扫描masscan负载均衡识别lbd

★★免责声明★★ 文章中涉及的程序(方法)可能带有攻击性&#xff0c;仅供安全研究与学习之用&#xff0c;读者将信息做其他用途&#xff0c;由Ta承担全部法律及连带责任&#xff0c;文章作者不承担任何法律及连带责任。 1、什么是masscan masscan在kali系统上是自带的端口扫描…

【golang学习之旅】报错:a declared but not used

目录 报错原因解决方法参考 报错 代码很简单&#xff0c;如下所示。可以发现a和b都飙红了&#xff1a; 运行后就会出现报错&#xff1a; 报错翻译过来就是a已经声明但未使用。当时我很疑惑&#xff0c;在其他语言中从来没有这种情况。况且这里的b不是赋值了吗&#xff0c;怎…
最新文章