オーディオとビデオの開発の旅 (50) - キャッシング中の再生中のキャッシュの断片化 (1)

目次

  1. キャッシュシャーディングとは
  2. シャードをキャッシュする理由
  3. 達成方法
  4. 材料
  5. 褒美

1. キャッシュの断片化とは

前回の記事で AndroidVideoCache を紹介したとき、完全にダウンロードされるまでデータをダウンロードし続けることはわかっていました。これはトラフィックの浪費につながります。たとえば、5MB のビデオの場合、ビット レートは 2Mb/s で、合計で 5Mx8/2=20 秒になります。帯域幅が 5MB/s の場合、5M のビデオが 1 秒でダウンロードされますが、ユーザーは興味がないため 2 秒間しか視聴できず、トラフィックの浪費と LRU キャッシュの抜け穴という 2 つの欠点が生じます。戦略。
この問題は、速度制限とキャッシング LRU 戦略を調整することで最適化できます。

同時に、別の問題もあります. レジューム アップロード スキームを使用して各リクエストの範囲を設定する場合、AndroidVideoCache が現在のキャッシュ位置に全体の長さの 20% を加えた範囲を超えてドラッグすると、キャッシュされません。
request.rangeOffset <= cacheAvailable + sourceLength * 0.2f

このロジックを分析するために絵を描いて、キャッシュが存在する場合にどのような問題が発生するかを見てみましょう。

なぜこのようなデザインにしたいのですか?この領域を超えた後もキャッシュを継続できるようにするにはどうすればよいでしょうか。
シーク後にデータを取得する方法を考えてみましょう。
次の 3 つのオプションがあります。

1. cached_position シーケンスに沿ってキャッシュを続行します
. 2. 現在ドラッグされているプログレス バーが cached_position を超えている限り、キャッシュは続行されず、後続のデータは完全にネットワーク リクエストになります。
3. ドラッグしたプログレス バーが cached_position を超えても、新しい位置から Range リクエストを開始する. 3 つの方法の
長所
と短所を比較します. オプション 1: cached_position の順序でキャッシュを続行します. プログレス バーをドラッグした位置cached_position > Far から遠く離れています。このソリューションを使用してプログレス バーをドラッグすると、再生が非常に遅くなるため、最初のソリューションは強制終了されました。
解決策 2: 解決策 2 の方法も可能です. プログレス バーをドラッグした後、動かなくなることはありません。現在、インターネットで人気のオープンソース プロジェクトhttps://github.com/danikula/AndroidVideoCache が採用されているソリューションです
ソリューション 3: ドラッグ後にフリーズする問題を解決し、また、順次ダウンロードしかできない問題を解決します.
現時点で は、これが最適なソリューションです

考えられる方法は次の2つです。

  1. 物理ファイルは空で、キャッシュは分割されており、データのない部分は 0 で埋められ、データのある部分は始点と終点を記録してデータを埋めます。—"このソリューションは、より多くのスペース (ファイル用のシステムの空のソリューションとは異なります) とメモリを消費します。このソリューションは、キャッシュされたフラグメントの開始情報と終了情報を記録するために使用されるキャッシュ フラグメント化情報ファイルを維持する必要があります。
  2. ロジック ファイルは空であり、キャッシュは断片化されており、キャッシュ ファイルは N 個のファイルに断片化されています。一部のファイルにデータがない場合、それらは作成されません。2 つの隣接する場合、データを含むレコードの開始点と終了点。ファイルの開始と終了 ドッキングおよびマージできます。このスキームは、断片化情報ファイルをキャッシュするスキームも採用できますが、フォルダとファイルの名前から直接区別することもできます。

2.キャッシュの断片化の理由

上記のセクションを通じて、AndroidVideoCache がシーク後にキャッシュしないシナリオと理由、およびキャッシュの断片化の概念を理解しました。このセクションでは、キャッシュの断片化が使用される理由を分析しましょう

キャッシュ シャーディングには次の利点があります。

  1. 大きなファイルを小さなファイルに分割して個別のキャッシュを作成します。これにより、必要に応じてストレージ スペースが割り当てられるという利点があります。

     

