FFmepg--25-h265解码yuv格式

文章目录

程序介绍

使用FFmpeg库解码视频文件(H.264/H.265/MPEG2)并输出YUV原始数据的程序

头文件引入和宏定义

引入了必要的标准库和FFmpeg库头文件。

定义了两个宏:

  • VIDEO_INBUF_SIZE(输入缓冲区大小)
  • VIDEO_REFILL_THRESH(重新填充阈值)

错误处理函数

av_get_err:将FFmpeg返回的错误码转换为错误信息字符串。

打印视频格式函数

print_video_format:打印AVFrame的宽度、高度和格式。

打印H.264和H.265的NALU类型函数

print_h264_nal_unit_typeprint_h265_nal_unit_type:分别用于打印H.264和H.265码流中的NALU类型和位置。

解码函数

decode:发送压缩数据包(AVPacket)到解码器,接收解码后的帧(AVFrame),并将YUV数据写入文件。

  • 使用avcodec_send_packet发送数据包。
  • 使用avcodec_receive_frame接收解码后的帧。
  • 如果是第一次收到帧,打印帧的格式。
  • 将YUV数据按照平面(Y、U、V)写入文件,注意使用linesize处理行对齐。

主函数

  • 参数检查:要求输入文件和输出文件。
  • 根据输入文件名的后缀判断视频编码格式(H.264、H.265或MPEG2)。
  • 查找对应的解码器。
  • 初始化解析器(用于解析裸流)。
  • 分配解码器上下文并打开解码器。
  • 打开输入文件和输出文件。
  • 分配输入缓冲区,并读取初始数据。
  • 循环读取输入文件,使用解析器解析出AVPacket,调用解码函数解码。
  • 在解析过程中,打印NALU的类型和位置(针对H.264和H.265)。
  • 当数据不足时,重新读取文件数据到缓冲区。
  • 冲刷解码器:发送空包处理缓冲区中剩余的数据。
  • 清理资源:关闭文件、释放内存等。

注意事项

写入YUV数据时提供了两种方法:

  • 正确方法:根据行偏移逐行写入。
  • 错误方法:直接写入整个平面(可能因行对齐填充导致问题)。
    注释中已说明错误原因。

该程序支持将H.264、H.265或MPEG2视频文件解码为YUV420P格式的原始数据。

提问

H.264 vs H.265 的主要不同?

  1. NAL单元类型解析方式不同
    c
    // H.264的NAL类型解析
    printf("nal_type:%d", pkt->data[4]&0x1f); // 取低5位

// H.265的NAL类型解析

printf("nal_type:%d", (pkt->data[4]&0x7e)>>1); // 取第2-7位,然后右移1位

  1. NAL头结构差异

H.264: NAL类型在第一个字节的低5位

H.265: NAL类型在第一个字节的第2-7位,需要更复杂的位操作

  1. 编码效率
    H.265相比H.264有更好的压缩效率,相同质量下码率可降低50%

H.265支持更高的分辨率和更复杂的预测模式

为什么在写入YUV420数据时,使用整体写入(一次fwrite)的方式是错误的,而使用逐行写入(循环fwrite)的方式是正确的?

错误写法中的 /4

c 复制代码
frame->width * frame->height / 4  // 为什么是/4?  
  • 宽度:U/V分量的水平分辨率是Y的一半 → /2
  • 高度:U/V分量的垂直分辨率是Y的一半 → /2
  • 总面积:(width/2) × (height/2) = width × height / 4

正确写法中的 /2

c 复制代码
// Y分量:全分辨率  
for(int j=0; j<frame->height; j++)           // 高度:不除  
    fwrite(..., frame->width, ...);          // 宽度:不除  

// U/V分量:半分辨率    
for(int j=0; j<frame->height/2; j++)         // 高度:/2  
    fwrite(..., frame->width/2, ...);        // 宽度:/2  

YUV420格式采用4:2:0采样:

  • Y分量:每个像素都有亮度值
  • U/V分量:每2×2的像素块共享1个U和1个V值

问题不在 /4 vs /2,而在于内存对齐:

假设 frame->width = 766, frame->height = 322

c 复制代码
// 错误写法计算的总字节数:  
Y: 766 × 322 = 246,652 字节  
U: 766 × 322 / 4 = 61,663 字节    
V: 766 × 322 / 4 = 61,663 字节  

