组件化功能基本完成

This commit is contained in:
laoyuyu
2019-09-29 20:07:21 +08:00
parent 7ee1347ab4
commit 2a92321785
337 changed files with 5186 additions and 3250 deletions

View File

@@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.arialyy.aria.m3u8" />

View File

@@ -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();
}
}

View File

@@ -0,0 +1,80 @@
package com.arialyy.aria.m3u8;
public class IdGenerator {
/**
* SnowFlake算法 64位Long类型生成唯一ID 第一位0表明正数 2-4241位表示毫秒时间戳差值起始值自定义
* 43-5210位机器编号5位数据中心编号5位进程编号 53-6412位毫秒内计数器 本机内存生成,性能高
* <p>
* 主要就是三部分: 时间戳进程id序列号 时间戳41id10位序列号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;
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">M3U8Component</string>
</resources>