基于 FFmpeg 的跨平台视频播放器简明教程(十一):一种简易播放器的架构介绍

系列文章目录

  1. 基于 FFmpeg 的跨平台视频播放器简明教程(一):FFMPEG + Conan 环境集成
  2. 基于 FFmpeg 的跨平台视频播放器简明教程(二):基础知识和解封装(demux)
  3. 基于 FFmpeg 的跨平台视频播放器简明教程(三):视频解码
  4. 基于 FFmpeg 的跨平台视频播放器简明教程(四):像素格式与格式转换
  5. 基于 FFmpeg 的跨平台视频播放器简明教程(五):使用 SDL 播放视频
  6. 基于 FFmpeg 的跨平台视频播放器简明教程(六):使用 SDL 播放音频和视频
  7. 基于 FFmpeg 的跨平台视频播放器简明教程(七):使用多线程解码视频和音频
  8. 基于 FFmpeg 的跨平台视频播放器简明教程(八):音画同步
  9. 基于 FFmpeg 的跨平台视频播放器简明教程(九):Seek 策略
  10. 基于 FFmpeg 的跨平台视频播放器简明教程(十):在 Android 运行 FFmpeg

前言

一个视频播放器需要的模块大致包括:

  • 视频解码
  • 音频解码
  • 视频画面输出
  • 音频播放
  • 图像格式转换
  • 音频重采样
  • 音画同步

经过前九章的学习,我们已经对以上模块有了深入的理解和实践。然而,目前的代码实现较为零散,缺乏统一的组织和抽象。

接下来,我们将进入移动端播放器的设计与开发阶段。为了能够最大限度地复用现有的模块和代码,我们需要对现有的代码进行整理和优化,形成一种有效的架构。本文将介绍一种简单但实用的架构,它能够满足我们的需求。

这种架构虽然简单,但是能够满足我们的需求。

架构介绍

在这里插入图片描述
整体框架如上图,每个模块职责清晰,其中:

  1. Decoder,负责解码音视频数据
  2. Source,负责提供音频/视频数据
  3. Output,负责显示画面,和播放音频

接下来对各个模块做详细说明。

音频/视频解码,Audio/Video Decoder

namespace j_video_player {
class IVideoDecoder {
public:
  virtual ~IVideoDecoder() = default;

  /**
   * open a video file
   * @param file_path video file path
   * @return 0 if success, otherwise return error code
   */
  virtual int open(const std::string &file_path) = 0;

  /**
   * check if the decoder is valid
   * @return true if valid, otherwise return false
   */
  virtual bool isValid() = 0;

  /**
   * close the decoder
   */
  virtual void close() = 0;

  /**
   * decode next frame
   * @return a shared_ptr of VideoFrame if success, otherwise return nullptr
   */
  virtual std::shared_ptr<Frame> decodeNextFrame() = 0;

  /**
   * seek to a timestamp quickly and get the video frame
   *
   * @param timestamp the timestamp(us) to seek
   * @return video frame if success, otherwise return nullptr
   */
  virtual std::shared_ptr<Frame> seekFrameQuick(int64_t timestamp) = 0;

  /**
   * seek to a timestamp precisely and get the video frame
   * @param timestamp the timestamp(us) to seek
   * @return video frame if success, otherwise return nullptr
   */
  virtual std::shared_ptr<Frame> seekFramePrecise(int64_t timestamp) = 0;

  /**
   * get the current position of the decoder
   * @return the current position(us)
   */
  virtual int64_t getPosition() = 0;

  virtual MediaFileInfo getMediaFileInfo() = 0;
};
} // namespace j_video_player

视频解码接口如上,其中

  • open(),即打开文件。打开后可以通过 getMediaFileInfo 获取文件的媒体信息,例如视频宽高、音频采样率等等
  • decodeNextFrame,顺序解码,获取下一帧数据
  • seekFrameQuick,快速 seek,但不保证精确
  • seekFramePrecise,精确 seek,可能更加耗时
  • getPosition,获取当前解码的位置,单位微妙(us)

音频解码接口与视频的一模一样,这是因为对于解码器而言,无论音频帧还是视频帧都是 frame,因此两边接口是一致的。

在实现上,我们使用 ffmpeg 实现了上述音频/视频解码接口。

«interface»
IVideoDecoder
open()
close()
decodeNextFrame()
seekFrameQuick()
seekFramePrecise()
FFmpegAVDecoder
«interface»
IAudioDecoder
open()
close()
decodeNextFrame()
seekFrameQuick()
seekFramePrecise()

具体实现请参考 FFmpegAVDecoder 源码

音频/视频源,Audio/Video Source


