YUV+PCM->H264+AAC-> MUX -> FLV 非0基础版本

流程图

在这里插入图片描述
ffmpeg 的 Mux 主要分为 三步操作:

  • avformat_write_header : 写⽂件头
  • av_write_frame/av_interleaved_write_frame: 写packet
  • av_write_trailer : 写⽂件尾
  • avcodec_parameters_from_context:将AVCodecContext结构体中码流参数拷⻉到AVCodecParameters结构体中
  • 和avcodec_parameters_to_context 将AVCodecParameters结构体中码流参数拷⻉到AVCodecContext结构体中

函数结构体梳理

avformat_alloc_output_context2

函数参数的介绍:

  • ctx:需要创建的context,返回NULL表示失败。

  • oformat:指定对应的AVOutputFormat,如果不指定,可以通过后⾯

  • format_name、filename两个参 数进⾏指定,让ffmpeg⾃⼰推断。

  • format_name: 指定⾳视频的格式,⽐如“flv”,“mpeg”等,如果设置为NULL,则由filename进⾏指 定,让ffmpeg⾃⼰推断。

  • filename: 指定⾳视频⽂件的路径,如果oformat、format_name为NULL,则ffmpeg内部根据 filename后缀名选择合适的复⽤器,⽐如xxx.flv则使⽤flv复⽤器。

AVOutputFormat

AVOutpufFormat表示输出⽂件容器格式,AVOutputFormat 结构主要包含的信息有:封装名称描述,编 码格式信息(video/audio 默认编码格式,⽀持的编码格式列表),⼀些对封装的操作函数。
ffmpeg⽀持各种各样的输出⽂件格式,MP4,FLV,3GP等等。⽽ AVOutputFormat 结构体则保存了这 些格式的信息和⼀些常规设置。 每⼀种封装对应⼀个 AVOutputFormat 结构,ffmpeg将AVOutputFormat 按照链表存储:

在这里插入图片描述

avformat_new_stream AVStream 即是流通道。

例如我们将 H264 和 AAC 码流存储为MP4⽂件的时候,就需要在 MP4⽂件中 增加两个流通道,⼀个存储Video:H264,⼀个存储Audio:AAC(假设H264和AAC只包含单个流通道).

av_interleaved_write_frame

将数据包写⼊输出媒体⽂件,并确保正确的交织(保持packet dts的增⻓性)。 该函数会在内部根据需要缓存packet,以确保输出⽂件中的packet按dts递增的顺序正确交织。

如果⾃⼰ 进⾏交织则应调⽤av_write_frame()

av_compare_ts

比较输入前后的

其他
  • const char *name; // 复⽤器名称
  • const char *long_name;//格式的描述性名称,易于阅读。
  • enum AVCodecID audio_codec; //默认的⾳频编解码器
  • enum AVCodecID video_codec; //默认的视频编解码器
  • enum AVCodecID subtitle_codec; //默认的字幕编解码器

工程梳理

基本初始化 have 代表有几路流 其他很熟悉了

首先封装一个流结构体

有常用基本参数

// 封装单个输出AVStream
typedef struct OutputStream
{
    AVStream *st;// 代表一个stream, 1路audio或1路video都代表独立的steam
    AVCodecContext *enc; //编码器上下文

    int64_t next_pts;   //生成下一帧的pts player
    int samples_count; //A——采样累计

    AVFrame *frame;     // 重采样后的frame,  视频叫scale
    AVFrame *tmp_frame; // 重采样前

    float t, tincr, tincr2; // 这几个参数用来生成PCM和YUV用的

    struct SwsContext *sws_ctx;     // 图像scale
    struct SwrContext *swr_ctx;     // 音频重采样


}OutputStream;
常规初始化
OutputStream video_st = { 0 }; // 封装视频编码相关的
    OutputStream audio_st = { 0 }; // 封装音频编码相关的
    const char *filename;   // 输出文件

    AVOutputFormat *fmt;    // 输出文件容器格式, 封装了复用规则,AVInputFormat则是封装了解复用规则
    AVFormatContext *oc;
    AVCodec *audio_codec, *video_codec;

    int ret;
    int have_video = 0, have_audio = 0;
    int encode_video = 0, encode_audio = 0;
    AVDictionary *opt = NULL;
    
    int i;

