当前位置:   article > 正文

音视频开发之旅(34) - 基于FFmpeg实现简单的视频解码器_avcodec_open2 error -13

avcodec_open2 error -13

目录

  1. FFmpeg解码过程流程图和关键的数据结构
  2. mp4通过FFmpeg解码YUV裸视频数据
  3. 遇到的问题
  4. 资料
  5. 收获

一、FFmpeg解码过程流程图和关键的数据结构

FFmpeg解码涉及的知识点比较多,很容易被函数和结构体搞定不知所错,我们先从整体上对解码流程有个认知,画了张解码流程图,如下

 

1.1 解码流程如下

  1. avformat_open_input 打开媒体文件
  2. avformat_find_stream_info 初始化AVFormatContext_
  3. 匹配到视频流的index
  4. avcodec_find_decoder 根据视频流信息的codec_id找到对应的解码器_
  5. avcodec_open2 使用给定的AVCodec初始化AVCodecContext_
  6. 初始化输出文件、解码AVPacket和AVFrame结构体
  7. av_read_frame 开始一帧一帧读取
  8. avcodec_send_packet
  9. avcodec_receive_frame
  10. 格式转换 、分别写入YUV文件
  11. Opengl渲染(本篇不涉及,放到后面单独篇学习实践)
  12. 释放资源

1.2 关键函数

下面我们来看下解码流程中的关键函数

1. av_register_all
在3.x或者以前的版本在使用ffmpeg的复用/解复用器或者编解码器之前一定要先调用该函数。但是4.x之后ffmpeg修了了内部实现,该函数可以省略不写。

2. avformat_open_input

 

  1. attribute_deprecated int av_open_input_file(AVFormatContext **ic_ptr,constchar *filename,
  2. AVInputFormat *fmt,
  3. int buf_size,
  4. AVFormatParameters *ap);

以输入方式打开一个媒体文件,codecs并没有打开,只读取了文件的头信息.

3. avformat_find_stream_info

 

  1. Read packets of a media file to get stream information
  2. int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

获取多媒体信息

4. avcodec_find_decoder

 

  1. Find a registered decoder with a matching codec ID
  2. AVCodec *avcodec_find_decoder(enum AVCodecID id);

根据codecID找到一个注册过的解码器

5. avcodec_open2

 

  1. Initialize the AVCodecContext to use the given AVCodec
  2. int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);

使用给定的AVCodec初始化AVCodecContext_

6. av_read_frame

 

  1. Return the next frame of a stream.
  2. @return 0 if OK, < 0 on error or end of file
  3. int av_read_frame(AVFormatContext *s, AVPacket *pkt);

读取一帧数据,读到的是AVPacket

7. avcodec_send_packet

 

  1. Supply raw packet data as input to a decoder.
  2. @return 0 on success, otherwise negative error code
  3. int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);

给解码器发送一帧压缩的AVPacket 数据

8. avcodec_receive_frame

 

  1. Return decoded output data from a decoder.
  2. @return 0 on success
  3. int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);

接收解码器解码的一帧AVFrame数据

9. sws_scale

 

  1. int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
  2. const int srcStride[], int srcSliceY, int srcSliceH,
  3. uint8_t *const dst[], const int dstStride[]);

解码后YUV格式的视频像素数据保存在AVFrame的data 0-3中。但这些像素并不是连续存储的,每行有效像素之后存储了一些无效像素,经过该函数处理,去掉无效数据。否则会出现花屏。

10. 资源释放相关函数

 

  1. av_packet_unref(packet);
  2. sws_freeContext(img_convert_ctx);
  3. fclose(pYUVFile);
  4. av_frame_free(&pFrameYUV);
  5. av_frame_free(&pFrame);
  6. avcodec_close(pCodecContext);
  7. avformat_close_input(&avFormatContext);

1.3 关键结构体

关键结构体包括AVFormatContext、AVStream、AVCodecContext、AVCodec、AVCodecParameters、AVPacket、AVFrame等
下面4张图片来自雷神

 

AVFormatCotext和AVInputFormat是和封装格式相关的结构体

 

AVStream、AVCodecContex、AVCodec是和编解码相关的结构体

 

AVPacket 与 AVFrame

 

1.4 补充知识

1. 宏定义define里面的##

 

  1. 宏定义define里面的##可能不太常见,它的含义就是拼接两个字符串,比如
  2. #define Conn(x,y) x##y
  3. 那么 int n = Conn(123,456); 结果就是n=123456;

2. 文件的打开方式

 

  1. File * fp = fopen(info.txt,"wb+")
  2. fprintf()
  3. 或者fwirte
  4. fclose(fp);