namespace j_video_player {
enum class SourceState {
  kIdle,
  kStopped,
  kPlaying,
  kSeeking,
  kPaused,
};
class ISource {
public:
  virtual ~ISource() = default;

  virtual int open(const std::string &file_path) = 0;
  virtual MediaFileInfo getMediaFileInfo() = 0;
  virtual int play() = 0;
  virtual int pause() = 0;
  virtual int stop() = 0;
  virtual int seek(int64_t timestamp) = 0;
  virtual SourceState getState() = 0;
  virtual int64_t getDuration() = 0;
  virtual int64_t getCurrentPosition() = 0;
  virtual std::shared_ptr<Frame> dequeueFrame() = 0;
  virtual int getQueueSize() = 0;
};

class IVideoSource : public ISource {
public:
  std::shared_ptr<Frame> dequeueFrame() override { return dequeueVideoFrame(); }
  virtual std::shared_ptr<Frame> dequeueVideoFrame() = 0;
};

class IAudioSource : public ISource {
public:
  std::shared_ptr<Frame> dequeueFrame() override { return dequeueAudioFrame(); }
  virtual std::shared_ptr<Frame> dequeueAudioFrame() = 0;
};

} // namespace j_video_player

ISource 类负责生产音频/视频帧,其中:

  1. open 即打开文件。打开后可以通过 getMediaFileInfo 获取文件的媒体信息,例如视频宽高、音频采样率等等
  2. playpausestop 负责 Source 的转态流转
  3. dequeueFrame 从队列中获取一个 Frame,通过这个接口,下游的消费者可以对音频/视频帧进行消费。
  4. IVideoSource 和 IAudioSource 继承自 ISource,并提供了额外的 dequeueVideoFramedequeueAudioFrame 方法
«interface»
ISource
open()
play()
pause()
stop()
«interface»
IVideoSource
dequeueVideoFrame()
«interface»
IAudioSource
dequeueAudioFrame()
SimpleSource

我们代码中的 SimpleSource 类是对 IVideoSourceIAudioSource 的具体实现。具体的:

  1. SimpleSource 持有一个 Decoder(VideoDecoder 或者 AudioDecoder ),内部使用 Decoder 进行音视频的解码。
  2. SimpleSource 拥有自己的解码线程,在调用 play 时将启动该线程。
  3. SimpleSource 拥有一个 Frame queue,默认大小为 3,也就是最多存放 3 帧数据,如果 queue 满了,则阻塞解码线程,等待消费者调用 dequeueFrame 消费数据

具体实现请参考 SimpleSource 源码

视频画面输出,VideoOutput


namespace j_video_player {
class VideoOutputParameters {
public:
  int width{0};
  int height{0};
  int fps{0};
  int pixel_format{0}; // AVPixelFormat
};

enum class OutputState { kIdle, kPlaying, kPaused, kStopped };

class IVideoOutput {
public:
  virtual ~IVideoOutput() = default;

  virtual int prepare(const VideoOutputParameters &parameters) = 0;
  virtual void attachVideoSource(std::shared_ptr<IVideoSource> source) = 0;
  virtual void attachImageConverter(
      std::shared_ptr<ffmpeg_utils::FFMPEGImageConverter> converter) = 0;
  virtual void
  attachAVSyncClock(std::shared_ptr<utils::ClockManager> clock) = 0;
  virtual int play() = 0;
  virtual int pause() = 0;
  virtual int stop() = 0;
  virtual OutputState getState() const = 0;
};
} // namespace j_video_player

IVideoOutput 类负责消费 Source 生产的视频帧,将其显示在窗口上。其中:

  1. prepare 用于进行一些初始化操作,例如根据 VideoOutputParameters 参数来设置输出窗口大小、像素格式等
  2. attachVideoSource,绑定一个 IVideoSource,意味着将从这个 Source 中获取数据(调用 dequeueVideoFrame 方法)
  3. attachImageConverter 方法用于绑定一个负责像素格式转换的类。这个类将无条件地将源发送过来的帧进行像素格式转换。从IVideoOutput的视角来看,它只知道要输出的格式,而无法知道源格式。因此,需要在外部设置转换器的参数。设置完成后,再将其附加到 IVideoOutput 上。
  4. attachAVSyncClock 方法用于绑定一个时钟对象,它负责纪录视频流和音频流的时间,IVideoOutput 可以利用时钟进行音画同步。
«interface»
IVideoOutput
prepare()
attachVideoSource()
attachImageConverter()
attachAVSyncClock()
play()
pause()
stop()
«interface»
BaseVideoOutput
drawFrame()
SDL2VideoOutput

