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数据进行测试
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个Bcodec_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. 创建AVFrameframe = 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. 创建AVPacketpkt = 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;
}
音频编码
与视频编码不同的内容:
- 编码器参数不一样
- 创建的帧的参数不一样
- 构建音频的方式与视频的方式差距很大 但是先不用管
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. 创建AVFrameframe = 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. 创建AVPacketpkt = 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
#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 frameAVFrame *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. 构造BITMAPINFOHEADERBITMAPINFOHEADER 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. 构造BITMAPFILEHEADERBITMAPFILEHEADER 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*3uint8_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 的倍数,要补 paddinguint8_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 获得SWSContextsws_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. 创建AVFrameframe = av_frame_alloc();if (!frame) {av_log(nullptr, AV_LOG_ERROR, "could not allocate frame\n");goto _ERROR;}// 9. 创建AVPacketpkt = 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) // 恢复对齐设置