最简单的基于FFMPEG的封装格式转换器(致敬雷霄骅)

最近项目需要,开始学习ffmpeg。网上资料很多,但是大多数资料都是几年前的。ffmpeg 的API 这几年变化蛮大的。按照网上的教程来写代码会遇到各种问题。所以我才想写这么一个专栏。用ffmpeg 比较新的 API 来把一些常用的功能都写一遍。

我大概学习 ffmpeg 有一周多了。感觉学习 ffmpeg 有三个资源可以好好利用。

  1. 机械工业出版社出版, 刘歧、赵文杰编著的《FFmpeg从入门到精通》
  2. 雷霄骅 的博客 https://blog.csdn.net/leixiaohua1020
  3. ffmpeg 的源代码,尤其是 doc\examples 目录下的例子程序。

我的这篇博客其实就是雷神的一篇博客(https://blog.csdn.net/leixiaohua1020/article/details/25422685)的更新版。

雷霄骅,中国传媒大学通信与信息系统专业博士生,写了很多音视频编解码方面的科普文章,被人们尊称为雷神。可惜天妒英才,2016 年猝死于实验室。他的博客确实写得很好,值得每一位准备学习音视频编解码的同学好好的读读。他的博客里也给出了很多 ffmpeg 的例子程序,但是由于ffmpeg API 版本更新,他的程序代码很多已经无法编译运行了。我准备写一系列的博客,把他的代码做少许修改,让这些代码能在最新版的 ffmpeg 上运行。

关于这篇博客中需要用到的 ffmpeg 的基础知识,请读雷神的博客 https://blog.csdn.net/leixiaohua1020/article/details/25422685。

首先,在正式程序开始之前。我先给一个更简单的例子程序。这个程序会打开一个媒体文件,然后输出这个媒体文件的一些基本信息。这里我们的媒体文件来自 https://samples.mplayerhq.hu/avi/AV36_1.avi 。

这里来介绍一下 https://samples.mplayerhq.hu 这个网站。大名鼎鼎的 mplayer 想必大家都听说过, samples.mplayerhq.hu 站点上的文件就是 mplayer 提供的用来测试 mplayer 解码能力的测试样本。基本上你听说过的、没听说过的格式文件都有。并且每个文件都不太大,用来测试我们的程序最合适不过。

下面是例子代码:

extern "C" {
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
}
void msg(const char * str, int ret)
{static char err[512];if(ret < 0){av_strerror(ret, err, 1024);printf("%s error: %s\n", str, err);exit(ret);}else{printf("%s : success.\n", str);}
}void test00()
{const char * filename = "D:\\AV36_1.avi";AVFormatContext *pFormatCtx = avformat_alloc_context();msg("avformat_open_input", avformat_open_input(&pFormatCtx, filename, nullptr, nullptr));msg("avformat_find_stream_info", avformat_find_stream_info(pFormatCtx, nullptr));av_dump_format(pFormatCtx, 0, filename, 0);avformat_close_input(&pFormatCtx);avformat_free_context(pFormatCtx);
}

这个代码的输出结果如下:

avformat_open_input : success.
avformat_find_stream_info : success.
Input #0, avi, from 'D:\AV36_1.avi':Duration: 00:00:32.93, start: 0.000000, bitrate: 2372 kb/sStream #0:0: Video: indeo5 (IV50 / 0x30355649), yuv410p, 320x240, 2058 kb/s, 15 fps, 15 tbr, 15 tbn, 15 tbcMetadata:title           : Steyr.avi ���#1Stream #0:1: Audio: adpcm_ms ([2][0][0][0] / 0x0002), 22050 Hz, 2 channels, s16, 176 kb/sMetadata:title           : Sound Forge 4.0 Audio

这里简单解释几点:

  1. msg() 函数是个辅助函数。用这个函数就不用像雷神的代码那样每一个函数调用都要用if 语句去判断返回值了。如果调用失败,会直接给出错误提示并退出程序。

  2. 另外就是这个代码其实不需要调用 avformat_find_stream_info()。因为 av_dump_format()不要求提前调用avformat_find_stream_info() 的。不过我们后面的封装格式转换程序需要这个操作,所以这里也没去掉这一步。

  3. avformat_alloc_context()和avformat_free_context() 配对的, avformat_open_input() 和avformat_close_input() 是配对的。

  4. AVFormatContext 是什么?简单的说一个媒体文件就要对应一个 AVFormatContext。所谓的Context 其实就是指的这个媒体文件的内容。

另外,我们可以看到这个视频文件中有两个 Steam。第一个 Stream 是视频流,编码格式是 indeo5。第二个Stream 是音频流,编码格式是 adpcm_ms。

好了下面开始我们的工作。我们要把AV36_1.avi 转换为 AV36_1.mkv 文件。并且保持 音视频的编码格式不变。

代码如下:

void test00()
{const char * filename = "D:\\AV36_1.avi";const char * filename2 = "D:\\AV36_1.mkv";AVFormatContext *pFormatCtx = avformat_alloc_context();msg("avformat_open_input",avformat_open_input(&pFormatCtx, filename, nullptr, nullptr));msg("avformat_find_stream_info",avformat_find_stream_info(pFormatCtx, nullptr));AVFormatContext *pFormatCtx2 = avformat_alloc_context();msg("avformat_alloc_output_context2",avformat_alloc_output_context2(&pFormatCtx2, nullptr, nullptr, filename2));for (int i = 0; i < pFormatCtx->nb_streams; i++){AVStream *in_stream = pFormatCtx->streams[i];AVStream * out_stream = avformat_new_stream(pFormatCtx2, NULL);avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);out_stream->codecpar->codec_tag = 0;}av_dump_format(pFormatCtx2, 0, filename2, 1);if (!(pFormatCtx2->oformat->flags & AVFMT_NOFILE)){msg("avio_open",avio_open(&pFormatCtx2->pb, filename2, AVIO_FLAG_WRITE));}msg("avformat_write_header", avformat_write_header(pFormatCtx2, nullptr));AVPacket pkt;while (1){int ret = av_read_frame(pFormatCtx, &pkt);if (ret < 0)  break;AVStream * in_stream  = pFormatCtx->streams[pkt.stream_index];AVStream  * out_stream = pFormatCtx2->streams[pkt.stream_index];/* copy packet */pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);pkt.pos = -1;av_interleaved_write_frame(pFormatCtx2, &pkt);av_packet_unref(&pkt);}msg("av_write_trailer", av_write_trailer(pFormatCtx2));avformat_close_input(&pFormatCtx2);avformat_free_context(pFormatCtx2);avformat_close_input(&pFormatCtx);avformat_free_context(pFormatCtx);
}

