鴻蒙開(kāi)源第三方組件-VideoCache視頻緩存組件
想了解更多內(nèi)容,請(qǐng)?jiān)L問(wèn):
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
前言
基于安卓平臺(tái)的視頻緩存組件VideoCache( https://github.com/danikula/AndroidVideoCache),實(shí)現(xiàn)了鴻蒙化遷移和重構(gòu),代碼已經(jīng)開(kāi)源到(https://gitee.com/isrc_ohos/android-video-cache_ohos),歡迎各位下載使用并提出寶貴意見(jiàn)!
背景
用戶在網(wǎng)速波動(dòng)較大的環(huán)境下瀏覽視頻時(shí),經(jīng)常會(huì)遇到由于網(wǎng)速較慢引起的持續(xù)加載或播放失敗的情況。VideoCache組件實(shí)現(xiàn)了視頻緩存功能,播放視頻的同時(shí),對(duì)視頻源進(jìn)行緩存。出現(xiàn)網(wǎng)速較慢的情況時(shí),手機(jī)讀取提前緩存好的視頻數(shù)據(jù),可以保證視頻的正常播放,給予用戶更流暢的觀看體驗(yàn)。
組件效果圖展示
1、主菜單界面: 視頻播放
安裝軟件后,只需要在鴻蒙設(shè)備上單擊HarmonyVideoCache軟件圖標(biāo),打開(kāi)軟件即可進(jìn)入主菜單界面,進(jìn)入主菜單界面后會(huì)自動(dòng)開(kāi)始播放視頻,如下圖所示。
圖 1 視頻播放的主菜單界面
2、驗(yàn)證緩存
等待視頻播放完成后,可以手動(dòng)關(guān)閉手機(jī)的數(shù)據(jù)連接和WIFI連接。
圖 2 關(guān)閉網(wǎng)絡(luò)連接
在關(guān)閉了網(wǎng)絡(luò)連接之后,回到VideoCache應(yīng)用中,點(diǎn)擊播放按鈕, 會(huì)發(fā)現(xiàn)視頻是可以通過(guò)本地緩存重新播放的。注意到圖1和圖3的區(qū)別,在圖1中任務(wù)欄可以看到有WIFI連接顯示,圖3 中沒(méi)有WIFI連接。
圖 3 緩存播放視頻
Sample解析
如圖4所示,該組件在本地與遠(yuǎn)程服務(wù)器之間建立了代理服務(wù)器。當(dāng)本地發(fā)送視頻網(wǎng)絡(luò)請(qǐng)求至代理服務(wù)器時(shí),代理服務(wù)器與遠(yuǎn)程服務(wù)器之間通過(guò)代理Socket連接,并將遠(yuǎn)程服務(wù)器的視頻數(shù)據(jù)回寫(xiě)到代理服務(wù)器的緩存中,本地播放視頻時(shí)從代理服務(wù)器的緩存中讀取數(shù)據(jù)(圖4援引自https://www.jianshu.com/p/4745de02dcdc)。下面詳細(xì)介紹視頻緩存的步驟。
圖4 VideoCache組件的視頻緩存原理
1、實(shí)例化HttpProxyCacheServer類的對(duì)象
HttpProxyCacheServer類可用于處理來(lái)自視頻播放器的播放請(qǐng)求,當(dāng)本地有緩存時(shí),向視頻播放器返回一個(gè)本地IP地址(LocalURL:以127.0.0.1開(kāi)頭),用于視頻的播放。
- private HttpProxyCacheServer mCacheServerProxy=null;
- public void onStart(Intent intent) {
- ...
- if (mCacheServerProxy == null) {
- Context context = this;
- //實(shí)例化HttpProxyCacheServer對(duì)象
- mCacheServerProxy = new HttpProxyCacheServer(context);
- }
- ...
- }
2、定義緩存監(jiān)聽(tīng)器CacheListener
CacheListener 用于監(jiān)聽(tīng)文件緩存的進(jìn)度,方便開(kāi)發(fā)者通過(guò)判斷緩存進(jìn)度,執(zhí)行各類操作。
onCacheAvailable()方法是設(shè)置CacheListener 監(jiān)聽(tīng)器時(shí)需要重寫(xiě)的方法,此方法的參數(shù)中:cacheFile表示緩存文件的地址;url表示網(wǎng)絡(luò)視頻的URL;percentsAvailable表示緩存進(jìn)度,取值為1~100,取值為100時(shí)表示全部視頻緩存完成。
基于percentAvailable變量,大多數(shù)視頻播放器有以下設(shè)計(jì):設(shè)置一個(gè)變量用于保存當(dāng)前的視頻播放進(jìn)度。在緩存監(jiān)聽(tīng)器CacheListener 中,比較當(dāng)前緩存進(jìn)度與當(dāng)前播放進(jìn)度的差值,如果超出了預(yù)設(shè)值,可以執(zhí)行特定操作以暫停緩存,直至二者的差值小于預(yù)設(shè)值,重新啟動(dòng)緩存。
- private CacheListener mCacheListener = new CacheListener() {
- @Override
- public void onCacheAvailable(File cacheFile, String url, int percentsAvailable) {
- //打印實(shí)時(shí)緩存進(jìn)度
- HiLog.info(new HiLogLabel(3,0,"cache"),"Saving……,percent:"+String.valueOf(percentsAvailable));
- //當(dāng)進(jìn)度達(dá)到100時(shí),可進(jìn)行一些特殊操作,此處僅以log打印為例
- if (percentsAvailable == 100 && !cacheFile.getPath().endsWith(".download")) {
- HiLog.info(new HiLogLabel(3,0,"cache"),"Download already!");
- }
- }
- };
3. 獲取LocalURL
將網(wǎng)絡(luò)視頻的URL與步驟2中的監(jiān)聽(tīng)器對(duì)象mCacheListener傳入HttpProxyCacheServer類的注冊(cè)方法中,即可對(duì)緩存進(jìn)行監(jiān)聽(tīng)。后通過(guò) HttpProxyCacheServer類的getProxyUrl()方法獲取網(wǎng)絡(luò)視頻URL對(duì)應(yīng)的LocalUrl。
- //注冊(cè)下載緩存監(jiān)聽(tīng)
- mCacheServerProxy.registerCacheListener(mCacheListener,URL);
- //獲取LocalURL
- localUrl = mCacheServerProxy.getProxyUrl(URL);
4、 使用LocalUrl作為視頻來(lái)源進(jìn)行播放,緩存功能即可實(shí)現(xiàn)。
Library解析
整個(gè)library分為五個(gè)部分:file、headers、slice、sourcestorage以及22個(gè)類文件,如圖2所示。
圖5 library的組成結(jié)構(gòu)
一、file
在file文件夾下的類主要涉及文件緩存相關(guān)的功能:
圖6 file文件夾的組成結(jié)構(gòu)
1、FileCache類
類中規(guī)定了緩存文件的命名格式(后加.download)和存儲(chǔ)的路徑,完成了緩存文件的創(chuàng)建。
- //定義緩存文件的后綴格式
- private static final String TEMP_POSTFIX = ".download";
- public FileCache(File file, DiskUsage diskUsage) throws ProxyCacheException {
- ...
- File directory = file.getParentFile();
- Files.makeDir(directory);
- boolean completed = file.exists();
- //文件的保存格式:根目錄文件+文件名+之前定義的文件后綴格式
- this.file = completed ? file : new File(file.getParentFile(), file.getName() + TEMP_POSTFIX);
- //文件權(quán)限設(shè)置。緩存完成,文件只能讀取;未緩存完成,文件可讀可寫(xiě)。
- this.dataFile = new RandomAccessFile(this.file, completed ? "r" : "rw");
- } catch (IOException e) {
- throw new ProxyCacheException("Error using file " + file + " as disc cache", e);
- }
2、Files類
此類是對(duì)JAVA中原有的File類的封裝,原File類僅可處理一個(gè)文件,F(xiàn)iles類可同時(shí)對(duì)多個(gè)文件進(jìn)行處理。
如下代碼中,getLruListFiles()方法的參數(shù)是一個(gè)directory,在方法中對(duì)directory(文件夾路徑)下的所有文件進(jìn)行拆分,返回了一個(gè)File參數(shù)類型的List列表,后續(xù)可對(duì)列表中的各個(gè)File文件進(jìn)行處理。
- static List<File> getLruListFiles(File directory) {
- //通過(guò)list對(duì)Files內(nèi)的文件進(jìn)行處理
- List<File> result = new LinkedList<>();
- File[] files = directory.listFiles();
- //為各file建立LastModifiedComparator
- //LastModifiedComparator可用于根據(jù)文件的上次修改的日期文件進(jìn)行排序
- if (files != null) {
- result = Arrays.asList(files);
- Collections.sort(result, new LastModifiedComparator());
- }
- return result;
- }
3、LruDiskUsage類
此類主要用于控制緩存文件的大小,它與Videocache平行開(kāi)了一個(gè)線程,實(shí)時(shí)記錄緩存文件的數(shù)量、大小、存儲(chǔ)空間等,超過(guò)預(yù)設(shè)的閾值時(shí),執(zhí)行特定的優(yōu)化操作。
- private void trim(List<File> files) {
- long totalSize = countTotalSize(files); //緩存文件的總大小
- int totalCount = files.size(); //緩存文件的總數(shù)量
- for (File file : files) {
- //未超過(guò)緩存文件的(總大小 & 總數(shù)量)的閾值時(shí),接收緩存
- boolean accepted = accept(file, totalSize, totalCount);
- if (!accepted) {
- long fileSize = file.length(); // 單一文件的大小
- boolean deleted = file.delete(); //文件是否為預(yù)備刪除的文件
- //如果是準(zhǔn)備刪除的文件
- if (deleted) {
- totalCount--; // 緩存文件的總數(shù)量-1
- totalSize -= fileSize; //緩存文件的總大小 - 預(yù)備刪除的單一文件的大小
- LOG.info("Cache file " + file +
- " is deleted because it exceeds cache limit");
- } else {
- LOG.error("Error deleting file " + file + " for trimming cache");
- }
- }
- }
- }
4、 Md5FileNameGenerator類
此類實(shí)現(xiàn)了為輸入文件路徑,生成對(duì)應(yīng)的MD5值的功能。MD5值是一種被"壓縮"的保密格式,可以確保信息完整傳輸。
- public class Md5FileNameGenerator implements FileNameGenerator {
- private static final int MAX_EXTENSION_LENGTH = 4;
- @Override
- public String generate(String url) {
- //獲取文件名的后綴
- String extension = getExtension(url);
- //獲取MD5值
- String name = ProxyCacheUtils.computeMD5(url);
- Boolean isEmpty = false;
- //文件后綴名為空時(shí),設(shè)置isEmpty 標(biāo)志位為true
- if (extension == null || extension.length() == 0)
- isEmpty = true;
- return isEmpty ? name : name + "." + extension;
- }
5、TotalCountLruDiskUsage類、TotalSizeLruDiskUsage類和UnlimitedDiskUsage類
LruDiskUsage類是標(biāo)題中前兩個(gè)類的父類,同時(shí)控制緩存文件的大小和數(shù)量,需要判斷當(dāng)前緩存文件的(總大小 & 總數(shù)量)未超過(guò)閾值時(shí),才會(huì)緩存新的文件。 TotalCountLruDiskUsage類和TotalSizeLruDiskUsage類分別只對(duì)緩存文件總數(shù)量或者緩存文件總大小進(jìn)行限制,滿足一個(gè)條件便可以緩存新的文件。
TotalCountLruDiskUsage類和TotalSizeLruDiskUsage類各有兩個(gè)方法:一個(gè)方法用于設(shè)定緩存文件的閾值;一個(gè)方法用于判斷當(dāng)前緩存數(shù)據(jù)是否超過(guò)了設(shè)定的閾值。
當(dāng)不需要進(jìn)行磁盤(pán)的緩存限制時(shí)使用UnlimitedDiskUsage類,其本身是一個(gè)空的類,不對(duì)緩存文件的數(shù)量和大小做任何限制。
- //控制緩存文件的總數(shù)量
- public class TotalCountLruDiskUsage extends LruDiskUsage {
- private final int maxCount;
- //設(shè)置緩存文件的總數(shù)量的閾值
- public TotalCountLruDiskUsage(int maxCount) {
- if (maxCount <= 0) {
- throw new IllegalArgumentException("Max count must be positive number!");
- }
- this.maxCount = maxCount;
- }
- //當(dāng)前緩存文件的總數(shù)量小于設(shè)定的閾值時(shí),新文件accept
- @Override
- protected boolean accept(File file, long totalSize, int totalCount) {
- return totalCount <= maxCount;
- }
- }
- //控制制緩存文件的總大小
- public class TotalSizeLruDiskUsage extends LruDiskUsage {
- private final long maxSize;
- //設(shè)置制緩存文件的總大小的閾值
- public TotalSizeLruDiskUsage(long maxSize) {
- if (maxSize <= 0) {
- throw new IllegalArgumentException("Max size must be positive number!");
- }
- this.maxSize = maxSize;
- }
- //當(dāng)前緩存文件的總大小小于設(shè)定的閾值時(shí),新文件accept
- @Override
- protected boolean accept(File file, long totalSize, int totalCount) {
- return totalSize <= maxSize;
- }
- }
二、headers
文件中涉及到的功能不多,僅有一個(gè)接口文件和一個(gè)能實(shí)現(xiàn)URL和文件路徑hashmap匹配功能的類文件,上述功能在HttpProxyCacheServer類中被調(diào)用。
圖7 headers文件夾的組成結(jié)構(gòu)
三、slice
鴻蒙程序的slice控件用于三方件遷移中的可視化調(diào)試,在這里我們對(duì)其不作進(jìn)一步的分析。
圖8 slice文件夾的組成結(jié)構(gòu)
四、sourcestorage
sourcestorage用于在數(shù)據(jù)庫(kù)中存儲(chǔ)SourInfo。SourInfo可用于存儲(chǔ)http請(qǐng)求源的一些信息,如URL,數(shù)據(jù)長(zhǎng)度Length,請(qǐng)求資源的類型MIME等。sourcestorage中的類主要在上述的HttpProxyCacheServer類中被調(diào)用。
圖9 sourcestorage文件夾的組成結(jié)構(gòu)
DatabaseSourceInfoStorage類用于做數(shù)據(jù)庫(kù)的初始化工作,數(shù)據(jù)庫(kù)里面存的字段主要是URL、Length、MIME,SourceInfo類是對(duì)這3個(gè)字段的封裝。類中包含了三個(gè)接口:get()、 put()、release(),可供外部調(diào)用,三個(gè)接口都是對(duì)SourceInfo的操作,主要用來(lái)查找和保存緩存的信息。
其余三個(gè)類是根據(jù)DatabaseSourceInfoStorage類進(jìn)行的工廠模式的生成,如果對(duì)這部分不明白的同學(xué)可以在網(wǎng)上搜索“設(shè)計(jì)模式-工廠模式”進(jìn)行學(xué)習(xí)。
- class DatabaseSourceInfoStorage extends DatabaseHelper implements SourceInfoStorage {
- //數(shù)據(jù)庫(kù)中存儲(chǔ)SourInfo:URL、Length、MIME
- private static final String TABLE = "SourceInfo";
- private static final String COLUMN_ID = "_id";
- private static final String COLUMN_URL = "url";
- private static final String COLUMN_LENGTH = "length";
- private static finavl String COLUMN_MIME = "mime";
- private static final String[] ALL_COLUMNS = new String[]{COLUMN_ID, COLUMN_URL,
- COLUMN_LENGTH, COLUMN_MIME};
- //創(chuàng)建數(shù)據(jù)庫(kù)的SQL
- private static final String CREATE_SQL =
- "CREATE TABLE " + TABLE + " (" +
- COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
- COLUMN_URL + " TEXT NOT NULL," +
- COLUMN_MIME + " TEXT," +
- COLUMN_LENGTH + " INTEGER" +
- ");";
- private final RdbStore myRdbStore;
- //連接的數(shù)據(jù)庫(kù)名字
- private final StoreConfig config =
- StoreConfig.newDefaultConfig("AndroidVideoCache.db");
- }
- //數(shù)據(jù)庫(kù)get指令,通過(guò)URL獲取SourceInfo
- public SourceInfo get(String url) {
- checkNotNull(url);
- ResultSet cursor = null;
- try{
- RdbPredicates predicates = new RdbPredicates(TABLE);
- predicates.equalTo(COLUMN_URL, url);
- cursor = this.myRdbStore.query(predicates, null);
- return cursor == null || !cursor.goToFirstRow() ? null : convert(cursor);
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- }
- //數(shù)據(jù)庫(kù)put指令,將url和SourceInfo在數(shù)據(jù)庫(kù)中登記綁定
- public void put(String url, SourceInfo sourceInfo) {
- checkAllNotNull(url, sourceInfo);
- SourceInfo sourceInfoFromDb = get(url);
- boolean exist = sourceInfoFromDb != null;
- RdbPredicates predicates = new RdbPredicates(TABLE);
- if (exist) {
- predicates.contains(COLUMN_URL, url);
- this.myRdbStore.update(convert(sourceInfo), predicates);
- } else {
- this.myRdbStore.insert(TABLE, convert(sourceInfo));
- }
- }
- //release指令:釋放數(shù)據(jù)庫(kù)控制流
- @Override
- public void release() {
- this.myRdbStore.close();
- }
五、主功能文件
這部分文件主要用于整合上述四個(gè)部分的功能,向外部提供VideoCache接口。
主要功能類如下圖所示,他們的外部調(diào)用方法在Sample中已經(jīng)詳細(xì)說(shuō)明,主要使用到的就是HttpProxyCacheServer類,下面對(duì)其內(nèi)部實(shí)現(xiàn)進(jìn)行詳細(xì)的講解。
圖10主要功能類主文件
1、構(gòu)造函數(shù)
在構(gòu)造函數(shù)中主要進(jìn)行了全局變量的初始化和對(duì)PROXY_HOST(VideoCache代理接口,也就是LocalURL所屬的代理接口)進(jìn)行訪問(wèn),判斷是否可以直接ping通。
- private HttpProxyCacheServer(Config config) {
- this.config = checkNotNull(config);
- try {
- //初始化各種全局變量
- InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
- this.serverSocket = new ServerSocket(0, 8, inetAddress);
- this.port = serverSocket.getLocalPort();
- IgnoreHostProxySelector.install(PROXY_HOST, port);
- CountDownLatch startSignal = new CountDownLatch(1);
- this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
- this.waitConnectionThread.start();
- startSignal.await(); // freeze thread, wait for server starts
- //獲取對(duì)PROXY_HOST& port的ping,判斷是否可以ping通
- this.pinger = new Pinger(PROXY_HOST, port);
- LOG.info("Proxy cache server started. Is it alive? " + isAlive());
- } catch (IOException | InterruptedException e) {
- socketProcessor.shutdown();
- throw new IllegalStateException("Error starting local proxy server", e);
- }
- }
2、registerCacheListener函數(shù)
這個(gè)函數(shù)主要實(shí)現(xiàn)的功能是對(duì)URL進(jìn)行注冊(cè)監(jiān)聽(tīng)。
- public void registerCacheListener(CacheListener cacheListener, String url) {
- checkAllNotNull(cacheListener, url);
- synchronized (clientsLock) {
- try {
- //對(duì)url獲取Clients,并為其注冊(cè)CacheListener
- getClients(url).registerCacheListener(cacheListener);
- } catch (ProxyCacheException e) {
- LOG.warn("Error registering cache listener", e);
- }
- }
- }
3、getProxyUrl函數(shù)
該函數(shù)實(shí)現(xiàn)了將(已經(jīng)注冊(cè)過(guò)的)URL轉(zhuǎn)化為cached LocalURL的功能。
- public String getProxyUrl(String url) {
- return getProxyUrl(url, true);
- }
- public String getProxyUrl(String url, boolean allowCachedFileUri) {
- if (allowCachedFileUri && isCached(url)) {
- File cacheFile = getCacheFile(url);
- touchFileSafely(cacheFile);
- return Uri.getUriFromFile(cacheFile).toString();
- }
- return isAlive() ? appendToProxyUrl(url) : url;
- }
當(dāng)傳入一個(gè)網(wǎng)絡(luò)視頻的URL時(shí),該方法會(huì)對(duì)該URL進(jìn)行判斷,如果可以在代理服務(wù)器上進(jìn)行緩存,則提供正確的LocalURL返回值,否則返回原URL。
項(xiàng)目貢獻(xiàn)人
呂澤 鄭森文 朱偉 陳美汝 張馨心
想了解更多內(nèi)容,請(qǐng)?jiān)L問(wèn):
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)