直播短视频系统源码,一步步实现缓存机制
直播短视频系统源码,一步步实现缓存机制
设计的伊始谈到,为了保证解耦, 我们希望缓存机制 不能修改播放器源码 ,但 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); } }
接下来,笔者通过伪代码的形式,简单阐述下创建本地代理连接的过程。
上文提到的本地服务 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(); } }
本地服务建立完毕,当用户尝试播放音视频时,播放器实际上访问类似 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 对象用于读取和加载缓存。
抽象地看待音视频的源,分为 远程音视频资源 和 本地音视频资源,当不使用缓存时,必然会从远程进行下载,并不断将音视频的流通过 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); } // ... }
更多的时候,无论音视频资源是否已下载,我们都希望通过缓存统一加载管理:
1、文件已下载:直接读取本地文件,将数据通过Socket不断传回给播放器;
2、文件未下载:新建一个本地文件,并开启远程下载任务,下载过程中,数据流不断涌入本地文件,本地文件大小、下载进度的变更都会响应式通知上层;除此之外,新的音视频流数据会通过Socket不断传回给播放器,播放器也会不断的推进播放进度。
由此可见,无论文件是否下载,缓存流程都是围绕 本地缓存文件 进行的,这也符合软件开发中的 唯一可信源 的概念。