【FFmpeg解码实战】(7)从零实现FFmpeg4.3 + SDL2视频播放器 - Video_Player 主类实现
发布日期:2021-06-29 14:55:26 浏览次数:3 分类:技术文章

本文共 8592 字,大约阅读时间需要 28 分钟。

【FFmpeg解码实战】(7)从零实现FFmpeg4.3 + SDL2视频播放器 - Video_Player 主类实现

本系列文章列表:

  1. 《》
  2. 《》
  3. 《》
  4. 《》
  5. 《》
  6. 《》
  7. 《》

本章开始,我们使用C++面向对象的思路来实现音视频播放器,主类为 Video_Player。

本次代码主要希望写的尽量规范、实现各部分模块化,最重要的是,写满详细的注释 !!!
代码要写的简单,还要写的美。

思路:

由于音视频播放器主要是由 ffmpeg 部分 及 SDL 部分来实现,因此在代码中,将这两者分别包装成独立的类。
ffmpe 类为:class Video_Player
SDL 类为:class SDL_Player

ffmpeg 类中负责,初始化音视频,解码,以及输出解码后的音频及视频。

SDL 类中主要负责,初始化 SDL UI界面,以及 播放 外部传来的 音频及视频。
这样将两者完全独立开来的好处在于,可以快速的将这两者中的一个替换成其他功能类似的模块。

目前,我们先实现音、视频 同步播放的功能,后续再增加按键 / 鼠标 以及 字幕解码显示 等功能,一步步的增加这个视频器的功能。

一、class Video_Player 类实现

1.1 class Video_Player 类定义

// 文件主类class Video_Player {
public: static Video_Player* get_instance(); // 唯一暴露对外的接口 int Open_Video(const char* filename); // 打开并播放音频 // packet 操作 int Get_stop_read_packet_flag(void); // 判断是否需要暂停读取 packet包 void Set_stop_read_packet_flag(int flag); // 配置暂停读取 packet包,停止 read packet包线程 int Read_packet(AVPacket* pkt); // 函数封装:读取一个packet包 void Get_stream_index(int* audio, int* video);// 函数封装:获取音视频的stream index void Packet_queue_put(AVPacket* pkt); // 将packet包放入队列 void Packet_queue_get(int stream_id, AVPacket* pkt); //从队列读取一个packet包 int Is_packet_queue_full(int stream_id); // 查询packet 队列是否满了 int Get_packet_queue_nb_packets(int stream_id); // 获得队列中包的数量protected: Video_Player(); ~Video_Player(); int Queue_Init(void); // 初始化音视频队列 int Clock_Init(void); // 初始化音视频参考时钟 int Demux_Init(void); // 初始化解复用器,创建解析packet线程 int Audio_Codec_Init(void); // 初始化音频解码器,创建音频解码线程 int Video_Codec_Init(void); // 初始化视频解码器,创建视频解码线程private: video_player_t* m_player; // 视频结构体};

Video_Player 类中采用的 是单例模式,

static Video_Player* g_instance = nullptr;

暴露对外的接口为 get_instanceOpen_Video

用户只能通过 get_instance 来创建或者获得当前class Video_Player的对象 g_instance

用户如果想播放一个视频文件,或者音频文件,

直接通过 Video_Player::get_instance()->Open_Video("filename") 就能快速的实现打开、解码、播放视频。

下面,根据代码运行调用流程,一步一步的来介绍 class Video_Player 中的方法的实现过程。

1.2 struct video_player_t 核心结构体实现

video_player_t 核心结构体中,包含了所有会用到的结构体变量,详细见注释。

// 视频文件结构体typedef struct {
char* filename; // 文件名 AVFormatContext* p_fmt_ctx; // 文件上下文结构体指针 AVStream* p_audio_stream; // 音频流结构体指针 AVStream* p_video_stream; // 视频流结构体指针 AVCodecContext* p_audio_codec_ctx; // 音频解码器上下文结构体指针 AVCodecContext* p_video_codec_ctx; // 视频解码器上下文结构体指针 int audio_index; int video_index; play_clock_t audio_clk; // 音频时钟 play_clock_t video_clk; // 视频时钟 packet_queue_t audio_pkt_queue; // 音频packet链表 packet_queue_t video_pkt_queue; // 视频packet链表 frame_queue_t audio_frame_queue; // 音频frame帧链表 frame_queue_t video_frame_queue; // 视频frame帧链表 AVFrame* p_frame_yuv; // 转变为YUV格式的视频帧 audio_param_t audio_param_src; // 转换格式前的音频参数 audio_param_t audio_param_dst; // 转换格式后的音频参数 int stop_read_packet_flag; // 停止读取packet 信号,会停止 read packet 线程}video_player_t;

