FFmpeg编解码
2025.7.01🐱

一、yuv编码为h264

使用yuv编码为h264,然后用ffplay播放h264文件

先准备一个mp4文件 然后使用ffmpeg命令提取视频数据并转为yuv格式

ffmpeg -i input.mp4 -pixel_format yuv420p -s 1280x720 output.yuv

把 input.mp4 里的视频“解码 → 颜色转换 → 缩放”后,按 720p YUV420p 的裸像素流输出到 output.yuv,注意提取的是纯YUV视频数据,没有音频数据,后面编码后播放也只是视频数据

最后编码成功的h264可以进行ffplay播放

ffplay -i input.264

编码流程:


yuv转h264函数体

整体流程:

pro.工程文件链接头文件和库文件

DEFINES += PROJECT_ROOT=\\\"$$PWD\\\" #.pro文件所在目录
# 当前目录下的 FFMPEG_Packet
FFMPEG_DIR = $$PWD/ffmpeg_packet
INCLUDEPATH += $$FFMPEG_DIR/include
LIBS += -L$$FFMPEG_DIR/lib \
        -lavcodec \
        -lavformat \
        -lavutil \
        -lswresample \
        -lswscale

yuv2h264函数

int encoder::yuv2h264()
{
    QString projectRoot = QStringLiteral(PROJECT_ROOT); //.pro文件所在目录
    qDebug() << projectRoot;
    QString inFilePath = projectRoot + "/output.yuv";
    QString outFilePath = projectRoot + + "/output.h264";
    int ret = 0;
    //准备编码器
    const AVCodec* codec = nullptr;
    AVCodecContext* codecContent = nullptr; //上下文
    AVPacket* packet = nullptr;
    AVFrame* frame = nullptr;
    FILE* inFile = nullptr, *outFile = nullptr;

    //查找264编码器
    codec = avcodec_find_encoder(AV_CODEC_ID_H264);
    if (!codec) {
        qDebug() << "avcodec_find_decoder failed";
        return -1;
    }

    //分配编码器上下文
    codecContent = avcodec_alloc_context3(codec);
    if (!codecContent) { qDebug() << "avcodec_alloc_context3 failed";
        return -2;
    }

    //设置上下文参数
    codecContent->width = 1280;
    codecContent->height = 720;
    codecContent->time_base = AVRational{1,25};
    codecContent->pix_fmt = AV_PIX_FMT_YUV420P;
    codecContent->framerate = AVRational{25,1};

    //打开编码器
    ret = avcodec_open2(codecContent, codec, NULL);
    if (ret != 0){
        qDebug() << "avcodec_open2 failed";
        return -3;
    }
    //分配编码包
    packet = av_packet_alloc();
    if (!packet) return -4;
    //分配编码帧
    frame = av_frame_alloc();
    if (!frame){
        return -5;
    }
    //设置帧参数
    frame->width = 1280;
    frame->height = 720;
    frame->format = AV_PIX_FMT_YUV420P;
    //申请frame的内存来存放帧数据
    ret = av_frame_get_buffer(frame, 0);
    if (ret){
        qDebug() << "av_frame_get_buffer failed";
        return -6;
    }

    //打开输入文件读数据
    inFile = fopen(inFilePath.toStdString().c_str(), "rb");
    if (!inFile){
        qDebug() << "open inFile failed";
        return -7;
    }
    //打开输出文件
    outFile = fopen(outFilePath.toStdString().c_str(), "wb");
    if (!outFile){
        qDebug() << "open outFile failed";
        return -8;
    }
    int frame_count = 0;
    //循环读数据编码
    while (!feof(inFile)) {
        ret = av_frame_is_writable(frame); //检查frame是否可写
        if (ret < 0){
            //我们要设置为可写
            ret = av_frame_make_writable(frame);
        }
        // 从yuv文件读取y u v分量到frame中 然后编码
        fread(frame->data[0], 1, frame->width * frame->height, inFile); //y分量
        fread(frame->data[1], 1, frame->width * frame->height / 4, inFile); //u分量
        fread(frame->data[2], 1, frame->width * frame->height / 4, inFile); //v分量
        
        frame->pts = frame_count++; //设置帧的时间戳
        //真正地编码
        encode(codecContent, packet, frame, outFile);
    }
    //编码完成后 发送空的帧 告诉编码器 已经没有数据了 刷新编码器
    encode(codecContent, packet, NULL, outFile);
    qDebug() << "encode success";
    //释放资源
    fclose(inFile);
    fclose(outFile);
    av_frame_free(&frame);
    av_packet_free(&packet);
    avcodec_free_context(&codecContent);
    return 0;
}

encode函数体

int encoder::encode(AVCodecContext *codecContext, AVPacket *packet, AVFrame *frame, FILE *outFile)
{
    int ret = avcodec_send_frame(codecContext, frame);
    if (ret < 0){
        qDebug() << "avcodec_send_frame failed";
        return -1;
    }
    // 编码成功开始循环接收
    while (ret == 0) {
        ret = avcodec_receive_packet(codecContext, packet);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){
            return 0;
        }
        else if(ret < 0){ //接收失败
            return -1;
        }
        else if (ret == 0){
            //接收成功 写入到输出文件
            fwrite(packet->data, 1, packet->size, outFile);
        }
    }
    return 0;
}

