欢迎访问我的博客原文:https://lightfish.cn/2018-12-20-ffmpeg-primer

前言

本文以 ffmpeg 工具,讲述如何认识音视频编程,你可以了解到常见视频格式的大概样子,一步步学会如何使用 ffmpeg 的 C 语言 API

本文重于动手实践,代码仓库:mpegUtil

笔者的开发环境:Arch Linux 4.19.12, ffmpeg version n4.1

解码过程总览

以下是解码流程图,逆向即是编码流程

本文是音视频编程入门篇,先略过传输协议层,主要讲格式层与编解码层的编程例子。

写在最前面的日志处理

边编程边执行,查看日志输出,是最直接的反馈,以感受学习的进度。对于 ffmpeg 的日志,需要提前这样处理:

/* log.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <libavutil/log.h>// 定义输出日志的函数,留白给使用者实现
extern void Ffmpeglog(int , char*);static void log_callback(void *avcl, int level, const char *fmt, va_list vl)
{(void) avcl;char log[1024] = {0};int n = vsnprintf(log, 1024, fmt, vl);if (n > 0 && log[n - 1] == '\n')log[n - 1] = 0;if (strlen(log) == 0)return;Ffmpeglog(level, log);
}void set_log_callback()
{// 给 av 解码器注册日志回调函数av_log_set_callback(log_callback);
}
/* main.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>void Ffmpeglog(int l, char* t) {if(l <= AV_LOG_INFO)fprintf(stdout, "%s\n", t);
}

ffmpeg 有不同等级的日志,本文只需使用 AV_LOG_INFO 即可。

第一步,查看音视频格式信息

料理食材的第一步,得先懂得食材的来源和特性。

  • 来源,互联网在线观看(http/rtmp)、播放设备上存储的视频文件(file)。
  • 格式,如何查看视频文件的格式呢,以下有 unix 命令行示例,至于 windows 系统,查看文件属性即可。
# linux 上查看视频文件信息
ffmpeg -i example.mp4

以某个mp4文件为例,输出:

Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'example.mp4':Metadata:major_brand     : isomminor_version   : 512compatible_brands: isomiso2avc1mp41encoder         : Wxmm_900012345Duration: 00:00:58.21, start: 0.000000, bitrate: 541 kb/sStream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 368x640, 487 kb/s, 24 fps, 24 tbr, 12288 tbn, 48 tbc (default)Metadata:handler_name    : VideoHandlerStream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 48 kb/s (default)Metadata:handler_name    : SoundHandler

根据命令输出信息,视频文件中有两个 stream, 即 video 与 audio,视频流与音频流。

  • stream 0, 是视频数据,编码格式为h264,24 fps 意为 24 frame per second,即每秒24帧,比特率487 kb/s,
  • stream 1, 是音频数据,编码格式为acc,采样率44100 Hz,比特率48 kb/s

【编程实操】读取音视频流的格式信息

在互联网场景中,在线观看视频才是常见需求,那么,计算机如何读取视频流的信息呢,下面以 ffmpeg 代码讲述

    /* C代码例子,省略了处理错误的逻辑 */AVFormatContext *fmt_ctx = NULL; // AV 格式上下文AVIOContext *avio_ctx = NULL; // AV IO 上下文unsigned char *avio_ctx_buffer = NULL; // input bufferfmt_ctx = avformat_alloc_context();// 获得 AV format 句柄avio_ctx_buffer = (unsigned char *)av_malloc(data_size); // ffmpeg分配内存的方法,给输入分配缓存/* fread(file) or memcpy(avio_ctx_buffer, inBuf, n)  省略拷贝文件流的步骤 */// 给 av format context 分配 io 操作的句柄,必要传参:输入数据的指针、数据大小、write_flag=0表明buffer不可写,其他参数忽略,置 NULLavio_ctx = avio_alloc_context(avio_ctx_buffer, data_size, 0, NULL, NULL, NULL, NULL);fmt_ctx->pb = avio_ctx;// 打开输入的数据流,读取 header 的格式内容,注意必须的后续处理 avformat_close_input()avformat_open_input(&fmt_ctx, NULL, NULL, NULL)// 获取音视频流的信息avformat_find_stream_info(fmt_ctx, NULL) 

