Android 音视频缓存机制的系统性设计_android 播放器缓存

整体设计

如何构建音视频缓存的代理?首先,我们需要了解 APP 音视频的常规缓存模式及其弊端:

02_1_cache_normal.png

如图所示,常规方式中,缓存相关逻辑是由播放器本身提供的,开发者仅需将视频地址的url交给播放器,播放器会自动进行加载播放。

这种方式下,想要自定义缓存就必须深入播放器源码,对于开源播放器而言,虽然源码上手成本较高,但至少是可针对源码进行定制,但对于部分非开源的播放器而言,开发者几乎无法直接触达内部缓存机制。


我们更希望,无论播放器本身是否开源,都能够在 不涉及理解和修改播放器源码 的情况下,完全控制音视频的缓存机制——我们先将这个中间角色称为 CacheService,整体流程如下:

02_1_cache_reactive.png

如图所示,播放器层的播放和加载流程都委托给 CacheService,后者内部实现了包括文件下载、文件读写等一系列的相关逻辑,最终转交给播放器进行播放。

需要注意的是,由于 CacheService 完全是由我们自己定义的,因此我们也可以监听到音视频文件的整个缓存流程,并直接回调通知给最上层的 APP(上图中onCacheStart()等回调),且整个过程完全是 响应式 的。

读者应该理解,之所以 直接回调通知最上层 ,是有这个前提——我们希望整个缓存流程都不会涉及到播放器层的改动,比如 Android 系统的 MediaPlayer,我们无法也不应该修改它。

具体实现

1、逻辑冲突

设计的伊始谈到,为了保证解耦, 我们希望缓存机制 不能修改播放器源码 ,但 MediaPlayer 如何在不改源码的情况下,将自身的缓存加载逻辑交给我们的 CacheService 呢?

如下述代码中所展示的,这种实现似乎无法避免:

public class MyMediaPlayer extends MediaPlayer {
  
  public final CacheService mProxy;
  
  @Override
  public void setDataSource(String url) {
    // super.setDataSource(url);
    mProxy.setDataSource(url);
  }
}
复制代码

必须承认,这也是一种与播放器的耦合,不能修改播放器源码 的设定似乎并不符合常理。

这里体现出了作者本身优秀的创造力,通过创建一个设备的本地代理服务 CacheService,在将视频资源的url交给播放器之前,先进行本地的一次转换,并将初始的url作为参数,拼接在本地代理的url上:

1.建立本地代理:比如 http://127.0.0.1:8090
2.拿到要缓存的视频地址,比如 https://xxx.mp4
3.拼接为新的地址:http://127.0.0.1:8090/https://xxx.mp4

拿到新的 url 并交给任意播放器后,播放器的加载都指向本地服务的新地址——即通过 Socket 连接建立的本地服务 CacheService,后者通过解析出请求中真正的 https://xxx.mp4 地址,创建对应的下载任务,并从下载的文件缓存中,读取 buffer 返回给播放器;同时,监控整个流程的 CacheService 响应式地回调过程中所有大大小小的事件。

经过这样设计,整个流程的调用变得非常简单:

public class MainActivity extends Activity {
  
  public final MediaPlayer mPlayer;
  
  @Override
  public void playVideo(String url) {
    final String proxyUrl = VideoUtils.getProxyUrl(url);
    // url = https://xxx.mp4
    // proxyUrl = http://127.0.0.1:8090/https://xxx.mp4
    mPlayer.setDataSource(proxyUrl);
  }
}
复制代码
2、创建代理服务器

接下来,笔者通过伪代码的形式,简单阐述下创建本地代理连接的过程。

上文提到的本地服务 CacheService在创建时,会自动初始化一个本地代理服务器,配置ip和自动分配端口号,这之后,服务完成初步建立,并立即开启一个线程,等待接收客户端的后续连接。

// 实际类名 HttpProxyCacheServer.java
public final class CacheService {
  
  private CacheService(Config config) {
        // 初始化ip和端口号
        InetAddress inetAddress = InetAddress.getByName("127.0.0.1");
        this.serverSocket = new ServerSocket(0, 8, inetAddress);
        this.port = serverSocket.getLocalPort();
        // 开启新的线程,等待后续接收客户端的连接
        this.waitConnectionThread = new Thread(new WaitRequestsRunnable());
        this.waitConnectionThread.start();
    }
}
复制代码
3、处理缓存请求