输出结果如下,可以看到执行成功了:

avformat_open_input : success.
avformat_find_stream_info : success.
avformat_alloc_output_context2 : success.
Output #0, matroska, to 'D:\AV36_1.mkv':Stream #0:0: Video: indeo5, yuv410p, 320x240, q=2-31, 2058 kb/sStream #0:1: Audio: adpcm_ms, 22050 Hz, 2 channels, s16, 176 kb/s
avio_open : success.
avformat_write_header : success.
av_write_trailer : success.

下面主要来讲讲和雷神的代码有区别的几行。在雷神的代码中,复制 Stream 时用到如下的代码块:

ret = avcodec_copy_context(out_stream->codec, in_stream->codec);if (ret < 0) {printf( "Failed to copy context from input to output stream codec context\n");goto end;}

在新版的 ffmpeg 中,avcodec_copy_context() 函数已经被废弃。我们不应该直接复制 AVStream 中的 codec。而应该复制 codecpar,所以需要改用 avcodec_parameters_copy() 函数。

out_stream->codec->codec_tag = 0;
也需要更新为:
out_stream->codecpar->codec_tag = 0;

雷神还有几行代码:

if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;

这几行代码中的CODEC_FLAG_GLOBAL_HEADER 也已经不存在了,可以改为 AV_CODEC_FLAG_GLOBAL_HEADER。或者向我这样干脆就不要了。

剩下的代码变化不大,我觉得值得讲一讲的是下面这几行代码:

pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);

这三行代码的作用是调整 pts,dts 和 duration。最开始我一直不理解,我又没有转码,这三个时间按说不会有变化,为啥要调整。后来我输出了 in_stream->time_base 和 out_stream->time_base 的值,才发现这两个值并不相同。按照我现在的理解,time_base 是和封装格式相关的。 avi 文件的 time_base 和 mkv 文件的 time_base 不同,而pts、dts 和 duration 都是以 time_base 作为基本单位的 ,所以需要转换一下。

这里还想再吐槽一下,av_read_frame() 输出的结果根本不是 frame ,而是 AVPacket。frame 和 packet 有什么关系呢?简单的说一个 packet 里面可能会有若干个 frame。具体有几个需要用解码器解一下才能确定。一个 packet 里的数据是属于某一个 stream 的,不会出现一个 packet 里既有视频数据又有音频数据的。具体属于哪个 stream 要通过 pkt.stream_index 来确定。那么如果我们只想要视频的 packet 怎么办呢?没有什么简便的办法,只能先获得一个packet 然后判读是不是我们要的那个 stream 的。如果不是就 av_packet_unref() ,然后再读。

