首页 > 其他分享 >音视频开发—使用FFmpeg从纯H264码流中提取图片 C语言实现

音视频开发—使用FFmpeg从纯H264码流中提取图片 C语言实现

时间:2024-07-14 11:55:32浏览次数:12  
标签:码流 FFmpeg frame ctx ret 音视频 codec av NULL

文章目录

从纯H.264码流中提取图片的过程包括解码JPEG编码两个主要步骤,以下是详细阐述

1.H264码流文件解码流程

关键流程

  • 查找编解码器

  • 初始化编解码器上下文—主要功能包括:

    存储编解码器参数:包括视频宽度、高度、像素格式、音频采样率、通道数等参数。

    存储编解码器状态:包括内部缓冲区、解码器状态、错误信息等。

    配置编解码器:允许用户通过设置上下文的属性来配置编解码器的行为。

    管理输入输出数据:负责管理编解码器的输入数据(如压缩视频流)和输出数据(如解码后的帧)。

  • 打开编解码器

  • 编解码

详细解码流程

在这里插入图片描述

详细步骤解析

初始化FFmpeg库

av_register_all();

打开输入文件

if ((ret = avformat_open_input(&format_ctx, input_filename, NULL, NULL)) < 0) {
    fprintf(stderr, "Could not open input file '%s'\n", input_filename);
    return ret;
}

找到输入文件的流信息

if ((ret = avformat_find_stream_info(format_ctx, NULL)) < 0) {
    fprintf(stderr, "Failed to retrieve input stream information\n");
    return ret;
}

找到视频流索引

for (int i = 0; i < format_ctx->nb_streams; i++) {
    if (format_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");
    return -1;
}

初始化解码器

codec = avcodec_find_decoder(format_ctx->streams[video_stream_index]->codecpar->codec_id);
if (!codec) {
    fprintf(stderr, "Could not find decoder\n");
    return -1;
}

codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
    fprintf(stderr, "Could not allocate video codec context\n");
    return -1;
}

if ((ret = avcodec_parameters_to_context(codec_ctx, format_ctx->streams[video_stream_index]->codecpar)) < 0) {
    fprintf(stderr, "Could not copy codec parameters to context\n");
    return ret;
}

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

分配帧和初始化SWS上下文

frame = av_frame_alloc();
if (!frame) {
    fprintf(stderr, "Could not allocate frame\n");
    return -1;
}

sws_ctx = sws_getContext(codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt,
                         codec_ctx->width, codec_ctx->height, AV_PIX_FMT_YUV420P,
                         SWS_BILINEAR, NULL, NULL, NULL);

读取帧并解码

while (av_read_frame(format_ctx, &packet) >= 0) {
    if (packet.stream_index == video_stream_index) {
        ret = avcodec_send_packet(codec_ctx, &packet);
        if (ret < 0) {
            fprintf(stderr, "Error sending packet for decoding\n");
            break;
        }

        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");
                return ret;
            }
            //处理帧,在这对Frame进行图片的编码
        }
    }
    av_packet_unref(&packet);
}

2.JPEG编码流程

与H264解码流程类似:均需要查找相关的编解码器,初始化上下文,打开编码器

详细编码流程

在这里插入图片描述

