FFmpeg05:编解码实战

基础介绍

结构体和函数

AVCodec 编码器结构体

AVCodecContext 编码器上下文

AVFrame 解码后的帧

av_frame_alloc/av_frame_free()

avcodec_alloc_context3()

avcodec_free_context()

解码步骤

  • 查找解码器(avcodec_find_decoder)
  • 打开解码器(avcodec_open2)
  • 解码(avcodec_decode_video2)

视频编码

指定输出文件为out.h264,编码器为libx264

伪造YUV420P数据进行测试

cpp 复制代码
extern "C" {
    #include <libavutil/log.h>
    #include <libavcodec/avcodec.h>
    #include <libavutil/opt.h>
}
#include <string>
using namespace std;

static int encode(AVCodecContext *ctx, AVFrame *frame, AVPacket *pkt, FILE *out) {
    int ret = -1;
    ret = avcodec_send_frame(ctx, frame);
    if (ret < 0) {
        av_log(nullptr, AV_LOG_ERROR, "send frame to encoder error\n");
        goto _END;
    }

    while (ret >= 0) {
        ret = avcodec_receive_packet(ctx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) {
            return -1;
        }
        fwrite(pkt->data, pkt->size, 1, out);
        av_packet_unref(pkt);
    }

_END:
    return 0;
}

int main(int argc, char* argv[]) {
    av_log_set_level(AV_LOG_DEBUG);
    string dst;
    string codec_name;
    int ret = -1;
    const AVCodec *codec = nullptr;
    AVCodecContext *codec_ctx = nullptr;
    FILE *fp = nullptr;
    AVFrame *frame = nullptr;
    AVPacket *pkt = nullptr;
    // 1. 输入参数
    if (argc < 3) {
        av_log(nullptr, AV_LOG_ERROR, "arguments must be more than 2\n");
        goto _ERROR;
    }

    dst = argv[1];
    codec_name = argv[2];

    // 2. 查找编码器
    codec = avcodec_find_encoder_by_name(codec_name.c_str());
    if (!codec) {
        av_log(nullptr, AV_LOG_ERROR, "could not find encoder %s\n", codec_name.c_str());
        goto _ERROR;
    }
    
    // 3. 创建编码器上下文
    codec_ctx = avcodec_alloc_context3(codec);
    if (!codec_ctx) {
        av_log(nullptr, AV_LOG_ERROR, "could not allocate codec context\n");
        goto _ERROR;
    }

    // 4. 设置编码器参数
    codec_ctx->width = 640;
    codec_ctx->height = 480;
    codec_ctx->bit_rate = 500000;

    codec_ctx->time_base = (AVRational){1, 25};
    codec_ctx->framerate = (AVRational){25, 1};
    codec_ctx->gop_size = 10; // 每10帧一个关键帧
    codec_ctx->max_b_frames = 1; // 一个gop允许1个B
    codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;

    // 设置264编码器的特殊参数
    if (codec->id == AV_CODEC_ID_H264) {
        // priv_data是编码器私有数据
        // 通过它可以设置编码器私有参数
        // 这些参数在不同编码器中是不一样的
        av_opt_set(codec_ctx->priv_data, "preset", "slow", 0);
    }

    // 5. 编码器与编码器上下文绑定到一起
    ret = avcodec_open2(codec_ctx, codec, nullptr);
    if (ret < 0) {
        av_log(nullptr, AV_LOG_ERROR, "could not open codec %s\n", codec_name.c_str());
        goto _ERROR;
    }
    // 6. 创建输出文件
    fp = fopen(dst.c_str(), "wb+");
    if (!fp) {
        av_log(nullptr, AV_LOG_ERROR, "could not open file %s\n", dst.c_str());
        goto _ERROR;
    }

    // 7. 创建AVFrame
    frame = av_frame_alloc();
    if (!frame) {
        av_log(nullptr, AV_LOG_ERROR, "could not allocate frame\n");
        goto _ERROR;
    }
    frame->width = codec_ctx->width;
    frame->height = codec_ctx->height;
    frame->format = codec_ctx->pix_fmt;

    // 真正的像素数据分配,并与AVFrame绑定,后面设置为0,会自动根据cpu对齐
    ret = av_frame_get_buffer(frame, 0);
    if (ret < 0) {
        av_log(nullptr, AV_LOG_ERROR, "could not allocate frame data\n");
        goto _ERROR;
    }

    // 8. 创建AVPacket
    pkt = av_packet_alloc();
    if (!pkt) {
        av_log(nullptr, AV_LOG_ERROR, "could not allocate packet\n");
        goto _ERROR;
    }
    // 9. 生成视频内容
    for (int i = 0; i < 25; ++i) {
        // 确保frame是否可写
        ret = av_frame_make_writable(frame);
        if (ret < 0) {
            break;
        }

        // Y分量
        for (int y = 0; y < codec_ctx->height; y++) {
            for (int x = 0; x < codec_ctx->width; x++) {
                frame->data[0][y * frame->linesize[0] + x] = x + y + i * 3;
            }
        }
        // UV分量
        for (int y = 0; y < codec_ctx->height / 2; y++) {
            for (int x = 0; x < codec_ctx->width / 2; x++) {
                // U分量:128是黑色
                frame->data[1][y * frame->linesize[1] + x] = 128 + y + i * 2;
                // V分量:64是黑色
                frame->data[2][y * frame->linesize[2] + x] = 64 + x + i * 5;

            }
        }

        frame->pts = i;
        // 10. 编码
        ret = encode(codec_ctx, frame, pkt, fp);

    }
    // 10. 编码 输入空帧目的是输出队列中剩余的数据
    encode(codec_ctx, nullptr, pkt, fp);

_ERROR:
    if (fp) {
        fclose(fp);
        fp = nullptr;
    }
    if (codec_ctx) {
        avcodec_free_context(&codec_ctx);
        codec_ctx = nullptr;
    }
    if (frame) {
        av_frame_free(&frame);
        frame = nullptr;
    }
    if (pkt) { 
        av_packet_free(&pkt);
        pkt = nullptr;
    }

    return 0;
}

