基于ImGui+FFmpeg实现播放器

基于ImGui+FFmpeg实现播放器

演示:

ImGui播放器

继续研究FFmpeg,之前做了一个SDL的播放器,发现SDL的可视化UI界面的功能稍微差了点,所以今天我们换了一个新的工具,也就是ImGui。

ImGui官方文档:https://github.com/ocornut/imgui

接下来讲解一下播放器的主要功能,以及实现的方案

播放页面

播放页面是基于OpenGL渲染,ImGui实现UI界面完成的。

实现流程如下:

  1. FFmpeg解析视频文件,将每一帧写入数组中
  2. 根据帧率计算每一帧的播放时间
  3. 循环渲染每一帧
cpp 复制代码
frame = frame_vector.at(current_index).frame;
render_image(frame);
ImGui::Image((ImTextureID)(intptr_t)texture_ids[0],
			 ImVec2(codec_ctx->width, codec_ctx->height));

暂停/快进功能

暂停和快进功能相对比较简单

定义一个 is_pause变量控制视频播放

定义一个 delay变量控制播放速度

cpp 复制代码
/**
 播放线程
 */
int play_thread(void* arg) {
	for (;;) {
		while (!is_pause && current_index < frame_vector.size()) {
			SDL_Delay((Uint32) delay);
			current_index ++;
			// 从头播放
			if (current_index == frame_vector.size() -1) {
				current_index = 0;
			}
		}
	}
	return 1;
}

帧解析

帧解析就是将数组中的每一帧展示出来,程序对帧的类型进行了区分,使用不同的颜色来区分 I帧,P帧,B帧。

cpp 复制代码
ImGui::BeginGroup();
ImGui::BeginChild("ScrollArea", ImVec2(0, codec_ctx->height), true, ImGuiWindowFlags_HorizontalScrollbar);
for(int i=0;i<frame_vector.size();i++) {
	if (frame_vector.at(i).frame->pict_type == AV_PICTURE_TYPE_I) {
		ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.0f, 0.0f, 1.0f));
		
	}
	else if (frame_vector.at(i).frame->pict_type == AV_PICTURE_TYPE_P) {
		ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.5f, 0.0f, 1.0f));
	}
	else {
		ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.5f, 1.0f));
	}
	if (ImGui::Button(get_frame_title(i))) {
		current_index = i;
	}
	// 恢复样式
	ImGui::PopStyleColor();
}
ImGui::EndChild();
ImGui::EndGroup();

宏块解析

根据鼠标的位置,找到对应的宏块 16*16,必将对应的字节打印出来,方便我们进行逐个宏块的分析。

源代码

cpp 复制代码
//
//  main.cpp
//  analyser
//
//  Created by chenhuaiyi on 2025/4/3.
//

#include <SDL.h>
#include "imgui.h"                      // ImGui核心
#include "imgui_impl_sdl2.h"             // SDL2后端
#include "imgui_impl_opengl3.h"         // OpenGL渲染器
#include "iostream"
#include "glew.h"
#include <vector>
#include "string.h"
#include <iomanip> // 包含格式控制函数
#include <cstdint>

// ffmpeg
extern "C" {
#include "libavcodec/avcodec.h"
#include "libswresample/swresample.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
#include "libavutil/time.h"
#include "libavutil/channel_layout.h"
#include "libavutil/log.h"
}

#define Y_SIZE 256
#define U_SIZE 64
#define V_SIZE 64

typedef struct Frame {
	AVFrame* 			frame;
	int64_t  			pts;		// pts 播放相关
	int64_t				duration;	// duration
	AVPictureType 		pict_type;	// 帧类型
}Frame;

/**
 单个宏块的定义 16*16 个字节
 y 256 byte
 u 64 byte
 v 64 byte
 */
typedef struct MircoBlock{
	uint8_t y[Y_SIZE];
	uint8_t u[U_SIZE];
	uint8_t v[V_SIZE];
} MircoBlock;

