组件化功能基本完成
This commit is contained in:
2
M3U8Component/src/main/AndroidManifest.xml
Normal file
2
M3U8Component/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.arialyy.aria.m3u8" />
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.arialyy.aria.m3u8;
|
||||
|
||||
import com.arialyy.aria.core.download.DTaskWrapper;
|
||||
import com.arialyy.aria.core.download.DownloadEntity;
|
||||
import com.arialyy.aria.core.download.M3U8Entity;
|
||||
import com.arialyy.aria.core.inf.IRecordHandler;
|
||||
import com.arialyy.aria.core.listener.IEventListener;
|
||||
import com.arialyy.aria.core.loader.AbsLoader;
|
||||
import com.arialyy.aria.core.wrapper.AbsTaskWrapper;
|
||||
import com.arialyy.aria.util.ALog;
|
||||
import com.arialyy.aria.util.FileUtil;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
public abstract class BaseM3U8Loader extends AbsLoader {
|
||||
protected M3U8TaskOption mM3U8Option;
|
||||
|
||||
public BaseM3U8Loader(IEventListener listener, DTaskWrapper wrapper) {
|
||||
super(listener, wrapper);
|
||||
mM3U8Option = (M3U8TaskOption) wrapper.getM3u8Option();
|
||||
mTempFile = new File(wrapper.getEntity().getFilePath());
|
||||
}
|
||||
|
||||
@Override protected long delayTimer() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取ts文件保存路径
|
||||
*
|
||||
* @param dirCache 缓存目录
|
||||
* @param threadId ts文件名
|
||||
*/
|
||||
public static String getTsFilePath(String dirCache, int threadId) {
|
||||
return String.format("%s/%s.ts", dirCache, threadId);
|
||||
}
|
||||
|
||||
protected String getCacheDir() {
|
||||
String cacheDir = mM3U8Option.getCacheDir();
|
||||
if (!new File(cacheDir).exists()) {
|
||||
FileUtil.createDir(cacheDir);
|
||||
}
|
||||
return cacheDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建索引文件
|
||||
*/
|
||||
protected boolean generateIndexFile() {
|
||||
File tempFile = new File(M3U8InfoThread.M3U8_INDEX_FORMAT, getEntity().getFilePath());
|
||||
if (!tempFile.exists()) {
|
||||
ALog.e(TAG, "源索引文件不存在");
|
||||
return false;
|
||||
}
|
||||
FileInputStream fis = null;
|
||||
FileOutputStream fos = null;
|
||||
try {
|
||||
String cacheDir = getCacheDir();
|
||||
fis = new FileInputStream(tempFile);
|
||||
fos = new FileOutputStream(getEntity().getFilePath());
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(fis));
|
||||
String line;
|
||||
int i = 0;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
byte[] bytes;
|
||||
if (line.startsWith("EXTINF")) {
|
||||
String tsPath = getTsFilePath(cacheDir, mRecord.threadRecords.get(i).threadId);
|
||||
bytes = tsPath.concat("\r\n").getBytes(Charset.forName("UTF-8"));
|
||||
i++;
|
||||
} else if (line.startsWith("EXT-X-KEY")) {
|
||||
M3U8Entity m3U8Entity = getEntity().getM3U8Entity();
|
||||
String keyInfo = String.format("#EXT-X-KEY:METHOD=%s,URI=%s,IV=%s\r\n", m3U8Entity.method,
|
||||
m3U8Entity.keyPath, m3U8Entity.iv);
|
||||
bytes = keyInfo.getBytes(Charset.forName("UTF-8"));
|
||||
} else {
|
||||
bytes = line.getBytes(Charset.forName("UTF-8"));
|
||||
}
|
||||
fos.write(bytes, 0, bytes.length);
|
||||
}
|
||||
fos.flush();
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
if (fis != null) {
|
||||
fis.close();
|
||||
}
|
||||
if (fos != null) {
|
||||
fos.close();
|
||||
}
|
||||
if (tempFile.exists()) {
|
||||
FileUtil.deleteFile(tempFile);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override public long getCurrentLocation() {
|
||||
return isRunning() ? getStateManager().getCurrentProgress() : getEntity().getCurrentProgress();
|
||||
}
|
||||
|
||||
@Override protected IRecordHandler getRecordHandler(AbsTaskWrapper wrapper) {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected DownloadEntity getEntity() {
|
||||
return (DownloadEntity) mTaskWrapper.getEntity();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.arialyy.aria.m3u8;
|
||||
|
||||
public class IdGenerator {
|
||||
/**
|
||||
* SnowFlake算法 64位Long类型生成唯一ID 第一位0,表明正数 2-42,41位,表示毫秒时间戳差值,起始值自定义
|
||||
* 43-52,10位,机器编号,5位数据中心编号,5位进程编号 53-64,12位,毫秒内计数器 本机内存生成,性能高
|
||||
* <p>
|
||||
* 主要就是三部分: 时间戳,进程id,序列号 时间戳41,id10位,序列号12位
|
||||
*
|
||||
* @author chiwei
|
||||
* @param args
|
||||
* @since JDK 1.6
|
||||
*/
|
||||
|
||||
private static volatile IdGenerator INSTANCE = null;
|
||||
|
||||
private final static long beginTs = 1483200000000L;
|
||||
|
||||
private long lastTs = 0L;
|
||||
|
||||
private long processId;
|
||||
private int processIdBits = 10;
|
||||
|
||||
private long sequence = 0L;
|
||||
private int sequenceBits = 12;
|
||||
|
||||
private IdGenerator() {
|
||||
|
||||
}
|
||||
|
||||
public static synchronized IdGenerator getInstance() {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = new IdGenerator();
|
||||
}
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
// 10位进程ID标识
|
||||
public IdGenerator(long processId) {
|
||||
if (processId > ((1 << processIdBits) - 1)) {
|
||||
throw new RuntimeException("进程ID超出范围,设置位数" + processIdBits + ",最大"
|
||||
+ ((1 << processIdBits) - 1));
|
||||
}
|
||||
this.processId = processId;
|
||||
}
|
||||
|
||||
private long timeGen() {
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public synchronized long nextId() {
|
||||
long ts = timeGen();
|
||||
if (ts < lastTs) {// 刚刚生成的时间戳比上次的时间戳还小,出错
|
||||
//throw new RuntimeException("时间戳顺序错误");
|
||||
ts = nextTs(lastTs);
|
||||
}
|
||||
if (ts == lastTs) {// 刚刚生成的时间戳跟上次的时间戳一样,则需要生成一个sequence序列号
|
||||
// sequence循环自增
|
||||
sequence = (sequence + 1) & ((1 << sequenceBits) - 1);
|
||||
// 如果sequence=0则需要重新生成时间戳
|
||||
if (sequence == 0) {
|
||||
// 且必须保证时间戳序列往后
|
||||
ts = nextTs(lastTs);
|
||||
}
|
||||
} else {// 如果ts>lastTs,时间戳序列已经不同了,此时可以不必生成sequence了,直接取0
|
||||
sequence = 0L;
|
||||
}
|
||||
lastTs = ts;// 更新lastTs时间戳
|
||||
return ((ts - beginTs) << (processIdBits + sequenceBits)) | (processId << sequenceBits)
|
||||
| sequence;
|
||||
}
|
||||
|
||||
private long nextTs(long lastTs) {
|
||||
long ts = timeGen();
|
||||
while (ts <= lastTs) {
|
||||
ts = timeGen();
|
||||
}
|
||||
return ts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
/*
|
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.arialyy.aria.m3u8;
|
||||
|
||||
import android.net.TrafficStats;
|
||||
import android.net.Uri;
|
||||
import android.os.Process;
|
||||
import android.text.TextUtils;
|
||||
import com.arialyy.aria.core.AriaConfig;
|
||||
import com.arialyy.aria.core.common.CompleteInfo;
|
||||
import com.arialyy.aria.core.download.DTaskWrapper;
|
||||
import com.arialyy.aria.core.download.DownloadEntity;
|
||||
import com.arialyy.aria.core.download.M3U8Entity;
|
||||
import com.arialyy.aria.core.processor.IBandWidthUrlConverter;
|
||||
import com.arialyy.aria.core.inf.OnFileInfoCallback;
|
||||
import com.arialyy.aria.core.wrapper.AbsTaskWrapper;
|
||||
import com.arialyy.aria.core.wrapper.ITaskWrapper;
|
||||
import com.arialyy.aria.exception.M3U8Exception;
|
||||
import com.arialyy.aria.exception.TaskException;
|
||||
import com.arialyy.aria.http.ConnectionHelp;
|
||||
import com.arialyy.aria.http.HttpTaskOption;
|
||||
import com.arialyy.aria.util.ALog;
|
||||
import com.arialyy.aria.util.CheckUtil;
|
||||
import com.arialyy.aria.util.CommonUtil;
|
||||
import com.arialyy.aria.util.FileUtil;
|
||||
import com.arialyy.aria.util.Regular;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 解析url中获取到到m3u8文件信息
|
||||
* https://www.cnblogs.com/renhui/p/10351870.html
|
||||
* https://blog.csdn.net/Guofengpu/article/details/54922865
|
||||
*/
|
||||
final public class M3U8InfoThread implements Runnable {
|
||||
public static final String M3U8_INDEX_FORMAT = "%s.index";
|
||||
private final String TAG = "M3U8InfoThread";
|
||||
private DownloadEntity mEntity;
|
||||
private DTaskWrapper mTaskWrapper;
|
||||
private int mConnectTimeOut;
|
||||
private OnFileInfoCallback onFileInfoCallback;
|
||||
private OnGetLivePeerCallback onGetPeerCallback;
|
||||
private HttpTaskOption mHttpOption;
|
||||
private M3U8TaskOption mM3U8Option;
|
||||
/**
|
||||
* 是否停止获取切片信息,{@code true}停止获取切片信息
|
||||
*/
|
||||
private boolean isStop = false;
|
||||
/**
|
||||
* m3u8文件信息
|
||||
*/
|
||||
private List<String> mInfos = new ArrayList<>();
|
||||
|
||||
public interface OnGetLivePeerCallback {
|
||||
void onGetPeer(String url);
|
||||
}
|
||||
|
||||
public M3U8InfoThread(DTaskWrapper taskWrapper, OnFileInfoCallback callback) {
|
||||
this.mTaskWrapper = taskWrapper;
|
||||
mEntity = taskWrapper.getEntity();
|
||||
mConnectTimeOut = AriaConfig.getInstance().getDConfig().getConnectTimeOut();
|
||||
onFileInfoCallback = callback;
|
||||
mHttpOption = (HttpTaskOption) taskWrapper.getTaskOption();
|
||||
mM3U8Option = (M3U8TaskOption) taskWrapper.getM3u8Option();
|
||||
mEntity.getM3U8Entity().setLive(mTaskWrapper.getRequestType() == AbsTaskWrapper.M3U8_LIVE);
|
||||
}
|
||||
|
||||
@Override public void run() {
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
|
||||
TrafficStats.setThreadStatsTag(UUID.randomUUID().toString().hashCode());
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
URL url = ConnectionHelp.handleUrl(mEntity.getUrl(), mHttpOption);
|
||||
conn = ConnectionHelp.handleConnection(url, mHttpOption);
|
||||
ConnectionHelp.setConnectParam(mHttpOption, conn);
|
||||
conn.setConnectTimeout(mConnectTimeOut);
|
||||
conn.connect();
|
||||
handleConnect(conn);
|
||||
} catch (IOException e) {
|
||||
failDownload(e.getMessage(), false);
|
||||
} finally {
|
||||
if (conn != null) {
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleConnect(HttpURLConnection conn) throws IOException {
|
||||
int code = conn.getResponseCode();
|
||||
if (code == HttpURLConnection.HTTP_OK) {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
String line = reader.readLine();
|
||||
if (TextUtils.isEmpty(line) || !line.equalsIgnoreCase("#EXTM3U")) {
|
||||
failDownload("读取M3U8信息失败,读取不到#EXTM3U标签", false);
|
||||
return;
|
||||
}
|
||||
List<String> extInf = new ArrayList<>();
|
||||
boolean isLive = mTaskWrapper.getRequestType() == ITaskWrapper.M3U8_LIVE;
|
||||
boolean isGenerateIndexFile = mTaskWrapper.getEntity().getM3U8Entity().isGenerateIndexFile();
|
||||
if (isGenerateIndexFile) {
|
||||
mInfos.add(line);
|
||||
}
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (isStop) {
|
||||
break;
|
||||
}
|
||||
if (isGenerateIndexFile) {
|
||||
mInfos.add(line);
|
||||
}
|
||||
if (line.startsWith("#EXT-X-ENDLIST")) {
|
||||
break;
|
||||
}
|
||||
ALog.d(TAG, line);
|
||||
if (line.startsWith("#EXTINF")) {
|
||||
String info = reader.readLine();
|
||||
mInfos.add(info);
|
||||
if (isLive) {
|
||||
if (onGetPeerCallback != null) {
|
||||
onGetPeerCallback.onGetPeer(info);
|
||||
}
|
||||
} else {
|
||||
extInf.add(info);
|
||||
}
|
||||
} else if (line.startsWith("#EXT-X-STREAM-INF")) {
|
||||
int setBand = mM3U8Option.getBandWidth();
|
||||
int bandWidth = getBandWidth(line);
|
||||
// 多码率的m3u8配置文件,清空信息
|
||||
if (isGenerateIndexFile && mInfos != null) {
|
||||
mInfos.clear();
|
||||
}
|
||||
if (setBand == 0) {
|
||||
handleBandWidth(conn, reader.readLine());
|
||||
} else if (bandWidth == setBand) {
|
||||
handleBandWidth(conn, reader.readLine());
|
||||
} else {
|
||||
failDownload(String.format("【%s】码率不存在", bandWidth), false);
|
||||
}
|
||||
return;
|
||||
} else if (line.startsWith("EXT-X-KEY")) {
|
||||
getKeyInfo(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLive && extInf.isEmpty()) {
|
||||
failDownload(String.format("获取M3U8下载地址列表失败,url: %s", mEntity.getUrl()), false);
|
||||
return;
|
||||
}
|
||||
if (!isLive && mEntity.getM3U8Entity().getPeerNum() == 0) {
|
||||
mEntity.getM3U8Entity().setPeerNum(extInf.size());
|
||||
mEntity.getM3U8Entity().update();
|
||||
}
|
||||
CompleteInfo info = new CompleteInfo();
|
||||
info.obj = extInf;
|
||||
generateIndexFile();
|
||||
onFileInfoCallback.onComplete(mEntity.getKey(), info);
|
||||
} else if (code == HttpURLConnection.HTTP_MOVED_TEMP
|
||||
|| code == HttpURLConnection.HTTP_MOVED_PERM
|
||||
|| code == HttpURLConnection.HTTP_SEE_OTHER
|
||||
|| code == HttpURLConnection.HTTP_CREATED // 201 跳转
|
||||
|| code == 307) {
|
||||
handleUrlReTurn(conn, conn.getHeaderField("Location"));
|
||||
} else if (code == HttpURLConnection.HTTP_NOT_FOUND) {
|
||||
failDownload("404错误", false);
|
||||
} else {
|
||||
failDownload(String.format("不支持的响应,code: %s", code), true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建索引文件
|
||||
*/
|
||||
private void generateIndexFile() {
|
||||
if (mTaskWrapper.getEntity().getM3U8Entity().isGenerateIndexFile()) {
|
||||
|
||||
String indexPath = String.format(M3U8_INDEX_FORMAT, mEntity.getFilePath());
|
||||
File indexFile = new File(indexPath);
|
||||
if (indexFile.exists()) {
|
||||
FileUtil.deleteFile(indexPath);
|
||||
}
|
||||
FileUtil.createFile(indexPath);
|
||||
|
||||
FileOutputStream fos = null;
|
||||
try {
|
||||
fos = new FileOutputStream(indexFile);
|
||||
for (String str : mInfos) {
|
||||
byte[] by = str.concat("\r\n").getBytes(Charset.forName("UTF-8"));
|
||||
fos.write(by, 0, by.length);
|
||||
}
|
||||
fos.flush();
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (fos != null) {
|
||||
try {
|
||||
fos.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否停止获取切片信息,{@code true}停止获取切片信息
|
||||
*/
|
||||
public void setStop(boolean isStop) {
|
||||
this.isStop = isStop;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直播切片信息获取回调
|
||||
*/
|
||||
public void setOnGetPeerCallback(OnGetLivePeerCallback peerCallback) {
|
||||
onGetPeerCallback = peerCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加密的密钥信息
|
||||
*/
|
||||
private void getKeyInfo(String line) {
|
||||
String temp = line.substring(line.indexOf(":") + 1);
|
||||
String[] params = temp.split(",");
|
||||
M3U8Entity m3U8Entity = mEntity.getM3U8Entity();
|
||||
for (String param : params) {
|
||||
if (param.startsWith("METHOD")) {
|
||||
m3U8Entity.method = param.split("=")[1];
|
||||
} else if (param.startsWith("URI")) {
|
||||
m3U8Entity.keyUrl = param.split("=")[1].replaceAll("\"", "");
|
||||
m3U8Entity.keyPath =
|
||||
new File(mEntity.getFilePath()).getParent() + "/" + CommonUtil.getStrMd5(
|
||||
m3U8Entity.keyUrl) + ".key";
|
||||
} else if (param.startsWith("IV")) {
|
||||
m3U8Entity.iv = param.split("=")[1];
|
||||
}
|
||||
}
|
||||
downloadKey(m3U8Entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取bandwidth
|
||||
*/
|
||||
private int getBandWidth(String line) {
|
||||
Pattern p = Pattern.compile(Regular.BANDWIDTH);
|
||||
Matcher m = p.matcher(line);
|
||||
if (m.find()) {
|
||||
return Integer.parseInt(m.group());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理30x跳转
|
||||
*/
|
||||
private void handleUrlReTurn(HttpURLConnection conn, String newUrl) throws IOException {
|
||||
ALog.d(TAG, "30x跳转,新url为【" + newUrl + "】");
|
||||
if (TextUtils.isEmpty(newUrl) || newUrl.equalsIgnoreCase("null")) {
|
||||
if (onFileInfoCallback != null) {
|
||||
onFileInfoCallback.onFail(mEntity, new TaskException(TAG, "获取重定向链接失败"), false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (newUrl.startsWith("/")) {
|
||||
Uri uri = Uri.parse(mEntity.getUrl());
|
||||
newUrl = uri.getHost() + newUrl;
|
||||
}
|
||||
|
||||
if (!CheckUtil.checkUrlNotThrow(newUrl)) {
|
||||
failDownload("下载失败,重定向url错误", false);
|
||||
return;
|
||||
}
|
||||
mHttpOption.setRedirectUrl(newUrl);
|
||||
mEntity.setRedirect(true);
|
||||
mEntity.setRedirectUrl(newUrl);
|
||||
String cookies = conn.getHeaderField("Set-Cookie");
|
||||
conn.disconnect(); // 关闭上一个连接
|
||||
URL url = ConnectionHelp.handleUrl(newUrl, mHttpOption);
|
||||
conn = ConnectionHelp.handleConnection(url, mHttpOption);
|
||||
ConnectionHelp.setConnectParam(mHttpOption, conn);
|
||||
conn.setRequestProperty("Cookie", cookies);
|
||||
conn.setConnectTimeout(mConnectTimeOut);
|
||||
conn.connect();
|
||||
handleConnect(conn);
|
||||
conn.disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理码率
|
||||
*/
|
||||
private void handleBandWidth(HttpURLConnection conn, String bandWidthM3u8Url) throws IOException {
|
||||
IBandWidthUrlConverter converter = mM3U8Option.getBandWidthUrlConverter();
|
||||
if (converter != null) {
|
||||
bandWidthM3u8Url = converter.convert(bandWidthM3u8Url);
|
||||
if (!bandWidthM3u8Url.startsWith("http")) {
|
||||
failDownload(String.format("码率转换器转换后的url地址无效,转换后的url:%s", bandWidthM3u8Url), false);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
ALog.d(TAG, "没有设置码率转换器");
|
||||
}
|
||||
mM3U8Option.setBandWidthUrl(bandWidthM3u8Url);
|
||||
ALog.d(TAG, String.format("新码率url:%s", bandWidthM3u8Url));
|
||||
String cookies = conn.getHeaderField("Set-Cookie");
|
||||
conn.disconnect(); // 关闭上一个连接
|
||||
URL url = ConnectionHelp.handleUrl(bandWidthM3u8Url, mHttpOption);
|
||||
conn = ConnectionHelp.handleConnection(url, mHttpOption);
|
||||
ConnectionHelp.setConnectParam(mHttpOption, conn);
|
||||
conn.setRequestProperty("Cookie", cookies);
|
||||
conn.setConnectTimeout(mConnectTimeOut);
|
||||
conn.connect();
|
||||
handleConnect(conn);
|
||||
conn.disconnect();
|
||||
}
|
||||
|
||||
private void failDownload(String errorInfo, boolean needRetry) {
|
||||
onFileInfoCallback.onFail(mEntity, new M3U8Exception(TAG, errorInfo), needRetry);
|
||||
}
|
||||
|
||||
/**
|
||||
* 密钥不存在,下载密钥
|
||||
*/
|
||||
private void downloadKey(M3U8Entity info) {
|
||||
HttpURLConnection conn = null;
|
||||
FileOutputStream fos = null;
|
||||
try {
|
||||
File keyF = new File(info.keyPath);
|
||||
if (!keyF.exists()) {
|
||||
ALog.d(TAG, "密钥不存在,下载密钥");
|
||||
FileUtil.createFile(keyF.getPath());
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
URL url = ConnectionHelp.handleUrl(info.keyUrl, mHttpOption);
|
||||
conn = ConnectionHelp.handleConnection(url, mHttpOption);
|
||||
ConnectionHelp.setConnectParam(mHttpOption, conn);
|
||||
conn.setConnectTimeout(mConnectTimeOut);
|
||||
conn.connect();
|
||||
InputStream is = conn.getInputStream();
|
||||
fos = new FileOutputStream(keyF);
|
||||
byte[] buffer = new byte[1024];
|
||||
int len;
|
||||
while ((len = is.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, len);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
if (fos != null) {
|
||||
fos.close();
|
||||
}
|
||||
if (conn != null) {
|
||||
conn.disconnect();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.arialyy.aria.m3u8;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import com.arialyy.aria.core.download.DTaskWrapper;
|
||||
import com.arialyy.aria.core.inf.IEntity;
|
||||
import com.arialyy.aria.core.listener.BaseDListener;
|
||||
import com.arialyy.aria.core.listener.IDLoadListener;
|
||||
import com.arialyy.aria.core.listener.ISchedulers;
|
||||
import com.arialyy.aria.core.task.AbsTask;
|
||||
import com.arialyy.aria.core.task.DownloadTask;
|
||||
import com.arialyy.aria.util.CommonUtil;
|
||||
|
||||
/**
|
||||
* 下载监听类
|
||||
*/
|
||||
public class M3U8Listener extends BaseDListener implements IDLoadListener {
|
||||
|
||||
M3U8Listener(AbsTask<DTaskWrapper> task, Handler outHandler) {
|
||||
super(task, outHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostPre(long fileSize) {
|
||||
mEntity.setFileSize(fileSize);
|
||||
mEntity.setConvertFileSize(CommonUtil.formatFileSize(fileSize));
|
||||
saveData(IEntity.STATE_POST_PRE, -1);
|
||||
sendInState2Target(ISchedulers.POST_PRE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切片开始下载
|
||||
*/
|
||||
public void onPeerStart(String m3u8Url, String peerPath, int peerIndex) {
|
||||
sendPeerStateToTarget(ISchedulers.M3U8_PEER_START, m3u8Url, peerPath, peerIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切片下载完成
|
||||
*/
|
||||
public void onPeerComplete(String m3u8Url, String peerPath, int peerIndex) {
|
||||
sendPeerStateToTarget(ISchedulers.M3U8_PEER_COMPLETE, m3u8Url, peerPath, peerIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切片下载失败
|
||||
*/
|
||||
public void onPeerFail(String m3u8Url, String peerPath, int peerIndex) {
|
||||
sendPeerStateToTarget(ISchedulers.M3U8_PEER_FAIL, m3u8Url, peerPath, peerIndex);
|
||||
}
|
||||
|
||||
private void sendPeerStateToTarget(int state, String m3u8Url, String peerPath, int peerIndex) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(ISchedulers.DATA_M3U8_URL, m3u8Url);
|
||||
bundle.putString(ISchedulers.DATA_M3U8_PEER_PATH, peerPath);
|
||||
bundle.putInt(ISchedulers.DATA_M3U8_PEER_INDEX, peerIndex);
|
||||
Message msg = outHandler.get().obtainMessage();
|
||||
msg.setData(bundle);
|
||||
msg.what = state;
|
||||
msg.arg1 = ISchedulers.IS_M3U8_PEER;
|
||||
msg.sendToTarget();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.arialyy.aria.m3u8;
|
||||
|
||||
import com.arialyy.aria.core.TaskRecord;
|
||||
import com.arialyy.aria.core.ThreadRecord;
|
||||
import com.arialyy.aria.core.common.AbsRecordHandlerAdapter;
|
||||
import com.arialyy.aria.core.download.DTaskWrapper;
|
||||
import com.arialyy.aria.core.download.DownloadEntity;
|
||||
import com.arialyy.aria.core.download.M3U8Entity;
|
||||
import com.arialyy.aria.core.wrapper.ITaskWrapper;
|
||||
import com.arialyy.aria.util.ALog;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* @Author lyy
|
||||
* @Date 2019-09-24
|
||||
*/
|
||||
public class M3U8RecordAdapter extends AbsRecordHandlerAdapter {
|
||||
private M3U8TaskOption mOption;
|
||||
|
||||
public M3U8RecordAdapter(DTaskWrapper wrapper) {
|
||||
super(wrapper);
|
||||
mOption = (M3U8TaskOption) wrapper.getM3u8Option();
|
||||
}
|
||||
|
||||
/**
|
||||
* 不处理live的记录
|
||||
*/
|
||||
@Override public void handlerTaskRecord(TaskRecord mTaskRecord) {
|
||||
String cacheDir = mOption.getCacheDir();
|
||||
long currentProgress = 0;
|
||||
int completeNum = 0;
|
||||
|
||||
M3U8Entity m3U8Entity = ((DownloadEntity) getEntity()).getM3U8Entity();
|
||||
// 重新下载所有切片
|
||||
boolean reDownload =
|
||||
(m3U8Entity.getPeerNum() <= 0 || (m3U8Entity.isGenerateIndexFile() && !new File(
|
||||
String.format(M3U8InfoThread.M3U8_INDEX_FORMAT, getEntity().getFilePath())).exists()));
|
||||
|
||||
for (ThreadRecord record : mTaskRecord.threadRecords) {
|
||||
File temp = new File(BaseM3U8Loader.getTsFilePath(cacheDir, record.threadId));
|
||||
if (!record.isComplete || reDownload) {
|
||||
if (temp.exists()) {
|
||||
temp.delete();
|
||||
}
|
||||
record.startLocation = 0;
|
||||
//ALog.d(TAG, String.format("分片【%s】未完成,将重新下载该分片", record.threadId));
|
||||
} else {
|
||||
if (!temp.exists()) {
|
||||
record.startLocation = 0;
|
||||
record.isComplete = false;
|
||||
ALog.w(TAG, String.format("分片【%s】不存在,将重新下载该分片", record.threadId));
|
||||
} else {
|
||||
completeNum++;
|
||||
currentProgress += temp.length();
|
||||
}
|
||||
}
|
||||
}
|
||||
mOption.setCompleteNum(completeNum);
|
||||
getEntity().setCurrentProgress(currentProgress);
|
||||
mTaskRecord.bandWidth = mOption.getBandWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* 不处理live的记录
|
||||
*
|
||||
* @param record 任务记录
|
||||
* @param threadId 线程id
|
||||
* @param startL 线程开始位置
|
||||
* @param endL 线程结束位置
|
||||
*/
|
||||
@Override
|
||||
public ThreadRecord createThreadRecord(TaskRecord record, int threadId, long startL, long endL) {
|
||||
ThreadRecord tr;
|
||||
tr = new ThreadRecord();
|
||||
tr.taskKey = record.filePath;
|
||||
tr.threadId = threadId;
|
||||
tr.isComplete = false;
|
||||
tr.startLocation = 0;
|
||||
tr.threadType = TaskRecord.TYPE_M3U8_VOD;
|
||||
tr.tsUrl = mOption.getUrls().get(threadId);
|
||||
return tr;
|
||||
}
|
||||
|
||||
@Override public TaskRecord createTaskRecord(int threadNum) {
|
||||
TaskRecord record = new TaskRecord();
|
||||
record.fileName = getEntity().getFileName();
|
||||
record.filePath = getEntity().getFilePath();
|
||||
record.threadRecords = new ArrayList<>();
|
||||
record.threadNum = threadNum;
|
||||
|
||||
int requestType = getWrapper().getRequestType();
|
||||
if (requestType == ITaskWrapper.M3U8_VOD) {
|
||||
record.taskType = TaskRecord.TYPE_M3U8_VOD;
|
||||
record.isOpenDynamicFile = true;
|
||||
record.bandWidth = mOption.getBandWidth();
|
||||
} else if (requestType == ITaskWrapper.M3U8_LIVE) {
|
||||
record.taskType = TaskRecord.TYPE_M3U8_LIVE;
|
||||
record.isOpenDynamicFile = true;
|
||||
record.bandWidth = mOption.getBandWidth();
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
@Override public int initTaskThreadNum() {
|
||||
if (getWrapper().getRequestType() == ITaskWrapper.M3U8_VOD) {
|
||||
return mOption.getUrls().size();
|
||||
}
|
||||
if (getWrapper().getRequestType() == ITaskWrapper.M3U8_LIVE) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.arialyy.aria.m3u8;
|
||||
|
||||
import com.arialyy.aria.core.processor.IBandWidthUrlConverter;
|
||||
import com.arialyy.aria.core.inf.ITaskOption;
|
||||
import com.arialyy.aria.core.processor.ITsMergeHandler;
|
||||
import com.arialyy.aria.core.processor.ILiveTsUrlConverter;
|
||||
import com.arialyy.aria.core.processor.IVodTsUrlConverter;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* m3u8任务配信息
|
||||
*/
|
||||
public class M3U8TaskOption implements ITaskOption {
|
||||
|
||||
/**
|
||||
* 所有ts文件的下载地址
|
||||
*/
|
||||
private List<String> urls;
|
||||
|
||||
/**
|
||||
* #EXTINF 标签信息处理器
|
||||
*/
|
||||
private SoftReference<IVodTsUrlConverter> vodUrlConverter;
|
||||
|
||||
/**
|
||||
* 缓存目录
|
||||
*/
|
||||
private String cacheDir;
|
||||
|
||||
/**
|
||||
* 是否合并ts文件 {@code true} 合并ts文件为一个
|
||||
*/
|
||||
private boolean mergeFile = true;
|
||||
|
||||
/**
|
||||
* 合并处理器
|
||||
*/
|
||||
private SoftReference<ITsMergeHandler> mergeHandler;
|
||||
|
||||
/**
|
||||
* 已完成的ts分片数量
|
||||
*/
|
||||
private int completeNum = 0;
|
||||
|
||||
/**
|
||||
* 视频时长,单位s
|
||||
*/
|
||||
private long duration;
|
||||
|
||||
/**
|
||||
* 码率
|
||||
*/
|
||||
private int bandWidth = 0;
|
||||
|
||||
/**
|
||||
* 码率url转换器
|
||||
*/
|
||||
private SoftReference<IBandWidthUrlConverter> bandWidthUrlConverter;
|
||||
|
||||
/**
|
||||
* 码率地址
|
||||
*/
|
||||
private String bandWidthUrl;
|
||||
|
||||
/**
|
||||
* 直播下载,ts url转换器
|
||||
*/
|
||||
private SoftReference<ILiveTsUrlConverter> liveTsUrlConverter;
|
||||
|
||||
/**
|
||||
* 直播的m3u8文件更新间隔
|
||||
*/
|
||||
private long liveUpdateInterval = 10 * 1000;
|
||||
|
||||
/**
|
||||
* 同时下载的分片数量
|
||||
*/
|
||||
private int maxTsQueueNum = 4;
|
||||
|
||||
/**
|
||||
* 指定的索引位置
|
||||
*/
|
||||
private int jumpIndex;
|
||||
|
||||
/**
|
||||
* 生成索引占位字段
|
||||
*/
|
||||
private boolean generateIndexFileTemp;
|
||||
|
||||
public boolean isGenerateIndexFileTemp() {
|
||||
return generateIndexFileTemp;
|
||||
}
|
||||
|
||||
public void setGenerateIndexFileTemp(boolean generateIndexFileTemp) {
|
||||
this.generateIndexFileTemp = generateIndexFileTemp;
|
||||
}
|
||||
|
||||
public int getJumpIndex() {
|
||||
return jumpIndex;
|
||||
}
|
||||
|
||||
public void setJumpIndex(int jumpIndex) {
|
||||
this.jumpIndex = jumpIndex;
|
||||
}
|
||||
|
||||
public int getMaxTsQueueNum() {
|
||||
return maxTsQueueNum;
|
||||
}
|
||||
|
||||
public void setMaxTsQueueNum(int maxTsQueueNum) {
|
||||
this.maxTsQueueNum = maxTsQueueNum;
|
||||
}
|
||||
|
||||
public long getLiveUpdateInterval() {
|
||||
return liveUpdateInterval;
|
||||
}
|
||||
|
||||
public void setLiveUpdateInterval(long liveUpdateInterval) {
|
||||
this.liveUpdateInterval = liveUpdateInterval;
|
||||
}
|
||||
|
||||
public ILiveTsUrlConverter getLiveTsUrlConverter() {
|
||||
return liveTsUrlConverter == null ? null : liveTsUrlConverter.get();
|
||||
}
|
||||
|
||||
public void setLiveTsUrlConverter(ILiveTsUrlConverter liveTsUrlConverter) {
|
||||
this.liveTsUrlConverter = new SoftReference<>(liveTsUrlConverter);
|
||||
}
|
||||
|
||||
public String getBandWidthUrl() {
|
||||
return bandWidthUrl;
|
||||
}
|
||||
|
||||
public void setBandWidthUrl(String bandWidthUrl) {
|
||||
this.bandWidthUrl = bandWidthUrl;
|
||||
}
|
||||
|
||||
public IBandWidthUrlConverter getBandWidthUrlConverter() {
|
||||
return bandWidthUrlConverter == null ? null : bandWidthUrlConverter.get();
|
||||
}
|
||||
|
||||
public void setBandWidthUrlConverter(IBandWidthUrlConverter bandWidthUrlConverter) {
|
||||
this.bandWidthUrlConverter = new SoftReference<>(bandWidthUrlConverter);
|
||||
}
|
||||
|
||||
public int getBandWidth() {
|
||||
return bandWidth;
|
||||
}
|
||||
|
||||
public void setBandWidth(int bandWidth) {
|
||||
this.bandWidth = bandWidth;
|
||||
}
|
||||
|
||||
public long getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public void setDuration(long duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
public int getCompleteNum() {
|
||||
return completeNum;
|
||||
}
|
||||
|
||||
public void setCompleteNum(int completeNum) {
|
||||
this.completeNum = completeNum;
|
||||
}
|
||||
|
||||
public boolean isMergeFile() {
|
||||
return mergeFile;
|
||||
}
|
||||
|
||||
public void setMergeFile(boolean mergeFile) {
|
||||
this.mergeFile = mergeFile;
|
||||
}
|
||||
|
||||
public ITsMergeHandler getMergeHandler() {
|
||||
return mergeHandler == null ? null : mergeHandler.get();
|
||||
}
|
||||
|
||||
public void setMergeHandler(ITsMergeHandler mergeHandler) {
|
||||
this.mergeHandler = new SoftReference<>(mergeHandler);
|
||||
}
|
||||
|
||||
public IVodTsUrlConverter getVodUrlConverter() {
|
||||
return vodUrlConverter == null ? null : vodUrlConverter.get();
|
||||
}
|
||||
|
||||
public void setVodUrlConverter(IVodTsUrlConverter vodUrlConverter) {
|
||||
this.vodUrlConverter = new SoftReference<>(vodUrlConverter);
|
||||
}
|
||||
|
||||
public List<String> getUrls() {
|
||||
return urls;
|
||||
}
|
||||
|
||||
public void setUrls(List<String> urls) {
|
||||
this.urls = urls;
|
||||
}
|
||||
|
||||
public String getCacheDir() {
|
||||
return cacheDir;
|
||||
}
|
||||
|
||||
public void setCacheDir(String cacheDir) {
|
||||
this.cacheDir = cacheDir;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.arialyy.aria.m3u8;
|
||||
|
||||
import com.arialyy.aria.core.common.RequestEnum;
|
||||
import com.arialyy.aria.core.common.SubThreadConfig;
|
||||
import com.arialyy.aria.core.download.DownloadEntity;
|
||||
import com.arialyy.aria.core.task.AbsThreadTaskAdapter;
|
||||
import com.arialyy.aria.exception.AriaIOException;
|
||||
import com.arialyy.aria.exception.TaskException;
|
||||
import com.arialyy.aria.http.ConnectionHelp;
|
||||
import com.arialyy.aria.http.HttpTaskOption;
|
||||
import com.arialyy.aria.util.ALog;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Created by lyy on 2017/1/18. 下载线程
|
||||
*/
|
||||
public class M3U8ThreadTaskAdapter extends AbsThreadTaskAdapter {
|
||||
private final String TAG = "M3U8ThreadTask";
|
||||
private HttpTaskOption mHttpTaskOption;
|
||||
|
||||
public M3U8ThreadTaskAdapter(SubThreadConfig config) {
|
||||
super(config);
|
||||
mHttpTaskOption = (HttpTaskOption) getTaskWrapper().getTaskOption();
|
||||
}
|
||||
|
||||
@Override protected void handlerThreadTask() {
|
||||
if (getThreadRecord().isComplete) {
|
||||
handleComplete();
|
||||
return;
|
||||
}
|
||||
HttpURLConnection conn = null;
|
||||
BufferedInputStream is = null;
|
||||
try {
|
||||
URL url = ConnectionHelp.handleUrl(getConfig().url, mHttpTaskOption);
|
||||
conn = ConnectionHelp.handleConnection(url, mHttpTaskOption);
|
||||
ALog.d(TAG, String.format("分片【%s】开始下载", getThreadRecord().threadId));
|
||||
ConnectionHelp.setConnectParam(mHttpTaskOption, conn);
|
||||
conn.setConnectTimeout(getTaskConfig().getConnectTimeOut());
|
||||
conn.setReadTimeout(getTaskConfig().getIOTimeOut()); //设置读取流的等待时间,必须设置该参数
|
||||
if (mHttpTaskOption.isChunked()) {
|
||||
conn.setDoInput(true);
|
||||
conn.setChunkedStreamingMode(0);
|
||||
}
|
||||
conn.connect();
|
||||
// 传递参数
|
||||
if (mHttpTaskOption.getRequestEnum() == RequestEnum.POST) {
|
||||
Map<String, String> params = mHttpTaskOption.getParams();
|
||||
if (params != null) {
|
||||
OutputStreamWriter dos = new OutputStreamWriter(conn.getOutputStream());
|
||||
Set<String> keys = params.keySet();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String key : keys) {
|
||||
sb.append(key).append("=").append(URLEncoder.encode(params.get(key))).append("&");
|
||||
}
|
||||
String paramStr = sb.toString();
|
||||
paramStr = paramStr.substring(0, paramStr.length() - 1);
|
||||
dos.write(paramStr);
|
||||
dos.flush();
|
||||
dos.close();
|
||||
}
|
||||
}
|
||||
|
||||
is = new BufferedInputStream(ConnectionHelp.convertInputStream(conn));
|
||||
if (mHttpTaskOption.isChunked()) {
|
||||
readChunked(is);
|
||||
} else if (getConfig().isOpenDynamicFile) {
|
||||
readDynamicFile(is);
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
fail(new TaskException(TAG,
|
||||
String.format("分片【%s】下载失败,filePath: %s, url: %s", getThreadRecord().threadId,
|
||||
getConfig().tempFile.getPath(), getEntity().getUrl()), e), false);
|
||||
} catch (IOException e) {
|
||||
fail(new TaskException(TAG,
|
||||
String.format("分片【%s】下载失败,filePath: %s, url: %s", getThreadRecord().threadId,
|
||||
getConfig().tempFile.getPath(), getEntity().getUrl()), e), true);
|
||||
} catch (Exception e) {
|
||||
fail(new TaskException(TAG,
|
||||
String.format("分片【%s】下载失败,filePath: %s, url: %s", getThreadRecord().threadId,
|
||||
getConfig().tempFile.getPath(), getEntity().getUrl()), e), false);
|
||||
} finally {
|
||||
try {
|
||||
if (is != null) {
|
||||
is.close();
|
||||
}
|
||||
if (conn != null) {
|
||||
conn.disconnect();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取chunked数据
|
||||
*/
|
||||
private void readChunked(InputStream is) {
|
||||
FileOutputStream fos = null;
|
||||
try {
|
||||
fos = new FileOutputStream(getConfig().tempFile, true);
|
||||
byte[] buffer = new byte[getTaskConfig().getBuffSize()];
|
||||
int len;
|
||||
while (getThreadTask().isLive() && (len = is.read(buffer)) != -1) {
|
||||
if (getThreadTask().isBreak()) {
|
||||
break;
|
||||
}
|
||||
if (mSpeedBandUtil != null) {
|
||||
mSpeedBandUtil.limitNextBytes(len);
|
||||
}
|
||||
fos.write(buffer, 0, len);
|
||||
progress(len);
|
||||
}
|
||||
handleComplete();
|
||||
} catch (IOException e) {
|
||||
fail(new AriaIOException(TAG,
|
||||
String.format("文件下载失败,savePath: %s, url: %s", getConfig().tempFile.getPath(),
|
||||
getConfig().url), e), true);
|
||||
} finally {
|
||||
if (fos != null) {
|
||||
try {
|
||||
fos.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态长度文件读取方式
|
||||
*/
|
||||
private void readDynamicFile(InputStream is) {
|
||||
FileOutputStream fos = null;
|
||||
FileChannel foc = null;
|
||||
ReadableByteChannel fic = null;
|
||||
try {
|
||||
int len;
|
||||
fos = new FileOutputStream(getConfig().tempFile, true);
|
||||
foc = fos.getChannel();
|
||||
fic = Channels.newChannel(is);
|
||||
ByteBuffer bf = ByteBuffer.allocate(getTaskConfig().getBuffSize());
|
||||
//如果要通过 Future 的 cancel 方法取消正在运行的任务,那么该任务必定是可以 对线程中断做出响应 的任务。
|
||||
|
||||
while (getThreadTask().isLive() && (len = fic.read(bf)) != -1) {
|
||||
if (getThreadTask().isBreak()) {
|
||||
break;
|
||||
}
|
||||
if (mSpeedBandUtil != null) {
|
||||
mSpeedBandUtil.limitNextBytes(len);
|
||||
}
|
||||
bf.flip();
|
||||
foc.write(bf);
|
||||
bf.compact();
|
||||
progress(len);
|
||||
}
|
||||
handleComplete();
|
||||
} catch (IOException e) {
|
||||
fail(new AriaIOException(TAG,
|
||||
String.format("文件下载失败,savePath: %s, url: %s", getConfig().tempFile.getPath(),
|
||||
getConfig().url), e), true);
|
||||
} finally {
|
||||
try {
|
||||
if (fos != null) {
|
||||
fos.flush();
|
||||
fos.close();
|
||||
}
|
||||
if (foc != null) {
|
||||
foc.close();
|
||||
}
|
||||
if (fic != null) {
|
||||
fic.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DownloadEntity getEntity() {
|
||||
return (DownloadEntity) getTaskWrapper().getEntity();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理完成配置文件的更新或事件回调
|
||||
*/
|
||||
private void handleComplete() {
|
||||
if (getThreadTask().isBreak()) {
|
||||
return;
|
||||
}
|
||||
complete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.arialyy.aria.m3u8.live;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import com.arialyy.aria.core.TaskRecord;
|
||||
import com.arialyy.aria.core.ThreadRecord;
|
||||
import com.arialyy.aria.core.common.SubThreadConfig;
|
||||
import com.arialyy.aria.core.download.DTaskWrapper;
|
||||
import com.arialyy.aria.core.inf.IThreadState;
|
||||
import com.arialyy.aria.core.listener.IEventListener;
|
||||
import com.arialyy.aria.core.manager.ThreadTaskManager;
|
||||
import com.arialyy.aria.core.task.ThreadTask;
|
||||
import com.arialyy.aria.m3u8.BaseM3U8Loader;
|
||||
import com.arialyy.aria.core.processor.ITsMergeHandler;
|
||||
import com.arialyy.aria.m3u8.IdGenerator;
|
||||
import com.arialyy.aria.m3u8.M3U8Listener;
|
||||
import com.arialyy.aria.m3u8.M3U8ThreadTaskAdapter;
|
||||
import com.arialyy.aria.util.ALog;
|
||||
import com.arialyy.aria.util.FileUtil;
|
||||
import java.io.File;
|
||||
import java.io.FilenameFilter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* M3U8点播文件下载器
|
||||
*/
|
||||
public class M3U8LiveLoader extends BaseM3U8Loader {
|
||||
/**
|
||||
* 最大执行数
|
||||
*/
|
||||
private static final int EXEC_MAX_NUM = 4;
|
||||
private Handler mStateHandler;
|
||||
private ArrayBlockingQueue<Long> mFlagQueue = new ArrayBlockingQueue<>(EXEC_MAX_NUM);
|
||||
private LiveStateManager mManager;
|
||||
private ReentrantLock LOCK = new ReentrantLock();
|
||||
private Condition mCondition = LOCK.newCondition();
|
||||
private LinkedBlockingQueue<String> mPeerQueue = new LinkedBlockingQueue<>();
|
||||
|
||||
public M3U8LiveLoader(M3U8Listener listener, DTaskWrapper wrapper) {
|
||||
super(listener, wrapper);
|
||||
}
|
||||
|
||||
@Override protected IThreadState createStateManager(Looper looper) {
|
||||
mManager = new LiveStateManager(looper, mListener);
|
||||
mStateHandler = new Handler(looper, mManager);
|
||||
return mManager;
|
||||
}
|
||||
|
||||
void offerPeer(String peerUrl) {
|
||||
mPeerQueue.offer(peerUrl);
|
||||
}
|
||||
|
||||
@Override protected void handleTask() {
|
||||
|
||||
new Thread(new Runnable() {
|
||||
@Override public void run() {
|
||||
String cacheDir = getCacheDir();
|
||||
int index = 0;
|
||||
while (!isBreak()) {
|
||||
try {
|
||||
LOCK.lock();
|
||||
while (mFlagQueue.size() < EXEC_MAX_NUM) {
|
||||
String url = mPeerQueue.poll();
|
||||
if (url == null) {
|
||||
break;
|
||||
}
|
||||
ThreadTask task = createThreadTask(cacheDir, index, url);
|
||||
getTaskList().put(index, task);
|
||||
mFlagQueue.offer(startThreadTask(task));
|
||||
index++;
|
||||
}
|
||||
if (mFlagQueue.size() > 0) {
|
||||
mCondition.await();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
LOCK.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override public long getFileSize() {
|
||||
return mTempFile.length();
|
||||
}
|
||||
|
||||
private void notifyLock() {
|
||||
try {
|
||||
LOCK.lock();
|
||||
long id = mFlagQueue.take();
|
||||
ALog.d(TAG, String.format("线程【%s】完成", id));
|
||||
mCondition.signalAll();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
LOCK.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动线程任务
|
||||
*
|
||||
* @return 线程唯一id标志
|
||||
*/
|
||||
private long startThreadTask(ThreadTask task) {
|
||||
ThreadTaskManager.getInstance().startThread(mTaskWrapper.getKey(), task);
|
||||
return IdGenerator.getInstance().nextId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置config
|
||||
*/
|
||||
private ThreadTask createThreadTask(String cacheDir, int indexId, String tsUrl) {
|
||||
ThreadRecord record = new ThreadRecord();
|
||||
record.taskKey = mRecord.filePath;
|
||||
record.isComplete = false;
|
||||
record.tsUrl = tsUrl;
|
||||
record.threadType = TaskRecord.TYPE_M3U8_LIVE;
|
||||
record.threadId = indexId;
|
||||
|
||||
SubThreadConfig config = new SubThreadConfig();
|
||||
config.url = tsUrl;
|
||||
config.tempFile = new File(getTsFilePath(cacheDir, indexId));
|
||||
config.isBlock = mRecord.isBlock;
|
||||
config.isOpenDynamicFile = mRecord.isOpenDynamicFile;
|
||||
config.taskWrapper = mTaskWrapper;
|
||||
config.record = record;
|
||||
config.stateHandler = mStateHandler;
|
||||
|
||||
if (!config.tempFile.exists()) {
|
||||
FileUtil.createFile(config.tempFile.getPath());
|
||||
}
|
||||
ThreadTask threadTask = new ThreadTask(config);
|
||||
M3U8ThreadTaskAdapter adapter = new M3U8ThreadTaskAdapter(config);
|
||||
threadTask.setAdapter(adapter);
|
||||
return threadTask;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并文件
|
||||
*
|
||||
* @return {@code true} 合并成功,{@code false}合并失败
|
||||
*/
|
||||
public boolean mergeFile() {
|
||||
if (getEntity().getM3U8Entity().isGenerateIndexFile()) {
|
||||
return generateIndexFile();
|
||||
}
|
||||
ITsMergeHandler mergeHandler = mM3U8Option.getMergeHandler();
|
||||
String cacheDir = getCacheDir();
|
||||
List<String> partPath = new ArrayList<>();
|
||||
String[] tsNames = new File(cacheDir).list(new FilenameFilter() {
|
||||
@Override public boolean accept(File dir, String name) {
|
||||
return name.endsWith(".ts");
|
||||
}
|
||||
});
|
||||
for (String tsName : tsNames) {
|
||||
partPath.add(cacheDir + "/" + tsName);
|
||||
}
|
||||
|
||||
boolean isSuccess;
|
||||
if (mergeHandler != null) {
|
||||
isSuccess = mergeHandler.merge(getEntity().getM3U8Entity(), partPath);
|
||||
} else {
|
||||
isSuccess = FileUtil.mergeFile(getEntity().getFilePath(), partPath);
|
||||
}
|
||||
if (isSuccess) {
|
||||
// 合并成功,删除缓存文件
|
||||
for (String pp : partPath) {
|
||||
File f = new File(pp);
|
||||
if (f.exists()) {
|
||||
f.delete();
|
||||
}
|
||||
}
|
||||
File cDir = new File(cacheDir);
|
||||
if (cDir.exists()) {
|
||||
cDir.delete();
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
ALog.e(TAG, "合并失败");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* M3U8线程状态管理,直播不处理停止状态、删除、失败的状态
|
||||
*/
|
||||
private class LiveStateManager implements IThreadState {
|
||||
private final String TAG = "M3U8ThreadStateManager";
|
||||
|
||||
/**
|
||||
* 任务状态回调
|
||||
*/
|
||||
private IEventListener mListener;
|
||||
private long mProgress; //当前总进度
|
||||
private Looper mLooper;
|
||||
|
||||
/**
|
||||
* @param listener 任务事件
|
||||
*/
|
||||
LiveStateManager(Looper looper, IEventListener listener) {
|
||||
mLooper = looper;
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出looper循环
|
||||
*/
|
||||
private void quitLooper() {
|
||||
ALog.d(TAG, "quitLooper");
|
||||
mLooper.quit();
|
||||
}
|
||||
|
||||
@Override public boolean handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case STATE_STOP:
|
||||
if (isBreak()) {
|
||||
ALog.d(TAG, "任务停止");
|
||||
quitLooper();
|
||||
}
|
||||
break;
|
||||
case STATE_CANCEL:
|
||||
if (isBreak()) {
|
||||
ALog.d(TAG, "任务取消");
|
||||
quitLooper();
|
||||
}
|
||||
break;
|
||||
case STATE_COMPLETE:
|
||||
notifyLock();
|
||||
break;
|
||||
case STATE_RUNNING:
|
||||
mProgress += (long) msg.obj;
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override public boolean isFail() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override public boolean isComplete() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override public long getCurrentProgress() {
|
||||
return mProgress;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.arialyy.aria.m3u8.live;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import com.arialyy.aria.core.common.AbsEntity;
|
||||
import com.arialyy.aria.core.common.CompleteInfo;
|
||||
import com.arialyy.aria.core.download.DTaskWrapper;
|
||||
import com.arialyy.aria.core.download.DownloadEntity;
|
||||
import com.arialyy.aria.core.processor.ILiveTsUrlConverter;
|
||||
import com.arialyy.aria.core.inf.OnFileInfoCallback;
|
||||
import com.arialyy.aria.core.loader.AbsLoader;
|
||||
import com.arialyy.aria.core.loader.AbsNormalLoaderUtil;
|
||||
import com.arialyy.aria.exception.BaseException;
|
||||
import com.arialyy.aria.exception.M3U8Exception;
|
||||
import com.arialyy.aria.m3u8.M3U8InfoThread;
|
||||
import com.arialyy.aria.m3u8.M3U8Listener;
|
||||
import com.arialyy.aria.m3u8.M3U8TaskOption;
|
||||
import com.arialyy.aria.util.ALog;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* M3U8直播文件下载工具,对于直播来说,需要定时更新m3u8文件
|
||||
* 工作流程:
|
||||
* 1、持续获取切片信息,直到调用停止|取消才停止获取切片信息
|
||||
* 2、完成所有分片下载后,合并ts文件
|
||||
* 3、删除该隐藏文件夹
|
||||
* 4、对于直播来说是没有停止的,停止就代表完成
|
||||
* 5、不处理直播切片下载失败的状态
|
||||
*/
|
||||
public class M3U8LiveUtil extends AbsNormalLoaderUtil {
|
||||
private final String TAG = "M3U8LiveDownloadUtil";
|
||||
private M3U8InfoThread mInfoThread;
|
||||
private ScheduledThreadPoolExecutor mTimer;
|
||||
private ExecutorService mInfoPool = Executors.newCachedThreadPool();
|
||||
private List<String> mPeerUrls = new ArrayList<>();
|
||||
private M3U8TaskOption mM3U8Option;
|
||||
|
||||
protected M3U8LiveUtil(DTaskWrapper wrapper, M3U8Listener listener) {
|
||||
super(wrapper, listener);
|
||||
wrapper.generateM3u8Option(M3U8TaskOption.class);
|
||||
mM3U8Option = (M3U8TaskOption) wrapper.getM3u8Option();
|
||||
}
|
||||
|
||||
@Override protected AbsLoader createLoader() {
|
||||
return new M3U8LiveLoader((M3U8Listener) getListener(), (DTaskWrapper) getTaskWrapper());
|
||||
}
|
||||
|
||||
@Override protected Runnable createInfoThread() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Runnable createLiveInfoThread(){
|
||||
M3U8InfoThread infoThread =
|
||||
new M3U8InfoThread((DTaskWrapper) getTaskWrapper(), new OnFileInfoCallback() {
|
||||
@Override public void onComplete(String key, CompleteInfo info) {
|
||||
ALog.d(TAG, "更新直播的m3u8文件");
|
||||
}
|
||||
|
||||
@Override public void onFail(AbsEntity entity, BaseException e, boolean needRetry) {
|
||||
fail(e, needRetry);
|
||||
}
|
||||
});
|
||||
infoThread.setOnGetPeerCallback(new M3U8InfoThread.OnGetLivePeerCallback() {
|
||||
@Override public void onGetPeer(String url) {
|
||||
if (mPeerUrls.contains(url)) {
|
||||
return;
|
||||
}
|
||||
mPeerUrls.add(url);
|
||||
ILiveTsUrlConverter converter = mM3U8Option.getLiveTsUrlConverter();
|
||||
if (converter != null) {
|
||||
if (TextUtils.isEmpty(mM3U8Option.getBandWidthUrl())) {
|
||||
url = converter.convert(((DownloadEntity) getTaskWrapper().getEntity()).getUrl(), url);
|
||||
} else {
|
||||
url = converter.convert(mM3U8Option.getBandWidthUrl(), url);
|
||||
}
|
||||
}
|
||||
if (TextUtils.isEmpty(url) || !url.startsWith("http")) {
|
||||
fail(new M3U8Exception(TAG, String.format("ts地址错误,url:%s", url)), false);
|
||||
return;
|
||||
}
|
||||
getLoader().offerPeer(url);
|
||||
}
|
||||
});
|
||||
return infoThread;
|
||||
}
|
||||
|
||||
@Override protected void onCancel() {
|
||||
super.onCancel();
|
||||
if (mInfoThread != null) {
|
||||
mInfoThread.setStop(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对于直播来说是没有停止的,停止就代表完成
|
||||
*/
|
||||
@Override protected void onStop() {
|
||||
super.onStop();
|
||||
handleComplete();
|
||||
}
|
||||
|
||||
private void handleComplete() {
|
||||
if (mInfoThread != null) {
|
||||
mInfoThread.setStop(true);
|
||||
closeTimer();
|
||||
if (mM3U8Option.isMergeFile()) {
|
||||
if (getLoader().mergeFile()) {
|
||||
getListener().onComplete();
|
||||
} else {
|
||||
getListener().onFail(false, new M3U8Exception(TAG, "合并文件失败"));
|
||||
}
|
||||
} else {
|
||||
getListener().onComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected void onStart() {
|
||||
super.onStart();
|
||||
startTimer();
|
||||
}
|
||||
|
||||
private void startTimer() {
|
||||
mTimer = new ScheduledThreadPoolExecutor(1);
|
||||
mTimer.scheduleWithFixedDelay(new Runnable() {
|
||||
@Override public void run() {
|
||||
mInfoThread = (M3U8InfoThread) createLiveInfoThread();
|
||||
mInfoPool.execute(mInfoThread);
|
||||
}
|
||||
}, 0, mM3U8Option.getLiveUpdateInterval(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void closeTimer() {
|
||||
if (mTimer != null && !mTimer.isShutdown()) {
|
||||
mTimer.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected void fail(BaseException e, boolean needRetry) {
|
||||
super.fail(e, needRetry);
|
||||
handleComplete();
|
||||
}
|
||||
|
||||
@Override public M3U8LiveLoader getLoader() {
|
||||
return (M3U8LiveLoader) super.getLoader();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,671 @@
|
||||
/*
|
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.arialyy.aria.m3u8.vod;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.util.SparseArray;
|
||||
import com.arialyy.aria.core.TaskRecord;
|
||||
import com.arialyy.aria.core.ThreadRecord;
|
||||
import com.arialyy.aria.core.common.SubThreadConfig;
|
||||
import com.arialyy.aria.core.download.DTaskWrapper;
|
||||
import com.arialyy.aria.core.event.Event;
|
||||
import com.arialyy.aria.core.event.EventMsgUtil;
|
||||
import com.arialyy.aria.core.event.PeerIndexEvent;
|
||||
import com.arialyy.aria.core.inf.IThreadState;
|
||||
import com.arialyy.aria.core.listener.IEventListener;
|
||||
import com.arialyy.aria.core.listener.ISchedulers;
|
||||
import com.arialyy.aria.core.manager.ThreadTaskManager;
|
||||
import com.arialyy.aria.core.task.ThreadTask;
|
||||
import com.arialyy.aria.exception.BaseException;
|
||||
import com.arialyy.aria.m3u8.BaseM3U8Loader;
|
||||
import com.arialyy.aria.core.processor.ITsMergeHandler;
|
||||
import com.arialyy.aria.m3u8.M3U8Listener;
|
||||
import com.arialyy.aria.m3u8.M3U8TaskOption;
|
||||
import com.arialyy.aria.m3u8.M3U8ThreadTaskAdapter;
|
||||
import com.arialyy.aria.util.ALog;
|
||||
import com.arialyy.aria.util.FileUtil;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* M3U8点播文件下载器
|
||||
*/
|
||||
public class M3U8VodLoader extends BaseM3U8Loader {
|
||||
/**
|
||||
* 最大执行数
|
||||
*/
|
||||
private int EXEC_MAX_NUM;
|
||||
private Handler mStateHandler;
|
||||
private ArrayBlockingQueue<TempFlag> mFlagQueue;
|
||||
private ArrayBlockingQueue<PeerIndexEvent> mJumpQueue;
|
||||
private ReentrantLock LOCK = new ReentrantLock();
|
||||
private ReentrantLock EVENT_LOCK = new ReentrantLock();
|
||||
private ReentrantLock JUMP_LOCK = new ReentrantLock();
|
||||
private Condition mWaitCondition = LOCK.newCondition();
|
||||
private Condition mEventQueueCondition = EVENT_LOCK.newCondition();
|
||||
private Condition mJumpCondition = JUMP_LOCK.newCondition();
|
||||
private SparseArray<ThreadRecord> mBeforePeer = new SparseArray<>();
|
||||
private SparseArray<ThreadRecord> mAfterPeer = new SparseArray<>();
|
||||
private VodStateManager mManager;
|
||||
private PeerIndexEvent mCurrentEvent;
|
||||
private String mCacheDir;
|
||||
private int aIndex = 0, bIndex = 0;
|
||||
private int mCurrentFlagSize;
|
||||
private boolean isJump = false, isDestroy = false;
|
||||
private int mCompleteNum = 0;
|
||||
private ExecutorService mJumpThreadPool;
|
||||
private Thread jumpThread = null;
|
||||
private M3U8TaskOption mM3U8Option;
|
||||
|
||||
M3U8VodLoader(M3U8Listener listener, DTaskWrapper wrapper) {
|
||||
super(listener, wrapper);
|
||||
mM3U8Option = (M3U8TaskOption) wrapper.getM3u8Option();
|
||||
mFlagQueue = new ArrayBlockingQueue<>(mM3U8Option.getMaxTsQueueNum());
|
||||
EXEC_MAX_NUM = mM3U8Option.getMaxTsQueueNum();
|
||||
mJumpQueue = new ArrayBlockingQueue<>(10);
|
||||
EventMsgUtil.getDefault().register(this);
|
||||
}
|
||||
|
||||
@Override protected IThreadState createStateManager(Looper looper) {
|
||||
mManager = new VodStateManager(looper, mRecord, mListener);
|
||||
mStateHandler = new Handler(looper, mManager);
|
||||
return mManager;
|
||||
}
|
||||
|
||||
@Override public void onDestroy() {
|
||||
super.onDestroy();
|
||||
isDestroy = true;
|
||||
EventMsgUtil.getDefault().unRegister(this);
|
||||
if (mJumpThreadPool != null && !mJumpThreadPool.isShutdown()) {
|
||||
mJumpThreadPool.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected void onPostPre() {
|
||||
super.onPostPre();
|
||||
initData();
|
||||
}
|
||||
|
||||
@Override public boolean isBreak() {
|
||||
return super.isBreak() || isDestroy;
|
||||
}
|
||||
|
||||
@Override protected void handleTask() {
|
||||
Thread th = new Thread(new Runnable() {
|
||||
@Override public void run() {
|
||||
while (!isBreak()) {
|
||||
try {
|
||||
JUMP_LOCK.lock();
|
||||
if (isJump) {
|
||||
mJumpCondition.await(5, TimeUnit.SECONDS);
|
||||
isJump = false;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
JUMP_LOCK.unlock();
|
||||
}
|
||||
|
||||
try {
|
||||
LOCK.lock();
|
||||
while (mFlagQueue.size() < EXEC_MAX_NUM && !isBreak()) {
|
||||
if (mCompleteNum == mRecord.threadRecords.size()) {
|
||||
break;
|
||||
}
|
||||
|
||||
ThreadRecord tr = getThreadRecord();
|
||||
if (tr == null || tr.isComplete) {
|
||||
ALog.d(TAG, "记录为空或记录已完成");
|
||||
break;
|
||||
}
|
||||
addTaskToQueue(tr);
|
||||
}
|
||||
if (mFlagQueue.size() > 0) {
|
||||
mWaitCondition.await();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
LOCK.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
th.start();
|
||||
}
|
||||
|
||||
@Override public long getFileSize() {
|
||||
return getEntity().getFileSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取线程记录
|
||||
*/
|
||||
private ThreadRecord getThreadRecord() {
|
||||
ThreadRecord tr = null;
|
||||
try {
|
||||
// 优先下载peer指针之后的数据
|
||||
if (bIndex == 0 && aIndex < mAfterPeer.size()) {
|
||||
//ALog.d(TAG, String.format("afterArray size:%s, index:%s", mAfterPeer.size(), aIndex));
|
||||
tr = mAfterPeer.valueAt(aIndex);
|
||||
aIndex++;
|
||||
}
|
||||
|
||||
// 如果指针之后的数组没有切片了,则重新初始化指针位置,并获取指针之前的数组获取切片进行下载
|
||||
if (mBeforePeer.size() > 0 && (tr == null || bIndex != 0) && bIndex < mBeforePeer.size()) {
|
||||
tr = mBeforePeer.valueAt(bIndex);
|
||||
bIndex++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动线程任务
|
||||
*/
|
||||
private void addTaskToQueue(ThreadRecord tr) throws InterruptedException {
|
||||
ThreadTask task = createThreadTask(mCacheDir, tr, tr.threadId);
|
||||
getTaskList().put(tr.threadId, task);
|
||||
getEntity().getM3U8Entity().setPeerIndex(tr.threadId);
|
||||
TempFlag flag = startThreadTask(task, tr.threadId);
|
||||
if (flag != null) {
|
||||
mFlagQueue.put(flag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据
|
||||
*/
|
||||
private void initData() {
|
||||
mCacheDir = getCacheDir();
|
||||
if (mM3U8Option.getJumpIndex() != 0) {
|
||||
mCurrentEvent = new PeerIndexEvent(mTaskWrapper.getKey(), mM3U8Option.getJumpIndex());
|
||||
resumeTask();
|
||||
return;
|
||||
}
|
||||
// 设置需要下载的切片
|
||||
mCompleteNum = 0;
|
||||
for (ThreadRecord tr : mRecord.threadRecords) {
|
||||
if (!tr.isComplete) {
|
||||
mAfterPeer.put(tr.threadId, tr);
|
||||
} else {
|
||||
mCompleteNum++;
|
||||
}
|
||||
}
|
||||
mManager.updateStateCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* 每隔几秒钟检查jump队列,取最新的事件处理
|
||||
*/
|
||||
private synchronized void startJumpThread() {
|
||||
jumpThread = new Thread(new Runnable() {
|
||||
@Override public void run() {
|
||||
try {
|
||||
PeerIndexEvent event;
|
||||
while (!isBreak()) {
|
||||
try {
|
||||
EVENT_LOCK.lock();
|
||||
PeerIndexEvent temp = null;
|
||||
// 取最新的事件
|
||||
while ((event = mJumpQueue.poll(1, TimeUnit.SECONDS)) != null) {
|
||||
temp = event;
|
||||
}
|
||||
|
||||
if (temp != null) {
|
||||
handleJump(temp);
|
||||
}
|
||||
mEventQueueCondition.await();
|
||||
} finally {
|
||||
EVENT_LOCK.unlock();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
jumpThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理跳转
|
||||
*/
|
||||
private void handleJump(PeerIndexEvent event) {
|
||||
if (isBreak()) {
|
||||
ALog.e(TAG, "任务已停止,处理跳转失败");
|
||||
return;
|
||||
}
|
||||
mCurrentEvent = event;
|
||||
if (mRecord == null || mRecord.threadRecords == null) {
|
||||
ALog.e(TAG, "跳到指定位置失败,记录为空");
|
||||
return;
|
||||
}
|
||||
if (event.peerIndex >= mRecord.threadRecords.size()) {
|
||||
ALog.e(TAG,
|
||||
String.format("切片索引设置错误,切片最大索引为:%s,当前设置的索引为:%s", mRecord.threadRecords.size(),
|
||||
event.peerIndex));
|
||||
return;
|
||||
}
|
||||
ALog.i(TAG, String.format("将优先下载索引【%s】之后的切片", event.peerIndex));
|
||||
|
||||
isJump = true;
|
||||
notifyWaitLock(false);
|
||||
mCurrentFlagSize = mFlagQueue.size();
|
||||
// 停止所有正在执行的线程任务
|
||||
try {
|
||||
TempFlag flag;
|
||||
while ((flag = mFlagQueue.poll()) != null) {
|
||||
flag.threadTask.stop();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
ALog.d(TAG, "完成停止队列中的切片任务");
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载指定索引后面的切片
|
||||
* 如果指定的切片索引大于切片总数,则此操作无效
|
||||
* 如果指定的切片索引小于当前正在下载的切片索引,并且指定索引和当前索引区间内有未下载的切片,则优先下载该区间的切片;否则此操作无效
|
||||
* 如果指定索引后的切片已经全部下载完成,但是索引前有未下载的切片,间会自动下载未下载的切片
|
||||
*/
|
||||
@Event
|
||||
public synchronized void jumpPeer(PeerIndexEvent event) {
|
||||
if (!event.key.equals(mTaskWrapper.getKey())) {
|
||||
return;
|
||||
}
|
||||
if (isBreak()) {
|
||||
ALog.e(TAG, "任务已停止,发送跳转事件失败");
|
||||
return;
|
||||
}
|
||||
if (jumpThread == null) {
|
||||
mJumpThreadPool = Executors.newSingleThreadExecutor();
|
||||
startJumpThread();
|
||||
}
|
||||
mJumpQueue.offer(event);
|
||||
mJumpThreadPool.submit(new Runnable() {
|
||||
@Override public void run() {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
notifyJumpQueue();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void notifyJumpQueue() {
|
||||
try {
|
||||
EVENT_LOCK.lock();
|
||||
mEventQueueCondition.signalAll();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
EVENT_LOCK.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重指定位置恢复任务
|
||||
*/
|
||||
private synchronized void resumeTask() {
|
||||
if (isBreak()) {
|
||||
ALog.e(TAG, "任务已停止,恢复任务失败");
|
||||
return;
|
||||
}
|
||||
if (mJumpQueue.size() > 0) {
|
||||
ALog.d(TAG, "有新定位,取消上一次操作");
|
||||
notifyJumpQueue();
|
||||
return;
|
||||
}
|
||||
ALog.d(TAG, "恢复切片任务");
|
||||
// 重新初始化需要下载的分片
|
||||
mBeforePeer.clear();
|
||||
mAfterPeer.clear();
|
||||
mFlagQueue.clear();
|
||||
aIndex = 0;
|
||||
bIndex = 0;
|
||||
mCompleteNum = 0;
|
||||
for (ThreadRecord tr : mRecord.threadRecords) {
|
||||
if (tr.isComplete) {
|
||||
mCompleteNum++;
|
||||
continue;
|
||||
}
|
||||
if (tr.threadId < mCurrentEvent.peerIndex) {
|
||||
mBeforePeer.put(tr.threadId, tr);
|
||||
} else {
|
||||
mAfterPeer.put(tr.threadId, tr);
|
||||
}
|
||||
}
|
||||
|
||||
ALog.i(TAG,
|
||||
String.format("beforeSize = %s, afterSize = %s, mCompleteNum = %s", mBeforePeer.size(),
|
||||
mAfterPeer.size(), mCompleteNum));
|
||||
ALog.i(TAG, String.format("完成处理数据的操作,将优先下载【%s】之后的切片", mCurrentEvent.peerIndex));
|
||||
mManager.updateStateCount();
|
||||
|
||||
try {
|
||||
JUMP_LOCK.lock();
|
||||
mJumpCondition.signalAll();
|
||||
} finally {
|
||||
JUMP_LOCK.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private M3U8Listener getListener() {
|
||||
return (M3U8Listener) mListener;
|
||||
}
|
||||
|
||||
private void notifyWaitLock(boolean isComplete) {
|
||||
try {
|
||||
LOCK.lock();
|
||||
if (isComplete) {
|
||||
TempFlag flag = mFlagQueue.poll(1, TimeUnit.SECONDS);
|
||||
if (flag != null) {
|
||||
ALog.d(TAG, String.format("切片【%s】完成", flag.threadId));
|
||||
}
|
||||
}
|
||||
mWaitCondition.signalAll();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
LOCK.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动线程任务
|
||||
*
|
||||
* @return 线程唯一id标志
|
||||
*/
|
||||
private TempFlag startThreadTask(ThreadTask task, int peerIndex) {
|
||||
if (isBreak()) {
|
||||
ALog.w(TAG, "任务已停止,启动线程任务失败");
|
||||
return null;
|
||||
}
|
||||
ThreadTaskManager.getInstance().startThread(mTaskWrapper.getKey(), task);
|
||||
getListener().onPeerStart(mTaskWrapper.getKey(), task.getConfig().tempFile.getPath(),
|
||||
peerIndex);
|
||||
TempFlag flag = new TempFlag();
|
||||
flag.threadTask = task;
|
||||
flag.threadId = peerIndex;
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置config
|
||||
*/
|
||||
private ThreadTask createThreadTask(String cacheDir, ThreadRecord record, int index) {
|
||||
SubThreadConfig config = new SubThreadConfig();
|
||||
config.url = record.tsUrl;
|
||||
config.tempFile = new File(BaseM3U8Loader.getTsFilePath(cacheDir, record.threadId));
|
||||
config.isBlock = mRecord.isBlock;
|
||||
config.isOpenDynamicFile = mRecord.isOpenDynamicFile;
|
||||
config.taskWrapper = mTaskWrapper;
|
||||
config.record = record;
|
||||
config.stateHandler = mStateHandler;
|
||||
config.peerIndex = index;
|
||||
if (!config.tempFile.exists()) {
|
||||
FileUtil.createFile(config.tempFile.getPath());
|
||||
}
|
||||
ThreadTask threadTask = new ThreadTask(config);
|
||||
M3U8ThreadTaskAdapter adapter = new M3U8ThreadTaskAdapter(config);
|
||||
threadTask.setAdapter(adapter);
|
||||
return threadTask;
|
||||
}
|
||||
|
||||
/**
|
||||
* M3U8线程状态管理
|
||||
*/
|
||||
private class VodStateManager implements IThreadState {
|
||||
private final String TAG = "M3U8ThreadStateManager";
|
||||
|
||||
/**
|
||||
* 任务状态回调
|
||||
*/
|
||||
private IEventListener listener;
|
||||
private int startThreadNum; // 启动的线程总数
|
||||
private int cancelNum = 0; // 已经取消的线程的数
|
||||
private int stopNum = 0; // 已经停止的线程数
|
||||
private int failNum = 0; // 失败的线程数
|
||||
private long progress; //当前总进度
|
||||
private TaskRecord taskRecord; // 任务记录
|
||||
private Looper looper;
|
||||
|
||||
/**
|
||||
* @param taskRecord 任务记录
|
||||
* @param listener 任务事件
|
||||
*/
|
||||
VodStateManager(Looper looper, TaskRecord taskRecord, IEventListener listener) {
|
||||
this.looper = looper;
|
||||
this.taskRecord = taskRecord;
|
||||
for (ThreadRecord record : taskRecord.threadRecords) {
|
||||
if (!record.isComplete) {
|
||||
startThreadNum++;
|
||||
}
|
||||
}
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
private void updateStateCount() {
|
||||
cancelNum = 0;
|
||||
stopNum = 0;
|
||||
failNum = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出looper循环
|
||||
*/
|
||||
private void quitLooper() {
|
||||
ALog.d(TAG, "quitLooper");
|
||||
looper.quit();
|
||||
}
|
||||
|
||||
@Override public boolean handleMessage(Message msg) {
|
||||
int peerIndex = msg.getData().getInt(ISchedulers.DATA_M3U8_PEER_INDEX);
|
||||
switch (msg.what) {
|
||||
case STATE_STOP:
|
||||
stopNum++;
|
||||
removeSignThread((ThreadTask) msg.obj);
|
||||
// 处理跳转位置后,恢复任务
|
||||
if (isJump && (stopNum == mCurrentFlagSize || mCurrentFlagSize == 0) && !isBreak()) {
|
||||
resumeTask();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isBreak()) {
|
||||
ALog.d(TAG, String.format("vod任务【%s】停止", mTempFile.getName()));
|
||||
quitLooper();
|
||||
}
|
||||
break;
|
||||
case STATE_CANCEL:
|
||||
cancelNum++;
|
||||
removeSignThread((ThreadTask) msg.obj);
|
||||
|
||||
if (isBreak()) {
|
||||
ALog.d(TAG, String.format("vod任务【%s】取消", mTempFile.getName()));
|
||||
quitLooper();
|
||||
}
|
||||
break;
|
||||
case STATE_FAIL:
|
||||
failNum++;
|
||||
for (ThreadRecord tr : mRecord.threadRecords) {
|
||||
if (tr.threadId == peerIndex) {
|
||||
mBeforePeer.put(peerIndex, tr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getListener().onPeerFail(mTaskWrapper.getKey(),
|
||||
msg.getData().getString(ISchedulers.DATA_M3U8_PEER_PATH), peerIndex);
|
||||
if (isFail()) {
|
||||
ALog.d(TAG, String.format("vod任务【%s】失败", mTempFile.getName()));
|
||||
Bundle b = msg.getData();
|
||||
listener.onFail(b.getBoolean(KEY_RETRY, true),
|
||||
(BaseException) b.getSerializable(KEY_ERROR_INFO));
|
||||
quitLooper();
|
||||
}
|
||||
break;
|
||||
case STATE_COMPLETE:
|
||||
if (isBreak()) {
|
||||
quitLooper();
|
||||
}
|
||||
mCompleteNum++;
|
||||
// 正在切换位置时,切片完成,队列减小
|
||||
if (isJump) {
|
||||
mCurrentFlagSize--;
|
||||
if (mCurrentFlagSize < 0) {
|
||||
mCurrentFlagSize = 0;
|
||||
}
|
||||
}
|
||||
|
||||
removeSignThread((ThreadTask) msg.obj);
|
||||
getListener().onPeerComplete(mTaskWrapper.getKey(),
|
||||
msg.getData().getString(ISchedulers.DATA_M3U8_PEER_PATH), peerIndex);
|
||||
handlerPercent();
|
||||
if (!isJump) {
|
||||
notifyWaitLock(true);
|
||||
}
|
||||
if (isComplete()) {
|
||||
ALog.d(TAG, String.format(
|
||||
"startThreadNum = %s, stopNum = %s, cancelNum = %s, failNum = %s, completeNum = %s, flagQueueSize = %s",
|
||||
startThreadNum, stopNum, cancelNum, failNum, mCompleteNum, mFlagQueue.size()));
|
||||
ALog.d(TAG, String.format("vod任务【%s】完成", mTempFile.getName()));
|
||||
if (mM3U8Option.isMergeFile()) {
|
||||
if (mergeFile()) {
|
||||
listener.onComplete();
|
||||
} else {
|
||||
listener.onFail(false, null);
|
||||
}
|
||||
} else {
|
||||
listener.onComplete();
|
||||
}
|
||||
quitLooper();
|
||||
}
|
||||
break;
|
||||
case STATE_RUNNING:
|
||||
progress += (long) msg.obj;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void removeSignThread(ThreadTask threadTask) {
|
||||
int index = getTaskList().indexOfValue(threadTask);
|
||||
if (index != -1) {
|
||||
getTaskList().removeAt(index);
|
||||
}
|
||||
ThreadTaskManager.getInstance().removeSingleTaskThread(mTaskWrapper.getKey(), threadTask);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置进度
|
||||
*/
|
||||
private void handlerPercent() {
|
||||
int completeNum = mM3U8Option.getCompleteNum();
|
||||
completeNum++;
|
||||
mM3U8Option.setCompleteNum(completeNum);
|
||||
int percent = completeNum * 100 / taskRecord.threadRecords.size();
|
||||
getEntity().setPercent(percent);
|
||||
getEntity().update();
|
||||
}
|
||||
|
||||
@Override public boolean isFail() {
|
||||
printInfo("isFail");
|
||||
return failNum != 0 && failNum == mFlagQueue.size() && !isJump;
|
||||
}
|
||||
|
||||
@Override public boolean isComplete() {
|
||||
return mCompleteNum == taskRecord.threadRecords.size() && !isJump;
|
||||
}
|
||||
|
||||
@Override public long getCurrentProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
private void printInfo(String tag) {
|
||||
if (false) {
|
||||
ALog.d(tag, String.format(
|
||||
"startThreadNum = %s, stopNum = %s, cancelNum = %s, failNum = %s, completeNum = %s, flagQueueSize = %s",
|
||||
startThreadNum, stopNum, cancelNum, failNum, mCompleteNum, mFlagQueue.size()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并文件
|
||||
*
|
||||
* @return {@code true} 合并成功,{@code false}合并失败
|
||||
*/
|
||||
private boolean mergeFile() {
|
||||
if (getEntity().getM3U8Entity().isGenerateIndexFile()) {
|
||||
return generateIndexFile();
|
||||
}
|
||||
ITsMergeHandler mergeHandler = mM3U8Option.getMergeHandler();
|
||||
String cacheDir = getCacheDir();
|
||||
List<String> partPath = new ArrayList<>();
|
||||
for (ThreadRecord tr : taskRecord.threadRecords) {
|
||||
partPath.add(BaseM3U8Loader.getTsFilePath(cacheDir, tr.threadId));
|
||||
}
|
||||
boolean isSuccess;
|
||||
if (mergeHandler != null) {
|
||||
isSuccess = mergeHandler.merge(getEntity().getM3U8Entity(), partPath);
|
||||
|
||||
if (mergeHandler.getClass().isAnonymousClass()) {
|
||||
mM3U8Option.setMergeHandler(null);
|
||||
}
|
||||
} else {
|
||||
isSuccess = FileUtil.mergeFile(taskRecord.filePath, partPath);
|
||||
}
|
||||
if (isSuccess) {
|
||||
// 合并成功,删除缓存文件
|
||||
File[] files = new File(cacheDir).listFiles();
|
||||
for (File f : files) {
|
||||
if (f.exists()) {
|
||||
f.delete();
|
||||
}
|
||||
}
|
||||
File cDir = new File(cacheDir);
|
||||
if (cDir.exists()) {
|
||||
cDir.delete();
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
ALog.e(TAG, "合并失败");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class TempFlag {
|
||||
ThreadTask threadTask;
|
||||
int threadId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.arialyy.aria.m3u8.vod;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import com.arialyy.aria.core.common.AbsEntity;
|
||||
import com.arialyy.aria.core.common.CompleteInfo;
|
||||
import com.arialyy.aria.core.download.DTaskWrapper;
|
||||
import com.arialyy.aria.core.download.DownloadEntity;
|
||||
import com.arialyy.aria.core.processor.IVodTsUrlConverter;
|
||||
import com.arialyy.aria.core.inf.OnFileInfoCallback;
|
||||
import com.arialyy.aria.core.loader.AbsLoader;
|
||||
import com.arialyy.aria.core.loader.AbsNormalLoaderUtil;
|
||||
import com.arialyy.aria.exception.BaseException;
|
||||
import com.arialyy.aria.exception.M3U8Exception;
|
||||
import com.arialyy.aria.m3u8.M3U8InfoThread;
|
||||
import com.arialyy.aria.m3u8.M3U8Listener;
|
||||
import com.arialyy.aria.m3u8.M3U8TaskOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* M3U8点播文件下载工具
|
||||
* 工作流程:
|
||||
* 1、创建一个和文件同父路径并且同名隐藏文件夹
|
||||
* 2、将所有m3u8的ts文件下载到该文件夹中
|
||||
* 3、完成所有分片下载后,合并ts文件
|
||||
* 4、删除该隐藏文件夹
|
||||
*/
|
||||
public class M3U8VodUtil extends AbsNormalLoaderUtil {
|
||||
|
||||
private M3U8Listener mListener;
|
||||
private List<String> mUrls = new ArrayList<>();
|
||||
private M3U8TaskOption mM3U8Option;
|
||||
|
||||
public M3U8VodUtil(DTaskWrapper wrapper, M3U8Listener listener) {
|
||||
super(wrapper, listener);
|
||||
wrapper.generateM3u8Option(M3U8TaskOption.class);
|
||||
mListener = listener;
|
||||
mM3U8Option = (M3U8TaskOption) wrapper.getM3u8Option();
|
||||
}
|
||||
|
||||
@Override protected AbsLoader createLoader() {
|
||||
return new M3U8VodLoader((M3U8Listener) getListener(), (DTaskWrapper) getTaskWrapper());
|
||||
}
|
||||
|
||||
@Override protected Runnable createInfoThread() {
|
||||
return new M3U8InfoThread((DTaskWrapper) getTaskWrapper(), new OnFileInfoCallback() {
|
||||
@Override public void onComplete(String key, CompleteInfo info) {
|
||||
IVodTsUrlConverter converter = mM3U8Option.getVodUrlConverter();
|
||||
if (converter != null) {
|
||||
if (TextUtils.isEmpty(mM3U8Option.getBandWidthUrl())) {
|
||||
mUrls.addAll(converter.convert(getEntity().getUrl(), (List<String>) info.obj));
|
||||
} else {
|
||||
mUrls.addAll(
|
||||
converter.convert(mM3U8Option.getBandWidthUrl(), (List<String>) info.obj));
|
||||
}
|
||||
} else {
|
||||
mUrls.addAll((Collection<? extends String>) info.obj);
|
||||
}
|
||||
if (mUrls.isEmpty()) {
|
||||
fail(new M3U8Exception(TAG, "获取地址失败"), false);
|
||||
return;
|
||||
} else if (!mUrls.get(0).startsWith("http")) {
|
||||
fail(new M3U8Exception(TAG, "地址错误,请使用IM3U8UrlExtInfHandler处理你的url信息"), false);
|
||||
return;
|
||||
}
|
||||
mM3U8Option.setUrls(mUrls);
|
||||
if (isStop()) {
|
||||
getListener().onStop(getEntity().getCurrentProgress());
|
||||
} else if (isCancel()) {
|
||||
getListener().onCancel();
|
||||
} else {
|
||||
getLoader().start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void onFail(AbsEntity entity, BaseException e, boolean needRetry) {
|
||||
fail(e, needRetry);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private DownloadEntity getEntity() {
|
||||
return (DownloadEntity) getTaskWrapper().getEntity();
|
||||
}
|
||||
}
|
||||
3
M3U8Component/src/main/res/values/strings.xml
Normal file
3
M3U8Component/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">M3U8Component</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user