FFmpeg-视频解码-YUV数据

视频解码过程

视频解码过程

⼀般解出来的是YUV420p

FFmpeg视频解码流程

FFmpeg视频解码流程

关键函数

关键函数说明:

  • avcodec_find_decoder:根据指定的AVCodecID查找注册的解码器。
  • av_parser_init:初始化AVCodecParserContext。
  • avcodec_alloc_context3:为AVCodecContext分配内存。
  • avcodec_open2:打开解码器。
  • av_parser_parse2:解析获得⼀个Packet。
  • avcodec_send_packet:将AVPacket压缩数据给解码器。
  • avcodec_receive_frame:获取到解码后的AVFrame数据。
  • av_get_bytes_per_sample: 获取每个sample中的字节数。

avcodec编解码API介绍

avcodec_send_packet、avcodec_receive_frame的API是FFmpeg3版本加⼊的。为了正确

的使⽤它们,有必要阅读FFmpeg的⽂档说明(请点击链接)。

FFmpeg提供了两组函数,分别⽤于编码和解码:

  • 解码:avcodec_send_packet()、avcodec_receive_frame()。
  • 解码:avcodec_send_frame()、avcodec_receive_packet()。

API的设计与编解码的流程⾮常贴切

建议的使⽤流程如下:

  1. 像以前⼀样设置并打开AVCodecContext。

  2. 输⼊有效的数据:

    解码:调⽤avcodec_send_packet()给解码器传⼊包含原始的压缩数据的AVPacket对象。

    编码:调⽤ avcodec_send_frame()给编码器传⼊包含解压数据的AVFrame对象。

    两种情况下推荐AVPacket和AVFrame都使⽤refcounted(引⽤计数)的模式,否则libavcodec可能不得不对输⼊的数据进⾏拷⻉。

  3. 在⼀个循环体内去接收codec的输出,即周期性地调⽤avcodec_receive_*()来接收codec输出的数据:

    解码:调⽤avcodec_receive_frame(),如果成功会返回⼀个包含未压缩数据的AVFrame。

    编码:调⽤avcodec_receive_packet(),如果成功会返回⼀个包含压缩数据的AVPacket。

    反复地调⽤avcodec_receive_packet()直到返回 AVERROR(EAGAIN)或其他错误。返回 AVERROR(EAGAIN)错误表示codec需要新的输⼊来输出更多的数据。对于每个输⼊的packet或frame,codec⼀般会输出⼀个frame或packet,但是也有可能输出0个或者多于1个

  4. 流处理结束的时候需要flush(冲刷) codec。因为codec可能在内部缓冲多个frame或 packet,出于性能或其他必要的情况(如考虑B帧的情况)。 处理流程如下:

    调⽤avcodec_send_*()传⼊的AVFrame或AVPacket指针设置为NULL。 这将进⼊ draining mode(排⽔模式)。

    反复地调⽤avcodec_receive_*()直到返回AVERROR_EOF,该⽅法在draining mode 时不会返回AVERROR(EAGAIN)的错误,除⾮你没有进⼊draining mode。

    当重新开启codec时,需要先调⽤ avcodec_flush_buffers()来重置codec。

说明:

  1. 编码或者解码刚开始的时候,codec可能接收了多个输⼊的frame或packet后还没有输出 数据,直到内部的buffer被填充满。上⾯的使⽤流程可以处理这种情况。
  2. 理论上,只有在输出数据没有被完全接收的情况调⽤avcodec_send_*()的时候才可能会发 ⽣AVERROR(EAGAIN)的错误。你可以依赖这个机制来实现区别于上⾯建议流程的处理⽅ 式,⽐如每次循环都调⽤avcodec_send_*(),在出现AVERROR(EAGAIN)错误的时候再去调⽤avcodec_receive_*()
  3. 并不是所有的codec都遵循⼀个严格、可预测的数据处理流程,唯⼀可以保证的是调⽤ avcodec_send_()/avcodec_receive_()返回AVERROR(EAGAIN)的时候去 avcodec_receive_()/avcodec_send_()会成功,否则不应该返回AVERROR(EAGAIN) 的错误。**⼀般来说,任何codec都不允许⽆限制地缓存输⼊或者输出。
  4. 在同⼀个AVCodecContext上混合使⽤新旧API是不允许的,这将导致未定义的⾏为。

avcodec_send_packet

函数int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt); 作⽤:⽀持将裸流数据包送给解码器

警告

  • 输⼊的avpkt-data缓冲区必须⼤于AV_INPUT_PADDING_SIZE,因为优化的字节流读取 器必须⼀次读取32或者64⽐特的数据
  • 不能跟之前的API(例如avcodec_decode_video2)混⽤,否则会返回不可预知的错误

备注

  • 在将包发送给解码器的时候,AVCodecContext必须已经通过avcodec_open2打开