关于打开方式的说明如下

 

  1. . r+ 以可读写方式打开文件,该文件必须存在。
  2.   rb+ 读写打开一个二进制文件,只允许读写数据。
  3.   rt+ 读写打开一个文本文件,允许读和写。
  4.   w 打开只写文件,若文件存在则文件长度清0,若文件不存在则建立该文件。
  5.   w+ 打开可读写文件,若文件存在则文件长度清为零,若文件不存在则建立该文件。
  6.   a 以附加的方式打开只写文件
  7.   a+ 以附加方式打开可读写的文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾后
  8.   wb 只写打开或新建一个二进制文件;只允许写数据。
  9.   wb+ 读写打开或建立一个二进制文件,允许读和写。
  10.   wt+ 读写打开或着建立一个文本文件;允许读写。
  11.   at+ 读写打开一个文本文件,允许读或在文本末追加数据。
  12.   ab+ 读写打开一个二进制文件,允许读或在文件末追加数据。

3. YUV数据类型

输出解码前的h264码流、输出解码后的YUV信息
使用Elecard StreamEye Tools查看输出的h264数据

视频显示的流程,就是将像素数据“画”在屏幕上的过程。
例如显示YUV,就是将YUV“画”在系统的窗口中。

 

  1. YUV 4:4:4采样,每一个Y对应一组UV分量。
  2. YUV 4:2:2采样,每两个Y共用一组UV分量。
  3. YUV 4:2:0采样,每四个Y共用一组UV分量。
  4. YUV420P,Y,U,V三个分量都是平面格式,分为I420和YV12。I420格式和YV12格式的不同处在U平面和V平面的位置不同。在I420格式中,U平面紧跟在Y平面之后,然后才是V平面(即:YUV);但YV12则是相反(即:YVU)。
  5. YUV420SP, Y分量平面格式,UV打包格式, 即NV12。 NV12与NV21类似,U 和 V 交错排列,不同在于UV顺序。
  6. I420: YYYYYYYY UU VV =>YUV420P (最常见的)
  7. YV12: YYYYYYYY VV UU =>YUV420P
  8. NV12: YYYYYYYY UVUV =>YUV420SP
  9. NV21: YYYYYYYY VUVU =>YUV420SP

二、mp4通过FFmpeg解码成YUV裸数据

通过上一小节,我们了解了FFmpeg解码流程和关键的结构体,这一小节我们来实践。