画像: 10 億レベルのビデオ再生技術の最適化により、Wang Hui の最終ドラフト 2.keyが明らかに

  1. 後でシーク キャッシュを実現するための基礎を築きます。
  2. キャッシュヒット率を向上できる
  3. 過剰なシークと過剰なデータによる再生遅延を減らします
  4. P2P 戦略を使用してトラフィックを節約すると、各小さなフラグメントを個別のシード ソースとして使用して、P2P ヒット率を向上させることができます。

3. 達成方法

キャッシュ シャーディングを実現するには、次の 2 つの問題を解決する必要があります。

  1. ストレージの管理とキャッシュされた断片化されたファイルのマージ
  2. キャッシュされた断片化ファイル情報の管理

 次に、キャッシュ フラグメンテーションを実装する次のオープン ソース プロジェクトJeffVideoCacheを分析してみましょう
.このオープン ソース プロジェクトは、MP4 キャッシュ フラグメンテーションを実装するだけでなく、m3u8 のサポートも追加します.AndroidVideoCache と比較すると、アーキテクチャ設計も大きく変更されています.
その中で、MP4 のキャッシュは物理ファイル ホール方式を採用していますが、M3U8 は論理ファイル ホール方式を採用しています。

スライスの位置情報を記録する VideoRange データ構造を定義する

public class VideoRange {
    private long mStart;   //分片的起始位置
    private long mEnd;     //分片的结束位置
}

LinkedHashMap<Long, VideoRange> mVideoRangeMap; //キャッシュされたビデオ範囲構造は VideoRange リストを保持し、キーは VideoRange の開始位置、値は VideoRange オブジェクトです。
2 つの VideoRange の間に部分的なオーバーラップがある場合、マージによって新しい VideoRange が合成されます。

この記事では、オープン ソース プロジェクトの MP4 物理ファイル ホール キャッシュ フラグメンテーションのスキームを分析し、次の記事では、M3U8 論理ファイル ホール キャッシュ フラグメンテーションのスキームを分析します。

コードから主なプロセスを見てみましょう

3.1 LocalProxyVideoControl#startRequestVideoInfo は
、キャッシュの開始、キャッシュの進行状況の更新、キャッシュの失敗、およびキャッシュの成功のためのコールバックを持つキャッシュ リスナーを追加し、キャッシュの断片化情報をトリガーし、変更されたファイルを記録するキャッシュの断片化情報ファイルを取得します。各フラグメントの。

//LocalProxyVideoControl#startRequestVideoInfo   
 
public void startRequestVideoInfo(String videoUrl, Map<String, String> headers, Map<String, Object> extraParams) {
        //待请求的url
        mVideoUrl = videoUrl;
        //添加缓存listener,有开始缓存、缓存进度更新、缓存失败、缓存成功的回调
        VideoProxyCacheManager.getInstance().addCacheListener(videoUrl, mListener);
        VideoProxyCacheManager.getInstance().setPlayingUrlMd5(ProxyCacheUtils.computeMD5(videoUrl));
        //重点分析startRequestVideoInfo
        VideoProxyCacheManager.getInstance().startRequestVideoInfo(videoUrl, headers, extraParams);
    }


 public void startRequestVideoInfo(String videoUrl, Map<String, String> headers, Map<String, Object> extraParams) {

...
//拿到缓存分片信息后,开始触发ranged逻辑
startNonM3U8Task(videoCacheInfo, headers);
...
}

3.2 startNonM3U8Task: MP4 フラグメント タスクのキャッシュ開始

//VideoProxyCacheManager#startNonM3U8Task   

    private void startNonM3U8Task(VideoCacheInfo cacheInfo, Map<String, String> headers) {
        VideoCacheTask cacheTask = mCacheTaskMap.get(cacheInfo.getVideoUrl());
        if (cacheTask == null) {
            //创建mp4缓存任务
            cacheTask = new Mp4CacheTask(cacheInfo, headers);
            //加入到map中,
            mCacheTaskMap.put(cacheInfo.getVideoUrl(), cacheTask);
        }
        startVideoCacheTask(cacheTask, cacheInfo);
    }

private void startVideoCacheTask(VideoCacheTask cacheTask, VideoCacheInfo cacheInfo) {
...
     //开始缓存任务
        cacheTask.startCacheTask();
...
}