BaseVideoOutput 继承自 IVideoOutput,BaseVideoOutput 内部启动另一个线程用于从 Source 中获取音频数据,并提供了 drawFrame 的虚方法用于图像上屏显示,具体实现细节参考 BaseVideoOutput,我们重点看线程做了啥:

void startOutputThread() {
    output_thread_ = std::make_unique<std::thread>([this]() {
      for (;;) {
        if (state_ == OutputState::kStopped || state_ == OutputState::kIdle) {
          break;
        } else if (state_ == OutputState::kPaused) {
          continue;
        } else if (state_ == OutputState::kPlaying) {
          if (source_ == nullptr) {
            LOGW("source is null, can't play. Please attach source first");
            break;
          }
          auto frame = source_->dequeueVideoFrame();
          if (frame == nullptr) {
            continue;
          }

          std::shared_ptr<Frame> frame_for_draw = convertFrame(frame);

          if (frame_for_draw != nullptr) {
            drawFrame(frame_for_draw);
            doAVSync(frame_for_draw->pts_d());
          }
        }
      }
    });
  }

当正在播放时,调用 source_->dequeueVideoFrame() 向源索取一帧;接着调用 convertFrame 方法将视频帧格式转换为预期的格式;然后,使用 drawFrame 方法将改帧渲染至屏幕;最后进行音画同步。

我们的代码中 SDL2VideoOutput 是对 BaseVideoOutput 的具体实现,具体细节请参考源码。

音频播放,AudioOutput


namespace j_video_player {
enum class AudioOutputState { kIdle, kPlaying, kStopped };
class AudioOutputParameters {
public:
  int sample_rate{44100};
  int channels{2};
  int num_frames_of_buffer{1024};

  bool isValid() const {
    return sample_rate > 0 && channels > 0 && num_frames_of_buffer > 0;
  }
};

class IAudioOutput {
public:
  virtual ~IAudioOutput() = default;

  virtual int prepare(const AudioOutputParameters &params) = 0;
  virtual void attachAudioSource(std::shared_ptr<IAudioSource> source) = 0;
  virtual void attachResampler(
      std::shared_ptr<ffmpeg_utils::FFmpegAudioResampler> resampler) = 0;
  virtual void
  attachAVSyncClock(std::shared_ptr<utils::ClockManager> clock) = 0;
  virtual int play() = 0;
  virtual int stop() = 0;
  virtual AudioOutputState getState() const = 0;
};
} // namespace j_video_player

IAudioOutput 负责播放音频,其中:

  1. prepare,用于一些初始化的操作,例如打开音频设备等
  2. attachAudioSource,绑定一个 Audio Source
  3. attachResampler 绑定一个 resampler 进行音频重采样。这个类将无条件地将源发送过来的音频进行重采样。从IAudioOutput的视角来看,它只知道要输出的格式,而无法知道源格式。因此,需要在外部设置重采样的参数。设置完成后,再将其附加到 IAudioOutput 上。
«interface»
IAudioOutput
prepare()
attachAudioSource()
attachResampler()
attachAVSyncClock()
play()
pause()
stop()
SDL2AudioOutput

我们的代码中 SDL2AudioOutput 是对 BaseVideoOutput 的具体实现,具体细节请参考源码。

组成播放器

各个模块已经讲解完毕,接下来只需要将他们组装起来,屏蔽一些细节就可以了。我们封装了一个 SimplePlayer 来做这样的事情,它使用起来非常简单,参考 my_tutorial08 :

int main(int argc, char *argv[]) {
  if (argc < 2) {
    printHelpMenu();
    return -1;
  }
  std::string in_file = argv[1];

  auto video_decoder = std::make_shared<FFmpegVideoDecoder>();
  auto audio_decoder = std::make_shared<FFmpegAudioDecoder>();
  auto video_source = std::make_shared<SimpleVideoSource>(video_decoder);
  auto audio_source = std::make_shared<SimpleAudioSource>(audio_decoder);

  auto video_output = std::make_shared<SDL2VideoOutput>();
  auto audio_output = std::make_shared<SDL2AudioOutput>();

  auto player =
      SimplePlayer{video_source, audio_source, video_output, audio_output};

  int ret = player.open(in_file);
  RETURN_IF_ERROR_LOG(ret, "open player failed, exit");

  auto media_file_info = player.getMediaFileInfo();

  VideoOutputParameters video_output_param;
  video_output_param.width = media_file_info.width;
  video_output_param.height = media_file_info.height;
  video_output_param.pixel_format = AVPixelFormat::AV_PIX_FMT_YUV420P;

  AudioOutputParameters audio_output_param;
  audio_output_param.sample_rate = 44100;
  audio_output_param.channels = 2;
  audio_output_param.num_frames_of_buffer = 1024;

  ret = player.prepare(video_output_param, audio_output_param);
  RETURN_IF_ERROR_LOG(ret, "prepare player failed, exit");

  player.play();
	
  // ....
}
  1. 创建好 Audio/VideoSource 和 Audio/VideoOutput 后,将他们塞到 SimplePlayer 构造函数即可
  2. player.open() 打开文件
  3. 设置 VideoOutputParameters 和 AudioOutputParameters,调用 prepare 函数进行一些初始化操作
  4. 使用 play/pause/stop/seek 等函数操作视频播放

