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

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

本文介绍一个基于 FFMPEG 的封装格式转换器。所谓的封装格式转换,就是在 AVI,FLV,MKV,MP4 这些格式之间转换(对应. avi,.flv,.mkv,.mp4 文件)。需要注意的是,本程序并不进行视音频的编码和解码工作。而是直接将视音频压缩码流从一种封装格式文件中获取出来然后打包成另外一种封装格式的文件。传统的转码程序工作原理如下图所示:
在这里插入图片描述
上图例举了一个举例:FLV(视频:H.264,音频:AAC)转码为AVI(视频:MPEG2,音频MP3)的例子。可见视频转码的过程通俗地讲相当于把视频和音频重新“录”了一遍。
本程序的工作原理如下图所示:
在这里插入图片描述
由图可见,本程序并不进行视频和音频的编解码工作,因此本程序和普通的转码软件相比,有以下两个特点:
处理速度极快。视音频编解码算法十分复杂,占据了转码的绝大部分时间。因为不需要进行视音频的编码和解码,所以节约了大量的时间。
视音频质量无损。因为不需要进行视音频的编码和解码,所以不会有视音频的压缩损伤。

配置

在这里插入图片描述
在这里插入图片描述

avcodec.lib
avdevice.lib
avfilter.lib
avformat.lib
avutil.lib
postproc.lib
swresample.lib
swscale.lib

代码

大部分代码参考FFmpeg/example下的remuxing.c文件,这里改为了cpp文件。

#include <iostream>
#include <thread>

/**
 * @file
 * libavformat/libavcodec demuxing and muxing API example.
 * libavformat/libavcodec解编和复用API示例
 *
 * Remux streams from one container format to another.
 * 将流从一种容器格式重新传输到另一种容器
 *
 * @example remuxing.c
 */

extern "C" {
    
    
    #include <libavutil/timestamp.h>
    #include <libavformat/avformat.h>
}

char av_ts_string[AV_TS_MAX_STRING_SIZE] = {
    
     0 };
#define av_ts2str(ts) av_ts_make_string(av_ts_string, ts)


char av_ts_buff[AV_TS_MAX_STRING_SIZE] = {
    
     0 };
#define av_ts2timestr(ts, tb) av_ts_make_time_string(av_ts_buff, ts, tb)


char av_error[AV_ERROR_MAX_STRING_SIZE] = {
    
     0 };
#define av_err2str(errnum) av_make_error_string(av_error, AV_ERROR_MAX_STRING_SIZE, errnum)


//----------打印文件信息----------start----------
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 test_stream_info(const char* input_filename)
{
    
    
    const char* filename = input_filename;
    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);
}
//----------打印文件信息----------end----------


//----------MP4转FLV----------start----------

static void log_packet(const AVFormatContext* fmt_ctx, const AVPacket* pkt, const char* tag)
{
    
    
    AVRational* time_base = &fmt_ctx->streams[pkt->stream_index]->time_base;

    printf("%s: pts:%s pts_time:%s dts:%s dts_time:%s duration:%s duration_time:%s stream_index:%d\n",
        tag,
        av_ts2str(pkt->pts), av_ts2timestr(pkt->pts, time_base),
        av_ts2str(pkt->dts), av_ts2timestr(pkt->dts, time_base),
        av_ts2str(pkt->duration), av_ts2timestr(pkt->duration, time_base),
        pkt->stream_index);
}