3.3 キャッシュ用のスレッドを開く

//Mp4CacheTask#startCacheTask      
public void startCacheTask() {
        //如果文件缓存完(整个文件,而不是单个缓存分片文件),直接通知完成
        if (mCacheInfo.isCompleted()) {
            notifyOnTaskCompleted();
            return;
        }
        notifyOnTaskStart();
        LogUtils.i(TAG, "startCacheTask");
        //获取缓存分片的对象(start 、end)
        VideoRange requestRange = getRequestRange(0L);
        //启动线程(线程池方式)进行缓存(下载)
        startVideoCacheThread(requestRange);
    }

    private void startVideoCacheThread(VideoRange requestRange) {
        mRequestRange = requestRange;
        //saveDir 是videocacheinfo存储的目录
        mVideoCacheThread = new Mp4VideoCacheThread(mVideoUrl, mHeaders, requestRange, mTotalSize, mSaveDir.getAbsolutePath(), mCacheThreadListener);
        //通过线程池来执行
   VideoProxyThreadUtils.submitRunnableTask(mVideoCacheThread);
    }

3.4 Mp4VideoCacheThread の実装を見てみましょう

public class Mp4VideoCacheThread implements Runnable {

   ...

    private VideoRange mRequestRange;//当前请求的video range
                     
    private boolean mIsRunning = true; //是否增长运行,该任务可以pause

    private String mMd5; //缓存文件的md5
    ...


    public void run() {
        //该缓存任务可以pause,如果没有在running直接返回
        if (!mIsRunning) {
            return;
        }
        //支持OKHttp和HttpUrlConnection两种方式进行网络请求
        if (ProxyCacheUtils.getConfig().useOkHttp()) {
            downloadVideoByOkHttp();
        } else {
            //使用HttpUrlConnection
            downloadVideo();
        }
    }
}

3.5 HttpUrlConnection がネットワーク リクエストを行う方法を分析してみ
ましょう.ここでは物理的なファイル ホールのスキームが採用されており、データのあるものは埋められていることがわかります。キャッシュキャッシュ情報ファイル(すべての開始および終了情報を記録)については、notifyOnCacheRangeCompleted などで更新します。

  /**
     * 通过HttpUrlConnection下载缓存片段
     */
    private void downloadVideo() {
        File videoFile;
        try {
            //mSaveDir是存储缓存片段的文件夹,该文件夹下有videocacheinfo和各个缓存片段;
            videoFile = new File(mSaveDir, mMd5 + StorageUtils.NON_M3U8_SUFFIX);
            if (!videoFile.exists()) {
                videoFile.createNewFile();
            }
        } catch (Exception e) {
            notifyOnCacheFailed(new VideoCacheException("Cannot create video file, exception="+e));
            return;
        }

        long requestStart = mRequestRange.getStart();
        long requestEnd = mRequestRange.getEnd();
        mHeaders.put("Range", "bytes=" + requestStart + "-" + requestEnd);
        HttpURLConnection connection = null;
        InputStream inputStream = null;
        RandomAccessFile randomAccessFile = null;

        try {
            //这里采用了物理文件空洞的方案。有数据的进行填充,并通过缓存信息文件记录所有的start和end信息
            randomAccessFile = new RandomAccessFile(videoFile.getAbsolutePath(), "rw");
            randomAccessFile.seek(requestStart);
            //这里为什么要把requestStart赋值给cachedSize??这里的命名不好改为cachedOffset更合适
            long cachedOffset = requestStart;
            LogUtils.i(TAG, "Start request : " + mRequestRange + ", CurrentCachedSize="+cachedOffset);
            connection = HttpUtils.getConnection(mVideoUrl, mHeaders);
            inputStream = connection.getInputStream();
            LogUtils.i(TAG, "Receive response");

            byte[] buffer = new byte[StorageUtils.DEFAULT_BUFFER_SIZE];
            int readLength;
            while(mIsRunning && (readLength = inputStream.read(buffer)) != -1) {
                if (cachedOffset >= requestEnd) {
                    cachedOffset = requestEnd;
                }
                if (cachedOffset + readLength > requestEnd) {
                    long read = requestEnd - cachedOffset;
                    randomAccessFile.write(buffer, 0, (int)read);
                    cachedOffset = requestEnd;
                } else {
                    randomAccessFile.write(buffer, 0, readLength);
                    cachedOffset += readLength;
                }

                //更新缓存进度
                notifyOnCacheProgress(cachedOffset);

                if (cachedOffset >= requestEnd) {
                    //缓存好了一段,通知回调
                    notifyOnCacheRangeCompleted();
                }
            }
            mIsRunning = false;
        } catch (Exception e) {
            notifyOnCacheFailed(e);
        } finally {
            mIsRunning = false;
            ProxyCacheUtils.close(inputStream);
            ProxyCacheUtils.close(randomAccessFile);
            HttpUtils.closeConnection(connection);
        }
    }