详细步骤解析

  1. 定义文件名

    • 使用snprintf生成保存JPEG图片的文件名,格式为frame<number>.jpg
    char filename[1024];
    snprintf(filename, sizeof(filename), "frame%d.jpg", frame_number);
    
  2. 寻找MJPEG编码器

    • 使用avcodec_find_encoder函数查找MJPEG编码器。如果找不到,记录错误日志并返回。
    AVCodec *jpeg_codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG);
    if (!jpeg_codec) {
        av_log(NULL, AV_LOG_ERROR, "Could not find JPEG codec!\n");
        return;
    }
    
  3. 分配编码器上下文

    • 使用avcodec_alloc_context3函数为MJPEG编码器分配一个编码器上下文。如果分配失败,记录错误日志并返回。
    AVCodecContext *codec_ctx = avcodec_alloc_context3(jpeg_codec);
    if (!codec_ctx) {
        av_log(NULL, AV_LOG_ERROR, "Could not alloc codec_ctx !\n");
        return;
    }
    
  4. 设置编码器参数

    • 设置编码器的像素格式、视频高度、宽度和时间基准。
    codec_ctx->pix_fmt = AV_PIX_FMT_YUVJ420P;
    codec_ctx->height = frame->height;
    codec_ctx->width = frame->width;
    codec_ctx->time_base = (AVRational){1, 25};
    
  5. 打开编码器

    • 使用avcodec_open2函数打开编码器。如果打开失败,记录错误日志并返回。
    int ret = avcodec_open2(codec_ctx, jpeg_codec, NULL);
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Could not open jpeg codec !\n");
        avcodec_free_context(&codec_ctx);
        return;
    }
    
  6. 初始化数据包

    • 初始化一个AVPacket来存储编码后的数据。
    AVPacket packet;
    av_init_packet(&packet);
    packet.data = NULL;
    packet.size = 0;
    
  7. 发送帧到编码器

    • 使用avcodec_send_frame函数将帧发送到编码器。如果发送失败,记录错误日志并返回。
    ret = avcodec_send_frame(codec_ctx, frame);
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Error sending frame to JPEG codec!\n");
        avcodec_free_context(&codec_ctx);
        return;
    }
    
  8. 接收编码后的数据包

    • 使用avcodec_receive_packet函数接收编码后的数据包。如果接收失败,记录错误日志并返回。
    ret = avcodec_receive_packet(codec_ctx, &packet);
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Error receiving packet from JPEG codec !\n");
        avcodec_free_context(&codec_ctx);
        return;
    }
    
  9. 写入文件

    • 打开一个文件,并将编码后的数据写入文件。如果文件打开失败,记录错误日志并返回。
    FILE *pic = fopen(filename, "wb");
    if (!pic) {
        av_log(NULL, AV_LOG_ERROR, "Could not open %s\n", filename);
        avcodec_free_context(&codec_ctx);
        av_packet_unref(&packet);
        return;
    }
    fwrite(packet.data, 1, packet.size, pic);
    fclose(pic);
    
  10. 记录成功日志

    • 记录成功写入文件的日志信息。
    av_log(NULL, AV_LOG_INFO, "write %s to jpeg success!\n", filename);
    
  11. 释放资源

    • 释放数据包和编码器上下文。
    av_packet_unref(&packet);
    avcodec_free_context(&codec_ctx);
    

3.完整示例代码

#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <libavutil/log.h>