编码成功

从libx264编码器输出日志中可以看到一些信息:

YUV 4:2:0色度抽样,8位深度,目标码率:2032.23 kb/s,I帧(关键帧):5个,平均QP(量化参数)19.39,平均大小27,843字节,P帧(预测帧):254个,平均QP 22.04,平均大小11,583字节,B帧(双向预测帧):101个,平均QP 23.26,平均大小5,711字节,B帧连续比例:56.1%连续B帧,16.7%非连续

最后用ffplay播放即可


二、pcm编码为aac

使用pcm编码为aac,然后使用ffplay播放

先准备一个mp4文件 然后使用ffmpeg命令提取音频数据并转为pcm格式,设置音频采样率为44.1kHz,设置为双声道立体声,指定输出格式为有符号16位小端PCM

ffmpeg -i input.mp4 -ar 44100 -ac 2 -f s16le output.pcm

最后编码成功的aac可以进行ffplay播放

ffplay -i output.aac

pcm转aac函数整体流程


pcm2aac函数体

int encoder::pcm2aac()
{
    QString projectRoot = QStringLiteral(PROJECT_ROOT); //.pro文件所在目录
    qDebug() << projectRoot;
    QString inputfile = projectRoot + "/output.pcm";
    QString outputfile = projectRoot + "/output.aac";
    //1. 查找编码器
    const AVCodec* avcodec = avcodec_find_encoder(AV_CODEC_ID_AAC);
    if (!avcodec){
        qDebug() << "avcodec_find_encoder failed";
        return -1;
    }

    //2. 设置编码器
    //分配编码器上下文
    AVCodecContext* avcodecContext = avcodec_alloc_context3(avcodec);
    if (!avcodecContext){
        qDebug() << "avcodec_alloc_context3 failed";
        return -2;
    }
    //设置上下文参数
    avcodecContext->sample_rate = 44100; //采样率
    avcodecContext->channels = 2; //通道数
    avcodecContext->sample_fmt = AV_SAMPLE_FMT_FLTP; //样本格式
    avcodecContext->channel_layout = AV_CH_LAYOUT_STEREO; //声道布局 //立体声
    avcodecContext->bit_rate = 64000; //比特率

    //3. 打开解码器
    int ret = 0;
    ret = avcodec_open2(avcodecContext, avcodec, NULL);
    if (ret != 0){ qDebug() << "avcodec_open2 failed";
        return -3;
    }

    //4. 分配创建输出上下文
    AVFormatContext* avFormatContext = nullptr;
    avformat_alloc_output_context2(&avFormatContext, NULL, NULL, outputfile.toStdString().c_str());
    if (!avFormatContext){
        qDebug() << "avformat_alloc_output_context2 failed";
        return -4;
    }

    //5. 创建流并添加到 AVFormatContext 输出上下文中 一个 AVFormatContext 可以包含多个流,例如视频流和音频流
    AVStream* st = avformat_new_stream(avFormatContext, NULL);
    if (!st){
        qDebug() << "avformat_new_stream failed";
        return -5;
    }

    //6. 设置流参数 将编码器上下文的参数复制到流的参数 保了流的参数与编码器的参数一致
    avcodec_parameters_from_context(st->codecpar, avcodecContext);

    //7. 打开输出文件
    ret = avio_open(&avFormatContext->pb, outputfile.toStdString().c_str(), AVIO_FLAG_WRITE);
    if (ret < 0){
        qDebug() << "avio_open failed";
        return -6;
    }

    //8. 写入文件头信息 (包括封装格式的特定信息 流信息(文件头中会包含每个流的参数这些信息是从上下文获取的))
    avformat_write_header(avFormatContext, NULL);

    //9. 初始化重采样 (设置重采样参数 初始化重采样上下文)
    SwrContext* swrContext = nullptr;
    //设置重采样参数
    swrContext = swr_alloc_set_opts(swrContext, avcodecContext->channel_layout, avcodecContext->sample_fmt,
                                    avcodecContext->sample_rate, AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, 44100, 0, 0);
    if (!swrContext){
        qDebug() << "swr_alloc_set_opts failed";
        return -7;
    }

    ret = swr_init(swrContext); //初始化重采样上下文
    if (ret < 0){ //看看参数是否合理
        qDebug() << "swr_init failed";
        return -7;
    }

    //10. 准备frame音频帧 (分配音频帧 设置帧参数 为帧分配内存)
    AVFrame* frame = nullptr;
    frame = av_frame_alloc(); //分配音频帧

    //设置frame参数
    frame->format = AV_SAMPLE_FMT_FLTP; //帧格式
    frame->channels = 2; //通道数
    frame->channel_layout = AV_CH_LAYOUT_STEREO; //声道布局 立体声
    frame->nb_samples = 1024; //样本点数量
    //为帧分配内存
    ret = av_frame_get_buffer(frame, 0);
    if (ret < 0){
        qDebug() << "av_frame_get_buffer failed";
        return -8;
    }

    /*编码阶段*/
    //开始读pcm数据 -> frame
    int readSize = frame->nb_samples * 2 * 2; //每次需要读的大小
    char* pcms = new char[readSize]; //申请内存
    //11. 打开输入文件
    FILE* fp = fopen(inputfile.toStdString().c_str(), "rb");
    if (!fp){
        qDebug() << "fopen inputfile failed";
        return -9;
    }

    //开始编码
    //12. 循环读数据编码并写入输出文件(读取pcm数据 重采样 编码写入输出文件_
    for(;;){
        AVPacket pkt;
        av_init_packet(&pkt); //初始化数据包Packet存储编码后的音频数据
        int len = fread(pcms,1,readSize, fp); //读取 PCM 数据
        if (len <= 0){ //读取到文件末尾或发生错误
            //刷新编码
            avcodec_send_frame(avcodecContext, nullptr);
            while (avcodec_receive_packet(avcodecContext, &pkt) != AVERROR_EOF);
            //循环接收编码器输出的剩余数据包 直到没有更多数据
            break;
        }else{
            //开始重采样 将 PCM 数据从原始格式转换为编码器所需的格式
            const uint8_t* data[1];
            data[0] = (uint8_t*) pcms;

            //重采样操作
            ret = swr_convert(swrContext, frame->data, frame->nb_samples, data, frame->nb_samples);
            if (ret <= 0){
                qDebug() << "swr_convert failed";
                break;
            }

            // 设置帧的时间戳
            static int frame_count = 0;
            frame->pts = frame_count++;

            //重采样成功 我们编码数据
            ret = avcodec_send_frame(avcodecContext, frame);
            if (ret < 0){ //编码失败
                qDebug() << "avcodec_send_frame failed";
                continue; //继续编码下一帧
            }
            ret = avcodec_receive_packet(avcodecContext, &pkt);
            if (ret == 0){
                //接收成功 将AVPacket压缩数据写入到输出文件
                av_interleaved_write_frame(avFormatContext, &pkt);
            }
        }
    }
    //13. 写入文件尾信息
    av_write_trailer(avFormatContext);
    //14. 刷新编码器
    avcodec_send_frame(avcodecContext, nullptr);
    //15. 释放资源
    fclose(fp);
    avio_close(avFormatContext->pb);
    avcodec_free_context(&avcodecContext);
    avformat_free_context(avFormatContext);
    return 0;
}