SimplePlayer 具体实现请参考源码。

总结

本文对一种简易的播放器架构进行了说明,该架构下播放器被分为若干模块,包括 Audio/VideoSource,Audio/VideoOutput 等。通过该架构设计我们能够灵活的扩展解码、上屏、音频播放等模块。

参考

  • FFmpegAVDecoder
  • SimpleSource
  • SDL2VideoOutput
  • BaseVideoOutput
  • SDL2AudioOutput
  • my_tutorial08
  • SimplePlayer

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/166846.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

生成式大模型的RLHF技术(一):基础

一、概述 大语言模型&#xff08;LLMs&#xff09;在预训练的过程中通常会捕捉数据的特征&#xff0c;而这些训练数据通常既包含高质量的也包含低质量的&#xff0c;因此模型有时会产生不被期望的行为&#xff0c;如编造事实&#xff0c;生成有偏见或有毒的文本&#xff0c;甚至…

商业园区的万能管理法,还怪高级的咧!

随着社会的不断发展和科技的飞速进步&#xff0c;视频监控技术已经成为维护安全、提高效率以及实现智能化管理的关键工具。 在这个信息时代&#xff0c;人们对于安全和管理的需求不断提升&#xff0c;而视频监控系统作为一种强大而灵活的解决方案&#xff0c;正日益受到各行各业…

QQ同步通讯录,详细操作方法来了!

腾讯QQ是一款功能丰富的即时通信软件&#xff0c;能够让用户随时随地与好友保持联系&#xff0c;不受时间和地域限制&#xff0c;受到了广大用户的喜爱和信赖。 为了能够快速添加QQ好友&#xff0c;我们可以通过开启通讯录来实现。那么&#xff0c;qq同步通讯录如何操作呢&…

数字IC前端学习笔记:异步复位,同步释放

相关阅读 数字IC前端https://blog.csdn.net/weixin_45791458/category_12173698.html?spm1001.2014.3001.5482 异步复位 异步复位是一种常见的复位方式&#xff0c;可以使电路进入一个可知的状态。但是不正确地使用异步复位会导致出现意想不到的错误&#xff0c;复位释放便是…

新生儿奶藓:原因、科普和注意事项

引言&#xff1a; 新生儿奶藓是一种常见的婴儿皮肤问题&#xff0c;通常在生后的头几个月内出现。尽管奶藓对婴儿的健康没有太大影响&#xff0c;但了解其原因、科普相关信息以及采取适当的注意事项是帮助父母更好地照顾婴儿皮肤的关键。本文将深入探讨新生儿奶藓的原因、相关…

【pytorch深度学习 应用篇02】训练中loss图的解读,训练中的问题与经验汇总

文章目录 loss图解析train loss ↘ \searrow ↘ ↗ \nearrow ↗ 先降后升 loss图解析 train loss ↘ \searrow ↘ 不断下降&#xff0c;test loss ↗ \nearrow ↗ 不断上升&#xff1a;原因很多&#xff0c;我是把workers1&#xff0c;batchSize8192train loss ↘ \searro…

【Linux】vscode远程连接ubuntu,含失败解决方案

删除vscode远程连接 打开‪C:\Users\GIGA\.ssh\config文件&#xff0c;GIGA是windows下自己的用户名。 删除‪C:\Users\GIGA\.ssh\config文件里的所有内容&#xff0c;点击保存&#xff1b;然后刷新。 可以看出SSH 远程连接已经被删除了。 vscode远程连接ubuntu 在弹出的…

nginx静态网站部署

Nginx是一个HTTP的web服务器&#xff0c;可以将服务器上的静态文件&#xff08;如HTML、图片等&#xff09;通过HTTP协议返回给浏览器客户端 案例&#xff1a;将ace-master这个静态网站部署到Nginx服务器上 通过Xftp将ace-master到linux服务器/opt/static目录下&#xff0c;为…

Spring高级bean的实例化方法

