├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── hz │ │ └── videocache │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── hz │ │ │ └── videocache │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── hz │ └── videocache │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── Readme.md ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── danikula │ └── videocache │ ├── ByteArrayCache.java │ ├── ByteArraySource.java │ ├── Cache.java │ ├── CacheListener.java │ ├── Config.java │ ├── GetRequest.java │ ├── HttpProxyCache.java │ ├── HttpProxyCacheServer.java │ ├── HttpProxyCacheServerClients.java │ ├── HttpUrlSource.java │ ├── IgnoreHostProxySelector.java │ ├── InterruptedProxyCacheException.java │ ├── M3u8ProxyUtil.java │ ├── Pinger.java │ ├── Preconditions.java │ ├── ProxyCache.java │ ├── ProxyCacheException.java │ ├── ProxyCacheUtils.java │ ├── Source.java │ ├── SourceInfo.java │ ├── StorageUtils.java │ ├── file │ ├── DiskUsage.java │ ├── FileCache.java │ ├── FileNameGenerator.java │ ├── Files.java │ ├── LruDiskUsage.java │ ├── Md5FileNameGenerator.java │ ├── TotalCountLruDiskUsage.java │ ├── TotalSizeLruDiskUsage.java │ └── UnlimitedDiskUsage.java │ ├── headers │ ├── EmptyHeadersInjector.java │ └── HeaderInjector.java │ ├── preload │ ├── PreloadHelper.java │ ├── PreloadLog.java │ └── PreloadTaskRunnable.java │ └── sourcestorage │ ├── DatabaseSourceInfoStorage.java │ ├── NoSourceInfoStorage.java │ ├── SourceInfoStorage.java │ └── SourceInfoStorageFactory.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VideoCacheSample 2 | 基于AndroidVideoCache 实现短视频秒加载边下边播;M3u8支持 3 | 4 | 如果你的原项目本身就集成了AndroidVideoCache库,可以直接依赖我的Library,没有冲突Api。只对源库少量文件做了修改。 5 | 6 | ### AndroidVideoCache(改造版) 7 | [点击跳转到 AndroidVideoCache](https://github.com/danikula/AndroidVideoCache) 8 | 9 | 基于AndroidVideoCache(v2.7.1)进行修改。主要增加以下特性: 10 | * 媒体文件预加载功能,用于实现短视频秒开场景 11 | * M3u8边下边存的支持 12 | 13 | #### 预加载 14 | 预加载功能类为 > com.danikula.videocache.preload.PreloadHelper 15 | ``` 16 | //设置预加载缓存大小单位字节,默认 256KB -> 256*1024 17 | PreloadHelper.getInstance().setPreloadSize(512*1024); 18 | //加载制定url链接,url为源地址(非代理url) 19 | PreloadHelper.getInstance().load(cacheServer,url); 20 | //停止所有预加载。线程池默认有5个预加载线程,只能停止还没执行的 21 | PreloadHelper.getInstance().stopAllPreload(); 22 | // 23 | ``` 24 | 25 | #### M3u8边下边存 26 | 因M3u8本身就做了分片处理,这里没有再去做预加载功能,主要实现思路: 27 | 1. 获取代理M3u8链接,访问本地代理。 28 | 2. 如果文件还没缓存,去下载并保存到本地,有下载直接读缓存(缓存的文件和其它格式一样,内容也是源文件的内容)。 29 | 3. 读取缓存,解析文件内容,替换分片视频地址指向本地代理-M3u8ProxyUtil:rewriteProxyBody 30 | 4. 将上一步替换后的内容,响应到socket也就是播放器。播放器自动播放指向代理的分片,然后走普通媒体缓存逻辑。 31 | 32 | 注意一下M3u8始终要经过我们的代理服务,而普通媒体文件缓存后是直接返回File地址。 33 | HttpProxyCacheServer.getProxyUrl(String url) 34 | ``` 35 | public String getProxyUrl(String url, boolean allowCachedFileUri) { 36 | if (allowCachedFileUri && isCached(url) && !M3u8ProxyUtil.isM3u8Url(url)) {//m3u8始终走代理 37 | File cacheFile = getCacheFile(url); 38 | touchFileSafely(cacheFile); 39 | return Uri.fromFile(cacheFile).toString(); 40 | } 41 | return isAlive() ? appendToProxyUrl(url) : url; 42 | } 43 | ``` 44 | 45 | #### 偶然出现的代理服务ping报错解决办法 46 | com.danikula.videocache.ProxyCacheException: Error pinging server (attempts: 3, max timeout: 280). If you see this message, please, report at https://github.com/danikula/AndroidVideoCache/issues/134. 47 | 48 | 1. add ` android:usesCleartextTraffic="true" ` into AndroidManifest.xml application; 49 | 2. add ` android:networkSecurityConfig="@xml/network_security_config" ` into AndroidManifest.xml application; 50 | ``` 51 | 52 | 53 | 54 | 127.0.0.1 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ``` 63 | 3. add `` into AndroidManifest.xml application; 64 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | compileSdk 32 7 | 8 | defaultConfig { 9 | applicationId "com.hz.videocache" 10 | minSdk 21 11 | targetSdk 32 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | } 29 | 30 | dependencies { 31 | 32 | implementation 'androidx.appcompat:appcompat:1.3.0' 33 | implementation 'com.google.android.material:material:1.4.0' 34 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 35 | implementation project(path: ':library') 36 | testImplementation 'junit:junit:4.13.2' 37 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 38 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 39 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hz/videocache/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.hz.videocache; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("com.hz.videocache", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/hz/videocache/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.hz.videocache; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.appcompat.app.AppCompatActivity; 5 | 6 | import android.media.MediaPlayer; 7 | import android.os.Bundle; 8 | import android.util.Log; 9 | import android.view.SurfaceHolder; 10 | import android.view.SurfaceView; 11 | import android.view.View; 12 | import android.widget.FrameLayout; 13 | 14 | import com.danikula.videocache.CacheListener; 15 | import com.danikula.videocache.HttpProxyCacheServer; 16 | import com.danikula.videocache.preload.PreloadHelper; 17 | 18 | import java.io.File; 19 | import java.io.IOException; 20 | 21 | public class MainActivity extends AppCompatActivity { 22 | private static final String TAG = MainActivity.class.getSimpleName(); 23 | HttpProxyCacheServer cacheServer; 24 | MediaPlayer mediaPlayer; 25 | 26 | @Override 27 | protected void onCreate(Bundle savedInstanceState) { 28 | super.onCreate(savedInstanceState); 29 | setContentView(R.layout.activity_main); 30 | 31 | mediaPlayer = new MediaPlayer(); 32 | 33 | if (cacheServer == null) { 34 | cacheServer = new HttpProxyCacheServer(this.getApplicationContext()); 35 | } 36 | } 37 | 38 | // String url = "https://vd2.bdstatic.com/mda-ndhguzmmzz9hkztg/cae_h264_delogo/1650328165572422632/mda-ndhguzmmzz9hkztg.mp4";//12f 39 | // String url = "https://vd3.bdstatic.com/mda-nck9gvjnys5duwbc/cae_h264_delogo/1647845706366501738/mda-nck9gvjnys5duwbc.mp4";//5f 40 | String url = "https://ksv-video-publish-m3u8.cdn.bcebos.com/d30ff91063ee24746b752a65903eda95dbfae750.m3u8"; 41 | 42 | public void clickCache(View view) { 43 | // String url = "https://vd3.bdstatic.com/mda-mme9x7eeygvncdpk/sc/cae_h264/1639596540411766788/mda-mme9x7eeygvncdpk.mp4"; 44 | 45 | //设置预加载缓存大小单位字节,默认 256KB -> 256*1024 46 | PreloadHelper.getInstance().setPreloadSize(512*1024); 47 | //加载制定url链接,url为源地址(非代理url) 48 | PreloadHelper.getInstance().load(cacheServer,url); 49 | cacheServer.registerCacheListener(new CacheListener() { 50 | @Override 51 | public void onCacheAvailable(File cacheFile, String url, int percentsAvailable) { 52 | Log.i(TAG, "onCacheAvailable: "+url+" "+percentsAvailable); 53 | } 54 | },url); 55 | 56 | } 57 | 58 | public void clickStart(View view) { 59 | start(cacheServer.getProxyUrl(url)); 60 | } 61 | 62 | public void start(String path) { 63 | try { 64 | SurfaceView surfaceView = new SurfaceView(this); 65 | surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { 66 | @Override 67 | public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) { 68 | 69 | } 70 | 71 | @Override 72 | public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) { 73 | 74 | } 75 | 76 | @Override 77 | public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) { 78 | 79 | } 80 | }); 81 | // mediaPlayer.setSurface(surfaceView.getHolder().getSurface()); 82 | FrameLayout view = findViewById(R.id.video_container); 83 | view.addView(surfaceView); 84 | 85 | mediaPlayer.setDataSource(path); 86 | mediaPlayer.prepare(); 87 | 88 | mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { 89 | @Override 90 | public void onPrepared(MediaPlayer mediaPlayer) { 91 | mediaPlayer.setDisplay(surfaceView.getHolder()); 92 | mediaPlayer.start(); 93 | } 94 | }); 95 | 96 | long start = System.currentTimeMillis(); 97 | mediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() { 98 | @Override 99 | public void onVideoSizeChanged(MediaPlayer mediaPlayer, int w, int h) { 100 | Log.i(TAG, "onVideoSizeChanged:" + w + "-" + h+" time:"+(System.currentTimeMillis()-start)); 101 | } 102 | }); 103 | } catch (IOException e) { 104 | e.printStackTrace(); 105 | } 106 | } 107 | 108 | @Override 109 | protected void onDestroy() { 110 | super.onDestroy(); 111 | if(mediaPlayer.isPlaying()){ 112 | mediaPlayer.stop(); 113 | } 114 | mediaPlayer.release(); 115 | mediaPlayer = null; 116 | } 117 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 18 | 19 | 26 |