FFmpeg4入门07:解码视频并保存为YUV格式文件

本文详细介绍了如何使用FFmpeg库解码视频并将其保存为YUV420P和YUV420SP两种格式的本地文件。通过C语言代码示例,展示了从打开视频文件、查找视频流、解码帧数据到转换格式并写入文件的完整过程。解码后的YUV文件可以使用ffplay播放验证效果。
摘要由CSDN通过智能技术生成

上一篇我们解码并保存了其中的几帧确保解码过程和结果是对的。本篇我们将解码整个视频并保存为标准的YUV格式(YUV格式具体信息详见YUV格式介绍),我们就选YUV420P(I420)作为输出格式。

保存文件需要对本地文件进行读写操作,那么首先要有文件操作指针,C为FILE,C++为iostream。

以C为例。

FILE *fp = fopen("result.yuv","w+b");

扩展名任意,只要数据格式对就可以了,最好是把数据格式标识出来,比如:1280x720_yuv420p.yuv

与上一篇文章相比,就是多了将像素值写入文件的部分。

解码流程图为:

flow

函数调用流程图为:

flow

YUV420P格式

YUV420P像素分为三个部分:Y/U/V,Y部分长度为width * height,U为width * height /4 ,V部分和U部分长度一样。(为什么会是这样,见YUV格式介绍)。

测试代码:

#include <stdio.h>
#include <stdlib.h>
#include "libavcodec/avcodec.h"
#include "libavfilter/avfilter.h"
#include "libavformat/avformat.h"
#include "libavutil/avutil.h"
#include "libavutil/ffversion.h"
#include "libswresample/swresample.h"
#include "libswscale/swscale.h"
#include "libpostproc/postprocess.h"

int main() {
    FILE *fp=fopen("result.yuv","w+b");
    if(fp==NULL){
        printf("Cannot open file.\n");
        return -1;
    }

    char filePath[]       = "/home/jackey/Videos/Sample.mkv";//文件地址
    int  videoStreamIndex = -1;//视频流所在流序列中的索引
    int ret=0;//默认返回值

    //需要的变量名并初始化
    AVFormatContext *fmtCtx=NULL;
    AVPacket *pkt =NULL;
    AVCodecContext *codecCtx=NULL;
    AVCodecParameters *avCodecPara=NULL;
    AVCodec *codec=NULL;
    AVFrame *yuvFrame = av_frame_alloc();

    do{
        //=========================== 创建AVFormatContext结构体 ===============================//
        //分配一个AVFormatContext,FFMPEG所有的操作都要通过这个AVFormatContext来进行
        fmtCtx = avformat_alloc_context();
        //==================================== 打开文件 ======================================//
        if ((ret=avformat_open_input(&fmtCtx, filePath, NULL, NULL)) != 0) {
            printf("cannot open video file\n");
            break;
        }

        //=================================== 获取视频流信息 ===================================//
        if ((ret=avformat_find_stream_info(fmtCtx, NULL)) < 0) {
            printf("cannot retrive video info\n");
            break;
        }

        //循环查找视频中包含的流信息,直到找到视频类型的流
        //便将其记录下来 保存到videoStreamIndex变量中
        for (unsigned int i = 0; i < fmtCtx->nb_streams; i++) {
            if (fmtCtx->streams[ i ]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
                videoStreamIndex = i;
                break;//找到视频流就退出
            }
        }

        //如果videoStream为-1 说明没有找到视频流
        if (videoStreamIndex == -1) {
            printf("cannot find video stream\n");
            break;
        }

        //打印输入和输出信息:长度 比特率 流格式等
        av_dump_format(fmtCtx, 0, filePath, 0);

        //=================================  查找解码器 ===================================//
        avCodecPara = fmtCtx->streams[ videoStreamIndex ]->codecpar;
        codec       = avcodec_find_decoder(avCodecPara->codec_id);
        if (codec == NULL) {
            printf("cannot find decoder\n");
            break;
        }
        //根据解码器参数来创建解码器内容
        codecCtx = avcodec_alloc_context3(codec);
        avcodec_parameters_to_context(codecCtx, avCodecPara);
        if (codecCtx == NULL) {
            printf("Cannot alloc context.");
            break;
        }

        //================================  打开解码器 ===================================//
        if ((ret=avcodec_open2(codecCtx, codec, NULL)) < 0) { // 具体采用什么解码器ffmpeg经过封装 我们无须知道
            printf("cannot open decoder\n");
            break;
        }

        int w=codecCtx->width;//视频宽度
        int h=codecCtx->height;//视频高度

        //=========================== 分配AVPacket结构体 ===============================//
        pkt = av_packet_alloc();                      //分配一个packet
        av_new_packet(pkt, codecCtx->width * codecCtx->height); //调整packet的数据

        //===========================  读取视频信息 ===============================//
        while (av_read_frame(fmtCtx, pkt) >= 0) { //读取的是一帧视频  数据存入一个AVPacket的结构中
            if (pkt->stream_index == videoStreamIndex){
                if (avcodec_send_packet(codecCtx, pkt) == 0){
                    while (avcodec_receive_frame(codecCtx, yuvFrame) == 0){
                        fwrite(yuvFrame->data[0],1,w*h,fp);//y
                        fwrite(yuvFrame->data[1],1,w*h/4,fp);//u
                        fwrite(yuvFrame->data[2],1,w*h/4,fp);//v
                    }
                }
            }
            av_packet_unref(pkt);//重置pkt的内容
        }
    }while(0);
    //===========================释放所有指针===============================//
    av_packet_free(&pkt);
    avcodec_close(codecCtx);
    avcodec_parameters_free(&avCodecPara);
    //avformat_close_input(&fmtCtx);
    //avformat_free_context(fmtCtx);
    av_frame_free(&yuvFrame);

    return ret;
}

