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