有些封装格式除了有视频流、音频流之外,还会有字幕流等其他的流。下面给个例子代码,只选择出视频流、音频流,其他的流都忽略。这个代码基本上是来自 doc\examples\remuxing.c , 我只做了少许的改写。

    const char * filename = "D:\\AV36_1.avi";const char * filename2 = "D:\\AV36_1.mkv";qDebug() << "hello";AVFormatContext *pFormatCtx = avformat_alloc_context();msg("avformat_open_input",avformat_open_input(&pFormatCtx, filename, nullptr, nullptr));msg("avformat_find_stream_info",avformat_find_stream_info(pFormatCtx, nullptr));av_dump_format(pFormatCtx, 0, filename, 0);int stream_index = 0;int *stream_mapping = NULL;int stream_mapping_size = 0;stream_mapping_size = pFormatCtx->nb_streams;stream_mapping = (int *)av_mallocz_array(stream_mapping_size, sizeof(*stream_mapping));AVFormatContext *pFormatCtx2 = avformat_alloc_context();msg("avformat_alloc_output_context2",avformat_alloc_output_context2(&pFormatCtx2, nullptr, nullptr, filename2));qDebug() << "pFormatCtx->nb_streams = " << pFormatCtx->nb_streams;for (int i = 0; i < pFormatCtx->nb_streams; i++){AVStream *in_stream = pFormatCtx->streams[i];AVCodecParameters *in_codecpar = in_stream->codecpar;qDebug() << "streams[" << i << "], " << "codec_type = " << (enum AVMediaType)in_codecpar->codec_type;if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&//  AVMEDIA_TYPE_AUDIO &&in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE){stream_mapping[i] = -1;continue;}stream_mapping[i] = stream_index++;AVStream * out_stream = avformat_new_stream(pFormatCtx2, NULL);avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);out_stream->codecpar->codec_tag = 0;}av_dump_format(pFormatCtx2, 0, filename2, 1);if (!(pFormatCtx2->oformat->flags & AVFMT_NOFILE)){msg("avio_open",avio_open(&pFormatCtx2->pb, filename2, AVIO_FLAG_WRITE));}msg("avformat_write_header", avformat_write_header(pFormatCtx2, nullptr));AVPacket pkt;while (1){int ret = av_read_frame(pFormatCtx, &pkt);if (ret < 0)  break;if (pkt.stream_index >= stream_mapping_size || stream_mapping[pkt.stream_index] < 0){av_packet_unref(&pkt);continue;}pkt.stream_index = stream_mapping[pkt.stream_index];AVStream * in_stream  = pFormatCtx->streams[pkt.stream_index];AVStream  * out_stream = pFormatCtx2->streams[pkt.stream_index];/* copy packet */pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);pkt.pos = -1;av_interleaved_write_frame(pFormatCtx2, &pkt);av_packet_unref(&pkt);}msg("av_write_trailer", av_write_trailer(pFormatCtx2));avformat_close_input(&pFormatCtx);/* close output */if (pFormatCtx2 && !(pFormatCtx2->flags & AVFMT_NOFILE)){avio_closep(&pFormatCtx2->pb);}avformat_free_context(pFormatCtx2);av_freep(&stream_mapping);

至此,本篇博客的主要内容就都讲完了。这里多说几句题外话。

  1. 在 win 下编译 ffmpeg 可以用 vcpkg,很方便。当然用 msys2 环境去编译也不难。
  2. 多读 doc\examples 下的代码。网上找的代码片段可能会失效,doc\examples 的代码永远不会。ffmpeg 的作者们在发布新版 ffmpeg 前,都会保证 doc\examples 的代码是可编译运行的。
  3. doc\examples 的代码很多,怎么入门呢。从简单的开始入手。哪个文件小就从哪个文件开始学。或者读雷霄骅的博客,然后在 doc\examples里面找对应的代码。

这篇博客的代码用C++ 改写了一遍:
https://blog.csdn.net/liyuanbhu/article/details/121744275

最简单的基于FFMPEG的封装格式转换器(致敬雷霄骅)相关推荐

  1. 最简单的基于FFMPEG的封装格式转换器(无编解码)

    2019独角兽企业重金招聘Python工程师标准>>> 本文介绍一个基于FFMPEG的封装格式转换器.所谓的封装格式转换,就是在AVI,FLV,MKV,MP4这些格式之间转换(对应. ...

  2. 最简单的基于FFMPEG的封装格式转换器(C++Qt 版)

    最简单的基于FFMPEG的封装格式转换器(C++Qt 版) 这篇博客是我上篇博客的延续.建议大家先看看我上篇博客: https://blog.csdn.net/liyuanbhu/article/de ...

  3. 最简单的基于FFMPEG的封装格式转换器

    最简单的基于FFMPEG的封装格式转换器(无编解码) https://blog.csdn.net/leixiaohua1020/article/details/25422685 =========== ...

  4. 最简单的基于FFmpeg的封装格式处理:视音频分离器(demuxer)

    ===================================================== 最简单的基于FFmpeg的封装格式处理系列文章列表: 最简单的基于FFmpeg的封装格式处理 ...

  5. 最简单的基于FFmpeg的封装格式处理:视音频复用器(muxer)

    ===================================================== 最简单的基于FFmpeg的封装格式处理系列文章列表: 最简单的基于FFmpeg的封装格式处理 ...

  6. 基于FFMPEG的封装格式转换器

    简介 本文介绍一个基于FFMPEG的封装格式转换器.所谓的封装格式转换,就是在AVI,FLV,MKV,MP4这些格式之间转换(对应.avi,.flv,.mkv,.mp4文件).需要注意的是,本程序并不 ...

  7. 基于FFmpeg的封装格式MP4(TS)

    一. 封装MP4原理: 每一帧音频或视频都有一个持续时间:duration: 采样频率是指将模拟声音波形进行数字化时,每秒钟抽取声波幅度样本的次数. .正常人听觉的频率范围大约在20Hz~20kHz之 ...

  8. 基于 FFMPEG 的像素格式变换(swscale,致敬雷霄骅)

    基于 FFMPEG 的像素格式变换(swscale,致敬雷霄骅) 前几天写了几篇关于ffmpeg 编程转封装的入门文章,下一步本来是要写转码或者编码的.但是发现无论是转码还是编码,都会遇到图像像素格式 ...

  9. 基于 FFMPEG 的视频编码(libavcodec ,致敬雷霄骅)

    基于 FFMPEG 的视频编码(libavcodec ,致敬雷霄骅) 本文参考了雷博士的博客: 最简单的基于FFmpeg的视频编码器-更新版(YUV编码为HEVC(H.265)) 还参考了另一篇博客: ...

  10. 【开源项目】基于FFmpeg的封装格式转换

    /* * 一笑奈何 * cn-yixiaonaihe.blog.csdn.net */#include <iostream> #include <thread> extern ...

最新文章

  1. 网络安全渗透--判断网站使用何种网页语言,判断网站所用服务器
  2. 在selenium中使用css选择器进行元素定位(一)
  3. mysql 多配置文件实例安装_mysql安装之多实例多配置文件安装
  4. 非对称卷积—Asymmetric Convolutions
  5. css线条伸缩_CSS3弹性伸缩布局(一)——box布局
  6. All about the “paper”
  7. 单片机固件烧录器 Firmware Writer Android APP
  8. 小米台灯突然自己亮了_米家台灯Pro,工作读书随我选
  9. 暗影精灵4 i5-8300H 核显驱动完美触控板声音亮度调节电池电量显示黑苹果EFI引导
  10. 画象棋棋盘c语言程序设计,绘制中国象棋棋盘(c语言).docx
  11. P3545 [POI2012]HUR-Warehouse Store [堆贪心]
  12. petalinux挂载88e1512、88e1111及base-t、base-x转换
  13. 【华为上机真题】工号不够用咋办
  14. (新版)SJTU-OJ-1011. John and Cows
  15. mp4文档ISO/IEC 14496 part 12解读
  16. 向sdcard中添加文件出现Failed to push the item(s)
  17. 塞尔希奥·阿奎罗和 The Sandbox 携手合作,激活元宇宙足球迷!
  18. 与“色情”割席,陌生人社交如何重塑品牌形象?
  19. 网页HTML中br元素及nobr元素的实际应用
  20. 6.47.2 Extended Asm - Assembler Instructions with C Expression Operands

热门文章

  1. ZEGO 自研客户端配置管理系统 —— 云控
  2. 水晶报表教程:手把手教你制作基本报表
  3. 计算机算法分析与设计心得体会,算法设计与分析课程的心得体会
  4. html面试信息登记表
  5. 《Ruminations on C++/C++沉思录》学习笔记一————koening和Moo夫妇访谈
  6. BiShop 模式识别与机器学习
  7. access mysql知乎_Access数据库如何使用?
  8. 多尺度卷积稀疏编码的无监督迁移学习
  9. batchplot插件用法_Batchplot辅助插件常见问题解决方法
  10. cad插件加载bplot成功用不了_教大家Batchplot使用常见问题的解决办法