FFmpeg H264视频解码全流程解析

目录

  • [1. 打开视频文件](#1. 打开视频文件)
  • [2. 查找解码器 avcodec_find_decoder](#2. 查找解码器 avcodec_find_decoder)
  • [3. 创建解码器上下文 avcodec_alloc_context3](#3. 创建解码器上下文 avcodec_alloc_context3)
  • [4. 打开解码器上下文 avcodec_open2](#4. 打开解码器上下文 avcodec_open2)
  • [5. 初始化音视频码流解析器 av_parser_init](#5. 初始化音视频码流解析器 av_parser_init)
  • [6. 初始化SDL部分](#6. 初始化SDL部分)
  • [7. 读取文件数据](#7. 读取文件数据)
  • [8. 解析音视频裸流数据,从中分离出一帧一帧的压缩编码数据 av_parser_parse2](#8. 解析音视频裸流数据,从中分离出一帧一帧的压缩编码数据 av_parser_parse2)
  • [9. 发送解码 avcodec_send_packet](#9. 发送解码 avcodec_send_packet)
  • [10. 接收解码 avcodec_receive_frame](#10. 接收解码 avcodec_receive_frame)
  • [11. SDL更新、渲染](#11. SDL更新、渲染)
  • [12. 冲刷解码器](#12. 冲刷解码器)
  • [13. 内存回收](#13. 内存回收)
  • 代码合集
  • 视频效果截图

之前已经使用ffmpeg编码生成了一段.h264的视频(800_600_25.h264),现在,将继续使用ffmpeg,将其解码出来;显示部分可以使用SDL2进行渲染;

整理流程如下:

cpp 复制代码
					开始(begin)
						↓
					1.打开视频文件(std::ifstream::open)
						↓
					2.查找解码器(avcodec_find_decoder)
						↓
					3.创建解码器上下文(avcodec_alloc_context3)
						↓
					4.打开解码器上下文(avcodec_open2)
						↓
					5.初始化音视频码流解析器(av_parser_init)
						↓
					6.初始化SDL部分
						↓
					7.读取文件数据(std::ifstream::read)
						↓
					8.解析音视频裸流数据(av_parser_parse2)
						↓
					9.发送解码(avcodec_send_packet)
						↓
					10.接收解码(avcodec_receive_frame)
						↓
					11.SDL更新、渲染
						↓
					12.冲刷解码器
						↓
					13.内存回收
						↓
					结束(end)

包含头文件:

cpp 复制代码
#include <iostream>
#include <fstream>
#include <thread>
#include <chrono>

extern "C" {
#include "libavcodec/avcodec.h"
#include "libavutil/avutil.h"
}

#include "SDL.h"


// 打印错误信息
static void PrintErr(int ret) {
    char buf[1024] = { 0 };
    av_strerror(ret, buf, sizeof(buf));
    std::cout << buf << std::endl;
}

1. 打开视频文件

使用了C++的std::ifstream文件处理

cpp 复制代码
std::ifstream ifs;
ifs.open("800_600_25.h264", std::ios::binary);
if (!ifs.is_open()) {
    std::cout << "ifs open file failed!\n";
    return -1;
}

注意,一定要以二进制方式打开文件,否则视频会出现乱码!

2. 查找解码器 avcodec_find_decoder

avcodec_find_decoder 用于根据指定的编码器 ID 查找并返回对应的解码器。

①函数原型

cpp 复制代码
const AVCodec *avcodec_find_decoder(enum AVCodecID id);

②参数说明

参数 类型 说明
id enum AVCodecID 请求的解码器 ID,如 AV_CODEC_ID_H264、AV_CODEC_ID_AAC 等

③返回值

  • 成功:返回一个指向 AVCodec 结构体的指针,表示找到的解码器
  • 失败:返回 NULL,表示没有找到匹配的解码器

④使用示例

cpp 复制代码
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
    std::cout << "avcodec_find_decoder failed!" << std::endl;
    return -1;
}

3. 创建解码器上下文 avcodec_alloc_context3

avcodec_alloc_context3 是 FFmpeg 库中用于分配并初始化编解码器上下文 AVCodecContext 结构体的核心 API。

①函数原型

cpp 复制代码
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);

②参数说明

参数 类型 说明
codec const AVCodec* 指向编解码器结构体的指针,通常通过 avcodec_find_encoder 或 avcodec_find_decoder 获得

③返回值

  • 成功:非NULL,返回指向已分配的 AVCodecContext 结构体的指针,各字段已设置为默认值
  • 失败:返回 NULL,分配失败

④使用示例

cpp 复制代码
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
    std::cout << "avcodec_alloc_context3 failed!" << std::endl;
    return -1;
}

4. 打开解码器上下文 avcodec_open2

avcodec_open2 是 FFmpeg 库中用于初始化编解码器上下文(AVCodecContext)并将其与指定的编解码器(AVCodec)进行关联的核心 API。

①函数原型

cpp 复制代码
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);

②参数说明

参数 类型 说明
avctx AVCodecContext* 需要初始化的编解码器上下文,必须已通过 avcodec_alloc_context3() 分配
codec const AVCodec* 要打开的编解码器。如果上下文中已经通过 avcodec_alloc_context3() 传入了非 NULL 的编解码器,则此参数必须为 NULL 或与之前传入的编解码器相同
options AVDictionary** 指向 AVDictionary 的指针,用于传递编解码器上下文选项和编解码器私有选项,一般传NULL即可

③返回值

  • 成功:0,编解码器已成功打开并初始化
  • 失败:< 0,返回负的错误码(AVERROR)

④使用示例

cpp 复制代码
int ret = avcodec_open2(codec_ctx, codec, 0);
if (0 != ret) {
    std::cout << "avcodec_open2 failed!" << std::endl;
    PrintErr(ret);
    return -1;
}

5. 初始化音视频码流解析器 av_parser_init

av_parser_init 是 FFmpeg 中用于初始化音视频码流解析器 的 API。它根据指定的编码器 ID(codec_id)创建一个 AVCodecParserContext 上下文,该上下文用于后续的裸流解析操作。

解析器的核心任务 :将输入的裸流 (即没有封装格式的纯编码数据,如 .h264.hevc.aac 文件)中的连续字节流分割成一帧一帧的压缩编码数据,并识别帧边界、帧类型(I/P/B 帧)等信息,为解码器提供可直接解码的数据包。

①函数原型

cpp 复制代码
AVCodecParserContext *av_parser_init(int codec_id);

②参数说明

参数 类型 说明
codec_id int 编码器 ID,来自 AVCodecID 枚举,如 AV_CODEC_ID_H264、AV_CODEC_ID_HEVC、AV_CODEC_ID_AAC 等

③返回值

  • 成功:非 NULL,返回指向已初始化的 AVCodecParserContext 结构体的指针
  • 失败:NULL,原因可能是:① FFmpeg 未包含该编码格式的解析器;② 内存不足

④使用示例

cpp 复制代码
AVCodecParserContext *parser_ctx = av_parser_init(codec_id);
if (!parser_ctx) {
    std::cout << "av_parser_init failed!" << std::endl;
    return -1;
}

6. 初始化SDL部分

不需要显示视频画面可忽略;这部分内容可在网上了解。

cpp 复制代码
SDL_Init(SDL_INIT_VIDEO);
// 创建窗口
SDL_Window *sdl_win = SDL_CreateWindow("ttttttttttt", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
                                       codec_ctx->width, codec_ctx->height, SDL_WINDOW_OPENGL);
// 创建渲染器
SDL_Renderer *render = SDL_CreateRenderer(sdl_win, -1, SDL_RENDERER_TARGETTEXTURE);
// 创建 纹理|材质
SDL_Texture *texture = SDL_CreateTexture(render, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,
                                         codec_ctx->width, codec_ctx->height);
// 设置线性插值(抗锯齿)
SDL_SetTextureScaleMode(texture, SDL_ScaleModeLinear);

7. 读取文件数据

使用C++的文件处理,

cpp 复制代码
unsigned char buf[4096] = { 0 };
unsigned char *data_in = nullptr;
int size_in = 0;


// 读取文件的内容到buf中
ifs.read((char *)buf, sizeof(buf));
size_in = ifs.gcount();
// 读取失败或读取完毕
if (0 >= size_in) {
    std::cout << "ifs.read end!" << std::endl;
    break;
}

8. 解析音视频裸流数据,从中分离出一帧一帧的压缩编码数据 av_parser_parse2

av_parser_parse2 是 FFmpeg 中用于解析裸流数据 (即没有封装格式的纯编码数据)的核心函数。它从输入的连续字节流中检测帧边界,并输出一帧完整的压缩编码数据(如一个 H.264 NALU 或一个完整的视频帧),同时提供帧类型、时间戳等元信息。

解析器不执行解码,只负责将数据流切割成独立的帧单元,供解码器使用。

①函数原型

cpp 复制代码
int av_parser_parse2(
    AVCodecParserContext *s,
    AVCodecContext *avctx,
    uint8_t **poutbuf,
    int *poutbuf_size,
    const uint8_t *buf,
    int buf_size,
    int64_t pts,
    int64_t dts,
    int64_t pos
);

②参数说明

参数 类型 说明
s AVCodecParserContext* 解析器上下文,由 av_parser_init() 初始化
avctx AVCodecContext* 解码器上下文,用于提供编码参数(如像素格式、宽高等)
poutbuf uint8_t** 输出:指向解析出的完整帧数据的指针(指向内部缓冲区,不应释放)
poutbuf_size int* 输出:解析出的帧数据的大小(字节数)。若为 0,表示未解析出完整帧
buf const uint8_t* 输入:待解析的原始数据缓冲区
buf_size int 输入:待解析数据的长度(字节)。若为 0,表示已无输入(冲刷)
pts int64_t 输入数据对应的显示时间戳(PTS),通常设为 AV_NOPTS_VALUE
dts int64_t 输入数据对应的解码时间戳(DTS),通常设为 AV_NOPTS_VALUE化
pos int64_t 输入数据在源文件中的偏移位置,可用于追踪,可设置为0

重要说明

  • poutbuf 指向的数据由解析器内部管理,调用者无需手动释放,但需注意在下一次调用 av_parser_parse2后,之前的数据可能失效,因此应在当前循环中立即使用。
  • buf 可以包含部分帧、完整帧、或多帧,解析器会内部缓冲跨边界的数据。
  • 当 buf_size == 0 时,表示数据流结束,解析器会输出内部残留的所有帧(冲刷)。
  • pts、dts、pos 一般传入 AV_NOPTS_VALUE 和 0,解析器会将其传递给输出的每一帧。

③返回值

返回值表示从输入 buf 中实际消耗的字节数(即已处理的字节数)。

  • 成功:>= 0,返回消耗字节数。调用者应将 buf 指针前移该数量,并减少剩余长度
  • 失败:< 0,表示解析失败

④使用示例

通过判断 poutbuf_size > 0 ,说明当前解析出一帧数据,可以进行处理;

cpp 复制代码
AVPacket *pkt = av_packet_alloc();

data_in = buf;
while (0 < size_in) {
    // 8.解析音视频裸流数据,从中分离出一帧一帧的压缩编码数据
    int re = av_parser_parse2(parser_ctx, codec_ctx, &pkt->data, &pkt->size,
                         data_in, size_in, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);

    data_in += re;
    size_in -= re;

    if (pkt->size) {
    	// 处理解析出来的pkt
	}
}
  1. buf不足以解析出一帧数据时,pkt->size为0,内部会记录当前的数据,下一次循环读取,再将新读取到的内容和先前的内容一起解析,还不满足,则循环往复,直到满足,pkt->size > 0
  2. 假设读取文件的内容为4096,解析一帧数据只用到了3000字节,剩余1096字节,内部依旧会记录下来,下一次循环读取,再将新读取到的内容和先前的内容一起解析,循环往复。

官方给出的例子:

cpp 复制代码
 * Example:
 * @code
 *   while(in_len){
 *       len = av_parser_parse2(myparser, AVCodecContext, &data, &size,
 *                                        in_data, in_len,
 *                                        pts, dts, pos);
 *       in_data += len;
 *       in_len  -= len;
 *
 *       if(size)
 *          decode_frame(data, size);
 *   }
 * @endcode

9. 发送解码 avcodec_send_packet

avcodec_send_packet 用于将压缩的编码数据包(AVPacket)送入解码器 进行解码。与avcodec_receive_frame配对使用。

①函数原型

cpp 复制代码
int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);

②参数说明

参数 类型 说明
avctx AVCodecContext* 已通过 avcodec_open2() 打开的解码器上下文
avpkt const AVPacket* 包含压缩编码数据的 AVPacket 指针,可以为 NULL

参数 avpkt 的详细说明

  • 正常输入:传入一个包含压缩编码数据的 AVPacket(通常从 av_read_frame 或 av_parser_parse2获得),解码器将对其进行解码。
  • 刷新解码器(Flush):传入 NULL,表示没有更多的输入数据包,通知解码器结束解码过程并输出所有缓存的剩余解码帧。

⚠️ 所有权说明:avpkt 的所有权始终属于调用者。解码器不会修改数据包的内容,但可能会在内部引用其数据(如果数据包是引用计数的)。调用者在调用完成后仍需负责调用av_packet_unref() 释放数据包。

③返回值

  • 成功:0,数据包已成功送入解码器
  • 失败:0 >,失败了

④使用示例

cpp 复制代码
ret = avcodec_send_packet(codec_ctx, pkt);
if (0 != ret) {
    std::cout << "avcodec_send_packet failed! ret = " << ret << std::endl;
    PrintErr(ret);
    goto FAILED;
}

10. 接收解码 avcodec_receive_frame

avcodec_receive_frame 用于从解码器中读取解码后的原始音视频帧(AVFrame 。与avcodec_send_packet配对使用。

①函数原型

cpp 复制代码
int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);

②参数说明

参数 类型 说明
avctx AVCodecContext* 已通过 avcodec_open2() 打开的解码器上下文
frame AVFrame* 输出参数,函数执行成功后会填充为解码后的原始音视频帧

参数 frame 的重要说明

  • 引用计数:输出的 AVFrame 由解码器分配,是引用计数的(refcounted)。
  • 自动解引用:函数在执行业务逻辑前,总是会先调用 av_frame_unref(frame) 清理 frame 中之前可能残留的数据。
  • 内存管理:调用者负责在使用完帧后调用av_frame_unref() 释放引用。

⚠️ 所有权说明:输出的 AVFrame 由解码器内部管理,调用者获得的是引用,不应手动释放其内部数据(av_free 等),而应使用 av_frame_unref()。

③返回值

  • 成功:0,数据包已成功送入解码器
  • 失败:0 >,失败了或当前还未有数据帧

④使用示例

cpp 复制代码
AVFrame *frame = av_frame_alloc();

while (0 <= ret) {
    // 10.接收解码
    ret = avcodec_receive_frame(codec_ctx, frame);
    if (AVERROR(EAGAIN) == ret || AVERROR_EOF == ret) {
    	// 当前还没有数据或者已经接收完毕
        break;
    }
    if (0 > ret) {
        std::cout << "avcodec_receive_frame failed! ret = " << ret << std::endl;
        PrintErr(ret);
        goto FAILED;
    }

	// other code ...
}

到此,frame就是已经被解码出来的一帧视频数据;

11. SDL更新、渲染

不需要显示视频画面可忽略;这部分内容可在网上了解。

cpp 复制代码
SDL_UpdateYUVTexture(texture, NULL, frame->data[0], frame->linesize[0],
        frame->data[1], frame->linesize[1], frame->data[2], frame->linesize[2]);

SDL_RenderClear(render);
SDL_RenderCopy(render, texture, NULL, NULL);
SDL_RenderPresent(render);

12. 冲刷解码器

将缓冲区中未被接收的frame全部读取出来;与正常发送解码,接收解码流程一致;

为什么需要冲刷(Flush)?

解码器内部有一个"延迟队列",如果不主动冲刷,最后的几帧画面会永远卡在队列里,导致视频播放丢失尾部几帧数据。

cpp 复制代码
// 1. 冲刷解析器(如有)
while (1) {
    int re = av_parser_parse2(parser_ctx, codec_ctx, &out_data, &out_size,
                              NULL, 0, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
    if (out_size == 0) break;
    pkt->data = out_data;
    pkt->size = out_size;
    // 发送 pkt 到解码器(可能需处理 EAGAIN)
    ret = avcodec_send_packet(codec_ctx, pkt);
    if (ret < 0 && ret != AVERROR(EAGAIN)) {
        // 错误处理
        break;
    }
    // 若 ret == EAGAIN,则需先接收帧,这里可简单循环处理,但为避免复杂,可忽略(极少发生)
}

// 2. 冲刷解码器
ret = avcodec_send_packet(codec_ctx, NULL);
if (0 != ret) {
    std::cout << "avcodec_send_packet failed! ret = " << ret << std::endl;
    PrintErr(ret);
    goto FAILED;
}

// 3. 接收剩余帧
while (0 <= ret) {
    ret = avcodec_receive_frame(codec_ctx, frame);
    if (AVERROR(EAGAIN) == ret || AVERROR_EOF == ret) {
        break;
    }
    if (0 > ret) {
        std::cout << "avcodec_receive_packet failed! ret = " << ret << std::endl;
        PrintErr(ret);
        goto FAILED;
    }

    std::cout << pts++ << "- ";

    SDL_UpdateYUVTexture(texture, NULL, frame->data[0], frame->linesize[0],
            frame->data[1], frame->linesize[1], frame->data[2], frame->linesize[2]);

    SDL_RenderClear(render);
    SDL_RenderCopy(render, texture, NULL, NULL);
    SDL_RenderPresent(render);
}

13. 内存回收

cpp 复制代码
ifs.close();
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&codec_ctx);
av_parser_close(parser_ctx);

if (texture) {
    SDL_DestroyTexture(texture);
}
if (render) {
    SDL_DestroyRenderer(render);
}
if (sdl_win) {
    SDL_DestroyWindow(sdl_win);
}
SDL_Quit();

代码合集

cpp 复制代码
#include <iostream>
#include <fstream>
#include <thread>
#include <chrono>

extern "C" {
#include "libavcodec/avcodec.h"
#include "libavutil/avutil.h"
}

#include "SDL.h"


// 打印错误信息
static void PrintErr(int ret) {
    char buf[1024] = { 0 };
    av_strerror(ret, buf, sizeof(buf));
    std::cout << buf << std::endl;
}

#undef main
int main(void)
{
    std::cout << av_version_info() << std::endl;

    // 1.打开视频文件
    std::ifstream ifs;
    ifs.open("800_600_25.h264", std::ios::binary);
    if (!ifs.is_open()) {
        std::cout << "ifs open file failed!\n";
        return -1;
    }



    AVCodecID codec_id = AV_CODEC_ID_H264;

    // 2.查找解码器
    const AVCodec *codec = avcodec_find_decoder(codec_id);
    if (!codec) {
        std::cout << "avcodec_find_decoder failed!" << std::endl;
        return -1;
    }

    // 3.创建解码器上下文
    AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
    if (!codec_ctx) {
        std::cout << "avcodec_alloc_context3 failed!" << std::endl;
        return -1;
    }
    codec_ctx->time_base = {1, 25};
    codec_ctx->width = 800;
    codec_ctx->height = 600;

    // 4.打开解码器上下文
    int ret = avcodec_open2(codec_ctx, codec, 0);
    if (0 != ret) {
        std::cout << "avcodec_open2 failed!" << std::endl;
        PrintErr(ret);
        return -1;
    }


    // 5.初始化音视频码流解析器
    AVCodecParserContext *parser_ctx = av_parser_init(codec_id);
    if (!parser_ctx) {
        std::cout << "av_parser_init failed!" << std::endl;
        return -1;
    }


    // 6.初始化SDL部分
    SDL_Init(SDL_INIT_VIDEO);
    // 创建窗口
    SDL_Window *sdl_win = SDL_CreateWindow("ttttttttttt", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
                                           codec_ctx->width, codec_ctx->height, SDL_WINDOW_OPENGL);
    // 创建渲染器
    SDL_Renderer *render = SDL_CreateRenderer(sdl_win, -1, SDL_RENDERER_TARGETTEXTURE);
    // 创建 纹理|材质
    SDL_Texture *texture = SDL_CreateTexture(render, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,
                                             codec_ctx->width, codec_ctx->height);
    // 设置线性插值(抗锯齿)
    SDL_SetTextureScaleMode(texture, SDL_ScaleModeLinear);


    int pts = 1;
    unsigned char buf[4096] = { 0 };
    unsigned char *data_in = nullptr;
    int size_in = 0;
    AVPacket *pkt = av_packet_alloc();
    AVFrame *frame = av_frame_alloc();

    while (1) {
        // 7.读取文件数据
        ifs.read((char *)buf, sizeof(buf));
        size_in = ifs.gcount();
        // 读取失败或读取完毕
        if (0 >= size_in) {
            std::cout << "ifs.read end!" << std::endl;
            break;
        }

        data_in = buf;
        while (0 < size_in) {
            // 8.解析音视频裸流数据,从中分离出一帧一帧的压缩编码数据
            int re = av_parser_parse2(parser_ctx, codec_ctx, &pkt->data, &pkt->size,
                                 data_in, size_in, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);

            data_in += re;
            size_in -= re;


            if (pkt->size) {
                // 9.发送解码
                ret = avcodec_send_packet(codec_ctx, pkt);
                if (0 != ret) {
                    std::cout << "avcodec_send_packet failed! ret = " << ret << std::endl;
                    PrintErr(ret);
                    goto FAILED;
                }

                while (0 <= ret) {
                    // 10.接收解码
                    ret = avcodec_receive_frame(codec_ctx, frame);
                    if (AVERROR(EAGAIN) == ret || AVERROR_EOF == ret) {
                        break;
                    }
                    if (0 > ret) {
                        std::cout << "avcodec_receive_frame failed! ret = " << ret << std::endl;
                        PrintErr(ret);
                        goto FAILED;
                    }


                    std::cout << pts++ << ", ";


                    // 11.SDL更新、渲染
                    SDL_UpdateYUVTexture(texture, NULL, frame->data[0], frame->linesize[0],
                            frame->data[1], frame->linesize[1], frame->data[2], frame->linesize[2]);

                    SDL_RenderClear(render);
                    SDL_RenderCopy(render, texture, NULL, NULL);
                    SDL_RenderPresent(render);

                    // 睡眠 40 毫秒
//                    std::this_thread::sleep_for(std::chrono::milliseconds(40));
                }
            }
        }
    }


    // 12.冲刷解码器
    ret = avcodec_send_packet(codec_ctx, NULL);
    if (0 != ret) {
        std::cout << "avcodec_send_packet failed! ret = " << ret << std::endl;
        PrintErr(ret);
        goto FAILED;
    }
    while (0 <= ret) {
        ret = avcodec_receive_frame(codec_ctx, frame);
        if (AVERROR(EAGAIN) == ret || AVERROR_EOF == ret) {
            break;
        }
        if (0 > ret) {
            std::cout << "avcodec_receive_packet failed! ret = " << ret << std::endl;
            PrintErr(ret);
            goto FAILED;
        }

        std::cout << pts++ << "- ";

        SDL_UpdateYUVTexture(texture, NULL, frame->data[0], frame->linesize[0],
                frame->data[1], frame->linesize[1], frame->data[2], frame->linesize[2]);

        SDL_RenderClear(render);
        SDL_RenderCopy(render, texture, NULL, NULL);
        SDL_RenderPresent(render);
    }


FAILED:
    // 13.内存回收
    ifs.close();
    av_frame_free(&frame);
    av_packet_free(&pkt);
    avcodec_free_context(&codec_ctx);
    av_parser_close(parser_ctx);

    if (texture) {
        SDL_DestroyTexture(texture);
    }
    if (render) {
        SDL_DestroyRenderer(render);
    }
    if (sdl_win) {
        SDL_DestroyWindow(sdl_win);
    }
    SDL_Quit();

    return 0;
}

视频效果截图

之前生成的视频是两千帧,这里读取出来处理的也差不多少是两千帧左右;

相关推荐
YH55269842 小时前
有什么AI工具能将网课和视频播客的内容转化成文档?
人工智能·音视频
程序员老陆3 小时前
FFmpeg 像素格式(pix_fmt)通关指南:从 YUV420P 到 NV12、RGB24 到底怎么选
ffmpeg·音视频·yuv·像素格式
小白学大数据5 小时前
基于Python的抖音网页版视频点赞数增长趋势追踪系统设计与实现
开发语言·爬虫·python·搜索引擎·音视频
DogDaoDao5 小时前
FFmpeg 9.0 “Lei“ 正式发布:十年之后,以你之名
ffmpeg·音视频·实时音视频·视频编解码·ffprobe·ffmpeg 9.0
开开心心_Every5 小时前
电脑文件搜索软件支持内容和拼音搜索
linux·服务器·人工智能·r语言·pdf·音视频·symfony
weixin_495248406 小时前
带硬字幕的老视频也能出海:短剧出海翻译服务商如何擦除重制?
人工智能·音视频
weixin_446260857 小时前
Video-DeepResearch:面向下一代视频多模态深度研究智能体
人工智能·音视频
硅谷秋水17 小时前
EgoSteer:一种基于第一人称视角视频、实现可控灵巧操作的全栈系统
深度学习·机器学习·语言模型·机器人·音视频
波罗丁牌21 小时前
uni-app 实现视频聊天、屏幕分享,支持Android、HarmonyOS、iOS
android·uni-app·音视频