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

devtools/2024/9/24 23:57:05/

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的时候说明是对的,继续执行ifif (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信息到multiffp_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;//重置内存为0memset(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帧结尾是否为0x80if (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.ppmy.cn/devtools/16466.html

相关文章

leetcode题目69:x的平方根【python】

题目描述 计算并返回 x 的平方根&#xff0c;其中 x 是非负整数。 由于返回类型是整数&#xff0c;结果只保留整数部分&#xff0c;小数部分将被舍去。 输入格式 x&#xff1a;一个非负整数。 输出格式 返回整数部分的平方根。 示例 示例 1 输入: x 4 输出: 2示例 2 …

Vim学习笔记01~04

第01章&#xff1a; 遁入空门&#xff0c;模式当道 1.什么是vim Vim是一个高效的文本编辑工具&#xff0c;并且可以在编程开发过程中发挥越来越重要的作用。 事实上&#xff0c;有不少编程高手使用他们来进行代码的开发&#xff0c;并且对此赞不绝口。 2.本系列目的 但是让…

1、k8s问题pod从service中剔除

一、起因 redis原来由两服务器的集群变为三服务器的集群&#xff0c;通过statefulset扩展了两节点&#xff0c;并把redis-app-0和redis-app-3从集群中去除&#xff0c;但是由于service路由后端不变&#xff0c;导致程序连接后端仍然可能到redis-app-0和redis-app-3 二、处理 …

GitHub/R3D3项目环境配置踩坑记录

1、前言 项目链接地址&#xff1a;SysCV/r3d3 (github.com) 按照安装步骤容易出现的问题&#xff0c;environment.yaml文件中安装相关包&#xff0c;其中还有两个pip install githttps://github.com/..........这两个建议注释掉&#xff0c;后面再来安装这两个。 2、问题及解…

自然语言处理(NLP)技术

自然语言处理&#xff08;NLP&#xff09;技术是一种处理和分析人类语言的技术&#xff0c;它可以帮助计算机理解、解释和生成自然语言。 以下是一些常见的使用NLP技术的例子&#xff1a; 1. 机器翻译&#xff1a;NLP技术可以将一种语言翻译成另一种语言&#xff0c;比如将英…

深度学习中的黑科技:自监督学习(Self-Supervised Learning)

在人工智能领域&#xff0c;深度学习已成为推动技术革新的核心力量。然而&#xff0c;深度学习的一个重要瓶颈是对大量标记数据的依赖性。在这个背景下&#xff0c;自监督学习(Self-Supervised Learning, SSL)作为一种新兴的学习范式&#xff0c;越来越受到研究者的关注。自监督…

ClickHouse 数据类型、表引擎与TTL

文章目录 数据类型注意事项 表引擎1.TinyLog 引擎2.MergeTree 引擎3.ReplacingMergeTree 引擎4.AggregatingMergeTree 引擎5.SummingMergeTree 引擎6.CollapsingMergeTree 引擎7.Distributed 引擎 TTL列级 TTL表级TTL 数据类型 ClickHouse 数据类型Java 数据类型数据范围UInt8…

DevOps(十二)Jenkins实战之Web发布到远程服务器

前面两篇博文介绍了怎么将django开发的web应用推送到gitlab源码仓库&#xff0c;然后jenkins服务器从gitlab仓库拉下来&#xff0c;布署到jenkins服务器上&#xff0c;并用supervisor进行进程管理&#xff0c;保证web应用一直能正常运行&#xff0c;今天我们继续优化&#xff0…