int test_remuxing() {
    
    
    AVOutputFormat* ofmt = NULL;
    AVFormatContext* ifmt_ctx = NULL, * ofmt_ctx = NULL;
    AVPacket pkt;
    const char* in_filename, * out_filename;
    int ret, i;
    int stream_index = 0;
    int* stream_mapping = NULL;
    int stream_mapping_size = 0;

    //if (argc < 3) {
    
    
    //    printf("usage: %s input output\n"
    //        "API example program to remux a media file with libavformat and libavcodec.\n"
    //        "The output format is guessed according to the file extension.\n"
    //        "\n", argv[0]);
    //    return 1;
    //}

    //in_filename  = argv[1];
    //out_filename = argv[2];
    in_filename = "D:\\javaCode\\androidVideo2022\\368_384.mp4";
    out_filename = "D:\\javaCode\\androidVideo2022\\368_384.flv";

    if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0) {
    
    
        fprintf(stderr, "Could not open input file '%s'", in_filename);
        goto end;
    }

    if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) {
    
    
        fprintf(stderr, "Failed to retrieve input stream information");
        goto end;
    }

    av_dump_format(ifmt_ctx, 0, in_filename, 0);

    avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, out_filename);
    if (!ofmt_ctx) {
    
    
        fprintf(stderr, "Could not create output context\n");
        ret = AVERROR_UNKNOWN;
        goto end;
    }

    stream_mapping_size = ifmt_ctx->nb_streams;
    stream_mapping = (int*)av_mallocz_array(stream_mapping_size, sizeof(*stream_mapping));
    if (!stream_mapping) {
    
    
        ret = AVERROR(ENOMEM);
        goto end;
    }

    ofmt = ofmt_ctx->oformat;

    for (i = 0; i < ifmt_ctx->nb_streams; i++) {
    
    
        AVStream* out_stream;
        AVStream* in_stream = ifmt_ctx->streams[i];
        AVCodecParameters* in_codecpar = in_stream->codecpar;

        if (in_codecpar->codec_type != 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++;

        out_stream = avformat_new_stream(ofmt_ctx, NULL);
        if (!out_stream) {
    
    
            fprintf(stderr, "Failed allocating output stream\n");
            ret = AVERROR_UNKNOWN;
            goto end;
        }

        ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
        if (ret < 0) {
    
    
            fprintf(stderr, "Failed to copy codec parameters\n");
            goto end;
        }
        out_stream->codecpar->codec_tag = 0;
    }
    av_dump_format(ofmt_ctx, 0, out_filename, 1);

    if (!(ofmt->flags & AVFMT_NOFILE)) {
    
    
        ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
        if (ret < 0) {
    
    
            fprintf(stderr, "Could not open output file '%s'", out_filename);
            goto end;
        }
    }

    ret = avformat_write_header(ofmt_ctx, NULL);
    if (ret < 0) {
    
    
        fprintf(stderr, "Error occurred when opening output file\n");
        goto end;
    }

    while (1) {
    
    
        AVStream* in_stream, * out_stream;

        ret = av_read_frame(ifmt_ctx, &pkt);
        if (ret < 0)
            break;

        in_stream = ifmt_ctx->streams[pkt.stream_index];
        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];
        out_stream = ofmt_ctx->streams[pkt.stream_index];
        log_packet(ifmt_ctx, &pkt, "in");

        /* 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;
        log_packet(ofmt_ctx, &pkt, "out");

        ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
        if (ret < 0) {
    
    
            fprintf(stderr, "Error muxing packet\n");
            break;
        }
        av_packet_unref(&pkt);
    }

    av_write_trailer(ofmt_ctx);
end:

    avformat_close_input(&ifmt_ctx);

    /* close output */
    if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
        avio_closep(&ofmt_ctx->pb);
    avformat_free_context(ofmt_ctx);

    av_freep(&stream_mapping);

    if (ret < 0 && ret != AVERROR_EOF) {
    
    
        fprintf(stderr, "Error occurred: %s\n", av_err2str(ret));
        return 1;
    }

    return 0;
}
//----------MP4转FLV----------end----------

//最简单的基于FFMPEG的封装格式转换器
int main(int argc, char** argv)
{
    
    
    //一 ,打印MP4文件信息
    const char* in_filename, * out_filename;
    in_filename  = "D:\\javaCode\\androidVideo2022\\368_384.mp4";
    out_filename = "D:\\javaCode\\androidVideo2022\\368_384.flv";
    //test_stream_info(in_filename);
    
    //二 ,把MP4解封装转为FLV
    //test_remuxing();

    //三 ,打印FLV文件信息
    test_stream_info(out_filename);
    return 0;


}


结果

在这里插入图片描述

关键函数说明

avformat_open_input

/**
 Open an input stream and read the header. The codecs are not opened.
 The stream must be closed with avformat_close_input().
 打开输入流并读取标头。编解码器未打开。流必须用avformat_close_input关闭
 */
