一些有关ffmpeg 使用(1)

1 解封装流程

1.1 什么解封装

封装的逆向操作:封装是把音频流、视频流、字幕流等不同成分按一定规则组合成视频文件(如 MP4、FLV ) ,复用器负责此过程。解封装则相反,是用解复用器(针对 MP4、FLV 等格式有对应解复用器)将视频文件再按规则拆分回音频流、视频流、字幕流等各个成分 。

流索引标识:解封装后,为区分各流,会给音频流、视频流等分配索引,如图中 audio_index = 1 表示音频流索引为 1 ,video_index = 0 表示视频流索引为 0 。程序后续可依这些索引分别处理对应流,比如解码视频流、播放音频流等 。

AVMEDIA_TYPE_VIDEO视频流

video_index = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,

-1,-1, NULL, 0)
AVMEDIA_TYPE_AUDIO音频流

audio_index = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO,

-1,-1, NULL, 0)

1.2 实验


第一部分: :ffmpeg -i in_filename # 打开流

cpp 复制代码
const char *default_filename = "believe.mp4";

    char *in_filename = NULL;

    if(argv[1] == NULL)
    {
        in_filename = default_filename;
    }
    else
    {
        in_filename = argv[1];
    }
    
AVFormatContext *ifmt_ctx = NULL//初始化上下文

 int ret = avformat_open_input(&ifmt_ctx, in_filename, NULL, NULL);//头部信息
    if (ret < 0)  //如果打开媒体文件失败,打印失败原因
    {
        char buf[1024] = { 0 };
        av_strerror(ret, buf, sizeof(buf) - 1);
        printf("open %s failed:%s\n", in_filename, buf);
        goto failed;
    }

    ret = avformat_find_stream_info(ifmt_ctx, NULL);//   流的信息
     if (ret < 0)  //如果打开媒体文件失败,打印失败原因
    {
        char buf[1024] = { 0 };
        av_strerror(ret, buf, sizeof(buf) - 1);
        printf("avformat_find_stream_info %s failed:%s\n", in_filename, buf);
        goto failed;
    }
av_dump_format(ifmt_ctx, 0, in_filename, 0);

printf_s("\n==== av_dump_format in_filename:%s ===\n", in_filename);
    av_dump_format(ifmt_ctx, 0, in_filename, 0);
    printf_s("\n==== av_dump_format finish =======\n\n");
    // url: 调用avformat_open_input读取到的媒体文件的路径/名字
    printf("media name:%s\n", ifmt_ctx->url);
    // nb_streams: nb_streams媒体流数量
    printf("stream number:%d\n", ifmt_ctx->nb_streams);
    // bit_rate: 媒体文件的码率,单位为bps
    printf("media average ratio:%lldkbps\n",(int64_t)(ifmt_ctx->bit_rate/1024));
    // 时间
    printf("total duration: %02d:%02d:%02d\n", hour, minute, second);
    printf("\n");

第二部分:分析流的信息 : 也是ffmpeg -i in_filename# 打开流

cpp 复制代码
  ret = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
       if (ret >= 0) {
           audioindex = ret;
           AVStream *in_stream = ifmt_ctx->streams[audioindex];
           printf("----- Audio info:\n");
           // index: 每个流成分在ffmpeg解复用分析后都有唯一的index作为标识
           printf("index:%d\n", in_stream->index);
           // sample_rate: 音频编解码器的采样率,单位为Hz
           printf("samplerate:%dHz\n", in_stream->codecpar->sample_rate);
           // codecpar->format: 音频采样格式
           if (AV_SAMPLE_FMT_FLTP == in_stream->codecpar->format) {
               printf("sampleformat:AV_SAMPLE_FMT_FLTP\n");
           } else if (AV_SAMPLE_FMT_S16P == in_stream->codecpar->format) {
               printf("sampleformat:AV_SAMPLE_FMT_S16P\n");
           }
           // channels: 音频信道数目
           printf("channel number:%d\n", in_stream->codecpar->channels);
           // codec_id: 音频压缩编码格式
           if (AV_CODEC_ID_AAC == in_stream->codecpar->codec_id) {
               printf("audio codec:AAC\n");
           } else if (AV_CODEC_ID_MP3 == in_stream->codecpar->codec_id) {
               printf("audio codec:MP3\n");
           } else {
               printf("audio codec_id:%d\n", in_stream->codecpar->codec_id);
           }
           // 音频总时长,单位为秒。注意如果把单位放大为毫秒或者微妙,音频总时长跟视频总时长不一定相等的
           if (in_stream->duration != AV_NOPTS_VALUE) {
               int duration_audio = (in_stream->duration) * av_q2d(in_stream->time_base);
               //将音频总时长转换为时分秒的格式打印到控制台上
               printf("audio duration: %02d:%02d:%02d\n",
                      duration_audio / 3600, (duration_audio % 3600) / 60, (duration_audio % 60));
           } else {
               printf("audio duration unknown");
           }
}