/// @brief 编码为图片
/// @param frame
/// @param frame_number
void save_frame_as_jpeg(AVFrame *frame, int frame_number)
{
    int ret;
    char filename[1024];
    snprintf(filename, sizeof(filename), "frame%d.jpg", frame_number);
    // 寻找MJPEG编码器
    AVCodec *jpeg_codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG);
    if (!jpeg_codec)
    {
        av_log(NULL, AV_LOG_ERROR, "Could not find JPEG codec!\n");
        return;
    }
    AVCodecContext *codec_ctx = avcodec_alloc_context3(jpeg_codec);
    if (!codec_ctx)
    {
        av_log(NULL, AV_LOG_ERROR, "Could not alloc codec_ctx !\n");
        return;
    }

    // 通过 AVCodecContext 设置jpeg codec 的参数
    codec_ctx->pix_fmt = AV_PIX_FMT_YUVJ420P;
    codec_ctx->height = frame->height;
    codec_ctx->width = frame->width;
    codec_ctx->time_base = (AVRational){1, 25};

    // 打开编码器
    ret = avcodec_open2(codec_ctx, jpeg_codec, NULL);
    if (ret < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Could not open jpeg codec !\n");
        return;
    }

    // 初始化数据包
    AVPacket packet;
    av_init_packet(&packet);
    packet.data = NULL;
    packet.size = 0;

    // 数据送入到编码器
    ret = avcodec_send_frame(codec_ctx, frame);
    if (ret < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Error sending frame to JPEG codec!\n");
        avcodec_free_context(&codec_ctx);
        return;
    }
    //接收编码后的数据包
    ret = avcodec_receive_packet(codec_ctx,&packet);
    if(ret<0){
        av_log(NULL, AV_LOG_ERROR, "Error receiving packet from JPEG codec !\n");
        avcodec_free_context(&codec_ctx);
        return;
    }

    //写入到图片文件
    FILE *pic =fopen(filename,"wb");
    if(!pic){
         av_log(NULL, AV_LOG_ERROR, "Could not open %s\n",filename);
         avcodec_free_context(&codec_ctx);
         av_packet_unref(&packet);
         return;
    }
    fwrite(packet.data, 1, packet.size, pic);
    fclose(pic);

    av_log(NULL, AV_LOG_INFO, "write %s to jpeg success!\n");
    av_packet_unref(&packet);
    avcodec_free_context(&codec_ctx);


}

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 < 2)
    {
        printf("Usage: %s <input file>\n", argv[0]);
        return -1;
    }
    const char *input_filename = argv[1]; // 输入H264码流文件名
    AVFormatContext *if_ctx = NULL;
    AVCodecContext *codec_ctx = NULL;
    AVCodec *codec = NULL;
    AVFrame *frame = NULL;
    AVPacket packet;
    struct SwsContext *sws_ctx = NULL; // 转换上下文
    int video_stream_index = -1;
    int ret = 0;
    int frame_count = 0;
    av_register_all(); // 初始化库
    ret = avformat_open_input(&if_ctx, input_filename, NULL, NULL);
    if (ret < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Open input file failed! %s\n", av_err2str(ret));
        return ret;
    }
    // 寻找流信息
    ret = avformat_find_stream_info(if_ctx, NULL);
    if (ret < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Find stream info  failed! %s\n", av_err2str(ret));
        return ret;
    }
    // 寻找视频流索引
    for (size_t i = 0; i < if_ctx->nb_streams; i++)
    {
        if (if_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            video_stream_index = i;
            break;
        }
    }
    if (video_stream_index == -1)
    {
        av_log(NULL, AV_LOG_ERROR, "Find video stream index  failed! \n");
        return ret;
    }

    // 寻找编码器
    codec = avcodec_find_decoder(if_ctx->streams[video_stream_index]->codecpar->codec_id);
    if (!codec)
    {
        av_log(NULL, AV_LOG_ERROR, "Find codec decoder  failed! \n");
        return ret;
    }
    // 分配解码器上下文
    codec_ctx = avcodec_alloc_context3(codec);
    if (!codec_ctx)
    {
        av_log(NULL, AV_LOG_ERROR, "Alloc codec ctx  failed!\n");
        return ret;
    }
    ret = avcodec_parameters_to_context(codec_ctx, if_ctx->streams[video_stream_index]->codecpar);
    if (ret < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Could not copy codec parameters to context %s\n", av_err2str(ret));
        return ret;
    }

    // 打开解码器
    ret = avcodec_open2(codec_ctx, codec, NULL);
    if (ret < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Could not open codec %s\n", av_err2str(ret));
        return ret;
    }

    frame = av_frame_alloc();
    if (!frame)
    {
        av_log(NULL, AV_LOG_ERROR, "Could not alloc frame\n");
        return -1;
    }

    sws_ctx = sws_getContext(codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt,
                             codec_ctx->width, codec_ctx->height, AV_PIX_FMT_YUV420P,
                             SWS_BILINEAR, NULL, NULL, NULL); // ;;用来转换帧的像素格式
    while (av_read_frame(if_ctx, &packet) >= 0)
    {
        if (packet.stream_index == video_stream_index)
        {
            ret = avcodec_send_packet(codec_ctx, &packet);
            if (ret < 0)
            {
                av_log(NULL, AV_LOG_ERROR, "Could not avcodec_send_packet %s\n", av_err2str(ret));
                break;
            }

            // 如果有B帧,可能有多个帧,因此使用while
            while (ret >= 0)
            {
                ret = avcodec_receive_frame(codec_ctx, frame);
                if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
                    break;
                else if (ret < 0)
                {
                    av_log(NULL, AV_LOG_ERROR, "Error during decoding %s\n", av_err2str(ret));
                    break;
                }

                // 处理解码后的帧,保存图片

                save_frame_as_jpeg(frame,frame_count);


                frame_count++;       


                av_log(NULL, AV_LOG_DEBUG, "Decode success!\n");
            }
        }
        av_packet_unref(&packet);
    }
    av_frame_free(&frame);
    avcodec_free_context(&codec_ctx);
    avformat_close_input(&if_ctx);
    av_log(NULL, AV_LOG_INFO, "FFMPEG DEBUG LOG END.....\n");
    return 0;
}

4.效果展示

当读取一个H264文件之后,将会逐帧对H264数据进行解码,并且对解码后的关键帧进行编码,成功之后将会提取出所有的图片

在这里插入图片描述

标签:码流,FFmpeg,frame,ctx,ret,音视频,codec,av,NULL
From: https://blog.csdn.net/weixin_46999174/article/details/140378981

