FFmpegシリアル化2-ビデオとオーディオを分離

序文

以前にFFmpegの開発環境を構築しましたが、今日はFFmpegライブラリを使用してmp4ファイルのカプセル化を解除し、mp4でビデオストリームとオーディオストリームを抽出して、別の出力ファイルに出力するように最善を尽くします。FFmpeg開発環境の構築に関する私の以前の記事を参照してください。

いわゆるビデオとオーディオの分離は私たちの一般的な用語であり、正式な用語はカプセル化解除と呼ばれています。カプセル化解除に対応するものはカプセル化またはマルチプレクサと呼ばれます。つまり、複数のビデオストリームまたはオーディオストリームをマルチメディアファイルに結合することをカプセル化と呼びます。

APIとデータ構造の紹介

FFmpegでのカプセル化解除の一般的なプロセスを次の図に示します。

ffmpegカプセル化解除のフローチャート

ここで注意すべき点の1つは、av_find_best_stream必要なストリームを取得できない可能性があることです。たとえば、オーディオストリームのインデックスを取得したい場合av_find_best_stream、作成者は、一部の形式では正常に取得できないことを発見しました。現時点では、トラバースしてカプセル化を解除する必要があります。コンテキストのストリームは、ストリームのデコーダタイプによって取得されます。たとえば、オーディオストリームを取得する場合は、デコーダのタイプがオーディオデコーダであるかどうかを判断できます。

以下では、ビデオデータとオーディオデータを分離するために使用される主なAPIと関連するデータ構造を紹介します。

1、libavformat

libavformatライブラリは、FFmpegでさまざまなメディアコンテナ形式を処理するためのライブラリであり、メディアファイルまたはメディアストリームの構成と基本情報を記述します。その2つの主な機能は、カプセル化とカプセル化解除です。FFmpeg全体で実行されると言えます。ルート。

解凍するときは、主にavformatavformat_alloc_contextでいくつかの関数を使用します。ここで、avformat_open_input関数のペアであり、一方が開かれ、もう一方が閉じられます。忘れないでください。そうしないと、メモリリークが発生します。avformat_close_inputavformat_open_inputavformat_close_inputavformat_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を呼び出して、ファイルを閉じます

作者は次のコードを2つの方法でテストしただけで、抽出された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,  // 0x0
        88200,  // 0x1
        64000,  // 0x2
        48000,  // 0x3
        44100,  // 0x4
        32000,  // 0x5
        24000,  // 0x6
        22050,  // 0x7
        16000,  // 0x8
        12000,  // 0x9
        11025,  // 0xa
        8000   // 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; // 默认使用48000hz
    int 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                          高8bits
    p_adts_header[1] = 0xf0;         //syncword:0xfff                          低4bits
    p_adts_header[1] |= (0 << 3);    //MPEG Version:0 for MPEG-4,1 for MPEG-2  1bit
    p_adts_header[1] |= (0 << 1);    //Layer:0                                 2bits
    p_adts_header[1] |= 1;           //protection absent:1                     1bit

    p_adts_header[2] = (profile) << 6;            //profile:profile               2bits
    p_adts_header[2] |=
            (sampling_frequency_index & 0x0f) << 2; //sampling frequency index:sampling_frequency_index  4bits
    p_adts_header[2] |= (0 << 1);             //private bit:0                   1bit
    p_adts_header[2] |= (channels & 0x04) >> 2; //channel configuration:channels  高1bit

    p_adts_header[3] = (channels & 0x03) << 6; //channel configuration:channels 低2bits
    p_adts_header[3] |= (0 << 5);               //original:0                1bit
    p_adts_header[3] |= (0 << 4);               //home:0                    1bit
    p_adts_header[3] |= (0 << 3);               //copyright id bit:0        1bit
    p_adts_header[3] |= (0 << 2);               //copyright id start:0      1bit
    p_adts_header[3] |= ((adtsLen & 0x1800) >> 11);           //frame length:value   高2bits

    p_adts_header[4] = (uint8_t) ((adtsLen & 0x7f8) >> 3);     //frame length:value    中间8bits
    p_adts_header[5] = (uint8_t) ((adtsLen & 0x7) << 5);       //frame length:value    低3bits
    p_adts_header[5] |= 0x1f;                                 //buffer fullness:0x7ff 高5bits
    p_adts_header[6] = 0xfc;      //‭11111100‬       //buffer fullness:0x7ff 低6bits

    return 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头的比如ts
                fwrite(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-開発環境の構築

私に従ってください、一緒に進歩してください、人生はコーディング以上のものです!

おすすめ

転載: juejin.im/post/7088723558423232519