----- Audio info:

index:1

samplerate:48000Hz

sampleformat:AV_SAMPLE_FMT_FLTP

channel number:2

audio codec:AAC

audio duration: 00:03:42

分别对应 音频流索引 采样率 采样精度 声道 编码器 时间

cpp 复制代码
ret = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
           if (ret >= 0) {
               videoindex = ret;
               AVStream *in_stream = ifmt_ctx->streams[videoindex];
               printf("----- Video info:\n");
               printf("index:%d\n", in_stream->index);
               // avg_frame_rate: 视频帧率,单位为fps,表示每秒出现多少帧
               printf("fps:%lffps\n", av_q2d(in_stream->avg_frame_rate));
               if (AV_CODEC_ID_MPEG4 == in_stream->codecpar->codec_id) //视频压缩编码格式
               {
                   printf("video codec:MPEG4\n");
               } else if (AV_CODEC_ID_H264 == in_stream->codecpar->codec_id) //视频压缩编码格式
               {
                   printf("video codec:H264\n");
               } else {
                   printf("video codec_id:%d\n", in_stream->codecpar->codec_id);
               }
               // 视频帧宽度和帧高度
               printf("width:%d height:%d\n", in_stream->codecpar->width,
                      in_stream->codecpar->height);
               //视频总时长,单位为秒。注意如果把单位放大为毫秒或者微妙,音频总时长跟视频总时长不一定相等的
               if (in_stream->duration != AV_NOPTS_VALUE) {
                   int duration_video = (in_stream->duration) * av_q2d(in_stream->time_base);
                   printf("video duration: %02d:%02d:%02d\n",
                          duration_video / 3600,
                          (duration_video % 3600) / 60,
                          (duration_video % 60)); //将视频总时长转换为时分秒的格式打印到控制台上
               } else {
                   printf("video duration unknown");
               }

               printf("\n");
           }

----- Video info:

index:0

fps:14.464607fps

video codec:H264

width:1920 height:1080

video duration: 00:03:42

视频流索引 帧率 编码器 分辨率 时长


第三部分:ffmpeg -i input.mp4 -vn -c:a aac output.aac

cpp 复制代码
 printf("audio profile:%d, FF_PROFILE_AAC_LOW:%d\n",
           ifmt_ctx->streams[audio_index]->codecpar->profile,
           FF_PROFILE_AAC_LOW);

    if(ifmt_ctx->streams[audio_index]->codecpar->codec_id != AV_CODEC_ID_AAC)
    {
        printf("the media file no contain AAC stream, it's codec_id is %d\n",
               ifmt_ctx->streams[audio_index]->codecpar->codec_id);
        goto failed;
    }
    // 读取媒体文件,并把aac数据帧写入到本地文件
    while(av_read_frame(ifmt_ctx, &pkt) >=0 )
    {
        if(pkt.stream_index == audio_index)
        {
            char adts_header_buf[7] = {0};
            adts_header(adts_header_buf, pkt.size,
                        ifmt_ctx->streams[audio_index]->codecpar->profile,
                        ifmt_ctx->streams[audio_index]->codecpar->sample_rate,
                        ifmt_ctx->streams[audio_index]->codecpar->channels);
            fwrite(adts_header_buf, 1, 7, aac_fd);  // 写adts header , ts流不适用,ts流分离出来的packet带了adts header
            len = fwrite( pkt.data, 1, pkt.size, aac_fd);   // 写adts data
            if(len != pkt.size)
            {
                av_log(NULL, AV_LOG_DEBUG, "warning, length of writed data isn't equal pkt.size(%d, %d)\n",
                       len,
                       pkt.size);
            }
        }
        av_packet_unref(&pkt);
    }

写入aac的head和数据包


第四部分 :提取视频流 ffmpeg -i nput.mp4 -an -c:v libx264 output.h264

