FFmpeg编码(YUV转H264)并改变视频分辨率示例

最简单的基于FFmpeg的编码器-纯净版(不包含libavformat)_雷霄骅的博客-CSDN博客_ffmpeg 编码器

初学音视频、ffmpeg。根据雷神的例子跑起来,调用libavcodec将YUV像素数据(YUV420P)编码为H.264码流,H.265为(HEVC)。

视频编码:

视频编码方式就是指通过特定的压缩技术,将某个视频格式的文件转换成另一种视频格式文件的方式。视频编码格式常见到的有:MPEG-2 TS、Divx、Xvid、H.264、WMV-HD和VC-1。

原始的图像和声音是需要占用很大的存储空间和带宽的,不适合运输和传送(例如例子的yuv数据),所以我们需要对原始图像和声音加工,压缩得更小。就是图像和声音的压缩方法。

 

编码流程:

  • 首先要有未压缩的 YUV 原始数据。
  • 其次要根据想要编码的格式选择特定的编码器。
  • 最后编码器的输出即为编码后的视频帧。

示例里编码完的ds.h264 文件在vlc 里播放可以看到分辨率为 480x272

FFmpeg编码(YUV转H264)并改变视频分辨率示例_第1张图片

现在研究把480x272 的输分辨率改为 320x270,需要使用以下函数:

FFmpeg中的 sws_scale() 函数主要是用来做视频像素格式和分辨率的转换。

// 初始化
struct SwsContext *sws_ctx(
            int srcW, /* 输入图像的宽度 */
            int srcH, /* 输入图像的宽度 */
            enum AVPixelFormat srcFormat, /* 输入图像的像素格式 */
            int dstW, /* 输出图像的宽度 */
            int dstH, /* 输出图像的高度 */
            enum AVPixelFormat dstFormat, /* 输出图像的像素格式 */
            int flags,/* 选择缩放算法(只有当输入输出图像大小不同时有效),一般选择SWS_FAST_BILINEAR */
            SwsFilter *srcFilter, /* 输入图像的滤波器信息, 若不需要传NULL */
            SwsFilter *dstFilter, /* 输出图像的滤波器信息, 若不需要传NULL */
            const double *param /* 特定缩放算法需要的参数(?),默认为NULL */
            );

// 转换的函数  
sws_scale(sws_ctx, src_frame->data, src_frame->linesize,  
          0, height, //源图像的高  
          dst_frame->data, dst_frame->linesize); 
    1.参数 SwsContext *c, 转换格式的上下文。也就是 sws_getContext 函数返回的结果。
    2.参数 const uint8_t *const srcSlice[], 输入图像的每个颜色通道的数据指针。其实就是解码后的        AVFrame中的data[]数组。因为不同像素的存储格式不同,所以srcSlice[]维数也有可能不同。
以YUV420P为例,它是planar格式,它的内存中的排布如下:
    YYYYYYYY UUUU VVVV
    使用FFmpeg解码后存储在AVFrame的data[]数组中时:
    data[0]——-Y分量, Y1, Y2, Y3, Y4, Y5, Y6, Y7, Y8……
    data[1]——-U分量, U1, U2, U3, U4……
    data[2]——-V分量, V1, V2, V3, V4……
    linesize[]数组中保存的是对应通道的数据宽度 ,
    linesize[0]——-Y分量的宽度
    linesize[1]——-U分量的宽度
    linesize[2]——-V分量的宽度

    而RGB24,它是packed格式,它在data[]数组中则只有一维,它在存储方式如下:
    data[0]: R1, G1, B1, R2, G2, B2, R3, G3, B3, R4, G4, B4……
    这里要特别注意,linesize[0]的值并不一定等于图片的宽度,有时候为了对齐各解码器的CPU,实际尺寸会大于图片的宽度,这点在我们编程时(比如OpengGL硬件转换/渲染)要特别注意,否则解码出来的图像会异常。

    3.参数const int srcStride[],输入图像的每个颜色通道的跨度。.也就是每个通道的行字节数,对应的是解码后的AVFrame中的linesize[]数组。根据它可以确立下一行的起始位置,不过stride和width不一定相同,这是因为:
a.由于数据帧存储的对齐,有可能会向每行后面增加一些填充字节这样 stride = width + N;
b.packet色彩空间下,每个像素几个通道数据混合在一起,例如RGB24,每个像素3字节连续存放,因此下一行的位置需要跳过3*width字节。

    4.参数int srcSliceY, int srcSliceH,定义在输入图像上处理区域,srcSliceY是起始位置,srcSliceH是处理多少行。如果srcSliceY=0,srcSliceH=height,表示一次性处理完整个图像。这种设置是为了多线程并行,例如可以创建两个线程,第一个线程处理 [0, h/2-1]行,第二个线程处理 [h/2, h-1]行。并行处理加快速度。
5.参数uint8_t *const dst[], const int dstStride[]定义输出图像信息(输出的每个颜色通道数据指针,每个颜色通道行字节数)


// 释放sws_scale
sws_freeContext(sws_ctx); 

在雷神代码基础上修改主要加了以上函数,加了一些不懂函数的注释。现在把代码贴出来,主要要区分开输入数据和输出数据:

#include 


#include 

#define __STDC_CONSTANT_MACROS

#ifdef _WIN32
//Windows
extern "C"
{
#include "libavutil/opt.h"
#include "libavcodec/avcodec.h"
#include "libavutil/imgutils.h"
};
#else
//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include 
#include 
#include 
#ifdef __cplusplus
};
#endif
#endif

//test different codec
#define TEST_H264  1
#define TEST_HEVC  0// H265


int main(int argc, char* argv[])
{
    AVCodec *pCodec;
    AVCodecContext *pCodecCtx= NULL;
    int i, ret, got_output;
    FILE *fp_in;
    FILE *fp_out;
    //    AVFrame *src_frame;//存储一帧未编码的像素数据。
    AVFrame *src_frame, *dst_frame;// 输入和输出
    AVPacket pkt;//存储一帧压缩编码数据。
    int y_size;
    int framecnt=0;

    char filename_in[]="../ds_480x272.yuv";

    static struct SwsContext *img_convert_ctx;


#if TEST_HEVC
    AVCodecID codec_id=AV_CODEC_ID_HEVC;
    char filename_out[]="ds.hevc";
#else
    AVCodecID codec_id=AV_CODEC_ID_H264;
    char filename_out[]="ds.h264";
#endif

    int src_w = 480, src_h = 272;
    int dst_w = 320, dst_h = 270;
    int framenum = 100;

    avcodec_register_all();//注册所有编解码器相关的组件。av_register_all():注册所有的编解码器,复用/解复用器等等组件

    pCodec = avcodec_find_encoder(codec_id);//查找ID值为AV_CODEC_ID_H264的AAC编码器
    if (!pCodec) {
        printf("Codec not found\n");
        return -1;
    }
    pCodecCtx = avcodec_alloc_context3(pCodec); // 为AVCodecContext分配内存 创建AVCodecContext结构体。AVCodecContext中很多的参数是编码的时候使用
    if (!pCodecCtx) {
        printf("Could not allocate video codec context\n");
        return -1;
    }
    pCodecCtx->bit_rate = 400000;//平均比特率 目标的码率,即采样的码率;显然,采样码率越大,视频大小越大
    pCodecCtx->width = dst_w;//如果是视频的话,代表宽和高 编码目标的视频帧大小,以像素为单位
    pCodecCtx->height = dst_h;
    //帧率的基本单位,我们用分数来表示,
    //用分数来表示的原因是,有很多视频的帧率是带小数的eg:NTSC 使用的帧率是29.97
    pCodecCtx->time_base.num=1; //根据该参数,可以把PTS转化为实际的时间(单位为秒s)
    pCodecCtx->time_base.den=25;
    pCodecCtx->gop_size = 10;//一组图片中的图片数
    //两个非B帧之间允许出现多少个B帧数
    //设置0表示不使用B帧
    //b 帧越多,图片越小
    pCodecCtx->max_b_frames = 1;
    pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;//像素格式 也就是说采用什么样的色彩空间来表明一个像素点
    //设置显示的率
    pCodecCtx->framerate = {10, 1};
    //量化参数设置(影响视频清晰度) 越小越清晰
    pCodecCtx->qmax = 51;
    pCodecCtx->qmin = 10;

    if (codec_id == AV_CODEC_ID_H264)
        av_opt_set(pCodecCtx->priv_data, "preset", "slow", 0);

    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) { //打开编码器。
        printf("Could not open codec\n");
        return -1;
    }

    src_frame = av_frame_alloc();//分配AVFrame AVFrame结构体一般用于存储原始数据(即非压缩数据,例如对视频来说是YUV,RGB,对音频来说是PCM
    if (!src_frame) {
        printf("Could not allocate video frame\n");
        return -1;
    }
    src_frame->format = pCodecCtx->pix_fmt; //帧的格式
    src_frame->width  = src_w;
    src_frame->height = src_h;//视频帧宽和高(1920x1080,1280x720...)



    dst_frame = av_frame_alloc();
    dst_frame->width  = dst_w;
    dst_frame->height = dst_h;  //初始化一个SwsContext
    img_convert_ctx = sws_getContext(src_frame->width, src_frame->height, \
                                     pCodecCtx->pix_fmt, dst_frame->width, dst_frame->height, \
                                     pCodecCtx->pix_fmt, 0, nullptr, nullptr, nullptr);

