基于 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 voidattachAVSyncClock(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 voidattachAVSyncClock(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.mzph.cn/news/149151.shtml

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

相关文章

面试其他注意事项

面试其他注意事项 一、面试反问 这个岗位的日常工作和主要职责是什么&#xff1f;咱们这边主要负责什么业务&#xff0c;用到了哪些技术呢&#xff1f;对于我们校招生有没有培养体系呢&#xff1f;脱产培训&#xff0c;还是边工作边熟悉&#xff1f;会有导师带嘛&#xff1f;…

滚动更新和回滚部署在 Kubernetes 中的工作原理

公众号「架构成长指南」&#xff0c;专注于生产实践、云原生、分布式系统、大数据技术分享。 在过去的几年中&#xff0c;Kubernetes 在生产环境中被广泛使用&#xff0c;它通过其声明式 API 提供了大量解决方案&#xff0c;用于编排容器。 Kubernetes 的一个显著特性是其具有…

Wireshark TS | 应用传输缓慢问题

问题背景 沿用之前文章的开头说明&#xff0c;应用传输慢是一种比较常见的问题&#xff0c;慢在哪&#xff0c;为什么慢&#xff0c;有时候光从网络数据包分析方面很难回答的一清二楚&#xff0c;毕竟不同的技术方向专业性太强&#xff0c;全栈大佬只能仰望&#xff0c;而我们…

Windows RS485\USB转换接头,连接modbus温度传感器接线方法

文章目录 背景接线方式安装RS485\USB转换接头的驱动程序查看COM口号&#xff08;Communication Port&#xff08;通讯端口&#xff09;&#xff09;测试modbus数据传输 背景 买了个rs485 modbus协议的温度传感器&#xff0c;因为想接到windows上&#xff0c;用传感器厂家提供的…

Polygon Miden VM中的哈希函数对比

1. 引言 在Polygon Miden VM中&#xff0c;使用了多个不同的哈希函数&#xff1a; 1&#xff09;“传统”哈希函数&#xff0c;如BLAKE3&#xff1a;对STARK之外的性能进行了优化。2&#xff09;algebraic哈希函数&#xff0c;如Rescue Prime&#xff1a;对STARK内部优化&…

APP软件外包开发流程

APP外包开发的流程可以根据具体项目的特点和需求有所变化&#xff0c;但一般而言&#xff0c;以下是一个通用的APP外包开发流程&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1.明确需求和目标&…

asp.net校园二手交易平台系统VS开发sqlserver数据库web结构c#编程计算机网页

一、源码特点 asp.net校园二手交易平台系统 是一套完善的web设计管理系统&#xff0c;系统采用mvc模式&#xff08;BLLDALENTITY&#xff09;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为 vs2010&#xff0c;数据库为sqlserver2008&a…

Kotlin学习(一)

Kotlin学习&#xff08;一&#xff09; 1.使用IDEA构建Kotlin项目 新建工程即可 我这里选择的Build System是IntelliJ&#xff0c;虽然我没用过但是这是Kotlin基础学习应该不会用到其他依赖 2.Hello World package com.simonfun main(args:Array<String>){println(&q…

UASRT(2)

UASRT参数配置 数据发送过程 1.双缓冲 当要发送三个数据 且是连续发送 第一个数据写入TDR寄存器 然后到移位寄存器发送&#xff08;一个一个bit的发送&#xff09;在第一个数据在移位寄存器发送的时候第二个数据就已经被写入TDR寄存器了等到第一个数据发送完第二个数据就进入…

jetbrains ai 提示该地区不可用的百分百解决方案,亲测有效

问题 申请 jetbrains 的 ai assistant 白名单已经通过&#xff0c;但是在使用 ai assistant 的过程中提示 The usage of the service is not permitted in your location ,我所在的地区是中国&#xff0c;目前该插件是对中国大陆关闭的。 刚开始我怀疑是代理的问题&#xff…

【STL】string类 (上) <vector>和<list>的简单使用

目录 一&#xff0c;什么是 STL 二&#xff0c;STL 的六大组件 三&#xff0c;标准库中的 string 类 1&#xff0c;string 类 2&#xff0c;string 类的常用接口 1&#xff0c;string类对象的常见构造 2&#xff0c;string&#xff08;const string& str&#xff…

OTP语音芯片 NV080D在智能空气检测仪的应用

随着人们对健康和环保的关注度不断提高&#xff0c;人们对看不见的家居环境也越来越重视。智能空气检测仪的市场需求也在不断增长中&#xff0c;呈现稳中向好的趋势。智能空气检测仪能够检测室内空气中的PM2.5、甲醛、TVOC等有害物质&#xff0c;同时还可以检测温湿度、空气质量…

5g路由器赋能园区无人配送车联网应用方案

随着人工智能、无人驾驶技术和自动化技术的不断进步&#xff0c;无人配送技术得到了极大的发展。园区内的物流配送任务通常是繁琐的&#xff0c;需要大量的人力资源和时间。无人配送技术能够提高配送效率并减少人力成本。无人配送车辆和机器人能够根据预定的路线和计划自动完成…

Rapid chain

这篇文章中提到 Elastico 运行6个epoch就会退化到公式失败率高达 0.97 omnileger 在第一个epoch需要一个初始化的随机种子&#xff0c;来初始化 VRF。这需要 O ( n 2 ) O(n^2) O(n2) 的复杂度&#xff0c;并且OminLedger 需要通过轻节点驱动枷锁和解锁的过程&#xff0c;这户家…

主键问题以及分布式 id

分布式 id 需要处理的问题主要是同一时间在多台机器中保证生成的 id 唯一&#xff0c;为了这么做我们可以这么做&#xff1a; 分布式 id 生成策略 先说几个已经被淘汰的策略引出分布式 id 的问题 1&#xff0c;UUID&#xff1a;UUID 随机并且唯一&#xff0c;在单一的数据库…

Android问题笔记四十六:解决open failed: EACCES (Permission denied) 问题

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列点击跳转>蓝桥系列点击跳转>ChatGPT和AIGC &#x1f449;关于作者 专…

[算法学习笔记](超全)概率与期望

引子 先来讲个故事 话说在神奇的OI大陆上&#xff0c;有一只paper mouse 有一天&#xff0c;它去商场购物&#xff0c;正好是11.11&#xff0c;商店有活动 它很荣幸被选上给1832抽奖 在抽奖箱里&#xff0c;有3个篮蓝球&#xff0c;12个红球 paper mouse能抽3次 蒟蒻的p…

Cascade-MVSNet论文笔记

Cascade-MVSNet论文笔记 摘要1 立体匹配&#xff08;Stereo Matching&#xff09;2 多视图立体视觉&#xff08;Multi-View Stereo&#xff09;3 立体视觉和立体视觉的高分辨率输出4 代价体表达方式&#xff08;Cost volume Formulation&#xff09;4.1 多视图立体视觉的3D代价…

RT-DETR优化改进:SEAM、MultiSEAM分割物与物相互遮挡、分割小目标性能

🚀🚀🚀本文改进:SEAM、MultiSEAM分割物体与物体相互遮挡性能 🚀🚀🚀SEAM、MultiSEAM分割物与物相互遮挡、分割小目标性能 🚀🚀🚀RT-DETR改进创新专栏:http://t.csdnimg.cn/vuQTz 学姐带你学习YOLOv8,从入门到创新,轻轻松松搞定科研; RT-DETR模型创新…

数字化时代,VR全景如何助力商企抢占市场份额?

随着5G技术的逐步落地&#xff0c;VR全景已经开始逐渐被应用到各行各业中了&#xff0c;VR餐饮、VR房产、VR景区、VR工厂、VR学校、VR博物馆等等&#xff0c;甚至大家所熟悉的汽车之家中的全景看车、贝壳和链接的全景看房等&#xff0c;所应用的都是VR全景的形式。 前几年电商对…