源代码备份
This commit is contained in:
1
M3U8Component/.gitignore
vendored
Normal file
1
M3U8Component/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
13
M3U8Component/bintray-release.gradle
Normal file
13
M3U8Component/bintray-release.gradle
Normal 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
|
||||
}
|
43
M3U8Component/build.gradle
Normal file
43
M3U8Component/build.gradle
Normal 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'
|
0
M3U8Component/consumer-rules.pro
Normal file
0
M3U8Component/consumer-rules.pro
Normal file
21
M3U8Component/proguard-rules.pro
vendored
Normal file
21
M3U8Component/proguard-rules.pro
vendored
Normal 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
|
2
M3U8Component/src/main/AndroidManifest.xml
Normal file
2
M3U8Component/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,2 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.arialyy.aria.m3u8" />
|
@ -0,0 +1,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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package com.arialyy.aria.m3u8;
|
||||
|
||||
public final class IdGenerator {
|
||||
/**
|
||||
* SnowFlake算法 64位Long类型生成唯一ID 第一位0,表明正数 2-42,41位,表示毫秒时间戳差值,起始值自定义
|
||||
* 43-52,10位,机器编号,5位数据中心编号,5位进程编号 53-64,12位,毫秒内计数器 本机内存生成,性能高
|
||||
* <p>
|
||||
* 主要就是三部分: 时间戳,进程id,序列号 时间戳41,id10位,序列号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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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错误码:%s,url:%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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
2
M3U8Component/src/main/resources/META-INF/MANIFEST.MF
Normal file
2
M3U8Component/src/main/resources/META-INF/MANIFEST.MF
Normal file
@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
|
@ -0,0 +1,2 @@
|
||||
com.arialyy.aria.m3u8.live.M3U8LiveUtil
|
||||
com.arialyy.aria.m3u8.vod.M3U8VodUtil
|
@ -0,0 +1 @@
|
||||
com.arialyy.aria.m3u8.M3U8Listener
|
Reference in New Issue
Block a user