前面提过了通过名字获取outputformat
指定编码器

 avformat_alloc_output_context2(&oc, NULL, NULL, filename);
    if (!oc)
    {
        // 如果不能根据文件后缀名找到合适的格式,那缺省使用flv格式
        printf("Could not deduce output format from file extension: using flv.\n");
        avformat_alloc_output_context2(&oc, NULL, "flv", filename);

    }
     fmt = oc->oformat; // 获取绑定的AVOutputFormat
    fmt->video_codec = AV_CODEC_ID_H264;    // 指定编码器
    fmt->audio_codec = AV_CODEC_ID_AAC;     // 指定编码器

初始化流和对应的编码器
if (fmt->video_codec != AV_CODEC_ID_NONE)
    {
        add_stream(&video_st, oc, &video_codec, fmt->video_codec);
        have_video = 1;
        encode_video = 1;
    }
    if (fmt->audio_codec != AV_CODEC_ID_NONE)
    {
        add_stream(&audio_st, oc, &audio_codec, fmt->audio_codec);
        have_audio = 1;
        encode_audio = 1;
    }
add stream

增加输出流,返回AVStream,并给codec赋值,但此时codec并未打开
就是 根据前面的输入的参数 设定编码器 初始化流 在和ctx绑定
音频 视频都是这样

先定义一个 AVCodecContext *codec_ctx;
新建码流 绑定到 AVFormatContext stream
ost->st = avformat_new_stream(oc, NULL);
oc 是outputformatcontext
然后
codec_ctx = avcodec_alloc_context3(*codec); // 创建编码器上下文
最后复制给 
ost->enc = codec_ctx
ost 就是输出流的封装格式
之后绑定基本参数
//增加输出流,返回AVStream,并给codec赋值,但此时codec并未打开
static void add_stream(OutputStream *ost, AVFormatContext *oc,
                       AVCodec **codec,
                       enum AVCodecID codec_id)
{
    AVCodecContext *codec_ctx;
    int i;

