loader 重构

This commit is contained in:
laoyuyu
2020-01-01 14:43:52 +08:00
170 changed files with 2862 additions and 21661 deletions

View File

@@ -0,0 +1,407 @@
/*
* 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.M3U8Exception;
import com.arialyy.aria.exception.TaskException;
import com.arialyy.aria.http.ConnectionHelp;
import com.arialyy.aria.http.HttpTaskOption;
import com.arialyy.aria.util.ALog;
import com.arialyy.aria.util.CheckUtil;
import com.arialyy.aria.util.CommonUtil;
import com.arialyy.aria.util.FileUtil;
import com.arialyy.aria.util.Regular;
import java.io.BufferedReader;
import java.io.File;
import java.io.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(conn);
} catch (IOException e) {
failDownload(e.getMessage(), false);
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
@Override public void setCallback(Callback callback) {
mCallback = callback;
}
private void handleConnect(HttpURLConnection conn) throws IOException {
int code = conn.getResponseCode();
if (code == HttpURLConnection.HTTP_OK) {
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = reader.readLine();
if (TextUtils.isEmpty(line) || !line.equalsIgnoreCase("#EXTM3U")) {
failDownload("读取M3U8信息失败读取不到#EXTM3U标签", false);
return;
}
List<String> extInf = new ArrayList<>();
boolean isLive = mTaskWrapper.getRequestType() == ITaskWrapper.M3U8_LIVE;
boolean isGenerateIndexFile =
((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(isGenerateIndexFile, 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;
} else 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);
} else 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】码率不存在", bandWidth), false);
}
return;
} else if (line.startsWith("#EXT-X-KEY")) {
addIndexInfo(isGenerateIndexFile, fos, line);
getKeyInfo(line);
} else {
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;
mCallback.onSucceed(mEntity.getKey(), 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_NOT_FOUND) {
failDownload("404错误", false);
} else {
failDownload(String.format("不支持的响应code: %s", code), true);
}
}
/**
* 添加切片信息到索引文件中
* 直播下载的索引只记录头部信息不记录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 line) {
String temp = line.substring(line.indexOf(":") + 1);
String[] params = temp.split(",");
M3U8Entity m3U8Entity = mEntity.getM3U8Entity();
for (String param : params) {
if (param.startsWith("METHOD")) {
m3U8Entity.method = param.split("=")[1];
} else if (param.startsWith("URI")) {
m3U8Entity.keyUrl = param.split("=")[1].replaceAll("\"", "");
m3U8Entity.keyPath =
new File(mEntity.getFilePath()).getParent() + "/" + CommonUtil.getStrMd5(
m3U8Entity.keyUrl) + ".key";
} else if (param.startsWith("IV")) {
m3U8Entity.iv = param.split("=")[1];
} else if (param.startsWith("KEYFORMAT")) {
m3U8Entity.keyFormat = param.split("=")[1];
} else if (param.startsWith("KEYFORMATVERSIONS")) {
m3U8Entity.keyFormatVersion = param.split("=")[1];
}
}
downloadKey(m3U8Entity);
}
/**
* 读取bandwidth
*/
private int getBandWidth(String line) {
Pattern p = Pattern.compile(Regular.BANDWIDTH);
Matcher m = p.matcher(line);
if (m.find()) {
return Integer.parseInt(m.group());
}
return 0;
}
/**
* 处理30x跳转
*/
private void handleUrlReTurn(HttpURLConnection conn, String newUrl) throws IOException {
ALog.d(TAG, "30x跳转新url为【" + newUrl + "");
if (TextUtils.isEmpty(newUrl) || newUrl.equalsIgnoreCase("null")) {
if (mCallback != null) {
mCallback.onFail(mEntity, new TaskException(TAG, "获取重定向链接失败"), 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(conn);
conn.disconnect();
}
/**
* 处理码率
*/
private void handleBandWidth(HttpURLConnection conn, String bandWidthM3u8Url) throws IOException {
IBandWidthUrlConverter converter = mM3U8Option.getBandWidthUrlConverter();
if (converter != null) {
bandWidthM3u8Url = converter.convert(bandWidthM3u8Url);
if (!bandWidthM3u8Url.startsWith("http")) {
failDownload(String.format("码率转换器转换后的url地址无效转换后的url%s", bandWidthM3u8Url), false);
return;
}
} else {
ALog.d(TAG, "没有设置码率转换器");
}
mM3U8Option.setBandWidthUrl(bandWidthM3u8Url);
ALog.d(TAG, String.format("新码率url%s", bandWidthM3u8Url));
String cookies = conn.getHeaderField("Set-Cookie");
conn.disconnect(); // 关闭上一个连接
URL url = ConnectionHelp.handleUrl(bandWidthM3u8Url, mHttpOption);
conn = ConnectionHelp.handleConnection(url, mHttpOption);
ConnectionHelp.setConnectParam(mHttpOption, conn);
conn.setRequestProperty("Cookie", cookies);
conn.setConnectTimeout(mConnectTimeOut);
conn.connect();
handleConnect(conn);
conn.disconnect();
}
private void failDownload(String errorInfo, boolean needRetry) {
mCallback.onFail(mEntity, new M3U8Exception(TAG, errorInfo), needRetry);
}
/**
* 密钥不存在,下载密钥
*/
private void downloadKey(M3U8Entity info) {
HttpURLConnection conn = null;
FileOutputStream fos = null;
try {
File keyF = new File(info.keyPath);
if (!keyF.exists()) {
ALog.d(TAG, "密钥不存在,下载密钥");
FileUtil.createFile(keyF);
} else {
return;
}
IKeyUrlConverter keyUrlConverter = mM3U8Option.getKeyUrlConverter();
String keyUrl = info.keyUrl;
if (keyUrlConverter != null) {
keyUrl = keyUrlConverter.convert(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 = conn.getInputStream();
fos = new FileOutputStream(keyF);
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.close();
}
if (conn != null) {
conn.disconnect();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}