parent
b052ef6642
commit
b670e3e383
@ -0,0 +1,52 @@ |
|||||||
|
/* |
||||||
|
* 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.core.download.m3u8; |
||||||
|
|
||||||
|
import com.arialyy.aria.core.common.AbsFileer; |
||||||
|
import com.arialyy.aria.core.download.DTaskWrapper; |
||||||
|
import com.arialyy.aria.core.download.DownloadEntity; |
||||||
|
import com.arialyy.aria.core.inf.IEventListener; |
||||||
|
import com.arialyy.aria.util.CommonUtil; |
||||||
|
import java.io.File; |
||||||
|
|
||||||
|
public abstract class BaseM3U8Loader extends AbsFileer<DownloadEntity, DTaskWrapper> { |
||||||
|
|
||||||
|
BaseM3U8Loader(IEventListener listener, DTaskWrapper wrapper) { |
||||||
|
super(listener, wrapper); |
||||||
|
} |
||||||
|
|
||||||
|
@Override protected long delayTimer() { |
||||||
|
return 2000; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取ts文件保存路径 |
||||||
|
* |
||||||
|
* @param dirCache 缓存目录 |
||||||
|
* @param threadId ts文件名 |
||||||
|
*/ |
||||||
|
public static String getTsFilePath(String dirCache, int threadId) { |
||||||
|
return String.format("%s/%s.ts", dirCache, threadId); |
||||||
|
} |
||||||
|
|
||||||
|
String getCacheDir() { |
||||||
|
String cacheDir = mTaskWrapper.asM3U8().getCacheDir(); |
||||||
|
if (!new File(cacheDir).exists()) { |
||||||
|
CommonUtil.createDir(cacheDir); |
||||||
|
} |
||||||
|
return cacheDir; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
/* |
||||||
|
* 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.core.download.m3u8; |
||||||
|
|
||||||
|
/** |
||||||
|
* M3U8 直播下载,ts url转换器,对于某些服务器,返回的ts地址可以是相对地址,也可能是处理过的 |
||||||
|
* 对于这种情况,你需要使用url转换器将地址转换为可正常访问的http地址 |
||||||
|
*/ |
||||||
|
public interface ILiveTsUrlConverter { |
||||||
|
|
||||||
|
/** |
||||||
|
* 处理#EXTINF信息,对于某些服务器,返回的切片信息有可能是相对地址,因此,你需要自行转换为可下载http连接 |
||||||
|
* |
||||||
|
* @param m3u8Url m3u8文件下载地址 |
||||||
|
* @param tsUrl ts文件下载地址 |
||||||
|
* @return 转换后的http地址 |
||||||
|
*/ |
||||||
|
String convert(String m3u8Url, String tsUrl); |
||||||
|
} |
@ -0,0 +1,179 @@ |
|||||||
|
/* |
||||||
|
* 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.core.download.m3u8; |
||||||
|
|
||||||
|
import android.text.TextUtils; |
||||||
|
import com.arialyy.aria.core.common.CompleteInfo; |
||||||
|
import com.arialyy.aria.core.common.IUtil; |
||||||
|
import com.arialyy.aria.core.common.OnFileInfoCallback; |
||||||
|
import com.arialyy.aria.core.download.DTaskWrapper; |
||||||
|
import com.arialyy.aria.core.inf.AbsEntity; |
||||||
|
import com.arialyy.aria.core.inf.IDownloadListener; |
||||||
|
import com.arialyy.aria.core.manager.ThreadTaskManager; |
||||||
|
import com.arialyy.aria.exception.BaseException; |
||||||
|
import com.arialyy.aria.exception.M3U8Exception; |
||||||
|
import com.arialyy.aria.util.ALog; |
||||||
|
import java.util.concurrent.ExecutorService; |
||||||
|
import java.util.concurrent.Executors; |
||||||
|
import java.util.concurrent.ScheduledThreadPoolExecutor; |
||||||
|
import java.util.concurrent.TimeUnit; |
||||||
|
|
||||||
|
/** |
||||||
|
* M3U8直播文件下载工具,对于直播来说,需要定时更新m3u8文件 |
||||||
|
* 工作流程: |
||||||
|
* 1、持续获取切片信息,直到调用停止|取消才停止获取切片信息 |
||||||
|
* 2、完成所有分片下载后,合并ts文件 |
||||||
|
* 3、删除该隐藏文件夹 |
||||||
|
* 4、对于直播来说是没有停止的,停止就代表完成 |
||||||
|
* 5、不处理直播切片下载失败的状态 |
||||||
|
*/ |
||||||
|
public class M3U8LiveDownloadUtil implements IUtil { |
||||||
|
private final String TAG = "M3U8LiveDownloadUtil"; |
||||||
|
|
||||||
|
private DTaskWrapper mWrapper; |
||||||
|
private IDownloadListener mListener; |
||||||
|
private boolean isStop = false, isCancel = false; |
||||||
|
private M3U8LiveLoader mLoader; |
||||||
|
private M3U8InfoThread mInfoThread; |
||||||
|
private ScheduledThreadPoolExecutor mTimer; |
||||||
|
private ExecutorService mInfoPool = Executors.newCachedThreadPool(); |
||||||
|
|
||||||
|
public M3U8LiveDownloadUtil(DTaskWrapper wrapper, IDownloadListener listener) { |
||||||
|
mWrapper = wrapper; |
||||||
|
mListener = listener; |
||||||
|
mLoader = new M3U8LiveLoader(mListener, mWrapper); |
||||||
|
} |
||||||
|
|
||||||
|
@Override public String getKey() { |
||||||
|
return mWrapper.getKey(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override public long getFileSize() { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
@Override public long getCurrentLocation() { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
@Override public boolean isRunning() { |
||||||
|
return mLoader.isRunning(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override public void cancel() { |
||||||
|
isCancel = true; |
||||||
|
mLoader.cancel(); |
||||||
|
if (mInfoThread != null) { |
||||||
|
mInfoThread.setStop(true); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 对于直播来说是没有停止的,停止就代表完成 |
||||||
|
*/ |
||||||
|
@Override public void stop() { |
||||||
|
isStop = true; |
||||||
|
handleComplete(); |
||||||
|
} |
||||||
|
|
||||||
|
private void handleComplete() { |
||||||
|
if (mInfoThread != null) { |
||||||
|
mInfoThread.setStop(true); |
||||||
|
closeTimer(); |
||||||
|
if (mLoader.mergeFile()) { |
||||||
|
mListener.onComplete(); |
||||||
|
} else { |
||||||
|
mListener.onFail(false, new M3U8Exception(TAG, "合并文件失败")); |
||||||
|
} |
||||||
|
} |
||||||
|
ILiveTsUrlConverter converter = mWrapper.asM3U8().getLiveTsUrlConverter(); |
||||||
|
if (converter != null && converter.getClass().isAnonymousClass()) { |
||||||
|
mWrapper.asM3U8().setLiveTsUrlConverter(null); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override public void start() { |
||||||
|
if (isStop || isCancel) { |
||||||
|
return; |
||||||
|
} |
||||||
|
mListener.onPre(); |
||||||
|
getLiveInfo(); |
||||||
|
mLoader.start(); |
||||||
|
startTimer(); |
||||||
|
} |
||||||
|
|
||||||
|
private void startTimer() { |
||||||
|
mTimer = new ScheduledThreadPoolExecutor(1); |
||||||
|
mTimer.scheduleWithFixedDelay(new Runnable() { |
||||||
|
@Override public void run() { |
||||||
|
mInfoThread = (M3U8InfoThread) getLiveInfo(); |
||||||
|
mInfoPool.execute(mInfoThread); |
||||||
|
} |
||||||
|
}, 0, 1000, TimeUnit.MILLISECONDS); |
||||||
|
} |
||||||
|
|
||||||
|
private void closeTimer() { |
||||||
|
if (mTimer != null && !mTimer.isShutdown()) { |
||||||
|
mTimer.shutdown(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override public void setMaxSpeed(int speed) { |
||||||
|
mLoader.setMaxSpeed(speed); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取直播文件信息 |
||||||
|
*/ |
||||||
|
private Runnable getLiveInfo() { |
||||||
|
M3U8InfoThread infoThread = new M3U8InfoThread(mWrapper, new OnFileInfoCallback() { |
||||||
|
@Override public void onComplete(String key, CompleteInfo info) { |
||||||
|
ALog.d(TAG, "更新直播的m3u8文件"); |
||||||
|
} |
||||||
|
|
||||||
|
@Override public void onFail(AbsEntity entity, BaseException e, boolean needRetry) { |
||||||
|
failDownload(e, needRetry); |
||||||
|
} |
||||||
|
}); |
||||||
|
infoThread.setOnGetPeerCallback(new M3U8InfoThread.OnGetLivePeerCallback() { |
||||||
|
@Override public void onGetPeer(String url) { |
||||||
|
ILiveTsUrlConverter converter = mWrapper.asM3U8().getLiveTsUrlConverter(); |
||||||
|
if (converter != null) { |
||||||
|
if (TextUtils.isEmpty(mWrapper.asM3U8().getBandWidthUrl())) { |
||||||
|
url = converter.convert(mWrapper.getEntity().getUrl(), url); |
||||||
|
} else { |
||||||
|
url = converter.convert(mWrapper.asM3U8().getBandWidthUrl(), url); |
||||||
|
} |
||||||
|
} |
||||||
|
if (TextUtils.isEmpty(url) || !url.startsWith("http")) { |
||||||
|
failDownload(new M3U8Exception(TAG, String.format("ts地址错误,url:%s", url)), false); |
||||||
|
return; |
||||||
|
} |
||||||
|
//todo url 需要去重
|
||||||
|
mLoader.offerPeer(url); |
||||||
|
} |
||||||
|
}); |
||||||
|
return infoThread; |
||||||
|
} |
||||||
|
|
||||||
|
private void failDownload(BaseException e, boolean needRetry) { |
||||||
|
if (isStop || isCancel) { |
||||||
|
return; |
||||||
|
} |
||||||
|
handleComplete(); |
||||||
|
mListener.onFail(needRetry, e); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,285 @@ |
|||||||
|
/* |
||||||
|
* 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.core.download.m3u8; |
||||||
|
|
||||||
|
import android.os.Handler; |
||||||
|
import android.os.Looper; |
||||||
|
import android.os.Message; |
||||||
|
import com.arialyy.aria.core.common.IThreadState; |
||||||
|
import com.arialyy.aria.core.common.SubThreadConfig; |
||||||
|
import com.arialyy.aria.core.common.TaskRecord; |
||||||
|
import com.arialyy.aria.core.common.ThreadRecord; |
||||||
|
import com.arialyy.aria.core.download.DTaskWrapper; |
||||||
|
import com.arialyy.aria.core.inf.IEventListener; |
||||||
|
import com.arialyy.aria.core.manager.ThreadTaskManager; |
||||||
|
import com.arialyy.aria.util.ALog; |
||||||
|
import com.arialyy.aria.util.CommonUtil; |
||||||
|
import com.arialyy.aria.util.FileUtil; |
||||||
|
import com.arialyy.aria.util.IdGenerator; |
||||||
|
import java.io.File; |
||||||
|
import java.io.FilenameFilter; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.concurrent.ArrayBlockingQueue; |
||||||
|
import java.util.concurrent.LinkedBlockingQueue; |
||||||
|
import java.util.concurrent.locks.Condition; |
||||||
|
import java.util.concurrent.locks.ReentrantLock; |
||||||
|
|
||||||
|
/** |
||||||
|
* M3U8点播文件下载器 |
||||||
|
*/ |
||||||
|
public class M3U8LiveLoader extends BaseM3U8Loader { |
||||||
|
/** |
||||||
|
* 最大执行数 |
||||||
|
*/ |
||||||
|
private static final int EXEC_MAX_NUM = 4; |
||||||
|
private Handler mStateHandler; |
||||||
|
private ArrayBlockingQueue<Long> mFlagQueue = new ArrayBlockingQueue<>(EXEC_MAX_NUM); |
||||||
|
private LiveStateManager mManager; |
||||||
|
private ReentrantLock LOCK = new ReentrantLock(); |
||||||
|
private Condition mCondition = LOCK.newCondition(); |
||||||
|
private LinkedBlockingQueue<String> mPeerQueue = new LinkedBlockingQueue<>(); |
||||||
|
|
||||||
|
M3U8LiveLoader(IEventListener listener, DTaskWrapper wrapper) { |
||||||
|
super(listener, wrapper); |
||||||
|
} |
||||||
|
|
||||||
|
@Override protected IThreadState getStateManager(Looper looper) { |
||||||
|
mManager = new LiveStateManager(looper, mListener); |
||||||
|
mStateHandler = new Handler(looper, mManager); |
||||||
|
return mManager; |
||||||
|
} |
||||||
|
|
||||||
|
void offerPeer(String peerUrl) { |
||||||
|
mPeerQueue.offer(peerUrl); |
||||||
|
} |
||||||
|
|
||||||
|
@Override protected void handleTask() { |
||||||
|
|
||||||
|
new Thread(new Runnable() { |
||||||
|
@Override public void run() { |
||||||
|
String cacheDir = getCacheDir(); |
||||||
|
int index = 0; |
||||||
|
while (!isBreak()) { |
||||||
|
try { |
||||||
|
LOCK.lock(); |
||||||
|
while (mFlagQueue.size() < EXEC_MAX_NUM) { |
||||||
|
String url = mPeerQueue.poll(); |
||||||
|
if (url == null) { |
||||||
|
break; |
||||||
|
} |
||||||
|
M3U8ThreadTask task = createThreadTask(cacheDir, index, url); |
||||||
|
getTaskList().put(index, task); |
||||||
|
mFlagQueue.offer(startThreadTask(task)); |
||||||
|
index++; |
||||||
|
} |
||||||
|
if (mFlagQueue.size() > 0) { |
||||||
|
mCondition.await(); |
||||||
|
} |
||||||
|
} catch (InterruptedException e) { |
||||||
|
e.printStackTrace(); |
||||||
|
} finally { |
||||||
|
LOCK.unlock(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}).start(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override protected void setMaxSpeed(int maxSpeed) { |
||||||
|
// TODO: 2019-06-06 展不支持
|
||||||
|
} |
||||||
|
|
||||||
|
private void notifyLock() { |
||||||
|
try { |
||||||
|
LOCK.lock(); |
||||||
|
long id = mFlagQueue.take(); |
||||||
|
ALog.d(TAG, String.format("线程【%s】完成", id)); |
||||||
|
mCondition.signalAll(); |
||||||
|
} catch (InterruptedException e) { |
||||||
|
e.printStackTrace(); |
||||||
|
} finally { |
||||||
|
LOCK.unlock(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 启动线程任务 |
||||||
|
* |
||||||
|
* @return 线程唯一id标志 |
||||||
|
*/ |
||||||
|
private long startThreadTask(M3U8ThreadTask task) { |
||||||
|
ThreadTaskManager.getInstance().startThread(mTaskWrapper.getKey(), task); |
||||||
|
return IdGenerator.getInstance().nextId(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 配置config |
||||||
|
*/ |
||||||
|
private M3U8ThreadTask createThreadTask(String cacheDir, int indexId, String tsUrl) { |
||||||
|
ThreadRecord record = new ThreadRecord(); |
||||||
|
record.key = mRecord.filePath; |
||||||
|
record.isComplete = false; |
||||||
|
record.tsUrl = tsUrl; |
||||||
|
record.threadType = TaskRecord.TYPE_M3U8_LIVE; |
||||||
|
record.threadId = indexId; |
||||||
|
|
||||||
|
SubThreadConfig<DTaskWrapper> config = new SubThreadConfig<>(); |
||||||
|
config.url = tsUrl; |
||||||
|
config.tempFile = new File(getTsFilePath(cacheDir, indexId)); |
||||||
|
config.isBlock = mRecord.isBlock; |
||||||
|
config.isOpenDynamicFile = mRecord.isOpenDynamicFile; |
||||||
|
config.taskWrapper = mTaskWrapper; |
||||||
|
config.record = record; |
||||||
|
config.stateHandler = mStateHandler; |
||||||
|
|
||||||
|
if (!config.tempFile.exists()) { |
||||||
|
CommonUtil.createFile(config.tempFile.getPath()); |
||||||
|
} |
||||||
|
return new M3U8ThreadTask(config); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 合并文件 |
||||||
|
* |
||||||
|
* @return {@code true} 合并成功,{@code false}合并失败 |
||||||
|
*/ |
||||||
|
public boolean mergeFile() { |
||||||
|
ITsMergeHandler mergeHandler = mTaskWrapper.asM3U8().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(mTaskWrapper.asM3U8().getKeyInfo(), partPath); |
||||||
|
if (mergeHandler.getClass().isAnonymousClass()) { |
||||||
|
mTaskWrapper.asM3U8().setMergeHandler(null); |
||||||
|
} |
||||||
|
} else { |
||||||
|
isSuccess = FileUtil.mergeFile(mEntity.getFilePath(), partPath); |
||||||
|
} |
||||||
|
if (isSuccess) { |
||||||
|
// 合并成功,删除缓存文件
|
||||||
|
for (String pp : partPath) { |
||||||
|
File f = new File(pp); |
||||||
|
if (f.exists()) { |
||||||
|
f.delete(); |
||||||
|
} |
||||||
|
} |
||||||
|
File cDir = new File(cacheDir); |
||||||
|
if (cDir.exists()) { |
||||||
|
cDir.delete(); |
||||||
|
} |
||||||
|
return true; |
||||||
|
} else { |
||||||
|
ALog.e(TAG, "合并失败"); |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* M3U8线程状态管理 |
||||||
|
*/ |
||||||
|
private class LiveStateManager implements IThreadState { |
||||||
|
private final String TAG = "M3U8ThreadStateManager"; |
||||||
|
|
||||||
|
/** |
||||||
|
* 任务状态回调 |
||||||
|
*/ |
||||||
|
private IEventListener mListener; |
||||||
|
private int mCancelNum = 0; // 已经取消的线程的数
|
||||||
|
private int mStopNum = 0; // 已经停止的线程数
|
||||||
|
private long mProgress; //当前总进度
|
||||||
|
private Looper mLooper; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param listener 任务事件 |
||||||
|
*/ |
||||||
|
LiveStateManager(Looper looper, IEventListener listener) { |
||||||
|
mLooper = looper; |
||||||
|
mListener = listener; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 退出looper循环 |
||||||
|
*/ |
||||||
|
private void quitLooper() { |
||||||
|
ALog.d(TAG, "quitLooper"); |
||||||
|
mLooper.quit(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override public boolean handleMessage(Message msg) { |
||||||
|
switch (msg.what) { |
||||||
|
case STATE_STOP: |
||||||
|
mStopNum++; |
||||||
|
if (isStop()) { |
||||||
|
ALog.d(TAG, "任务停止"); |
||||||
|
mListener.onStop(mProgress); |
||||||
|
quitLooper(); |
||||||
|
} |
||||||
|
break; |
||||||
|
case STATE_CANCEL: |
||||||
|
mCancelNum++; |
||||||
|
if (isCancel()) { |
||||||
|
ALog.d(TAG, "任务取消"); |
||||||
|
mListener.onCancel(); |
||||||
|
quitLooper(); |
||||||
|
} |
||||||
|
break; |
||||||
|
case STATE_COMPLETE: |
||||||
|
notifyLock(); |
||||||
|
break; |
||||||
|
case STATE_RUNNING: |
||||||
|
mProgress += (long) msg.obj; |
||||||
|
break; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@Override public boolean isStop() { |
||||||
|
// 某些服务器后一次性发送多个ts地址,所以不能简单使用mStopNum == mFlagQueue.size();判断状态
|
||||||
|
//return mStopNum == mFlagQueue.size();
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@Override public boolean isFail() { |
||||||
|
// 直播下载不处理失败的切片
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@Override public boolean isComplete() { |
||||||
|
// 直播不处理完成
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@Override public boolean isCancel() { |
||||||
|
//return mCancelNum == mFlagQueue.size();
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@Override public long getCurrentProgress() { |
||||||
|
return mProgress; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,270 @@ |
|||||||
|
/* |
||||||
|
* 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.simple.core.download.m3u8; |
||||||
|
|
||||||
|
import android.arch.lifecycle.Observer; |
||||||
|
import android.arch.lifecycle.ViewModelProviders; |
||||||
|
import android.os.Bundle; |
||||||
|
import android.support.annotation.Nullable; |
||||||
|
import android.util.Log; |
||||||
|
import android.view.Menu; |
||||||
|
import android.view.MenuItem; |
||||||
|
import android.view.View; |
||||||
|
import android.widget.Toast; |
||||||
|
import com.arialyy.annotations.Download; |
||||||
|
import com.arialyy.aria.core.Aria; |
||||||
|
import com.arialyy.aria.core.download.DownloadEntity; |
||||||
|
import com.arialyy.aria.core.download.DownloadTarget; |
||||||
|
import com.arialyy.aria.core.download.DownloadTask; |
||||||
|
import com.arialyy.aria.core.download.m3u8.ILiveTsUrlConverter; |
||||||
|
import com.arialyy.aria.util.ALog; |
||||||
|
import com.arialyy.aria.util.CommonUtil; |
||||||
|
import com.arialyy.frame.util.show.T; |
||||||
|
import com.arialyy.simple.R; |
||||||
|
import com.arialyy.simple.base.BaseActivity; |
||||||
|
import com.arialyy.simple.common.ModifyPathDialog; |
||||||
|
import com.arialyy.simple.common.ModifyUrlDialog; |
||||||
|
import com.arialyy.simple.databinding.ActivityM3u8LiveBinding; |
||||||
|
import java.io.File; |
||||||
|
|
||||||
|
public class M3U8LiveDownloadActivity extends BaseActivity<ActivityM3u8LiveBinding> { |
||||||
|
|
||||||
|
private String mUrl; |
||||||
|
private String mFilePath; |
||||||
|
private M3U8LiveModule mModule; |
||||||
|
private DownloadTarget mTarget; |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void init(Bundle savedInstanceState) { |
||||||
|
super.init(savedInstanceState); |
||||||
|
setTitle(getString(R.string.m3u8_live)); |
||||||
|
Aria.download(this).register(); |
||||||
|
mModule = ViewModelProviders.of(this).get(M3U8LiveModule.class); |
||||||
|
mModule.getHttpDownloadInfo(this).observe(this, new Observer<DownloadEntity>() { |
||||||
|
|
||||||
|
@Override public void onChanged(@Nullable DownloadEntity entity) { |
||||||
|
if (entity == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
mTarget = Aria.download(M3U8LiveDownloadActivity.this).load(entity.getUrl()); |
||||||
|
getBinding().setStateStr(getString(R.string.start)); |
||||||
|
getBinding().setUrl(entity.getUrl()); |
||||||
|
getBinding().setFilePath(entity.getFilePath()); |
||||||
|
mUrl = entity.getUrl(); |
||||||
|
mFilePath = entity.getFilePath(); |
||||||
|
} |
||||||
|
}); |
||||||
|
getBinding().setViewModel(this); |
||||||
|
} |
||||||
|
|
||||||
|
public void chooseUrl() { |
||||||
|
ModifyUrlDialog dialog = |
||||||
|
new ModifyUrlDialog(this, getString(R.string.modify_url_dialog_title), mUrl); |
||||||
|
dialog.show(getSupportFragmentManager(), "ModifyUrlDialog"); |
||||||
|
} |
||||||
|
|
||||||
|
public void chooseFilePath() { |
||||||
|
ModifyPathDialog dialog = |
||||||
|
new ModifyPathDialog(this, getString(R.string.modify_file_path), mFilePath); |
||||||
|
dialog.show(getSupportFragmentManager(), "ModifyPathDialog"); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean onCreateOptionsMenu(Menu menu) { |
||||||
|
getMenuInflater().inflate(R.menu.menu_single_task_activity, menu); |
||||||
|
return super.onCreateOptionsMenu(menu); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean onMenuItemClick(MenuItem item) { |
||||||
|
int speed = -1; |
||||||
|
String msg = ""; |
||||||
|
switch (item.getItemId()) { |
||||||
|
case R.id.help: |
||||||
|
msg = "一些小知识点:\n" |
||||||
|
+ "1、你可以在注解中增加链接,用于指定被注解的方法只能被特定的下载任务回调,以防止progress乱跳\n" |
||||||
|
+ "2、当遇到网络慢的情况时,你可以先使用onPre()更新UI界面,待连接成功时,再在onTaskPre()获取完整的task数据,然后给UI界面设置正确的数据\n" |
||||||
|
+ "3、你可以在界面初始化时通过Aria.download(this).load(URL).getPercent()等方法快速获取相关任务的一些数据"; |
||||||
|
showMsgDialog("tip", msg); |
||||||
|
break; |
||||||
|
case R.id.speed_0: |
||||||
|
speed = 0; |
||||||
|
break; |
||||||
|
case R.id.speed_128: |
||||||
|
speed = 128; |
||||||
|
break; |
||||||
|
case R.id.speed_256: |
||||||
|
speed = 256; |
||||||
|
break; |
||||||
|
case R.id.speed_512: |
||||||
|
speed = 512; |
||||||
|
break; |
||||||
|
case R.id.speed_1m: |
||||||
|
speed = 1024; |
||||||
|
break; |
||||||
|
} |
||||||
|
if (speed > -1) { |
||||||
|
msg = item.getTitle().toString(); |
||||||
|
Aria.download(this).setMaxSpeed(speed); |
||||||
|
T.showShort(this, msg); |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Download.onWait |
||||||
|
void onWait(DownloadTask task) { |
||||||
|
if (task.getKey().equals(mUrl)) { |
||||||
|
Log.d(TAG, "wait ==> " + task.getDownloadEntity().getFileName()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Download.onPre |
||||||
|
protected void onPre(DownloadTask task) { |
||||||
|
if (task.getKey().equals(mUrl)) { |
||||||
|
getBinding().setStateStr(getString(R.string.stop)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Download.onTaskStart |
||||||
|
void taskStart(DownloadTask task) { |
||||||
|
if (task.getKey().equals(mUrl)) { |
||||||
|
getBinding().setFileSize(task.getConvertFileSize()); |
||||||
|
ALog.d(TAG, "isComplete = " + task.isComplete() + ", state = " + task.getState()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Download.onTaskRunning |
||||||
|
protected void running(DownloadTask task) { |
||||||
|
if (task.getKey().equals(mUrl)) { |
||||||
|
ALog.d(TAG, "isRunning"); |
||||||
|
getBinding().setProgress(task.getPercent()); |
||||||
|
getBinding().setSpeed(task.getConvertSpeed()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Download.onTaskResume |
||||||
|
void taskResume(DownloadTask task) { |
||||||
|
if (task.getKey().equals(mUrl)) { |
||||||
|
getBinding().setStateStr(getString(R.string.stop)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Download.onTaskStop |
||||||
|
void taskStop(DownloadTask task) { |
||||||
|
if (task.getKey().equals(mUrl)) { |
||||||
|
getBinding().setStateStr(getString(R.string.resume)); |
||||||
|
getBinding().setSpeed(""); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Download.onTaskCancel |
||||||
|
void taskCancel(DownloadTask task) { |
||||||
|
if (task.getKey().equals(mUrl)) { |
||||||
|
getBinding().setProgress(0); |
||||||
|
getBinding().setStateStr(getString(R.string.start)); |
||||||
|
getBinding().setSpeed(""); |
||||||
|
Log.d(TAG, "cancel"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Download.onTaskFail |
||||||
|
void taskFail(DownloadTask task, Exception e) { |
||||||
|
if (task.getKey().equals(mUrl)) { |
||||||
|
Toast.makeText(M3U8LiveDownloadActivity.this, getString(R.string.download_fail), |
||||||
|
Toast.LENGTH_SHORT) |
||||||
|
.show(); |
||||||
|
getBinding().setStateStr(getString(R.string.start)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Download.onTaskComplete |
||||||
|
void taskComplete(DownloadTask task) { |
||||||
|
if (task.getKey().equals(mUrl)) { |
||||||
|
getBinding().setProgress(100); |
||||||
|
Toast.makeText(M3U8LiveDownloadActivity.this, getString(R.string.download_success), |
||||||
|
Toast.LENGTH_SHORT).show(); |
||||||
|
getBinding().setStateStr(getString(R.string.re_start)); |
||||||
|
getBinding().setSpeed(""); |
||||||
|
ALog.d(TAG, "md5: " + CommonUtil.getFileMD5(new File(task.getDownloadPath()))); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected int setLayoutId() { |
||||||
|
return R.layout.activity_m3u8_live; |
||||||
|
} |
||||||
|
|
||||||
|
public void onClick(View view) { |
||||||
|
switch (view.getId()) { |
||||||
|
case R.id.start: |
||||||
|
ALog.d(TAG, "isRunning = " + mTarget.isRunning()); |
||||||
|
if (mTarget.isRunning()) { |
||||||
|
Aria.download(this).load(mUrl).stop(); |
||||||
|
} else { |
||||||
|
startD(); |
||||||
|
} |
||||||
|
break; |
||||||
|
case R.id.cancel: |
||||||
|
Aria.download(this).load(mUrl).cancel(true); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void startD() { |
||||||
|
Aria.download(M3U8LiveDownloadActivity.this) |
||||||
|
.load(mUrl) |
||||||
|
.useServerFileName(true) |
||||||
|
.setFilePath(mFilePath, true) |
||||||
|
.asM3U8() |
||||||
|
//.setBandWidthUrlConverter(new IBandWidthUrlConverter() {
|
||||||
|
// @Override public String convert(String bandWidthUrl) {
|
||||||
|
// int index = mUrl.lastIndexOf("/");
|
||||||
|
// return mUrl.substring(0, index + 1) + bandWidthUrl;
|
||||||
|
// }
|
||||||
|
//})
|
||||||
|
.asLive() |
||||||
|
.setLiveTsUrlConvert(new ILiveTsUrlConverter() { |
||||||
|
@Override public String convert(String m3u8Url, String tsUrl) { |
||||||
|
int index = m3u8Url.lastIndexOf("/"); |
||||||
|
String parentUrl = m3u8Url.substring(0, index + 1); |
||||||
|
return parentUrl + tsUrl; |
||||||
|
} |
||||||
|
}) |
||||||
|
//.setLiveTsUrlConvert(new IVodTsUrlConverter() {
|
||||||
|
// @Override public List<String> convert(String m3u8Url, List<String> tsUrls) {
|
||||||
|
// int index = m3u8Url.lastIndexOf("/");
|
||||||
|
// String parentUrl = m3u8Url.substring(0, index + 1);
|
||||||
|
// List<String> newUrls = new ArrayList<>();
|
||||||
|
// for (String url : tsUrls) {
|
||||||
|
// newUrls.add(parentUrl + url);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return newUrls;
|
||||||
|
// }
|
||||||
|
//})
|
||||||
|
.start(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override protected void dataCallback(int result, Object data) { |
||||||
|
super.dataCallback(result, data); |
||||||
|
if (result == ModifyUrlDialog.MODIFY_URL_DIALOG_RESULT) { |
||||||
|
mModule.uploadUrl(this, String.valueOf(data)); |
||||||
|
} else if (result == ModifyPathDialog.MODIFY_PATH_RESULT) { |
||||||
|
mModule.updateFilePath(this, String.valueOf(data)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,95 @@ |
|||||||
|
/* |
||||||
|
* 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.simple.core.download.m3u8; |
||||||
|
|
||||||
|
import android.arch.lifecycle.LiveData; |
||||||
|
import android.arch.lifecycle.MutableLiveData; |
||||||
|
import android.content.Context; |
||||||
|
import android.os.Environment; |
||||||
|
import android.text.TextUtils; |
||||||
|
import com.arialyy.aria.core.Aria; |
||||||
|
import com.arialyy.aria.core.download.DownloadEntity; |
||||||
|
import com.arialyy.aria.util.ALog; |
||||||
|
import com.arialyy.frame.base.BaseViewModule; |
||||||
|
import com.arialyy.simple.util.AppUtil; |
||||||
|
import java.io.File; |
||||||
|
|
||||||
|
public class M3U8LiveModule extends BaseViewModule { |
||||||
|
private final String M3U8_LIVE_URL_KEY = "M3U8_LIVE_URL_KEY"; |
||||||
|
private final String M3U8_LIVE_PATH_KEY = "M3U8_LIVE_PATH_KEY"; |
||||||
|
// 多码率地址:
|
||||||
|
private final String defUrl = "http://ivi.bupt.edu.cn/hls/cctv6hd.m3u8"; |
||||||
|
private final String filePath = |
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath() |
||||||
|
+ "/live.ts"; |
||||||
|
|
||||||
|
private MutableLiveData<DownloadEntity> liveData = new MutableLiveData<>(); |
||||||
|
private DownloadEntity singDownloadInfo; |
||||||
|
|
||||||
|
/** |
||||||
|
* 单任务下载的信息 |
||||||
|
*/ |
||||||
|
LiveData<DownloadEntity> getHttpDownloadInfo(Context context) { |
||||||
|
String url = AppUtil.getConfigValue(context, M3U8_LIVE_URL_KEY, defUrl); |
||||||
|
String filePath = AppUtil.getConfigValue(context, M3U8_LIVE_PATH_KEY, this.filePath); |
||||||
|
|
||||||
|
singDownloadInfo = Aria.download(context).getDownloadEntity(url); |
||||||
|
if (singDownloadInfo == null) { |
||||||
|
singDownloadInfo = new DownloadEntity(); |
||||||
|
singDownloadInfo.setUrl(url); |
||||||
|
File temp = new File(this.filePath); |
||||||
|
singDownloadInfo.setFilePath(filePath); |
||||||
|
singDownloadInfo.setFileName(temp.getName()); |
||||||
|
} else { |
||||||
|
AppUtil.setConfigValue(context, M3U8_LIVE_PATH_KEY, singDownloadInfo.getDownloadPath()); |
||||||
|
AppUtil.setConfigValue(context, M3U8_LIVE_URL_KEY, singDownloadInfo.getUrl()); |
||||||
|
} |
||||||
|
liveData.postValue(singDownloadInfo); |
||||||
|
|
||||||
|
return liveData; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 更新文件保存路径 |
||||||
|
* |
||||||
|
* @param filePath 文件保存路径 |
||||||
|
*/ |
||||||
|
void updateFilePath(Context context, String filePath) { |
||||||
|
if (TextUtils.isEmpty(filePath)) { |
||||||
|
ALog.e(TAG, "文件保存路径为空"); |
||||||
|
return; |
||||||
|
} |
||||||
|
File temp = new File(filePath); |
||||||
|
AppUtil.setConfigValue(context, M3U8_LIVE_PATH_KEY, filePath); |
||||||
|
singDownloadInfo.setFileName(temp.getName()); |
||||||
|
singDownloadInfo.setFilePath(filePath); |
||||||
|
liveData.postValue(singDownloadInfo); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 更新url |
||||||
|
*/ |
||||||
|
void uploadUrl(Context context, String url) { |
||||||
|
if (TextUtils.isEmpty(url)) { |
||||||
|
ALog.e(TAG, "下载地址为空"); |
||||||
|
return; |
||||||
|
} |
||||||
|
AppUtil.setConfigValue(context, M3U8_LIVE_URL_KEY, url); |
||||||
|
singDownloadInfo.setUrl(url); |
||||||
|
liveData.postValue(singDownloadInfo); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
android:height="24dp" |
||||||
|
android:viewportHeight="1024" |
||||||
|
android:viewportWidth="1024" |
||||||
|
android:width="24dp"> |
||||||
|
<path |
||||||
|
android:fillColor="@color/icon_color" |
||||||
|
android:pathData="M690.42,297.75c3.04,0 5.23,0 7.42,0 24.08,0 48.29,-0.73 72.37,0.36 34.79,1.58 65.19,13.99 88.18,41.11 14.6,17.15 22.01,37.46 22.01,59.84 0.24,123.21 0.49,246.54 -0.12,369.75 -0.12,35.15 -12.53,66.29 -39.53,90.25 -17.27,15.45 -37.95,22.99 -61.06,23.6 -16.18,0.36 -32.48,0.12 -48.65,0.12 -155.08,0 -310.16,-0.49 -465.23,0.24 -59.11,0.24 -110.8,-45 -120.41,-99.61 -1.34,-7.78 -1.7,-15.69 -1.7,-23.6 -0.12,-117.37 0.61,-234.75 -0.36,-352.12 -0.49,-59.36 38.8,-95.11 84.65,-105.82 11.92,-2.8 24.45,-3.65 36.73,-3.89 24.08,-0.61 48.17,-0.12 73.34,-0.12 -1.34,-2.55 -2.19,-4.5 -3.16,-6.32 -9.37,-16.54 -18.85,-33.08 -28.1,-49.75 -6.57,-11.92 -4.14,-25.06 5.84,-33.57 6.81,-5.84 13.26,-7.3 20.56,-2.07 7.42,5.35 14.47,11.8 19.95,19.22 14.96,20.43 29.68,41.11 43.06,62.64 5.23,8.39 10.58,10.1 19.58,10.1 67.87,-0.36 135.86,-0.24 203.73,-0.12 4.87,0 7.54,-1.34 9.85,-5.72 12.65,-23.84 25.54,-47.44 38.56,-71.03 5.96,-10.83 15.57,-15.69 27.85,-14.47 11.07,0.97 18.73,7.05 23.11,17.27 3.53,8.15 2.55,16.3 -1.58,23.96 -7.78,14.35 -15.69,28.83 -23.6,43.18C692.61,293.01 691.76,294.95 690.42,297.75zM518.56,834.87c84.9,0 169.79,-0.12 254.81,0.12 23.84,0.12 39.89,-10.95 50.48,-31.38 7.05,-13.74 9,-28.46 9,-43.54 0,-87.82 0,-175.51 0,-263.33 0,-30.65 -0.12,-61.18 0,-91.83 0.12,-23.6 -11.07,-40.02 -31.62,-50.48 -13.26,-6.81 -27.61,-8.88 -42.33,-8.88 -164.56,0 -329.13,0 -493.69,0.12 -7.66,0 -15.45,0.73 -22.87,2.19 -33.93,6.69 -50.96,27.61 -50.96,62.03 0,116.76 0.61,233.53 -0.24,350.29 -0.24,40.38 36.37,75.9 75.29,75.29C350.35,834.01 434.52,834.87 518.56,834.87z"/> |
||||||
|
<path |
||||||
|
android:fillColor="@color/icon_color" |
||||||
|
android:pathData="M446.31,601.95c0,-28.58 0,-57.17 0,-85.75 0,-15.33 8.88,-20.68 22.01,-13.26 50.6,28.1 101.2,56.31 151.67,84.53 1.58,0.85 3.16,1.7 4.74,2.68 10.22,6.32 10.1,18.37 -0.36,24.45 -11.31,6.69 -22.87,13.01 -34.42,19.46 -40.62,22.62 -81.37,45.25 -121.99,67.87 -13.01,7.18 -21.77,1.95 -21.77,-13.01C446.31,659.84 446.31,630.89 446.31,601.95z"/> |
||||||
|
</vector> |
@ -0,0 +1,112 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<layout xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
xmlns:bind="http://schemas.android.com/apk/res-auto" |
||||||
|
> |
||||||
|
|
||||||
|
<data> |
||||||
|
<variable |
||||||
|
name="fileSize" |
||||||
|
type="String" |
||||||
|
/> |
||||||
|
<variable |
||||||
|
name="speed" |
||||||
|
type="String" |
||||||
|
/> |
||||||
|
<variable |
||||||
|
name="progress" |
||||||
|
type="int" |
||||||
|
/> |
||||||
|
<variable |
||||||
|
name="stateStr" |
||||||
|
type="String" |
||||||
|
/> |
||||||
|
|
||||||
|
<variable |
||||||
|
name="url" |
||||||
|
type="String" |
||||||
|
/> |
||||||
|
<variable |
||||||
|
name="filePath" |
||||||
|
type="String" |
||||||
|
/> |
||||||
|
<variable |
||||||
|
name="hint" |
||||||
|
type="String" |
||||||
|
/> |
||||||
|
<variable |
||||||
|
name="viewModel" |
||||||
|
type="com.arialyy.simple.core.download.m3u8.M3U8LiveDownloadActivity" |
||||||
|
/> |
||||||
|
</data> |
||||||
|
|
||||||
|
<LinearLayout |
||||||
|
xmlns:tools="http://schemas.android.com/tools" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="match_parent" |
||||||
|
android:fitsSystemWindows="true" |
||||||
|
android:orientation="vertical" |
||||||
|
tools:context=".core.download.SingleTaskActivity" |
||||||
|
> |
||||||
|
|
||||||
|
<include layout="@layout/layout_bar"/> |
||||||
|
|
||||||
|
<com.arialyy.simple.widget.SvgTextView |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:layout_marginLeft="16dp" |
||||||
|
android:layout_marginRight="16dp" |
||||||
|
android:layout_marginTop="16dp" |
||||||
|
bind:iconClickListener="@{() -> viewModel.chooseUrl()}" |
||||||
|
bind:svg_text_view_icon="@drawable/ic_modify" |
||||||
|
bind:text="@{@string/url(url)}" |
||||||
|
/> |
||||||
|
|
||||||
|
<com.arialyy.simple.widget.SvgTextView |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:layout_marginLeft="16dp" |
||||||
|
android:layout_marginRight="16dp" |
||||||
|
android:layout_marginTop="8dp" |
||||||
|
bind:iconClickListener="@{() -> viewModel.chooseFilePath()}" |
||||||
|
bind:svg_text_view_icon="@drawable/ic_choose_file" |
||||||
|
bind:text="@{@string/file_path(filePath)}" |
||||||
|
/> |
||||||
|
|
||||||
|
<TextView |
||||||
|
android:id="@+id/hint" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:text="@{hint}" |
||||||
|
android:textColor="@color/black" |
||||||
|
/> |
||||||
|
|
||||||
|
<LinearLayout |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:orientation="horizontal" |
||||||
|
> |
||||||
|
<TextView |
||||||
|
android:id="@+id/speed" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:layout_marginLeft="16dp" |
||||||
|
android:layout_weight="1" |
||||||
|
android:text="@{speed}" |
||||||
|
android:textColor="@color/black" |
||||||
|
/> |
||||||
|
|
||||||
|
<Button |
||||||
|
android:id="@+id/start" |
||||||
|
android:layout_width="0dp" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:layout_weight="1" |
||||||
|
android:onClick="onClick" |
||||||
|
android:text="@{stateStr ?? @string/start}" |
||||||
|
style="?buttonBarButtonStyle" |
||||||
|
/> |
||||||
|
|
||||||
|
</LinearLayout> |
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout> |
||||||
|
</layout> |
Loading…
Reference in new issue