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

news2024/9/20 8:08:54

文章目录

    • 1.H264码流文件解码流程
      • 关键流程
      • 详细解码流程
      • 详细步骤解析
    • 2.JPEG编码流程
      • 详细编码流程
      • 详细步骤解析
    • 3.完整示例代码
    • 4.效果展示

从纯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数据进行解码,并且对解码后的关键帧进行编码,成功之后将会提取出所有的图片

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1922403.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微信小程序---分包加载

一、分包加载 1. 什么是分包加载 什么是分包加载 ❓ 小程序的代码通常是由许多页面、组件以及资源等组成&#xff0c;随着小程序功能的增加&#xff0c;代码量也会逐渐增加&#xff0c;体积过大就会导致用户打开速度变慢&#xff0c;影响用户的使用体验。 分包加载是一种小…

线性代数|机器学习-P23梯度下降

文章目录 1. 梯度下降[线搜索方法]1.1 线搜索方法&#xff0c;运用一阶导数信息1.2 经典牛顿方法&#xff0c;运用二阶导数信息 2. hessian矩阵和凸函数2.1 实对称矩阵函数求导2.2. 线性函数求导 3. 无约束条件下的最值问题4. 正则化4.1 定义4.2 性质 5. 回溯线性搜索法 1. 梯度…

nodejs模板引擎(一)

在 Node.js 中使用模板引擎可以让您更轻松地生成动态 HTML 页面&#xff0c;通过将静态模板与动态数据结合&#xff0c;您可以创建可维护且易于扩展的 Web 应用程序。以下是一个使用 Express 框架和 EJS 模板引擎的基本示例&#xff1a; 安装必要的依赖&#xff1a; 首先&#…

(四)stm32之通信协议

一.串口通信 1、全双工、半双工、单工 单工:只能一个人传输,只能向一个方向传输 半双工:只能一个人传输,可以多个方向传输 全双工:多方传输,多个方向传输 2、同步通信、一步通信 异步通信:双方时钟可以不同步,发送的信息封装(加上起始位、停止位)实现同步,效率低,…

生成式AI推动药物发现革命:加速开发,降低成本

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

Ubuntu 22.04.4 LTS (linux) Auditd 安全审计rm命令 记录操作

1 audit增加rm 规则 #sudo vim /etc/audit/rules.d/audit.rules -w /bin/rm -p x -k delfile #重新启动服务 sudo systemctl restart auditd #查看规则 sudo auditctl -l -w /bin/rm -p x -k delfile 2 测试规则 touch test.txt rm test.tx 3 查看日志 sudo ausear…

LDAPWordlistHarvester:基于LDAP数据的字典生成工具

关于LDAPWordlistHarvester LDAPWordlistHarvester是一款功能强大的字典列表生成工具&#xff0c;该工具可以根据LDAP中的详细信息生成字典列表文件&#xff0c;广大研究人员随后可以利用生成的字典文件测试目标域账号的非随机密码安全性。 工具特征 1、支持根据LDAP中的详细信…

liunx笔记1

线程池的基本概念是&#xff0c;在应用程序启动时创建一定数量的线程&#xff0c;并将它们保存在线程池中。当需要执行任务时&#xff0c;从线程池中获取一个空闲的线程&#xff0c;将任务分配给该线程执行。当任务执行完毕后&#xff0c;线程将返回到线程池&#xff0c;可以被…

【RNN练习】天气预测

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 一、环境及数据准备 1. 我的环境 语言环境&#xff1a;Python3.11.9编译器&#xff1a;Jupyter notebook深度学习框架&#xff1a;TensorFlow 2.15.0 2. 导…

手机和电脑通过TCP传输

一.工具 手机端&#xff1a;网络调试精灵 电脑端&#xff1a;野火网络调试助手 在开始通信之前&#xff0c;千万要查看一下电脑的防火墙是否关闭&#xff0c;否则可能会无法通信 在开始通信之前&#xff0c;千万要查看一下电脑的防火墙是否关闭&#xff0c;否则可能会无法通信…

