【FFmpeg】调用FFmpeg和SDL2进行解码后渲染播放
参考: 雷霄骅博士,100行代码实现最简单的基于FFMPEG+SDL的视频播放器(SDL1.x)
示例工程:
【FFmpeg】调用FFmpeg库实现264软编
【FFmpeg】调用FFmpeg库实现264软解
【FFmpeg】调用ffmpeg库进行RTMP推流和拉流
1.FFmpeg编译
参考: FFmpeg在Windows下的编译
本文使用的版本为FFmpeg-7.0
2.SDL2库的准备
SDL2直接使用SDL-2.30.3版本的lib文件,
参考:github链接 [SDL2-devel-2.30.3-VC.zip]
3.调用FFmpeg和SDL2进行解码后渲染播放
3.1 基本框架
在实现时,对参考博客中代码进行了修改
- 不再使用av_register_all()函数对编解码器进行初始化
- 修改部分变量的赋值方式,因为变量存储的位置发生了变化
- 修改部分函数的使用方式,因为部分函数已经被废弃
- 没有进行文件的存储,而是直接播放
在解码和渲染过程中,主要使用的函数有
函数名 | 作用 |
---|---|
avformat_alloc_context | 创建AVFormatContext(格式的上下文结构体),记录流的格式信息 |
avformat_open_input | 打开输入文件的流,读取头信息给到AVFormatContext,但没有打开编解码器 |
avformat_find_stream_info | 读取媒体文件的包来获取流信息(对于MPEG格式文件很有用,因为MPEG格式文件是没有头信息的) |
avcodec_find_decoder | 根据AVCodecID查找解码器 |
avcodec_alloc_context3 | 根据AVCodec创建上下文信息AVCodecContext |
avcodec_open2 | 打开AVCodec |
av_frame_alloc | 创建AVFrame |
av_image_fill_arrays | 根据长宽和颜色格式,获取所提供的buffer的每个通道的首地址和每个通道的长度信息 |
av_dump_format | 打印format信息 |
sws_getContext | 获取sws上下文结构体,是使用sws_scale的基础 |
SDL_Init | 根据Flag来初始化SDL库 |
SDL_CreateWindow | 创建SDL窗口,此时可以指定窗口的长宽和渲染方式 |
SDL_CreateRender | 创建渲染器 |
SDL_CreateTexture | 创建纹理信息,推送纹理到window的操作是由render完成的 |
av_read_frame | 读取帧信息 |
avcodec_send_packet | 向解码器输送帧,进行解码 |
avcodec_receive_frame | 获取解码器已经解码的帧 |
sws_scale | 剪裁图片 |
SDL_UpdateYUVTexture | 更新sdl_texture的信息 |
SDL_RenderClear | 清理渲染器,使用绘图颜色来清除 |
SDL_RenderCopy | 将纹理的一部分复制到当前渲染目标 |
SDL_RenderPresent | 使用自上次调用以来执行的任何呈现来更新屏幕 |
SDL_Delay | 延时 |
av_packet_unref | 释放packet |
sws_freeContext | 释放sws上下文信息 |
SDL_Quit | 释放SDL库 |
av_frame_free | 释放AVFrame |
avformat_close_input | 释放AVFormatContext |
程序主要流程:
===== 解码器流程 =====
- 创建接收输入流格式的结构体(avformat_alloc_context)
- 打开输入文件的流,并读取头信息(avformat_open_input)
- 读取输入流的包来获取流信息(avformat_find_stream_info)
- 查找解码器(avcodec_find_decoder)
- 创建解码器上下文结构体(avcodec_alloc_context3)
- 打开解码器(avcodec_open2)
- 获取输出文件各通道的首地址和各通道的长度(av_image_fill_arrays)
- 获取sws上下文结构体,该结构是使用sws_scale的基础(sws_getContext)
===== SDL流程 =====
- 初始化SDL(SDL_Init)
- 创建SDL窗口(SDL_CreateWindow)
- 创建渲染器(SDL_CreateRenderer)
- 创建纹理信息(SDL_CreateTexture)
- 循环读取帧信息(av_read_frame)
- 将帧信息送入到解码器当中进行解码(avcodec_send_packet)
- 获取解码器解码的帧(avcodec_receive_frame)
- 将获取的帧进行裁剪(sws_scale)
- 根据裁剪的帧信息,更新SDL_Texture
- 清空渲染器(SDL_RenderClear)
- 将纹理信息复制到渲染器中(SDL_RenderCopy)
- 将纹理信息通过渲染器推送到屏幕(SDL_RenderPresent)
- 延时显示(SDL_Delay)
===== 流程结束 =====
- 释放结构体信息(av_packet_unref,sws_freeContext,SDL_Quit,av_frame_free,avformat_close_input)
在上述流程中,渲染器这一系统的输入是所要渲染的帧(会拷贝到Texture中),工作系统是渲染器(Render),渲染的结果是(Window)。
3.2 实现代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "sdl2.h"
#ifdef _WIN32
//Windows
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "SDL.h"
#include "libavutil/imgutils.h"
};
#else
//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "SDL.h"
#include "libavutil/imgutils.h"
#ifdef __cplusplus
};
#endif
#endif
int sdl2()
{
AVFormatContext* av_fmt_ctx;
int i;
int video_idx;
AVCodecContext* av_codec_ctx;
const AVCodec* av_codec;
AVFrame* av_frm;
AVFrame* av_frm_yuv;
unsigned char* out_buffer;
AVPacket* av_pkt;
int y_size;
int ret;
int got_picture;
struct SwsContext* img_convert_ctx;
int screen_wdt = 0;
int screen_hgt = 0;
SDL_Window* sdl_window;
SDL_Renderer* sdl_render;
SDL_Texture* sdl_texture;
SDL_Rect sdl_rect;
FILE* fp_yuv;
// avformat_network_init();
av_fmt_ctx = avformat_alloc_context();
char file_path[] = "enc_out.bin";
ret = avformat_open_input(&av_fmt_ctx, file_path, NULL, NULL);
if(ret < 0) {
fprintf(stderr, "could not open input steram, error code:%d\n", ret);
return -1;
}
ret = avformat_find_stream_info(av_fmt_ctx, NULL);
if (ret < 0) {
fprintf(stderr, "could not find stream information, error code:%d\n", ret);
return -1;
}
video_idx = -1;
for (i = 0; i < av_fmt_ctx->nb_streams; i++) {
if (av_fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_idx = i;
break;
}
}
if (video_idx == -1) {
fprintf(stderr, "could not find video stream\n");
return -1;
}
// av_codec_ctx = av_fmt_ctx->streams[video_idx]->codecpar->codec_ctx;
av_codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!av_codec) {
fprintf(stderr, "could not find encoder\n");
return -1;
}
av_codec_ctx = avcodec_alloc_context3(av_codec);
if (!av_codec_ctx) {
fprintf(stderr, "could not alloc codec context\n");
return -1;
}
ret = avcodec_open2(av_codec_ctx, av_codec, NULL);
if ( ret < 0) {
fprintf(stderr, "could not open codec\n");
return -1;
}
av_frm = av_frame_alloc();
av_frm_yuv = av_frame_alloc();
av_codec_ctx->width = av_fmt_ctx->streams[video_idx]->codecpar->width;
av_codec_ctx->height = av_fmt_ctx->streams[video_idx]->codecpar->height;
av_codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
out_buffer = (unsigned char*)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, av_codec_ctx->width, av_codec_ctx->height, 1));
// 根据长宽和颜色格式,将out_buffer的每个通道的首地址和每个通道的长度解析出来
av_image_fill_arrays(av_frm_yuv->data, av_frm_yuv->linesize, out_buffer, AV_PIX_FMT_YUV420P, av_codec_ctx->width, av_codec_ctx->height, 1);
av_pkt = (AVPacket*)av_malloc(sizeof(AVPacket));
// output info
fprintf(stdout, "--------------- File Information ----------------\n");
av_dump_format(av_fmt_ctx, 0, file_path, 0);
fprintf(stdout, "-------------------------------------------------\n");
// 获取sws上下文结构体,是使用sws_scale()的基础
img_convert_ctx = sws_getContext(av_codec_ctx->width, av_codec_ctx->height, av_codec_ctx->pix_fmt,
av_codec_ctx->width, av_codec_ctx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
// 初始化SDL
// 视频模块 | 音频模块 | 计时器模块
ret = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER);
if (ret < 0) {
fprintf(stderr, "could not init sdl, error code:%d\n", ret);
return -1;
}
screen_wdt = av_codec_ctx->width;
screen_hgt = av_codec_ctx->height;
// SDL2.0 support multiple windows
// 创建SDL窗口,并指定OpenGL渲染
// (name, rect.x, rect.y, rect.w, rect.h, render_library)
sdl_window = SDL_CreateWindow("player window", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, screen_wdt, screen_hgt, SDL_WINDOW_OPENGL);
if (!sdl_window) {
fprintf(stderr, "SDL: could not create window - existing:%s\n", SDL_GetError());
return -1;
}
// 创建渲染器,渲染的对象是sdl_window
// (SDL_Window object, index, flags)
// index = -1 表示使用第一个支持flags的渲染驱动进行渲染
// 0表示不使用特定参数,如使用硬件渲染等
sdl_render = SDL_CreateRenderer(sdl_window, -1, 0);
// IYUV: Y + U + V (3 planes)
// YV12: Y + V + U (3 planes)
// 创建纹理信息,推送纹理到window的手是render
// (render, format, access, wdt, hgt,)
sdl_texture = SDL_CreateTexture(sdl_render, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, av_codec_ctx->width, av_codec_ctx->height);
sdl_rect.x = 0;
sdl_rect.y = 0;
sdl_rect.w = screen_wdt;
sdl_rect.h = screen_hgt;
// frame process and render
while (av_read_frame(av_fmt_ctx, av_pkt) >= 0) {
if (av_pkt->stream_index == video_idx) {
ret = avcodec_send_packet(av_codec_ctx, av_pkt);
if (ret < 0) {
fprintf(stderr, "decode error\n");
return -1;
}
while (ret >= 0) {
ret = avcodec_receive_frame(av_codec_ctx, av_frm);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return -1;
}
else if (ret < 0) {
fprintf(stderr, "Error during decoding, error code:%d\n", ret);
return -1;
}
// 裁剪图片,存储到av_frm_yuv当中
sws_scale(img_convert_ctx, (const unsigned char* const*)av_frm->data, av_frm->linesize, 0, av_codec_ctx->height,
av_frm_yuv->data, av_frm_yuv->linesize);
// 根据av_frm_yuv的data,更新sdl_texture信息
SDL_UpdateYUVTexture(sdl_texture, &sdl_rect,
av_frm_yuv->data[0], av_frm_yuv->linesize[0],
av_frm_yuv->data[1], av_frm_yuv->linesize[1],
av_frm_yuv->data[2], av_frm_yuv->linesize[2]);
SDL_RenderClear(sdl_render); // 用绘图颜色清除当前渲染目标
SDL_RenderCopy(sdl_render, sdl_texture, NULL, &sdl_rect); // 将纹理的一部分复制到当前渲染目标
SDL_RenderPresent(sdl_render); // 使用自上次调用以来执行的任何呈现更新屏幕
SDL_Delay(40); // delay 40ms
}
}
av_packet_unref(av_pkt);
}
// flush decoder
// ...
sws_freeContext(img_convert_ctx);
SDL_Quit();
av_frame_free(&av_frm);
av_frame_free(&av_frm_yuv);
// avcodec_close(av_codec); // deprecate
avformat_close_input(&av_fmt_ctx);
return 0;
}
4.测试结果
--------------- File Information ----------------
Input #0, h264, from 'enc_out.bin':
Duration: N/A, bitrate: N/A
Stream #0:0: Video: h264 (High), yuv420p(progressive), 1920x1200, 25 fps, 0.08 tbr, 1200k tbn
-------------------------------------------------
这里打印了文件信息,同时会弹出播放界面进行播放。同样地,也可以结合之前的实时拉流操作,将实时拉流下来的帧进行解码渲染,实现实时播放器的功能。