Skip to content

edgesider/simpleplayer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SimplePlayer

一个简单的播放器。

依赖库

  • 解封装、解码:FFmpeg
  • 视频渲染:OpenGL
  • 音频渲染:OpenAL

实现

队列实现

线程划分

  • 一个解封装线程(Demux Thread),负责从一个文件中解出音频流和视频流,分别将流中的包序列送入音频和视频解码队列(Decode Queue)。
  • 两个解码线程(Decode Thread),分别负责从音频和视频解码队列取出包,进行解码并将数据帧分别输出到音频和视频播放队列中(Play Queue)。
  • 两个播放线程(Play Thread),分别从两个播放队列中取出帧,进行播放控制。
  • 一个渲染线程(Render Thread),渲染线程负责在屏幕上显示内容(视频内容、文字等),以及捕获事件(鼠标、键盘、窗口等)。视频播放线程会将要渲染的帧提交给渲染线程以显示内容。

队列长度

解码队列和播放队列都有长度限制,当消费方速率不及生产方导致队列满了之后,生产方需要等待队列空闲之后才可以继续生产并入队(使用queue_dequeue_wait)。

解封装线程需要同时向两个解码队列入队,同时两个队列都需要等待,因此可能出现某个队列满了,阻塞另一个队列的情况。

实际上,播放线程和渲染线程之间的数据传递也是用的队列,不过与上面几个队列相比,渲染线程作为消费者的消费逻辑不太一样,它不会保证处理队列里面的每一个元素,而是每次只取队列的最后一个元素,同时跳过并释放其他元素。这是由于渲染队列需要尽量使得每帧都能显示出最新的画面,而不能因为排队而延迟。具体来说,渲染队列由VSync驱动,视频播放线程由视频帧率驱动,因此通常比视频播放线程更新频率高,这时候该队列基本上长度都小于等于1;如果遇到高帧率视频,或是屏幕刷新率低,导致视频播放线程更新速率大于等于VSync速率,该队列就会堆积多个帧,而渲染线程只需要取最新一帧即可,确保及时渲染。

音画同步

测试看下来,在没有同步的时候,视频逐渐慢于音频,且差距不断扩大;直接原因可以理解为:

  • 音频始终在连续播放,时长由框架保证;
  • 视频需要每帧之后通过usleep等待一帧的时间,忽略了渲染、排队、线程调度等其他过程所消耗的时间,而这部分时间也是很难去估算的,这会导致实际两帧之间的时间比预期的要长,因此速度上就慢于音频了。

因此需要:一是完善视频的帧间隔;二是需要一个不断调整的机制,同步二者的时间,即音视频同步。

同步思路如下:

  1. 音频始终正常播放,同时维护播放时间;
  2. 视频解码出来之后,计算与音频的时间差d,与阈值T作比较:
    • -T < d < T: 正常渲染;
    • d <= -T: 视频比音频慢,丢掉当前帧;
    • d >= T: 视频比音频快,等待d时长。

TODO

  • 音频播放
  • 音频卡顿问题
    • alSourcePlay 调用时机不合适
    • alSourcePlay: When called on a source which is already playing, the source will restart at the beginning.

  • 拆文件
  • 拆线程、拆队列
    • 线程同步
      • pthread_mutex / pthread_cond
  • 完善音频播放
    • 重写队列逻辑
    • 音频状态跳动
      • 包速率莫名变低,导致队列消耗完,AL将状态转到了STOPPED,出现音频卡顿
      • 视频帧的消耗速率比实际的pts要慢,而音频的播放由音频库维护,因此速度不会慢。这导致了音频包队列和音频帧队列依次逐渐被消耗完,阻塞音频播放,从而卡顿。
      • 本质上还是因为视频播放机制不完善,以及缺乏音视频同步机制。
    • 音频时间更新
  • 音画同步
    • ? 是否需要两个FormatContext分别解析音视频流?
    • 合并音视频PlayContext
  • 播放控制
    • 键盘操作
      • VSync驱动视频渲染(glfwPollEvents),每次判断视频画面是否需要更新(观察者模式),避免事件处理与视频渲染的耦合。
      • 事件处理
    • 事件机制
      • 音视频的StreamContext中各包含一个事件队列,在每一帧渲染处理完之后,从这个队列中取事件来消费
      • 事件对象通过引用计数管理
    • 暂停/继续
      • 播放线程消费到暂停事件之后,挂起整个线程(音频播放线程还需要暂停al的播放),直到下一个继续事件。
      • 这里要考虑是否需要将整个播放线程都改造成事件驱动的?改造之后,帧数据和事件在同一个循环里处理。
    • 播放状态定义
      • Prepare (TODO)
      • Play
      • Pause
      • Stop (TODO)
    • 前进/后退
      • Seek事件
          1. 暂停解封装、解码的同时,需要清空包队列、帧队列、音频缓冲队列等
          • Queue支持原子地清空
          • 由生产者清空队列
          • 通过SEEK_START事件触发,处理完之后等待SEEK_END事件
          1. 解封装线程调用av_seek_frame
          • av_seek_frame成功之后,由解封装线程触发SEEK_END事件
          1. 继续解封装、解码
      • 处理队列满、解封装结束等导致线程等待,无法处理事件的情况
        • 队列支持timed_wait,每次定时唤醒之后,处理事件
      • 将每个线程都改造为事件驱动的,支持通用事件。
      • 如何在同一个线程中同时处理数据和事件(如同时处理包队列和事件队列)
        1. 队列支持selector
          • condition variable list
          • pipe
        2. 将所有数据、事件都通过同一个队列传递
          • 因为会有单独清除数据不清理事件的需要,队列要支持按需清理之类的操作
        3. (目前)入队、出队时使用timed_wait,在超时之后处理事件
          • 如果timed_wait等待的是入队,同时处理事件的时候发生了清空队列的 操作,就会导致之前timed_wait的一帧立马入队,相当于队列清空不彻底
      • Seek粒度和准确度
      • 帧缓存,避免seek之后全部销毁(VirtualSeekBar)
  • 音量
  • 支持缩放
  • FPS显示
  • 支持按视频时间同步、按外部时钟同步
  • 字幕
  • 优化日志系统
    • 支持Tag
    • 支持日志等级
  • 单元测试
  • 错误处理
  • Kotlin/Native
  • 视频右侧花条
    • 似乎是GL需要纹理以8像素对齐
  • VBR/VFR
  • MacOS适配
    • glfw需要放到主线程
  • 单线程播放的可行性
  • xmake - sysroot

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published