ffmpeg 有一个方法直接打印音视频信息

av_dump_format(fmt_ctx, 0, NULL, 0);

print: (源码的输出格式凌乱,笔者整理过)

Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '(null)':
Stream #0:0 : Video: h264 (High) (avc1 / 0x31637661), yuv420p, 368x640, 487 kb/s 24 fps,
Stream #0:1 : Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 48 kb/s

实践编程,获取音视频信息,当然需要细致地调用API,看下面代码

  • 查看音视频流的索引、类型、解码器类型
  avformat_find_stream_info(fmt_ctx, NULL);for(int i=0; i<fmt_ctx->nb_streams; i++){AVStream *stream = fmt_ctx->streams[i];AVCodecParameters *codec_par = stream->codecpar;av_log(NULL, AV_LOG_INFO, "find audio stream index=%d, type=%s, codec id=%d", i, av_get_media_type_string(codec_par->codec_type), codec_par->codec_id);}

print:

find audio stream index=0, type=video, codec id=27
find audio stream index=1, type=audio, codec id=86018
  • 看到没,上面代码只获得解码器的id值(枚举类型),那么解码器的信息呢,加上下面代码,可以看到音视频流的格式,同时获得解码器,方便“解码步骤”使用。
  AVCodec *decodec = NULL;decodec = avcodec_find_decoder(codec_par->codec_id); // 获得解码器av_log(NULL, AV_LOG_INFO, "find codec name=%s\t%s", decodec->name, decodec->long_name);

print:

find audio stream index=0, type=video, codec id=27
find codec name=h264    H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
find audio stream index=1, type=audio, codec id=86018
find codec name=aac     AAC (Advanced Audio Coding)
  • 关于视频,可以查看帧率(一秒有多少帧画面)
  // 获得一个分数AVRational framerate = av_guess_frame_rate(fmt_ctx, stream, NULL);av_log(NULL, AV_LOG_INFO, "video framerate=%d/%d", framerate.num, framerate.den);

print:

video framerate=24/1

至此,我们掌握了如何利用 ffmpeg 的 C 语言 API 来读取音视频文件流的信息

第二步,解码

简单说一下音视频文件的解码过程,对大部分音视频格式来说,在原始流的数据中,不同类型的流会按时序先后交错在一起,这是多路复用,这样的数据分布,即有利于播放器打开本地文件,读取某一时段的音视频时,方便进行fseek操作(移动文件描述符的读写指针);也有利于网络在线观看视频,“空投”从某一刻开始播放视频,从文件某一段下载数据。

直观的看下面的循环读取文件流的代码

    /* begin 解码过程 */AVPacket *pkt;AVFrame *frame;// 分配原始文件流packet的缓存pkt = av_packet_alloc();// 分配 AV 帧 的内存frame = av_frame_alloc();// 在循环中不断读取下一个文件流的 packet 包while (av_read_frame(fmt_ctx, pkt) >= 0) {if(pkt->size){/*demux 解复用原始流的数据中,不同格式的流会交错在一起(多路复用)从原始流中读取的每一个 packet 的流可能是不一样的,需要判断 packet 的流索引,按类型处理*/if(pkt->stream_index == video_stream_idx){// 此处省略处理视频的逻辑}else if(pkt->stream_index == audio_stream_idx){// 此处省略处理音频的逻辑}}av_packet_unref(pkt);av_frame_unref(frame);}/* end 解码过程 */// flush dataavcodec_send_packet(video_decodec_ctx, NULL);avcodec_send_packet(audio_decodec_ctx, NULL);