音频编码

与视频编码不同的内容:

  • 编码器参数不一样
  • 创建的帧的参数不一样
  • 构建音频的方式与视频的方式差距很大 但是先不用管
cpp 复制代码
extern "C" {
    #include <libavutil/log.h>
    #include <libavcodec/avcodec.h>
    #include <libavutil/opt.h>
}
#include <string>
using namespace std;

static int select_best_samplerate(const AVCodec *codec) {
    const int *p = codec->supported_samplerates;
    int best_samplerate = 0;
    if (!p) {
        return 44100;
    }
    while (*p) {
        // 找到最接近44100的采样率
        if (!best_samplerate || abs(44100 - *p) < abs(44100 - best_samplerate)) {
            best_samplerate = *p;
        }
        p++;
    }
    return best_samplerate;
}

static int check_sample_fmt(const AVCodec *codec, enum AVSampleFormat sample_fmt) {
    const enum AVSampleFormat *p = codec->sample_fmts;
    while (*p != AV_SAMPLE_FMT_NONE) {
        if (*p == sample_fmt) {
            return 1;
        }
        p++;
    }
    av_log(nullptr, AV_LOG_ERROR, "sample format %s not support\n", av_get_sample_fmt_name(sample_fmt));
    return 0;
}

static int encode(AVCodecContext *ctx, AVFrame *frame, AVPacket *pkt, FILE *out) {
    int ret = -1;
    ret = avcodec_send_frame(ctx, frame);
    if (ret < 0) {
        av_log(nullptr, AV_LOG_ERROR, "send frame to encoder error\n");
        goto _END;
    }

    while (ret >= 0) {
        ret = avcodec_receive_packet(ctx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) {
            return -1;
        }
        fwrite(pkt->data, pkt->size, 1, out);
        av_packet_unref(pkt);
    }

_END:
    return 0;
}
/**
 * 参数1:输出文件名
 * 参数2:编码器名称,比如 libfdk-aac 暂时去掉
 */