3.6 更新キャッシュ断片化情報の通知

//Mp4CacheTask#notifyOnCacheRangeCompleted  
   /**
     * @param startPosition :上一个缓存分片的 end
     */  
 private void notifyOnCacheRangeCompleted(long startPosition) {
        //这时候已经缓存好了一段分片,可以更新一下video range数据结构了
        updateVideoRangeInfo();
        if (mCacheInfo.isCompleted()) {
            notifyOnTaskCompleted();
        } else {
            if (startPosition == mTotalSize) {
                //说明已经缓存好,但是整视频中间还有一些洞,但是不影响,可以忽略
            } else {
                //开启下一段视频分片的缓存
                VideoRange requestRange = getRequestRange(startPosition);
                //是否开启下一缓存分片的下载。
                // 这里可以再精准的控制下,按需下载
                startVideoCacheThread(requestRange);
            }
        }
    }

3.7 キャッシュの断片化情報を更新する
この方法はより重要で、キャッシュの断片化情報を統合し、重複部分をマージし、videoRange リストを再生成します。更新後のファイルに更新します

//Mp4CacheTask#updateVideoRangeInfo

private synchronized void updateVideoRangeInfo() {
        if (mVideoRangeMap.size() > 0) {
            long finalStart = -1;
            long finalEnd = -1;

            long requestStart = mRequestRange.getStart();
            long requestEnd = mRequestRange.getEnd();

            for(Map.Entry<Long, VideoRange> entry : mVideoRangeMap.entrySet()) {
                VideoRange videoRange = entry.getValue();
                long startResult = VideoRangeUtils.determineVideoRangeByPosition(videoRange, requestStart);
                long endResult = VideoRangeUtils.determineVideoRangeByPosition(videoRange, requestEnd);

                if (finalStart == -1) {
                    if (startResult == 1) {
                        //如果requestStart小于遍历的一个片段的start位置,取requestStart
                        finalStart = requestStart;
                    } else if (startResult == 2) {
                        //如果requestStart在遍历的一个片段的start和end中,取该片段的start
                        finalStart = videoRange.getStart();
                    } else {
                        //如果超出继续遍历其他片段,进行对比
                        //先别急着赋值,还要看下一个videoRange
                    }
                }
                if (finalEnd == -1) {
                    if (endResult == 1) {
                        finalEnd = requestEnd;
                    } else if (endResult == 2) {
                        finalEnd = videoRange.getEnd();
                    } else {
                        //先别急着赋值,还要看下一个videoRange
                    }
                }
                //该循环的目的是确定finalStart和finalEnd,用于确定VideoRange
                if (finalStart != -1 && finalEnd != -1) {
                    break;
                }
            }
            if (finalStart == -1) {
                finalStart = requestStart;
            }
            if (finalEnd == -1) {
                finalEnd = requestEnd;
            }

            VideoRange finalVideoRange = new VideoRange(finalStart, finalEnd);
            LogUtils.i(TAG, "updateVideoRangeInfo--->finalVideoRange: " + finalVideoRange);

            LinkedHashMap<Long, VideoRange> tempVideoRangeMap = new LinkedHashMap<>();
            for(Map.Entry<Long, VideoRange> entry : mVideoRangeMap.entrySet()) {
                VideoRange videoRange = entry.getValue();
                if (VideoRangeUtils.containsVideoRange(finalVideoRange, videoRange)) {
                    //如果finalVideoRange包含videoRange
                    tempVideoRangeMap.put(finalVideoRange.getStart(), finalVideoRange);
                } else if (VideoRangeUtils.compareVideoRange(finalVideoRange, videoRange) == 1) {
                    //如果两个没有交集,且finalVideoRange的end 小于videoRange的start,则map先加入finalVideoRange再加入videoRange
                    tempVideoRangeMap.put(finalVideoRange.getStart(), finalVideoRange);
                    tempVideoRangeMap.put(videoRange.getStart(), videoRange);
                } else if (VideoRangeUtils.compareVideoRange(finalVideoRange, videoRange) == 2) {
                    //如果两个没有交集,且finalVideoRange的start 大于videoRange的end,则map先加入videoRange再加入finalVideoRange
                    tempVideoRangeMap.put(videoRange.getStart(), videoRange);
                    tempVideoRangeMap.put(finalVideoRange.getStart(), finalVideoRange);
                }
            }
            mVideoRangeMap.clear();
            mVideoRangeMap.putAll(tempVideoRangeMap);
        } else {
            LogUtils.i(TAG, "updateVideoRangeInfo--->mRequestRange : " + mRequestRange);
            mVideoRangeMap.put(mRequestRange.getStart(), mRequestRange);
        }

        LinkedHashMap<Long, Long> tempSegMap = new LinkedHashMap<>();
        //进行了merge?
        for(Map.Entry<Long, VideoRange> entry : mVideoRangeMap.entrySet()) {
            VideoRange videoRange = entry.getValue();
            LogUtils.i(TAG, "updateVideoRangeInfo--->Result videoRange : " + videoRange);
            tempSegMap.put(videoRange.getStart(), videoRange.getEnd());
        }
        //最小化锁的作用范围
        synchronized (mSegMapLock) {
            mVideoSegMap.clear();
            mVideoSegMap.putAll(tempSegMap);
        }
        mCacheInfo.setVideoSegMap(mVideoSegMap);

        // 当mVideoRangeMap只有一个片段,并且该ranged是完整的这个那个缓存文件(不是某个子片段),则标记为completed
        if (mVideoRangeMap.size() == 1) {
            VideoRange videoRange = mVideoRangeMap.get(0L);
            LogUtils.i(TAG, "updateVideoRangeInfo---> videoRange : " + videoRange);
            if (videoRange != null && videoRange.equals(new VideoRange(0, mTotalSize))) {
                LogUtils.i(TAG, "updateVideoRangeInfo--->Set completed");
                mCacheInfo.setIsCompleted(true);
            }
        }

        //子线程中,更新缓存信息文件
        saveVideoInfo();
    }

 public static void saveVideoCacheInfo(VideoCacheInfo info, File dir) {
        File file = new File(dir, INFO_FILE);
        ObjectOutputStream fos = null;
        try {
            synchronized (sInfoFileLock) {
                fos = new ObjectOutputStream(new FileOutputStream(file));
                fos.writeObject(info);
            }
        } catch (Exception e) {

        } finally {
            ProxyCacheUtils.close(fos);
        }
    }

キャッシング シャーディングの物理ファイル ホールのスキームの分析と分析は基本的にここまでです. オープン ソースの JeffVideoCache の作者に感謝します. 次の記事では、キャッシュ シャーディングで使用される論理ファイル ホールのスキームを分析します
.交換

4. 情報

  1. ジェフビデオキャッシュ
  2. Toutiao はサイド バイ サイド ブロードキャスト ソリューションを使用しています
  3. 主要な推奨事項 - QZone の 10 億レベルのビデオ再生技術の最適化により、Wang Hui の最終ドラフトが明らかになりました 2.key

5.収穫

この記事の学習分析より

  1. キャッシュ シャーディングとは何か、なぜ、どのように行うのかを学ぶ
  2. キャッシュ フラグメンテーション物理ファイル ホール スキームの実装を分析します。

お読みいただきありがとうございます.
次の記事では, キャッシュの断片化ロジック ファイル ホール ソリューションの実装を分析します. 公式アカウント「オーディオとビデオの開発の旅」に注目して、一緒に学び、成長してください.
交換へようこそ

おすすめ

転載: blog.csdn.net/u011570979/article/details/119535614
おすすめ