int avformat_open_input(AVFormatContext **ps, const char *url, ff_const59 AVInputFormat *fmt, AVDictionary **options);

avformat_find_stream_info

/**
 Read packets of a media file to get stream information. This
 is useful for file formats with no headers such as MPEG. This
 function also computes the real framerate in case of MPEG-2 repeat
 frame mode.
 读取媒体文件的数据包以获取流信息。
 这对于没有标头的文件格式(如MPEG)很有用。
 此函数还计算MPEG-2重复帧模式下的实际帧速率

 The logical file position is not changed by this function;
 examined packets may be buffered for later processing.
 此功能不会更改逻辑文件位置;
 被检查的数据包可以被缓冲以供以后处理

 */
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

av_dump_format

/**
  Print detailed information about the input or output format, such as
  duration, bitrate, streams, container, programs, metadata, side data,
  codec and time base.
 打印有关输入或输出格式的详细信息,例如持续时间、比特率、流、容器、程序、元数据、副数据、编解码器和时基

  @param ic        the context to analyze
  @param index     index of the stream to dump information about
  @param url       the URL to print, such as source or destination file
  @param is_output Select whether the specified context is an input(0) or output(1)
 */
void av_dump_format(AVFormatContext *ic, int index,
                    const char *url, int is_output);

avformat_alloc_output_context2

/**
  Allocate an AVFormatContext for an output format.
  为输出格式分配AVFormatContext
  
  avformat_free_context() can be used to free the context and
  everything allocated by the framework within it.
  
  avformat_free_context()可用于释放上下文和框架在其中分配的所有内容
 
  @param *ctx is set to the created format context, or to NULL in
  case of failure
  @param oformat format to use for allocating the context, if NULL
  format_name and filename are used instead
  @param format_name the name of output format to use for allocating the
  context, if NULL filename is used instead
  @param filename the name of the filename to use for allocating the
  context, may be NULL
  @return >= 0 in case of success, a negative AVERROR code in case of
  failure
 */
int avformat_alloc_output_context2(AVFormatContext **ctx, ff_const59 AVOutputFormat *oformat,
                                   const char *format_name, const char *filename);

av_mallocz_array

//使用av_mallocz为阵列分配内存块
av_alloc_size(1, 2) void *av_mallocz_array(size_t nmemb, size_t size);

函数av_mallocz_arrayav_malloc_array 唯一区别在于内部分配内存函数调用 av_mallocz 实现,也就是分配的内存块会清零。

此函数比 av_malloc 多一个动作,会将分配块的所有字节置零。这也是命名多加一个“z”的含义,zero 的意思。

看源码确实也是在调用 av_malloc 后,又接了 memset 置 0 操作。
libavutil/mem.c

void *av_mallocz(size_t size)
{
    
    
    void *ptr = av_malloc(size);
    if (ptr)
        memset(ptr, 0, size);
    return ptr;
}

avformat_new_stream 创建新的流

/**
  Add a new stream to a media file.
 将新流添加到媒体文件
 
  When demuxing, it is called by the demuxer in read_header(). If the
  flag AVFMTCTX_NOHEADER is set in s.ctx_flags, then it may also
  be called in read_packet().
 当demuxing时,它由read_header()中的demuxer调用。如果标志AVFMTCTX_NOHEADER在s.ctx_flags中设置,那么它也可以在read_packet中调用
 
  When muxing, should be called by the user before avformat_write_header().
 当muxing时,应在avformat_write_header之前由用户调用
 
  User is required to call avcodec_close() and avformat_free_context() to
  clean up the allocation by avformat_new_stream().
 用户需要调用avcodec_close()和avformat_free_context()来清理avformat_new_stream的分配
 */
AVStream *avformat_new_stream(AVFormatContext *s, const AVCodec *c);

avcodec_parameters_copy

/**
  Copy the contents of src to dst. Any allocated fields in dst are freed and
  replaced with newly allocated duplicates of the corresponding fields in src.
 将src的内容复制到dst。dst中的任何分配字段都将被释放,并替换为src中相应字段的新分配副本

  @return >= 0 on success, a negative AVERROR code on failure.
 */