    /* 查找编码器 */
    *codec = avcodec_find_encoder(codec_id);    //通过codec_id找到编码器
    if (!(*codec))
    {
        fprintf(stderr, "Could not find encoder for '%s'\n",
                avcodec_get_name(codec_id));
        exit(1);
    }
    // 新建码流 绑定到 AVFormatContext stream->index 有设置
    ost->st = avformat_new_stream(oc, NULL);    // 创建一个流成分
    if (!ost->st)
    {
        fprintf(stderr, "Could not allocate stream\n");
        exit(1);
    }
    /* 为什么要 -1呢?每次调用avformat_new_stream的时候nb_streams+1
       但id是从0开始, 比如第1个流:对应流id = nb_streams(1) -1 = 0
                        第2个流:对应流id = nb_streams(2) -1 = 1
    */
    ost->st->id = oc->nb_streams - 1;
    codec_ctx = avcodec_alloc_context3(*codec); // 创建编码器上下文
    if (!codec_ctx)
    {
        fprintf(stderr, "Could not alloc an encoding context\n");
        exit(1);
    }
    ost->enc = codec_ctx;
    // 初始化编码器参数
    switch ((*codec)->type)
    {
        case AVMEDIA_TYPE_AUDIO:
            codec_ctx->codec_id = codec_id;
            codec_ctx->sample_fmt  = (*codec)->sample_fmts ?    // 采样格式
                                     (*codec)->sample_fmts[0] : AV_SAMPLE_FMT_FLTP;
            codec_ctx->bit_rate    = 64000;     // 码率
            codec_ctx->sample_rate = 44100;     // 采样率
            if ((*codec)->supported_samplerates)
            {
                codec_ctx->sample_rate = (*codec)->supported_samplerates[0];
                for (i = 0; (*codec)->supported_samplerates[i]; i++)
                {
                    if ((*codec)->supported_samplerates[i] == 44100)
                        codec_ctx->sample_rate = 44100;
                }
            }
            codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
            codec_ctx->channels        = av_get_channel_layout_nb_channels(codec_ctx->channel_layout);
            if ((*codec)->channel_layouts)
            {
                codec_ctx->channel_layout = (*codec)->channel_layouts[0];
                for (i = 0; (*codec)->channel_layouts[i]; i++) {
                    if ((*codec)->channel_layouts[i] == AV_CH_LAYOUT_STEREO)
                        codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
                }
            }

            codec_ctx->channels        = av_get_channel_layout_nb_channels(codec_ctx->channel_layout);
            // 设置timebase, 使用采样率
            ost->st->time_base = (AVRational){ 1, codec_ctx->sample_rate };
            break;

        case AVMEDIA_TYPE_VIDEO:
            codec_ctx->codec_id = codec_id;
            codec_ctx->bit_rate = 400000;
            /* Resolution must be a multiple of two. */
            codec_ctx->width    = 352;      // 分辨率
            codec_ctx->height   = 288;
            codec_ctx->max_b_frames = 1;
            /* timebase: This is the fundamental unit of time (in seconds) in terms
             * of which frame timestamps are represented. For fixed-fps content,
             * timebase should be 1/framerate and timestamp increments should be
             * identical to 1. */
            ost->st->time_base = (AVRational){ 1, STREAM_FRAME_RATE };  // 时基
            codec_ctx->time_base       = ost->st->time_base;
            codec_ctx->gop_size      = STREAM_FRAME_RATE; //
            codec_ctx->pix_fmt       = STREAM_PIX_FMT;
            break;

        default:
            break;
    }

    /* 看看格式是否希望steam头是分开的 */
    if (oc->oformat->flags & AVFMT_GLOBALHEADER)
        codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;    //
}
创建各种buffer
    if (have_video)
        open_video(oc, video_codec, &video_st, opt);

    if (have_audio)
        open_audio(oc, audio_codec, &audio_st, opt);
open_video

先关联编码器

ret = avcodec_open2(codec_ctx, codec, &opt);

然后分配buffer 大小由传入的相关参数决定

ost->frame = alloc_picture(codec_ctx->pix_fmt, codec_ctx->width, codec_ctx->height);

如果格式不是YUV420P 如果输出格式不是YUV420P,则使用临时YUV420P则需要转换 然后将其转换为所需的输出格式。

    if (codec_ctx->pix_fmt != AV_PIX_FMT_YUV420P)
{
    // 编码器格式需要的数据不是 AV_PIX_FMT_YUV420P才需要 调用图像scale
    ost->tmp_frame = alloc_picture(AV_PIX_FMT_YUV420P, codec_ctx->width, codec_ctx->height);
    if (!ost->tmp_frame)
    {
        fprintf(stderr, "Could not allocate temporary picture\n");
        exit(1);
    }
}

然后AVCodecContext结构体中码流参数拷⻉到AVCodecParameters结构体 也就是拷贝刚刚设定以及初始化的各种 codec_ctx 中参数拷贝到
流中ost->st->codecpar