上面代码是对音视频流进行解复用的主要过程,在循环中分别处理不同类型的流数据,到了这一步,就是使用解码器对循环中获取的 packet 包进行解码。

解码前的准备

ffmepg 中,解码工具需要初始化好两个指针,一个是解码器,一个是解码器上下文,上下文是用来存储此次操作的变量集合,比如 io 的句柄、解码的帧数累加值,视频的帧率等等。让我们重新编写上面读取音视频流的循环,给音视频流分别分配好这两个指针,并且处理好错误返回值。(下面代码的 goto 语句暂且略过,后面再提)

   // find codecint video_stream_idx = -1, audio_stream_idx = -1;AVStream *video_stream = NULL, *audio_stream = NULL;AVCodecContext *video_decodec_ctx=NULL, *audio_decodec_ctx=NULL;// AVFormatContext.nb_stream 记录了该 URL 中包含有几路流for(int i=0; i<fmt_ctx->nb_streams; i++){AVStream *stream = fmt_ctx->streams[i];AVCodecParameters *codec_par = stream->codecpar;AVCodec *decodec = NULL;AVCodecContext *decodec_ctx = NULL;av_log(NULL, AV_LOG_INFO, "find audio stream index=%d, type=%s, codec id=%d", i, av_get_media_type_string(codec_par->codec_type), codec_par->codec_id);// 获得解码器decodec = avcodec_find_decoder(codec_par->codec_id);if(!decodec){av_log(NULL, AV_LOG_ERROR, "fail to find decodec\n");goto clean2;}av_log(NULL, AV_LOG_INFO, "find codec name=%s\t%s", decodec->name, decodec->long_name);// 分配解码器上下文句柄decodec_ctx = avcodec_alloc_context3(decodec);if(!decodec_ctx){av_log(NULL, AV_LOG_ERROR, "fail to allocate codec context\n");goto clean2;}// 复制流信息到解码器上下文if(avcodec_parameters_to_context(decodec_ctx, codec_par) < 0){av_log(NULL, AV_LOG_ERROR, "fail to copy codec parameters to decoder context\n");avcodec_free_context(&decodec_ctx);goto clean2;}// 初始化解码器if ((ret = avcodec_open2(decodec_ctx, decodec, NULL)) < 0) {av_log(NULL, AV_LOG_ERROR, "Failed to open %s codec\n", decodec->name);return ret;}if( stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){// 视频的属性,帧率,这里 av_guess_frame_rate() 非必须,看业务是否需要使用帧率参数decodec_ctx->framerate = av_guess_frame_rate(fmt_ctx, stream, NULL);av_log(NULL, AV_LOG_INFO, "video framerate=%d/%d", decodec_ctx->framerate.num, decodec_ctx->framerate.den);video_stream_idx = i;video_stream = stream;video_decodec_ctx = decodec_ctx;} else if( stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO){audio_stream_idx = i;audio_stream = stream;audio_decodec_ctx = decodec_ctx;} }

以上方式是循环读取文件的所有stream,便于查看文件中有什么流,如视频、音频、字幕等,若是业务需求,只要对单独一个流(比如视频),可以用以下方式获取特定的流。

    if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {av_log(NULL, AV_LOG_ERROR, "Could not find stream information\n");goto clean1;}ret = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);if (ret < 0) {av_log(NULL, AV_LOG_ERROR, "Could not find %s stream\n",av_get_media_type_string(type));return ret;}int stream_index = ret;AVStream *st = fmt_ctx->streams[stream_index];

解码的循环

