源代码备份

This commit is contained in:
TC999
2024-08-20 16:54:35 +08:00
parent c4db18da39
commit e2a5f92e23
791 changed files with 90314 additions and 2 deletions

1
M3U8Component/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,13 @@
apply plugin: 'com.novoda.bintray-release'
publish {
// artifactId = 'aria-compiler'
// uploadName = 'AriaCompiler'
artifactId = 'm3u8Component'
uploadName = 'M3U8Component'
userOrg = rootProject.ext.userOrg
groupId = rootProject.ext.groupId
publishVersion = rootProject.ext.publishVersion
desc = rootProject.ext.desc
website = rootProject.ext.website
licences = rootProject.ext.licences
}

View File

@ -0,0 +1,43 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
debug{
debuggable true
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
lintOptions {
abortOnError false
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(path: ':HttpComponent')
implementation project(path: ':PublicComponent')
}
//apply from: 'bintray-release.gradle'
ext{
PUBLISH_ARTIFACT_ID = 'm3u8'
}
apply from: '../gradle/mavenCentral-release.gradle'

View File

21
M3U8Component/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

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,29 @@
/*
* 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;
/**
* 点播文件默认的码率转换器
*/
class BandWidthDefConverter implements IBandWidthUrlConverter {
@Override public String convert(String m3u8Url, String bandWidthUrl) {
int index = m3u8Url.lastIndexOf("/");
return m3u8Url.substring(0, index + 1) + bandWidthUrl;
}
}

View File

@ -0,0 +1,155 @@
/*
* 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.text.TextUtils;
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.listener.IEventListener;
import com.arialyy.aria.core.loader.AbsNormalLoader;
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 AbsNormalLoader<DTaskWrapper> {
protected M3U8TaskOption mM3U8Option;
public BaseM3U8Loader(DTaskWrapper wrapper, IEventListener listener) {
super(wrapper, listener);
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);
}
public String getCacheDir() {
String cacheDir = mM3U8Option.getCacheDir();
if (TextUtils.isEmpty(cacheDir)) {
cacheDir = FileUtil.getTsCacheDir(getEntity().getFilePath(), mM3U8Option.getBandWidth());
}
if (!new File(cacheDir).exists()) {
FileUtil.createDir(cacheDir);
}
return cacheDir;
}
/**
* 创建索引文件
*/
public boolean generateIndexFile(boolean isLive) {
File tempFile =
new File(String.format(M3U8InfoTask.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")) {
fos.write(line.concat("\r\n").getBytes(Charset.forName("UTF-8")));
String tsPath = getTsFilePath(cacheDir, mRecord.threadRecords.get(i).threadId);
bytes = tsPath.concat("\r\n").getBytes(Charset.forName("UTF-8"));
reader.readLine(); // 继续读一行,避免写入源索引文件的切片地址
i++;
} else if (line.startsWith("#EXT-X-KEY")) {
M3U8Entity m3U8Entity = getEntity().getM3U8Entity();
StringBuilder sb = new StringBuilder("#EXT-X-KEY:");
sb.append("METHOD=").append(m3U8Entity.method);
sb.append(",URI=\"").append(m3U8Entity.keyPath).append("\"");
if (!TextUtils.isEmpty(m3U8Entity.iv)) {
sb.append(",IV=").append(m3U8Entity.iv);
}
if (!TextUtils.isEmpty(m3U8Entity.keyFormat)) {
sb.append(",KEYFORMAT=\"").append(m3U8Entity.keyFormat).append("\"");
sb.append(",KEYFORMATVERSIONS=\"")
.append(TextUtils.isEmpty(m3U8Entity.keyFormatVersion) ? "1"
: m3U8Entity.keyFormatVersion)
.append("\"");
}
sb.append("\r\n");
bytes = sb.toString().getBytes(Charset.forName("UTF-8"));
} else {
bytes = line.concat("\r\n").getBytes(Charset.forName("UTF-8"));
}
fos.write(bytes, 0, bytes.length);
}
// 直播的索引文件需要在结束的时候才写入结束标志
if (isLive) {
fos.write("#EXT-X-ENDLIST".concat("\r\n").getBytes(Charset.forName("UTF-8")));
}
fos.flush();
return true;
} 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 getCurrentProgress() {
return isRunning() ? getStateManager().getCurrentProgress() : getEntity().getCurrentProgress();
}
protected DownloadEntity getEntity() {
return mTaskWrapper.getEntity();
}
}

View File

