使用FFmpeg库对mp4文件进行解封装,提取mp4中的视频流和音频流输出到单独的输出文件中。

所谓的分离视频和音频是我们通俗的说法,官方的说法叫解封装。与解封装对应的叫封装或复用器,也就是将多个视频流或音频流合并成一个多媒体文件就叫封装。

API及数据结构介绍

在FFmpeg中解封装的大致流程如下图所示:

ffmpeg解封装流程图

在这里需要注意的一个点是av_find_best_stream不一定能获取到你想要的流,比如你想通过av_find_best_stream获取音频流的索引,笔者开发中发现对于某些格式是无法获取成功的, 此时需要遍历一下解封装上下文的流,通过流的解码器类型来进行获取,例如你想要获取音频流,则可以判断解码器的类型是否是音频解码器即可。

下面介绍一下实现分离视频和音频数据所需要使用到的主要API以及相关的数据结构。

本文福利, C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)↓↓↓↓↓↓见下面↓↓文章底部↓↓

1、libavformat

libavformat库,是FFmpeg中用于处理各种媒体容器格式的库,它描述了一个媒体文件或媒体流的构成和基本信息,它的两个主要功能就是封装和解封装,可以说它是贯穿整个FFmpeg的根。

在解封装时,我们主要用到avformat中的几个函数avformat_alloc_context、avformat_open_input和avformat_close_input,其中avformat_open_input和avformat_close_input是 一对搭配使用的函数,一个打开一个关闭,千万不要忘记avformat_close_input,否则会发生内存泄漏。

2、AVPacket

AVPacket类,用于存储编码后的帧数据。它一般由解封装导出,然后传递给解码器作为输入;又或者,从编码器作为输出,然后传递给封装去进行写入。

AVPacket可以表示一个视频包或者一个音频包,内部包含了这个视频包或音频包的播放时长,播放时间戳、二进制数据等相关信息。对于音视频等二进制数据,AVPacket内部使用了引用计数的方式进行数据共享。

对于AVPacket的那个字段,我们点进去头文件可以看到每个字段都有清晰的注释解析,这里就不细说了,例如:

typedef struct AVPacket {/*** A reference to the reference-counted buffer where the packet data is* stored.* May be NULL, then the packet data is not reference-counted.*/AVBufferRef *buf;/*** Presentation timestamp in AVStream->time_base units; the time at which* the decompressed packet will be presented to the user.* Can be AV_NOPTS_VALUE if it is not stored in the file.* pts MUST be larger or equal to dts as presentation cannot happen before* decompression, unless one wants to view hex dumps. Some formats misuse* the terms dts and pts/cts to mean something different. Such timestamps* must be converted to true pts/dts before they are stored in AVPacket.*/int64_t pts;/*** Decompression timestamp in AVStream->time_base units; the time at which* the packet is decompressed.* Can be AV_NOPTS_VALUE if it is not stored in the file.*/int64_t dts;uint8_t *data;int   size;int   stream_index;/*** A combination of AV_PKT_FLAG values*/int   flags;/*** Additional packet data that can be provided by the container.* Packet can contain several types of side information.*/AVPacketSideData *side_data;int side_data_elems;/*** Duration of this packet in AVStream->time_base units, 0 if unknown.* Equals next_pts - this_pts in presentation order.*/int64_t duration;int64_t pos;                            ///< byte position in stream, -1 if unknown/*** for some private data of the user*/void *opaque;/*** AVBufferRef for free use by the API user. FFmpeg will never check the* contents of the buffer ref. FFmpeg calls av_buffer_unref() on it when* the packet is unreferenced. av_packet_copy_props() calls create a new* reference with av_buffer_ref() for the target packet's opaque_ref field.** This is unrelated to the opaque field, although it serves a similar* purpose.*/AVBufferRef *opaque_ref;/*** Time base of the packet's timestamps.* In the future, this field may be set on packets output by encoders or* demuxers, but its value will be by default ignored on input to decoders* or muxers.*/AVRational time_base;
} AVPacket;

下面是使用FFmpeg进行解封装的主要API调用:

avformat_alloc_context     #封装结构体分配内存 // 可以不调用,avformat_open_input会判断入参是否为NULL,自行分配
avformat_open_input         #打开输入文件用于读取数据
av_find_best_stream#获取流信息
针对每个stream处理- pFormatContext->nb_streams- avcodec_find_decoder     #根据流中的编码参数AVCodecParameters,查找是否支持该编码- 判断流的类型 pLocalCodecParameters->codec_type- 保存AVCodecParameters和AVCodec,用于后续处理av_read_frame            #读取一包AVPacket数据包

提取视频

在FFMpeg中一般mp4解封装提取到的H264裸流是不带start code的,也就是提取到的这种H264裸流不能使用ffplay直接播放,还好FFmpeg很贴心地给我们提供了一个h264_mp4toannexb过滤器,通过这个过滤器我们可以很方便地 给提取到的H264加上start code,从而能让ffplay直接播放。

废话少说,直接上代码:

AVFormatContext *avFormatContext = nullptr;
AVPacket *avPacket = nullptr;
AVFrame *avFrame = nullptr;
FILE *h264_out = nullptr;
FILE *audio_out = nullptr;AVBSFContext *bsf_ctx = nullptr;void init_h264_mp4toannexb(AVCodecParameters *avCodecParameters) {if (nullptr == bsf_ctx) {const AVBitStreamFilter *bsfilter = av_bsf_get_by_name("h264_mp4toannexb");// 2 初始化过滤器上下文av_bsf_alloc(bsfilter, &bsf_ctx); //AVBSFContext;// 3 添加解码器属性avcodec_parameters_copy(bsf_ctx->par_in, avCodecParameters);av_bsf_init(bsf_ctx);}
}void MediaDeMuxerCore::de_muxer_video(std::string media_path, std::string out_video_path) {// 分配上下文avFormatContext = avformat_alloc_context();// 打开输入文件avformat_open_input(&avFormatContext, media_path.c_str(), nullptr, nullptr);// 获取视频流索引int video_index = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);if (video_index < 0) {std::cout << "没有找到视频流" << std::endl;} else {// 打印媒体信息av_dump_format(avFormatContext, 0, media_path.c_str(), 0);h264_out = fopen(out_video_path.c_str(), "wb");AVStream *video_stream = avFormatContext->streams[video_index];avPacket = av_packet_alloc();av_init_packet(avPacket);while (true) {int rect = av_read_frame(avFormatContext, avPacket);if (rect < 0) {std::cout << "视频流读取完毕" << std::endl;break;} else if (video_index == avPacket->stream_index) { // 只需要视频的std::cout << "写入视频size:" << avPacket->size << std::endl;// 这里需要注意一下,一般的mp4读出来的的packet是不带start code的,需要手动加上,如果是ts的话则是带上了start code的// 初始化过滤器,如果本身就是带了start code的调这个也没事,不会重复添加init_h264_mp4toannexb(video_stream->codecpar);if (av_bsf_send_packet(bsf_ctx, avPacket) != 0) {av_packet_unref(avPacket);   // 减少引用计数continue;       // 需要更多的包}av_packet_unref(avPacket);   // 减少引用计数while (av_bsf_receive_packet(bsf_ctx, avPacket) == 0) {// printf("fwrite size:%d\n", pkt->size);size_t size = fwrite(avPacket->data, 1, avPacket->size, h264_out);av_packet_unref(avPacket); //减少引用计数}} else {av_packet_unref(avPacket); //减少引用计数}}// 刷fflush(h264_out);}avformat_close_input(&avFormatContext);
}

提取音频

对于FFmpeg中解封装的音频AAC文件来说,mp4文件解封装出来的音频不附带adts头信息的,但是笔者看到有资料说对于ts格式的话好像解封装出来又是带有adts头的(笔者这个没有验证过)。对于这些没有附带adts头信息的aac音频文件,ffplay也是无法直接播放的,因此我们在提取音频信息时需要手动加上adts头信息。

针对添加adts头信息的话我们有两种方式,一种是对对adts比较熟悉的,可以在每个音频包的前面增加7个或者9个字节即可。还有一种就是使用FFmpeg的复用器封装功能,让其自动加上adts头信息。

其中使用FFmpeg内部复用器封装的步骤如下:

1、调用 av_guess_format 让ffmpeg帮你找到一个合适的文件格式。
2、调用 avformat_new_stream 为输出文件创建一个新流。3、调用 avio_open 打开新创建的文件。4、调用 avformat_write_header 写文件头。5、调用 av_interleaved_write_frame 写文件内容。6、调用 av_write_trailer 写文件尾。7、调用 avio_close 关闭文件

后面的代码笔者两种方式都简单测试了一下,提取到的aac音频文件都可以正常播放。

下面贴一下全部代码:

MediaDeMuxerCore.h#include <iostream>class MediaDeMuxerCore {public:MediaDeMuxerCore();~MediaDeMuxerCore();// 提取视频 h264裸流void de_muxer_video(std::string media_path,std::string out_video_path);// 提取音频 例如aac流void de_muxer_audio(std::string media_path,std::string out_audio_path);// 使用容器封装的方式提取aac流void de_muxer_audio_by_stream(std::string media_path,std::string out_audio_path);private:};
MediaDeMuxerCore.cpp#include "MediaDeMuxerCore.h"extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libavformat/avformat.h>
#include <libavcodec/bsf.h>
}MediaDeMuxerCore::MediaDeMuxerCore() {}AVFormatContext *avFormatContext = nullptr;
AVPacket *avPacket = nullptr;
AVFrame *avFrame = nullptr;
FILE *h264_out = nullptr;
FILE *audio_out = nullptr;AVBSFContext *bsf_ctx = nullptr;void init_h264_mp4toannexb(AVCodecParameters *avCodecParameters) {if (nullptr == bsf_ctx) {const AVBitStreamFilter *bsfilter = av_bsf_get_by_name("h264_mp4toannexb");// 2 初始化过滤器上下文av_bsf_alloc(bsfilter, &bsf_ctx); //AVBSFContext;// 3 添加解码器属性avcodec_parameters_copy(bsf_ctx->par_in, avCodecParameters);av_bsf_init(bsf_ctx);}
}void MediaDeMuxerCore::de_muxer_video(std::string media_path, std::string out_video_path) {// 分配上下文avFormatContext = avformat_alloc_context();// 打开输入文件avformat_open_input(&avFormatContext, media_path.c_str(), nullptr, nullptr);// 获取视频流索引int video_index = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);if (video_index < 0) {std::cout << "没有找到视频流" << std::endl;} else {// 打印媒体信息av_dump_format(avFormatContext, 0, media_path.c_str(), 0);h264_out = fopen(out_video_path.c_str(), "wb");AVStream *video_stream = avFormatContext->streams[video_index];avPacket = av_packet_alloc();av_init_packet(avPacket);while (true) {int rect = av_read_frame(avFormatContext, avPacket);if (rect < 0) {std::cout << "视频流读取完毕" << std::endl;break;} else if (video_index == avPacket->stream_index) { // 只需要视频的std::cout << "写入视频size:" << avPacket->size << std::endl;// 这里需要注意一下,一般的mp4读出来的的packet是不带start code的,需要手动加上,如果是ts的话则是带上了start code的// 初始化过滤器,如果本身就是带了start code的调这个也没事,不会重复添加init_h264_mp4toannexb(video_stream->codecpar);if (av_bsf_send_packet(bsf_ctx, avPacket) != 0) {av_packet_unref(avPacket);   // 减少引用计数continue;       // 需要更多的包}av_packet_unref(avPacket);   // 减少引用计数while (av_bsf_receive_packet(bsf_ctx, avPacket) == 0) {// printf("fwrite size:%d\n", pkt->size);size_t size = fwrite(avPacket->data, 1, avPacket->size, h264_out);av_packet_unref(avPacket); //减少引用计数}} else {av_packet_unref(avPacket); //减少引用计数}}// 刷fflush(h264_out);}avformat_close_input(&avFormatContext);
}const int sampling_frequencies[] = {96000,  // 0x088200,  // 0x164000,  // 0x248000,  // 0x344100,  // 0x432000,  // 0x524000,  // 0x622050,  // 0x716000,  // 0x812000,  // 0x911025,  // 0xa8000   // 0xb// 0xc d e f是保留的
};int adts_header(char *const p_adts_header, const int data_length,const int profile, const int samplerate,const int channels) {int sampling_frequency_index = 3; // 默认使用48000hzint adtsLen = data_length + 7;// 匹配采样率int frequencies_size = sizeof(sampling_frequencies) / sizeof(sampling_frequencies[0]);int i = 0;for (i = 0; i < frequencies_size; i++) {if (sampling_frequencies[i] == samplerate) {sampling_frequency_index = i;break;}}if (i >= frequencies_size) {std::cout << "没有找到支持的采样率" << std::endl;return -1;}p_adts_header[0] = 0xff;         //syncword:0xfff                          高8bitsp_adts_header[1] = 0xf0;         //syncword:0xfff                          低4bitsp_adts_header[1] |= (0 << 3);    //MPEG Version:0 for MPEG-4,1 for MPEG-2  1bitp_adts_header[1] |= (0 << 1);    //Layer:0                                 2bitsp_adts_header[1] |= 1;           //protection absent:1                     1bitp_adts_header[2] = (profile) << 6;            //profile:profile               2bitsp_adts_header[2] |=(sampling_frequency_index & 0x0f) << 2; //sampling frequency index:sampling_frequency_index  4bitsp_adts_header[2] |= (0 << 1);             //private bit:0                   1bitp_adts_header[2] |= (channels & 0x04) >> 2; //channel configuration:channels  高1bitp_adts_header[3] = (channels & 0x03) << 6; //channel configuration:channels 低2bitsp_adts_header[3] |= (0 << 5);               //original:0                1bitp_adts_header[3] |= (0 << 4);               //home:0                    1bitp_adts_header[3] |= (0 << 3);               //copyright id bit:0        1bitp_adts_header[3] |= (0 << 2);               //copyright id start:0      1bitp_adts_header[3] |= ((adtsLen & 0x1800) >> 11);           //frame length:value   高2bitsp_adts_header[4] = (uint8_t) ((adtsLen & 0x7f8) >> 3);     //frame length:value    中间8bitsp_adts_header[5] = (uint8_t) ((adtsLen & 0x7) << 5);       //frame length:value    低3bitsp_adts_header[5] |= 0x1f;                                 //buffer fullness:0x7ff 高5bitsp_adts_header[6] = 0xfc;      //‭11111100‬       //buffer fullness:0x7ff 低6bitsreturn 0;
}/*** @param media_path* @param out_audio_path*/
void MediaDeMuxerCore::de_muxer_audio(std::string media_path, std::string out_audio_path) {// 分配上下文avFormatContext = avformat_alloc_context();// 打开输入文件avformat_open_input(&avFormatContext, media_path.c_str(), nullptr, nullptr);// 获取视频流索引int audio_index = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);audio_out = fopen(out_audio_path.c_str(), "wb");if (audio_index < 0) {std::cout << "没有找到音频流" << std::endl;} else {// 打印媒体信息av_dump_format(avFormatContext, 0, media_path.c_str(), 0);audio_out = fopen(out_audio_path.c_str(), "wb");AVStream *audio_stream = avFormatContext->streams[audio_index];avPacket = av_packet_alloc();av_init_packet(avPacket);while (true) {int rect = av_read_frame(avFormatContext, avPacket);if (rect < 0) {std::cout << "音频流读取完毕" << std::endl;break;} else if (audio_index == avPacket->stream_index) { // 只需要音频的// adts 头是7个字节,也有可能是9个字节char adts_header_buf[7] = {0};adts_header(adts_header_buf, avPacket->size,avFormatContext->streams[audio_index]->codecpar->profile,avFormatContext->streams[audio_index]->codecpar->sample_rate,avFormatContext->streams[audio_index]->codecpar->channels);// 先写adts头,有些是解封装出来就带有adts头的比如tsfwrite(adts_header_buf, 1, 7, audio_out);// 写入aac包fwrite(avPacket->data, 1, avPacket->size, audio_out);av_packet_unref(avPacket); //减少引用计数} else {av_packet_unref(avPacket); //减少引用计数}}// 刷流fflush(audio_out);}}void MediaDeMuxerCore::de_muxer_audio_by_stream(std::string media_path, std::string out_audio_path) {// 分配上下文avFormatContext = avformat_alloc_context();// 打开输入文件avformat_open_input(&avFormatContext, media_path.c_str(), nullptr, nullptr);// 获取视频流索引int audio_index = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);audio_out = fopen(out_audio_path.c_str(), "wb");if (audio_index < 0) {std::cout << "没有找到音频流" << std::endl;} else {std::cout << "音频时长:" << avFormatContext->streams[audio_index]->duration * av_q2d(avFormatContext->streams[audio_index]->time_base) << std::endl;AVFormatContext *out_format_context = avformat_alloc_context();const AVOutputFormat *avOutputFormat = av_guess_format(nullptr,out_audio_path.c_str(), nullptr);out_format_context->oformat = avOutputFormat;AVStream *aac_stream = avformat_new_stream(out_format_context, NULL);// 编码信息拷贝int ret = avcodec_parameters_copy(aac_stream->codecpar,avFormatContext->streams[audio_index]->codecpar);ret = avio_open(&out_format_context->pb,out_audio_path.c_str(),AVIO_FLAG_WRITE);if(ret < 0){std::cout << "输出流打开失败" << std::endl;}avformat_write_header(out_format_context, nullptr);avPacket = av_packet_alloc();av_init_packet(avPacket);while (true){ret = av_read_frame(avFormatContext,avPacket);if(ret < 0){std::cout << "read end " << std::endl;break;}if(avPacket->stream_index == audio_index){avPacket->stream_index = aac_stream->index;// 时间基转换av_packet_rescale_ts(avPacket,avPacket->time_base,aac_stream->time_base);ret = av_write_frame(out_format_context,avPacket);if(ret < 0){std::cout << "aad 写入失败" << std::endl;} else{std::cout << "aad 写入成功"  << std::endl;}}av_packet_unref(avPacket);}av_write_trailer(out_format_context);avformat_flush(out_format_context);}}MediaDeMuxerCore::~MediaDeMuxerCore() {if (nullptr != avFormatContext) {avformat_free_context(avFormatContext);}if (nullptr != avPacket) {av_packet_free(&avPacket);}if (nullptr != avFrame) {av_frame_free(&avFrame);}if (nullptr != h264_out) {fclose(h264_out);h264_out = nullptr;}if (nullptr != audio_out) {fclose(audio_out);audio_out = nullptr;}if (nullptr != bsf_ctx) {av_bsf_free(&bsf_ctx);}
}

原文链接:FFmpeg分离视频和音频 - 资料 - 我爱音视频网 - 构建全国最权威的音视频技术交流分享论坛

本文福利, C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)↓↓↓↓↓↓见下面↓↓文章底部↓↓