修改上面解码的循环,以视频流为例,如何从流中读取帧,为便于理解,在关键地方有清楚的注释。

    while (av_read_frame(fmt_ctx, pkt) >= 0) {if(pkt->size){/*demux 解复用原始流的数据中,不同格式的流会交错在一起(多路复用)从原始流中读取的每一个 packet 的流可能是不一样的,需要判断 packet 的流索引,按类型处理*/if(pkt->stream_index == video_stream_idx){// 向解码器发送原始压缩数据 packetif((ret = avcodec_send_packet(video_decodec_ctx, pkt)) < 0){av_log(NULL, AV_LOG_ERROR, "Error sending a packet for decoding, ret=%d", ret);break;}/*解码输出视频帧avcodec_receive_frame()返回 EAGAIN 表示需要更多帧来参与编码像 MPEG等格式, P帧(预测帧)需要依赖I帧(关键帧)或者前面的P帧,使用比较或者差分方式编码读取frame需要循环,因为读取多个packet后,可能获得多个frame*/ while(ret >= 0){ret = avcodec_receive_frame(video_decodec_ctx, frame);if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){break;}/* DEBUG 打印出视频的时间pts = display timestamp视频流有基准时间 time_base ,即每 1 pts 的时间间隔(单位秒)使用 pts * av_q2d(time_base) 可知当前帧的显示时间*/if(video_decodec_ctx->frame_number%100 == 0){av_log(NULL, AV_LOG_INFO, "read video No.%d frame, pts=%d, timestamp=%f seconds", video_decodec_ctx->frame_number, frame->pts, frame->pts * av_q2d(video_stream->time_base));}/*在第一个视频帧读取成功时,可以进行:1、若要转码,初始化相应的编码器2、若要加过滤器,比如水印、旋转等,这里初始化 filter*/if (video_decodec_ctx->frame_number == 1) {}else{}av_frame_unref(frame);}}else if(pkt->stream_index == audio_stream_idx){}}av_packet_unref(pkt);av_frame_unref(frame); }

回收内存

上文的代码中,多次出现 goto 语句,我认为适当的使用 goto 使编程更加方便,比如执行过程结束的清理工作,以下是回收 ffmpeg AV 库产生的各种变量的内存,C/C++语言编程都需要多注意这一点。

clean5:av_frame_free(&frame);//av_parser_close(parser);
clean4:av_packet_free(&pkt);
clean3:if(NULL != video_decodec_ctx)avcodec_free_context(&video_decodec_ctx);if(NULL != audio_decodec_ctx)avcodec_free_context(&audio_decodec_ctx);
clean2:av_freep(&fmt_ctx->pb->buffer);av_freep(&fmt_ctx->pb);
clean1:avformat_close_input(&fmt_ctx);
end:return ret;

自由发挥

看到了这里,可以说入门 ffmpeg 编程了,什么,你问后面的转码怎么做?笔者就留白了,本文已经介绍了最基本的解码过程了,编码也就是逆向过程,我建议阅读 ffmepg 官方源码的example,以及多了解音视频各种格式的知识。

实际例子

我提供两个小例子在 github 上

  • 转码GIF
  • 生成缩略图

请安装好 linux 下 ffmepg 环境,找到例子代码里的 Makefile 文件编译,例如:

make -f Makefile_test_dump_info

以后我会将这两个小例子修改,实现跨语言调用,如 nodejs addon 或 golang cgo

Reference

ffmpeg example (本文代码就是从example改过来的)

一步步教ffmpeg的C语言音视频编程相关推荐

  1. 【ffmpeg for wince】音视频编解码多平台移植(for window/wince)

    from: http://www.cnblogs.com/windwithlife/archive/2009/05/31/1492728.html 终于完成了了第二个Client side原型(for ...

  2. 从零开始学习音视频编程技术(七) FFMPEG Qt视频播放器之SDL的使用

    从零开始学习音视频编程技术(七) FFMPEG Qt视频播放器之SDL的使用 原文地址:http://blog.yundiantech.com/?log=blog&id=10 前面介绍了使用F ...

  3. 从零开始学习音视频编程技术(六) FFMPEG Qt视频播放器之显示图像

    从零开始学习音视频编程技术(六) FFMPEG Qt视频播放器之显示图像 原文地址:http://blog.yundiantech.com/?log=blog&id=9 前面讲解了如何用FFM ...

  4. 从零开始学习音视频编程技术(四) FFMPEG的使用

    零开始学习音视频编程技术(四) FFMPEG的使用 原文地址:http://blog.yundiantech.com/?log=blog&id=7 音视频开发中最常做的就是编解码的操作了,以H ...

  5. ffmpeg命令录制windows音视频

    欢迎转载请注明出处:海漩涡 http://blog.csdn.net/tanhuifang520                 ffmpeg命令录制windows音视频 一.下载ffmpeg 存放在 ...

  6. FFmpeg入门详解--音视频原理及应用:梅会东:清华大学出版社

    大家好,我的第一本书正式出版了,可以在京东各大店铺抢购哦. <FFmpeg入门详解--音视频原理及应用:梅会东:清华大学出版社> 京东自营链接:https://item.jd.com/13 ...

  7. 教你如何做抖音视频广告,助你脱颖而出

    教你抖音如何做视频广告?在抖音上制作视频广告,能够充分利用抖音的平台以及流量优势,有效助力品牌的营销转化.巨量学提供了大量关于抖音视频广告的制作技巧,能够帮助您提升视频制作能力,在众多营销推广中脱颖而 ...

  8. C++音视频编程探秘

    C++音视频编程探秘(C++ Audio and Video Programming Unveiled) 一.引言(Introduction) C++音视频编程简介(Overview of C++ A ...

  9. 从零开始学习音视频编程技术--转自雲天之巔

    此为转载文章,主要是为了个人阅读方便,将博主的系列文章罗列出来,点击直接跳转. 从零开始学习音视频编程技术(一) 视频格式讲解 从零开始学习音视频编程技术(二) 音频格式讲解 从零开始学习音视频编程技 ...

最新文章

  1. (干货)微信小程序转发好友
  2. Paddle网络结构中的层和模型
  3. 汇编和python-python语言属于汇编语言吗?_后端开发
  4. think php a方法,PHP_ThinkPHP之A方法实例讲解,ThinkPHP的A方法用于在内部实例 - phpStudy...
  5. SuperSocket入门(二)- 探索AppServer、AppSession,Conmmand和App.config
  6. pb 修改数据窗口种指定字段位置_第三章 Python数据类型 容器
  7. java年份换算_java中日期的换算处理
  8. nmealib解析-----(1)
  9. 知己知彼:一篇来自前端同学对后端接口的吐槽!
  10. python百分号字符串_python--003--百分号字符串拼接、format
  11. Fragment中获取Activity的Context
  12. 聚合支付、第四方支付有哪些平台?
  13. 编程方式实现Excel转为JPG/PDF等格式
  14. 大岩量化小白科普:什么是量化交易?什么是宽客?
  15. 计算机硕士毕业论文范文,计算机论文:精选计算机硕士毕业论文范文十篇.docx...
  16. Kubernetes集群部署篇( 一)
  17. python 桑基图_数据可视化之 Sankey 桑基图的实现
  18. 计算机应用中dss是,基于数据仓库的决策支持系统(DSS)-计算机应用专业论文.docx...
  19. FPGA学习-m序列信号发生器
  20. GIt远程仓库pull拉取代码

热门文章

  1. 新来了个23岁的测试员,本以为是菜鸡,没想到是扮猪吃老虎
  2. harmonyos手机发布,HarmonyOS要来了!华为EMUI已改名 6月2日正式发布
  3. 校园表白墙、微信表白墙、校园墙 微信小程序 JAVA 开发记录与分享
  4. 【崔庆才教材】《Python3网络爬虫开发实战》3.4爬取猫眼电影排行代码更正(绕过美团验证码)
  5. 服务如何做熔断,降级,限流?
  6. CSS 实现background-image背景图片全屏铺满自适应
  7. 如何正确使用LCR测试仪测量电子元件
  8. 翻译要忠实于原文吗?
  9. 图文+视频手把手教您:如何增加Excel的可撤消(可撤销)次数
  10. 小白入坑安全测试指南