本地服务建立完毕,当用户尝试播放音视频时,播放器实际上访问类似 http://127.0.0.1:8009/https://xxx.mp4 的地址,这时我们的 CacheService 中接到了对应的消息。

针对每一次请求,我们都能解析到真实音视频文件的地址(https://xxx.mp4),为了提高复用性,我们声明一个HttpProxyCache类,为每一个音视频配置一个对应的 HttpProxyCache 以进行管理:

class HttpProxyCache extends ProxyCache {
    // 视频资源的url地址
    private final HttpUrlSource source;
    // 视频资源的本地文件信息
    private final FileCache cache;
}
复制代码

实际上还不够,我们还需要针对每个音视频缓存过程的回调进行管理,因此,基于此再封装一层,使用 HttpProxyCacheServerClients 管理一个音视频资源:

final class HttpProxyCacheServerClients {
    private final String url;       // 视频资源url
    private volatile HttpProxyCache proxyCache;  // 缓存信息
    private final List<CacheListener> listeners = new CopyOnWriteArrayList<>(); // 缓存监听
}
复制代码

简单概括一下,针对一次新的音视频资源加载,会构建一个新的 HttpProxyCacheServerClients,内部除了相关信息的成员,还包含了 HttpProxyCache 对象用于读取和加载缓存。

4、远程加载流程

抽象地看待音视频的源,分为 远程音视频资源本地音视频资源,当不使用缓存时,必然会从远程进行下载,并不断将音视频的流通过 Socket 向播放器传输。

这里我们将 抽象为 Source:

public interface Source {

    // 建立打开资源
    void open(long offset) throws ProxyCacheException;

    // 获取音视频的长度
    long length() throws ProxyCacheException;

    // 不断读取音视频数据
    int read(byte[] buffer) throws ProxyCacheException;

    // 关闭释放资源
    void close() throws ProxyCacheException;
}
复制代码

对于远程加载的完整流程,本质上就是建立、打开、读取和关闭一个远程连接 HttpURLConnection的过程,核心代码如下:

public class HttpUrlSource implements Source {
    @Override
    public void open(long offset){
      HttpURLConnection connection = openConnection(offset, -1);
    }
    
    @Override
    public int read(byte[] buffer){
      return inputStream.read(buffer, 0, buffer.length);
    }
    // ...
}
复制代码
5、缓存加载流程

更多的时候,无论音视频资源是否已下载,我们都希望通过缓存统一加载管理:

1、文件已下载:直接读取本地文件,将数据通过Socket不断传回给播放器;

2、文件未下载:新建一个本地文件,并开启远程下载任务,下载过程中,数据流不断涌入本地文件,本地文件大小、下载进度的变更都会响应式通知上层;除此之外,新的音视频流数据会通过Socket不断传回给播放器,播放器也会不断的推进播放进度。

由此可见,无论文件是否下载,缓存流程都是围绕 本地缓存文件 进行的,这也符合软件开发中的 唯一可信源 的概念。


接下来笔者针对部分细节问题进行探讨。

6、自定义缓存策略

缓存所占用的空间往往会成为迫使用户卸载应用的最后一根稻草。

开发者不能无上限对音视频资源进行缓存,通常的维护手法是通过 限制空间大小,比如,用户通常可以接受视频类应用有 1G 左右的缓存空间,即时通信类应用也许会更大些。

因此我们的缓存库也需要提供这样的能力,可通过实现DiskUsage接口,实现不同的缓存策略。



## 总结:

各行各样都会淘汰一些能力差的,不仅仅是IT这个行业,所以,不要被程序猿是吃青春饭等等这类话题所吓倒,也不要觉得,找到一份工作,就享受安逸的生活,你在安逸的同时,别人正在奋力的向前跑,这样与别人的差距也就会越来越遥远,加油,希望,我们每一个人,成为更好的自己。


* BAT大厂面试题、独家面试工具包,

* 资料包括 数据结构、Kotlin、计算机网络、Framework源码、数据结构与算法、小程序、NDK、Flutter
  ![](https://img-blog.csdnimg.cn/img_convert/351b2a6134beb4fbbc3fb1a60bcedbb7.webp?x-oss-process=image/format,png)




**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化学习资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618156601)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

mg-9eA5TKmN-1714337787878)]




**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化学习资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618156601)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 24
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值