1.3 Video_Player() 构造方法

在构造方法中,主要是基础功能的初始化,功能类似基础设施。

包括:

  1. 分配私有结构体 video_player_t* m_player;的内存。
  2. 初始化音/视频 packet list 链表,初始化音/视频 frame 帧队列。
  3. 初始化音/视频参考时钟。
  4. 初始化SDL UI界面(待实现)
Video_Player::Video_Player(){
g_failed_flag = false; if (m_player != NULL) {
cout << __func__ << ": m_player != NULL !!!\n"; g_failed_flag = true; return; } // 分配并清零 视频结构体内存 m_player = (video_player_t*)av_mallocz(sizeof(video_player_t)); if (!m_player) {
cout << __func__ << ": video_player_t(video_player_t) Failed !!!\n"; g_failed_flag = true; return; } m_player->stop_read_packet_flag = 0; // 停止读取 packet包信号 m_player->audio_index = -1; // 音频流 stream id m_player->video_index = -1; // 视频流 stream id // 初始化音视频队列 if (Queue_Init() < 0) {
cout << __func__ << ": Queue_Init() Failed !!!\n"; g_failed_flag = true; return; } // 初始化音视频时钟 if (Clock_Init() < 0) {
cout << __func__ << ": Clock_Init() Failed !!!\n"; g_failed_flag = true; return; } // SDL 类对象初始化 // TODO:}

1.4 ~Video_Player() 析构函数

当用户调用 delete 释放对象实例时,会自动调用当前析构函数,释放所申请的所有内存。

析构函数调用时机,目前代码暂定如下两种场景:

  1. 实例化对象对程出错时,会调用 delete 删除对象实例。
  2. 播放音/视频结束 或 中止播放时,会自动调用 delete删除对象实例。
Video_Player::~Video_Player(){
if (m_player != NULL) {
// 清除audio queue for (int i = 0; i < m_player->audio_frame_queue.max_size; i++) {
if (m_player->audio_frame_queue.queue[i].frame != NULL) {
av_frame_free(&(m_player->audio_frame_queue.queue[i].frame)); } } // 清除video queue for (int i = 0; i < m_player->video_frame_queue.max_size; i++) {
if (m_player->video_frame_queue.queue[i].frame != NULL) {
av_frame_free(&(m_player->video_frame_queue.queue[i].frame)); } } // 释放 AVFormatContext 内存 if (m_player->p_fmt_ctx != NULL) {
avformat_close_input(&m_player->p_fmt_ctx); // 关闭文件 avformat_free_context(m_player->p_fmt_ctx); // 释放上下文内存 } // 释放 filename 内存 if (m_player->filename != NULL) {
av_freep(&m_player->filename); } // 释放视频结构体对象内存 av_freep(m_player); } cout << __func__ << ": success ^_^\n";}

1.5 get_instance() 创建 / 获得类对象

get_instance() 方法中,如果当前类未实例化,则做实例化,自动运行类构造函数。

如果当前类已经实例化过了,则直接返回类实例化的对象 g_instance

在实例化过程中,一旦出错,检测到 g_failed_flag == true 则直接调用 类析构函数,释放申请的内存,并返回用户 NULL空指针表示实例化失败。

Video_Player* Video_Player::get_instance(){
if (g_instance == NULL) {
g_instance = new Video_Player; if (g_failed_flag == true && g_instance != nullptr) {
cout << __func__ << ": new Video_Player Failed !!!\n"; delete(g_instance); g_instance = nullptr; } } return g_instance;}

1.6 Open_Video() 打开、解码、播放 音 / 视频主函数

Open_Video() 方法可以说是主函数,通过这 一个函数,就能够实现视频播放的整个流程,简化调用者的代码。

包括如下:

  1. 打开音 / 视频文件
  2. 将音/ 视 频文件解复用,创建线程,将音视频中的packet 包保存在对应的音频 / 视频 packet list 中。
  3. 如果存在视频,则创建视频解码器,创建线程,对视频 packet list 上的包进行解码,将解码后的 frame 帧保存在 视频帧队列中。
  4. 如果存在音频,则创建音频解码器,创建线程,对音频 packet list 上的包进行解码,将解码后的 frame 帧保存在 音频帧队列中。
  5. 创建对应的 SDL 音频 或 视频 线程,负责将将 视频 / 音频 帧队列上的数据通过SDL 显示/ 播放出来。
  6. 在Open_Video() 过程一旦出错,则直接调用 delete ,自动析构所申请的资源。