int avcodec_parameters_copy(AVCodecParameters *dst, const AVCodecParameters *src);

avio_open

/**
  Create and initialize a AVIOContext for accessing the
  resource indicated by url.
  创建并初始化AVIOContext,用于访问url指示的资源
 */
int avio_open(AVIOContext **s, const char *url, int flags);

avformat_write_header

/**
  Allocate the stream private data and write the stream header to
  an output media file.
 分配流私有数据并将流头写入输出媒体文件
 */
int avformat_write_header(AVFormatContext *s, AVDictionary **options);

av_read_frame

/**
  Return the next frame of a stream.
  返回流的下一帧

  This function returns what is stored in the file, and does not validate
  that what is there are valid frames for the decoder. 
  It will split what is stored in the file into frames and return one for each call. 
  It will not omit invalid data between valid frames so as to give the decoder the maximum
  information possible for decoding.
 此函数返回文件中存储的内容,但不验证解码器是否有有效的帧。
 它会将存储在文件中的内容拆分为帧,并为每个调用返回一个帧。
 它不会省略有效帧之间的无效数据,以便给解码器提供最大可能的解码信息
 
  On success, the returned packet is reference-counted (pkt->buf is set) and
  valid indefinitely. The packet must be freed with av_packet_unref() when
  it is no longer needed. For video, the packet contains exactly one frame.
  For audio, it contains an integer number of frames if each frame has
  a known fixed size (e.g. PCM or ADPCM data). If the audio frames have
  a variable size (e.g. MPEG audio), then it contains one frame.
 
  pkt->pts, pkt->dts and pkt->duration are always set to correct
  values in AVStream.
  time_base units (and guessed if the format cannot
  provide them). pkt->pts can be AV_NOPTS_VALUE if the video format
  has B-frames, so it is better to rely on pkt->dts if you do not
  decompress the payload.
 
  @return 0 if OK, < 0 on error or end of file. On error, pkt will be blank
          (as if it came from av_packet_alloc()).
 
  @note pkt will be initialized, so it may be uninitialized, but it must not
        contain data that needs to be freed.
 */
int av_read_frame(AVFormatContext *s, AVPacket *pkt);

av_packet_unref

/**
  Wipe the packet.
 擦除数据包

  Unreference the buffer referenced by the packet and reset the
  remaining packet fields to their default values.
 取消对数据包引用的缓冲区的引用,并将其余数据包字段重置为其默认值
 
  @param pkt The packet to be unreferenced.
 */
void av_packet_unref(AVPacket *pkt);

av_rescale_q_rnd

/**
 * Rescale a 64-bit integer by 2 rational numbers with specified rounding.
 * 使用指定的舍入将64位整数重缩放2个有理数
 *
 * The operation is mathematically equivalent to `a * bq / cq`.
 *
 * @see av_rescale(), av_rescale_rnd(), av_rescale_q()
 */
int64_t av_rescale_q_rnd(int64_t a, AVRational bq, AVRational cq,
                         enum AVRounding rnd) av_const;

av_rescale_q

/**
 Rescale a 64-bit integer by 2 rational numbers.
 将64位整数重缩放2个有理数

 The operation is mathematically equivalent to `a * bq / cq`.
 
 This function is equivalent to av_rescale_q_rnd() with #AV_ROUND_NEAR_INF.
 
 @see av_rescale(), av_rescale_rnd(), av_rescale_q_rnd()
 */
int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq) av_const;

av_interleaved_write_frame

//将数据包写入输出媒体文件以确保正确的交织
int av_interleaved_write_frame(AVFormatContext *s, AVPacket *pkt);

av_write_trailer

//将流片尾写入输出媒体文件并释放文件私有数据
int av_write_trailer(AVFormatContext *s);

参考资料

最简单的基于FFMPEG的封装格式转换器-无编解码(雷神)
最简单的基于FFMPEG的封装格式转换器-新
FFmpeg 源码之内存管理函数族

猜你喜欢

转载自blog.csdn.net/e891377/article/details/127366136