int main(int argc, char* argv[]) {
    av_log_set_level(AV_LOG_DEBUG);
    string dst;
    // string codec_name;
    int ret = -1;
    const AVCodec *codec = nullptr;
    AVCodecContext *codec_ctx = nullptr;
    FILE *fp = nullptr;
    AVFrame *frame = nullptr;
    AVPacket *pkt = nullptr;
    uint16_t *samples = nullptr;
    float t = 0;
    float tincr = 0;
    AVChannelLayout stereo_layout;

    // 1. 输入参数
    if (argc < 3) {
        av_log(nullptr, AV_LOG_ERROR, "arguments must be more than 2\n");
        goto _ERROR;
    }

    dst = argv[1];
    // codec_name = argv[2];

    // 2. 查找编码器
    // codec = avcodec_find_encoder_by_name(codec_name.c_str());
    codec = avcodec_find_encoder_by_name("libfdk_aac");  // MAC默认不带这个,需要自己编译, 支持
    // codec = avcodec_find_encoder(AV_CODEC_ID_AAC);
    if (!codec) {
        av_log(nullptr, AV_LOG_ERROR, "could not find encoder aac\n");
        goto _ERROR;
    }
    
    // 3. 创建编码器上下文
    codec_ctx = avcodec_alloc_context3(codec);
    if (!codec_ctx) {
        av_log(nullptr, AV_LOG_ERROR, "could not allocate codec context\n");
        goto _ERROR;
    }

    // 4. 设置编码器参数
    codec_ctx->bit_rate = 64000;
    codec_ctx->sample_fmt = AV_SAMPLE_FMT_S16;
    if (!check_sample_fmt(codec, codec_ctx->sample_fmt)) {
        av_log(nullptr, AV_LOG_ERROR, "encoder does not support sample format %s\n", av_get_sample_fmt_name(codec_ctx->sample_fmt));
        goto _ERROR;
    }

    codec_ctx->sample_rate = select_best_samplerate(codec);
    av_channel_layout_copy(&codec_ctx->ch_layout, &stereo_layout);

    // 设置264编码器的特殊参数
    if (codec->id == AV_CODEC_ID_H264) {
        // priv_data是编码器私有数据
        // 通过它可以设置编码器私有参数
        // 这些参数在不同编码器中是不一样的
        av_opt_set(codec_ctx->priv_data, "preset", "slow", 0);
    }

    // 5. 编码器与编码器上下文绑定到一起
    ret = avcodec_open2(codec_ctx, codec, nullptr);
    if (ret < 0) {
        av_log(nullptr, AV_LOG_ERROR, "could not open codec aac\n");
        goto _ERROR;
    }
    // 6. 创建输出文件
    fp = fopen(dst.c_str(), "wb+");
    if (!fp) {
        av_log(nullptr, AV_LOG_ERROR, "could not open file %s\n", dst.c_str());
        goto _ERROR;
    }

    // 7. 创建AVFrame
    frame = av_frame_alloc();
    if (!frame) {
        av_log(nullptr, AV_LOG_ERROR, "could not allocate frame\n");
        goto _ERROR;
    }
    frame->nb_samples = codec_ctx->frame_size; // 每个通道的采样点数
    frame->format = codec_ctx->sample_fmt;
    av_channel_layout_copy(&frame->ch_layout, &codec_ctx->ch_layout);

    // 真正的数据分配,并与AVFrame绑定,后面设置为0,会自动根据cpu对齐
    ret = av_frame_get_buffer(frame, 0);
    if (ret < 0) {
        av_log(nullptr, AV_LOG_ERROR, "could not allocate frame data\n");
        goto _ERROR;
    }

    // 8. 创建AVPacket
    pkt = av_packet_alloc();
    if (!pkt) {
        av_log(nullptr, AV_LOG_ERROR, "could not allocate packet\n");
        goto _ERROR;
    }

    tincr = 2 * M_PI * 440.0 / codec_ctx->sample_rate; // 440hz
    // 9. 生成音频内容
    for (int i = 0; i < 200; i++) { // 200 * 1024 / 44100 = 4.6s
        // 设置数据
        ret = av_frame_make_writable(frame);
        if (ret < 0) {
            av_log(nullptr, AV_LOG_ERROR, "frame not writable\n");
            goto _ERROR;
        }

        samples = (uint16_t*)frame->data[0];
        for (int j = 0; j < codec_ctx->frame_size; j++) {
            samples[2*j] = (int)sin(t) * 10000;
            for (int k = 1; k < codec_ctx->ch_layout.nb_channels; k++) {
                samples[2*j + k] = samples[2*j];
            }
            t += tincr;
        }
        
        // 编码
        ret = encode(codec_ctx, frame, pkt, fp);
        if (ret < 0) {
            av_log(nullptr, AV_LOG_ERROR, "encode error\n");
            goto _ERROR;
        }
    }
    // 10. 编码 输入空帧目的是输出队列中剩余的数据
    encode(codec_ctx, nullptr, pkt, fp);

_ERROR:
    if (fp) {
        fclose(fp);
        fp = nullptr;
    }
    if (codec_ctx) {
        avcodec_free_context(&codec_ctx);
        codec_ctx = nullptr;
    }
    if (frame) {
        av_frame_free(&frame);
        frame = nullptr;
    }
    if (pkt) { 
        av_packet_free(&pkt);
        pkt = nullptr;
    }

    return 0;
}

