• 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成员变量中。

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

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

    1. int FSPlay::read_thread(void *arg) {
    2. //...省略代码
    3. if (pkt->stream_index == is->video_stream && pkt_in_play_range
    4. && !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) {
    5. packet_queue_put(&is->videoq, pkt);
    6. //调用ffp_handleSEI读取SEI信息到multi
    7. ffp_handleSEI(pkt);
    8. }
    9. //...省略代码
    10. }

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

    1. void FSPlay::video_image_display(VideoState *is)
    2. {
    3. Frame *vp;
    4. vp = frame_queue_peek_last(&is->pictq);
    5. if (rgbFrame == NULL) {
    6. rgbFrame = av_frame_alloc();
    7. }
    8. av_image_alloc(rgbFrame->data, rgbFrame->linesize, vp->width, vp->height, AV_PIX_FMT_RGB24, 1);
    9. enum AVPixelFormat sw_pix_fmt = (enum AVPixelFormat)(vp->format);
    10. swsContext = sws_getContext(vp->width, vp->height, sw_pix_fmt, vp->width, vp->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
    11. SDL_LockMutex(is->pictq.mutex);
    12. sws_scale(swsContext, vp->frame->data, vp->frame->linesize, 0, vp->frame->height, rgbFrame->data, rgbFrame->linesize);
    13. SDL_UnlockMutex(is->pictq.mutex);
    14. //如果不需要查询SEI信息(单摄不需要查询,或者multi没有获得时走单摄的显示逻辑。
    15. if (!needSearchSEI || !multi.acquired) {
    16. VideoFrame *videoFrame = (VideoFrame *)malloc(sizeof(VideoFrame));
    17. videoFrame->width = vp->width;
    18. videoFrame->height = vp->height;
    19. videoFrame->planar = 1;
    20. videoFrame->pixels[0] = (uint8_t *)malloc(vp->width * vp->height * 3);
    21. videoFrame->format = AV_PIX_FMT_RGB24;
    22. copyFrameData(videoFrame, rgbFrame);
    23. if (renderCallback != NULL && openglesView != NULL) {
    24. renderCallback(openglesView, videoFrame);
    25. }
    26. free(videoFrame->pixels[0]);
    27. free(videoFrame);
    28. } else {
    29. //创建destination1用来放主摄的rgb数据。
    30. VideoFrame *destination1 = (VideoFrame *)malloc(sizeof(VideoFrame));
    31. destination1->width = multi.primary.width;
    32. destination1->height = multi.primary.height;
    33. destination1->planar = 1;
    34. //根据目标主摄尺寸分配内存buf。
    35. destination1->pixels[0] = (uint8_t *)malloc(destination1->width * destination1->height * 3);
    36. destination1->format = AV_PIX_FMT_RGB24;
    37. //根据multi.primary的x,y,width,height拷贝数据从rgbFrame到destination1->pixels[0]中。
    38. copyFrameData(destination1, rgbFrame, &(multi.primary));
    39. //通过renderCallback将主摄的显示view和画面数据回调给opengl端进行绘制。
    40. if (renderCallback != NULL && openglesView != NULL) {
    41. renderCallback(openglesView, destination1);
    42. }
    43. //释放资源。
    44. free(destination1->pixels[0]);
    45. free(destination1);
    46. //创建destination2用来放次摄的rgb数据。
    47. VideoFrame *destination2 = (VideoFrame *)malloc(sizeof(VideoFrame));
    48. destination2->width = multi.secondary.width;
    49. destination2->height = multi.secondary.height;
    50. destination2->planar = 1;
    51. //根据目标次摄尺寸分配内存buf。
    52. destination2->pixels[0] = (uint8_t *)malloc(destination2->width * destination2->height * 3);
    53. destination2->format = AV_PIX_FMT_RGB24;
    54. //根据multi.secondary的x,y,width,height拷贝数据从rgbFrame到destination2->pixels[0]中。
    55. copyFrameData(destination2, rgbFrame, &(multi.secondary));
    56. //通过renderSecondCallback将次摄的显示view和画面数据回调给opengl端进行绘制。
    57. if (renderSecondCallback != NULL && renderSecondView != NULL) {
    58. renderSecondCallback(renderSecondView, destination2);
    59. }
    60. //释放资源。
    61. free(destination2->pixels[0]);
    62. free(destination2);
    63. }
    64. av_freep(&rgbFrame->data[0]);
    65. sws_freeContext(swsContext);
    66. swsContext = NULL;
    67. }

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

    1. //将source中的数据根据range标识的x,y,width,height拷贝到destination中
    2. void FSPlay::copyFrameData(VideoFrame *destination, AVFrame *source, FSFrameRange *range) {
    3. //获取原始数据指针
    4. uint8_t *src = source->data[0];
    5. //获取目标数据指针
    6. uint8_t *dst = destination->pixels[0];
    7. //获取linesize,src每次换行时通过linesize进行偏移。
    8. int linesize = source->linesize[0];
    9. //拿到目标的宽高,width为byte数,height为循环次数
    10. int width = destination->width * 3;
    11. int height = destination->height;
    12. //重置内存为0
    13. memset(dst, 0, width * height);
    14. //将src指针偏移到需要拷贝的首行
    15. src += linesize * range->y;
    16. //遍历height次。
    17. for (int i = 0; i < height; ++i) {
    18. //拷贝单行数据,从src偏移x * 3开始拷贝,共拷贝width长度。
    19. memcpy(dst, src + range->x * 3, width);
    20. //目标指针偏移一行
    21. dst += width;
    22. //src指针偏移一行
    23. src += linesize;
    24. }
    25. }

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

    1. ByteBuf sei_saas_data_without_nal_unit(const uint8_t *buf, int size) {
    2. /* 先搜索前IV_SEI_PROBE_SIZE字节 */
    3. int index = 0;
    4. int end = MIN(IV_SEI_PROBE_SIZE, size);
    5. __SEARCH__:
    6. while (index < end) {
    7. auto byteBuf = search_sei_data_by_uuid(buf + index, size - index);
    8. if (byteBuf.size() == 0) {
    9. index = end;
    10. break;
    11. }
    12. return byteBuf;
    13. }
    14. /* 若后面还有数据,再搜索后IV_SEI_PROBE_SIZE字节 */
    15. if (index < size) {
    16. index = MAX(index, size - IV_SEI_PROBE_SIZE);
    17. end = size;
    18. goto __SEARCH__;
    19. }
    20. return ByteBuf();
    21. }
    22. /**
    23. 跟据UUID搜索自定义SEIData数据位置
    24. - Parameters:
    25. - p: 搜索起始地址
    26. - size: 搜索区间长度
    27. - sei_buf: 查找到的SEIData数据位置, IV_SEI_UUID[]开头
    28. @return 返回SEIData数据长度
    29. */
    30. static ByteBuf search_sei_data_by_uuid(const uint8_t *p, int size) {
    31. int i = 2;
    32. while (size - i > IV_SEI_UUID_LEN) {
    33. // 检查是哪个版本的SEI协议
    34. for (int v = 0; v < sizeof(SEI_UUIDs) / sizeof(SEI_UUIDs[0]); v++) {
    35. auto &uuid = SEI_UUIDs[v];
    36. // a.(低成本)初步匹配前4字节
    37. // b.(高成本)校验UUID是否全匹配T平台协议
    38. if (CHECK_FIRST_4BYTES_EQUAL(p+i, uuid) && memcmp(p + i + 4, &uuid[4], IV_SEI_UUID_LEN-4) == 0){
    39. int k = i - 1;
    40. // c. 获取净荷长度, 往后累加数值直到不是0xFF后为止,累加的数值作为数据长度
    41. int payloadLen = p[k];
    42. while (k > 0 && p[--k] == 0xFF) {
    43. payloadLen += 0xFF;
    44. }
    45. ByteBuf res;
    46. if (v == 0) {
    47. res = ByteBuf(p + i, p + i + payloadLen);
    48. } else {
    49. res = remove_redundant_bytes(p + i, payloadLen);
    50. }
    51. // d. SEI类型值是用户自定义的固定为0x05
    52. // e. 校验SEI帧结尾是否为0x80
    53. if (p[k ] == IV_SEI_USER_DATA &&
    54. p[i + payloadLen] == IV_SEI_DATA_END) {
    55. return res;
    56. }
    57. }
    58. }
    59. i++;
    60. }
    61. return ByteBuf();
    62. }

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

  • 相关阅读:
    【21天算法挑战赛】排序算法——希尔排序
    基于Springboot图书馆管理系统、Springboot图书借阅系统设计与实现 毕业设计开题报告
    C#使用MX Component实现三菱PLC软元件数据采集的完整步骤(仿真)
    GitHub 上线重量级分布式架构原理设计笔记,开源的东西看着就是爽
    数学建模__非线性规划Python实现
    P3205 [HNOI2010]合唱队
    Kubernetes部署(八):k8s项目交付----(5)持续部署
    第一个接受素数定理的人
    SpringSecurity系列——认证(Authentication)架构day2-3(源于官网5.7.2版本)
    MySQL-变量/错误处理(GLOBAL/SESSION/SET/DECLARE CONDITION FOR/DECALRE HANDLER FOR)
  • 原文地址:https://blog.csdn.net/fsmpeg/article/details/138163635