因为使用aria下载后有些视频不能正常播放.找了很多原因也没找出来.后来可能想是视频加密解密的问题.改了加解密逻辑之后问题还是存在,aria的源码也不想分析,想着可能是断点下载的时候丢了几个字节.就想着换个下载库会不会好点,就看了okdownload这个开源库,有不少人在使用.是把项目建了个分支,下载代码改成okdownload了.结果证明不是aria下载框架的问题.是播放器的缓存没有及时清理导致的.这个问题其实也可以证明:卸载app后重新下载就可以正常播放.
aria这个下载库相对还说使用还是比较简单好用的.okdownload这个库还是需要仔细研究一番才能知道怎么用,状态怎么控制.得花个2-3天的时间去消化代码.网上很多都是官方的文档照着搬过来,自己不摸索还是一头雾水.本篇只说明怎么使用,不在介绍api.
aria地址:Introduction · Aria 使用指南
okdownload的文档这个介绍还是比较全的,有源码分析:OKDownload 下载框架 断点续传 MD - 白乾涛 - 博客园
项目代码中有用到litepal用于记录下载记录和文件加密的地方,不做介绍.owdownload本身存储的记录是不能作为下载记录的.因为他存储的是下载时的状态数据.下载完成之后就自动清除了. 项目中尽量避免使用okdownload的下载id作为查询条件. 多用户的情况可以使用下载url和一些其他的参数作为查询条件.
1.引用
// // core
implementation "com.liulishuo.okdownload:okdownload:1.0.7"
// provide sqlite to store breakpoints
implementation "com.liulishuo.okdownload:sqlite:1.0.7"
// provide okhttp to connect to backend
implementation "com.liulishuo.okdownload:okhttp:1.0.7"
如果下载不下来需要指定仓库:
repositories {
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
}
2.初始化参数设置,按需要设置
void initOkDownload() {
OkDownload.Builder builder = new OkDownload.Builder(this)
.downloadStore(Util.createDefaultDatabase(this))// //断点信息存储的位置,默认是SQLite数据库
.callbackDispatcher(new CallbackDispatcher() {
})监听回调分发器,默认在主线程回调
.downloadDispatcher(new DownloadDispatcher() {
})下载管理机制,最大下载任务数、同步异步执行下载任务的处理
.connectionFactory(Util.createDefaultConnectionFactory())//选择网络请求框架,默认是OkHttp
.outputStreamFactory(new DownloadUriOutputStream.Factory())// //构建文件输出流DownloadOutputStream,是否支持随机位置写入
.downloadStrategy(new DownloadStrategy())//下载策略,文件分为几个线程下载
.processFileStrategy(new ProcessFileStrategy())//多文件写文件的方式,默认是根据每个线程写文件的不同位置,支持同时写入
.monitor(new DownloadMonitor() {
//开始任务
@Override
public void taskStart(DownloadTask task) {
LogUtil.i("taskStart");
}
//断点下载任务
@Override
public void taskDownloadFromBreakpoint(@NonNull DownloadTask task, @NonNull BreakpointInfo info) {
LogUtil.i("断点任务下载taskDownloadFromBreakpoint:" + task.getFilename());
}
//新任务
@Override
public void taskDownloadFromBeginning(@NonNull DownloadTask task, @NonNull BreakpointInfo info,
@Nullable ResumeFailedCause cause) {
LogUtil.i("新建任务下载taskDownloadFromBeginning:" + task.getFilename());
}
//任务结束
@Override
public void taskEnd(DownloadTask task, EndCause cause, @Nullable Exception realCause) {
LogUtil.i("下载完成taskEnd");
// 此处可以实现自己的逻辑,比如修改下载记录的任务状态,对下载文件进行加密
}
});下载状态监听
OkDownload.setSingletonInstance(builder.build());
// DownloadDispatcher.setMaxParallelRunningCount(3);
//设置监视器
// OkDownload.with().setMonitor(new DownloadMonitor() {});
//最大并行下载数
// DownloadDispatcher.setMaxParallelRunningCount(3);
//
// RemitStoreOnSQLite.setRemitToDBDelayMillis(3000);
//取消全部任务
// OkDownload.with().downloadDispatcher().cancelAll();
//移除某个任务
// OkDownload.with().breakpointStore().remove(taskId);
}
上面的一般需求都能满足.
3.创建下载任务
/**
* 创建下载任务实例
*
* @param url
* @param parentPath
* @param fileName
* @return
*/
public static DownloadTask createTasK(String url, String parentPath, String fileName) {
return new DownloadTask.Builder(url, parentPath, fileName)
.setFilenameFromResponse(false)//是否使用 response header or url path 作为文件名,此时会忽略指定的文件名,默认false
.setPassIfAlreadyCompleted(true)//如果文件已经下载完成,再次下载时,是否忽略下载,默认为true(忽略),设为false会从头下载
.setConnectionCount(1) //需要用几个线程来下载文件,默认根据文件大小确定;如果文件已经 split block,则设置后无效
.setPreAllocateLength(false) //在获取资源长度后,设置是否需要为文件预分配长度,默认false
.setMinIntervalMillisCallbackProcess(1500) //通知调用者的频率,避免anr,默认3000
.setWifiRequired(false)//是否只允许wifi下载,默认为false
.setAutoCallbackToUIThread(true) //是否在主线程通知调用者,默认为true
//.setHeaderMapFields(new HashMap<String, List<String>>())//设置请求头
//.addHeader(String key, String value)//追加请求头
.setPriority(0)//设置优先级,默认值是0,值越大下载优先级越高
.setReadBufferSize(4096)//设置读取缓存区大小,默认4096
.setFlushBufferSize(16384)//设置写入缓存区大小,默认16384
.setSyncBufferSize(65536)//写入到文件的缓冲区大小,默认65536
.setSyncBufferIntervalMillis(2000) //写入文件的最小时间间隔,默认2000
.build();
}
4.监听下载
在初始化的时候有个设置监视的地方:DownloadMonitor 这方法里面有任务从创建到执行完成的一些状态回调.如果要监听下载中的进度就要针对每个任务添加任务监听,
1.okdownload提供了6种:
DownloadListener、DownloadListener1、DownloadListener2、DownloadListener3、DownloadListener4、DownloadListener4WithSpeed.每个类都有不同的实现.任务完成后自动移除监听.可以去官方文档看看每个监听实现了哪些方法.根据自己的业务需求选择使用.
例举两个:
DownloadListener2只监视任务的创建和任务的结束,这个是用在下载目录中选择下载项后标记是否下载,下载是否否完成的.因为这个地方不用显示下载进度.只要知道哪项记录有没有添加下载;
DownloadListener4WithSpeed提供了更多的回调方法包括下载进度.下载进度需要自己保存,暂时没有找到可以获取的api,可以用hashmap用url作为key临时保存下载进度.
2.另外一个比较重要的UnifiedListenerManager
如果一个任务同时需要多个监听.比如有几个页面都需要监听同一个任务的状态就要用到这个辅助类.这个类的必须使用单例模式创建,保证全局只有一个实例.
public class MyUnifiedListenerManager {
//内存屏障 关键字volatile
volatile static UnifiedListenerManager sListenerManager;
public static UnifiedListenerManager getListenerManager() {
if (sListenerManager == null) {
//线程锁
synchronized (MyUnifiedListenerManager.class) {
sListenerManager = new UnifiedListenerManager();
}
}
return sListenerManager;
}
}
添加监听
1.下载任务和监听绑定关系(不管任务是不是在执行中)
MyUnifiedListenerManager.getListenerManager().attachAndEnqueueIfNotRun(preTasK,mDownloadListener4WithSpeed);
2.如果任务没有运行的话就开始执行下载任务并关联监听.
MyUnifiedListenerManager.getListenerManager().attachAndEnqueueIfNotRun(downloadTask,mDownloadListener4WithSpeed);
3.立即执行任务并关联监听.
MyUnifiedListenerManager.getListenerManager().enqueueTaskWithUnifiedListener(preTasK,mDownloadListener4WithSpeed);
移除监听
MyUnifiedListenerManager.getListenerManager().detachListener(sameTask,mDownloadListener4WithSpeed);
5.获取下载状态
DownloadTask preTasK = DownloadUtil.createPreTasK(item);
StatusUtil.Status status = StatusUtil.getStatus(preTasK);
StatusUtil.getStatus()有两种使用方法:可以传入任务对象,也可以传入任务对象的id.共有5个状态
PENDING:任务已经添加到下载队列了 RUNNING:任务正在运行 COMPLETED:任务下载完成.如果没有断点记录,本地有下载文件不管是不是完成都会认为是下载完成. IDLE:任务空闲状态,就是暂停的任务 // may completed, but no filename can't ensure. UNKNOWN:未添加的任务,如果项目中已下载的任务在杀死app后在打开app之后的任务状态还是unknow,可以检查第一次获取任务状态时候创建任务的方法.需要使用上面完整的方式创建任务实例.这样okdownload会从自己的记录中匹配相同参数的任务,如果有了就会返回已经存在任务的信息.没有则会创建新的任务.不要使用new DownloadTask.Builder(url, parentPath, fileName).build()这种简单的方式创建,这种就会返回unknown.完成写法能找到任务记录,返回任务id.
6.任务操作
1.暂停任务
DownloadTask preTasK = DownloadUtil.createPreTasK(item);
preTasK.cancel();
或者使用
OkDownload.with().downloadDispatcher().cancel(preTasK.getId());
2.开始任务或者继续任务
MyUnifiedListenerManager.getListenerManager().attachAndEnqueueIfNotRun(preTasK,mDownloadListener4WithSpeed);
3.取消任务
DownloadTask downloadTask = DownloadUtil.createPreTasK(downLoadVideo);
//删除任务并移除文件
MyUnifiedListenerManager.getListenerManager().detachListener(downloadTask.getId());
OkDownload.with().downloadDispatcher().cancel(downloadTask.getId());//暂停下载
OkDownload.with().breakpointStore().remove(downloadTask.getId());
File file = new File(DownloadUtil.getDownloadFilePath(downLoadVideo.getCourseId(),
DownloadUtil.getDownloadFileName(downLoadVideo.getVideoId(), downLoadVideo.getVideoUrl())));
if (file != null && file.exists()) {
file.delete();
}
LitePal.delete(DownLoadVideo.class, downLoadVideo.getId());
7.DownlaodUtils工具类
public class DownloadUtil {
/**
* 获取下载根目录
*
* @param context
* @param type
* @return
*/
public static File getRootDir(Context context, String type) {
String state = Environment.getExternalStorageState();
File root;
if (state.equals(Environment.MEDIA_MOUNTED)) {
root = context.getApplicationContext().getExternalFilesDir(type);
} else {
String path = context.getApplicationContext().getFilesDir().getPath();
root = new File(path + File.separator + type);
if (!root.exists() || !root.isFile()) {
root.mkdirs();
}
}
return root;
}
/**
* 获取课程下载目录
*
* @param courseId
* @return
*/
public static String getDownloadCourseDir(String courseId) {
String dir = getRootDir(App.getContext().getApplicationContext(),
Environment.DIRECTORY_DOWNLOADS) + File.separator + "." + courseId;
File file = new File(dir);
if (!file.exists() || !file.isDirectory()) {
file.mkdirs();
}
// + File.separator + videoInfo.getVideoName() + videoInfo.getVideoUrl().substring(videoInfo.getVideoUrl()
// .lastIndexOf('.'))
return dir;
}
public static String getDownloadUrl(String videoUrl) {
return !TextUtils.isEmpty(videoUrl) ? videoUrl.replace("/index.m3u8", ".mp4") : videoUrl;
}
/**
* 下载文件名
*
* @param fileName
* @param videoUrl
* @return
*/
public static String getDownloadFileName(String fileName, String videoUrl) {
return videoUrl.lastIndexOf(".") > 0 ? fileName + videoUrl.substring(videoUrl.lastIndexOf(".")) :
fileName + "." + videoUrl;
}
/**
* 获取下载文件路径,
*
* @param courseId 直播或者录播id
* @param fileName 文件存储的名字,前期用的视频名字,后面改用视频id
* @return
*/
public static String getDownloadFilePath(String courseId, String fileName) {
String dir =
getRootDir(App.getContext().getApplicationContext(),
Environment.DIRECTORY_DOWNLOADS) + File.separator + "." + courseId;
File file = new File(dir);
if (!file.exists() || !file.isDirectory()) {
file.mkdirs();
}
return dir + File.separator + fileName;
}
/**
* 使用下载载体创建下载实例
*
* @param item
* @return
*/
public static DownloadTask createPreTasK(DownLoadVideo item) {
return createPreTasK(item.getVideoUrl(), getDownloadCourseDir(item.getCourseId()),
getDownloadFileName(item.getVideoId(), item.getVideoUrl()));
}
/**
* 简单创建下载
*
* @param url
* @param parentPath
* @param fileName
* @return
*/
public static DownloadTask createPreTasK(String url, String parentPath, String fileName) {
return new DownloadTask.Builder(url, parentPath, fileName).build();
}
/**
* 使用下载载体创建下载实例
*
* @param item 存储下载信息的自定义对象.
* @return
*/
public static DownloadTask createTasK(DownLoadVideo item) {
return createTasK(item.getVideoUrl(), getDownloadCourseDir(item.getCourseId()),
getDownloadFileName(item.getVideoId(), item.getVideoUrl()));
}
/**
* 创建下载任务实例
*
* @param url
* @param parentPath
* @param fileName
* @return
*/
public static DownloadTask createTasK(String url, String parentPath, String fileName) {
return new DownloadTask.Builder(url, parentPath, fileName)
.setFilenameFromResponse(false)//是否使用 response header or url path 作为文件名,此时会忽略指定的文件名,默认false
.setPassIfAlreadyCompleted(true)//如果文件已经下载完成,再次下载时,是否忽略下载,默认为true(忽略),设为false会从头下载
.setConnectionCount(1) //需要用几个线程来下载文件,默认根据文件大小确定;如果文件已经 split block,则设置后无效
.setPreAllocateLength(false) //在获取资源长度后,设置是否需要为文件预分配长度,默认false
.setMinIntervalMillisCallbackProcess(1500) //通知调用者的频率,避免anr,默认3000
.setWifiRequired(false)//是否只允许wifi下载,默认为false
.setAutoCallbackToUIThread(true) //是否在主线程通知调用者,默认为true
//.setHeaderMapFields(new HashMap<String, List<String>>())//设置请求头
//.addHeader(String key, String value)//追加请求头
.setPriority(0)//设置优先级,默认值是0,值越大下载优先级越高
.setReadBufferSize(4096)//设置读取缓存区大小,默认4096
.setFlushBufferSize(16384)//设置写入缓存区大小,默认16384
.setSyncBufferSize(65536)//写入到文件的缓冲区大小,默认65536
.setSyncBufferIntervalMillis(2000) //写入文件的最小时间间隔,默认2000
.build();
}
}
后面还有任务队列.类似下载组任务的功能.项目中没用到就不说明了.使用okdownload主要是在两个地方耽误了不少时间,一个是UnifiedListenerManager,一开始没有想到这个是干嘛用.不能实现项目中的业务,后来看了一点代码才明白是怎么使用的,还有一个就是下载任务的状态,已下载的任务杀死app之后总是不能使用断点续传的功能.跟踪代码发现是在创建任务实例的时候没有找到okdownload记录的断点记录,每次都当成新的任务,使用完整创建实例的方法就可以正常断点下载了.