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

news2024/11/15 10:52:39

系列文章目录

  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.coloradmin.cn/o/1228986.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python用最小二乘法实现平面拟合

文章目录 数学原理代码实现测试 数学原理 平面方程可写为 A x B y C z D 0 AxByCzD0 AxByCzD0 假设 C C C不为0&#xff0c;则上式可以改写为 z a x b y d zaxbyd zaxbyd 则现有一组点 { p i } \{p_i\} {pi​}&#xff0c;则根据 x i , y i x_i,y_i xi​,yi​以及平面…

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

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

08.智慧商城——购物车布局、全选反选、功能实现

01. 购物车 - 静态布局 基本结构 <template><div class"cart"><van-nav-bar title"购物车" fixed /><!-- 购物车开头 --><div class"cart-title"><span class"all">共<i>4</i>件商品…

《微信小程序开发从入门到实战》学习十八

3.3 开发创建投票页面 3.3.5 数据的双向传递 通过上一小节的代码和预览效果可以看到使用时间函数可以将视图层传递到逻辑层。 视图层数据由小程序管理&#xff0c;逻辑层通常保存在data对象&#xff0c;必须由开发者自己管理。 微信开发工具的AppData的面板可以实时查看到页…

计算机网络必知必会——传输层TCP

&#x1f4d1;前言 本文主要SpringBoot通过DevTools实现热部署的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是青衿&#x1f947; ☁️博客首页&#xff1a;CSDN主页放风讲故事 &#x1f304;每日一句&…

安全测试工具分为 SAST、DAST和IAST 您知道吗?

相信刚刚步入安全测试领域的同学都会发现&#xff0c;安全测试领域工具甚多&#xff0c;不知如何选择&#xff01;其实安全测试工具大致分为三类&#xff1a;SAST、DAST和IAST。本文就带大家快速的了解这三者的本质区别&#xff01; SAST &#xff08;Static Application Secu…

MacOs 删除第三方软件

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

Spring高级bean的实例化方法

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

微信怎么发状态?简单教程,一学就会!

微信是一个非常实用的社交应用&#xff0c;不仅提供了基础的聊天功能&#xff0c;还推出了很多其他有趣的功能。比如微信个人状态&#xff0c;这个功能可以让用户随时随地分享自己的心情和动态。那么&#xff0c;微信怎么发状态呢&#xff1f;本文将为大家介绍有关微信发状态的…

数据结构【DS】串

朴素模式匹配算法的时间复杂度是多少&#xff1f; 最坏的时间复杂度为&#xff1a;&#x1d476;(&#x1d48e;&#x1d48f;) KMP算法的时间复杂度是多少&#xff1f; 最坏的时间复杂度为&#xff1a;&#x1d476;(&#x1d48e;&#x1d48f;)求next数组的时间复杂度为&…

[AutoSar]导出task mapping 表到excel

目录 关键词平台说明背景实现方法 关键词 嵌入式、C语言、autosar 平台说明 项目ValueOSautosar OSautosar厂商vector芯片厂商TI编程语言C&#xff0c;C编译器HighTec (GCC) 背景 为了做文档输出&#xff0c;要导出task mapping 到excel。 实现方法 1.按住shift&#xf…

制作Go程序的Docker容器(以及容器和主机的网络问题)

今天突然遇到需要将 Go 程序制作成 Docker 的需求&#xff0c;所以进行了一些研究。方法很简单&#xff0c;但是官方文档和教程有些需要注意的地方&#xff0c;所以写本文进行记录。 源程序 首先介绍一下示例程序&#xff0c;示例程序是一个 HTTP 服务器&#xff0c;会显示si…

C++标准模板(STL)- 类型支持 (类型关系,检查一个类型是否派生自另一个类型,std::is_base_of)

类型特性 类型特性定义一个编译时基于模板的结构&#xff0c;以查询或修改类型的属性。 试图特化定义于 <type_traits> 头文件的模板导致未定义行为&#xff0c;除了 std::common_type 可依照其所描述特化。 定义于<type_traits>头文件的模板可以用不完整类型实例…

代码随想录-刷题第二天

977. 有序数组的平方 题目链接&#xff1a;977. 有序数组的平方 思路&#xff1a;双指针思想&#xff0c;数组是有序的且含有负数&#xff0c;其中元素的平方一定是两边最大。定义两个指针&#xff0c;从两端开始向中间靠近&#xff0c;每次比较两个指针的元素平方大小&#…

BLIP-2:冻结现有视觉模型和大语言模型的预训练模型

Li J, Li D, Savarese S, et al. Blip-2: Bootstrapping language-image pre-training with frozen image encoders and large language models[J]. arXiv preprint arXiv:2301.12597, 2023. BLIP-2&#xff0c;是 BLIP 系列的第二篇&#xff0c;同样出自 Salesforce 公司&…

聚观早报 |地平线征程6芯片来了;鸿蒙智行官网上线

【聚观365】11月20日消息 地平线征程6芯片来了 鸿蒙智行官网上线 Redmi K70系列设计细节 vivo X100系列明日开卖 特斯拉将交付10辆Cybertruck电动皮卡 地平线征程6芯片来了 据“地平线HorizonRobotics”公众号消息&#xff0c;在2023广州车展上&#xff0c;地平线宣布征程…

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

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

[oeasy]python001_先跑起来_python_三大系统选择_windows_mac_linux

先跑起来 &#x1f94a; Python 什么是 Python&#xff1f; Python [ˈpaɪθɑ:n]是 一门 适合初学者 的编程语言 类库 众多 几行代码 就能 出 很好效果 应用场景丰富 在 各个应用领域 都有 行内人制作的 python 工具类库 非常专业、 好用 特别是 人工智能领域 pytho…

理化生考试系统中智能排考系统的功能

理化生考试系统是基于大数据技术以及智能图像识别技术研发的教育工具&#xff0c;可以帮助学生以及老师快捷准确的进行理化生实验的学习和考试。它主要应用于省、市级学生实验操作考核中考、学业考试的考试平台&#xff0c;通过信息技术应用让实验教学清晰高效&#xff0c;考前…

好莱坞罢工事件!再次警醒人类重视AI监管,人工智能矛盾一触即发!

原创 | 文 BFT机器人 关注国外新闻的应该都知道&#xff0c;最近焦点新闻是好莱坞史上最大规模的一场罢工运动。这场维持118天的罢工运动&#xff0c;终于在11月9号早上12点在好莱坞宣布结束。这场罢工运动虽是演员工会和代表资方的影视制片人联盟的茅盾&#xff0c;但直接引发…