本章节将介绍如何将yuv文件或pcm文件编码成h264和aac文件,以下代码运行后会将输入的pcm文件编码成aac文件,如果需要编码yuv文件需要修改输入文件和输出文件,同时将enum AVCodecID codec_id设为AV_CODEC_ID_H264.

完整代码

在运行目录准备好yuv文件和pcm文件,编译运行后会在输出目录生成编码后的h264或aac文件。

#ifdef __cplusplus
extern "C"
{
#endif
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libavcodec/avcodec.h>
#include <libavutil/channel_layout.h>
#include <libavutil/common.h>
#include <libavutil/frame.h>
#include <libavutil/samplefmt.h>
#include <libavutil/time.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
#ifdef __cplusplus
}
#endif

// 添加7字节的adts音频头,固定格式
static void get_adts_header(AVCodecContext *ctx, uint8_t *adts_header, int aac_length)
{
    uint8_t freq_idx = 0; // 索引
    uint32_t frame_length = aac_length + 7;
    switch (ctx->sample_rate) {   // 根据采样率得到索引值
        case 96000: freq_idx = 0; break;
        case 88200: freq_idx = 1; break;
        case 64000: freq_idx = 2; break;
        case 48000: freq_idx = 3; break;
        case 44100: freq_idx = 4; break;
        case 32000: freq_idx = 5; break;
        case 24000: freq_idx = 6; break;
        case 22050: freq_idx = 7; break;
        case 16000: freq_idx = 8; break;
        case 12000: freq_idx = 9; break;
        default:    freq_idx = 4; break;
    }
    adts_header[0] = 0xFF;
    adts_header[1] = 0xF1;
    adts_header[2] = ((ctx->profile) << 6) + (freq_idx << 2) + (ctx->channels >> 2);
    adts_header[3] = (((ctx->channels & 3) << 6) + (frame_length  >> 11));
    adts_header[4] = ((frame_length & 0x7FF) >> 3);
    adts_header[5] = (((frame_length & 7) << 5) + 0x1F);
    adts_header[6] = 0xFC;
}

// 编码一帧数据(音频或者视频)
static int encode(AVCodecContext *ctx, AVFrame *frame, AVPacket *pkt, FILE *out_file)
{
    /* 使用x264进行编码时,不会增加avframe对应buffer的reference*/
    int ret = avcodec_send_frame(ctx, frame);    // 发送帧数据给解码器,send后需要用循环receive多次
    if (ret < 0) { return -1; }

    while (ret >= 0) {                           // send 1次,然后receive多次, 直到AVERROR(EAGAIN)或者AVERROR_EOF
        ret = avcodec_receive_packet(ctx, pkt);  // 函数内部会释放packet,while里不需要调用av_packet_unref();
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;  // 解码完成
        } else if (ret < 0) {
            return -1; // 解码出错
        }
        if((ctx->flags & AV_CODEC_FLAG_GLOBAL_HEADER)) { // 音频需要额外的adts header写入
            uint8_t aac_header[7];                       // adts音频头
            get_adts_header(ctx, aac_header, pkt->size); // 获取adts音频头
            if (fwrite(aac_header, 1, 7, out_file) != 7) {    // 将adts音频头写入文件
                return -1; // 音频头写入失败失败
            }
        }

        if (fwrite(pkt->data, 1, pkt->size, out_file) != (size_t)pkt->size) { // 将编码后的数据写入文件
            return -1; // 写文件失败
        }
    }
    return -1;
}

// 采样格式转化,将f32le packed转为float palanar
static void f32le_convert_to_fltp(float *f32le, float *fltp, int nb_samples) {
    float *fltp_l = fltp;                 // 左通道
    float *fltp_r = fltp + nb_samples;    // 右通道
    for(int i = 0; i < nb_samples; i++) { // 这里只支持2通道的转换
        fltp_l[i] = f32le[i*2];
        fltp_r[i] = f32le[i*2+1];
    }
}

