• 基于 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    视频解码接口如上,其中

    • 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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    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());
              }
            }
          }
        });
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    当正在播放时,调用 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    1. 创建好 Audio/VideoSource 和 Audio/VideoOutput 后,将他们塞到 SimplePlayer 构造函数即可
    2. player.open() 打开文件
    3. 设置 VideoOutputParameters 和 AudioOutputParameters,调用 prepare 函数进行一些初始化操作
    4. 使用 play/pause/stop/seek 等函数操作视频播放

    SimplePlayer 具体实现请参考源码。

    总结

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

    参考

  • 相关阅读:
    postgresql 格式化查询树为图片 —— pgNodeGraph 与 pg_node2graph
    多元线性回归_梯度下降法实现【Python机器学习系列(六)】
    ssm基于JavaEE的电脑销售管理系统设计与实现毕业设计源码021143
    Worthington羧基转移丨碳酸酐酶的应用和文献参考
    MySQL数据库:基础部分
    个人练习-PAT甲级-1132 Cut Integer
    图05 --- 最短路径问题:算法与实现
    用Python实现广度优先搜索
    UML 的工厂方法设计模式 策略设计模式 抽象工厂设计模式 观察者设计模式
    .NET快速对接极光消息推送
  • 原文地址:https://blog.csdn.net/weiwei9363/article/details/134470831