ret = avcodec_parameters_from_context(ost->st->codecpar, codec_ctx);
static void open_video(AVFormatContext *oc, AVCodec *codec, OutputStream *ost, AVDictionary *opt_arg)
{
    int ret;
    AVCodecContext *codec_ctx = ost->enc;
    AVDictionary *opt = NULL;

    av_dict_copy(&opt, opt_arg, 0);

    /* open the codec */
    // 1. 关联编码器
    ret = avcodec_open2(codec_ctx, codec, &opt);
    av_dict_free(&opt);
    if (ret < 0)
    {
        fprintf(stderr, "Could not open video codec: %s\n", av_err2str(ret));
        exit(1);
    }
    // 2. 分配帧buffer
    /* allocate and init a re-usable frame */
    ost->frame = alloc_picture(codec_ctx->pix_fmt, codec_ctx->width, codec_ctx->height);
    if (!ost->frame)
    {
        fprintf(stderr, "Could not allocate video frame\n");
        exit(1);
    }

    /* If the output format is not YUV420P, then a temporary YUV420P
     * picture is needed too. It is then converted to the required
     * output format. */
    ost->tmp_frame = NULL;
    if (codec_ctx->pix_fmt != AV_PIX_FMT_YUV420P)
    {
        // 编码器格式需要的数据不是 AV_PIX_FMT_YUV420P才需要 调用图像scale
        ost->tmp_frame = alloc_picture(AV_PIX_FMT_YUV420P, codec_ctx->width, codec_ctx->height);
        if (!ost->tmp_frame)
        {
            fprintf(stderr, "Could not allocate temporary picture\n");
            exit(1);
        }
    }

    /* copy the stream parameters to the muxer */
    ret = avcodec_parameters_from_context(ost->st->codecpar, codec_ctx);
    if (ret < 0)
    {
        fprintf(stderr, "Could not copy the stream parameters\n");
        exit(1);
    }
}
alloc_picture

通过传入格式 创建对应的frame

static AVFrame *alloc_picture(enum AVPixelFormat pix_fmt, int width, int height)
{
    AVFrame *picture;
    int ret;

    picture = av_frame_alloc();
    if (!picture)
        return NULL;

    picture->format = pix_fmt;
    picture->width  = width;
    picture->height = height;

    /* allocate the buffers for the frame data */
    ret = av_frame_get_buffer(picture, 32);
    if (ret < 0)
    {
        fprintf(stderr, "Could not allocate frame data.\n");
        exit(1);
    }

    return picture;
}
判断输出文件
if (!(fmt->flags & AVFMT_NOFILE))
    {
        // 打开对应的输出文件,没有则创建
        ret = avio_open(&oc->pb, filename, AVIO_FLAG_WRITE);
        if (ret < 0)
        {
            fprintf(stderr, "Could not open '%s': %s\n", filename,
                    av_err2str(ret));
            return 1;
        }
    }

写入header

audio AVstream->base_time = 1/44100, video AVstream->base_time = 1/25
    /* 写头部. 到底做了什么操作呢? 对应steam的time_base被改写 和封装格式有关系*/
    ret = avformat_write_header(oc, &opt);// 返回值 会改变 base_time   base_time audio = 1/1000 video = 1/1000

####编码音频和视频
判断那个时间比较早 编码早的

 while (encode_video || encode_audio)
    {
        /* select the stream to encode */
        if (encode_video &&         // video_st.next_pts值 <= audio_st.next_pts时
            (!encode_audio || av_compare_ts(video_st.next_pts, video_st.enc->time_base,
                                            audio_st.next_pts, audio_st.enc->time_base) <= 0)) {
            printf("\nwrite_video_frame\n");
            encode_video = !write_video_frame(oc, &video_st);
        }
        else
        {
            printf("\nwrite_audio_frame\n");
            encode_audio = !write_audio_frame(oc, &audio_st);
        }
    }
write_video_frame

就说编码+写入pkt中
oc 是formatctx 音频那边也是同样一个 这就保证了写入目标一致性
ret = write_frame(oc, &codec_ctx->time_base, ost->st, &pkt);

