【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 基本框架

在实现时,对参考博客中代码进行了修改

  1. 不再使用av_register_all()函数对编解码器进行初始化
  2. 修改部分变量的赋值方式,因为变量存储的位置发生了变化
  3. 修改部分函数的使用方式,因为部分函数已经被废弃
  4. 没有进行文件的存储,而是直接播放

在解码和渲染过程中,主要使用的函数有

函数名作用
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

程序主要流程:
===== 解码器流程 =====

  1. 创建接收输入流格式的结构体(avformat_alloc_context)
  2. 打开输入文件的流,并读取头信息(avformat_open_input)
  3. 读取输入流的包来获取流信息(avformat_find_stream_info)
  4. 查找解码器(avcodec_find_decoder)
  5. 创建解码器上下文结构体(avcodec_alloc_context3)
  6. 打开解码器(avcodec_open2)
  7. 获取输出文件各通道的首地址和各通道的长度(av_image_fill_arrays)
  8. 获取sws上下文结构体,该结构是使用sws_scale的基础(sws_getContext)

===== SDL流程 =====

  1. 初始化SDL(SDL_Init)
  2. 创建SDL窗口(SDL_CreateWindow)
  3. 创建渲染器(SDL_CreateRenderer)
  4. 创建纹理信息(SDL_CreateTexture)
  5. 循环读取帧信息(av_read_frame)
  6. 将帧信息送入到解码器当中进行解码(avcodec_send_packet)
  7. 获取解码器解码的帧(avcodec_receive_frame)
  8. 将获取的帧进行裁剪(sws_scale)
  9. 根据裁剪的帧信息,更新SDL_Texture
  10. 清空渲染器(SDL_RenderClear)
  11. 将纹理信息复制到渲染器中(SDL_RenderCopy)
  12. 将纹理信息通过渲染器推送到屏幕(SDL_RenderPresent)
  13. 延时显示(SDL_Delay)

===== 流程结束 =====

  1. 释放结构体信息(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
-------------------------------------------------

这里打印了文件信息,同时会弹出播放界面进行播放。同样地,也可以结合之前的实时拉流操作,将实时拉流下来的帧进行解码渲染,实现实时播放器的功能。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值