文章目錄
- 1.準備工作
- 2.壓縮編碼工作流程
- 3.詳細步驟
- 1. 初始化日志和參數檢查
- 2. 輸入/輸出文件的打開
- 3. 查找和初始化編碼器
- 4. 打開編碼器
- 5. 幀內存的分配和初始化
- 6. 設置轉換上下文(SWS)
- 7. 讀取和轉換數據
- 8. 編碼過程
- 9. 資源清理
- 4.完整示例代碼
1.準備工作
原始YUV文件,只包含圖像的原始信息,無論是播放還是進行H264壓縮編碼,都需要知曉文件格式。
-
像素格式:常見的格式有YUV420P、YUV422P、YUV420打包格式:NV12 NV21 等等
-
分辨率:寬度和高度
FFmpeg播放YUV文件示例:
ffplay -f rawvideo -pixel_format yuv422p -video_size 1920x1080 input_1920x1080_yuv422p_1.yuv
命令解析:
ffplay
: 這是用于播放視頻和音頻文件的命令行媒體播放器。-f rawvideo
: 這個選項指定輸入文件的格式。rawvideo
表示輸入文件是一個未經壓縮處理的視頻數據流。-pixel_format yuv422p
: 指定了像素格式。yuv422p
是一種 YUV 格式,其中 Y、U 和 V 分量是平面分隔的(p 表示平面),并且色度(U 和 V)采樣是每兩個像素共享一次(即 4:2:2 采樣)。這個格式常見于專業視頻編輯和后期處理中。-video_size 1920x1080
: 設置視頻的分辨率為 1920x1080 像素,這通常是全高清視頻的標準分辨率。input_1920x1080_yuv422p_1.yuv
: 這是輸入文件的路徑和名稱。文件擴展名.yuv
通常用于存儲原始的 YUV 格式視頻數據。
效果如下:這里以一張高清圖片為示例
2.壓縮編碼工作流程
總體流程:
需要注意的是:編碼H264一般使用X264的居多,要求輸入的格式一般為YUV420格式,因此如果是YUV422需要轉換為YUV420格式
3.詳細步驟
1. 初始化日志和參數檢查
av_log_set_level(AV_LOG_DEBUG);av_log(NULL, AV_LOG_INFO, "FFMPEG DEBUG LOG BEGIN.....\n");if (argc < 5){printf("Usage: %s <input file> <width> <height> <output file>\n", argv[0]);return -1;}
- 使用
av_log_set_level
設置日志級別,用于調試信息輸出。 - 檢查傳入的命令行參數數量,確保提供了足夠的信息來進行后續處理。
2. 輸入/輸出文件的打開
// 讀取YUV 文件input_file = fopen(input_filename, "rb");if (!input_file){fprintf(stderr, "Could not open input file '%s'\n", input_filename);CLEANUP(failure);return -1;}// 打開要寫入的文件output_file = fopen(output_filename, "wb");if (!output_file){fprintf(stderr, "Could not open output file '%s'\n", output_filename);CLEANUP(failure);return -1;}
- 嘗試打開輸入文件(YUV數據源)和輸出文件(存儲編碼后的視頻)。如果文件打開失敗,跳轉到
failure
標簽進行資源釋放和退出。
3. 查找和初始化編碼器
- 查找H264編碼器。如果找不到編碼器,跳轉到
failure
。
// 查找編碼器codec = avcodec_find_encoder(AV_CODEC_ID_H264);if (!codec){av_log(NULL, AV_LOG_ERROR, "H264 Codec not found.....\n");CLEANUP(failure);return -1;}// 分配編碼器上下文codec_ctx = avcodec_alloc_context3(codec);if (!codec_ctx){av_log(NULL, AV_LOG_ERROR, "Could not allocate video codec context.....\n");CLEANUP(failure);return -1;}
- 為編碼器分配上下文,并設置編碼參數,如比特率、分辨率、幀率等。這些參數對最終視頻的質量和文件大小有直接影響。
// 設置編碼器的參數codec_ctx->bit_rate = 4000000; //與圖像質量直接掛鉤codec_ctx->height = height;codec_ctx->width = width;codec_ctx->time_base = (AVRational){1, 25};codec_ctx->framerate = (AVRational){25, 1};codec_ctx->gop_size = 10;codec_ctx->max_b_frames = 0; // 設置成0,可以減少編碼器負擔codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; // 使用YUV420P進行編碼
4. 打開編碼器
- 嘗試打開已配置的編碼器。如果編碼器無法打開,跳轉到
failure
。
// 打開編碼器if (avcodec_open2(codec_ctx, codec, NULL) < 0){av_log(NULL, AV_LOG_ERROR, "Could not open codec.....\n");CLEANUP(failure);return -1;}
5. 幀內存的分配和初始化
- 分配內存給原始幀
frame
和用于轉換的幀sws_frame
。如果分配失敗,進行資源清理并跳轉到failure
。 - 對這兩個幀進行格式設置和內存分配。
// 分配原始幀和轉換幀frame = av_frame_alloc();sws_frame = av_frame_alloc();if (!frame || !sws_frame){fprintf(stderr, "Could not allocate video frame\n");fclose(input_file);fclose(output_file);avcodec_free_context(&codec_ctx);if (frame)av_frame_free(&frame);if (sws_frame)av_frame_free(&sws_frame);return -1;}frame->format = codec_ctx->pix_fmt;frame->width = width;frame->height = height;ret = av_image_alloc(frame->data, frame->linesize, width, height, codec_ctx->pix_fmt, 32);if (ret < 0){CLEANUP(failure);return -1;}sws_frame->format = AV_PIX_FMT_YUV422P;sws_frame->width = width;sws_frame->height = height;ret = av_image_alloc(sws_frame->data, sws_frame->linesize, width, height, AV_PIX_FMT_YUV422P, 32);if (ret < 0){fprintf(stderr, "Could not allocate raw picture buffer for SWS frame\n");CLEANUP(failure);return -1;}
記得也要packet進行初始化,調了半個小時才發現,忘給AVPacket初始化了,一直報內存錯誤。。。。。。
// 分配數據包av_init_packet(&packet); // 初始化packetpacket.data = NULL;packet.size = 0;
6. 設置轉換上下文(SWS)
- 初始化用于像素格式轉換的
SwsContext
。這個上下文負責將YUV422P格式轉換成編碼器需要的YUV420P格式。
// 創建SWS上下文sws_ctx = sws_getContext(width, height, AV_PIX_FMT_YUV422P, width, height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);if (!sws_ctx){fprintf(stderr, "Could not initialize the conversion context\n");fclose(input_file);fclose(output_file);if (frame)av_freep(&frame->data[0]);if (sws_frame)av_freep(&sws_frame->data[0]);av_frame_free(&frame);av_frame_free(&sws_frame);avcodec_free_context(&codec_ctx);return -1;}
7. 讀取和轉換數據
- 從輸入文件中讀取YUV422P數據到
sws_frame
,然后使用sws_scale
函數進行格式轉換,并存儲到frame
。 - 如果讀取失敗,輸出錯誤信息并跳轉到
failure
。
fread(sws_frame->data[0], 1, width * height * 2, input_file)
sws_scale(sws_ctx, (const uint8_t *const *)sws_frame->data, sws_frame->linesize, 0, height, frame->data, frame->linesize);
8. 編碼過程
- 將轉換后的幀發送到編碼器。
// 數據送入編碼器ret = avcodec_send_frame(codec_ctx, frame);if (ret < 0){fprintf(stderr, "Error sending frame for encoding\n");CLEANUP(failure);}
- 循環調用
avcodec_receive_packet
來接收編碼后的數據,并將其寫入輸出文件。 - 在循環中,使用
av_packet_unref
來釋放已經寫入文件的數據包內存。
// 接收編碼后的數據包while (ret >= 0){ret = avcodec_receive_packet(codec_ctx, &packet);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)break;else if (ret < 0){fprintf(stderr, "Error during encoding\n");CLEANUP(failure);break;}// 寫入到文件fwrite(packet.data, 1, packet.size, output_file);av_packet_unref(&packet);}
- 最后,發送一個空幀到編碼器以刷新所有待處理的幀。
// 刷新編碼器ret = avcodec_send_frame(codec_ctx, NULL);if (ret < 0){fprintf(stderr, "Error sending flush packet to encoder\n");CLEANUP(failure);}while (ret >= 0){ret = avcodec_receive_packet(codec_ctx, &packet);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)break;else if (ret < 0){fprintf(stderr, "Error during encoding\n");CLEANUP(failure);break;}fwrite(packet.data, 1, packet.size, output_file);av_packet_unref(&packet);}
9. 資源清理
- 在正常執行結束和錯誤處理 (
failure
標簽) 中,關閉文件、釋放分配的內存和其他資源。
fclose(input_file);fclose(output_file);if (frame)av_freep(&frame->data[0]);if (sws_frame)av_freep(&sws_frame->data[0]);av_frame_free(&frame);av_frame_free(&sws_frame);avcodec_free_context(&codec_ctx);
4.完整示例代碼
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <libavutil/log.h>
// 錯誤處理和資源釋放
#define CLEANUP(label) \do \{ \goto label; \} while (0)int main(int argc, char *argv[])
{av_log_set_level(AV_LOG_DEBUG);av_log(NULL, AV_LOG_INFO, "FFMPEG DEBUG LOG BEGIN.....\n");if (argc < 5){printf("Usage: %s <input file> <width> <height> <output file>\n", argv[0]);return -1;}// 程序輸入const char *input_filename = argv[1];int width = atoi(argv[2]);int height = atoi(argv[3]);const char *output_filename = argv[4];// 相關變量初始化AVCodecContext *codec_ctx = NULL;AVCodec *codec = NULL;AVFrame *frame = NULL;AVFrame *sws_frame = NULL;AVPacket packet;struct SwsContext *sws_ctx = NULL;FILE *input_file = NULL;FILE *output_file = NULL;int ret;// 讀取YUV 文件input_file = fopen(input_filename, "rb");if (!input_file){fprintf(stderr, "Could not open input file '%s'\n", input_filename);CLEANUP(failure);return -1;}// 打開要寫入的文件output_file = fopen(output_filename, "wb");if (!output_file){fprintf(stderr, "Could not open output file '%s'\n", output_filename);CLEANUP(failure);return -1;}// 查找編碼器codec = avcodec_find_encoder(AV_CODEC_ID_H264);if (!codec){av_log(NULL, AV_LOG_ERROR, "H264 Codec not found.....\n");CLEANUP(failure);return -1;}// 分配編碼器上下文codec_ctx = avcodec_alloc_context3(codec);if (!codec_ctx){av_log(NULL, AV_LOG_ERROR, "Could not allocate video codec context.....\n");CLEANUP(failure);return -1;}// 設置編碼器的參數codec_ctx->bit_rate = 4000000; //與圖像質量codec_ctx->height = height;codec_ctx->width = width;codec_ctx->time_base = (AVRational){1, 25};codec_ctx->framerate = (AVRational){25, 1};codec_ctx->gop_size = 10;codec_ctx->max_b_frames = 0; // 設置成0,可以減少編碼器負擔codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; // 使用YUV420P進行編碼// 打開編碼器if (avcodec_open2(codec_ctx, codec, NULL) < 0){av_log(NULL, AV_LOG_ERROR, "Could not open codec.....\n");CLEANUP(failure);return -1;}// 分配原始幀和轉換幀frame = av_frame_alloc();sws_frame = av_frame_alloc();if (!frame || !sws_frame){fprintf(stderr, "Could not allocate video frame\n");fclose(input_file);fclose(output_file);avcodec_free_context(&codec_ctx);if (frame)av_frame_free(&frame);if (sws_frame)av_frame_free(&sws_frame);return -1;}frame->format = codec_ctx->pix_fmt;frame->width = width;frame->height = height;ret = av_image_alloc(frame->data, frame->linesize, width, height, codec_ctx->pix_fmt, 32);if (ret < 0){CLEANUP(failure);return -1;}sws_frame->format = AV_PIX_FMT_YUV422P;sws_frame->width = width;sws_frame->height = height;ret = av_image_alloc(sws_frame->data, sws_frame->linesize, width, height, AV_PIX_FMT_YUV422P, 32);if (ret < 0){fprintf(stderr, "Could not allocate raw picture buffer for SWS frame\n");CLEANUP(failure);return -1;}// 創建SWS上下文sws_ctx = sws_getContext(width, height, AV_PIX_FMT_YUV422P, width, height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);if (!sws_ctx){fprintf(stderr, "Could not initialize the conversion context\n");fclose(input_file);fclose(output_file);if (frame)av_freep(&frame->data[0]);if (sws_frame)av_freep(&sws_frame->data[0]);av_frame_free(&frame);av_frame_free(&sws_frame);avcodec_free_context(&codec_ctx);return -1;}// 分配數據包av_init_packet(&packet); // 初始化packetpacket.data = NULL;packet.size = 0;// 讀取YUV422P數據到sws_frame中if (fread(sws_frame->data[0], 1, width * height * 2, input_file) == width * height * 2){sws_scale(sws_ctx, (const uint8_t *const *)sws_frame->data, sws_frame->linesize, 0, height, frame->data, frame->linesize);frame->pts = 0; // 設置時間戳// 數據送入編碼器ret = avcodec_send_frame(codec_ctx, frame);if (ret < 0){fprintf(stderr, "Error sending frame for encoding\n");CLEANUP(failure);}// 接收編碼后的數據包while (ret >= 0){ret = avcodec_receive_packet(codec_ctx, &packet);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)break;else if (ret < 0){fprintf(stderr, "Error during encoding\n");CLEANUP(failure);break;}// 寫入到文件fwrite(packet.data, 1, packet.size, output_file);av_packet_unref(&packet);}}else{fprintf(stderr, "Error reading input file\n");}// 刷新編碼器ret = avcodec_send_frame(codec_ctx, NULL);if (ret < 0){fprintf(stderr, "Error sending flush packet to encoder\n");CLEANUP(failure);}while (ret >= 0){ret = avcodec_receive_packet(codec_ctx, &packet);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)break;else if (ret < 0){fprintf(stderr, "Error during encoding\n");CLEANUP(failure);break;}fwrite(packet.data, 1, packet.size, output_file);av_packet_unref(&packet);}// 釋放資源fclose(input_file);fclose(output_file);if (frame)av_freep(&frame->data[0]);if (sws_frame)av_freep(&sws_frame->data[0]);av_frame_free(&frame);av_frame_free(&sws_frame);avcodec_free_context(&codec_ctx);return 0;failure:if (input_file)fclose(input_file);if (output_file)fclose(output_file);if (frame)av_freep(&frame->data[0]);if (sws_frame)av_freep(&sws_frame->data[0]);av_frame_free(&frame);av_frame_free(&sws_frame);avcodec_free_context(&codec_ctx);return -1;
}
該程序輸出結果為.h264的碼流文件,這里并沒有添加文件容器(MP4/MKV/MOV)
可以直接調用ffplay進行播放,無需指定任何參數,因為已經封裝到了NAL單元中,查看是否壓編碼成功。
可以看出有了顯著的壓縮效果,如果就降低碼率,還可以進行縮小文件大小