可以发现pcm转aac的编码好像要比yuv转h264复杂,多了下面一些步骤:一个是要去创建流 以及设置流参数和编码器上下文参数一致 还有一个是要写入文件头信息文件尾信息 还有要进行重采样后才能编码
这些差异源于音频编码的时序敏感性(需要精确到采样点)和容器格式的强制性要求。视频编码由于帧独立性更强,处理流程相对简单。(具体需要更深入的去了解aac编码格式h264编码格式

关于为什么需要创建流和让流参数和编码器参数一致,而之前的yuv转h264不用:

H264裸流通过SPS/PPS自带参数,视频帧自带时间戳和关键帧标记,而AAC需要流作为数据载体,流对象存储着时间基(time_base)、编码参数等关键信息,同时要确保封装时使用的参数与编码器一致,不然可能出现采样率/声道数不匹配等问题

关于为什么需要写入文件头和文件尾 :
音频裸流AAC需要ADTS头记录采样率、声道数等元信息,同时ADTS还包含同步字字段,用于定位帧起始位置,用于帧同步而视频裸流(H264)自带帧同步信息 (可以看看aac音频分析那一篇)

可以进行一个对比看看:

  • H264裸流(如 .h264 文件)通过NALU(Network Abstraction Layer Unit)单元组织数据
  • 每个NALU以 00 00 00 01 或 00 00 01 作为起始码(Sync Byte)
  • 关键帧(I帧)自带SPS/PPS参数集,包含:SPS(Sequence Parameter Set),profile_idc/level_idc/帧率/分辨率等;PPS(Picture Parameter Set) 熵编码类型/切片组数等

因此可以直接写入文件,播放器通过解析NALU头即可同步解码

ADTS头是AAC音频流的一部分 :

  • ADTS(Audio Data Transport Stream)头是AAC裸流(原始AAC数据)自带的头部信息
  • 每个AAC数据帧前都有7-9字节的ADTS头(类似H264的NALU头)

音频帧没有类似视频的关键帧概念,所以需要容器头维护,而文件尾写入用于更新最终的帧计数和时长统计

关于编码前为什么需要重采样:

输入PCM通常是S16格式,而AAC编码器需要FLTP格式,而视频YUV420P可以直接被H264编码器接受

音频重采样还处理了:

  • 声道混合/分离
  • 采样点对齐(1024 samples/frame)
  • 比特深度转换

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
下一篇