├── .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 |
39 | * public onCreate(Bundle state) {
40 | * super.onCreate(state);
41 | *
42 | * HttpProxyCacheServer proxy = getProxy();
43 | * String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
44 | * videoView.setVideoPath(proxyUrl);
45 | * }
46 | *
47 | * private HttpProxyCacheServer getProxy() {
48 | * // should return single instance of HttpProxyCacheServer shared for whole app.
49 | * }
50 | *
51 | *
52 | * @author Alexey Danilov (danikula@gmail.com).
53 | */
54 | public class HttpProxyCacheServer {
55 |
56 | private static final Logger LOG = LoggerFactory.getLogger("HttpProxyCacheServer");
57 | private static final String PROXY_HOST = "127.0.0.1";
58 |
59 | private final Object clientsLock = new Object();
60 | private final ExecutorService socketProcessor = Executors.newFixedThreadPool(8);
61 | private final Map94 | * If file for this url is fully cached (it means method {@link #isCached(String)} returns {@code true}) 95 | * then file:// uri to cached file will be returned. 96 | *
97 | * Calling this method has same effect as calling {@link #getProxyUrl(String, boolean)} with 2nd parameter set to {@code true}. 98 | * 99 | * @param url a url to file that should be cached. 100 | * @return a wrapped by proxy url if file is not fully cached or url pointed to cache file otherwise. 101 | */ 102 | public String getProxyUrl(String url) { 103 | return getProxyUrl(url, true); 104 | } 105 | 106 | /** 107 | * Returns url that wrap original url and should be used for client (MediaPlayer, ExoPlayer, etc). 108 | *
109 | * If parameter {@code allowCachedFileUri} is {@code true} and file for this url is fully cached 110 | * (it means method {@link #isCached(String)} returns {@code true}) then file:// uri to cached file will be returned. 111 | * 112 | * @param url a url to file that should be cached. 113 | * @param allowCachedFileUri {@code true} if allow to return file:// uri if url is fully cached 114 | * @return a wrapped by proxy url if file is not fully cached or url pointed to cache file otherwise (if {@code allowCachedFileUri} is {@code true}). 115 | */ 116 | public String getProxyUrl(String url, boolean allowCachedFileUri) { 117 | if (allowCachedFileUri && isCached(url) && !M3u8ProxyUtil.isM3u8Url(url)) {//m3u8始终走代理 118 | File cacheFile = getCacheFile(url); 119 | touchFileSafely(cacheFile); 120 | return Uri.fromFile(cacheFile).toString(); 121 | } 122 | return isAlive() ? appendToProxyUrl(url) : url; 123 | } 124 | 125 | public void registerCacheListener(CacheListener cacheListener, String url) { 126 | checkAllNotNull(cacheListener, url); 127 | synchronized (clientsLock) { 128 | try { 129 | getClients(url).registerCacheListener(cacheListener); 130 | } catch (ProxyCacheException e) { 131 | LOG.warn("Error registering cache listener", e); 132 | } 133 | } 134 | } 135 | 136 | public void unregisterCacheListener(CacheListener cacheListener, String url) { 137 | checkAllNotNull(cacheListener, url); 138 | synchronized (clientsLock) { 139 | try { 140 | getClients(url).unregisterCacheListener(cacheListener); 141 | } catch (ProxyCacheException e) { 142 | LOG.warn("Error registering cache listener", e); 143 | } 144 | } 145 | } 146 | 147 | public void unregisterCacheListener(CacheListener cacheListener) { 148 | checkNotNull(cacheListener); 149 | synchronized (clientsLock) { 150 | for (HttpProxyCacheServerClients clients : clientsMap.values()) { 151 | clients.unregisterCacheListener(cacheListener); 152 | } 153 | } 154 | } 155 | 156 | /** 157 | * Checks is cache contains fully cached file for particular url. 158 | * 159 | * @param url an url cache file will be checked for. 160 | * @return {@code true} if cache contains fully cached file for passed in parameters url. 161 | */ 162 | public boolean isCached(String url) { 163 | checkNotNull(url, "Url can't be null!"); 164 | return getCacheFile(url).exists(); 165 | } 166 | 167 | public void shutdown() { 168 | LOG.info("Shutdown proxy server"); 169 | 170 | shutdownClients(); 171 | 172 | config.sourceInfoStorage.release(); 173 | 174 | waitConnectionThread.interrupt(); 175 | try { 176 | if (!serverSocket.isClosed()) { 177 | serverSocket.close(); 178 | } 179 | } catch (IOException e) { 180 | onError(new ProxyCacheException("Error shutting down proxy server", e)); 181 | } 182 | } 183 | 184 | private boolean isAlive() { 185 | return pinger.ping(3, 70); // 70+140+280=max~500ms 186 | } 187 | 188 | private String appendToProxyUrl(String url) { 189 | return String.format(Locale.US, "http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url)); 190 | } 191 | 192 | public File getCacheFile(String url) { 193 | File cacheDir = config.cacheRoot; 194 | String fileName = config.fileNameGenerator.generate(url); 195 | return new File(cacheDir, fileName); 196 | } 197 | 198 | private void touchFileSafely(File cacheFile) { 199 | try { 200 | config.diskUsage.touch(cacheFile); 201 | } catch (IOException e) { 202 | LOG.error("Error touching file " + cacheFile, e); 203 | } 204 | } 205 | 206 | private void shutdownClients() { 207 | synchronized (clientsLock) { 208 | for (HttpProxyCacheServerClients clients : clientsMap.values()) { 209 | clients.shutdown(); 210 | } 211 | clientsMap.clear(); 212 | } 213 | } 214 | 215 | private void waitForRequest() { 216 | try { 217 | while (!Thread.currentThread().isInterrupted()) { 218 | Socket socket = serverSocket.accept(); 219 | LOG.debug("Accept new socket " + socket); 220 | socketProcessor.submit(new SocketProcessorRunnable(socket)); 221 | } 222 | } catch (IOException e) { 223 | onError(new ProxyCacheException("Error during waiting connection", e)); 224 | } 225 | } 226 | 227 | private void processSocket(Socket socket) { 228 | try { 229 | GetRequest request = GetRequest.read(socket.getInputStream()); 230 | LOG.debug("Request to cache proxy:" + request); 231 | String url = ProxyCacheUtils.decode(request.uri); 232 | if (pinger.isPingRequest(url)) { 233 | pinger.responseToPing(socket); 234 | } else { 235 | HttpProxyCacheServerClients clients = getClients(url); 236 | clients.processRequest(request, socket); 237 | } 238 | } catch (SocketException e) { 239 | // There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458 240 | // So just to prevent log flooding don't log stacktrace 241 | LOG.debug("Closing socket… Socket is closed by client."); 242 | } catch (ProxyCacheException | IOException e) { 243 | onError(new ProxyCacheException("Error processing request", e)); 244 | } finally { 245 | releaseSocket(socket); 246 | LOG.debug("Opened connections: " + getClientsCount()); 247 | } 248 | } 249 | 250 | private HttpProxyCacheServerClients getClients(String url) throws ProxyCacheException { 251 | synchronized (clientsLock) { 252 | HttpProxyCacheServerClients clients = clientsMap.get(url); 253 | if (clients == null) { 254 | clients = new HttpProxyCacheServerClients(url, config); 255 | clientsMap.put(url, clients); 256 | } 257 | return clients; 258 | } 259 | } 260 | 261 | private int getClientsCount() { 262 | synchronized (clientsLock) { 263 | int count = 0; 264 | for (HttpProxyCacheServerClients clients : clientsMap.values()) { 265 | count += clients.getClientsCount(); 266 | } 267 | return count; 268 | } 269 | } 270 | 271 | private void releaseSocket(Socket socket) { 272 | closeSocketInput(socket); 273 | closeSocketOutput(socket); 274 | closeSocket(socket); 275 | } 276 | 277 | private void closeSocketInput(Socket socket) { 278 | try { 279 | if (!socket.isInputShutdown()) { 280 | socket.shutdownInput(); 281 | } 282 | } catch (SocketException e) { 283 | // There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458 284 | // So just to prevent log flooding don't log stacktrace 285 | LOG.debug("Releasing input stream… Socket is closed by client."); 286 | } catch (IOException e) { 287 | onError(new ProxyCacheException("Error closing socket input stream", e)); 288 | } 289 | } 290 | 291 | private void closeSocketOutput(Socket socket) { 292 | try { 293 | if (!socket.isOutputShutdown()) { 294 | socket.shutdownOutput(); 295 | } 296 | } catch (IOException e) { 297 | LOG.warn("Failed to close socket on proxy side: {}. It seems client have already closed connection.", e.getMessage()); 298 | } 299 | } 300 | 301 | private void closeSocket(Socket socket) { 302 | try { 303 | if (!socket.isClosed()) { 304 | socket.close(); 305 | } 306 | } catch (IOException e) { 307 | onError(new ProxyCacheException("Error closing socket", e)); 308 | } 309 | } 310 | 311 | private void onError(Throwable e) { 312 | LOG.error("HttpProxyCacheServer error", e); 313 | } 314 | 315 | private final class WaitRequestsRunnable implements Runnable { 316 | 317 | private final CountDownLatch startSignal; 318 | 319 | public WaitRequestsRunnable(CountDownLatch startSignal) { 320 | this.startSignal = startSignal; 321 | } 322 | 323 | @Override 324 | public void run() { 325 | startSignal.countDown(); 326 | waitForRequest(); 327 | } 328 | } 329 | 330 | private final class SocketProcessorRunnable implements Runnable { 331 | 332 | private final Socket socket; 333 | 334 | public SocketProcessorRunnable(Socket socket) { 335 | this.socket = socket; 336 | } 337 | 338 | @Override 339 | public void run() { 340 | processSocket(socket); 341 | } 342 | } 343 | 344 | /** 345 | * Builder for {@link HttpProxyCacheServer}. 346 | */ 347 | public static final class Builder { 348 | 349 | private static final long DEFAULT_MAX_SIZE = 512 * 1024 * 1024; 350 | 351 | private File cacheRoot; 352 | private FileNameGenerator fileNameGenerator; 353 | private DiskUsage diskUsage; 354 | private SourceInfoStorage sourceInfoStorage; 355 | private HeaderInjector headerInjector; 356 | 357 | public Builder(Context context) { 358 | this.sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(context); 359 | this.cacheRoot = StorageUtils.getIndividualCacheDirectory(context); 360 | this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE); 361 | this.fileNameGenerator = new Md5FileNameGenerator(); 362 | this.headerInjector = new EmptyHeadersInjector(); 363 | } 364 | 365 | /** 366 | * Overrides default cache folder to be used for caching files. 367 | *
368 | * By default AndroidVideoCache uses 369 | * '/Android/data/[app_package_name]/cache/video-cache/' if card is mounted and app has appropriate permission 370 | * or 'video-cache' subdirectory in default application's cache directory otherwise. 371 | *
372 | * Note directory must be used only for AndroidVideoCache files. 373 | * 374 | * @param file a cache directory, can't be null. 375 | * @return a builder. 376 | */ 377 | public Builder cacheDirectory(File file) { 378 | this.cacheRoot = checkNotNull(file); 379 | return this; 380 | } 381 | 382 | /** 383 | * Overrides default cache file name generator {@link Md5FileNameGenerator} . 384 | * 385 | * @param fileNameGenerator a new file name generator. 386 | * @return a builder. 387 | */ 388 | public Builder fileNameGenerator(FileNameGenerator fileNameGenerator) { 389 | this.fileNameGenerator = checkNotNull(fileNameGenerator); 390 | return this; 391 | } 392 | 393 | /** 394 | * Sets max cache size in bytes. 395 | *396 | * All files that exceeds limit will be deleted using LRU strategy. 397 | * Default value is 512 Mb. 398 | *
399 | * Note this method overrides result of calling {@link #maxCacheFilesCount(int)} 400 | * 401 | * @param maxSize max cache size in bytes. 402 | * @return a builder. 403 | */ 404 | public Builder maxCacheSize(long maxSize) { 405 | this.diskUsage = new TotalSizeLruDiskUsage(maxSize); 406 | return this; 407 | } 408 | 409 | /** 410 | * Sets max cache files count. 411 | * All files that exceeds limit will be deleted using LRU strategy. 412 | * Note this method overrides result of calling {@link #maxCacheSize(long)} 413 | * 414 | * @param count max cache files count. 415 | * @return a builder. 416 | */ 417 | public Builder maxCacheFilesCount(int count) { 418 | this.diskUsage = new TotalCountLruDiskUsage(count); 419 | return this; 420 | } 421 | 422 | /** 423 | * Set custom DiskUsage logic for handling when to keep or clean cache. 424 | * 425 | * @param diskUsage a disk usage strategy, cant be {@code null}. 426 | * @return a builder. 427 | */ 428 | public Builder diskUsage(DiskUsage diskUsage) { 429 | this.diskUsage = checkNotNull(diskUsage); 430 | return this; 431 | } 432 | 433 | /** 434 | * Add headers along the request to the server 435 | * 436 | * @param headerInjector to inject header base on url 437 | * @return a builder 438 | */ 439 | public Builder headerInjector(HeaderInjector headerInjector) { 440 | this.headerInjector = checkNotNull(headerInjector); 441 | return this; 442 | } 443 | 444 | /** 445 | * Builds new instance of {@link HttpProxyCacheServer}. 446 | * 447 | * @return proxy cache. Only single instance should be used across whole app. 448 | */ 449 | public HttpProxyCacheServer build() { 450 | Config config = buildConfig(); 451 | return new HttpProxyCacheServer(config); 452 | } 453 | 454 | private Config buildConfig() { 455 | return new Config(cacheRoot, fileNameGenerator, diskUsage, sourceInfoStorage, headerInjector); 456 | } 457 | 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /library/src/main/java/com/danikula/videocache/HttpProxyCacheServerClients.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import android.os.Handler; 4 | import android.os.Looper; 5 | import android.os.Message; 6 | 7 | import com.danikula.videocache.file.FileCache; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.net.Socket; 12 | import java.util.List; 13 | import java.util.concurrent.CopyOnWriteArrayList; 14 | import java.util.concurrent.atomic.AtomicInteger; 15 | 16 | import static com.danikula.videocache.Preconditions.checkNotNull; 17 | 18 | /** 19 | * Client for {@link HttpProxyCacheServer} 20 | * 21 | * @author Alexey Danilov (danikula@gmail.com). 22 | */ 23 | final class HttpProxyCacheServerClients { 24 | 25 | private final AtomicInteger clientsCount = new AtomicInteger(0); 26 | private final String url; 27 | private volatile HttpProxyCache proxyCache; 28 | private final List
16 | * It is important to ignore system proxy for localhost connection.
17 | *
18 | * @author Alexey Danilov (danikula@gmail.com).
19 | */
20 | class IgnoreHostProxySelector extends ProxySelector {
21 |
22 | private static final List
47 | * NOTE: Can be null in some unpredictable cases (if SD card is unmounted and
48 | * {@link android.content.Context#getCacheDir() Context.getCacheDir()} returns null).
49 | */
50 | private static File getCacheDirectory(Context context, boolean preferExternal) {
51 | File appCacheDir = null;
52 | String externalStorageState;
53 | try {
54 | externalStorageState = Environment.getExternalStorageState();
55 | } catch (NullPointerException e) { // (sh)it happens
56 | externalStorageState = "";
57 | }
58 | if (preferExternal && MEDIA_MOUNTED.equals(externalStorageState)) {
59 | appCacheDir = getExternalCacheDir(context);
60 | }
61 | if (appCacheDir == null) {
62 | appCacheDir = context.getCacheDir();
63 | }
64 | if (appCacheDir == null) {
65 | String cacheDirPath = "/data/data/" + context.getPackageName() + "/cache/";
66 | LOG.warn("Can't define system cache directory! '" + cacheDirPath + "%s' will be used.");
67 | appCacheDir = new File(cacheDirPath);
68 | }
69 | return appCacheDir;
70 | }
71 |
72 | private static File getExternalCacheDir(Context context) {
73 | File dataDir = new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data");
74 | File appCacheDir = new File(new File(dataDir, context.getPackageName()), "cache");
75 | if (!appCacheDir.exists()) {
76 | if (!appCacheDir.mkdirs()) {
77 | LOG.warn("Unable to create external cache directory");
78 | return null;
79 | }
80 | }
81 | return appCacheDir;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/library/src/main/java/com/danikula/videocache/file/DiskUsage.java:
--------------------------------------------------------------------------------
1 | package com.danikula.videocache.file;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 |
6 | /**
7 | * Declares how {@link FileCache} will use disc space.
8 | *
9 | * @author Alexey Danilov (danikula@gmail.com).
10 | */
11 | public interface DiskUsage {
12 |
13 | void touch(File file) throws IOException;
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/library/src/main/java/com/danikula/videocache/file/FileCache.java:
--------------------------------------------------------------------------------
1 | package com.danikula.videocache.file;
2 |
3 | import com.danikula.videocache.Cache;
4 | import com.danikula.videocache.ProxyCacheException;
5 |
6 | import java.io.File;
7 | import java.io.IOException;
8 | import java.io.RandomAccessFile;
9 |
10 | /**
11 | * {@link Cache} that uses file for storing data.
12 | *
13 | * @author Alexey Danilov (danikula@gmail.com).
14 | */
15 | public class FileCache implements Cache {
16 |
17 | public static final String TEMP_POSTFIX = ".download";
18 |
19 | private final DiskUsage diskUsage;
20 | public File file;
21 | private RandomAccessFile dataFile;
22 |
23 | public FileCache(File file) throws ProxyCacheException {
24 | this(file, new UnlimitedDiskUsage());
25 | }
26 |
27 | public FileCache(File file, DiskUsage diskUsage) throws ProxyCacheException {
28 | try {
29 | if (diskUsage == null) {
30 | throw new NullPointerException();
31 | }
32 | this.diskUsage = diskUsage;
33 | File directory = file.getParentFile();
34 | Files.makeDir(directory);
35 | boolean completed = file.exists();
36 | this.file = completed ? file : new File(file.getParentFile(), file.getName() + TEMP_POSTFIX);
37 | this.dataFile = new RandomAccessFile(this.file, completed ? "r" : "rw");
38 | } catch (IOException e) {
39 | throw new ProxyCacheException("Error using file " + file + " as disc cache", e);
40 | }
41 | }
42 |
43 | @Override
44 | public synchronized long available() throws ProxyCacheException {
45 | try {
46 | return (int) dataFile.length();
47 | } catch (IOException e) {
48 | throw new ProxyCacheException("Error reading length of file " + file, e);
49 | }
50 | }
51 |
52 | @Override
53 | public synchronized int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
54 | try {
55 | dataFile.seek(offset);
56 | return dataFile.read(buffer, 0, length);
57 | } catch (IOException e) {
58 | String format = "Error reading %d bytes with offset %d from file[%d bytes] to buffer[%d bytes]";
59 | throw new ProxyCacheException(String.format(format, length, offset, available(), buffer.length), e);
60 | }
61 | }
62 |
63 | @Override
64 | public synchronized void append(byte[] data, int length) throws ProxyCacheException {
65 | try {
66 | if (isCompleted()) {
67 | throw new ProxyCacheException("Error append cache: cache file " + file + " is completed!");
68 | }
69 | dataFile.seek(available());
70 | dataFile.write(data, 0, length);
71 | } catch (IOException e) {
72 | String format = "Error writing %d bytes to %s from buffer with size %d";
73 | throw new ProxyCacheException(String.format(format, length, dataFile, data.length), e);
74 | }
75 | }
76 |
77 | @Override
78 | public synchronized void close() throws ProxyCacheException {
79 | try {
80 | dataFile.close();
81 | diskUsage.touch(file);
82 | } catch (IOException e) {
83 | throw new ProxyCacheException("Error closing file " + file, e);
84 | }
85 | }
86 |
87 | @Override
88 | public synchronized void complete() throws ProxyCacheException {
89 | if (isCompleted()) {
90 | return;
91 | }
92 |
93 | close();
94 | String fileName = file.getName().substring(0, file.getName().length() - TEMP_POSTFIX.length());
95 | File completedFile = new File(file.getParentFile(), fileName);
96 | boolean renamed = file.renameTo(completedFile);
97 | if (!renamed) {
98 | throw new ProxyCacheException("Error renaming file " + file + " to " + completedFile + " for completion!");
99 | }
100 | file = completedFile;
101 | try {
102 | dataFile = new RandomAccessFile(file, "r");
103 | diskUsage.touch(file);
104 | } catch (IOException e) {
105 | throw new ProxyCacheException("Error opening " + file + " as disc cache", e);
106 | }
107 | }
108 |
109 | @Override
110 | public synchronized boolean isCompleted() {
111 | return !isTempFile(file);
112 | }
113 |
114 | /**
115 | * Returns file to be used fo caching. It may as original file passed in constructor as some temp file for not completed cache.
116 | *
117 | * @return file for caching.
118 | */
119 | public File getFile() {
120 | return file;
121 | }
122 |
123 | private boolean isTempFile(File file) {
124 | return file.getName().endsWith(TEMP_POSTFIX);
125 | }
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/library/src/main/java/com/danikula/videocache/file/FileNameGenerator.java:
--------------------------------------------------------------------------------
1 | package com.danikula.videocache.file;
2 |
3 | /**
4 | * Generator for files to be used for caching.
5 | *
6 | * @author Alexey Danilov (danikula@gmail.com).
7 | */
8 | public interface FileNameGenerator {
9 |
10 | String generate(String url);
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/library/src/main/java/com/danikula/videocache/file/Files.java:
--------------------------------------------------------------------------------
1 | package com.danikula.videocache.file;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 |
6 | import java.io.File;
7 | import java.io.IOException;
8 | import java.io.RandomAccessFile;
9 | import java.util.Arrays;
10 | import java.util.Collections;
11 | import java.util.Comparator;
12 | import java.util.Date;
13 | import java.util.LinkedList;
14 | import java.util.List;
15 |
16 | /**
17 | * Utils for work with files.
18 | *
19 | * @author Alexey Danilov (danikula@gmail.com).
20 | */
21 | class Files {
22 |
23 | private static final Logger LOG = LoggerFactory.getLogger("Files");
24 |
25 | static void makeDir(File directory) throws IOException {
26 | if (directory.exists()) {
27 | if (!directory.isDirectory()) {
28 | throw new IOException("File " + directory + " is not directory!");
29 | }
30 | } else {
31 | boolean isCreated = directory.mkdirs();
32 | if (!isCreated) {
33 | throw new IOException(String.format("Directory %s can't be created", directory.getAbsolutePath()));
34 | }
35 | }
36 | }
37 |
38 | static List