*
*
* encode one video frame and send it to the muxer
* return 1 when encoding is finished, 0 otherwise
*/
static int write_video_frame(AVFormatContext *oc, OutputStream *ost)
{
    int ret;
    AVCodecContext *codec_ctx;
    AVFrame *frame;
    int got_packet = 0;
    AVPacket pkt = { 0 };

    codec_ctx = ost->enc;

    frame = get_video_frame(ost);

    av_init_packet(&pkt);

    /* encode the image */
    //  编码的另外一种方式 第二种方式罢了
    ret = avcodec_encode_video2(codec_ctx, &pkt, frame, &got_packet);
    if (ret < 0)
    {
        fprintf(stderr, "Error encoding video frame: %s\n", av_err2str(ret));
        exit(1);
    }

    if (got_packet)
    {
        ret = write_frame(oc, &codec_ctx->time_base, ost->st, &pkt);
    }
    else
    {
        ret = 0;
    }

    if (ret < 0)
    {
        fprintf(stderr, "Error while writing video frame: %s\n", av_err2str(ret));
        exit(1);
    }

    // 这里之所以有两个判断条件
    // frame非NULL: 表示还在产生YUV数据帧
    // got_packet为1: 编码器还有缓存的帧
    return (frame || got_packet) ? 0 : 1;
}
get_video_frame

设定基础帧 和 重采样 然后产生数据 如果不一样就重采样 一样就不重采样返回数据
就这样简单
音频和视频逻辑类型 说一个就可以

产生一帧视频数据 如果大于需要的时间就不产生! 并且传入这一帧数据对应的PTS

 // 我们测试时只产生STREAM_DURATION(这里是5.0秒)的视频数据   通过next_pts*time_base 得到结果不能大于5 因为只要五秒数据
if (av_compare_ts(ost->next_pts, codec_ctx->time_base,
                  STREAM_DURATION, (AVRational){ 1, 1 }) >= 0)
tatic AVFrame *get_video_frame(OutputStream *ost)
{
    AVCodecContext *codec_ctx = ost->enc;

    /* check if we want to generate more frames */
    // 我们测试时只产生STREAM_DURATION(这里是5.0秒)的视频数据  如果数据比5秒还大那就有问题
    if (av_compare_ts(ost->next_pts, codec_ctx->time_base,
                      STREAM_DURATION, (AVRational){ 1, 1 }) >= 0)
        return NULL;

    /* when we pass a frame to the encoder, it may keep a reference to it
     * internally; make sure we do not overwrite it here */
    // 看看帧可以用不  有没有在用
    if (av_frame_make_writable(ost->frame) < 0)
        exit(1);

    if (codec_ctx->pix_fmt != AV_PIX_FMT_YUV420P)
    {
        /* as we only generate a YUV420P picture, we must convert it
         * to the codec pixel format if needed */
        if (!ost->sws_ctx)
        {
            ost->sws_ctx = sws_getContext(codec_ctx->width, codec_ctx->height,
                                          AV_PIX_FMT_YUV420P,
                                          codec_ctx->width, codec_ctx->height,
                                          codec_ctx->pix_fmt,
                                          SCALE_FLAGS, NULL, NULL, NULL);
            if (!ost->sws_ctx) {
                fprintf(stderr
                        ,
                        "Could not initialize the conversion context\n");
                exit(1);
            }
        }
        //产生数据
        fill_yuv_image(ost->tmp_frame, ost->next_pts, codec_ctx->width, codec_ctx->height);
        //视频重采样
        sws_scale(ost->sws_ctx, (const uint8_t * const *) ost->tmp_frame->data,
                  ost->tmp_frame->linesize, 0, codec_ctx->height, ost->frame->data,
                  ost->frame->linesize);
    } else {
        fill_yuv_image(ost->frame, ost->next_pts, codec_ctx->width, codec_ctx->height);
    }

    ost->frame->pts = ost->next_pts++;  // 为什么+1? 单位是 1/25 = 40ms
    // 0  1 2  -> 0 40ms 80ms
    return ost->frame;
}

源码地址

代码地址
https://github.com/zycccer/aac_mp3_pcm

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值