生成图片

解码视频并保存为PGM或BMP图

其中PGM是只有灰度;

而BMP是彩色的,但确实四位对齐的,所以需要做一些额外操作:

  • 需要内存对齐#pragma pack(push, 1):保证写入的结构体的大小和规范中一致,而不被cpp内存影响
  • 又由于BMP是四字节对齐,不能直接把帧数据全部写入,而是一行一行写入,遇到一行数据不是4的倍数的,需要补0来padding
cpp 复制代码
#include <iostream>
using namespace std;
extern "C" {
    #include <libavformat/avformat.h>
    #include <libavutil/log.h>
    #include <libavutil/avutil.h>
    #include <libavcodec/avcodec.h>
    #include <libavutil/opt.h>
    #include <libswscale/swscale.h>
}
#define WORD uint16_t
#define DWORD uint32_t
#define LONG int32_t

#pragma pack(push, 1) // 强制1字节对齐
typedef struct tagBITMAPFILEHEADER {
  WORD  bfType;
  DWORD bfSize;
  WORD  bfReserved1;
  WORD  bfReserved2;
  DWORD bfOffBits;
} BITMAPFILEHEADER, *LPBITMAPFILEHEADER, *PBITMAPFILEHEADER;

typedef struct tagBITMAPINFOHEADER {
  DWORD biSize;
  LONG  biWidth;
  LONG  biHeight;
  WORD  biPlanes;
  WORD  biBitCount;
  DWORD biCompression;
  DWORD biSizeImage;
  LONG  biXPelsPerMeter;
  LONG  biYPelsPerMeter;
  DWORD biClrUsed;
  DWORD biClrImportant;
} BITMAPINFOHEADER, *LPBITMAPINFOHEADER, *PBITMAPINFOHEADER;