// 窗口尺寸
const int WIDTH = 1600;
const int HEIGHT = 1200;

GLuint texture_ids[2];

std::vector<Frame> frame_vector;
int current_index = 0;

const char* file = "//Users/chenhuaiyi/workspace/ffmpeg/files/恋爱_重编码.mp4";
AVFormatContext* format_ctx = avformat_alloc_context();
int video_stream = -1;
const AVCodec* 		codec = NULL;
AVCodecContext*		codec_ctx;
double				delay; 			// 延迟单位ms
int					is_pause = 0;

char* get_frame_title(int num) {
	// 为最大整数长度(含符号和结束符)分配空间
	char buffer[32];
	// 将整数写入缓冲区
	snprintf(buffer, sizeof(buffer), "frame:%d", num);
	// 复制到动态内存
	char* str = strdup(buffer);
	return str;
}

/**
 获取当前帧的时间戳
 */
double getCurrentTimeStamp() {
	AVFrame* frame = frame_vector.at(current_index).frame;
	double tag = av_q2d(format_ctx->streams[video_stream]->time_base);
	return frame->pts * tag;
}

/**
 播放线程
 */
int play_thread(void* arg) {
	for (;;) {
		while (!is_pause && current_index < frame_vector.size()) {
			SDL_Delay((Uint32) delay);
			current_index ++;
			// 从头播放
			if (current_index == frame_vector.size() -1) {
				current_index = 0;
			}
		}
	}
	return 1;
}


/**
 宏块渲染
 */
void render_block(float x, float y, AVFrame* frame, uint8_t* src[]) {
	
	src[0] = static_cast<uint8_t*>(av_malloc(Y_SIZE));
	src[1] = static_cast<uint8_t*>(av_malloc(U_SIZE));
	src[2] = static_cast<uint8_t*>(av_malloc(V_SIZE));
	
	int x_int = static_cast<int>(x);
	int y_int = static_cast<int>(y);
	// 计算当前坐标是哪个宏块
	int block_x = x / 16;
	int block_y = y / 16;
	
	int cb_block_x = block_x / 2;
	int cb_block_y = block_y / 2;
	
	// y
	uint8_t* y_start = frame->data[0] +
	(block_y * 16) * frame->linesize[0] +  // 行偏移
	(block_x * 16);                        // 列偏移
	for (int row = 0; row < 16; row++) {
		const uint8_t* src_row = y_start + row * frame->linesize[0];
		std::memcpy(src[0] + (row * 16), src_row, 16);
	}
	
	// === 提取 Cb 分量 ===
	uint8_t* cb_start = frame->data[1] +
	(cb_block_y * 8) * frame->linesize[1] +  // 行偏移
	(cb_block_x * 8);                        // 列偏移
	
	for (int row = 0; row < 8; row++) {
		const uint8_t* src_row = cb_start + row * frame->linesize[1];
		std::memcpy(src[1] + (row * 8), src_row, 8);
	}
	
	// === 提取 Cr 分量 ===
	uint8_t* cr_start = frame->data[2] +
	(cb_block_y * 8) * frame->linesize[2] +  // 行偏移
	(cb_block_x * 8);                        // 列偏移
	
	for (int row = 0; row < 8; row++) {
		const uint8_t* src_row = cr_start + row * frame->linesize[2];
		std::memcpy(src[2] + (row * 8), src_row, 8);
	}
	
	// 渲染当前宏块到图像
	struct SwsContext*	sws_ctx;
	sws_ctx = sws_getContext(16,
							 16,
							 AV_PIX_FMT_YUV420P,
							 16,
							 16,
							 AV_PIX_FMT_RGBA,
							 SWS_BILINEAR,
							 NULL, NULL, NULL);
	/**
	 初始数据分配
	 */
	int linesize[3] = {16, 8, 8};
	
	AVFrame* frame2 = av_frame_alloc();
	frame2->width = 16;
	frame2->height = 16;
	frame2->format = AV_PIX_FMT_RGBA;
	av_frame_get_buffer(frame2, 0);
	
	sws_scale(sws_ctx,
			  (uint8_t const **)src,
			  linesize,
			  0,
			  frame2->height,
			  frame2->data,
			  frame2->linesize);
	sws_freeContext(sws_ctx);
	
	glBindTexture(GL_TEXTURE_2D, texture_ids[1]);
	glTexImage2D(GL_TEXTURE_2D,
				 0,
				 GL_RGBA,
				 16,
				 16,
				 0,
				 GL_RGBA,
				 GL_UNSIGNED_BYTE,
				 frame2->data[0]);
	
	av_free(frame2);
}