参数

  • avctx:解码上下⽂
  • avpkt:输⼊AVPakcet.通常情况下,输⼊数据是⼀个单⼀的视频帧或者⼏个完整的⾳频帧。调⽤者保留包的原有属性,解码器不会修改包的内容。解码器可能创建对包的引⽤。 如果包没有引⽤计数将拷⻉⼀份。跟以往的API不⼀样,输⼊的包的数据将被完全地消耗, 如果包含有多个帧要求多次调⽤avcodec_recvive_frame,直到avcodec_recvive_frame返回VERROR(EAGAIN)AVERROR_EOF。输⼊参数可以为 NULL,或者AVPacket的data域设置为NULL或者size域设置为0,表示将刷新所有的包,意味着数据流已经结束了。第⼀次发送刷新会总会成功,第⼆次发送刷新包是没有必要的,并且返回AVERROR_EOF,如果×××缓存了⼀些帧,返回⼀个刷新包,将会返回所有的解码包

返回值

  • 0: 表示成功
  • AVERROR(EAGAIN):当前状态不接受输⼊,⽤户必须先使⽤avcodec_receive_frame() 读 取数据帧;
  • AVERROR_EOF:解码器已刷新,不能再向其发送新包;
  • AVERROR(EINVAL):没有打开解码器,或者这是⼀个编码器,或者要求刷新;
  • AVERRO(ENOMEN):⽆法将数据包添加到内部队列。

avcodec_receive_frame

函数int avcodec_receive_frame ( AVCodecContext * avctx, AVFrame * frame )

作⽤:从解码器返回已解码的输出数据。

参数

  • avctx: 编解码器上下⽂
  • frame: 获取使⽤reference-counted机制的audio或者video帧(取决于解码器类型)。请注意,在执⾏其他操作之前,函数内部将始终先调⽤av_frame_unref(frame)。

返回值

  • 0: 成功,返回⼀个帧
  • AVERROR(EAGAIN): 该状态下没有帧输出,需要使⽤avcodec_send_packet发送新的packet到解码器
  • AVERROR_EOF: 解码器已经被完全刷新,不再有输出帧
  • AVERROR(EINVAL): 编解码器没打开
  • 其他<0的值: 具体查看对应的错误码

H264解码YUV实战

#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 20480
#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);// 格式需要注意
}

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测试时可以看出该种方法是错误的
        //  写入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[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_H264;
    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 (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);

    printf("main finish, please enter Enter and exit\n");
    return 0;
}

程序输入参数判断

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

根据文件后缀,判断AV_CODEC_ID_H264|AV_CODEC_ID_MPEG2VIDEO

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

根据codec->id初始化裸流解析器 AVCodecParserContext(数据) + AVCodecParser(方法)

parser = av_parser_init(codec->id);

分配解码器codec上下文

odec_ctx = avcodec_alloc_context3(codec);

打开解码器和解码器上下文进行关联

if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
    fprintf(stderr, "Could not open codec\n");
    exit(1);
}

读取原始裸流

data = inbuf;
data_size = fread(inbuf, 1, VIDEO_INBUF_SIZE, infile);

解析出一个完整的数据包

ret = av_parser_parse2(parser, codec_ctx, &pkt->data, &pkt->size,
                       data, data_size,
                       AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);

将数据包发送给解码器解码

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;
}

接收解码后的帧数据

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);
    }
    ......略
 }
while 循环读帧

while 循环读帧 根据ret == AVERROR(EAGAIN) || ret == AVERROR_EOF 判断

写入解码后的YUV数据/YUV420P格式写入文件

// 正确写法  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);

循环解码结束冲刷解码器

/* 冲刷解码器 */
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);

程序运行

width: 766
height: 322
format: 0
main finish, please enter Enter and exit

Process finished with exit code 0
ffplay -pixel_format yuv420p -video_size 766x322 -framerate 25 source.200kbps.766x322_10s.yuv

附录

分离H264或mpeg2video视频格式数据

提取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.mpeg2v

播放YUV

ffplay -pixel_format yuv420p -video_size 766x322 -framerate 25 source.200kbps.766x322_10s.yuv

FFmpeg命令查找重定向

⽐如我们在-f fmt打算指定格式时,怎么知道什么样的格式才是适合的format?

可以通过ffmpeg -formats | grep xx的⽅式去查找。

查找Audio的裸流解复⽤器

ffmpeg -formats | grep audio
 D  aea             MD STUDIO audio
 DE daud            D-Cinema audio
  E mp2             MP2 (MPEG audio layer 2)
 DE mp3             MP3 (MPEG audio layer 3)
 DE oma             Sony OpenMG audio
 D  wsaud           Westwood Studios audio
 D  wve             Psion 3 audio

查找Video的裸流解复⽤器

ffmpeg -formats | grep video

  libswresample   3.  7.100 /  3.  7.100
  libpostproc    55.  7.100 / 55.  7.100
  E a64             a64 - video for Commodore 64
 DE avs2            raw AVS2-P2/IEEE1857.4 video
 DE cavsvideo       raw Chinese AVS (Audio Video Standard) video
 D  cdxl            Commodore CDXL video
 DE h264            raw H.264 video
 DE hevc            raw HEVC video
 D  iv8             IndigoVision 8000 video
 DE m4v             raw MPEG-4 video
 DE mjpeg           raw MJPEG video
 D  mjpeg_2000      raw MJPEG 2000 video
  E mpeg1video      raw MPEG-1 video
  E mpeg2video      raw MPEG-2 video
 D  mpegvideo       raw MPEG video
  E null            raw null video
 DE rawvideo        raw video
 D  ser             SER (Simple uncompressed video format for astronomical capturing)
 DE vc1             raw VC-1 video
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容