因为FFmpeg软解后的帧格式为YUV420P,也就不用进行格式转换了,直接将解码后的数据写入本地就行了。

解码结果为:

-rw-r--r-- 1 jackey jackey     64928  45 19:45 main.o
-rw-r--r-- 1 jackey jackey     40882  45 19:34 Makefile
-rw-r--r-- 1 jackey jackey 277850880  45 19:45 result.yuv
-rw-r--r-- 1 jackey jackey   1657362  818  2017 Sample.mkv
-rwxr-xr-x 1 jackey jackey     66976  45 19:45 video_decode_mp42yuv

mkv格式视频大小为1.7MB,解码后的YUV格式视频大小为278MB。

我使用ffplay播放YUV格式视频:

ffplay -pixel_format yuv420p -s 1280x534 result.yuv

和源视频打开效果一样就是解码正常。

YUV420SP格式

因为FFmpeg使用CPU软解后的YUV格式为YUV420P,本部分在CPU软解码之后,我们将其转换为YUV420SP并写入本地文件。

#include <stdio.h>
#include <stdlib.h>
#include "libavcodec/avcodec.h"
#include "libavfilter/avfilter.h"
#include "libavformat/avformat.h"
#include "libavutil/avutil.h"
#include "libavutil/ffversion.h"
#include "libswresample/swresample.h"
#include "libswscale/swscale.h"
#include "libpostproc/postprocess.h"

