想起来之前做视频缓存的工具类,没事记一下,中间用了一个开源的缓存的进度库,其他的都是自己写的
其中网络请求时用的OkHttp3
缓存进度库用的是:
compile 'io.github.lizhangqu:coreprogress:1.0.2'
封装的工具类:DownloadUtil.java
import android.text.TextUtils;
import android.util.Log;
import com.hkzr.docopad.minterface.OnDownloadListener;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import io.github.lizhangqu.coreprogress.ProgressHelper;
import io.github.lizhangqu.coreprogress.ProgressUIListener;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* 创 建: lt
* 作 用: 断点下载的类
* 使用方法:
* 注意事项:
*/
public class DownloadUtil {
private Call call;
private static DownloadUtil downloadUtil;
private OkHttpClient okHttpClient;
private List<Object[]> list;//call,url,startBytes,path,OnDownloadListener
private OnDownloadListener onDownloadListener;
private final static int ADD = 0;
private final static int CANCEL = 1;
private final static int SET = 2;
private DownloadUtil() {
okHttpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)//设置超时时间之类的
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS).build();
list = new ArrayList<>();
}
/**
* 单例初始化
*/
public static DownloadUtil init() {
if (downloadUtil == null) {
synchronized (DownloadUtil.class) {
if (downloadUtil == null) {
downloadUtil = new DownloadUtil();
}
}
}
return downloadUtil;
}
/**
* 添加下载请求
*
* @param startBytes 从第几个字节开始
* @param path 保存的路径
* @param url 下载链接
* @param onDownloadListener 回调
*/
public void addDownload(long startBytes, String path, String url, OnDownloadListener onDownloadListener) {
//添加Range头
//传入链接
Call call = okHttpClient.newCall(new Request.Builder().addHeader("RANGE", "bytes=" + startBytes + "-").url(url).get().build());
list.add(new Object[]{call, url, startBytes, path, onDownloadListener});
loop(ADD);
}
/**
* 下载请求
*
* @param startBytes 从第几个字节开始
* @param path 保存的路径
*/
private void download(final long startBytes, final String path) {
Log.i("lllttt", "DownloadUtil : " + startBytes);
//回调
try {
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
//失败了
if (onDownloadListener != null) {
onDownloadListener.onFailure(e);
}
}
@Override
public void onResponse(Call call, Response response) {
ResponseBody body = response.body();
ResponseBody responseBody = ProgressHelper.withProgress(body, new ProgressUIListener() {
@Override
public void onUIProgressChanged(long numBytes, long totalBytes, float percent, float speed) {
if (onDownloadListener != null) {
onDownloadListener.onProgress(numBytes, totalBytes, percent, speed);
}
}
@Override
public void onUIProgressStart(long totalBytes) {
if (onDownloadListener != null) {
onDownloadListener.onStart(totalBytes);
}
}
@Override
public void onUIProgressFinish() {
if (onDownloadListener != null) {
onDownloadListener.onFinish();
}
try {
list.remove(0);
} catch (Exception e) {
e.printStackTrace();
}
DownloadUtil.this.call = null;
onDownloadListener = null;
loop(ADD);
}
});
save(responseBody, startBytes, path);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
private void save(ResponseBody body, long startsPoint, String path) {
InputStream in = body.byteStream();
FileChannel channelOut = null;
// 随机访问文件,可以指定断点续传的起始位置
RandomAccessFile randomAccessFile = null;
try {
randomAccessFile = new RandomAccessFile(path, "rwd");
//Chanel NIO中的用法,由于RandomAccessFile没有使用缓存策略,直接使用会使得下载速度变慢,亲测缓存下载3.3秒的文件,用普通的RandomAccessFile需要20多秒。
channelOut = randomAccessFile.getChannel();
randomAccessFile.seek(startsPoint);
// 内存映射,直接使用RandomAccessFile,是用其seek方法指定下载的起始位置,使用缓存下载,在这里指定下载位置。
MappedByteBuffer mappedBuffer = channelOut.map(FileChannel.MapMode.READ_WRITE, startsPoint, body.contentLength());
byte[] buffer = new byte[1024];
int len = 0;
while ((len = in.read(buffer)) != -1) {
mappedBuffer.put(buffer, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
in.close();
if (channelOut != null) {
channelOut.close();
}
if (randomAccessFile != null) {
randomAccessFile.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 停止下载(暂停)
*
* @param url 下载路径
*/
public void cancelDownload(String url) {
loop(CANCEL, url);
}
/**
* 停止所有
*/
public void cancelAllDownload() {
if (list != null)
list.clear();
if (call != null)
call.cancel();
call = null;
if (onDownloadListener != null)
onDownloadListener.onFailure(null);
this.onDownloadListener = null;
}
/**
* 开始某个下载任务
*
* @param url 链接
*/
public void startDownLoad(long startBytes, String path, String url, OnDownloadListener onDownloadListener) {
if (startBytes < 0) //todo
startBytes = 0;
//查询有没有这个下载链接,有就让他挪到第一位,没有就添加到第一位
if (list != null && list.size() == 1 && !isHave(url)) {
//如果只有几个暂停的,并开启了一个暂停的,并把第二个暂停的打开
//应该把那个下载中的放为等待中,这个为开始下载
Object[] objects = list.get(0);
this.call.cancel();
this.call = null;
this.onDownloadListener = null;
Call call = okHttpClient.newCall(new Request.Builder().addHeader("RANGE", "bytes=" + startBytes + "-").url(url).get().build());
list.add(0, new Object[]{call, url, startBytes, path, onDownloadListener});
loop(ADD);
}
if (isHave(url)) {
//挪到第一位
Object[] objs = null;
for (Object[] objects : list) {
if (objects[1].equals(url)) {
objs = objects;
break;
}
}
if (list == null || list.size() <= 1) {
loop(ADD);
} else {
list.remove(objs);
list.add(1, objs);
loop(CANCEL, String.valueOf(list.get(0)[1]));
}
} else {
//添加到第一位
Call c = okHttpClient.newCall(new Request.Builder().addHeader("RANGE", "bytes=" + startBytes + "-").url(url).get().build());
list.add(0, new Object[]{c, url, startBytes, path, onDownloadListener});
if (call != null)
this.call.cancel();
this.call = null;
loop(ADD);
}
}
/**
* 判断这个任务是否在运行
*
* @param url 链接
*/
public boolean isRun(String url) {
if (list == null || list.size() == 0) {
return false;
}
return list.get(0)[1].equals(url);
}
/**
* 判断这个任务是否存在
*
* @param url 链接
*/
public boolean isHave(String url) {
if (list == null || list.size() == 0) {
return false;
}
for (Object[] obj : list) {
if (TextUtils.equals((String) obj[1], url))
return true;
}
return false;
}
/**
* 判断是否还有任务
*/
public boolean isHave() {
if (list == null)
return false;
return !(list.size() == 0);
}
/**
* 给没有回调的下载请求设置回调
*
* @param url 视频下载链接
* @param onDownloadListener 下载回调
*/
public void setListener(String url, OnDownloadListener onDownloadListener) {
loop(SET, url, onDownloadListener);
}
private void loop(int i) {
loop(i, null);
}
private void loop(int i, String url) {
loop(i, url, null);
}
/**
* 轮询
*/
private void loop(int i, String url, OnDownloadListener onDownloadListener) {
if (list == null || list.size() == 0)
return;
switch (i) {
case ADD:
if (call != null)
return;
Object[] objects = list.get(0);
this.call = (Call) objects[0];
this.onDownloadListener = (OnDownloadListener) objects[4];
download((long) objects[2], (String) objects[3]);
break;
case CANCEL:
for (int j = 0; j < list.size(); j++) {
Object[] objs = list.get(j);
if (objs[1].equals(url)) {
//如果url一样
((Call) objs[0]).cancel();
list.remove(objs);
if (j == 0) {
call = null;
this.onDownloadListener = null;
loop(ADD);
}
}
}
break;
case SET:
for (int j = 0; j < list.size(); j++) {
if (list.get(j)[1].equals(url)) {
//如果url一样
list.get(j)[4] = onDownloadListener;
if (j == 0)
this.onDownloadListener = onDownloadListener;
}
}
break;
}
}
}
回调:OnDownloadListener.java
/**
* 创 建: lt
* 作 用: 下载的回调
* 使用方法:
* 注意事项:
*/
public interface OnDownloadListener {
/**
* 下载失败
*/
void onFailure(Exception e);
/**
* 下载开始
*
* @param totalBytes 一共多少字节
*/
void onStart(long totalBytes);
/**
* 下载完成
*/
void onFinish();
/**
* 下载中(不是第一次的话,会使总字节数发生变化,所以要在第一次下载的时候保存总字节数,然后自己计算百分比)
*
* @param numBytes 已经下载了多少字节
* @param totalBytes 总字节
* @param percent 万分比(乘以100为百分比)
* @param speed 下载速度(* 1000 / 1024 / 1024 =M/s?)
*/
void onProgress(long numBytes, long totalBytes, float percent, float speed);
}
下载用的服务:VideoCacheService.java
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import com.hkzr.docopad.app.App;
import com.hkzr.docopad.greendao.VideoCacheInfoDao;
import com.hkzr.docopad.minterface.OnDownloadListener;
import com.hkzr.docopad.model.VideoCacheInfo;
import com.hkzr.docopad.utils.DownloadUtil;
import com.hkzr.docopad.utils.NetUtil;
import com.hkzr.docopad.utils.SDCardUtil;
import com.hkzr.docopad.utils.ToastUtil;
import java.util.ArrayList;
import java.util.List;
public class VideoCacheService extends Service {
private DownloadUtil init;
private boolean isCheck = false;//是否在检查网络和存储空间中
public VideoCacheService() {
}
@Override
public IBinder onBind(Intent intent) {
return new BindVideoCache();
}
@Override
public void onCreate() {
super.onCreate();
init = DownloadUtil.init();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
}
public void oneLoad(String videoName, long videoSize, String videoUrl,
int lookProgress, String videoId, long videoDuration
, String videoImg, String savePath) {
//如果sd卡可用,并且大于视频大小就可以走
//如果内部存储大于视频大小就可以走
if (!(SDCardUtil.isSDCardEnabled() && SDCardUtil.getSDCardAllSize() > videoSize || SDCardUtil.getAvailableInternalMemorySize() > videoSize)) {
ToastUtil.showToast("磁盘已满,请清理足够的存储空间");
return;
}
VideoCacheInfo videoCacheInfo = new VideoCacheInfo(null,//id
videoName,//视频名称
videoSize,//视频大小
videoUrl,//视频路径
0,//下载的百分比进度
lookProgress,//播放的百分比进度
videoId,//视频的id
videoDuration,//视频持续时长,毫秒单位?
videoImg,//视频预览图片
savePath,//保存路径
0,//下载字节数
VideoCacheInfo.STAUS_NO_START);//状态,未开始
App.ormDao.getVideoCacheInfoDao().insert(videoCacheInfo);
load(0, savePath, videoUrl, null);
}
public void load(final long startBytes, String path, final String url, final OnDownloadListener onDownloadListener) {
//如果磁盘满了或者无网络,则无需添加到队列中
if (!NetUtil.isNetworkAvailable(getApplicationContext())) {
ToastUtil.showToast("当前无网络,请检查网络链接后重试");
if (onDownloadListener != null)
onDownloadListener.onFailure(null);
return;
}
init.addDownload(startBytes, path, url, onDownloadListener);
if (!isCheck) {
isCheck = true;
handler.sendEmptyMessage(0);
}
}
public void cancel(String url) {
init.cancelDownload(url);
for (VideoCacheInfo info : App.ormDao.getVideoCacheInfoDao().queryBuilder().where(VideoCacheInfoDao.Properties.VideoUrl.eq(url)).list()) {
info.setStatus(VideoCacheInfo.STATUS_PAUSED);
App.ormDao.getVideoCacheInfoDao().update(info);
}
}
public void cancelAll() {
init.cancelAllDownload();
for (VideoCacheInfo info : App.ormDao.loadAll(VideoCacheInfo.class)) {
if (info.getStatus() != VideoCacheInfo.STATUS_FINISH && info.getStatus() != VideoCacheInfo.STATUS_PAUSED) {
info.setStatus(VideoCacheInfo.STAUS_NO_START);
App.ormDao.getVideoCacheInfoDao().update(info);
}
}
}
public boolean isHave(String url) {
return init.isHave(url);
}
public void setListener(String url, OnDownloadListener onDownloadListener) {
if (!NetUtil.isNetworkAvailable(getApplicationContext())) {
ToastUtil.showToast("当前无网络,请检查网络链接后重试");
if (onDownloadListener != null)
onDownloadListener.onFailure(null);
return;
}
init.setListener(url, onDownloadListener);
}
public void startDownLoad(long startBytes, String path, String url, OnDownloadListener onDownloadListener) {
if (!NetUtil.isNetworkAvailable(getApplicationContext())) {
ToastUtil.showToast("当前无网络,请检查网络链接后重试");
if (onDownloadListener != null)
onDownloadListener.onFailure(null);
return;
}
for (VideoCacheInfo info : App.ormDao.getVideoCacheInfoDao().queryBuilder().where(VideoCacheInfoDao.Properties.VideoUrl.eq(url)).list()) {
info.setStatus(VideoCacheInfo.STATUS_STARTED);
App.ormDao.getVideoCacheInfoDao().update(info);
}
init.startDownLoad(startBytes, path, url, onDownloadListener);
if (!isCheck) {
isCheck = true;
handler.sendEmptyMessage(0);
}
}
public void startAll() {
List<VideoCacheInfo> videoCacheInfos = App.ormDao.getVideoCacheInfoDao().loadAll();
if (videoCacheInfos == null || videoCacheInfos.size() == 0)
return;
List<VideoCacheInfo> list = new ArrayList<>();
for (VideoCacheInfo info : videoCacheInfos) {
if ((info.getStatus() == VideoCacheInfo.STAUS_NO_START || info.getStatus() == VideoCacheInfo.STATUS_STARTED) && !isHave(info.getVideoUrl())) {
//表示需要下载,并且没在队列中
list.add(info);
}
}
if (list.size() == 0)
return;
for (VideoCacheInfo info : list) {
load(info.getDownloadSpeed(), info.getLocalPath(), info.getVideoUrl(), null);
}
}
Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
if (!init.isHave()) {
//如果没有任务了就不检查了
isCheck = false;
return;
}
//还有任务就检查网络和内部存储
if (!NetUtil.isNetworkAvailable(getApplicationContext())) {
//没有网络就提醒,并暂停所有任务
init.cancelAllDownload();
ToastUtil.showToast("当前无网络,已暂停缓存任务");
isCheck=false;
return;
}
if ((SDCardUtil.isSDCardEnabled() && SDCardUtil.getSDCardAllSize() < 60 * 1024)
|| SDCardUtil.getAvailableInternalMemorySize() < 60 * 1024 * 1024) {
init.cancelAllDownload();
ToastUtil.showToast("存储空间不足,已暂停缓存任务");
isCheck=false;
return;
}
handler.sendEmptyMessageDelayed(0, 6000);
}
};
public boolean isRun(String url) {
return init.isRun(url);
}
public class BindVideoCache extends Binder {
/**
* 第一次添加下载请求
*
* @param videoName 视频名称
* @param videoSize 视频大小
* @param videoUrl 视频路径
* @param lookProgress 播放的百分比进度
* @param videoId 视频的id
* @param videoDuration 视频持续时长,毫秒单位?
* @param videoImg 视频预览图片
* @param savePath 保存路径
*/
public void mOneLoad(String videoName, long videoSize, String videoUrl,
int lookProgress, String videoId, long videoDuration
, String videoImg, String savePath) {//ps:其实这个方法根据设计模式原则应该是直接传一个对象的,懒得改了
oneLoad(videoName, videoSize, videoUrl,
lookProgress, videoId, videoDuration
, videoImg, savePath);
}
/**
* 开始下载(非第一次下载)
*
* @param startBytes 从第几个字节开始
* @param path 保存的路径
* @param url 下载链接
* @param onDownloadListener 回调
*/
public void mLoad(long startBytes, String path, String url, OnDownloadListener onDownloadListener) {
load(startBytes, path, url, onDownloadListener);
}
/**
* 停止下载(暂停)
*
* @param url 下载路径
*/
public void mCancel(String url) {
cancel(url);
}
/**
* 停止所有
*/
public void mCancelAll() {
cancelAll();
}
/**
* 判断缓存队列中是否存在该任务
*
* @param url 任务链接
* @return 是否存在
*/
public boolean mIsHave(String url) {
return isHave(url);
}
/**
* 设置回调
*
* @param url 视频链接
* @param onDownloadListener 下载回调
*/
public void mSetListener(String url, OnDownloadListener onDownloadListener) {
setListener(url, onDownloadListener);
}
/**
* 开始下载(暂停或停止后)
*
* @param startBytes 从第几个字节开始
* @param path 保存的路径
* @param url 下载链接
* @param onDownloadListener 回调
*/
public void mStartDownLoad(long startBytes, String path, String url, OnDownloadListener onDownloadListener) {
startDownLoad(startBytes, path, url, onDownloadListener);
}
/**
* 判断是否正在下载
*
* @param url 视频链接
*/
public boolean mIsRun(String url) {
return isRun(url);
}
/**
* 是否在缓存中或缓存过
*
* @param vid 视频id
*/
public boolean isVideoCacheOrCacheing(String vid) {
List<VideoCacheInfo> list = App.ormDao.getVideoCacheInfoDao().queryBuilder().where(VideoCacheInfoDao.Properties.VideoId.eq(vid)).list();
if (list != null && list.size() > 0) {
//表示缓存过了或在缓存队列中
return true;
}
return false;
}
/**
* 加载并开始所有等待中和缓存中的任务,无监听
* 如果已被加载,则不理会
*/
public void mStartAll() {
startAll();
}
}
}
附加一个缓存时使用的Bean类,使用了GreenDao来保存下载的进度和本地磁盘位置等信息:VideoCacheInfo.java
import org.greenrobot.greendao.annotation.Entity;
import org.greenrobot.greendao.annotation.Generated;
import org.greenrobot.greendao.annotation.Id;
import org.greenrobot.greendao.annotation.Keep;
import org.greenrobot.greendao.annotation.Transient;
import java.io.Serializable;
/**
* 作 用: 缓存视频的bean类
*/
@Entity
public class VideoCacheInfo implements Serializable {
/**
* 0: 未开始, 正等待
* 1: 已经开始
* 2: 已停止,未完成
* 3: 已暂停
* 4: 已完成
*/
@Transient
public static final int STAUS_NO_START = 0;
@Transient
public static final int STATUS_STARTED = 1;
@Transient
public static final int STATUS_STOPED = 2;
@Transient
public static final int STATUS_PAUSED = 3;
@Transient
public static final int STATUS_FINISH = 4;
@Id(autoincrement = true)
private Long id;
@Transient
private HistoryBean history;
private String videoName;//视频名称
private long videoSize;//视频大小
private String videoUrl;//视频链接
private int downProgress;//下载进度
private int lookProgress = -1;//观看进度
private String videoId;//服务器id
private long videoDuration;//视频持续时间
private String videoIcon;//图片静态图片
private String localPath;//本地存放路径
private long downloadSpeed = 0;//下载字节数
private int status = STAUS_NO_START;//状态
@Transient
private boolean isCheck;
@Generated(hash = 1303373854)
public VideoCacheInfo(Long id, String videoName, long videoSize,
String videoUrl, int downProgress, int lookProgress, String videoId,
long videoDuration, String videoIcon, String localPath,
long downloadSpeed, int status) {
this.id = id;
this.videoName = videoName;
this.videoSize = videoSize;
this.videoUrl = videoUrl;
this.downProgress = downProgress;
this.lookProgress = lookProgress;
this.videoId = videoId;
this.videoDuration = videoDuration;
this.videoIcon = videoIcon;
this.localPath = localPath;
this.downloadSpeed = downloadSpeed;
this.status = status;
}
@Generated(hash = 843470261)
public VideoCacheInfo() {
}
public int getLookProgress() {
return lookProgress;
}
public void setLookProgress(int lookProgress) {
this.lookProgress = lookProgress;
}
public boolean isCheck() {
return isCheck;
}
public void setCheck(boolean check) {
isCheck = check;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getVideoName() {
return videoName;
}
public void setVideoName(String videoName) {
this.videoName = videoName;
}
public long getVideoSize() {
return videoSize;
}
public void setVideoSize(long videoSize) {
this.videoSize = videoSize;
}
public String getVideoUrl() {
return videoUrl;
}
public void setVideoUrl(String videoUrl) {
this.videoUrl = videoUrl;
}
public int getDownProgress() {
return downProgress;
}
public void setDownProgress(int downProgress) {
this.downProgress = downProgress;
}
public String getVideoId() {
return videoId;
}
public void setVideoId(String videoId) {
this.videoId = videoId;
}
public long getVideoDuration() {
return videoDuration;
}
public void setVideoDuration(long videoDuration) {
this.videoDuration = videoDuration;
}
public String getVideoIcon() {
return videoIcon;
}
public void setVideoIcon(String videoIcon) {
this.videoIcon = videoIcon;
}
public long getDownloadSpeed() {
return downloadSpeed;
}
public void setDownloadSpeed(long downloadSpeed) {
this.downloadSpeed = downloadSpeed;
}
@Keep
public HistoryBean getHistory() {
return history;
}
@Keep
public void setHistory(HistoryBean history) {
this.history = history;
}
public String getLocalPath() {
return localPath;
}
public void setLocalPath(String localPath) {
this.localPath = localPath;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
@Override
public String toString() {
return "VideoCacheInfo{" +
"id=" + id +
", history=" + history +
", videoName='" + videoName + '\'' +
", videoSize='" + videoSize + '\'' +
", videoUrl='" + videoUrl + '\'' +
", downProgress=" + downProgress +
", videoId='" + videoId + '\'' +
", videoDuration=" + videoDuration +
", videoIcon='" + videoIcon + '\'' +
", localPath='" + localPath + '\'' +
", downloadSpeed=" + downloadSpeed +
", status=" + status +
'}';
}
}
有帮助或者有问题可以留言
对Kotlin或KMP感兴趣的同学可以进Q群 101786950
如果这篇文章对您有帮助的话
可以扫码请我喝瓶饮料或咖啡(如果对什么比较感兴趣可以在备注里写出来)