cpp 复制代码
    pkt = av_packet_alloc();
    av_init_packet(pkt);
    const AVBitStreamFilter *bsfilter = av_bsf_get_by_name("h264_mp4toannexb");
    AVBSFContext *bsf_ctx = NULL;
    // 2 初始化过滤器上下文
    av_bsf_alloc(bsfilter, &bsf_ctx); //AVBSFContext;
    // 3 添加解码器属性
    avcodec_parameters_copy(bsf_ctx->par_in, ifmt_ctx->streams[videoindex]->codecpar);
    av_bsf_init(bsf_ctx);

    file_end = 0;
    while (0 == file_end)
    {
        if((ret = av_read_frame(ifmt_ctx, pkt)) < 0)
        {
            // 没有更多包可读
            file_end = 1;
            printf("read file end: ret:%d\n", ret);
        }
        if(ret == 0 && pkt->stream_index == videoindex)
        {
#if 1
            int input_size = pkt->size;
            int out_pkt_count = 0;
            if (av_bsf_send_packet(bsf_ctx, pkt) != 0) // bitstreamfilter内部去维护内存空间
            {
                av_packet_unref(pkt);   // 你不用了就把资源释放掉
                continue;       // 继续送
            }
            av_packet_unref(pkt);   // 释放资源
            while(av_bsf_receive_packet(bsf_ctx, pkt) == 0)
            {
                out_pkt_count++;
                // printf("fwrite size:%d\n", pkt->size);
                size_t size = fwrite(pkt->data, 1, pkt->size, outfp);
                if(size != pkt->size)
                {
                    printf("fwrite failed-> write:%u, pkt_size:%u\n", size, pkt->size);
                }
                av_packet_unref(pkt);
            }
            if(out_pkt_count >= 2)
            {
                printf("cur pkt(size:%d) only get 1 out pkt, it get %d pkts\n",
                       input_size, out_pkt_count);
            }

#else       // TS流可以直接写入
            size_t size = fwrite(pkt->data, 1, pkt->size, outfp);
            if(size != pkt->size)
            {
                printf("fwrite failed-> write:%u, pkt_size:%u\n", size, pkt->size);
            }
            av_packet_unref(pkt);
#endif

使用了一个过滤器 为 AVBitStreamFilter 要为h264一个annexb模式服务


2 解码

上面我们获取了 两个流 分别为aac的音频流和h264的视频流,现在我们要对这两个流解码

2.1 函数

cpp 复制代码
const AVCodec *codec;
AVCodecContext *codec_ctx= NULL;
 AVCodecParserContext *parser = NULL
 // 查找解码器
    codec = avcodec_find_decoder(audio_codec_id);  // AV_CODEC_ID_AAC
    if (!codec) {
        fprintf(stderr, "Codec not found\n");
        exit(1);
    }
    // 获取裸流的解析器 AVCodecParserContext(数据)  +  AVCodecParser(方法)
    parser = av_parser_init(codec->id);
    if (!parser) {
        fprintf(stderr, "Parser not found\n");
        exit(1);
    }
    // 分配codec上下文
    codec_ctx = avcodec_alloc_context3(codec);
    if (!codec_ctx) {
        fprintf(stderr, "Could not allocate audio codec context\n");
        exit(1);
    }
  • codec:编解码器
  • AVCodecContext :编解码器上下文

基本概念

编解码器上下文(Codec Context)本质上是一个数据结构,它用来存储编解码器在工作过程中所需要的各种参数、状态信息以及中间数据。简单来说,它就是编解码器的 "工作环境",编解码器依据上下文中的这些信息来完成编码或者解码的操作。
作用

参数配置:编解码器上下文可以对编解码器的各种参数进行配置。以视频编解码器为例,这些参数可能包括视频的分辨率、帧率、比特率、像素格式等;对于音频编解码器,可能涉及采样率、声道数、采样格式等。通过对这些参数的合理配置,能够让编解码器根据具体需求进行工作。

状态管理:它会记录编解码器在运行过程中的状态信息。例如,在解码过程中,可能会记录当前解码到的帧号、是否遇到错误等信息;在编码过程中,可能会记录编码进度、已经编码的帧数等。

数据传递:编解码器上下文还承担着在不同模块之间传递数据的任务。比如,在解码时,输入的编码数据会通过上下文传递给解码器;解码完成后,解码后的原始数据也会通过上下文传递给后续的处理模块。

  • AVCodecParserContext:⽤于解析输⼊的数据流并把它分成⼀帧⼀帧的压缩编码数据。⽐较形象的说法就是把⻓⻓的⼀段连续的数据"切割"成⼀段段的数据。

  • 解码

    • avcodec_send_packet():
      函数 int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
      作用:⽀持将裸流数据包送给解码器
    • avcodec_receive_frame
      函数 :int avcodec_receive_frame ( AVCodecContext * avctx, AVFrame * frame )
      作⽤ :从解码器返回已解码的输出数据。
      返回值 :AVERROR(EAGAIN): 该状态下没有帧输出,需要使⽤avcodec_send_packet发送新的packet到解码器
      AVERROR_EOF: 解码器已经被完全刷新,不再有输出帧
cpp 复制代码
static void decode(AVCodecContext *dec_ctx, AVPacket *pkt, AVFrame *frame,
                   FILE *outfile)
{
    int i, ch;
    int ret, data_size;
    /* send the packet with the compressed data to the decoder */
    ret = avcodec_send_packet(dec_ctx, pkt);
    if(ret == AVERROR(EAGAIN))
    {
        fprintf(stderr, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
    }
    else if (ret < 0)
    {
        fprintf(stderr, "Error submitting the packet to the decoder, err:%s, pkt_size:%d\n",
                av_get_err(ret), pkt->size);
//        exit(1);
        return;
    }

    /* read all the output frames (infile general there may be any number of them */
    while (ret >= 0)
    {
        // 对于frame, avcodec_receive_frame内部每次都先调用
        ret = avcodec_receive_frame(dec_ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            return;
        else if (ret < 0)
        {
            fprintf(stderr, "Error during decoding\n");
            exit(1);
        }
        data_size = av_get_bytes_per_sample(dec_ctx->sample_fmt);
        if (data_size < 0)
        {
            /* This should not occur, checking just for paranoia */
            fprintf(stderr, "Failed to calculate data size\n");
            exit(1);
        }
        static int s_print_format = 0;
        if(s_print_format == 0)
        {
            s_print_format = 1;
            print_sample_format(frame);
        }
        /**
            P表示Planar(平面),其数据格式排列方式为 :
            LLLLLLRRRRRRLLLLLLRRRRRRLLLLLLRRRRRRL...(每个LLLLLLRRRRRR为一个音频帧)
            而不带P的数据格式(即交错排列)排列方式为:
            LRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRL...(每个LR为一个音频样本)
         播放范例:   ffplay -ar 48000 -ac 2 -f f32le believe.pcm
          */
        for (i = 0; i < frame->nb_samples; i++)
        {
            for (ch = 0; ch < dec_ctx->channels; ch++)  // 交错的方式写入, 大部分float的格式输出
                fwrite(frame->data[ch] + data_size*i, 1, data_size, outfile);
        }
    }

是解一个包中的帧的样本点

cpp 复制代码
static void decode(AVCodecContext *dec_ctx, AVPacket *pkt, AVFrame *frame,
                   FILE *outfile)
{
    int ret;
    /* send the packet with the compressed data to the decoder */
    ret = avcodec_send_packet(dec_ctx, pkt);//发包
    if(ret == AVERROR(EAGAIN))//错误处理
    {
        fprintf(stderr, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
    }
    else if (ret < 0)
    {
        fprintf(stderr, "Error submitting the packet to the decoder, err:%s, pkt_size:%d\n",
                av_get_err(ret), pkt->size);
        return;
    }

    /* read all the output frames (infile general there may be any number of them */
    while (ret >= 0)
    {
        // 对于frame, avcodec_receive_frame内部每次都先调用
        ret = avcodec_receive_frame(dec_ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            return;
        else if (ret < 0)
        {
            fprintf(stderr, "Error during decoding\n");
            exit(1);
        }
        static int s_print_format = 0;
        if(s_print_format == 0)
        {
            s_print_format = 1;
            print_video_format(frame);
        }

        // 一般H264默认为 AV_PIX_FMT_YUV420P, 具体怎么强制转为 AV_PIX_FMT_YUV420P 在音视频合成输出的时候讲解
        // frame->linesize[1]  对齐的问题
        // 正确写法  linesize[]代表每行的字节数量,所以每行的偏移是linesize[]
        for(int j=0; j<frame->height; j++)
            fwrite(frame->data[0] + j * frame->linesize[0], 1, frame->width, outfile);
        for(int j=0; j<frame->height/2; j++)
            fwrite(frame->data[1] + j * frame->linesize[1], 1, frame->width/2, outfile);
        for(int j=0; j<frame->height/2; j++)
            fwrite(frame->data[2] + j * frame->linesize[2], 1, frame->width/2, outfile);

        // 错误写法 用source.200kbps.766x322_10s.h264测试时可以看出该种方法是错误的
        //  写入y分量
//        fwrite(frame->data[0], 1, frame->width * frame->height,  outfile);//Y
//        // 写入u分量
//        fwrite(frame->data[1], 1, (frame->width) *(frame->height)/4,outfile);//U:宽高均是Y的一半
//        //  写入v分量
//        fwrite(frame->data[2], 1, (frame->width) *(frame->height)/4,outfile);//V:宽高均是Y的一半
    }
}

是解一个包中的 帧中的yuv

3 自定义io

cpp 复制代码
AVIOContext *avio_alloc_context(
    unsigned char *buffer,
    int buffer_size,
    int write_flag,
    void *opaque,
    int (*read_packet)(void *opaque, uint8_t *buf, int buf_size),
    int (*write_packet)(void *opaque, uint8_t *buf, int buf_size),
    int64_t (*seek)(void *opaque, int64_t offset, int whence)
);
  • unsigned char *buffer

    指向预先分配好的缓冲区的指针,该缓冲区用于存储从数据源读取的数据(读取模式)或要写入数据源的数据(写入模式)。在调用此函数之前,你需要使用 av_malloc 等内存分配函数为其分配足够的内存空间。

  • int buffer_size

    缓冲区的大小,以字节为单位。该值指定了 buffer 所指向的内存区域的大小。

  • int write_flag

    一个布尔值,用于指示 AVIOContext 的操作模式。

    • 0 表示只读模式,即 AVIOContext 仅用于从数据源读取数据。
    • 非零值(通常为 1)表示可写模式,即 AVIOContext 用于向数据源写入数据。
  • void *opaque

    一个通用指针,可传递任意用户数据,通常是与数据源相关的上下文信息,如文件指针、网络套接字等。这个指针会被传递给后续的 read_packet、write_packet 和 seek 回调函数,以便在这些函数中使用。

  • int (*read_packet)(void *opaque, uint8_t *buf, int buf_size)

    一个指向读取回调函数的指针,用于从数据源读取数据。当 AVIOContext 需要从数据源读取数据时,会调用这个函数。

    函数参数:

    void *opaque:即前面传入的 opaque 指针,可用于获取数据源的上下文信息。

    uint8_t *buf:指向用于存储读取数据的缓冲区。

    int buf_size:缓冲区的大小,即最多可读取的字节数。

    函数返回值:成功读取的字节数,如果返回值小于 0,则表示发生了错误。

  • int (*write_packet)(void *opaque, uint8_t *buf, int buf_size)

    一个指向写入回调函数的指针,用于向数据源写入数据。当 AVIOContext 需要向数据源写入数据时,会调用这个函数。

    函数参数与 read_packet 类似,只是 buf 中的数据是要写入数据源的数据。

    函数返回值:成功写入的字节数,如果返回值小于 0,则表示发生了错误。

  • int64_t (*seek)(void *opaque, int64_t offset, int whence)

    一个指向定位回调函数的指针,用于在数据源中移动读写位置。当 AVIOContext 需要在数据源中定位到特定位置时,会调用这个函数。

    函数参数:

    void *opaque:同样是前面传入的 opaque 指针。

    int64_t offset:相对于 whence 指定位置的偏移量。

    int whence:定位的起始位置,取值与标准 C 库中的 fseek 函数的 whence 参数类似,常见取值有 SEEK_SET(文件开头)、SEEK_CUR(当前位置)、SEEK_END(文件末尾)。

    函数返回值:新的读写位置,如果返回值小于 0,则表示发生了错误。

cpp 复制代码
 uint8_t *io_buffer = av_malloc(BUF_SIZE);
    AVIOContext *avio_ctx = avio_alloc_context(io_buffer, BUF_SIZE, 0, (void *)in_file,    \
                                               read_packet, NULL, NULL);
    AVFormatContext *format_ctx = avformat_alloc_context();
    format_ctx->pb = avio_ctx;

当调用这个的时候

复制代码
ret = av_read_frame(format_ctx, packet); //使用自定义io
相关推荐
RenderNow10 小时前
深耕ffmpeg系列之AVFrame
ffmpeg
xiaoh_715 小时前
解决视频处理中的 HEVC 解码错误:Could not find ref with POC xxx【已解决】
python·ffmpeg·音视频
王江奎16 小时前
Android FFmpeg 交叉编译全指南:NDK编译 + CMake 集成
android·ffmpeg
qq_316837751 天前
使用ffmpeg 将图片合成为视频,填充模糊背景,并添加两段音乐
ffmpeg·音视频
林鸿群2 天前
Mediamtx与FFmpeg远程与本地推拉流使用
ffmpeg
unix2linux3 天前
YOLO v5 Series - FFmpeg & (HTML5 + FLV.js ) & ONNX YOLOv5s Integrating
yolo·ffmpeg·html5
Antonio9153 天前
【音视频】FFmpeg解封装
ffmpeg·音视频
Antonio9154 天前
【音视频】FFmpeg内存模型
ffmpeg·音视频
hjjdebug4 天前
全面介绍AVFilter 的添加和使用
ffmpeg·avfilter