// 但如果 linesize[1] = 384(对齐到32字节):  
实际U分量大小:384 × 161 = 61,824 字节  // 大于61,663!  
  • /4:用于计算U/V分量的总像素数(宽高各减半)
  • /2:用于确定循环次数和每行像素数(分别处理宽高减半)
  • 核心问题:错误写法忽略了linesize的内存对齐,直接假设数据连续存储,导致写入的数据可能包含填充字节或缺少有效数据。

code

c 复制代码
/**
* @projectName   07-05-decode_audio
* @brief         解码音频,主要的测试格式aac和mp3
* @author        Liao Qingfu
* @date          2020-01-16
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <libavutil/frame.h>
#include <libavutil/mem.h>

#include <libavcodec/avcodec.h>

#define VIDEO_INBUF_SIZE (1024*1024)
#define VIDEO_REFILL_THRESH 4096




static char err_buf[128] = {0};
static char* av_get_err(int errnum)
{
    av_strerror(errnum, err_buf, 128);
    return err_buf;
}

static void print_video_format(const AVFrame *frame)
{
    printf("width: %u\n", frame->width);
    printf("height: %u\n", frame->height);
    printf("format: %u\n", frame->format);// 格式需要注意
}

void print_h264_nal_unit_type(uint8_t *data, size_t size)
{
    int i = 0;
    while (i+3 < size ) {
        if(data[i] == 0 && data[i+1]==0 && data[i+2] == 0 && data[i+3] == 1 ) {
            i += 4;
            printf("%02x nal_type:%d, pos:%d\n", data[i], data[i]&0x1f, i);
            continue;
        }
        if(data[i] == 0 && data[i+1]==0 && data[i+2] == 1) {
            i += 3;
            printf("%02x nal_type:%d, pos:%d\n", data[i], data[i]&0x1f, i);
            continue;
        }
        i++;
    }
}


void print_h265_nal_unit_type(uint8_t *data, size_t size)
{
    int i = 0;
    while (i+3 < size ) {
        if(data[i] == 0 && data[i+1]==0 && data[i+2] == 0 && data[i+3] == 1 ) {
            i += 4;
            printf("%02x nal_type:%d, pos:%d\n", data[i],(data[i]&0x7e)>>1, i);
            continue;
        }
        if(data[i] == 0 && data[i+1]==0 && data[i+2] == 1) {
            i += 3;
            printf("%02x nal_type:%d, pos:%d\n",data[i], (data[i]&0x7e)>>1, i);
            continue;
        }
        i++;
    }
}

static void decode(AVCodecContext *dec_ctx, AVPacket *pkt, AVFrame *frame,
                   FILE *outfile)
{
    int ret;
    /* send the packet with the compressed data to the decoder */
    ret = avcodec_send_packet(dec_ctx, pkt);
    if(ret == AVERROR(EAGAIN))
    {
        fprintf(stderr, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
    }
    else if (ret < 0)
    {
        fprintf(stderr, "Error submitting the packet to the decoder, err:%s, pkt_size:%d\n",
                av_get_err(ret), pkt->size);
        return;
    }

    /* read all the output frames (infile general there may be any number of them */
    while (ret >= 0)
    {
        // 对于frame, avcodec_receive_frame内部每次都先调用
        ret = avcodec_receive_frame(dec_ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            return;
        else if (ret < 0)
        {
            fprintf(stderr, "Error during decoding\n");
            exit(1);
        }
        static int s_print_format = 0;
        if(s_print_format == 0)
        {
            s_print_format = 1;
            print_video_format(frame);
        }

        // 一般H264默认为 AV_PIX_FMT_YUV420P, 具体怎么强制转为 AV_PIX_FMT_YUV420P 在音视频合成输出的时候讲解
        // frame->linesize[1]  对齐的问题
        // 正确写法  linesize[]代表每行的字节数量,所以每行的偏移是linesize[]
        for(int j=0; j<frame->height; j++)
            fwrite(frame->data[0] + j * frame->linesize[0], 1, frame->width, outfile);
        for(int j=0; j<frame->height/2; j++)
            fwrite(frame->data[1] + j * frame->linesize[1], 1, frame->width/2, outfile);
        for(int j=0; j<frame->height/2; j++)
            fwrite(frame->data[2] + j * frame->linesize[2], 1, frame->width/2, outfile);

        // 错误写法 用source.200kbps.766x322_10s.h264/h265测试时可以看出该种方法是错误的
        //  写入y分量
//        fwrite(frame->data[0], 1, frame->width * frame->height,  outfile);//Y
//        // 写入u分量
//        fwrite(frame->data[1], 1, (frame->width) *(frame->height)/4,outfile);//U:宽高均是Y的一半
//        //  写入v分量
//        fwrite(frame->data[2], 1, (frame->width) *(frame->height)/4,outfile);//V:宽高均是Y的一半
    }
}
// 注册测试的时候不同分辨率的问题
// 提取H264: ffmpeg -i source.200kbps.768x320_10s.flv -vcodec libx264 -an -f h264 source.200kbps.768x320_10s.h264
// 提取MPEG2: ffmpeg -i source.200kbps.768x320_10s.flv -vcodec mpeg2video -an -f mpeg2video source.200kbps.768x320_10s.mpeg2
// 播放:ffplay -pixel_format yuv420p -video_size 768x320 -framerate 25  source.200kbps.768x320_10s.yuv
int main(int argc, char **argv)
{
    const char *outfilename;
    const char *filename;
    const AVCodec *codec;
    AVCodecContext *codec_ctx= NULL;
    AVCodecParserContext *parser = NULL;
    int len = 0;
    int ret = 0;
    FILE *infile = NULL;
    FILE *outfile = NULL;
    // AV_INPUT_BUFFER_PADDING_SIZE 在输入比特流结尾的要求附加分配字节的数量上进行解码
    uint8_t *inbuf = malloc(VIDEO_INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE);
    uint8_t *data = NULL;
    size_t   data_size = 0;
    AVPacket *pkt = NULL;
    AVFrame *decoded_frame = NULL;

    if (argc <= 2)
    {
        fprintf(stderr, "Usage: %s <input file> <output file>\n", argv[0]);
        exit(0);
    }
    filename    = argv[1];
    outfilename = argv[2];

    pkt = av_packet_alloc();
    enum AVCodecID video_codec_id = AV_CODEC_ID_H265;
    if(strstr(filename, "264") != NULL)
    {
        video_codec_id = AV_CODEC_ID_H264;
    }
    else if(strstr(filename, "mpeg2") != NULL)
    {
        video_codec_id = AV_CODEC_ID_MPEG2VIDEO;
    }
    else
    {
        printf("default codec id:%d\n", video_codec_id);
    }

    // 查找解码器
    codec = avcodec_find_decoder(video_codec_id);  // AV_CODEC_ID_H264
    if (!codec) {
        fprintf(stderr, "Codec not found\n");
        exit(1);
    }
    // 获取裸流的解析器 AVCodecParserContext(数据)  +  AVCodecParser(方法)
    parser = av_parser_init(codec->id);
    if (!parser) {
        fprintf(stderr, "Parser not found\n");
        exit(1);
    }
    // 分配codec上下文
    codec_ctx = avcodec_alloc_context3(codec);
    if (!codec_ctx) {
        fprintf(stderr, "Could not allocate audio codec context\n");
        exit(1);
    }

    // 将解码器和解码器上下文进行关联
    if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
        fprintf(stderr, "Could not open codec\n");
        exit(1);
    }

    // 打开输入文件
    infile = fopen(filename, "rb");
    if (!infile) {
        fprintf(stderr, "Could not open %s\n", filename);
        exit(1);
    }
    // 打开输出文件
    outfile = fopen(outfilename, "wb");
    if (!outfile) {
        av_free(codec_ctx);
        exit(1);
    }

    // 读取文件进行解码
    data      = inbuf;
    data_size = fread(inbuf, 1, VIDEO_INBUF_SIZE, infile);

    while (data_size > 0)
    {
        if (!decoded_frame)
        {
            if (!(decoded_frame = av_frame_alloc()))
            {
                fprintf(stderr, "Could not allocate audio frame\n");
                exit(1);
            }
        }

        ret = av_parser_parse2(parser, codec_ctx, &pkt->data, &pkt->size,
                               data, data_size,
                               AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
        if (ret < 0)
        {
            fprintf(stderr, "Error while parsing\n");
            exit(1);
        }
        data      += ret;   // 跳过已经解析的数据
        data_size -= ret;   // 对应的缓存大小也做相应减小

        if(video_codec_id == AV_CODEC_ID_H264) {
            if(pkt->data[0] == 0 && pkt->data[1]==0 && pkt->data[2] == 0 && pkt->data[3] == 1 )
                printf("\nstart_code:%02x %02x %02x %02x, nal_type:%d, size:%d\n", pkt->data[0],pkt->data[1],pkt->data[2],pkt->data[3], pkt->data[4]&0x1f,pkt->size);
            if(pkt->data[0] == 0 && pkt->data[1]==0 && pkt->data[2] == 1 )
                printf("\nstart_code:%02x %02x %02x, nal_type:%d, size:%d\n", pkt->data[0],pkt->data[1],pkt->data[2],  pkt->data[3]&0x1f,pkt->size);
            print_h264_nal_unit_type(pkt->data, pkt->size);
        }

        if(video_codec_id == AV_CODEC_ID_H265) {
            if(pkt->data[0] == 0 && pkt->data[1]==0 && pkt->data[2] == 0 && pkt->data[3] == 1 )
                printf("\nstart_code:%02x %02x %02x %02x, nal_type:%d, size:%d\n", pkt->data[0],pkt->data[1],pkt->data[2],pkt->data[3], (pkt->data[4]&0x7e)>>1,pkt->size);
            if(pkt->data[0] == 0 && pkt->data[1]==0 && pkt->data[2] == 1 )
                printf("\nstart_code:%02x %02x %02x, nal_type:%d, size:%d\n", pkt->data[0],pkt->data[1],pkt->data[2],  (pkt->data[3]&0x7e)>>1,pkt->size);
            print_h265_nal_unit_type(pkt->data, pkt->size);
        }

        if (pkt->size)
            decode(codec_ctx, pkt, decoded_frame, outfile);



        if (data_size < VIDEO_REFILL_THRESH)    // 如果数据少了则再次读取
        {
            memmove(inbuf, data, data_size);    // 把之前剩的数据拷贝到buffer的起始位置
            data = inbuf;
            // 读取数据 长度: VIDEO_INBUF_SIZE - data_size
            len = fread(data + data_size, 1, VIDEO_INBUF_SIZE - data_size, infile);
            if (len > 0)
                data_size += len;
        }
    }

    /* 冲刷解码器 */
    pkt->data = NULL;   // 让其进入drain mode
    pkt->size = 0;
    decode(codec_ctx, pkt, decoded_frame, outfile);

    fclose(outfile);
    fclose(infile);

    avcodec_free_context(&codec_ctx);
    av_parser_close(parser);
    av_frame_free(&decoded_frame);
    av_packet_free(&pkt);

    free(inbuf);

    printf("main finish, please enter Enter and exit\n");
    return 0;
}
相关推荐
Industio_触觉智能7 小时前
瑞芯微RK3562平台FFmpeg硬件编解码移植及性能测试实战攻略
ffmpeg·视频编解码·瑞芯微·rk3562·触觉智能
weixin_462446239 小时前
Python 使用 FFmpeg 给视频添加内嵌字幕(SRT)完整教程(含代码示例)
python·ffmpeg·音视频
百***35511 天前
从MySQL5.7平滑升级到MySQL8.0的最佳实践分享
ffmpeg
android_cai_niao1 天前
编译最新版本FFmpeg为so
ffmpeg·freetype·harfbuzz·drawtext·文字水印
feiyangqingyun1 天前
祖传独创/全网唯一/Qt结合ffmpeg实现读取ts文件节目流/动态切换多节目/实时切换不同轨道
qt·ffmpeg·节目流
i***58672 天前
从MySQL5.7平滑升级到MySQL8.0的最佳实践分享
ffmpeg
Everbrilliant892 天前
FFmpeg解码视频数据ANativeWindow播放
ffmpeg·音视频·ffmpeg视频解码·anativewindow·threadsafequeue·解码线程·渲染线程
hjjdebug3 天前
ffmpeg 问答系列->demux 部分
ffmpeg·基本概念·流参数
chjqxxxx3 天前
php使用ffmpeg实现视频随机截图并转成图片
ffmpeg·php·音视频