int main() {
    FILE *fp=fopen("result.yuv","w+b");
    if(fp==NULL){
        printf("Cannot open file.\n");
        return -1;
    }

    char filePath[]       = "/home/jackey/Videos/Sample.mkv";//文件地址
    int  videoStreamIndex = -1;//视频流所在流序列中的索引
    int ret=0;//默认返回值

    //需要的变量名并初始化
    AVFormatContext *fmtCtx=NULL;
    AVPacket *pkt =NULL;
    AVCodecContext *codecCtx=NULL;
    AVCodecParameters *avCodecPara=NULL;
    AVCodec *codec=NULL;
    AVFrame *yuvFrame = av_frame_alloc();
    AVFrame *nv12Frame = av_frame_alloc();

    unsigned char *out_buffer;

    do{
        //=========================== 创建AVFormatContext结构体 ===============================//
        //分配一个AVFormatContext,FFMPEG所有的操作都要通过这个AVFormatContext来进行
        fmtCtx = avformat_alloc_context();
        //==================================== 打开文件 ======================================//
        if ((ret=avformat_open_input(&fmtCtx, filePath, NULL, NULL)) != 0) {
            printf("cannot open video file\n");
            break;
        }

        //=================================== 获取视频流信息 ===================================//
        if ((ret=avformat_find_stream_info(fmtCtx, NULL)) < 0) {
            printf("cannot retrive video info\n");
            break;
        }

        //循环查找视频中包含的流信息,直到找到视频类型的流
        //便将其记录下来 保存到videoStreamIndex变量中
        for (unsigned int i = 0; i < fmtCtx->nb_streams; i++) {
            if (fmtCtx->streams[ i ]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
                videoStreamIndex = i;
                break;//找到视频流就退出
            }
        }

        //如果videoStream为-1 说明没有找到视频流
        if (videoStreamIndex == -1) {
            printf("cannot find video stream\n");
            break;
        }

        //打印输入和输出信息:长度 比特率 流格式等
        av_dump_format(fmtCtx, 0, filePath, 0);

        //=================================  查找解码器 ===================================//
        avCodecPara = fmtCtx->streams[ videoStreamIndex ]->codecpar;
        codec       = avcodec_find_decoder(avCodecPara->codec_id);
        if (codec == NULL) {
            printf("cannot find decoder\n");
            break;
        }
        //根据解码器参数来创建解码器内容
        codecCtx = avcodec_alloc_context3(codec);
        avcodec_parameters_to_context(codecCtx, avCodecPara);
        if (codecCtx == NULL) {
            printf("Cannot alloc context.");
            break;
        }

        //================================  打开解码器 ===================================//
        if ((ret=avcodec_open2(codecCtx, codec, NULL)) < 0) { // 具体采用什么解码器ffmpeg经过封装 我们无须知道
            printf("cannot open decoder\n");
            break;
        }

        int w=codecCtx->width;//视频宽度
        int h=codecCtx->height;//视频高度

        //================================ 设置数据转换参数 ================================//
        struct SwsContext *img_ctx = sws_getContext(
                    codecCtx->width, codecCtx->height, codecCtx->pix_fmt, //源地址长宽以及数据格式
                    codecCtx->width, codecCtx->height, AV_PIX_FMT_NV12,  //目的地址长宽以及数据格式
                    SWS_BICUBIC, NULL, NULL, NULL);                       //算法类型  AV_PIX_FMT_YUVJ420P   AV_PIX_FMT_BGR24

        //==================================== 分配空间 ==================================//
        //一帧图像数据大小
        int numBytes = av_image_get_buffer_size(AV_PIX_FMT_NV12, codecCtx->width, codecCtx->height, 1);
        out_buffer = (unsigned char *)av_malloc(numBytes * sizeof(unsigned char));

        //=========================== 分配AVPacket结构体 ===============================//
        pkt = av_packet_alloc();                      //分配一个packet
        av_new_packet(pkt, codecCtx->width * codecCtx->height); //调整packet的数据
        //会将pFrameRGB的数据按RGB格式自动"关联"到buffer  即nv12Frame中的数据改变了
        //out_buffer中的数据也会相应的改变
        av_image_fill_arrays(nv12Frame->data, nv12Frame->linesize, out_buffer, AV_PIX_FMT_NV12,
                             codecCtx->width, codecCtx->height, 1);

        //===========================  读取视频信息 ===============================//
        while (av_read_frame(fmtCtx, pkt) >= 0) { //读取的是一帧视频  数据存入一个AVPacket的结构中
            if (pkt->stream_index == videoStreamIndex){
                if (avcodec_send_packet(codecCtx, pkt) == 0){
                    while (avcodec_receive_frame(codecCtx, yuvFrame) == 0){
                        sws_scale(img_ctx,
                                  (const uint8_t* const*)yuvFrame->data,
                                  yuvFrame->linesize,
                                  0,
                                  h,
                                  nv12Frame->data,
                                  nv12Frame->linesize);
                        fwrite(nv12Frame->data[0],1,w*h,fp);//y
                        fwrite(nv12Frame->data[1],1,w*h/2,fp);//uv
                    }
                }
            }
            av_packet_unref(pkt);//重置pkt的内容
        }
    }while(0);
    //===========================释放所有指针===============================//
    av_packet_free(&pkt);
    avcodec_close(codecCtx);
    avcodec_parameters_free(&avCodecPara);
    //avformat_close_input(&fmtCtx);
    //avformat_free_context(fmtCtx);
    av_frame_free(&yuvFrame);
    av_frame_free(&nv12Frame);

    av_free(out_buffer);

    return ret;
}

获得的结果也是278MB,使用ffplayer播放