浅析Kafka Streams消息流式处理流程及原理

以下结合案例&#xff1a;统计消息中单词出现次数&#xff0c;来测试并说明kafka消息流式处理的执行流程 Maven依赖 <dependencies><dependency><groupId>org.apache.kafka</groupId><artifactId>kafka-streams</artifactId><exclusio…

全国大学生数据建模比赛c题——基于蔬菜类商品的自动定价与补货决策的研究分析

基于蔬菜类商品的自动定价与补货决策的研究分析 摘要 商超蔬菜不易保存&#xff0c;其质量会随着销售时间的增加而变差&#xff0c;影响商超收益&#xff0c;因此&#xff0c;基于各蔬菜品类的历史销售数据&#xff0c;制定合理的销售策略和补货决策对商超的营收十分关键。本文…

HTTP-响应协议(响应状态码、HTTP-协议解析)

HTTP-响应协议 2.3.1 格式介绍 与HTTP的请求一样&#xff0c;HTTP响应的数据也分为3部分&#xff1a;响应行、响应头 、响应体 响应行(以上图中红色部分)&#xff1a;响应数据的第一行。响应行由协议及版本、响应状态码、状态码描述组成 协议/版本&#xff1a;HTTP/1.1响应状态…

fullcalendar基础使用

fullcalendar日历插件&#xff0c;下面是实现的一个基础模版实现任务的添加修改操作。 <div><div id"calendar" ref"calendarRef"></div><el-dialogv-model"dialogTableVisible"title"添加任务"width"500&…

ASP.NET Core中创建中间件的几种方式

前言 今天我们一起来盘点一下在ASP.NET Core应用程序中添加和创建中间件常见的四种方式。 中间件介绍 ASP.NET Core中间件&#xff08;Middleware&#xff09;是用于处理HTTP请求和响应的组件&#xff0c;它们被安排在请求处理管道中&#xff0c;并按顺序执行。中间件的设计是为…

什么是IOT 可编程控制系统

IOT可编程控制系统GF-MAXCC是一种基于物联网&#xff08;Internet of Things, IoT&#xff09;技术的可编程中央控制主机。它集成了多种先进的技术和功能&#xff0c;能够在物联网系统中发挥关键作用&#xff0c;实现对多种设备的集中管理和控制。 一、定义与概述 定义&#x…

PHP全功能微信投票迷你平台系统小程序源码

&#x1f525;让决策变得超简单&#xff01;&#x1f389; &#x1f680;【一键创建&#xff0c;秒速启动】 嘿小伙伴们&#xff0c;你还在为组织投票而手忙脚乱吗&#xff1f;来试试这款全功能投票迷你微信小程序吧&#xff01;只需轻轻一点&#xff0c;无论是班级选举、社团…

家政服务小程序:提高家政服务,新商机!

当下&#xff0c;社会生活的节奏非常快&#xff0c;人们忙于工作&#xff0c;在日常生活家务清洁中面临着时间、精力不足的问题&#xff0c;因此对家政服务的需求日益增加&#xff0c;这也推动了家政行业的迅速发展。目前不少年轻人都开始涌入到了家政行业中&#xff0c;市场的…

debian 12 PXE Server 批量部署系统

pxe server 前言 PXE&#xff08;Preboot eXecution Environment&#xff0c;预启动执行环境&#xff09;是一种网络启动协议&#xff0c;允许计算机通过网络启动而不是使用本地硬盘。PXE服务器是实现这一功能的服务器&#xff0c;它提供了启动镜像和引导加载程序&#xff0c;…

报表控件DevExpress Reporting中文教程 - 如何创建穿透钻取报表?

DevExpress Reporting是.NET Framework下功能完善的报表平台&#xff0c;它附带了易于使用的Visual Studio报表设计器和丰富的报表控件集&#xff0c;包括数据透视表、图表&#xff0c;因此您可以构建无与伦比、信息清晰的报表。 钻取报表允许用户通过单击主/活动报表文档中的…