@ -0,0 +1,77 @@
package com.arialyy.aria.m3u8;
public final class IdGenerator {
/**
* SnowFlake算法 64位Long类型生成唯一ID 第一位0表明正数 2-4241位表示毫秒时间戳差值起始值自定义
* 43-5210位机器编号5位数据中心编号5位进程编号 53-6412位毫秒内计数器 本机内存生成,性能高
* <p>
* 主要就是三部分: 时间戳进程id序列号 时间戳41id10位序列号12位
*
* @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,437 @@
/*
* 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.loader.IInfoTask;
import com.arialyy.aria.core.loader.ILoaderVisitor;
import com.arialyy.aria.core.processor.IBandWidthUrlConverter;
import com.arialyy.aria.core.processor.IKeyUrlConverter;
import com.arialyy.aria.core.wrapper.AbsTaskWrapper;
import com.arialyy.aria.core.wrapper.ITaskWrapper;
import com.arialyy.aria.exception.AriaM3U8Exception;
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.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://tools.ietf.org/html/rfc8216
* https://www.cnblogs.com/renhui/p/10351870.html
* https://blog.csdn.net/Guofengpu/article/details/54922865
*/
final public class M3U8InfoTask implements IInfoTask {
public static final String M3U8_INDEX_FORMAT = "%s.index";
private final String TAG = "M3U8InfoThread";
private DownloadEntity mEntity;
private DTaskWrapper mTaskWrapper;
private int mConnectTimeOut;
private OnGetLivePeerCallback onGetPeerCallback;
private HttpTaskOption mHttpOption;
private M3U8TaskOption mM3U8Option;
private Callback mCallback;
/**
* 是否停止获取切片信息,{@code true}停止获取切片信息
*/
private boolean isStop = false;
@Override public void accept(ILoaderVisitor visitor) {
visitor.addComponent(this);
}
public interface OnGetLivePeerCallback {
void onGetPeer(String url, String extInf);
}
public M3U8InfoTask(DTaskWrapper taskWrapper) {
this.mTaskWrapper = taskWrapper;
mEntity = taskWrapper.getEntity();
mConnectTimeOut = AriaConfig.getInstance().getDConfig().getConnectTimeOut();
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(mEntity.getUrl(), conn);
} catch (IOException e) {
failDownload(e.getMessage(), false);
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
@Override public void setCallback(Callback callback) {
mCallback = callback;
}
@Override public void stop() {
this.isStop = true;
}
@Override public void cancel() {
this.isStop = true;
}
private void handleConnect(String tsListUrl, HttpURLConnection conn) throws IOException {
int code = conn.getResponseCode();
if (code == HttpURLConnection.HTTP_OK) {
BufferedReader reader =
new BufferedReader(new InputStreamReader(ConnectionHelp.convertInputStream(conn)));
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 =
((M3U8TaskOption) mTaskWrapper.getM3u8Option()).isGenerateIndexFile();
// 写入索引信息的流
FileOutputStream fos = null;
if (isGenerateIndexFile) {
String indexPath = String.format(M3U8_INDEX_FORMAT, mEntity.getFilePath());
File indexFile = new File(indexPath);
if (!indexFile.exists()) {
FileUtil.createFile(indexPath);
} else {
//FileUtil.deleteFile(indexPath);
}
fos = new FileOutputStream(indexFile);
ALog.d(TAG, line);
addIndexInfo(true, fos, line);
}
while ((line = reader.readLine()) != null) {
if (isStop) {
break;
}
ALog.d(TAG, line);
if (line.startsWith("#EXT-X-ENDLIST")) {
// 点播文件的下载写入结束标志,直播文件的下载在停止时才写入结束标志
addIndexInfo(isGenerateIndexFile && !isLive, fos, line);
break;
}
if (line.startsWith("#EXTINF")) {
String url = reader.readLine();
if (isLive) {
if (onGetPeerCallback != null) {
onGetPeerCallback.onGetPeer(url, line);
}
} else {
extInf.add(url);
}
ALog.d(TAG, url);
addIndexInfo(isGenerateIndexFile && !isLive, fos, line);
addIndexInfo(isGenerateIndexFile && !isLive, fos, url);
continue;
}
if (line.startsWith("#EXT-X-STREAM-INF")) {
addIndexInfo(isGenerateIndexFile, fos, line);
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】码率不存在", setBand), false);
}
return;
}
if (line.startsWith("#EXT-X-KEY")) {
addIndexInfo(isGenerateIndexFile, fos, line);
getKeyInfo(tsListUrl, line);
continue;
}
addIndexInfo(isGenerateIndexFile, fos, 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;
onSucceed(info);
if (fos != null) {
fos.close();
}
} 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_BAD_REQUEST) {
failDownload("下载失败错误,错误码:" + code, false);
} else {
failDownload(String.format("不支持的响应code: %s", code), true);
}
}
private void onSucceed(CompleteInfo info) {
if (isStop) {
return;
}
mCallback.onSucceed(mEntity.getKey(), info);
}
/**
* 添加切片信息到索引文件中
* 直播下载的索引只记录头部信息不记录EXTINF中的信息该信息在onGetPeer的方法中添加。
* 点播下载记录所有信息
*
* @param write true 将信息写入文件
* @param info 切片信息
*/
private void addIndexInfo(boolean write, FileOutputStream fos, String info)
throws IOException {
if (!write) {
return;
}
fos.write(info.concat("\r\n").getBytes(Charset.forName("UTF-8")));
}
/**
* 是否停止获取切片信息,{@code true}停止获取切片信息
*/
public void setStop(boolean isStop) {
this.isStop = isStop;
}
/**
* 直播切片信息获取回调
*/
public void setOnGetPeerCallback(OnGetLivePeerCallback peerCallback) {
onGetPeerCallback = peerCallback;
}
/**
* 获取加密的密钥信息
*/
private void getKeyInfo(String tsListUrl, 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("\"", "");
String keyPath;
if (((M3U8TaskOption) mTaskWrapper.getM3u8Option()).getKeyPath() == null) {
keyPath = new File(mEntity.getFilePath()).getParent() + "/"
+ CommonUtil.getStrMd5(m3U8Entity.keyUrl) + ".key";
} else {
keyPath = ((M3U8TaskOption) mTaskWrapper.getM3u8Option()).getKeyPath();
}
m3U8Entity.keyPath = keyPath;
} else if (param.startsWith("IV")) {
m3U8Entity.iv = param.split("=")[1];
} else if (param.startsWith("KEYFORMAT")) {
m3U8Entity.keyFormat = param.split("=")[1];
} else if (param.startsWith("KEYFORMATVERSIONS")) {
m3U8Entity.keyFormatVersion = param.split("=")[1];
}
}
downloadKey(tsListUrl, 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 (mCallback != null) {
mCallback.onFail(mEntity, new AriaM3U8Exception("获取重定向链接失败"), false);
}
return;
}
if (newUrl.startsWith("/")) {
Uri uri = Uri.parse(mEntity.getUrl());
newUrl = uri.getHost() + newUrl;
}
if (!CheckUtil.checkUrl(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(newUrl, conn);
conn.disconnect();
}
/**
* 处理码率
*/
private void handleBandWidth(HttpURLConnection conn, String bandWidthM3u8Url) throws IOException {
IBandWidthUrlConverter converter = mM3U8Option.isUseDefConvert() ? new BandWidthDefConverter()
: mM3U8Option.getBandWidthUrlConverter();
if (converter != null) {
bandWidthM3u8Url = converter.convert(mEntity.getUrl(), 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(bandWidthM3u8Url, conn);
conn.disconnect();
}
private void failDownload(String errorInfo, boolean needRetry) {
if (isStop) {
return;
}
mCallback.onFail(mEntity, new AriaM3U8Exception(errorInfo), needRetry);
}
/**
* 密钥不存在,下载密钥
*/
private void downloadKey(String tsListUr, M3U8Entity info) {
HttpURLConnection conn = null;
FileOutputStream fos = null;
try {
File keyF = new File(info.keyPath);
if (!keyF.exists()) {
ALog.d(TAG, "密钥不存在,下载密钥");
FileUtil.createFile(keyF);
} else {
return;
}
IKeyUrlConverter keyUrlConverter = mM3U8Option.getKeyUrlConverter();
String keyUrl = info.keyUrl;
if (keyUrlConverter != null) {
keyUrl = keyUrlConverter.convert(mEntity.getUrl(), tsListUr, keyUrl);
}
if (TextUtils.isEmpty(keyUrl)) {
ALog.e(TAG, "m3u8密钥key url 为空");
return;
}
URL url = ConnectionHelp.handleUrl(keyUrl, mHttpOption);
conn = ConnectionHelp.handleConnection(url, mHttpOption);
ConnectionHelp.setConnectParam(mHttpOption, conn);
conn.setConnectTimeout(mConnectTimeOut);
conn.connect();
InputStream is = ConnectionHelp.convertInputStream(conn);
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,91 @@
/*
* 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.Message;
import com.arialyy.aria.core.inf.IEntity;
import com.arialyy.aria.core.inf.TaskSchedulerType;
import com.arialyy.aria.core.listener.BaseListener;
import com.arialyy.aria.core.listener.IDLoadListener;
import com.arialyy.aria.core.listener.ISchedulers;
import com.arialyy.aria.core.task.DownloadTask;
import com.arialyy.aria.util.CommonUtil;
import com.arialyy.aria.util.DeleteM3u8Record;
/**
* 下载监听类
*/
public final class M3U8Listener extends BaseListener implements IDLoadListener {
@Override
public void onPostPre(long fileSize) {
mEntity.setFileSize(fileSize);
mEntity.setConvertFileSize(CommonUtil.formatFileSize(fileSize));
saveData(IEntity.STATE_POST_PRE, -1);
sendInState2Target(ISchedulers.POST_PRE);
}
@Override public void supportBreakpoint(boolean support) {
if (!support) {
sendInState2Target(ISchedulers.NO_SUPPORT_BREAK_POINT);
}
}
/**
* 切片开始下载
*/
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();
}
@Override protected void handleCancel() {
int sType = getTask(DownloadTask.class).getSchedulerType();
if (sType == TaskSchedulerType.TYPE_CANCEL_AND_NOT_NOTIFY) {
mEntity.setComplete(false);
mEntity.setState(IEntity.STATE_WAIT);
DeleteM3u8Record.getInstance().deleteRecord(mEntity, mTaskWrapper.isRemoveFile(), false);
} else {
DeleteM3u8Record.getInstance().deleteRecord(mEntity, mTaskWrapper.isRemoveFile(), true);
}
}
}

View File

@ -0,0 +1,275 @@
/*
* 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.inf.ITaskOption;
import com.arialyy.aria.core.processor.IBandWidthUrlConverter;
import com.arialyy.aria.core.processor.IKeyUrlConverter;
import com.arialyy.aria.core.processor.ILiveTsUrlConverter;
import com.arialyy.aria.core.processor.ITsMergeHandler;
import com.arialyy.aria.core.processor.IVodTsUrlConverter;
import java.lang.ref.SoftReference;
import java.util.List;
/**
* m3u8任务配信息
*/
public final 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 generateIndexFile = false;
/**
* 加密密钥的解密处理器
*/
private SoftReference<IKeyUrlConverter> keyUrlConverter;
/**
* 忽略下载失败的ts切片。
* true即使有失败的切片下载完成后也要合并所有切片并进入complete回调
*/
private boolean ignoreFailureTs = false;
/**
* 密钥文件保存路径
*/
private String keyPath;
/**
* 是否使用默认的码率转换器和Ts转换器
*/
private boolean useDefConvert = false;
public boolean isUseDefConvert() {
return useDefConvert;
}
public void setUseDefConvert(boolean useDefConvert) {
this.useDefConvert = useDefConvert;
}
public String getKeyPath() {
return keyPath;
}
public boolean isIgnoreFailureTs() {
return ignoreFailureTs;
}
public void setIgnoreFailureTs(boolean ignoreFailureTs) {
this.ignoreFailureTs = ignoreFailureTs;
}
public IKeyUrlConverter getKeyUrlConverter() {
return keyUrlConverter == null ? null : keyUrlConverter.get();
}
public void setKeyUrlConverter(IKeyUrlConverter keyUrlConverter) {
this.keyUrlConverter = new SoftReference<>(keyUrlConverter);
}
public boolean isGenerateIndexFile() {
return generateIndexFile;
}
public void setGenerateIndexFile(boolean generateIndexFile) {
this.generateIndexFile = generateIndexFile;
}
public int getJumpIndex() {
return jumpIndex;
}
public void setJumpIndex(int jumpIndex) {
this.jumpIndex = jumpIndex;
}
public int getMaxTsQueueNum() {
return maxTsQueueNum == 0 ? 4 : maxTsQueueNum;
}
public void setMaxTsQueueNum(int maxTsQueueNum) {
this.maxTsQueueNum = maxTsQueueNum;
}
public long getLiveUpdateInterval() {
return liveUpdateInterval == 0 ? 10 * 1000 : 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,278 @@
/*
* 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.Uri;
import android.text.TextUtils;
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.AriaM3U8Exception;
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 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 final class M3U8ThreadTaskAdapter extends AbsThreadTaskAdapter {
private final String TAG = CommonUtil.getClassName(this);
private HttpTaskOption mHttpTaskOption;
private BufferedInputStream is = null;
public M3U8ThreadTaskAdapter(SubThreadConfig config) {
super(config);
mHttpTaskOption = (HttpTaskOption) getTaskWrapper().getTaskOption();
}
@Override protected void handlerThreadTask() {
if (getThreadRecord().isComplete) {
handleComplete();
return;
}
HttpURLConnection conn = null;
try {
URL url = ConnectionHelp.handleUrl(getThreadConfig().url, mHttpTaskOption);
conn = ConnectionHelp.handleConnection(url, mHttpTaskOption);
ALog.d(TAG, String.format("分片【%s】开始下载", getThreadRecord().threadId));
if (mHttpTaskOption.isChunked()) {
conn.setDoInput(true);
conn.setChunkedStreamingMode(0);
}
// 传递参数
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();
}
}
handleConn(conn);
} catch (MalformedURLException e) {
fail(new AriaM3U8Exception(
String.format("分片【%s】下载失败filePath: %s, url: %s", getThreadRecord().threadId,
getThreadConfig().tempFile.getPath(), getEntity().getUrl()), e), false);
} catch (IOException e) {
fail(new AriaM3U8Exception(
String.format("分片【%s】下载失败filePath: %s, url: %s", getThreadRecord().threadId,
getThreadConfig().tempFile.getPath(), getEntity().getUrl()), e), true);
} catch (Exception e) {
fail(new AriaM3U8Exception(
String.format("分片【%s】下载失败filePath: %s, url: %s", getThreadRecord().threadId,
getThreadConfig().tempFile.getPath(), getEntity().getUrl()), e), false);
} finally {
try {
if (is != null) {
is.close();
}
if (conn != null) {
conn.disconnect();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handleConn(HttpURLConnection conn) throws IOException {
ConnectionHelp.setConnectParam(mHttpTaskOption, conn);
conn.setConnectTimeout(getTaskConfig().getConnectTimeOut());
conn.setReadTimeout(getTaskConfig().getIOTimeOut()); //设置读取流的等待时间,必须设置该参数
conn.connect();
int code = conn.getResponseCode();
if (code == HttpURLConnection.HTTP_OK) {
is = new BufferedInputStream(ConnectionHelp.convertInputStream(conn));
if (mHttpTaskOption.isChunked()) {
readChunked(is);
} else if (getThreadConfig().isBlock) {
readDynamicFile(is);
}
} 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 {
fail(new AriaM3U8Exception(
String.format("连接错误http错误码%surl%s", code, getThreadConfig().url)),
false);
}
conn.disconnect();
}
/**
* 处理30x跳转
*/
private void handleUrlReTurn(HttpURLConnection conn, String newUrl) throws IOException {
ALog.d(TAG, "30x跳转新url为【" + newUrl + "");
if (TextUtils.isEmpty(newUrl) || newUrl.equalsIgnoreCase("null")) {
fail(new AriaM3U8Exception("下载失败重定向url为空"), false);
return;
}
if (newUrl.startsWith("/")) {
Uri uri = Uri.parse(getThreadConfig().url);
newUrl = uri.getHost() + newUrl;
}
if (!CheckUtil.checkUrl(newUrl)) {
fail(new AriaM3U8Exception("下载失败重定向url错误"), false);
return;
}
String cookies = conn.getHeaderField("Set-Cookie");
conn.disconnect(); // 关闭上一个连接
URL url = ConnectionHelp.handleUrl(newUrl, mHttpTaskOption);
conn = ConnectionHelp.handleConnection(url, mHttpTaskOption);
if (!TextUtils.isEmpty(cookies)) {
conn.setRequestProperty("Cookie", cookies);
}
if (mHttpTaskOption.isChunked()) {
conn.setDoInput(true);
conn.setChunkedStreamingMode(0);
}
handleConn(conn);
}
/**
* 读取chunked数据
*/
private void readChunked(InputStream is) {
FileOutputStream fos = null;
try {
fos = new FileOutputStream(getThreadConfig().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 AriaM3U8Exception(
String.format("文件下载失败savePath: %s, url: %s", getThreadConfig().tempFile.getPath(),
getThreadConfig().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(getThreadConfig().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 AriaM3U8Exception(
String.format("文件下载失败savePath: %s, url: %s", getThreadConfig().tempFile.getPath(),
getThreadConfig().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,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.live;
import com.arialyy.aria.core.TaskRecord;
import com.arialyy.aria.core.ThreadRecord;
import com.arialyy.aria.core.common.RecordHandler;
import com.arialyy.aria.core.loader.IRecordHandler;
import com.arialyy.aria.core.wrapper.AbsTaskWrapper;
import com.arialyy.aria.core.wrapper.ITaskWrapper;
import com.arialyy.aria.m3u8.M3U8TaskOption;
import com.arialyy.aria.util.DeleteM3u8Record;
import com.arialyy.aria.util.RecordUtil;
import java.util.ArrayList;
/**
* 直播m3u8文件处理器
*/
final class LiveRecordHandler extends RecordHandler {
private M3U8TaskOption mOption;
LiveRecordHandler(AbsTaskWrapper wrapper) {
super(wrapper);
}
public void setOption(M3U8TaskOption option) {
mOption = option;
}
@Override public void onPre() {
super.onPre();
DeleteM3u8Record.getInstance().deleteRecord(getEntity().getFilePath(), true, true);
}
/**
* @deprecated 直播文件不需要处理任务记录
*/
@Deprecated
@Override public void handlerTaskRecord(TaskRecord record) {
if (record.threadRecords == null) {
record.threadRecords = new ArrayList<>();
}
}
/**
* @deprecated 交由{@link #createThreadRecord(TaskRecord, String, int)} 处理
*/
@Override
@Deprecated
public ThreadRecord createThreadRecord(TaskRecord record, int threadId, long startL, long endL) {
return null;
}
/**
* 创建线程记录
*
* @param taskRecord 任务记录
* @param tsUrl ts下载地址
* @param threadId 线程id
*/
ThreadRecord createThreadRecord(TaskRecord taskRecord, String tsUrl, int threadId) {
ThreadRecord tr = new ThreadRecord();
tr.taskKey = taskRecord.filePath;
tr.isComplete = false;
tr.tsUrl = tsUrl;
tr.threadType = taskRecord.taskType;
tr.threadId = threadId;
tr.startLocation = 0;
taskRecord.threadRecords.add(tr);
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;
record.isBlock = true;
record.taskType = ITaskWrapper.M3U8_LIVE;
record.bandWidth = mOption.getBandWidth();
return record;
}
@Override public int initTaskThreadNum() {
return 1;
}
}

View File

@ -0,0 +1,167 @@
/*
* 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.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import com.arialyy.aria.core.TaskRecord;
import com.arialyy.aria.core.download.DTaskWrapper;
import com.arialyy.aria.core.inf.IThreadStateManager;
import com.arialyy.aria.core.listener.IEventListener;
import com.arialyy.aria.core.listener.ISchedulers;
import com.arialyy.aria.core.loader.ILoaderVisitor;
import com.arialyy.aria.m3u8.M3U8Listener;
import com.arialyy.aria.m3u8.M3U8TaskOption;
import com.arialyy.aria.util.ALog;
import com.arialyy.aria.util.CommonUtil;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import static com.arialyy.aria.m3u8.M3U8InfoTask.M3U8_INDEX_FORMAT;
final class LiveStateManager implements IThreadStateManager {
private final String TAG = CommonUtil.getClassName(getClass());
private M3U8Listener mListener;
private long mProgress; //当前总进度
private Looper mLooper;
private DTaskWrapper mTaskWrapper;
private M3U8TaskOption mM3U8Option;
private FileOutputStream mIndexFos;
private M3U8LiveLoader mLoader;
/**
* @param listener 任务事件
*/
LiveStateManager(DTaskWrapper wrapper, IEventListener listener) {
mTaskWrapper = wrapper;
mListener = (M3U8Listener) listener;
mM3U8Option = (M3U8TaskOption) mTaskWrapper.getM3u8Option();
}
private Handler.Callback mCallback = new Handler.Callback() {
@Override public boolean handleMessage(Message msg) {
int peerIndex = msg.getData().getInt(ISchedulers.DATA_M3U8_PEER_INDEX);
switch (msg.what) {
case STATE_STOP:
if (mLoader.isBreak()) {
ALog.d(TAG, "任务停止");
quitLooper();
}
break;
case STATE_CANCEL:
if (mLoader.isBreak()) {
ALog.d(TAG, "任务取消");
quitLooper();
}
break;
case STATE_COMPLETE:
mLoader.notifyLock(true, peerIndex);
if (mM3U8Option.isGenerateIndexFile() && !mLoader.isBreak()) {
addExtInf(mLoader.getCurExtInfo().url, mLoader.getCurExtInfo().extInf);
}
mListener.onPeerComplete(mTaskWrapper.getKey(),
msg.getData().getString(ISchedulers.DATA_M3U8_PEER_PATH), peerIndex);
break;
case STATE_RUNNING:
Bundle b = msg.getData();
if (b != null) {
long len = b.getLong(IThreadStateManager.DATA_ADD_LEN, 0);
mProgress += len;
}
break;
case STATE_FAIL:
mLoader.notifyLock(false, peerIndex);
mListener.onPeerFail(mTaskWrapper.getKey(),
msg.getData().getString(ISchedulers.DATA_M3U8_PEER_PATH), peerIndex);
break;
}
return true;
}
};
void setLoader(M3U8LiveLoader loader) {
mLoader = loader;
}
/**
* 退出looper循环
*/
private void quitLooper() {
ALog.d(TAG, "quitLooper");
mLooper.quit();
if (mIndexFos != null) {
try {
mIndexFos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 给索引文件添加extInfo信息
*/
private void addExtInf(String url, String extInf) {
File indexFile =
new File(String.format(M3U8_INDEX_FORMAT, mTaskWrapper.getEntity().getFilePath()));
if (!indexFile.exists()) {
ALog.e(TAG, String.format("索引文件【%s】不存在添加peer的extInf失败", indexFile.getPath()));
return;
}
try {
if (mIndexFos == null) {
mIndexFos = new FileOutputStream(indexFile, true);
}
mIndexFos.write(extInf.concat("\r\n").getBytes(Charset.forName("UTF-8")));
mIndexFos.write(url.concat("\r\n").getBytes(Charset.forName("UTF-8")));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override public boolean isFail() {
return false;
}
@Override public boolean isComplete() {
return false;
}
@Override public long getCurrentProgress() {
return mProgress;
}
@Override public void updateCurrentProgress(long currentProgress) {
mProgress = currentProgress;
}
@Override public void setLooper(TaskRecord taskRecord, Looper looper) {
mLooper = looper;
}
@Override public Handler.Callback getHandlerCallback() {
return mCallback;
}
@Override public void accept(ILoaderVisitor visitor) {
visitor.addComponent(this);
}
}

View File

@ -0,0 +1,29 @@
/*
* 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 com.arialyy.aria.core.processor.ILiveTsUrlConverter;
/**
* 默认的m3u8 ts转换器
*/
class LiveTsDefConverter implements ILiveTsUrlConverter {
@Override public String convert(String m3u8Url, String tsUrl) {
int index = m3u8Url.lastIndexOf("/");
String parentUrl = m3u8Url.substring(0, index + 1);
return parentUrl + tsUrl;
}
}

View File

@ -0,0 +1,369 @@
/*
* 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.text.TextUtils;
import com.arialyy.aria.core.ThreadRecord;
import com.arialyy.aria.core.common.AbsEntity;
import com.arialyy.aria.core.common.CompleteInfo;
import com.arialyy.aria.core.common.SubThreadConfig;
import com.arialyy.aria.core.download.DTaskWrapper;
import com.arialyy.aria.core.inf.IThreadStateManager;
import com.arialyy.aria.core.loader.IInfoTask;
import com.arialyy.aria.core.loader.IRecordHandler;
import com.arialyy.aria.core.loader.IThreadTaskBuilder;
import com.arialyy.aria.core.manager.ThreadTaskManager;
import com.arialyy.aria.core.processor.ILiveTsUrlConverter;
import com.arialyy.aria.core.processor.ITsMergeHandler;
import com.arialyy.aria.core.task.ThreadTask;
import com.arialyy.aria.core.wrapper.ITaskWrapper;
import com.arialyy.aria.exception.AriaException;
import com.arialyy.aria.exception.AriaM3U8Exception;
import com.arialyy.aria.m3u8.BaseM3U8Loader;
import com.arialyy.aria.m3u8.IdGenerator;
import com.arialyy.aria.m3u8.M3U8InfoTask;
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.io.FilenameFilter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* M3U8点播文件下载器
*/
final class M3U8LiveLoader extends BaseM3U8Loader {
/**
* 最大执行数
*/
private static int EXEC_MAX_NUM = 4;
private Handler mStateHandler;
private ArrayBlockingQueue<Long> mFlagQueue = new ArrayBlockingQueue<>(EXEC_MAX_NUM);
private ReentrantLock LOCK = new ReentrantLock();
private Condition mCondition = LOCK.newCondition();
private LinkedBlockingQueue<ExtInfo> mPeerQueue = new LinkedBlockingQueue<>();
private ExtInfo mCurExtInfo;
private M3U8InfoTask mInfoTask;
private ScheduledThreadPoolExecutor mTimer;
private List<String> mPeerUrls = new ArrayList<>();
M3U8LiveLoader(DTaskWrapper wrapper, M3U8Listener listener) {
super(wrapper, listener);
if (((M3U8TaskOption) wrapper.getM3u8Option()).isGenerateIndexFile()) {
ALog.i(TAG, "直播文件下载,创建索引文件的操作将导致只能同时下载一个切片");
EXEC_MAX_NUM = 1;
}
}
ExtInfo getCurExtInfo() {
return mCurExtInfo;
}
private void offerPeer(ExtInfo extInfo) {
mPeerQueue.offer(extInfo);
}
@Override protected void handleTask(Looper looper) {
if (isBreak()) {
return;
}
// 处理记录
getRecordHandler().setOption(mM3U8Option);
mRecord = getRecordHandler().getRecord(0);
// 初始化状态管理器
getStateManager().setLooper(mRecord, looper);
getStateManager().setLoader(this);
mStateHandler = new Handler(looper, getStateManager().getHandlerCallback());
// 循环获取直播文件列表
startLoaderLiveInfo();
// 启动定时器
startTimer();
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) {
ExtInfo extInfo = mPeerQueue.poll();
if (extInfo == null) {
break;
}
mCurExtInfo = extInfo;
ThreadTask task = createThreadTask(cacheDir, index, extInfo.url);
getTaskList().add(task);
mFlagQueue.offer(startThreadTask(task, task.getConfig().peerIndex));
index++;
}
if (mFlagQueue.size() > 0) {
mCondition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOCK.unlock();
}
}
}
}).start();
}
@Override protected LiveStateManager getStateManager() {
return (LiveStateManager) super.getStateManager();
}
private LiveRecordHandler getRecordHandler() {
return (LiveRecordHandler) mRecordHandler;
}
@Override public long getFileSize() {
return mTempFile.length();
}
void notifyLock(boolean success, int peerId) {
try {
LOCK.lock();
long id = mFlagQueue.take();
if (success) {
ALog.d(TAG, String.format("切片【%s】下载成功", peerId));
} else {
ALog.e(TAG, String.format("切片【%s】下载失败", peerId));
}
mCondition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOCK.unlock();
}
}
/**
* 启动线程任务
*
* @return 线程唯一id标志
*/
private long startThreadTask(ThreadTask task, int indexId) {
ThreadTaskManager.getInstance().startThread(mTaskWrapper.getKey(), task);
((M3U8Listener) getListener()).onPeerStart(mTaskWrapper.getKey(),
task.getConfig().tempFile.getPath(),
indexId);
return IdGenerator.getInstance().nextId();
}
/**
* 配置config
*/
private ThreadTask createThreadTask(String cacheDir, int indexId, String tsUrl) {
ThreadRecord tr = getRecordHandler().createThreadRecord(mRecord, tsUrl, indexId);
SubThreadConfig config = new SubThreadConfig();
config.url = tsUrl;
config.tempFile = new File(getTsFilePath(cacheDir, indexId));
config.isBlock = mRecord.isBlock;
config.taskWrapper = mTaskWrapper;
config.record = tr;
config.stateHandler = mStateHandler;
config.peerIndex = indexId;
config.threadType = SubThreadConfig.getThreadType(ITaskWrapper.M3U8_LIVE);
config.updateInterval = SubThreadConfig.getUpdateInterval(ITaskWrapper.M3U8_LIVE);
config.ignoreFailure = mM3U8Option.isIgnoreFailureTs();
if (!config.tempFile.exists()) {
FileUtil.createFile(config.tempFile);
}
ThreadTask threadTask = new ThreadTask(config);
M3U8ThreadTaskAdapter adapter = new M3U8ThreadTaskAdapter(config);
threadTask.setAdapter(adapter);
return threadTask;
}
/**
* 合并文件
*
* @return {@code true} 合并成功,{@code false}合并失败
*/
private boolean mergeFile() {
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) {
FileUtil.deleteFile(pp);
}
File cDir = new File(cacheDir);
FileUtil.deleteDir(cDir);
return true;
} else {
ALog.e(TAG, "合并失败");
return false;
}
}
@Override public void addComponent(IRecordHandler recordHandler) {
mRecordHandler = recordHandler;
}
@Override public void addComponent(IInfoTask infoTask) {
mInfoTask = (M3U8InfoTask) infoTask;
mInfoTask.setCallback(new IInfoTask.Callback() {
@Override public void onSucceed(String key, CompleteInfo info) {
ALog.d(TAG, "更新直播的m3u8文件");
}
@Override public void onFail(AbsEntity entity, AriaException e, boolean needRetry) {
}
});
mInfoTask.setOnGetPeerCallback(new M3U8InfoTask.OnGetLivePeerCallback() {
@Override public void onGetPeer(String url, String extInf) {
if (mPeerUrls.contains(url)) {
return;
}
mPeerUrls.add(url);
ILiveTsUrlConverter converter = mM3U8Option.isUseDefConvert() ?
new LiveTsDefConverter() :
mM3U8Option.getLiveTsUrlConverter();
if (converter != null) {
if (TextUtils.isEmpty(mM3U8Option.getBandWidthUrl())) {
url = converter.convert(getEntity().getUrl(), url);
} else {
url = converter.convert(mM3U8Option.getBandWidthUrl(), url);
}
}
if (TextUtils.isEmpty(url) || !url.startsWith("http")) {
fail(new AriaM3U8Exception(String.format("ts地址错误url%s", url)), false);
return;
}
offerPeer(new M3U8LiveLoader.ExtInfo(url, extInf));
}
});
}
private void fail(AriaM3U8Exception e, boolean needRetry) {
getListener().onFail(needRetry, e);
handleComplete();
}
private void handleComplete() {
if (mInfoTask != null) {
mInfoTask.setStop(true);
closeInfoTimer();
if (mM3U8Option.isGenerateIndexFile()) {
if (generateIndexFile(true)) {
getListener().onComplete();
} else {
getListener().onFail(false, new AriaM3U8Exception("创建索引文件失败"));
}
} else if (mM3U8Option.isMergeFile()) {
if (mergeFile()) {
getListener().onComplete();
} else {
getListener().onFail(false, new AriaM3U8Exception("合并文件失败"));
}
} else {
getListener().onComplete();
}
}
}
/**
* 开始循环加载m3u8信息
*/
private void startLoaderLiveInfo() {
mTimer = new ScheduledThreadPoolExecutor(1);
mTimer.scheduleWithFixedDelay(new Runnable() {
@Override public void run() {
mInfoTask.run();
}
}, 0, mM3U8Option.getLiveUpdateInterval(), TimeUnit.MILLISECONDS);
}
private void closeInfoTimer() {
if (mTimer != null && !mTimer.isShutdown()) {
mTimer.shutdown();
}
}
/**
* 需要在{@link #addComponent(IRecordHandler)} 后调用
*/
@Override public void addComponent(IThreadStateManager threadState) {
mStateManager = threadState;
}
/**
* @deprecated m3u8 不需要实现这个
*/
@Deprecated
@Override public void addComponent(IThreadTaskBuilder builder) {
}
@Override protected void checkComponent() {
if (mRecordHandler == null) {
throw new NullPointerException("任务记录组件为空");
}
if (mInfoTask == null) {
throw new NullPointerException(("文件信息组件为空"));
}
if (mStateManager == null) {
throw new NullPointerException("任务状态管理组件为空");
}
}
static class ExtInfo {
String url;
String extInf;
ExtInfo(String url, String extInf) {
this.url = url;
this.extInf = extInf;
}
}
}

View File

@ -0,0 +1,61 @@
/*
* 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 com.arialyy.aria.core.download.DTaskWrapper;
import com.arialyy.aria.core.loader.AbsNormalLoaderUtil;
import com.arialyy.aria.core.loader.LoaderStructure;
import com.arialyy.aria.http.HttpTaskOption;
import com.arialyy.aria.m3u8.M3U8InfoTask;
import com.arialyy.aria.m3u8.M3U8Listener;
import com.arialyy.aria.m3u8.M3U8TaskOption;
/**
* M3U8直播文件下载工具对于直播来说需要定时更新m3u8文件
* 工作流程:
* 1、持续获取切片信息直到调用停止|取消才停止获取切片信息
* 2、完成所有分片下载后合并ts文件
* 3、删除该隐藏文件夹
* 4、对于直播来说是没有停止的停止就代表完成
* 5、不处理直播切片下载失败的状态
*/
public class M3U8LiveUtil extends AbsNormalLoaderUtil {
public M3U8LiveUtil() {
}
@Override public DTaskWrapper getTaskWrapper() {
return (DTaskWrapper) super.getTaskWrapper();
}
@Override public M3U8LiveLoader getLoader() {
if (mLoader == null) {
getTaskWrapper().generateM3u8Option(M3U8TaskOption.class);
getTaskWrapper().generateTaskOption(HttpTaskOption.class);
mLoader = new M3U8LiveLoader(getTaskWrapper(), (M3U8Listener) getListener());
}
return (M3U8LiveLoader) mLoader;
}
@Override public LoaderStructure BuildLoaderStructure() {
LoaderStructure structure = new LoaderStructure();
structure.addComponent(new LiveRecordHandler(getTaskWrapper()))
.addComponent(new M3U8InfoTask(getTaskWrapper()))
.addComponent(new LiveStateManager(getTaskWrapper(), getListener()));
structure.accept(getLoader());
return structure;
}
}

View File

@ -0,0 +1,596 @@
/*
* 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.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import com.arialyy.aria.core.ThreadRecord;
import com.arialyy.aria.core.common.AbsEntity;
import com.arialyy.aria.core.common.CompleteInfo;
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.IThreadStateManager;
import com.arialyy.aria.core.loader.IInfoTask;
import com.arialyy.aria.core.loader.IRecordHandler;
import com.arialyy.aria.core.loader.IThreadTaskBuilder;
import com.arialyy.aria.core.manager.ThreadTaskManager;
import com.arialyy.aria.core.processor.IVodTsUrlConverter;
import com.arialyy.aria.core.task.ThreadTask;
import com.arialyy.aria.core.wrapper.ITaskWrapper;
import com.arialyy.aria.exception.AriaException;
import com.arialyy.aria.exception.AriaM3U8Exception;
import com.arialyy.aria.m3u8.BaseM3U8Loader;
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.Collection;
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.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* M3U8点播文件下载器
*/
final 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 PeerIndexEvent mCurrentEvent;
private String mCacheDir;
private AtomicInteger afterPeerIndex = new AtomicInteger();
private AtomicInteger beforePeerIndex = new AtomicInteger();
private AtomicInteger mCompleteNum = new AtomicInteger();
private AtomicInteger mCurrentFlagSize = new AtomicInteger();
private boolean isJump = false, isDestroy = false;
private ExecutorService mJumpThreadPool;
private Thread jumpThread = null;
private M3U8TaskOption mM3U8Option;
private Looper mLooper;
M3U8VodLoader(DTaskWrapper wrapper, M3U8Listener listener) {
super(wrapper, listener);
mM3U8Option = (M3U8TaskOption) wrapper.getM3u8Option();
mFlagQueue = new ArrayBlockingQueue<>(mM3U8Option.getMaxTsQueueNum());
EXEC_MAX_NUM = mM3U8Option.getMaxTsQueueNum();
mJumpQueue = new ArrayBlockingQueue<>(10);
EventMsgUtil.getDefault().register(this);
}
@Override protected M3U8Listener getListener() {
return (M3U8Listener) super.getListener();
}
SparseArray<ThreadRecord> getBeforePeer() {
return mBeforePeer;
}
int getCompleteNum() {
return mCompleteNum.get();
}
void setCompleteNum(int completeNum) {
mCompleteNum.set(completeNum);
}
int getCurrentFlagSize() {
mCurrentFlagSize.set(mFlagQueue.size());
return mCurrentFlagSize.get();
}
void setCurrentFlagSize(int currentFlagSize) {
mCurrentFlagSize.set(currentFlagSize);
}
boolean isJump() {
return isJump;
}
File getTempFile() {
return mTempFile;
}
@Override public void onDestroy() {
super.onDestroy();
isDestroy = true;
EventMsgUtil.getDefault().unRegister(this);
if (mJumpThreadPool != null && !mJumpThreadPool.isShutdown()) {
mJumpThreadPool.shutdown();
}
}
@Override protected void handleTask(Looper looper) {
if (isBreak()) {
return;
}
mLooper = looper;
mInfoTask.run();
}
private void startThreadTask() {
// 处理任务记录
((VodRecordHandler) mRecordHandler).setOption(mM3U8Option);
mRecord = mRecordHandler.getRecord(0);
// 处理任务管理器
mStateHandler = new Handler(mLooper, getStateManager().getHandlerCallback());
getStateManager().setVodLoader(this);
getStateManager().setLooper(mRecord, mLooper);
// 初始化ts数据
initData();
// 启动定时器
startTimer();
if (getStateManager().isComplete()){
Log.d(TAG, "任务已完成");
getStateManager().handleTaskComplete();
return;
}
// 启动线程开始下载ts切片
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.get() == 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 (beforePeerIndex.get() == 0 && afterPeerIndex.get() < mAfterPeer.size()) {
//ALog.d(TAG, String.format("afterArray size:%s, index:%s", mAfterPeer.size(), aIndex));
tr = mAfterPeer.valueAt(afterPeerIndex.get());
afterPeerIndex.getAndIncrement();
}
// 如果指针之后的数组没有切片了,则重新初始化指针位置,并获取指针之前的数组获取切片进行下载
if (mBeforePeer.size() > 0
&& (tr == null || beforePeerIndex.get() != 0)
&& beforePeerIndex.get() < mBeforePeer.size()) {
tr = mBeforePeer.valueAt(beforePeerIndex.get());
beforePeerIndex.getAndIncrement();
}
} catch (Exception e) {
e.printStackTrace();
}
return tr;
}
/**
* 启动线程任务
*/
private void addTaskToQueue(ThreadRecord tr) throws InterruptedException {
ThreadTask task = createThreadTask(mCacheDir, tr, tr.threadId);
getTaskList().add(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.set(0);
for (ThreadRecord tr : mRecord.threadRecords) {
if (!tr.isComplete) {
mAfterPeer.put(tr.threadId, tr);
} else {
mCompleteNum.getAndIncrement();
}
}
getStateManager().updateStateCount();
if (mCompleteNum.get() <= 0) {
getListener().onStart(0);
} else {
int percent = mCompleteNum.get() * 100 / mRecord.threadRecords.size();
getListener().onResume(percent);
}
}
/**
* 每隔几秒钟检查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.set(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();
}
}
/**
* 从指定位置恢复任务
*/
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();
afterPeerIndex.set(0);
beforePeerIndex.set(0);
mCompleteNum.set(0);
for (ThreadRecord tr : mRecord.threadRecords) {
if (tr.isComplete) {
mCompleteNum.getAndIncrement();
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));
getStateManager().updateStateCount();
try {
JUMP_LOCK.lock();
mJumpCondition.signalAll();
} finally {
JUMP_LOCK.unlock();
}
}
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.taskWrapper = mTaskWrapper;
config.record = record;
config.stateHandler = mStateHandler;
config.peerIndex = index;
config.threadType = SubThreadConfig.getThreadType(ITaskWrapper.M3U8_LIVE);
config.updateInterval = SubThreadConfig.getUpdateInterval(ITaskWrapper.M3U8_LIVE);
config.ignoreFailure = mM3U8Option.isIgnoreFailureTs();
if (!config.tempFile.exists()) {
FileUtil.createFile(config.tempFile);
}
ThreadTask threadTask = new ThreadTask(config);
M3U8ThreadTaskAdapter adapter = new M3U8ThreadTaskAdapter(config);
threadTask.setAdapter(adapter);
return threadTask;
}
@Override public void addComponent(IRecordHandler recordHandler) {
mRecordHandler = recordHandler;
}
@Override public void addComponent(IInfoTask infoTask) {
mInfoTask = infoTask;
final List<String> urls = new ArrayList<>();
mInfoTask.setCallback(new IInfoTask.Callback() {
@Override public void onSucceed(String key, CompleteInfo info) {
IVodTsUrlConverter converter = mM3U8Option.isUseDefConvert() ?
new VodTsDefConverter() :
mM3U8Option.getVodUrlConverter();
if (converter != null) {
if (TextUtils.isEmpty(mM3U8Option.getBandWidthUrl())) {
urls.addAll(
converter.convert(getEntity().getUrl(), (List<String>) info.obj));
} else {
urls.addAll(
converter.convert(mM3U8Option.getBandWidthUrl(), (List<String>) info.obj));
}
} else {
urls.addAll((Collection<? extends String>) info.obj);
}
if (urls.isEmpty()) {
fail(new AriaM3U8Exception("获取地址失败"), false);
return;
} else if (!urls.get(0).startsWith("http")) {
fail(new AriaM3U8Exception("地址错误请使用IVodTsUrlConverter处理你的url信息"), false);
return;
}
mM3U8Option.setUrls(urls);
if (isStop) {
getListener().onStop(getEntity().getCurrentProgress());
} else if (isCancel) {
getListener().onCancel();
} else {
startThreadTask();
}
}
@Override public void onFail(AbsEntity entity, AriaException e, boolean needRetry) {
fail(e, needRetry);
}
});
}
protected void fail(AriaException e, boolean needRetry) {
if (isBreak()) {
return;
}
getListener().onFail(needRetry, e);
onDestroy();
}
/**
* 需要在 {@link #addComponent(IRecordHandler)}后调用
*/
@Override public void addComponent(IThreadStateManager threadState) {
mStateManager = threadState;
}
/**
* m3u8 不需要实现这个
*/
@Deprecated
@Override public void addComponent(IThreadTaskBuilder builder) {
}
@Override
protected VodStateManager getStateManager() {
return (VodStateManager) mStateManager;
}
@Override protected void checkComponent() {
if (mRecordHandler == null) {
throw new NullPointerException("任务记录组件为空");
}
if (mInfoTask == null) {
throw new NullPointerException(("文件信息组件为空"));
}
if (getStateManager() == null) {
throw new NullPointerException("任务状态管理组件为空");
}
}
private static class TempFlag {
ThreadTask threadTask;
int threadId;
}
}

View File

@ -0,0 +1,61 @@
/*
* 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 com.arialyy.aria.core.download.DTaskWrapper;
import com.arialyy.aria.core.loader.AbsNormalLoader;
import com.arialyy.aria.core.loader.AbsNormalLoaderUtil;
import com.arialyy.aria.core.loader.LoaderStructure;
import com.arialyy.aria.http.HttpTaskOption;
import com.arialyy.aria.m3u8.M3U8InfoTask;
import com.arialyy.aria.m3u8.M3U8Listener;
import com.arialyy.aria.m3u8.M3U8TaskOption;
/**
* M3U8点播文件下载工具
* 工作流程:
* 1、创建一个和文件同父路径并且同名隐藏文件夹
* 2、将所有m3u8的ts文件下载到该文件夹中
* 3、完成所有分片下载后合并ts文件
* 4、删除该隐藏文件夹
*/
public final class M3U8VodUtil extends AbsNormalLoaderUtil {
public M3U8VodUtil() {
}
@Override public DTaskWrapper getTaskWrapper() {
return (DTaskWrapper) super.getTaskWrapper();
}
@Override public AbsNormalLoader getLoader() {
if (mLoader == null) {
getTaskWrapper().generateM3u8Option(M3U8TaskOption.class);
getTaskWrapper().generateTaskOption(HttpTaskOption.class);
mLoader = new M3U8VodLoader(getTaskWrapper(), (M3U8Listener) getListener());
}
return mLoader;
}
@Override public LoaderStructure BuildLoaderStructure() {
LoaderStructure structure = new LoaderStructure();
structure.addComponent(new VodRecordHandler(getTaskWrapper()))
.addComponent(new M3U8InfoTask(getTaskWrapper()))
.addComponent(new VodStateManager(getTaskWrapper(), (M3U8Listener) getListener()));
structure.accept(getLoader());
return structure;
}
}

View File

@ -0,0 +1,133 @@
/*
* 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 com.arialyy.aria.core.TaskRecord;
import com.arialyy.aria.core.ThreadRecord;
import com.arialyy.aria.core.common.RecordHandler;
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.m3u8.BaseM3U8Loader;
import com.arialyy.aria.m3u8.M3U8InfoTask;
import com.arialyy.aria.m3u8.M3U8TaskOption;
import com.arialyy.aria.util.ALog;
import com.arialyy.aria.util.FileUtil;
import java.io.File;
import java.util.ArrayList;
/**
* @author lyy
* Date: 2019-09-24
*/
final class VodRecordHandler extends RecordHandler {
private M3U8TaskOption mOption;
VodRecordHandler(DTaskWrapper wrapper) {
super(wrapper);
}
public void setOption(M3U8TaskOption option) {
mOption = option;
}
/**
* 不处理live的记录
*/
@Override public void handlerTaskRecord(TaskRecord mTaskRecord) {
String cacheDir = mOption.getCacheDir();
long currentProgress = 0;
int completeNum = 0;
File targetFile = new File(mTaskRecord.filePath);
if (!targetFile.exists()) {
FileUtil.createFile(targetFile);
}
M3U8Entity m3U8Entity = ((DownloadEntity) getEntity()).getM3U8Entity();
// 重新下载所有切片
boolean reDownload =
(m3U8Entity.getPeerNum() <= 0 || (mOption.isGenerateIndexFile() && !new File(
String.format(M3U8InfoTask.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()) {
FileUtil.deleteFile(temp);
}
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 = record.taskType;
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;
record.isBlock = true;
record.taskType = ITaskWrapper.M3U8_VOD;
record.bandWidth = mOption.getBandWidth();
return record;
}
@Override public int initTaskThreadNum() {
if (getWrapper().getRequestType() == ITaskWrapper.M3U8_VOD) {
return
mOption.getUrls() == null || mOption.getUrls().isEmpty() ? 1 : mOption.getUrls().size();
}
if (getWrapper().getRequestType() == ITaskWrapper.M3U8_LIVE) {
return 1;
}
return 0;
}
}

View File

@ -0,0 +1,317 @@
/*
* 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 com.arialyy.aria.core.TaskRecord;
import com.arialyy.aria.core.ThreadRecord;
import com.arialyy.aria.core.download.DTaskWrapper;
import com.arialyy.aria.core.download.DownloadEntity;
import com.arialyy.aria.core.inf.IThreadStateManager;
import com.arialyy.aria.core.listener.ISchedulers;
import com.arialyy.aria.core.loader.ILoaderVisitor;
import com.arialyy.aria.core.manager.ThreadTaskManager;
import com.arialyy.aria.core.processor.ITsMergeHandler;
import com.arialyy.aria.core.task.ThreadTask;
import com.arialyy.aria.exception.AriaException;
import com.arialyy.aria.exception.AriaM3U8Exception;
import com.arialyy.aria.m3u8.BaseM3U8Loader;
import com.arialyy.aria.m3u8.M3U8Listener;
import com.arialyy.aria.m3u8.M3U8TaskOption;
import com.arialyy.aria.util.ALog;
import com.arialyy.aria.util.CommonUtil;
import com.arialyy.aria.util.FileUtil;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* m3u8 点播下载状态管理器
*/
public final class VodStateManager implements IThreadStateManager {
private final String TAG = CommonUtil.getClassName(getClass());
private M3U8Listener listener;
private int startThreadNum; // 启动的线程总数
private AtomicInteger cancelNum = new AtomicInteger(0); // 已经取消的线程的数
private AtomicInteger stopNum = new AtomicInteger(0); // 已经停止的线程数
private AtomicInteger failNum = new AtomicInteger(0); // 失败的线程数
private long progress;
private TaskRecord taskRecord; // 任务记录
private Looper looper;
private DTaskWrapper wrapper;
private M3U8TaskOption m3U8Option;
private M3U8VodLoader loader;
/**
* @param listener 任务事件
*/
VodStateManager(DTaskWrapper wrapper, M3U8Listener listener) {
this.wrapper = wrapper;
this.listener = listener;
m3U8Option = (M3U8TaskOption) wrapper.getM3u8Option();
progress = wrapper.getEntity().getCurrentProgress();
}
private Handler.Callback callback = new Handler.Callback() {
@Override public boolean handleMessage(Message msg) {
int peerIndex = msg.getData().getInt(ISchedulers.DATA_M3U8_PEER_INDEX);
switch (msg.what) {
case STATE_STOP:
stopNum.getAndIncrement();
removeSignThread((ThreadTask) msg.obj);
// 处理跳转位置后,恢复任务
if (loader.isJump()
&& (stopNum.get() == loader.getCurrentFlagSize() || loader.getCurrentFlagSize() == 0)
&& !loader.isBreak()) {
loader.resumeTask();
return true;
}
if (loader.isBreak()) {
ALog.d(TAG, String.format("vod任务【%s】停止", loader.getTempFile().getName()));
quitLooper();
}
break;
case STATE_CANCEL:
cancelNum.getAndIncrement();
removeSignThread((ThreadTask) msg.obj);
if (loader.isBreak()) {
ALog.d(TAG, String.format("vod任务【%s】取消", loader.getTempFile().getName()));
quitLooper();
}
break;
case STATE_FAIL:
failNum.getAndIncrement();
for (ThreadRecord tr : taskRecord.threadRecords) {
if (tr.threadId == peerIndex) {
loader.getBeforePeer().put(peerIndex, tr);
break;
}
}
getListener().onPeerFail(wrapper.getKey(),
msg.getData().getString(ISchedulers.DATA_M3U8_PEER_PATH), peerIndex);
if (isFail()) {
ALog.d(TAG, String.format("vod任务【%s】失败", loader.getTempFile().getName()));
Bundle b = msg.getData();
listener.onFail(b.getBoolean(DATA_RETRY, true),
(AriaException) b.getSerializable(DATA_ERROR_INFO));
quitLooper();
}
break;
case STATE_COMPLETE:
if (loader.isBreak()) {
quitLooper();
}
loader.setCompleteNum(loader.getCompleteNum() + 1);
// 正在切换位置时,切片完成,队列减小
if (loader.isJump()) {
loader.setCurrentFlagSize(loader.getCurrentFlagSize() - 1);
if (loader.getCurrentFlagSize() < 0) {
loader.setCurrentFlagSize(0);
}
}
removeSignThread((ThreadTask) msg.obj);
getListener().onPeerComplete(wrapper.getKey(),
msg.getData().getString(ISchedulers.DATA_M3U8_PEER_PATH), peerIndex);
handlerPercent();
if (!loader.isJump()) {
loader.notifyWaitLock(true);
}
if (isComplete()) {
handleTaskComplete();
}
break;
case STATE_RUNNING:
Bundle b = msg.getData();
if (b != null) {
long len = b.getLong(IThreadStateManager.DATA_ADD_LEN, 0);
progress += len;
}
break;
}
return true;
}
};
/**
* 处理m3u8以完成
*/
void handleTaskComplete() {
ALog.d(TAG, String.format(
"startThreadNum = %s, stopNum = %s, cancelNum = %s, failNum = %s, completeNum = %s, flagQueueSize = %s",
startThreadNum, stopNum, cancelNum, failNum, loader.getCompleteNum(),
loader.getCurrentFlagSize()));
ALog.d(TAG, String.format("vod任务【%s】完成", loader.getTempFile().getName()));
if (m3U8Option.isGenerateIndexFile()) {
if (loader.generateIndexFile(false)) {
listener.onComplete();
} else {
listener.onFail(false, new AriaM3U8Exception("创建索引文件失败"));
}
} else if (m3U8Option.isMergeFile()) {
if (mergeFile()) {
listener.onComplete();
} else {
listener.onFail(false, null);
}
} else {
listener.onComplete();
}
quitLooper();
}
void updateStateCount() {
cancelNum.set(0);
stopNum.set(0);
failNum.set(0);
}
@Override public void setLooper(TaskRecord taskRecord, Looper looper) {
this.looper = looper;
this.taskRecord = taskRecord;
for (ThreadRecord record : taskRecord.threadRecords) {
if (!record.isComplete) {
startThreadNum++;
}
}
}
@Override public Handler.Callback getHandlerCallback() {
return callback;
}
private DownloadEntity getEntity() {
return wrapper.getEntity();
}
private M3U8Listener getListener() {
return listener;
}
void setVodLoader(M3U8VodLoader loader) {
this.loader = loader;
}
/**
* 退出looper循环
*/
private void quitLooper() {
ALog.d(TAG, "quitLooper");
looper.quit();
}
private void removeSignThread(ThreadTask threadTask) {
loader.getTaskList().remove(threadTask);
ThreadTaskManager.getInstance().removeSingleTaskThread(wrapper.getKey(), threadTask);
}
/**
* 设置进度
*/
private void handlerPercent() {
int completeNum = m3U8Option.getCompleteNum();
completeNum++;
m3U8Option.setCompleteNum(completeNum);
int percent = completeNum * 100 / taskRecord.threadRecords.size();
getEntity().setPercent(percent);
getEntity().update();
}
@Override public boolean isFail() {
printInfo("isFail");
return failNum.get() != 0 && failNum.get() == loader.getCurrentFlagSize() && !loader.isJump();
}
@Override public boolean isComplete() {
if (m3U8Option.isIgnoreFailureTs()) {
return loader.getCompleteNum() + failNum.get() >= taskRecord.threadRecords.size()
&& !loader.isJump();
} else {
return loader.getCompleteNum() == taskRecord.threadRecords.size() && !loader.isJump();
}
}
@Override public long getCurrentProgress() {
return progress;
}
@Override public void updateCurrentProgress(long currentProgress) {
progress = currentProgress;
}
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, loader.getCompleteNum(),
loader.getCurrentFlagSize()));
}
}
/**
* 合并文件
*
* @return {@code true} 合并成功,{@code false}合并失败
*/
private boolean mergeFile() {
ITsMergeHandler mergeHandler = m3U8Option.getMergeHandler();
String cacheDir = loader.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()) {
m3U8Option.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;
}
}
@Override public void accept(ILoaderVisitor visitor) {
visitor.addComponent(this);
}
}

View File

@ -0,0 +1,36 @@
/*
* 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 com.arialyy.aria.core.processor.IVodTsUrlConverter;
import java.util.ArrayList;
import java.util.List;
/**
* 默认的m3u8 ts转换器
*/
class VodTsDefConverter implements IVodTsUrlConverter {
@Override public List<String> convert(String m3u8Url, List<String> tsUrls) {
int index = m3u8Url.lastIndexOf("/");
List<String> convertedTsUrl = new ArrayList<>();
String parentUrl = m3u8Url.substring(0, index + 1);
for (String temp : tsUrls) {
convertedTsUrl.add(parentUrl + temp);
}
return convertedTsUrl;
}
}

View File

@ -0,0 +1,2 @@
Manifest-Version: 1.0

View File

@ -0,0 +1,2 @@
com.arialyy.aria.m3u8.live.M3U8LiveUtil
com.arialyy.aria.m3u8.vod.M3U8VodUtil

View File

@ -0,0 +1 @@
com.arialyy.aria.m3u8.M3U8Listener