loader 重构
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user