当前位置:   article > 正文

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

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_ptr<fsbase::multi_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上。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/509866
推荐阅读
相关标签
  

闽ICP备14008679号