ffplay -pixel_format nv12 -s 1280x534 result.yuv

如果显示结果和源视频文件一样,就表示解码正常。

result

GitHub项目地址(源代码):ffmpeg_Beginner中的7.1video_decode_mp42yuv420p和7.2video_decode_mp42yuv420sp

FFmpeg4入门07:解码视频并保存为YUV格式文件

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
以下是使用c语言和FFmpeg解码视频yuv420p格式并保存文件的代码示例: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <libavutil/imgutils.h> #include <libavutil/parseutils.h> #include <libavutil/opt.h> #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> int main(int argc, char *argv[]) { AVFormatContext *fmt_ctx = NULL; int ret; if (argc < 2) { fprintf(stderr, "Usage: %s <input file>\n", argv[0]); exit(1); } ret = avformat_open_input(&fmt_ctx, argv[1], NULL, NULL); if (ret < 0) { fprintf(stderr, "Could not open input file '%s'\n", argv[1]); exit(1); } ret = avformat_find_stream_info(fmt_ctx, NULL); if (ret < 0) { fprintf(stderr, "Could not find stream information\n"); exit(1); } int video_stream_index = -1; for (int i = 0; i < fmt_ctx->nb_streams; i++) { if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { video_stream_index = i; break; } } if (video_stream_index == -1) { fprintf(stderr, "Could not find video stream\n"); exit(1); } AVCodecParameters *codecpar = fmt_ctx->streams[video_stream_index]->codecpar; AVCodec *codec = avcodec_find_decoder(codecpar->codec_id); if (!codec) { fprintf(stderr, "Codec not found\n"); exit(1); } AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); if (!codec_ctx) { fprintf(stderr, "Could not allocate codec context\n"); exit(1); } ret = avcodec_parameters_to_context(codec_ctx, codecpar); if (ret < 0) { fprintf(stderr, "Could not copy codec parameters to codec context\n"); exit(1); } ret = avcodec_open2(codec_ctx, codec, NULL); if (ret < 0) { fprintf(stderr, "Could not open codec\n"); exit(1); } AVPacket pkt; av_init_packet(&pkt); pkt.data = NULL; pkt.size = 0; AVFrame *frame = av_frame_alloc(); if (!frame) { fprintf(stderr, "Could not allocate frame\n"); exit(1); } int frame_count = 0; while (av_read_frame(fmt_ctx, &pkt) >= 0) { if (pkt.stream_index == video_stream_index) { ret = avcodec_send_packet(codec_ctx, &pkt); if (ret < 0) { fprintf(stderr, "Error sending packet to decoder\n"); exit(1); } while (ret >= 0) { ret = avcodec_receive_frame(codec_ctx, frame); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { break; } else if (ret < 0) { fprintf(stderr, "Error during decoding\n"); exit(1); } char filename[256]; snprintf(filename, sizeof(filename), "frame-%d.yuv", frame_count); FILE *fp = fopen(filename, "wb"); if (!fp) { fprintf(stderr, "Could not open output file '%s'\n", filename); exit(1); } for (int i = 0; i < codec_ctx->height; i++) { fwrite(frame->data[0] + i * frame->linesize[0], 1, codec_ctx->width, fp); } for (int i = 0; i < codec_ctx->height / 2; i++) { fwrite(frame->data[1] + i * frame->linesize[1], 1, codec_ctx->width / 2, fp); } for (int i = 0; i < codec_ctx->height / 2; i++) { fwrite(frame->data[2] + i * frame->linesize[2], 1, codec_ctx->width / 2, fp); } fclose(fp); frame_count++; } } av_packet_unref(&pkt); } avcodec_free_context(&codec_ctx); avformat_close_input(&fmt_ctx); avformat_free_context(fmt_ctx); av_frame_free(&frame); return 0; } ``` 该示例代码从命令行读取一个输入文件名,并使用FFmpeg打开该文件。然后它搜索视频流并使用视频解码解码视频帧。对于每个解码帧,它将像素数据写入一个yuv文件。每个yuv文件的名称都是“frame-N.yuv”,其中N是帧计数器。最后,它释放所有使用的资源并退出。 请注意,此示例代码假定输入文件是可解码视频文件。如果您想处理其他类型的输入文件,请根据需要进行修改。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

幽迷狂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值