具体步骤说明和代码实现如下:

 

  1. #include <jni.h>
  2. #include <string>
  3. extern "C" {
  4. #include "include/libavcodec/avcodec.h"
  5. #include "include/libavformat/avformat.h"
  6. #include "include/log.h"
  7. #include <libswscale/swscale.h>
  8. #include <libavutil/imgutils.h>
  9. }
  10. extern "C"
  11. JNIEXPORT jint JNICALL
  12. Java_android_spport_mylibrary2_Demo_decodeVideo(JNIEnv *env, jobject thiz, jstring inputPath,
  13. jstring outPath) {
  14. //申请avFormatContext空间,记得要释放
  15. AVFormatContext *avFormatContext = avformat_alloc_context();
  16. const char *url = env->GetStringUTFChars(inputPath, 0);
  17. //1. 打开媒体文件
  18. int reuslt = avformat_open_input(&avFormatContext, url, NULL, NULL);
  19. if (reuslt != 0) {
  20. LOGE("open input error url=%s, result=%d", url, reuslt);
  21. return -1;
  22. }
  23. //2.读取媒体文件信息,给avFormatContext赋值
  24. if (avformat_find_stream_info(avFormatContext, NULL) < 0) {
  25. LOGE("find stream error");
  26. return -1;
  27. }
  28. //3. 匹配到视频流的index
  29. int videoIndex = -1;
  30. for (int i = 0; i < avFormatContext->nb_streams; i++) {
  31. AVMediaType codecType = avFormatContext->streams[i]->codecpar->codec_type;
  32. LOGI("avcodec type %d", codecType);
  33. if (AVMEDIA_TYPE_VIDEO == codecType) {
  34. videoIndex = i;
  35. break;
  36. }
  37. }
  38. if (videoIndex == -1) {
  39. LOGE("not find a video stream");
  40. return -1;
  41. }
  42. AVCodecParameters *pCodecParameters = avFormatContext->streams[videoIndex]->codecpar;
  43. //4. 根据视频流信息的codec_id找到对应的解码器
  44. AVCodec *pCodec = avcodec_find_decoder(pCodecParameters->codec_id);
  45. if (pCodec == NULL) {
  46. LOGE("Couldn`t find Codec");
  47. return -1;
  48. }
  49. AVCodecContext *pCodecContext = avFormatContext->streams[videoIndex]->codec;
  50. //5.使用给定的AVCodec初始化AVCodecContext
  51. int openResult = avcodec_open2(pCodecContext, pCodec, NULL);
  52. if (openResult < 0) {
  53. LOGE("avcodec open2 result %d", openResult);
  54. return -1;
  55. }
  56. const char *outPathStr = env->GetStringUTFChars(outPath, NULL);
  57. //6. 初始化输出文件、解码AVPacket和AVFrame结构体
  58. //新建一个二进制文件,已存在的文件将内容清空,允许读写
  59. FILE *pYUVFile = fopen(outPathStr, "wb+");
  60. if (pYUVFile == NULL) {
  61. LOGE(" fopen outPut file error");
  62. return -1;
  63. }
  64. auto *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
  65. //avcodec_receive_frame时作为参数,获取到frame,获取到的frame有些可能是错误的要过滤掉,否则相应帧可能出现绿屏
  66. AVFrame *pFrame = av_frame_alloc();
  67. //作为yuv输出的frame承载者,会进行缩放和过滤出错的帧,YUV相应的数据也是从该对象中读取
  68. AVFrame *pFrameYUV = av_frame_alloc();
  69. //out_buffer中数据用于渲染的,且格式为YUV420P
  70. uint8_t *out_buffer = (unsigned char *) av_malloc(
  71. av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecContext->width,
  72. pCodecContext->height, 1));
  73. av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, out_buffer,
  74. AV_PIX_FMT_YUV420P, pCodecContext->width, pCodecContext->height, 1);
  75. // 由于解码出来的帧格式不一定是YUV420P的,在渲染之前需要进行格式转换
  76. struct SwsContext *img_convert_ctx = sws_getContext(pCodecContext->width, pCodecContext->height,
  77. pCodecContext->pix_fmt,
  78. pCodecContext->width, pCodecContext->height,
  79. AV_PIX_FMT_YUV420P,
  80. SWS_BICUBIC, NULL, NULL, NULL);
  81. int readPackCount = -1;
  82. int frame_cnt = 0;
  83. clock_t startTime = clock();
  84. //7. 开始一帧一帧读取
  85. while ((readPackCount = av_read_frame(avFormatContext, packet) >= 0)) {
  86. LOGI(" read fame count is %d", readPackCount);
  87. if (packet->stream_index == videoIndex) {
  88. //8. send AVPacket
  89. int sendPacket = avcodec_send_packet(pCodecContext, packet);
  90. //return 0 on success, otherwise negative error code:
  91. if (sendPacket != 0) {
  92. LOGE("avodec send packet error %d", sendPacket);
  93. continue;
  94. }
  95. //9. receive frame
  96. // 0: success, a frame was returned
  97. int receiveFrame = avcodec_receive_frame(pCodecContext, pFrame);
  98. if (receiveFrame != 0) {
  99. //如果接收到的fame不等于0,忽略这次receiver否则会出现绿屏帧
  100. LOGE("avcodec_receive_frame error %d", receiveFrame);
  101. continue;
  102. }
  103. //10. 格式转换
  104. sws_scale(img_convert_ctx, (const uint8_t *const *) pFrame->data, pFrame->linesize,
  105. 0, pCodecContext->height,
  106. pFrameYUV->data, pFrameYUV->linesize);
  107. //11. 分别写入YUV数据
  108. int y_size = pCodecParameters->width * pCodecParameters->height;
  109. //YUV420p
  110. fwrite(pFrameYUV->data[0], 1, y_size, pYUVFile);//Y
  111. fwrite(pFrameYUV->data[1], 1, y_size / 4, pYUVFile);//U
  112. fwrite(pFrameYUV->data[2], 1, y_size / 4, pYUVFile);//V
  113. //输出I、P、B帧信息
  114. char pictypeStr[10] = {0};
  115. switch (pFrame->pict_type) {
  116. case AV_PICTURE_TYPE_I: {
  117. sprintf(pictypeStr, "I");
  118. break;
  119. }
  120. case AV_PICTURE_TYPE_P: {
  121. sprintf(pictypeStr, "P");
  122. break;
  123. }
  124. case AV_PICTURE_TYPE_B: {
  125. sprintf(pictypeStr, "B");
  126. break;
  127. }
  128. }
  129. LOGI("Frame index %5d. Tpye %s", frame_cnt, pictypeStr);
  130. frame_cnt++;
  131. }
  132. }
  133. LOGI("frame count is %d", frame_cnt);
  134. clock_t endTime = clock();
  135. //long类型用%ld输出
  136. LOGI("decode video use Time %ld", (endTime - startTime));
  137. //12.释放相关资源
  138. //释放packet
  139. av_packet_unref(packet);
  140. sws_freeContext(img_convert_ctx);
  141. fclose(pYUVFile);
  142. av_frame_free(&pFrameYUV);
  143. av_frame_free(&pFrame);
  144. avcodec_close(pCodecContext);
  145. avformat_close_input(&avFormatContext);
  146. return 0;
  147. }
  148. }