static int savePic(unsigned char *buf, int linesize, int width, int height, string fileName) {
    FILE *fp = fopen(fileName.c_str(), "wb+");
    
    if (!fp) {
        av_log(nullptr, AV_LOG_ERROR, "could not open %s\n", fileName.c_str());
        return -1;
    }
    // 写入头部信息,固定值,这个是PGM格式的magic头
    fprintf(fp, "P5\n%d %d\n255\n", width, height);
    for (int i = 0; i < height; i++) {
        fwrite(buf + i * linesize, 1, width, fp);
    }
    fclose(fp);
    return 0;
}
static void saveBMP(SwsContext *sws_ctx, AVFrame *frame, int w, int h, string fileName) {
    int dataSize = w * h * 3;
    FILE *fp = nullptr;
    // 1. 先进行转换,将YUV frame转成BGR24 frame
    AVFrame *frameBGR = av_frame_alloc();
    frameBGR->width = w;
    frameBGR->height = h;
    frameBGR->format = AV_PIX_FMT_BGR24;
    av_frame_get_buffer(frameBGR, 0);
    
    sws_scale(sws_ctx, frame->data, frame->linesize, 0, 
        frame->height, frameBGR->data, frameBGR->linesize);

    // BMP规范:每行字节数必须 4 字节对齐
    int rowSize = (w * 3 + 3) & ~3;
    int imageSize = rowSize * h;

    // 2. 构造BITMAPINFOHEADER
    BITMAPINFOHEADER infoHeader;
    infoHeader.biSize = sizeof(BITMAPINFOHEADER);
    infoHeader.biWidth = w;
    infoHeader.biHeight = h * (-1);
    infoHeader.biBitCount = 24;
    infoHeader.biCompression = 0;
    infoHeader.biClrImportant = 0;
    infoHeader.biClrUsed = 0;
    infoHeader.biXPelsPerMeter = 0;
    infoHeader.biYPelsPerMeter = 0;
    infoHeader.biPlanes = 1;

    // 3. 构造BITMAPFILEHEADER
    BITMAPFILEHEADER fileHeader = {0,};
    fileHeader.bfType = 0x4d42; // "BM"
    
    fileHeader.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + imageSize;
    fileHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
    
    // 4. 将数据写入
    fp = fopen(fileName.c_str(), "wb");
    fwrite(&fileHeader, sizeof(BITMAPFILEHEADER), 1, fp);
    fwrite(&infoHeader, sizeof(BITMAPINFOHEADER), 1, fp);

    // 每行写入,注意 frameBGR->linesize[0] 可能大于 w*3
    uint8_t *srcData = frameBGR->data[0];
    for (int y = 0; y < h; y++) {
        fwrite(srcData + y * frameBGR->linesize[0], 1, w * 3, fp);

        // 如果 w*3 不是 4 的倍数,要补 padding
        uint8_t padding[3] = {0, 0, 0};
        fwrite(padding, 1, rowSize - w * 3, fp);
    }
    
    // fwrite(frameBGR->data[0], 1, dataSize, fp); // 不能直接这样写,因为BMP是每行都是4位对齐的,只能逐行写入,不够四位需要补零
    
    // 5. 释放资源
    fclose(fp);
    av_freep(&frameBGR->data[0]);
    av_free(frameBGR);
}

static int decode(AVCodecContext *ctx, SwsContext *sws_ctx, AVFrame *frame, AVPacket *pkt, string fileName) {
    int ret = -1;
    ret = avcodec_send_packet(ctx, pkt);
    if (ret < 0) {
        av_log(nullptr, AV_LOG_ERROR, "send frame to encoder error\n");
        goto _END;
    }

    while (ret >= 0) {
        ret = avcodec_receive_frame(ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) {
            return -1;
        }
        string buf = "";
        buf += fileName;
        buf += "-";
        buf += to_string(ctx->frame_num);
        buf += ".bmp";

        // saveBMP(sws_ctx, frame, frame->width, frame->height, buf);
        savePic(frame->data[0], frame->linesize[0], frame->width, frame->height, buf);
        if (pkt) {
            av_packet_unref(pkt);
        }
    }

_END:
    return 0;
}

/** 
 * argv[0]: 可执行程序的路径
 * argv[1]: 源文件路径
 * argv[2]: 目的文件路径
 * */ 