//    int dst_bytes_num = avpicture_get_size(pCodecCtx->pix_fmt, dst_frame->width, dst_frame->height);//计算这个格式的图片,需要多少字节来存储
//    uint8_t* dst_buff = (uint8_t *)av_malloc(dst_bytes_num * sizeof(uint8_t));//申请空间来存放图片数据。包含源数据和目标数据
    //    前面的av_frame_alloc函数,只是为这个AVFrame结构体分配了内存,
    //    而该类型的指针指向的内存还没分配。这里把av_malloc得到的内存和AVFrame关联起来。
    //    当然,其还会设置AVFrame的其他成员
//    avpicture_fill((AVPicture *)dst_frame, dst_buff, pCodecCtx->pix_fmt, dst_frame->width, dst_frame->height);//ffmpeg4.2.2中就已经被抛弃了, 取而代之的是av_image_fill_arrays()

//以上注释的代码也能实现 av_image_alloc
    av_image_alloc(dst_frame->data, dst_frame->linesize, dst_frame->width, dst_frame->height,
                         pCodecCtx->pix_fmt, 16);

    ret = av_image_alloc(src_frame->data, src_frame->linesize, src_frame->width, src_frame->height,
                         pCodecCtx->pix_fmt, 16);//按照指定的宽、高、像素格式来分析图像内存
    //    pointers[4]:保存图像通道的地址。如果是RGB,则前三个指针分别指向R,G,B的内存地址。第四个指针保留不用
    //    linesizes[4]:保存图像每个通道的内存对齐的步长,即一行的对齐内存的宽度,此值大小等于图像宽度。
    //     w:                 要申请内存的图像宽度。
    //     h:                  要申请内存的图像高度。
    //     pix_fmt:        要申请内存的图像的像素格式。
    //     align:            用于内存对齐的值。
    //     返回值:所申请的内存空间的总大小。如果是负值,表示申请失败。
    if (ret < 0) {
        printf("Could not allocate raw picture buffer\n");
        return -1;
    }
    //Input raw data
    fp_in = fopen(filename_in, "rb");//使用给定的模式 mode 打开 filename 所指向的文件。 rb+读写打开一个二进制文件,允许读数据。
    if (!fp_in) {
        printf("Could not open %s\n", filename_in);
        return -1;
    }
    //Output bitstream
    fp_out = fopen(filename_out, "wb");//wb 只写打开或新建一个二进制文件;只允许写数据。
    if (!fp_out) {
        printf("Could not open %s\n", filename_out);
        return -1;
    }

    y_size = src_frame->width * src_frame->height;
    //Encode
    for (i = 0; i < framenum; i++) {
        av_init_packet(&pkt);//设置默认值
        pkt.data = NULL;    // packet data will be allocated by the encoder
        pkt.size = 0;   //从给定流 fp_in 读取数据到 src_frame->data[0] 所指向的数组中。

        //Read raw YUV data
        if (fread(src_frame->data[0],1,y_size,fp_in)<= 0||		// Y
                fread(src_frame->data[1],1,y_size/4,fp_in)<= 0||	// U
                fread(src_frame->data[2],1,y_size/4,fp_in)<= 0){	// V
            return -1;
        }else if(feof(fp_in)){//测试给定流 fp_in 的文件结束标识符。
            break;
        }

        sws_scale(img_convert_ctx, (uint8_t const * const *)src_frame->data,
                  src_frame->linesize, 0, src_frame->height, dst_frame->data, dst_frame->linesize);


        src_frame->pts = i;
        /* encode the image */
        ret = avcodec_encode_video2(pCodecCtx, &pkt, dst_frame, &got_output);//编码一帧数据。要传转换完的 dst_frame

        if (ret < 0) {
            printf("Error encoding frame\n");
            return -1;
        }
        if (got_output) {
            printf("Succeed to encode frame: %5d\tsize:%5d\n",framecnt,pkt.size);
            framecnt++;
            fwrite(pkt.data, 1, pkt.size, fp_out);//把 pkt.data 所指向的数组中的数据写入到给定流 fp_out 中
            av_free_packet(&pkt);//清空pkt中data以及buf的内容,并没有把pkt的指针清空
        }
    }
    //Flush Encoder
    for (got_output = 1; got_output; i++) {
        ret = avcodec_encode_video2(pCodecCtx, &pkt, NULL, &got_output);
        if (ret < 0) {
            printf("Error encoding frame\n");
            return -1;
        }
        if (got_output) {
            printf("Flush Encoder: Succeed to encode 1 frame!\tsize:%5d\n",pkt.size);
            fwrite(pkt.data, 1, pkt.size, fp_out);
            av_free_packet(&pkt);
        }
    }

    fclose(fp_out);//关闭流 fp_out。刷新所有的缓冲区。
    avcodec_close(pCodecCtx);//关闭编码器
    av_free(pCodecCtx);//释放已分配av_malloc() AVCodecContext
    av_freep(&src_frame->data[0]);//释放并清理指针
    av_frame_free(&src_frame);//释放AVFrame
    av_frame_free(&dst_frame);//释放AVFrame
	sws_freeContext(img_convert_ctx);	
    return 0;
}

下面是转换完的视频: 

FFmpeg编码(YUV转H264)并改变视频分辨率示例_第2张图片

 参考文档:

FFmpeg编码基础流程_zhaodb_的博客-CSDN博客_ffmpeg编码流程 FFmpeg: FFmepg中的sws_scale() 函数分析 - 夜行过客 - 博客园

图像视频编码和FFmpeg(3)-----用FFmpeg进行图像格式转换和AVFrame简介 avpicture_fill - bw_0927 - 博客园ffmpeg的API函数用法 :sws_scale函数的用法-具体应用 - 怀想天空2013 - 博客园图像视频编码和FFmpeg(3)-----用FFmpeg进行图像格式转换和AVFrame简介 avpicture_fill - bw_0927 - 博客园

你可能感兴趣的:(ffmpeg,音视频,ffmpeg)