解码后的数据使用ffplayer进行播放。注意参数设置, 比如格式和分辨率等

 

  1. eg:
  2. ffplay /Users/yabin/Desktop/tmp/ffmpeg/output8.yuv -pix_fmt yuv420p -s 784x480

代码已上传至 github
https://github.com/ayyb1988/ffmpegvideodecodedemo

三、遇到的问题

  1. avformat_open_input -13

原因: 没有读写权限导致,-13是权限相关的错误,

 

  1. 在AndroidManifest.xml中加入以下权限
  2. 然后在代码上添加动态权限检查
  1. 生成的yuv导出来后用ffplay或者yuvplayer播放 出现花屏

 

  1. Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 784x480, 338 kb/s, 25 fps, 25 tbr, 16k tbn, 50 tbc (default)
  2. ffplay /Users/yabin/Desktop/tmp/ffmpeg/output.yuv -pix_fmt yuv420p -s 784x480

原因:没有对YUV数据设置格式和分辨率等信息

 

  1. uint8_t *out_buffer = (unsigned char *) av_malloc(
  2. av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecContext->width,
  3. pCodecContext->height, 1));
  4. av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, out_buffer,
  5. AV_PIX_FMT_YUV420P, pCodecContext->width, pCodecContext->height, 1);
  6. struct SwsContext *img_convert_ctx = sws_getContext(pCodecContext->width, pCodecContext->height,
  7. pCodecContext->pix_fmt,
  8. pCodecContext->width, pCodecContext->height,
  9. AV_PIX_FMT_YUV420P,
  10. SWS_BICUBIC, NULL, NULL, NULL);
  11. ...
  12. sws_scale(img_convert_ctx, (const uint8_t *const *) pFrame->data, pFrame->linesize,
  13. 0, pCodecContext->height,
  14. pFrameYUV->data, pFrameYUV->linesize);

  1. 第一帧出现绿屏
    原因: 如果接收到的fame不等于0,要忽略这次receiver否则会出现绿屏帧

 

  1. int sendPacket = avcodec_send_packet(pCodecContext, packet);
  2. //return 0 on success, otherwise negative error code:
  3. if (sendPacket != 0) {
  4. LOGE("avodec send packet error %d", sendPacket);
  5. continue;
  6. }
  7. // 0: success, a frame was returned
  8. int receiveFrame = avcodec_receive_frame(pCodecContext, pFrame);
  9. if (receiveFrame != 0) {
  10. //如果接收到的fame不等于0,忽略这次receiver否则会出现绿屏帧
  11. LOGE("avcodec_receive_frame error %d", receiveFrame);
  12. continue;
  13. }

四、 资料

  1. 《音视频开发进阶》
  2. FFMPEG中最关键的结构体之间的关系
  3. ffmpeg函数介绍
  4. 100行代码实现最简单的基于FFMPEG+SDL的视频播放器(SDL1.x)
  5. 最简单的基于FFmpeg的移动端例子:Android 视频解码器-单个库版
  6. 图文详解YUV420数据格式
  7. ffmpeg flv转MP4 一点心得
  8. FFmpeg编解码处理1-转码全流程简介
  9. FFmpeg源代码简单分析:常见结构体的初始化和销毁(AVFormatContext,AVFrame等)

测试视频来自:FFmpeg编解码处理1-转码全流程简介
下载测试文件(右键另存为):tnmil2.flv

五、收获

  1. 了解ffmpeg解码流程
  2. 了解ffmpeg关键的结构以及之间的关系
  3. 解码mp4为视频裸数据YUV
  4. 花屏、录屏问题分析解决

与Mediacodec解码对比、YUV渲染播放等内容,我们后续章节来学习实践。

感谢你的阅读

下一篇我们学习实践使用FFmpeg解码音频,欢迎关注公众号“音视频开发之旅”,一起学习成长。

欢迎交流

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

闽ICP备14008679号