/**
 帧渲染
 */
void render_image(AVFrame* frame) {
	
	AVFrame* frame2 = av_frame_alloc();
	struct SwsContext*	sws_ctx;
	
	sws_ctx = sws_getContext(frame->width,
							 frame->height,
							 (AVPixelFormat)frame->format,
							 frame->width,
							 frame->height,
							 AV_PIX_FMT_RGBA,
							 SWS_BILINEAR,
							 NULL, NULL, NULL);
	
	if (sws_ctx == NULL) {
		av_log(NULL, AV_LOG_ERROR, "sws context init error\n");
		return;
	}
	
	frame2->width = frame->width;
	frame2->height = frame->height;
	frame2->format = AV_PIX_FMT_RGBA;
	av_frame_get_buffer(frame2, 0);
	
	sws_scale(sws_ctx,
			  (uint8_t const **)frame->data,
			  frame->linesize,
			  0,
			  frame2->height,
			  frame2->data,
			  frame2->linesize);
	sws_freeContext(sws_ctx);
	
	glBindTexture(GL_TEXTURE_2D, texture_ids[0]);
	glTexImage2D(GL_TEXTURE_2D,
				 0,
				 GL_RGBA,
				 frame2->width,
				 frame2->height,
				 0,
				 GL_RGBA,
				 GL_UNSIGNED_BYTE,
				 frame2->data[0]);
	av_free(frame2);
	
}

