在线播放的数据读取与缓存

2018-02-28  本文已影响69人  风风风筝

https://github.com/sigma18/ExoPlayer
https://github.com/sigma18/MediaData

1. player 不关心数据来源

player 完全不想关心数据的来源,不想关心是来自网络还是磁盘

内存中维护一个队列 MemoryCacheManager(每一个元素包装为 BufferItem),player 只管从 MemoryCacheManager 读取数据(从队列中取出一个 BufferItem),当 MemoryCacheManager 中没有数据时,player 读取数据的线程被挂起,当 MemoryCacheManager 有数据时会被唤醒

2. MemoryCacheManager 队列的数据来源

启动一个线程(DataFetcher)循环读取数据,优先从磁盘读取,磁盘没有则从网络读取,每次从网络读到的数据写入磁盘。每次读到的数据包装为 BufferItem 然后存入 MemoryCacheManager

3、用户体验和流量浪费的平衡

给 MemoryCacheManager 设置一个占用内存的上限(mCacheSizeLimit),当 MemoryCacheManager 队列占用的内存达到我们设置的上限时,线程 DataFetcher 被挂起,当 MemoryCacheManager 有数据被取走时会被唤醒
通过调整 mCacheSizeLimit 从而在用户体验和服务器流量、用户手机流量之间找到一个平衡点

4、磁盘缓存碎片

4.1 碎片的产生

当播放一个未缓存过的视频时,假设已从网络读取数据 10000 字节,此时拖动进度条,player 请求数据的 position 发生改变,假设 position = 20000,然后继续播放到结束,假设又从从网络读取数据 5000 字节。那么磁盘应该存在 2 段缓存,第一段是从 0~10000,第二段是从 20000~25000

4.2 碎片记录

为记录这些磁盘缓存碎片,设计了2个数据库表:主表 Video 和 子表 VideoPart
Video 主要包含:id, url, size, cache_dir, last_use_time
VideoPart 主要包含:id, video_id, start, end, cache_name

cache_dir 对应磁盘上为此 video 创建的文件夹名字,cache_name 对应缓存碎片的文件名

4.3 碎片合并

假设当前待写入的碎片是 videoPart,待写入的数据长度为 writeLength,每次要写入的时候,查询数据库找出存在叠加区域的碎片 nextPart(如果有的话)

long end = videoPart.getEnd() + writeLength;
if (nextPart.getStart() <= end && end < nextPart.getEnd()) 

如果找到 nextPart,算出叠加的区域

int redundantLength = (int) (end - nextPart.getStart());

然后写入有用的数据再合并 2 个文件

writeLength = writeLength - redundantLength;
FileUtils.write(accessFile, buffer, position, writeLength);
FileUtils.merge(accessFile, nextFile);

5. 无缝衔接从网络和磁盘读取数据

  1. 判断磁盘上是否有包含 position 的碎片,如果没有则进入 3,有则进入 2
  2. 从磁盘读取,直到这一段缓存到结尾,position += x(假设这一段缓存有 x 字节)
  3. 建立网络连接,设置 RANGE bytes=position
  4. 从网络读取(假设请求到 y 字节),position += y
  5. 判断磁盘上是否有包含 position 的碎片,没有则进入 4,有则进入 2

6. 移动网络

每次从网络读取时,如果当前是移动网络
如果是不被允许的,则会发出通知并挂起线程(DataFetcher),也就停止读数据

7. 脏数据

在一些特殊情况下,可能会产生无效的缓存,比如

  1. 在播放过程中,进程被杀死
  2. 用户删掉 SD 卡的缓存文件

脏数据包括

  1. 无效的数据库记录,因为对应的文件不存在,或者 end - start ≠ file.length()
  2. 无效的缓存文件,因为没有数据库记录使用它

所以在每次应用启动的时候,对所有 Video 记录进行校验,删掉脏数据
在每次新开一个 Video 读写磁盘缓存时,再对这个 Video 记录进行一次校验

[DiskCacheManager.java]

public synchronized void open(String url) {
    ...
    mVideo = mDbHelper.queryVideoByUrl(mUrl);
    if (mVideo != null) {
        ...
        checkCache(mVideo);
    }
}

8. 磁盘缓存 LRU

每次写入磁盘后都要判断缓存文件的大小总和是否超出上限,如果超出则删掉 last_use_time 最小的那个 Video 记录和文件,但是至少保留当前正在使用的这个 Video

[DiskCacheManager.java]

public synchronized void open(String url) {
    ....
    mVideo = mDbHelper.queryVideoByUrl(mUrl);
    if (mVideo != null) {
        ....
        mVideo.setLastUseTime(System.currentTimeMillis());
        mDbHelper.update(mVideo);
        checkCache(mVideo);
    }
}

private void trimToSize(VideoPart videoPart) {
    while (mCacheSize > MAX_CACHE_SIZE) {
        Video video = mDbHelper.queryEldestVideo();
        if (video.getId() == videoPart.getVideoId()) {
            return;
        }
        File videoDir = new File(VideoDataSource.getInstance().getCacheDir(), video.getCacheDir());
        if (videoDir.exists()) {
            File[] videoPartFiles = videoDir.listFiles();
            if (videoPartFiles != null) {
                for (File videoPartFile : videoPartFiles) {
                    mCacheSize -= videoPartFile.length();
                    videoPartFile.delete();
                }
            }
            videoDir.delete();
        }
        mDbHelper.deleteVideo(video.getId());
    }
}

9. 数据库快照

很显然在这个应用场景里,数据库增删改的次数远小于查询,所以没必要每次都去查询数据库,只需要查询一次并放在内存即可。但要确保两者的一致性。

[DiskCacheManager.java]

private List<VideoPart> mVideoPartList;

每次读写大概率是使用上一次的 VideoPart,不必每次都遍历 mVideoPartList 进行查找

[DiskCacheManager.java]

private VideoPart mReadVideoPart;
private RandomAccessFile mReadFile;

private VideoPart mWriteVideoPart;
private RandomAccessFile mWriteFile;
上一篇下一篇

猜你喜欢

热点阅读