V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Kinnikuman
V2EX  ›  程序员

视频播放器播放网络视频时的缓存播放策略问题请教

  •  1
     
  •   Kinnikuman · 10 天前 · 1052 次点击

    tg video

    比如上面的 telegram 上的视频播放,在进度条能看到预先加载了一段。同样的 infuse 和 IINA 等播放器也都是会做预加载功能(可以明显在进度条中看到)。

    那么预加载的视频是在内存中还是硬盘中?因为视频占用体积很大,不可能缓存在内存中吧?缓存在硬盘中,播放策略是什么?缓存的内容是视频文件本身,还是需要解码的 Packet?

    第 1 条附言  ·  7 天前
    我在研究播放器缓存的实现可行性方案,当然将 packet 缓存到内存是一个可行的方案,但我在考虑是否可以将影片缓存到硬盘中。
    比如 telegram 或者夸克播放网络上的文件,能在进度条看到缓存很大,如果视频很大,预加载一段视频会使用很大的内存,这明显不符合开发规范。所以猜测是缓存到硬盘里,我考虑了两个方案:

    (假如都使用 ffmpeg 解码播放的逻辑)

    方案一:播放和缓存两个分开,使用 ffmpeg 加载并播放一个网络视频,然后另一个线程下载这个视频,下载线程会利用带宽优势提前将文件下载完成到硬盘中,当下载完成时,再通知播放线程,播放线程切换到播放本地文件。

    上面这个方案的缺点是,只有当完全缓存完成后才切换到本地文件播放,当播放和下载同时进行时候占用带宽比较严重,需要平衡下载线程的速度,从而不影响播放体验。而且不清楚下载完成后播放线程切换到播放本地文件会不会对用户有感知。

    夸克云盘的视频播放是将整个文件都缓存下载的,但 telegram 则不是整个文件缓存,如果看到某个地方暂停,是不会继续缓存,所以应该不是这个方案。就像 mpv 开启了 --cache-secs ,默认提前缓存多长时间。


    方案二:使用更复杂的逻辑来控制,下面我将尝试讲一下这一块逻辑,请各位大佬帮忙看下是否可行。

    首先 libav 使用 avformat_open_input 打开一个网络视频,然后 av_read_frame 可以读取视频的 packet(你要知道它和 frame 的区别), 平时的播放逻辑是在一个 while 循环中一直读取并渲染播放,这里就需要将 packet 放到内存中一个 fifo 数组中,数组满了则暂停循环,播放时候根据 fps/pts 等来控制 frame 在屏幕上的时间然后再播放下一帧。这里从内存中取出 packet 并处理解码然后将解码后的 frame 渲染到屏幕上,是一个很常见的播放逻辑,生产者生产数据,消费者消费数据,解码并渲染。现在要加硬盘缓存,就需要加一点逻辑,生产者将 av_read_frame 读取到的 packet 写入磁盘中,这里也可以控制写入磁盘的大小,如果没有写满,可以一直写。消费者从磁盘中读取数据到内存数组中,播放从这个数组中取 packet 进行解码。这里会有几个问题,频繁的磁盘读写很慢,所以再加一个内存缓存,进行批量写入。从磁盘中读取数据到内存中,应该也是比较慢的,所以需要再加一个二级缓存。甚至受制于磁盘读写速度,可能还要加上异步 IO 读写操作。

    我不知道上面的逻辑有没有讲清楚,我比较怀疑上面我考虑的是不是复杂了?也没有研究过 mpv 的源码(看不太懂),我现在只是在做可行性的研究。
    第 2 条附言  ·  7 天前
    刚才附言 1 有个错误,我测试了下 telegram 暂停视频也是会继续缓存的,直到将整个视频都下载完成。
    12 条回复    2024-06-16 15:08:41 +08:00
    ysc3839
        1
    ysc3839  
       10 天前 via Android
    一般是内存
    MrDream
        2
    MrDream  
       10 天前 via iPhone
    是在内存里。缓存的是 h264 和 h265 这种压缩后的数据,体积也还好,不算大。像你图中缓存了 1/3 的话,大概 30 多 MB 吧。又不是缓存解码后的 YUV ,那个太大了。
    Kinnikuman
        3
    Kinnikuman  
    OP
       10 天前
    @ysc3839
    @MrDream
    就像 infuse 观看一个 100 多 G 的视频,我看进度条也能缓存很多,这内存吃不消吧。

    缓存肯定是 Packet 而不是 Frame ,解码后的 Frame 太大了。
    IvanLi127
        4
    IvanLi127  
       10 天前
    那个缓存不是指远程文件下载到本地的缓存嘛?我感觉是在磁盘的临时文件中存着原始文件内容,拖动进度条时再读到内存处理
    我凭直觉猜的,蹲大佬科普
    kuanat
        5
    kuanat  
       10 天前 via Android
    @Kinnikuman

    如果是本地文件,缓存进度条可能和内存没有太大关系。

    缓存的真实量取决于内核管理的 page cache ,假如剩余空间足够大,这个 page cache 会在第一次访问文件的时候把存储中的内容全部加载,不内存不足的话会加载一部分。在应用程序看来,fd 已经在了,stream 读取也开始了,就看你读多少。

    这个 page cache 的容量主要影响拖动进度条,超出去了就会去读存储,没超的话,内核会有自己算法加载新的内容进有限的 page cache 里。
    PTLin
        6
    PTLin  
       10 天前
    这完全不就是软件策略的原因吗,有可能是下载到临时文件夹然后又加载到内存一部分
    MrDream
        7
    MrDream  
       10 天前 via iPhone
    我们做的手机端播放器,缓存的几秒视频,就放在 malloc 出来的内存里,没落盘。
    changxiangzhong
        8
    changxiangzhong  
       10 天前 via Android
    4k uhd Netflix 的 typical bitrate 是 15mbps 。其实缓存在磁盘/nand/emmc 是足够的。没必要缓存在内存的。假设缓存 8 秒,也 15MB 了。还是很大的,没必要
    expy
        9
    expy  
       8 天前
    一般都放内存吧,存的是解码之前的视频音频,hevc aac 之类的。
    https://mpv.io/manual/stable/#cache
    Kinnikuman
        10
    Kinnikuman  
    OP
       7 天前
    @MrDream @PTLin @expy @kuanat 大佬们请看我的附言,有了比较详细的描述,帮我指正下。
    expy
        11
    expy  
       7 天前
    @Kinnikuman 我没有播放器开发经验,实现细节不知道。方案一应该不用下载两遍,比如搞个固定大小的缓冲区,下载线程往里面写,播放线程读。

    mpv 的 FAQ 有一点相关描述。
    https://github.com/mpv-player/mpv/wiki/FAQ#user-content-Why_were_some_cache_options_removed_or_changed_stream_cache
    kuanat
        12
    kuanat  
       2 天前
    @Kinnikuman #10

    这个场景我也没什么经验,结合 #9 #11 楼的引用随便说说。

    1. 硬盘、内存速度

    目前常见的硬件,即便是机械硬盘,其加载速度也远大于被加载视频的播放速度,更不用说内存了。

    2. 硬盘文件的缓存机制

    我在前面的回复里解释了一部分。再进一步说,你的应用不会直接读写硬盘,操作系统内核替你“智能”完成硬盘文件在内存中的缓存工作。

    当然你也可以手动申请内存,然后读取硬盘文件内容之后保存在申请到的内存中,同时自己编写缓存内容更新的逻辑。这样做只适用于非常有限的场景,比如磁盘 IO 长期被后台应用占用,或者需要反复在多个超大(超过内存容量)文件之间切换。对于一个视频播放器而言,不需要考虑这些事情。

    3. 缓存容量设定

    我理解你设想中的方案都有一个隐式前提,视频文件非常大,需要尽可能多缓存。但是一般来说,除非你要提供“保存视频文件到本地以备无网络时观看”这个本质上名为“下载”功能,绝大多数时间只缓存一定量的数据即可。换个说法,下载功能可以是按需( on-demand )的,填满缓存容量就可以暂停。

    也就是说,内存、硬盘的二级缓存机制是没有必要的。用内存缓存的唯一目的就是避免硬盘读写,硬盘缓存是解决内存缓存不够的才用的,一旦使用硬盘作为缓存就没必要再做内存缓存。

    4. 缓存的内容取决于来源协议或者格式

    根据上面的分析,本地文件技术上说是不需要缓存的,或者说不需要你手动缓存的。

    网络视频有可能是以网盘作为后端,本质上还是特定格式视频文件的形式,也有可能是基于某种流媒体协议。对于前者,缓存的就是文件。对于后者,缓存的是视频流意义上的 packet (非网络意义的 packet )。

    5. 播放器本身不关心缓存后端的来源是什么,只关心从特定的来源流式读取。

    比如桌面浏览器可以指定缓存后端是内存还是硬盘,但内嵌播放器是无需关心的。播放器这类本地应用,也只需要为数据流的消费者提供一个来源。即播放逻辑永远是固定的,至于来源(缓存)与播放行为是无关的。

    6. 下载、解码和播放之间解耦

    前面有人引用 mpv 处理缓存的逻辑,它缓存的对象是 packet 经过 demuxer 处理后的数据,即缓存发生于 demuxer 和 decoder 之间。前面 3/4/5 点综合起来说的也是这个意思,下载并不是直接对接播放的,缓存发生于中间解码的部分。不需要对下载和播放逻辑做特殊处理,所有的特殊行为都在中间解码阶段。

    7. 缓存的底层数据结构

    缓存确实可以用 FIFO 数据结构表达,考虑到播放器的使用场景,固定容量的缓存一定会遇到周期性完整替换的需求。

    可以考虑 ring buffer ,这样就需要控制生产者写入速率与消费者播放速率一致。或者考虑 double buffering ,用手动切换缓存后端简化速率匹配。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3157 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 11:17 · PVG 19:17 · LAX 04:17 · JFK 07:17
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.