int main(int argc, char* argv[]) {
	
	int ret = -1;
	ret = avformat_open_input(&format_ctx, file, NULL, NULL);
	if (ret < 0) {
		av_log(NULL, AV_LOG_ERROR, "Format open error: %s\n", av_err2str(ret));
		return -1;
	}
	
	if (avformat_find_stream_info(format_ctx, NULL) < 0) {
		printf("文件探测流信息失败");
	}
	
	video_stream = av_find_best_stream(format_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
	if (video_stream == -1) {
		av_log(NULL, AV_LOG_ERROR, "do not find video stream\n");
		return -1;
	}
	
	codec = avcodec_find_decoder(format_ctx->streams[video_stream]->codecpar->codec_id);
	if (codec == NULL) {
		av_log(NULL, AV_LOG_ERROR, "do not find encoder\n");
		return -1;
	}
	
	codec_ctx = avcodec_alloc_context3(codec);
	ret = avcodec_parameters_to_context(codec_ctx, format_ctx->streams[video_stream]->codecpar);
	if (ret < 0) {
		av_log(NULL, AV_LOG_ERROR, "codec context init error: %s\n", av_err2str(ret));
		return -1;
	}
	
	ret = avcodec_open2(codec_ctx, codec, NULL);
	if (ret < 0) {
		av_log(NULL, AV_LOG_ERROR, "encoder open error: %s\n", av_err2str(ret));
		return -1;
	}
	
	AVFrame* frame = av_frame_alloc();
	AVPacket packet;
	
	
	while (!av_read_frame(format_ctx, &packet)) {
		
		if (packet.stream_index == video_stream) {
			
			ret = avcodec_send_packet(codec_ctx, &packet);
			if (ret < 0) {
				av_log(NULL, AV_LOG_ERROR, "packet send error: %s\n", av_err2str(ret));
			}
			
			while (!avcodec_receive_frame(codec_ctx, frame)) {
				struct Frame frame_node;
				
				AVFrame* cpy_frame = av_frame_alloc();
				cpy_frame->width = codec_ctx->width;
				cpy_frame->height = codec_ctx->height;
				cpy_frame->format = codec_ctx->pix_fmt;
				av_frame_get_buffer(cpy_frame, 0);
				ret = av_frame_copy_props(cpy_frame, frame);
				ret = av_frame_copy(cpy_frame, frame);
				if (ret < 0) {
					av_log(NULL, AV_LOG_ERROR, "frame copy error:%s\n", av_err2str(ret));
				}
				
				frame_node.frame = cpy_frame;
				frame_node.duration = frame->duration;
				frame_node.pts = frame->pts;
				frame_node.pict_type = frame->pict_type;
				
				frame_vector.push_back(frame_node);
			}
		}
		
		av_packet_unref(&packet);
	}
	
	// 1. 初始化SDL
	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER) != 0) {
		SDL_Log("SDL Initialization Failed! %s", SDL_GetError());
		return -1;
	}
	
	// 2. 创建窗口
	SDL_Window* window = SDL_CreateWindow(
										  "ImGui + SDL2 Example",
										  SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
										  WIDTH, HEIGHT,
										  SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE
										  );
	
	if (!window) {
		SDL_Log("Window Creation Failed! %s", SDL_GetError());
		return -1;
	}
	
	// 3. 创建OpenGL上下文
	SDL_GLContext gl_context = SDL_GL_CreateContext(window);
	if (!gl_context) {
		SDL_Log("OpenGL Context Creation Failed! %s", SDL_GetError());
		return -1;
	}
	
	// 4. 初始化ImGui
	IMGUI_CHECKVERSION();
	ImGui::CreateContext();
	ImGuiIO& io = ImGui::GetIO();
	io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
	
	// 5. 设置ImGui风格和主题
	ImGui::StyleColorsDark();
	
	// 6. 初始化 ImGui的SDL和OpenGL后端
	ImGui_ImplSDL2_InitForOpenGL(window, gl_context);
	ImGui_ImplOpenGL3_Init("#version 120"); // 根据OpenGL版本调整
	
	// 初始化纹理
	glGenTextures(1, texture_ids);
	for (int i=0; i<2; i++) {
		glBindTexture(GL_TEXTURE_2D, texture_ids[i]);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	}
	
	// 计算每一帧的延迟
	delay =  1/av_q2d(format_ctx->streams[video_stream]->avg_frame_rate)*1000;
	SDL_CreateThread(play_thread, "play_thread", NULL);
	
	// 7. 主循环
	bool is_running = true;
	while (is_running) {
		SDL_Event event;
		while (SDL_PollEvent(&event)) {
			ImGui_ImplSDL2_ProcessEvent(&event);
			if (event.type == SDL_QUIT)
				is_running = false;
			if (event.type == SDL_KEYDOWN) {
				
			}
		}
		
		// 开始 ImGui帧
		ImGui_ImplOpenGL3_NewFrame();
		ImGui_ImplSDL2_NewFrame();
		ImGui::NewFrame();
		
		// ---------------
		// 在此处编写UI逻辑:
		ImGui::Begin("Hello, World!");           // 开始窗口
		
		
		ImGui::BeginGroup();
		
		frame = frame_vector.at(current_index).frame;
		render_image(frame);
		ImGui::Image((ImTextureID)(intptr_t)texture_ids[0],
					 ImVec2(codec_ctx->width, codec_ctx->height));
		
		// 获取 Image 的矩形区域
		ImVec2 img_min = ImGui::GetItemRectMin(); // 左上角坐标(全局坐标)
		ImVec2 img_max = ImGui::GetItemRectMax(); // 右下角坐标(全局坐标)
		ImVec2 mouse_global = ImGui::GetMousePos();
		if (ImGui::IsItemHovered()) {
			// 计算局部坐标
			ImVec2 local_mouse = ImVec2(
										mouse_global.x - img_min.x,
										mouse_global.y - img_min.y
										);
			// 显示坐标(归一化到纹理比例)
			float normalized_x = local_mouse.x / codec_ctx->width;
			float normalized_y = local_mouse.y / codec_ctx->height;
			
			uint8_t* src[3];
			render_block(local_mouse.x, local_mouse.y, frame, src);
			
			// 打印YUV数据
			ImGui::SameLine();
			ImGui::BeginGroup();
			std::string y_str;
			for (int i=0; i<16; i++) {
				for (int j=0; j<16; j++) {
					char buffer[3];
					snprintf(buffer, sizeof(buffer), "%02X", *(src[0] + i*16+j));
					y_str += buffer;
				}
				y_str.append("\n");
			}
			ImGui::Text(y_str.c_str());
			
			std::string u_str;
			for (int i=0; i<8; i++) {
				for (int j=0; j<8; j++) {
					char buffer[3];
					snprintf(buffer, sizeof(buffer), "%02X", *(src[1] + i*8+j));
					u_str += buffer;
				}
				u_str.append("\n");
			}
			ImGui::Text(u_str.c_str());
			
			std::string v_str;
			for (int i=0; i<8; i++) {
				for (int j=0; j<8; j++) {
					char buffer[3];
					snprintf(buffer, sizeof(buffer), "%02X", *(src[2] + i*8+j));
					v_str += buffer;
				}
				v_str.append("\n");
			}
			ImGui::Text(v_str.c_str());
			
			for (int i=0; i<3; i++) {
				av_free(src[i]);
			}
			ImGui::EndGroup();
			
			ImGui::Text("Relative Coordinates: %.2f, %.2f", local_mouse.x, local_mouse.y);
			ImGui::Text("Normalized: %.2f, %.2f", normalized_x, normalized_y);
			ImGui::Image((ImTextureID)(intptr_t)texture_ids[1],
						 ImVec2(16, 16));
			
			// 鼠标宏块显示
			int block_x = local_mouse.x / 16;
			int block_y = local_mouse.y / 16;
			ImVec2 rect_min(img_min.x + block_x * 16, img_min.y + block_y * 16);
			ImVec2 rect_max(rect_min.x + 16, rect_min.y + 16);
			ImGui::GetWindowDrawList()->AddRect(
												rect_min,
												rect_max,
												ImGui::GetColorU32(ImGuiCol_Header), // 红色
												0.0f,
												0,
												4.0f // 线宽
												);
		}
		
		
		ImGui::BeginGroup();
		if (ImGui::Button("<<")) {
			delay*=2;
		}
		ImGui::SameLine();
		if (ImGui::Button("stop")) {
			is_pause = !is_pause;
		}
		ImGui::SameLine();
		if (ImGui::Button(">>")) {
			delay/=2;
		}
		ImGui::EndGroup();
		
		ImGui::BeginGroup();
		ImGui::Text("decoder&encoder");
		ImGui::Text("decoder: %s", codec->name);
		ImGui::Text("time_base: %f", av_q2d(format_ctx->streams[video_stream]->time_base));
		ImGui::Text("frame_rate: %.2f", av_q2d(format_ctx->streams[video_stream]->avg_frame_rate));
		ImGui::Text("frame_size: %lld", format_ctx->streams[video_stream]->nb_frames);
		ImGui::Text("bit_rate: %lld", codec_ctx->bit_rate);
		ImGui::Text("duration: %lld", format_ctx->duration);
		ImGui::EndGroup();
		
		ImGui::SameLine();
		ImGui::BeginGroup();
		ImGui::Text("SPS&PPS");
		ImGui::Text("profile_idc: %d", codec_ctx->profile);
		ImGui::Text("profile_name: %s", av_get_profile_name(codec, codec_ctx->profile));
		ImGui::Text("level_idc: %d", codec_ctx->level);
		ImGui::Text("frame_cropping_flag: %d", codec_ctx->apply_cropping);
		ImGui::Text("gop_size: %d", codec_ctx->gop_size);
		ImGui::EndGroup();
		
		ImGui::SameLine();
		ImGui::BeginGroup();
		ImGui::Text("frame detail");
		ImGui::Text("pts: %lld", frame->pts);
		ImGui::Text("duration: %lld", frame->duration);
		ImGui::Text("pict_type: %c", av_get_picture_type_char(frame->pict_type));
		ImGui::Text("format: %d", frame->format);
		ImGui::Text("pkt_size: %d", frame->pkt_size);
		ImGui::Text("pkt_pos: %d", frame->pkt_pos);
		ImGui::Text("pkt_dts: %lld", frame->pkt_dts);
		ImGui::Text("play_timestamp: %.2f", getCurrentTimeStamp());
		ImGui::EndGroup();
		
		ImGui::SameLine();
		
		ImGui::EndGroup();
		
		
		ImGui::SameLine();
		ImGui::BeginGroup();
		ImGui::BeginChild("ScrollArea", ImVec2(0, codec_ctx->height), true, ImGuiWindowFlags_HorizontalScrollbar);
		for(int i=0;i<frame_vector.size();i++) {
			if (frame_vector.at(i).frame->pict_type == AV_PICTURE_TYPE_I) {
				ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.0f, 0.0f, 1.0f));
				
			}
			else if (frame_vector.at(i).frame->pict_type == AV_PICTURE_TYPE_P) {
				ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.5f, 0.0f, 1.0f));
			}
			else {
				ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.5f, 1.0f));
			}
			if (ImGui::Button(get_frame_title(i))) {
				current_index = i;
			}
			// 恢复样式
			ImGui::PopStyleColor();
		}
		ImGui::EndChild();
		ImGui::EndGroup();
		ImGui::End();
		
		// ---------------
		
		// 渲染
		ImGui::Render();
		glViewport(0, 0, WIDTH, HEIGHT);
		glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);
		ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
		
		// 切换缓冲区
		SDL_GL_SwapWindow(window);
	}
	
	// 8. 清理和退出
	av_packet_unref(&packet);
	
	for(Frame f : frame_vector) {
		av_free(f.frame);
	}
	
	avcodec_free_context(&codec_ctx);
	avformat_free_context(format_ctx);
	
	ImGui_ImplOpenGL3_Shutdown();
	ImGui_ImplSDL2_Shutdown();
	ImGui::DestroyContext();
	
	SDL_GL_DeleteContext(gl_context);
	SDL_DestroyWindow(window);
	SDL_Quit();
	
	return 0;
}
相关推荐
软行41 分钟前
LeetCode 每日一题 2845. 统计趣味子数组的数目
数据结构·c++·算法·leetcode
小贾要学习1 小时前
【C++】继承----下篇
android·java·c++
未来可期LJ1 小时前
【Test】单例模式❗
开发语言·c++
我想进大厂1 小时前
图论---染色法(判断是否为二分图)
数据结构·c++·算法·深度优先·图论
纪元A梦1 小时前
华为OD机试真题——推荐多样性(2025A卷:200分)Java/python/JavaScript/C++/C语言/GO六种最佳实现
java·javascript·c++·python·华为od·go·华为od机试题
OpenC++3 小时前
【C++QT】Buttons 按钮控件详解
c++·经验分享·qt·leetcode·microsoft
xiaoh_73 小时前
解决视频处理中的 HEVC 解码错误:Could not find ref with POC xxx【已解决】
python·ffmpeg·音视频
YuforiaCode3 小时前
第十二届蓝桥杯 2021 C/C++组 直线
c语言·c++·蓝桥杯
王江奎4 小时前
Android FFmpeg 交叉编译全指南:NDK编译 + CMake 集成
android·ffmpeg
草莓啵啵~4 小时前
搜索二叉树-key的搜索模型
数据结构·c++