FFmpeg连载2-分离视频和音频
前言
前面我們搭建好了FFmpeg的開發環境,今天我們就來小試牛刀,使用FFmpeg庫對mp4文件進行解封裝,提取mp4中的視頻流和音頻流輸出到單獨的輸出文件中。
關于FFmpeg開發環境搭建可以看我之前的文章:FFmpeg連載1-開發環境搭建
所謂的分離視頻和音頻是我們通俗的說法,官方的說法叫解封裝。與解封裝對應的叫封裝或復用器,也就是將多個視頻流或音頻流合并成一個多媒體文件就叫封裝。
API及數據結構介紹
在FFmpeg中解封裝的大致流程如下圖所示:
在這里需要注意的一個點是av_find_best_stream不一定能獲取到你想要的流,比如你想通過av_find_best_stream獲取音頻流的索引,筆者開發中發現對于某些格式是無法獲取成功的,
此時需要遍歷一下解封裝上下文的流,通過流的解碼器類型來進行獲取,例如你想要獲取音頻流,則可以判斷解碼器的類型是否是音頻解碼器即可。
下面介紹一下實現分離視頻和音頻數據所需要使用到的主要API以及相關的數據結構。
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連載1-開發環境搭建
關注我,一起進步,人生不止coding!!!
總結
以上是生活随笔為你收集整理的FFmpeg连载2-分离视频和音频的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 理解密码学中的双线性映射
- 下一篇: 设计模式:(生成器模式)