int main()
{
    char           *in_file    = "f32le.pcm";                    // 输入文件
    char           *out_file   = "out.aac";                      // 输出文件
    FILE           *infile     = fopen(in_file, "rb");           // 打开输入文件
    FILE           *outfile    = fopen(out_file, "wb");          // 打开输出文件
    AVFrame        *frame      = av_frame_alloc();               // 用于存放编码前的音频帧
    AVPacket       *pkt        = av_packet_alloc();              // 用于存放编码后的音频帧
    enum AVCodecID codec_id    = AV_CODEC_ID_AAC;                // 编码器ID,libx264
    const AVCodec  *codec      = avcodec_find_encoder(codec_id); // 解码器
    AVCodecContext *codec_ctx  = avcodec_alloc_context3(codec);  // 解码器上下文

    if (!infile)    { exit(1); }  // 输入文件打开失败
    if (!outfile)   { exit(1); }  // 输出文件打开失败
    if (!frame)     { exit(1); }  // AVFrame创建失败
    if (!pkt)       { exit(1); }  // AVPacket创建失败
    if (!codec)     { exit(1); }  // 找不到解码器
    if (!codec_ctx) { exit(1); }  // 解码器上下文创建失败

    if (codec->id == AV_CODEC_ID_H264) { // 视频设置
        codec_ctx->width        = 1280;  // 设置分辨率*/
        codec_ctx->height       = 720;
        codec_ctx->time_base    = (AVRational){1, 25}; // 时间基数 1/25
        codec_ctx->framerate    = (AVRational){25, 1}; // 帧率25帧/秒
        codec_ctx->gop_size     = 25;   // I帧间隔,如果frame->pict_type设置为AV_PICTURE_TYPE_I, 则忽略gop_size的设置,一直当做I帧编码
        codec_ctx->max_b_frames = 2;    // 如果不想包含B帧则设置为0
        codec_ctx->pix_fmt      = AV_PIX_FMT_YUV420P;
        codec_ctx->bit_rate     = 3000000;   // 编码器比特率
        av_opt_set(codec_ctx->priv_data, "preset", "medium", 0);
        av_opt_set(codec_ctx->priv_data, "profile", "main", 0);   // 默认是high
        av_opt_set(codec_ctx->priv_data, "tune","zerolatency",0); // 直播时才使用该设置
        // av_opt_set(codec_ctx->priv_data, "tune","film",0); //  画质film
    } else { // 音频设置
        codec_ctx->codec_id       = codec_id;              // 编码器ID
        codec_ctx->bit_rate       = 128*1024;              // 比特率
        codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;   // 音频通道类型,可通过遍历codec->channel_layouts数组判断解码器是否支持该通道类型
        codec_ctx->sample_rate    = 48000;                 // 采样率48000,可通过遍历codec->supported_samplerates数组判断解码器是否支持该采样率
        codec_ctx->channels       = av_get_channel_layout_nb_channels(codec_ctx->channel_layout); // 通道数
        codec_ctx->sample_fmt     = AV_SAMPLE_FMT_FLTP;    // 采样格式,可通过遍历codec->sample_fmts数组判断解码器是否支持该采样格式
        codec_ctx->codec_type     = AVMEDIA_TYPE_AUDIO;    // 解码器类型,音频解码器
        codec_ctx->profile        = FF_PROFILE_AAC_LOW;    //
        codec_ctx->flags = AV_CODEC_FLAG_GLOBAL_HEADER;    // ffmpeg默认的aac是不带adts,而fdk_aac默认带adts,这里我们强制不带
    }

    if (avcodec_open2(codec_ctx, codec, NULL) < 0) { // 将codec_ctx和codec进行绑定
        exit(1);
    }
    printf("thread_count: %d, thread_type:%d\n", codec_ctx->thread_count, codec_ctx->thread_type);
    printf("frame_size:%d\n\n", codec_ctx->frame_size);           // 帧大小,avcodec_open2函数执行后才有frame_size

    int frame_bytes = 0;
    if (codec->id == AV_CODEC_ID_H264) { // 视频设置
	    frame->format = codec_ctx->pix_fmt;
        frame->width  = codec_ctx->width;
        frame->height = codec_ctx->height;
        // 计算出每一帧的数据 像素格式 * 宽 * 高
        frame_bytes = av_image_get_buffer_size(frame->format, frame->width, frame->height, 1);
	} else { // 音频设置
		// 每次送多少数据给编码器由frame_size、sample_fmt、channel_layout决定
        frame->nb_samples     = codec_ctx->frame_size;     // 每帧单个通道的采样点数
        frame->format         = codec_ctx->sample_fmt;     // 采样格式
        frame->channel_layout = codec_ctx->channel_layout; // 声道布局
        frame->channels       = av_get_channel_layout_nb_channels(frame->channel_layout);
        // 计算出每一帧的数据 单个采样点的字节 * 通道数目 * 每帧采样点数量
        frame_bytes = av_get_bytes_per_sample(frame->format) * frame->channels * frame->nb_samples;
	}
    
    if (av_frame_get_buffer(frame, 0) < 0) { // 为AVFrame分配空间
        exit(1);  // AVFrame空间分配失败
    }

    printf("frame_bytes %d\n", frame_bytes);
    uint8_t *frame_buf = (uint8_t *)malloc(frame_bytes);
    uint8_t *pcm_temp_buf = (uint8_t *)malloc(frame_bytes);
    if(!frame_buf || !pcm_temp_buf) {
        return 1; // 缓冲区空间分配失败
    }

    int64_t all_begin_time = av_gettime_relative() / 1000;   // 编码开始时间
    int64_t pts = 0; // 时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据
    printf("============================== Start Encode ==============================\n");
    for (;;) {
        memset(frame_buf, 0, frame_bytes); // 清空缓冲区
        size_t read_bytes = fread(frame_buf, 1, frame_bytes, infile);
        if(read_bytes <= 0) {
            break; // 读到文件末尾,编码结束
        }

        // 确保该frame可写,新写入的数据和编码器保存的数据不能产生冲突
        if(av_frame_make_writable(frame) != 0) { // av_frame_is_writable(frame)函数可以判断frame是否可写
            break; // frame不可写,退出
        }

        if (codec->id == AV_CODEC_ID_H264){ // 视频
            av_image_fill_arrays(frame->data, frame->linesize, frame_buf,
                                 frame->format, frame->width, frame->height, 1);
            pts += 40; // 40ms一帧,一秒25帧
        } else { // 音频
            memset(pcm_temp_buf, 0, frame_bytes);
            f32le_convert_to_fltp((float *)frame_buf, (float *)pcm_temp_buf, frame->nb_samples); // 将f32le packed转为float palanar
            av_samples_fill_arrays(frame->data, frame->linesize,    // 将读取到的PCM数据填充到frame去
                    pcm_temp_buf, frame->channels, frame->nb_samples, frame->format, 0);
            pts += frame->nb_samples; // 使用采样率作为时间戳的单位,一个nb_samples代表一个单位pts
        }

        frame->pts = pts;         // 设置pts,具体换算成秒 pts/采样率
        if (encode(codec_ctx, frame, pkt, outfile) < 0) { // 一帧一帧编码
            break; // 编码失败
        }
    }

    encode(codec_ctx, NULL, pkt, outfile);  // 冲刷编码器,可能有一小段未编码完成
    printf("============================== End of Encode ==============================\n");
    printf("Encode time: %4.4fs\n", ((av_gettime_relative() / 1000) - all_begin_time) / 1000.0); // 计算编码总时长

    if(frame_buf) { // 释放内存
        free(frame_buf);
        frame_buf = NULL;
    }
    if (pcm_temp_buf) {
        free(pcm_temp_buf);
        pcm_temp_buf = NULL;
    }
    fclose(infile);// 关闭文件
    fclose(outfile);
    av_frame_free(&frame);
    av_packet_free(&pkt);
    avcodec_free_context(&codec_ctx);
    getchar();
    return 0;
}