bean的实例化方法 构造方法 实例化bean第一种&#xff1a;使用默认无参构造函数(常用) 第二种创建bean实例&#xff1a;静态工厂实例化&#xff08;了解&#xff09; 第三种&#xff1a;实例工厂&#xff08;了解&#xff09;与FactoryBean&#xff08;实用&#xff09;

这些好用的录屏专家,你都知道吗?(干货)

在数字时代&#xff0c;录制屏幕已经成为沟通、教育和创作的重要工具。无论您是一位教育者、企业家还是内容创作者&#xff0c;能够熟练地使用录屏软件将帮助您传达信息和创作内容。在本文中&#xff0c;我们将介绍三款优秀的录屏专家&#xff0c;以帮助您找到最适合自己需求的…

如何通过算法模型进行数据预测

当今数据时代背景下更加重视数据的价值&#xff0c;企业信息化建设会越来越完善&#xff0c;越来越体系化&#xff0c;以数据说话&#xff0c;通过数据为企业提升渠道转化率、改善企业产品、实现精准运营&#xff0c;为企业打造自助模式的数据分析成果&#xff0c;以数据驱动决…

springboot学习笔记

目录 概述 常见的SSM搭建项目弊端 什么是springboot 特点 1.简化部署 2.简化配置&#xff0c;注解代替xml 3.简化依赖配置 4.应用监控 springboot与springmvc&#xff0c;springcloud关系 创建springboot项目 spring4提供的注解 Spring的发展 Java配置 1.核心注解…

构造函数,原型对象,实例对象

1.构造函数、原型对象、实例对象三者分别是什么&#xff1f; 构造函数&#xff1a;用来创建对象的函数&#xff0c;创建实例对象的模板 。构造函数的函数名尽量首字母大写(为了区分普通函数和构造函数)原型对象&#xff1a;每一个函数在创建的时候&#xff0c;系统都会给分配一…

wpf devexpress 绑定数据编辑器

定义视图模型 打开前一个项目 打开RegistrationViewModel.cs文件添加如下属性到RegistrationViewModel类 [POCOViewModel] public class RegistrationViewModel {public static RegistrationViewModel Create() {return ViewModelSource.Create(() > new RegistrationVie…

振弦式渗压计的安装方式及注意要点

振弦式渗压计的安装方式及注意要点 振弦式渗压计是一种高精度、高效率的地下水位测量仪器。它可以测量地下水位的高度&#xff0c;计算地下水的压力&#xff0c;从而推算出地下水的流量。对于地下水资源管理和保护、治理工程等方面具有非常重要的意义。在安装振弦式渗压计时&a…

什么是媒体见证?媒体宣传有哪些好处?

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 一&#xff0c;什么是媒体见证&#xff1f; 媒体见证是指企业举办活动&#xff0c;发布会&#xff0c;邀请媒体现场采访的一种宣传方式&#xff0c;媒体到场后&#xff0c;对其进行记录…

金蝶云星空对接打通旺店通·旗舰奇门采购退料单查询接口与创建货品档案接口

金蝶云星空对接打通旺店通旗舰奇门采购退料单查询接口与创建货品档案接口 来源系统:金蝶云星空 金蝶K/3Cloud在总结百万家客户管理最佳实践的基础上&#xff0c;提供了标准的管理模式&#xff1b;通过标准的业务架构&#xff1a;多会计准则、多币别、多地点、多组织、多税制应用…

ModuleNotFoundError: No module named ‘pycocotools‘

cuda 12.1 pytorch 2.0.1 python 3.11 运行代码&#xff0c;报该错误&#xff0c;尝试了以下方法解决&#xff1a; 方法一 # step 1: 安装cython pip install Cython# step 2: 安装pycocotools pip install githttps://github.com/philferriere/cocoapi.git#eggpycocotools…

MacOs 删除第三方软件

AppStore下载的软件 如果删除AppStore下载的软件&#xff0c;直接长按软件&#xff0c;点击删除或拖到废纸篓就可以完成软件的删除 第三方软件 但是第三方下载的软件&#xff0c;无法拖进废纸篓&#xff0c;长按软件也没有右上角的小叉 可以通过以下方法实现对软件的卸载 …

EMQX vs Mosquitto | MQTT Broker 对比

物联网开发者需要为自己的物联网项目选择合适的 MQTT 消息产品或服务&#xff0c;从而构建可靠高效的基础数据层&#xff0c;保障上层物联网业务。目前市面上有很多开源的 MQTT 产品&#xff0c;在性能功能等方面各有优点。本文将选取目前最为流行的两个开源 MQTT Broker&#…