相关文章

  • FFmpeg开发笔记(三十八)APP如何访问SRS推流的RTMP直播地址
    ​《FFmpeg开发实战:从零基础到短视频上线》一书在第10章介绍了轻量级流媒体服务器MediaMTX,通过该工具可以测试RTSP/RTMP等流媒体协议的推拉流。不过MediaMTX的功能实在是太简单了,无法应用于真实直播的生产环境,真正能用于生产环境的流媒体服务器还要看SRS或者ZLMediaKit。SRS是一......
  • FFmpeg开发笔记(三十七)分析SRS对HLS协议里TS包的插帧操作
    ​《FFmpeg开发实战:从零基础到短视频上线》一书的“2.1.2 音视频文件的封装格式”介绍了视频流的PS格式和TS格式。由于TS包的长度固定,从TS流的任一片段开始都能独立解码,因此可以把TS当成音视频文件的封装格式。鉴于TS包的独立解码特性,HLS协议引入了TS格式作为传输单元。HLS协......
  • 音视频开发基础知识
    视频解码解复用(Demux):解复用也可叫解封装。这里有一个概念叫封装格式,封装格式指的是音视频的组合格式,常见的有mp4、flv、mkv等。通俗来讲,封装是将音频流、视频流、字幕流以及其他附件按一定规则组合成一个封装的产物。而解封装起着与封装相反的作用,将一个流媒体文件拆解成音频......
  • ffmpeg命令合并视频,点一下脚本就可以直接合并了
    打开记事本。复制并粘贴以下代码:保存为merge_videos.bat双击merge_videos.bat文件运行脚本。@echooffchcp65001>nul::创建临时文件列表set"list=filelist.txt"ifexist"%list%"del"%list%"::将当前目录下所有mp4文件添加到文件列表for%%iin(*.mp4)do(......
  • 编译ffmpeg 并支持 NVIDIA 硬解码
    1.简述所谓硬件解码就是利用专用的硬件(比如说nvenc)进行解码区别与利用通用计算单元进行解码(CPU,cuda)2.所需要的sdkcuda11.1nvccffmpeg5.1.2nv-codec-header11.1.5.2下载位置4.安装ffnvcodec省略安装cuda和nvcc的方法显卡驱动最好大于430.1.4安装ffnvc......
  • 微信小程序 - 选项卡切换 - 视频播放 - (图解+代码流程)
    目录一、选项卡切换效果图1.选项卡切换.wxml代码2.选项卡切换.wxss代码3.选项卡切换.js代码neirclik函数onLoad函数ctqis函数二、视频播放效果图1.视频播放.wxml代码视频组件1.视频播放.wxss代码3.视频播放.js代码**随机颜色生成函数getRandomColor()****页......
  • c#winfrom+ffmpeg视频一键自动化剪辑批量生成视频软件(一)
    简单介绍一下1,设置字幕字体内容2,视频素材库自定义文件夹3,视频背景音乐库4,一键全自动配置剪辑,多选背景音乐,多选字体字幕样式,无限批量生成视频数量个数设置。我历时一个月开发了一套全新的视频批量剪辑软件,结合了C#WinForms和FFmpeg技术,旨在提供一种简单而功能强大的解决方案......
  • Linux系统配置Opencv+cuda+ffmpeg开发环境,-217:Gpu API call unknown error code问题
    Opencv是当前比较热门的图像处理开源算法库,但是随着深度学习在图像存储里领域的大放异彩,基于python的图像处理和深度学习算法大有超越opencv的趋势。opencv在最近的版本更新中,重点都放在了人工智能算法方面,本文介绍linux环境下配置支持GPU/cuda的ffmpeg和opencv开发环境,并将其中遇......
  • 微信小程序-首页制作 - (图解+代码流程)
    目录首页制作效果图一、轮播图的制作1.首页轮播图.wxml代码2.swiper和swiper-item组件二、滑动视图效果图1.首页滑动视图.wxml代码scroll-view组件2.首页滑动视图.wxss代码white-space:nowrap;三、标题和学员作品图片布局效果图1.标题和作品图片.wxml代......
  • FFmpeg开发笔记(三十六)Linux环境安装SRS实现视频直播推流
    ​《FFmpeg开发实战:从零基础到短视频上线》一书在第10章介绍了轻量级流媒体服务器MediaMTX,通过该工具可以测试RTSP/RTMP等流媒体协议的推拉流。不过MediaMTX的功能实在是太简单了,无法应用于真实直播的生产环境,真正能用于生产环境的流媒体服务器还要看SRS或者ZLMediaKit。SRS是一......