int main(int argc, char* argv[]) {
    // 1. 处理参数
    string src;
    string dst;
    int ret = -1;
    int idx = -1;
    AVFormatContext *pFmtCtx = nullptr;
    AVStream *inStream = nullptr;
    AVPacket *pkt = nullptr;
    AVFrame *frame = nullptr;
    const AVCodec *codec = nullptr;
    AVCodecContext *codec_ctx = nullptr;
    SwsContext *sws_ctx = nullptr;

    av_log_set_level(AV_LOG_INFO);
    if (argc < 3) {
        av_log(nullptr, AV_LOG_ERROR, "arguments must be more than 2\n");
        exit(-1);
    }
    src = argv[1];
    dst = argv[2];

    // 2. 打开多媒体文件
    ret = avformat_open_input(&pFmtCtx, src.c_str(), nullptr, nullptr);
    if (ret < 0) {
        av_log(nullptr, AV_LOG_ERROR, "%s\n", av_err2str(ret));
        exit(-1);
    }

    // 3. 从多媒体文件中找到视频流
    idx = av_find_best_stream(pFmtCtx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
    if (idx < 0) {
        av_log(pFmtCtx, AV_LOG_ERROR, "could not find audio stream in %s\n", src.c_str());
        goto _ERROR;
    }

    inStream = pFmtCtx->streams[idx];
    // 4. 查找解码器
    codec = avcodec_find_decoder(inStream->codecpar->codec_id);
    if (!codec) {
        av_log(nullptr, AV_LOG_ERROR, "could not find encoder x264\n");
        goto _ERROR;
    }
    
    // 5. 创建解码器上下文
    codec_ctx = avcodec_alloc_context3(codec);
    if (!codec_ctx) {
        av_log(nullptr, AV_LOG_ERROR, "could not allocate codec context\n");
        goto _ERROR;
    }

    // 6. 根据流参数,设置解码器上下文
    avcodec_parameters_to_context(codec_ctx, inStream->codecpar);

    // 7. 解码器与解码器上下文绑定到一起
    ret = avcodec_open2(codec_ctx, codec, nullptr);
    if (ret < 0) {
        av_log(nullptr, AV_LOG_ERROR, "could not open codec x264\n");
        goto _ERROR;
    }

    // 7.1 获得SWSContext
    sws_ctx = sws_getContext(codec_ctx->width, codec_ctx->height, AV_PIX_FMT_YUV420P, 
        codec_ctx->width, codec_ctx->height, AV_PIX_FMT_BGR24, SWS_BICUBIC, nullptr, nullptr, nullptr);

    // 8. 创建AVFrame
    frame = av_frame_alloc();
    if (!frame) {
        av_log(nullptr, AV_LOG_ERROR, "could not allocate frame\n");
        goto _ERROR;
    }

    // 9. 创建AVPacket
    pkt = av_packet_alloc();
    if (!pkt) {
        av_log(nullptr, AV_LOG_ERROR, "could not allocate packet\n");
        goto _ERROR;
    }
   
    
    // 10. 读取视频帧,进行编码,写入目的文件
    while (av_read_frame(pFmtCtx, pkt) >= 0) {
        if (pkt->stream_index == idx) {
            decode(codec_ctx, sws_ctx, frame, pkt, dst);
        }
        av_packet_unref(pkt);
    }
    decode(codec_ctx, sws_ctx, frame, nullptr, dst); // 这里传nullptr,表示刷新编码器
    
    // 11. 释放资源,关闭文件
_ERROR:
    if (pFmtCtx) {
        avformat_close_input(&pFmtCtx);
        pFmtCtx = nullptr;
    }
    if (codec_ctx) {
        avcodec_free_context(&codec_ctx);
        codec_ctx = nullptr;
    }
    if (frame) {
        av_frame_free(&frame);
        frame = nullptr;
    }
    if (pkt) {
        av_packet_free(&pkt);
        pkt = nullptr;
    }

    return 0;
}

#pragma pack(pop) // 恢复对齐设置