FFmpeg分离(解封装)视频和音频相关推荐

  1. FFmpeg解封装、解码音频和视频(分别使用OpenGL和OpenAL播放)

    1 ffmpeg解码大致流程   下图是ffmpeg解码播放音视频的基本流程: 首先是网络媒体解协议,解协议之后得到对应的媒体文件比如mp4,ts等,这些格式是媒体文件的封装格式,也就是将音频,视频, ...

  2. ffmpeg所支持的所有视频或音频文件类型

    1. 按 我们可以通过ffmpeg -formats来获取ffmpeg所支持的所有文件类型,其中文件前的标注说明如下: D. = Demuxing supported(支持视频/音频封装) .E = ...

  3. FFMPEG常用的一些命令介绍:音频录制、视频录制

    1.视频和音频单独抓取 如果指定输入格式和设备,则ffmpeg可以直接捕获视频和音频. Linux下捕获摄像头的数据保存成视频文件: # ffmpeg -f video4linux2 -s 1280x ...

  4. 人声分离:快速分离视频和音频中的人声和背景伴奏,超简单

    目前很多手机上比较常见的视频剪辑APP都有将视频声去掉这个功能,通过特定音频消音的方式将视频里声音部分去除,但是往往我们在做视频剪辑的时候,只是需要视频中的人声或者视频中的背景伴奏时,这些主流视频剪辑 ...

  5. 使用ffmpeg进行简单的视频编辑

    视频格式小科普 在开始下面的教程之前有必要先简单科普一下视频格式的知识. 视频格式是一种非常不专业的叫法,事实上,视频有编码格式和容器格式两种.编码格式之于容器格式就像牛奶之于杯子一样. 常见的视频文 ...

  6. 基于 ffmpeg + Webassembly 实现前端视频帧提取

    作者:jordiwang  https://juejin.im/post/6854573219454844935 现有的前端视频帧提取主要是基于 canvas + video 标签的方式,在用户本地选 ...

  7. 视频提取音频 - 手机视频在线提取音频提取器

    怎样将视频中的音频提取出来?一刀工具箱提供一键可将手机相册中的视频提取音频,便捷的快速获取到视频中的音频文件,视频提取音频的软件,免费转换 mp3 格式的工具. 代码片段 buildVideo(){l ...

  8. 音视频解封装--解封装分离音频AAC文件

    1:简介 如下图所示,解封装就是将Flv.MP4等文件解封装为视频H264或H265压缩数据,音频解封装为AAC压缩数据. 2:ADTS头结构 ADTS的全称是Audio Data Transport ...

  9. FFMPEG 视频图像解封装解码

    FFMPEG4.0 音频解码解封装 FFMPEG 音频封装编码 下面的函数方法基于最新的FFMPEG 4.0(4.X): 本文讲是如何从一个视频文件中提取出其中的图像数据,并将图像数据保存到文件中. ...

最新文章

  1. 微信小程序超级占内存_6款宝藏微信小程序,简单又实用,节约内存就靠它了...
  2. 【设计模式】责任链模式 ( 简介 | 适用场景 | 优缺点 | 代码示例 )
  3. 计算机与应用教学,教学方法与教学手段
  4. 2014年3月29日缅甸将举行人口普查
  5. python 学习 [day6]
  6. 列表(list)之一定义 添加 删除 排序 反转 索引等其他操作
  7. PHP中session特点及用途,PHP特点之会话机制2——Session及其使用
  8. ArcGIS个人数据库(mdb)中矢量字段无法删除
  9. python+OpenCV图像处理(五)图像的阈值分割
  10. [bzoj1086][SCOI2005]王室联邦
  11. 第十一章 Hibernate的查询 HQL面向对象的查询语言
  12. 进阶篇第九期:相册与拍照的后处理
  13. 如何制作一寸、二寸、六寸照片。以后不用再去照相馆了!!! 转~版本更新...
  14. SAP ABAP APO计划订单生产日期调整
  15. 预测模型如何改进自动化决策
  16. HP喷墨打印机真空加墨的方法
  17. python对mysql增删改查+计算器+九九乘法表
  18. 【学习笔记】语义分割综述
  19. Android Animator(Android动画)
  20. Android 高德地图添加线段纹理

热门文章

  1. 切比雪夫距离 入门例题
  2. 无限流量与5G即将来临,我们距淘汰Wi-Fi还有多少时间?
  3. 基于微信小程序java音乐播放器毕业设计论文/程序代码
  4. 一度智信|拼多多店铺取名大全
  5. JS实现将数字金额转换为大写人民币汉字
  6. 2的零次方加到2的n次方c语言,计算2的N次方........有什么错吗?
  7. 分布式事务二 基础理论
  8. 学计算机要选什么科,计算机要学什么科目
  9. 对100层楼两个玻璃球测试问题的理解
  10. win10查看linux文件夹,Win10系统访问Linux子系统中文件的教程