// 打开并播放音频int Video_Player::Open_Video(const char* filename){
if (m_player == NULL) {
if (get_instance() == NULL) {
return -1; } } m_player->filename = av_strdup(filename); if (m_player->filename == nullptr) {
cout << __func__ << ": av_strdup failed, filename = " << filename << endl; goto Failed; } // 将文件解复用,创建线程读取所有的 packet 保存在packet_queue_t 链表上. if (Demux_Init() < 0) {
cout << __func__ << ": Demux_Init() Failed !!!\n"; goto Failed; } // 如果存在音频流,则初始化音频解码器,创建线程开始解码并播放 if (m_player->audio_index != -1) {
if (Audeo_Codec_Init() < 0) {
cout << __func__ << ": Audeo_Codec_Init() Failed !!!\n"; goto Failed; } // TODO: 初始化 SDL 音频 ,创建SDL 音频播放线程 } // 如果存在视频流,则初始化视频解码器,创建线程开始解码并播放 if (m_player->video_index != -1) {
// TODO // TODO: 初始化 SDL 视频 ,创建SDL 音频播放线程 } cout << __func__ << ": success ^_^\n"; return 0;Failed: if (g_instance != NULL) {
delete(g_instance); }}

1.7 Demux_Init() 解复用初始化

从class 定义上可以看到,解复用函数 Demux_Init() 为 protected 类型的函数,并不会暴露给用户调用。

主要功能包括:

  1. 打开音视频文件
  2. 获取文件中的所有音频 / 视频流信息。
  3. 创建解复用线程,负责将解复用后的 音频/视频流 分别保存在对应的 packet list 上。
// 初始化解复用器,创建解析packet线程int Video_Player::Demux_Init(void){
if (m_player == NULL) return -1; AVFormatContext* p_fmt_ctx = NULL; // 1. 分配文件格式上下文结构体内存 p_fmt_ctx = avformat_alloc_context(); if (!p_fmt_ctx) {
cout << __func__ << ": avformat_alloc_context() Failed !!!\n"; return AVERROR(ENOMEM); } // 2. 打开文件 if (avformat_open_input(&p_fmt_ctx, m_player->filename, NULL, NULL) < 0) {
cout << __func__ << ": avformat_open_input() Failed !!!\n"; return -1; } m_player->p_fmt_ctx = p_fmt_ctx; // 3. 获取视频中所有流的信息,保存在 p_fmt_ctx->streams数组中,流数量为 p_fmt_ctx->p_fmt_ctx->nb_streams if (avformat_find_stream_info(p_fmt_ctx, NULL) < 0) {
cout << __func__ << ": avformat_find_stream_info() Failed !!!\n"; return -1; } // 4. 打印流信息 av_dump_format(p_fmt_ctx, 0, m_player->filename, 0); // 5. 查找第一个音频流/视频流 for (int i = 0; i < p_fmt_ctx->nb_streams; i++) {
AVStream* p_stream = p_fmt_ctx->streams[i]; if (p_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && m_player->audio_index == -1) {
m_player->audio_index = i; } else if (p_stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && m_player->video_index == -1) {
m_player->video_index = i; } if (m_player->audio_index != -1 && m_player->video_index != -1) {
m_player->p_audio_stream = p_fmt_ctx->streams[m_player->audio_index]; m_player->p_video_stream = p_fmt_ctx->streams[m_player->video_index]; cout << __func__ << ": 找到音频流 index=" << m_player->audio_index << " 视频流 index=" << m_player->video_index << endl; } } // 6. 存在只有音频或只有视频的情况,只要有一个流,就创建线程,开始解析 packet if (m_player->audio_index != -1 || m_player->video_index != -1) {
// 7. 创建线程,开始解析音视频packet 包 thread p_demux_thread = thread(demux_packet_thread_func); p_demux_thread.detach(); } else {
cout << __func__ << ": Failed !!!\n"; return -1; } return 0;}

1.8 demux_packet_thread_func() 解复用线程函数

转载地址:https://ciellee.blog.csdn.net/article/details/110535667 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:FFmpeg 和 SDL 教程 Part 1 - 分离音、视频数据
下一篇:【FFmpeg解码实战】(6)从零实现FFmpeg4.3 + SDL2视频播放器

发表评论

最新留言

做的很好,不错不错
[***.243.131.199]2024年04月21日 00时23分16秒