初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
TODO: Add your license here.
+15
View File
@@ -0,0 +1,15 @@
# m3u8_downloader
m3u8下载器
## Getting Started
This project is a starting point for a Flutter
[plug-in package](https://flutter.dev/developing-packages/),
a specialized package that includes platform-specific implementation code for
Android and/or iOS.
For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
@@ -0,0 +1,31 @@
group 'com.vincent.m3u8Downloader'
version '1.0'
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
}
}
rootProject.allprojects {
repositories {
google()
jcenter()
}
}
apply plugin: 'com.android.library'
android {
compileSdkVersion 36
defaultConfig {
minSdkVersion 24
}
}
@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
@@ -0,0 +1 @@
rootProject.name = 'm3u8Downloader'
@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.vincent.m3u8Downloader">
</manifest>
@@ -0,0 +1,100 @@
package com.vincent.m3u8Downloader;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.AssetManager;
import java.util.concurrent.atomic.AtomicBoolean;
import io.flutter.FlutterInjector;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.loader.FlutterLoader;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.JSONMethodCodec;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.view.FlutterCallbackInformation;
/**
* @Author: Vincent
* @CreateAt: 2021/08/26 16:19
* @Desc: 初始化运行回调调度程序的后台隔离,用于在后台启动时调用Dart回调。
*/
public class FlutterBackgroundExecutor implements MethodChannel.MethodCallHandler {
public static final String SHARED_PREFERENCES_KEY = "vincent.m3u8.downloader.pref";
public static final String CALLBACK_DISPATCHER_HANDLE_KEY = "callback_dispatcher_handle_key";
private static final String TAG = "M3u8Downloader background";
private MethodChannel backgroundChannel;
private FlutterEngine backgroundFlutterEngine;
private final AtomicBoolean isCallbackDispatcherReady = new AtomicBoolean(false);
public static void setCallbackDispatcher(Context context, long callbackHandle) {
SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0);
prefs.edit().putLong(CALLBACK_DISPATCHER_HANDLE_KEY, callbackHandle).apply();
}
public boolean isRunning() {
return isCallbackDispatcherReady.get();
}
private void onInitialized() {
isCallbackDispatcherReady.set(true);
}
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
String method = call.method;
if (method.equals("didInitializeDispatcher")) {
onInitialized();
result.success(true);
} else {
result.notImplemented();
}
}
void startBackgroundIsolate(Context context) {
if (!isRunning()) {
SharedPreferences p = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0);
long callbackHandle = p.getLong(CALLBACK_DISPATCHER_HANDLE_KEY, 0);
startBackgroundIsolate(context, callbackHandle);
}
}
public void startBackgroundIsolate(Context context, long callbackHandle) {
if (backgroundFlutterEngine != null) {
Log.e(TAG, "Background isolate already started");
return;
}
Log.i(TAG, "Starting Background isolate...");
FlutterLoader flutterLoader = FlutterInjector.instance().flutterLoader();
flutterLoader.startInitialization(context);
flutterLoader.ensureInitializationComplete(context, null);
String appBundlePath = flutterLoader.findAppBundlePath();
AssetManager assets = context.getAssets();
if (appBundlePath != null && !isRunning()) {
backgroundFlutterEngine = new FlutterEngine(context);
FlutterCallbackInformation flutterCallback = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle);
if (flutterCallback == null) {
Log.e(TAG, "Fatal: failed to find callback");
return;
}
DartExecutor executor = backgroundFlutterEngine.getDartExecutor();
initializeMethodChannel(executor);
DartExecutor.DartCallback dartCallback = new DartExecutor.DartCallback(assets, appBundlePath, flutterCallback);
executor.executeDartCallback(dartCallback);
}
}
private void initializeMethodChannel(BinaryMessenger isolate) {
backgroundChannel = new MethodChannel(isolate, "vincent/m3u8_downloader_background", JSONMethodCodec.INSTANCE);
backgroundChannel.setMethodCallHandler(this);
}
public void executeDartCallbackInBackgroundIsolate(long callbackHandle, Object args) {
backgroundChannel.invokeMethod("", new Object[]{callbackHandle, args});
}
}
@@ -0,0 +1,389 @@
package com.vincent.m3u8Downloader;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import io.flutter.Log;
import com.vincent.m3u8Downloader.bean.M3U8;
import com.vincent.m3u8Downloader.bean.M3U8Task;
import com.vincent.m3u8Downloader.bean.M3U8TaskState;
import com.vincent.m3u8Downloader.downloader.M3U8DownloadConfig;
import com.vincent.m3u8Downloader.downloader.M3U8DownloadTask;
import com.vincent.m3u8Downloader.downloader.M3U8Downloader;
import com.vincent.m3u8Downloader.listener.OnM3U8DownloadListener;
import com.vincent.m3u8Downloader.utils.M3U8Log;
import com.vincent.m3u8Downloader.utils.M3U8Util;
import com.vincent.m3u8Downloader.utils.NotificationUtil;
import org.json.JSONObject;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.JSONMethodCodec;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry;
/** M3U8DownloaderPlugin */
public class M3U8DownloaderPlugin implements FlutterPlugin, MethodCallHandler, PluginRegistry.NewIntentListener, ActivityAware {
private static final String CHANNEL_NAME = "m3u8_downloader";
private MethodChannel channel;
private Context context;
private Activity mainActivity;
private Handler handler;
private final Object initializationLock = new Object();
private boolean showNotification = true;
private final FlutterBackgroundExecutor backgroundExecutor = new FlutterBackgroundExecutor();
private String fileName = "";
private long progressCallbackHandle = -1;
private long successCallbackHandle = -1;
private long errorCallbackHandle = -1;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger());
}
private void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger) {
synchronized (initializationLock) {
if (channel != null) {
return;
}
this.context = applicationContext;
handler = new Handler(Looper.getMainLooper());
channel = new MethodChannel( messenger, CHANNEL_NAME, JSONMethodCodec.INSTANCE);
channel.setMethodCallHandler(this);
}
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) {
String method = call.method;
switch (method) {
case "initialize":
long callbackHandle = call.argument("handle");
FlutterBackgroundExecutor.setCallbackDispatcher(context, callbackHandle);
backgroundExecutor.startBackgroundIsolate(context);
result.success(true);
break;
case "config":
try {
M3U8DownloadConfig config = M3U8DownloadConfig.build(context);
if (call.hasArgument("saveDir") && call.argument("saveDir") != JSONObject.NULL) {
String saveDir = call.argument("saveDir");
config.setSaveDir(saveDir);
}
if (call.hasArgument("showNotification") && call.argument("showNotification") != JSONObject.NULL) {
boolean show = call.argument("showNotification");
showNotification = show;
config.setShowNotification(show);
}
if (call.hasArgument("connTimeout") && call.argument("connTimeout") != JSONObject.NULL) {
int connTimeout = call.argument("connTimeout");
config.setConnTimeout(connTimeout);
}
if (call.hasArgument("readTimeout") && call.argument("readTimeout") != JSONObject.NULL) {
int readTimeout = call.argument("readTimeout");
config.setReadTimeout(readTimeout);
}
if (call.hasArgument("threadCount") && call.argument("threadCount") != JSONObject.NULL) {
int threadCount = call.argument("threadCount");
config.setThreadCount(threadCount);
}
if (call.hasArgument("debugMode") && call.argument("debugMode") != JSONObject.NULL) {
boolean debugMode = call.argument("debugMode");
config.setDebugMode(debugMode);
}
if (call.hasArgument("convertMp4") && call.argument("convertMp4") != JSONObject.NULL) {
boolean convertMp4 = call.argument("convertMp4");
config.setConvertMp4(convertMp4);
}
progressCallbackHandle = call.hasArgument("progressCallback") && call.argument("progressCallback") != JSONObject.NULL ? (long) call.argument("progressCallback") : -1;
successCallbackHandle = call.hasArgument("successCallback") && call.argument("successCallback") != JSONObject.NULL ? (long) call.argument("successCallback") : -1;
errorCallbackHandle = call.hasArgument("errorCallback") && call.argument("errorCallback") != JSONObject.NULL ? (long) call.argument("errorCallback") : -1;
result.success(true);
}catch (Error e){
result.success(false);
}
break;
case "download":
if (!call.hasArgument("url")) {
result.error("1", "url必传", "");
return;
}
if (!call.hasArgument("name")) {
result.error("1", "name必传", "");
return;
}
showNotification = M3U8DownloadConfig.isShowNotification();
String url = call.argument("url");
if(M3U8Downloader.getInstance().isFinished(url)){
result.success("已下载完成了");
return;
}
String currentTaskUrl = null;
if(M3U8Downloader.getInstance().m3U8DownLoadTask != null){
currentTaskUrl = M3U8Downloader.getInstance().m3U8DownLoadTask.m3u8Url;
}
if(currentTaskUrl != null
&& currentTaskUrl.equals(url)
&& M3U8Downloader.getInstance().m3U8DownLoadTask.isRunning()){
result.success("正在执行");
return;
}
fileName = call.argument("name");
NotificationUtil.getInstance().cancel();
if (showNotification) {
NotificationUtil.getInstance().build(context);
}
M3U8Downloader.getInstance().setOnM3U8DownloadListener(mDownloadListener);
M3U8Downloader.getInstance().download(url);
result.success(null);
break;
case "searchInfo":
if (!call.hasArgument("url")) {
result.error("1", "url必传", "");
return;
}
String movieUrl = call.argument("url");
Map infoMap = M3U8Downloader.getInstance().searchTaskInfo(movieUrl);
result.success(infoMap);
break;
case "pause":
// progressCallbackHandle = -1;
// successCallbackHandle = -1;
// errorCallbackHandle = -1;
if (!call.hasArgument("url")) {
result.error("1", "url必传", "");
return;
}
String pauseUrl = call.argument("url");
M3U8Downloader.getInstance().pause(pauseUrl);
result.success(null);
break;
case "pauseAll":
// progressCallbackHandle = -1;
// successCallbackHandle = -1;
// errorCallbackHandle = -1;
M3U8Downloader.getInstance().pauseAll();
result.success(null);
break;
case "delete":
if (!call.hasArgument("url")) {
result.error("1", "url必传", "");
return;
}
final String deleteUrl = call.argument("url");
handler.post(new Runnable() {
@Override
public void run() {
boolean flag = M3U8Downloader.getInstance().delete(deleteUrl);
result.success(flag);
}
});
break;
case "deleteAll":
handler.post(new Runnable() {
@Override
public void run() {
boolean flag = M3U8Downloader.getInstance().deleteAll();
result.success(flag);
}
});
break;
case "isRunning":
result.success(M3U8Downloader.getInstance().isRunning());
break;
case "getSavePath":
if (!call.hasArgument("url")) {
result.error("1", "url必传", "");
return;
}
String saveUrl = call.argument("url");
Map<String, String> res = new HashMap<>();
res.put("baseDir", M3U8Util.getSaveFileDir(saveUrl));
res.put("baseDirTmp", M3U8Util.getSaveFileDirTmp(saveUrl));
res.put("m3u8", M3U8DownloadTask.getM3U8Path(saveUrl));
result.success(res);
break;
default:
result.notImplemented();
break;
}
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
}
final OnM3U8DownloadListener mDownloadListener = new OnM3U8DownloadListener() {
@Override
public void onDownloadPrepare(M3U8Task task) {
if (showNotification) {
NotificationUtil.getInstance().updateNotification(fileName, M3U8TaskState.PREPARE, 0);
}
}
@Override
public void onDownloadPending(M3U8Task task) {
if (showNotification) {
NotificationUtil.getInstance().updateNotification(fileName, M3U8TaskState.PENDING, Math.round(task.getProgress() * 100));
}
}
@Override
public void onDownloadProgress(M3U8DownloadTask task) {
if (progressCallbackHandle != -1) {
Log.e("load proress callback:", "!=-1");
final Map<String, Object> args = new HashMap<>();
args.put("url", task.m3u8Url);
args.put("progress", task.progress());
handler.post(new Runnable() {
@Override
public void run() {
Log.e("load proress callback run:", "");
backgroundExecutor.executeDartCallbackInBackgroundIsolate(progressCallbackHandle, args);
}
});
}else {
Log.e("load proress callback:", "==-1");
}
}
@Override
public void onDownloadItem(M3U8Task task, long itemFileSize, int totalTs, int curTs) {
}
@Override
public void onDownloadSuccess(M3U8DownloadTask task) {
; if (showNotification) {
NotificationUtil.getInstance().updateNotification(fileName, M3U8TaskState.SUCCESS, 100);
}
String saveDir = M3U8Util.getSaveFileDir(task.m3u8Url);
String filePath = "";
if (task.m3u8Url != null) {
filePath = task.currentM3U8.getLocalPath();
}
final Map<String, Object> args = new HashMap<>();
args.put("url", task.m3u8Url);
args.put("dir", saveDir);
args.put("filePath", filePath);
//下载成功
if (successCallbackHandle != -1) {
handler.post(new Runnable() {
@Override
public void run() {
backgroundExecutor.executeDartCallbackInBackgroundIsolate(successCallbackHandle, args);
}
});
}
}
@Override
public void onDownloadPause(M3U8Task task) {
if (showNotification) {
NotificationUtil.getInstance().updateNotification(fileName, M3U8TaskState.PAUSE, Math.round(task.getProgress() * 100));
}
}
@Override
public void onConvert() {
if (showNotification) {
NotificationCompat.Builder builder = NotificationUtil.getInstance().getBuilder();
if (builder == null) return;
builder.setContentText("正在转成MP4")
.setProgress(100, 100, true)
.setOngoing(true)
.setSmallIcon(android.R.drawable.stat_sys_download);
NotificationManagerCompat.from(context).notify(NotificationUtil.NOTIFICATION_ID, builder.build());
}
}
@Override
public void onDownloadError(String url, Throwable error) {
if (errorCallbackHandle != -1) {
final Map<String, Object> args = new HashMap<>();
args.put("url", url);
if(error != null) {
args.put("error", error.getMessage());
}else {
args.put("error", "no reason");
}
handler.post(new Runnable() {
@Override
public void run() {
backgroundExecutor.executeDartCallbackInBackgroundIsolate(errorCallbackHandle, args);
}
});
}
}
@Override
public void onStop(M3U8Task task) {
if (showNotification) {
NotificationUtil.getInstance().cancel();
}
}
};
@Override
public boolean onNewIntent(Intent intent) {
if (NotificationUtil.ACTION_SELECT_NOTIFICATION.equals(intent.getAction())) {
M3U8Log.d("selectNotification");
channel.invokeMethod("selectNotification", null);
if (mainActivity != null) {
mainActivity.setIntent(intent);
}
return true;
}
return false;
}
@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
binding.addOnNewIntentListener(this);
mainActivity = binding.getActivity();
}
@Override
public void onDetachedFromActivityForConfigChanges() {
this.mainActivity = null;
}
@Override
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
onAttachedToActivity(binding);
}
@Override
public void onDetachedFromActivity() {
this.mainActivity = null;
}
}
@@ -0,0 +1,111 @@
package com.vincent.m3u8Downloader.bean;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: Vincent
* @CreateAt: 2021/08/25 16:31
* @Desc: m3u8实体类
*/
public class M3U8 {
private String baseUrl;
private String dirPath;
private String localPath;
private String key;
private String iv;
public String methodCode;
public String methodKeyURL;
public int localTSFileCount = 0;
private List<M3U8Ts> tsList = new ArrayList<>();
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public String getDirPath() {
return dirPath;
}
public void setDirPath(String dirPath) {
this.dirPath = dirPath;
}
public String getLocalPath() {
return localPath;
}
public void setLocalPath(String localPath) {
this.localPath = localPath;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getIv() {
return iv;
}
public void setIv(String iv) {
this.iv = iv;
}
public List<M3U8Ts> getTsList() {
return tsList;
}
public void setTsList(List<M3U8Ts> tsList) {
this.tsList = tsList;
}
public void addTs(M3U8Ts ts) {
this.tsList.add(ts);
}
public long getTotalFileSize() {
long fileSize = 0;
for (M3U8Ts m3U8Ts : tsList){
fileSize = fileSize + m3U8Ts.getFileSize();
}
return fileSize;
}
public long getTotalTime() {
long totalTime = 0;
for (M3U8Ts m3U8Ts : tsList){
totalTime = totalTime + (int)(m3U8Ts.getSeconds() * 1000);
}
return totalTime;
}
@Override
public String toString() {
return "M3U8{" +
"basePath='" + baseUrl + '\'' +
", dirPath='" + dirPath + '\'' +
", localPath='" + localPath + '\'' +
", key='" + key + '\'' +
", iv='" + iv + '\'' +
", totalFileSize=" + getTotalFileSize() +
", totalTime=" + getTotalTime() +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
M3U8 m3U8 = (M3U8) o;
return baseUrl.equals(m3U8.baseUrl);
}
}
@@ -0,0 +1,93 @@
package com.vincent.m3u8Downloader.bean;
import com.vincent.m3u8Downloader.utils.M3U8Util;
/**
* @Author: Vincent
* @CreateAt: 2021/08/25 17:20
* @Desc: M3U8下载任务
*/
public class M3U8Task {
private String url;
private M3U8TaskState state = M3U8TaskState.DEFAULT;
private long speed;
private float progress;
private M3U8 m3U8;
private M3U8Task() {}
public M3U8Task(String url){
this.url = url;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public M3U8TaskState getState() {
return state;
}
public void setState(M3U8TaskState state) {
this.state = state;
}
public long getSpeed() {
return speed;
}
public void setSpeed(long speed) {
this.speed = speed;
}
public float getProgress() {
return progress;
}
public void setProgress(float progress) {
this.progress = progress;
}
public M3U8 getM3U8() {
return m3U8;
}
public void setM3U8(M3U8 m3U8) {
this.m3U8 = m3U8;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
M3U8Task m3U8Task = (M3U8Task) o;
return url.equals(m3U8Task.url);
}
public String getFormatSpeed() {
if (speed == 0) return "";
return M3U8Util.formatFileSize(speed) + "/s";
}
public long getTotalSize() {
if (m3U8 == null) return 0;
return m3U8.getTotalFileSize();
}
public String getFormatTotalSize() {
if (m3U8 == null) return "";
long fileSize = getTotalSize();
if (fileSize == 0) return "";
return M3U8Util.formatFileSize(fileSize);
}
public String getFormatCurrentSize() {
if (m3U8 == null)return "";
return M3U8Util.formatFileSize((long)(progress * m3U8.getTotalFileSize()));
}
}
@@ -0,0 +1,41 @@
package com.vincent.m3u8Downloader.bean;
/**
* @Author: Vincent
* @CreateAt: 2021/08/25 17:18
* @Desc: 下载任务状态
*/
public enum M3U8TaskState {
/**
* 默认状态
*/
DEFAULT,
/**
* 下载排队中
*/
PENDING,
/**
* 下载准备中
*/
PREPARE,
/**
* 正在下载中
*/
DOWNLOADING,
/**
* 下载成功
*/
SUCCESS,
/**
* 下载失败
*/
ERROR,
/**
* 暂停下载
*/
PAUSE,
/**
* 存储空间不足
*/
ENOSPC,
}
@@ -0,0 +1,95 @@
package com.vincent.m3u8Downloader.bean;
import android.util.Log;
import com.vincent.m3u8Downloader.utils.EncryptUtil;
import java.net.MalformedURLException;
import java.net.URL;
/**
* @Author: Vincent
* @CreateAt: 2021/08/25 16:36
* @Desc: m3u8切片
*/
public class M3U8Ts implements Comparable<M3U8Ts> {
/**
* ts网络请求地址(完整的网络请求地址请使用obtainFullUrl
*/
private String url;
/**
* 文件大小
*/
private long fileSize;
/**
* ts秒数
*/
private float seconds;
public M3U8Ts(String url, float seconds) {
this.url = url;
this.seconds = seconds;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public long getFileSize() {
return fileSize;
}
public void setFileSize(long fileSize) {
this.fileSize = fileSize;
}
public float getSeconds() {
return seconds;
}
public void setSeconds(float seconds) {
this.seconds = seconds;
}
@Override
public int compareTo(M3U8Ts m3U8Ts) {
return url.compareTo(m3U8Ts.url);
}
@Override
public String toString() {
return "M3U8Ts{" +
"url='" + url + '\'' +
", fileSize=" + fileSize +
", seconds=" + seconds +
'}';
}
/**
* 获取加密后的文件名
* @return ts文件名
*/
public String obtainEncodeTsFileName(){
if (url == null) return "error.ts";
String fileName = EncryptUtil.md5Encode(url).concat(".ts");
return fileName;
}
/**
* 获取完整的URL地址
* @param hostUrl host地址
* @return URL地址
*/
public URL obtainFullUrl(String hostUrl) throws MalformedURLException {
if (url == null || hostUrl == null) {
return null;
}
URL host = new URL(hostUrl);
return new URL(host, url);
}
}
@@ -0,0 +1,92 @@
package com.vincent.m3u8Downloader.downloader;
import android.content.Context;
import android.os.Environment;
import com.vincent.m3u8Downloader.utils.SpHelper;
import java.io.File;
/**
* @Author: Vincent
* @CreateAt: 2021/08/25 21:37
* @Desc: 配置类
*/
public class M3U8DownloadConfig {
private static final String TAG_SAVE_DIR = "TAG_SAVE_DIR_M3U8";
private static final String TAG_THREAD_COUNT = "TAG_THREAD_COUNT_M3U8";
private static final String TAG_CONN_TIMEOUT = "TAG_CONN_TIMEOUT_M3U8";
private static final String TAG_READ_TIMEOUT = "TAG_READ_TIMEOUT_M3U8";
private static final String TAG_DEBUG = "TAG_DEBUG_M3U8";
private static final String TAG_SHOW_NOTIFICATION = "TAG_SHOW_NOTIFICATION_M3U8";
private static final String TAG_CONVERT_MP4 = "TAG_CONVERT_MP4";
public static M3U8DownloadConfig build(Context context){
SpHelper.init(context);
return new M3U8DownloadConfig();
}
public M3U8DownloadConfig setSaveDir(String saveDir){
SpHelper.putString(TAG_SAVE_DIR, saveDir);
return this;
}
@SuppressWarnings("deprecation")
public static String getSaveDir(){
return SpHelper.getString(TAG_SAVE_DIR, Environment.getExternalStorageDirectory().getPath() + File.separator + "M3u8Downloader");
}
public M3U8DownloadConfig setThreadCount(int threadCount){
if (threadCount > 5) threadCount = 5;
if (threadCount <= 0) threadCount = 1;
SpHelper.putInt(TAG_THREAD_COUNT, threadCount);
return this;
}
public static int getThreadCount(){
return SpHelper.getInt(TAG_THREAD_COUNT, 3);
}
public M3U8DownloadConfig setConnTimeout(int connTimeout){
SpHelper.putInt(TAG_CONN_TIMEOUT, connTimeout);
return this;
}
public static int getConnTimeout(){
return SpHelper.getInt(TAG_CONN_TIMEOUT, 10 * 1000);
}
public M3U8DownloadConfig setReadTimeout(int readTimeout){
SpHelper.putInt(TAG_READ_TIMEOUT, readTimeout);
return this;
}
public static int getReadTimeout(){
return SpHelper.getInt(TAG_READ_TIMEOUT, 30 * 60 * 1000);
}
public M3U8DownloadConfig setDebugMode(boolean debug){
SpHelper.putBoolean(TAG_DEBUG, debug);
return this;
}
public static boolean isDebugMode(){
return SpHelper.getBoolean(TAG_DEBUG, false);
}
public M3U8DownloadConfig setShowNotification(boolean show){
SpHelper.putBoolean(TAG_SHOW_NOTIFICATION, show);
return this;
}
public static boolean isShowNotification(){
return SpHelper.getBoolean(TAG_SHOW_NOTIFICATION, true);
}
public M3U8DownloadConfig setConvertMp4(boolean convertMp4){
SpHelper.putBoolean(TAG_CONVERT_MP4, convertMp4);
return this;
}
}
@@ -0,0 +1,338 @@
package com.vincent.m3u8Downloader.downloader;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import com.vincent.m3u8Downloader.bean.M3U8;
import com.vincent.m3u8Downloader.bean.M3U8Ts;
import com.vincent.m3u8Downloader.listener.OnInfoCallback;
import com.vincent.m3u8Downloader.listener.OnM3U8DownloadListener;
import com.vincent.m3u8Downloader.utils.M3U8Log;
import com.vincent.m3u8Downloader.utils.M3U8Util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Author: Vincent
* @CreateAt: 2021/08/25 22:26
* @Desc: M3U8下载任务
*/
public class M3U8DownloadTask {
public static final String LOCAL_FILE_NAME = "local.m3u8";
public static final String REMOTE_FILE_NAME = "remote.m3u8";
public static final String M3U8_KEY_NAME = "key.key";
private static final int WHAT_ON_ERROR = 1001;
private static final int WHAT_ON_PROGRESS = 1002;
private static final int WHAT_ON_SUCCESS = 1003;
private static final int WHAT_ON_START_DOWNLOAD = 1004;
private static final int WHAT_ON_CONVERT = 1005;
// 文件保存地址
public String m3u8Url;
// 文件保存地址
private String saveDir;
// 当前M3U8
public M3U8 currentM3U8;
// 线程池
private ExecutorService executor;
// 任务是否正在运行
private boolean isRunning = false;
// 当前已经在下完成的大小
private long curLength = 0;
// 当前下载完成的文件个数
private final AtomicInteger curTs = new AtomicInteger(0);
// 总文件个数
private volatile int totalTs = 0;
// 单个文件的大小
private volatile long itemFileSize = 0;
// 下载任务监听器
int connTimeout;
int readTimeout;
int threadCount;
public float progress(){
if (totalTs <= 0){
return 0;
}else {
return (float) (curTs.get() * 100.0/totalTs);
}
}
private final WeakHandler mHandler = new WeakHandler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
return true;
}
});
public M3U8DownloadTask() {
connTimeout = M3U8DownloadConfig.getConnTimeout();
readTimeout = M3U8DownloadConfig.getReadTimeout();
threadCount = M3U8DownloadConfig.getThreadCount();
}
public boolean isRunning() {
return isRunning;
}
/**
* 开始下载
* @param url m3u8下载地址
* @param onTaskDownloadListener 任务下载监听器
*/
public void download(final String url, final OnM3U8DownloadListener onTaskDownloadListener) {
m3u8Url = url;
saveDir = M3U8Util.getSaveFileDirTmp(url);
File dirTmp = new File(saveDir);
if(!dirTmp.exists()){
if(!dirTmp.mkdir()){
M3U8Log.e( "fail:" + dirTmp.getPath());
}
}
isRunning = true;
getM3U8Info(url, new OnInfoCallback() {// 获取m3u8
@Override
public void success(final M3U8 m3u8) {
currentM3U8 = m3u8;
start(m3u8, onTaskDownloadListener);
}
@Override
public void error(Exception e) {
isRunning = false;
currentM3U8 = null;
onTaskDownloadListener.onDownloadError(url, e.getCause());
}
});
}
/**
* 获取m3u8信息
* @param url m3u8地址
* @param callback 回调函数
*/
private synchronized void getM3U8Info(final String url, final OnInfoCallback callback) {
new Thread(new Runnable() {
@Override
public void run() {
try {
M3U8 m3u8 = M3U8Util.parseIndex(url, saveDir + File.separator + REMOTE_FILE_NAME);
curTs.set(m3u8.localTSFileCount);
totalTs = m3u8.getTsList().size();
callback.success(m3u8);
} catch (Exception e) {
callback.error(e);
}
}
}).start();
}
/**
* 开始下载
*/
private void start(final M3U8 m3u8Model, final OnM3U8DownloadListener onTaskDownloadListener) {
new Thread() {
@Override
public void run() {
try {
batchDownloadTs(m3u8Model, onTaskDownloadListener);// 开始下载
if (isRunning) {
String m3u8Path = saveDir + File.separator + LOCAL_FILE_NAME;
if (TextUtils.isEmpty(currentM3U8.methodKeyURL)) {
M3U8Util.createLocalM3U8(m3u8Path, currentM3U8);
} else {
M3U8Util.createLocalM3U8(m3u8Path, currentM3U8, M3U8_KEY_NAME, currentM3U8.getIv());
}
File file = new File(saveDir);
if (file.isDirectory()){
String finishPath = M3U8Util.getSaveFileDir(m3u8Url);
if(file.renameTo(new File(finishPath))){
currentM3U8.setLocalPath(finishPath + File.separator + LOCAL_FILE_NAME);
currentM3U8.setDirPath(finishPath);
}else{
M3U8Log.e("movie cache==文件替换失败" + file.getPath());
}
}
isRunning = false;
onTaskDownloadListener.onDownloadSuccess(M3U8DownloadTask.this);
}
} catch (InterruptedIOException e) {
// 被中断了,使用stop时会抛出这个,不需要处理
isRunning = false;
onTaskDownloadListener.onDownloadError(m3u8Url, e.getCause());
} catch (IOException e) {
isRunning = false;
onTaskDownloadListener.onDownloadError(m3u8Url, e.getCause());
handlerError(e);
} catch (Exception e) {
isRunning = false;
onTaskDownloadListener.onDownloadError(m3u8Url, e.getCause());
handlerError(e);
}
}
}.start();
}
/**
* 批量下载ts切片
* @param m3u8Info M3U8对象
*/
private void batchDownloadTs(final M3U8 m3u8Info, final OnM3U8DownloadListener onTaskDownloadListener) {
final File dir = new File(saveDir);
if (!dir.exists()) {
dir.mkdirs();
}
if (!TextUtils.isEmpty(m3u8Info.getKey())) {
try {// 保存key文件
M3U8Util.saveFile(m3u8Info.getKey(), saveDir + File.separator + "key.key");
} catch (IOException e) {
M3U8Log.e("saveFile fail!" + e.getMessage());
handlerError(e);
}
}
M3U8Log.d("Downloading !");
final String basePath = m3u8Info.getBaseUrl();
for (final M3U8Ts m3u8Ts : m3u8Info.getTsList()) {
if (!isRunning){
return;
}
netTaskLoad(m3u8Ts, dir, basePath, onTaskDownloadListener);
}
}
private void netTaskLoad(M3U8Ts m3u8Ts, File dir, String basePath, final OnM3U8DownloadListener onTaskDownloadListener){
File file;
String fileName = dir + File.separator + m3u8Ts.obtainEncodeTsFileName();
try {
file = new File(fileName);
} catch (Exception e) {
file = new File(dir + File.separator + m3u8Ts.getUrl());
}
if (!file.exists()) {
M3U8Log.d("ts load url=========" + m3u8Ts.getUrl());
FileOutputStream fos = null;
InputStream inputStream = null;
boolean readFinished = false;
try {
URL url = m3u8Ts.obtainFullUrl(basePath);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(connTimeout);
conn.setReadTimeout(readTimeout);
if (conn.getResponseCode() == 200) {
inputStream = conn.getInputStream();
File tmpFile = new File(fileName + "_tmp");
if(tmpFile.exists()){
tmpFile.delete();
}
fos = new FileOutputStream(tmpFile);//会自动创建文件
int len;
byte[] buf = new byte[1024];
while ((len = inputStream.read(buf)) != -1) {
curLength += len;
fos.write(buf, 0, len);//写入流中
}
fos.close();
if(!tmpFile.renameTo(file)){
M3U8Log.e("rename file fail:" + tmpFile.getPath());
}
} else {
handlerError(new Throwable("ts:" + url + "netCode:" + conn.getResponseCode()));
}
readFinished = true;
} catch (MalformedURLException e) {
M3U8Log.e("MalformedURLException" + e.getMessage());
handlerError(e);
} catch (IOException e) {
M3U8Log.e("IOException" + e.getMessage());
handlerError(e);
} catch (Error e) {
M3U8Log.e("Error=" + e.getMessage());
} finally {
// 如果没有读取完,则删除
if (!readFinished && file.exists()) {
file.delete();
}
// 关流
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
itemFileSize = file.length();
m3u8Ts.setFileSize(itemFileSize);
curTs.incrementAndGet();
onTaskDownloadListener.onDownloadProgress(this);
}
} else {
itemFileSize = file.length();
m3u8Ts.setFileSize(itemFileSize);
}
}
/**
* 停止任务
*/
public void stop() {
M3U8Log.d("=========task stop");
isRunning = false;
}
/**
* 处理异常
* @param e 异常信息
*/
private void handlerError(Throwable e) {
if (!"Task running".equals(e.getMessage())) {
// stop();
}
// 不提示被中断的情况
if ("thread interrupted".equals(e.getMessage())) {
return;
}
e.printStackTrace();
// Message msg = Message.obtain();
// msg.obj = e;
// msg.what = WHAT_ON_ERROR;
// mHandler.sendMessage(msg);
}
/**
* 获取m3u8本地路径
* @param url m3u8地址
* @return 文件路径
*/
public static String getM3U8Path(String url) {
return M3U8Util.getSaveFileDir(url) + File.separator + LOCAL_FILE_NAME;
}
}
@@ -0,0 +1,273 @@
package com.vincent.m3u8Downloader.downloader;
import android.text.TextUtils;
import com.vincent.m3u8Downloader.bean.M3U8;
import com.vincent.m3u8Downloader.listener.OnM3U8DownloadListener;
import com.vincent.m3u8Downloader.listener.OnTaskDownloadListener;
import com.vincent.m3u8Downloader.utils.M3U8Log;
import com.vincent.m3u8Downloader.utils.M3U8Util;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
/**
* @Author: Vincent
* @CreateAt: 2021/08/25 22:17
* @Desc: M3U8下载器
*/
public class M3U8Downloader {
private static M3U8Downloader instance;
public M3U8DownloadTask m3U8DownLoadTask;
private OnM3U8DownloadListener onM3U8DownloadListener;
private long currentTime;
private M3U8Downloader() {
}
public static M3U8Downloader getInstance(){
if (null == instance) {
instance = new M3U8Downloader();
}
return instance;
}
public void setOnM3U8DownloadListener(OnM3U8DownloadListener onM3U8DownloadListener) {
this.onM3U8DownloadListener = onM3U8DownloadListener;
}
/**
* 防止快速点击引起ThreadPoolExecutor频繁创建销毁引起crash
* @return 是否快速点击
*/
private boolean isQuicklyClick(){
boolean result = false;
if (System.currentTimeMillis() - currentTime <= 100){
result = true;
M3U8Log.d("is too quickly click!");
}
currentTime = System.currentTimeMillis();
return result;
}
public boolean isFinished(String url){
String dirPath = M3U8Util.getSaveFileDir(url);
File file = new File(dirPath);
File mp4File = new File(dirPath + "/loacl.mp4");
if(file.exists() && file.isDirectory()){
if (!mp4File.exists()){
}
return true;
}
return false;
}
public HashMap<String, String> searchTaskInfo(String url) {
HashMap<String, String> infoMap = new HashMap<String, String>();
String currentTaskUrl = null;
if(m3U8DownLoadTask != null
&& m3U8DownLoadTask.isRunning()){
currentTaskUrl = m3U8DownLoadTask.m3u8Url;
}
if(currentTaskUrl != null && currentTaskUrl.equals(url)){
infoMap.put("isLoaderRunning", "1");
}else {
infoMap.put("isLoaderRunning", "0");
M3U8Log.d("searchTaskInfo url===" + currentTaskUrl);
M3U8Log.d("searchTaskInfo url===" + url);
}
File file = new File(M3U8Util.getSaveFileDir(url));
if(currentTaskUrl != null && currentTaskUrl.equals(url)){
double progress = m3U8DownLoadTask.progress();
infoMap.put("progress", String.format("%.2f", progress));
}else if(file.exists() && file.isDirectory()){
infoMap.put("progress", "100.00");
infoMap.put("localPath", M3U8Util.getSaveFileDir(url) + File.separator + M3U8DownloadTask.LOCAL_FILE_NAME);
}else {
File tmpDir = new File(M3U8Util.getSaveFileDirTmp(url));
if(tmpDir.exists() && tmpDir.isDirectory()){
String m3u8InfoFilePath = M3U8Util.getSaveFileDirTmp(url) + File.separator + M3U8DownloadTask.REMOTE_FILE_NAME;
File m3u8InfoFile = new File(m3u8InfoFilePath);
if (m3u8InfoFile.exists()){
try {
M3U8 m3u8 = M3U8Util.parseIndex(url, m3u8InfoFilePath);
File[] files = tmpDir.listFiles();
int allTaskCount = m3u8.getTsList().size() + 3;
int finishCount = files.length;
if(allTaskCount > 3){
double progress = finishCount*100.0/allTaskCount;
infoMap.put("progress", String.format("%.2f", progress));
}else {
infoMap.put("progress", "0.00");
infoMap.put("error", "m3u8 文件解析错误");
}
}catch (Error | IOException e){
infoMap.put("progress", "0.00");
infoMap.put("error", "m3u8 文件异常");
}
}else {
infoMap.put("progress", "0.00");
}
}else {
infoMap.put("progress", "0.00");
}
}
return infoMap;
}
/**
* 下载m3u8
* @param url m3u8下载地址
*/
public void download(String url) {
if (TextUtils.isEmpty(url) || isQuicklyClick()) return;
// 暂停之前的
if(m3U8DownLoadTask != null){
m3U8DownLoadTask.stop();
m3U8DownLoadTask = null;
}
// 开启新的下载
m3U8DownLoadTask = new M3U8DownloadTask();
try {
M3U8Log.d("start downloading: " + url);
m3U8DownLoadTask.download(url, onM3U8DownloadListener);
} catch (Exception e){
e.printStackTrace();
M3U8Log.e("startDownloadTask Error:"+e.getMessage());
}
}
/**
* 暂停任务(非当前任务)
*/
public void pause(String url){
M3U8Log.d("pause download: " + url);
if(m3U8DownLoadTask != null && m3U8DownLoadTask.m3u8Url.equals(url)){
m3U8DownLoadTask.stop();
}
}
public void pauseAll(){
if(m3U8DownLoadTask != null){
m3U8DownLoadTask.stop();
m3U8DownLoadTask = null;
}
}
/**
* 删除下载文件。非线程安全
* @param url 下载地址
* @return 删除状态
*/
public boolean delete(final String url){
if (m3U8DownLoadTask != null && m3U8DownLoadTask.m3u8Url.equals(url)) {
m3U8DownLoadTask.stop();
m3U8DownLoadTask = null;
}
String saveDir = M3U8Util.getSaveFileDir(url);
String saveDirTmp = M3U8Util.getSaveFileDirTmp(url);
// 删除文件夹
boolean isDelete = M3U8Util.clearDir(new File(saveDir));
boolean isDeleteTmp = M3U8Util.clearDir(new File(saveDirTmp));
return isDelete && isDeleteTmp;
}
public boolean deleteAll() {
if (m3U8DownLoadTask != null) {
m3U8DownLoadTask.stop();
}
String saveDir = M3U8Util.getSaveDir();
boolean isDelete = M3U8Util.clearDir(new File(saveDir));
return isDelete;
}
/**
* 是否正在下载
* @return 运行状态
*/
public boolean isRunning(){
return m3U8DownLoadTask.isRunning();
}
/**
* 下载任务监听器
*/
private final OnTaskDownloadListener onTaskDownloadListener = new OnTaskDownloadListener() {
private long lastLength;
private float downloadProgress;
@Override
public void onStart() {
// currentM3U8Task.setState(M3U8TaskState.PREPARE);
// if (onM3U8DownloadListener != null){
// onM3U8DownloadListener.onDownloadPrepare(currentM3U8Task);
// }
// M3U8Log.d("onDownloadPrepare: "+ currentM3U8Task.getUrl());
}
@Override
public void onStartDownload(int totalTs, int curTs) {
M3U8Log.d("onStartDownload: "+totalTs+"|"+curTs);
//
// currentM3U8Task.setState(M3U8TaskState.DOWNLOADING);
// if (totalTs > 0) {
// downloadProgress = 1.0f * curTs / totalTs;
// }
}
@Override
public void onDownloadItem(long itemFileSize, int totalTs, int curTs) {
}
@Override
public void onProgress(M3U8DownloadTask tas) {
// if (onM3U8DownloadListener != null && m3U8DownLoadTask != null){
// currentM3U8Task.setProgress(tas.progress());
// onM3U8DownloadListener.onDownloadProgress(currentM3U8Task);
// }
}
@Override
public void onConvert() {
M3U8Log.d("onConvert!");
if (onM3U8DownloadListener != null){
onM3U8DownloadListener.onConvert();
}
}
@Override
public void onSuccess(M3U8 m3U8) {
M3U8Log.d("m3u8 Downloader onSuccess: "+ m3U8);
m3U8DownLoadTask.stop();
// currentM3U8Task.setM3U8(m3U8);
// currentM3U8Task.setState(M3U8TaskState.SUCCESS);
// if (onM3U8DownloadListener != null) {
// onM3U8DownloadListener.onDownloadSuccess(currentM3U8Task);
// }
}
@Override
public void onError(Throwable error) {
// if (error.getMessage() != null && error.getMessage().contains("ENOSPC")){
// currentM3U8Task.setState(M3U8TaskState.ENOSPC);
// }else {
// currentM3U8Task.setState(M3U8TaskState.ERROR);
// }
// if (onM3U8DownloadListener != null) {
// onM3U8DownloadListener.onDownloadError(currentM3U8Task.getUrl(), error);
// }
// M3U8Log.e("onError: " + error.getMessage());
}
};
}
@@ -0,0 +1,294 @@
package com.vincent.m3u8Downloader.downloader;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.lang.ref.WeakReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Memory safer implementation of android.os.Handler
* <p/>
* Original implementation of Handlers always keeps hard reference to handler in queue of execution.
* If you create anonymous handler and post delayed message into it, it will keep all parent class
* for that time in memory even if it could be cleaned.
* <p/>
* This implementation is trickier, it will keep WeakReferences to runnables and messages,
* and GC could collect them once WeakHandler instance is not referenced any more
* <p/>
*
* @see Handler
*
* Created by Dmytro Voronkevych on 17/06/2014.
*/
@SuppressWarnings("unused")
public class WeakHandler {
private final Handler.Callback mCallback; // hard reference to Callback. We need to keep callback in memory
private final ExecHandler mExec;
private Lock mLock = new ReentrantLock();
@SuppressWarnings("ConstantConditions")
@VisibleForTesting
final ChainedRef mRunnables = new ChainedRef(mLock, null);
public WeakHandler() {
mCallback = null;
mExec = new ExecHandler();
}
public WeakHandler(@Nullable Handler.Callback callback) {
mCallback = callback; // Hard referencing body
mExec = new ExecHandler(new WeakReference<>(callback)); // Weak referencing inside ExecHandler
}
public WeakHandler(@NonNull Looper looper) {
mCallback = null;
mExec = new ExecHandler(looper);
}
public WeakHandler(@NonNull Looper looper, @NonNull Handler.Callback callback) {
mCallback = callback;
mExec = new ExecHandler(looper, new WeakReference<>(callback));
}
public final boolean post(@NonNull Runnable r) {
return mExec.post(wrapRunnable(r));
}
public final boolean postAtTime(@NonNull Runnable r, long uptimeMillis) {
return mExec.postAtTime(wrapRunnable(r), uptimeMillis);
}
public final boolean postAtTime(Runnable r, Object token, long uptimeMillis) {
return mExec.postAtTime(wrapRunnable(r), token, uptimeMillis);
}
public final boolean postDelayed(Runnable r, long delayMillis) {
return mExec.postDelayed(wrapRunnable(r), delayMillis);
}
public final boolean postAtFrontOfQueue(Runnable r) {
return mExec.postAtFrontOfQueue(wrapRunnable(r));
}
public final void removeCallbacks(Runnable r) {
final WeakRunnable runnable = mRunnables.remove(r);
if (runnable != null) {
mExec.removeCallbacks(runnable);
}
}
public final void removeCallbacks(Runnable r, Object token) {
final WeakRunnable runnable = mRunnables.remove(r);
if (runnable != null) {
mExec.removeCallbacks(runnable, token);
}
}
public final boolean sendMessage(Message msg) {
return mExec.sendMessage(msg);
}
public final boolean sendEmptyMessage(int what) {
return mExec.sendEmptyMessage(what);
}
public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
return mExec.sendEmptyMessageDelayed(what, delayMillis);
}
public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) {
return mExec.sendEmptyMessageAtTime(what, uptimeMillis);
}
public final boolean sendMessageDelayed(Message msg, long delayMillis) {
return mExec.sendMessageDelayed(msg, delayMillis);
}
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
return mExec.sendMessageAtTime(msg, uptimeMillis);
}
public final boolean sendMessageAtFrontOfQueue(Message msg) {
return mExec.sendMessageAtFrontOfQueue(msg);
}
public final void removeMessages(int what) {
mExec.removeMessages(what);
}
public final void removeMessages(int what, Object object) {
mExec.removeMessages(what, object);
}
public final void removeCallbacksAndMessages(Object token) {
mExec.removeCallbacksAndMessages(token);
}
public final boolean hasMessages(int what) {
return mExec.hasMessages(what);
}
public final boolean hasMessages(int what, Object object) {
return mExec.hasMessages(what, object);
}
public final Looper getLooper() {
return mExec.getLooper();
}
private WeakRunnable wrapRunnable(@NonNull Runnable r) {
//noinspection ConstantConditions
if (r == null) {
throw new NullPointerException("Runnable can't be null");
}
final ChainedRef hardRef = new ChainedRef(mLock, r);
mRunnables.insertAfter(hardRef);
return hardRef.wrapper;
}
private static class ExecHandler extends Handler {
private final WeakReference<Callback> mCallback;
@SuppressWarnings("deprecation")
ExecHandler() {
mCallback = null;
}
@SuppressWarnings("deprecation")
ExecHandler(WeakReference<Callback> callback) {
mCallback = callback;
}
ExecHandler(Looper looper) {
super(looper);
mCallback = null;
}
ExecHandler(Looper looper, WeakReference<Callback> callback) {
super(looper);
mCallback = callback;
}
@Override
public void handleMessage(@NonNull Message msg) {
if (mCallback == null) {
return;
}
final Callback callback = mCallback.get();
if (callback == null) { // Already disposed
return;
}
callback.handleMessage(msg);
}
}
static class WeakRunnable implements Runnable {
private final WeakReference<Runnable> mDelegate;
private final WeakReference<ChainedRef> mReference;
WeakRunnable(WeakReference<Runnable> delegate, WeakReference<ChainedRef> reference) {
mDelegate = delegate;
mReference = reference;
}
@Override
public void run() {
final Runnable delegate = mDelegate.get();
final ChainedRef reference = mReference.get();
if (reference != null) {
reference.remove();
}
if (delegate != null) {
delegate.run();
}
}
}
static class ChainedRef {
@Nullable
ChainedRef next;
@Nullable
ChainedRef prev;
@NonNull
final Runnable runnable;
@NonNull
final WeakRunnable wrapper;
@NonNull
Lock lock;
public ChainedRef(@NonNull Lock lock, @NonNull Runnable r) {
this.runnable = r;
this.lock = lock;
this.wrapper = new WeakRunnable(new WeakReference<>(r), new WeakReference<>(this));
}
public WeakRunnable remove() {
lock.lock();
try {
if (prev != null) {
prev.next = next;
}
if (next != null) {
next.prev = prev;
}
prev = null;
next = null;
} finally {
lock.unlock();
}
return wrapper;
}
public void insertAfter(@NonNull ChainedRef candidate) {
lock.lock();
try {
if (this.next != null) {
this.next.prev = candidate;
}
candidate.next = this.next;
this.next = candidate;
candidate.prev = this;
} finally {
lock.unlock();
}
}
@Nullable
public WeakRunnable remove(Runnable obj) {
lock.lock();
try {
ChainedRef curr = this.next; // Skipping head
while (curr != null) {
if (curr.runnable == obj) { // We do comparison exactly how Handler does inside
return curr.remove();
}
curr = curr.next;
}
} finally {
lock.unlock();
}
return null;
}
}
}
@@ -0,0 +1,14 @@
package com.vincent.m3u8Downloader.listener;
import com.vincent.m3u8Downloader.bean.M3U8;
/**
* @Author: Vincent
* @CreateAt: 2021/08/26 9:58
* @Desc: 获取M3U8文件信息的回调函数
*/
public interface OnInfoCallback {
void success(M3U8 m3u8);
void error(Exception e);
}
@@ -0,0 +1,69 @@
package com.vincent.m3u8Downloader.listener;
import com.vincent.m3u8Downloader.bean.M3U8Task;
import com.vincent.m3u8Downloader.downloader.M3U8DownloadTask;
/**
* @Author: Vincent
* @CreateAt: 2021/08/25 22:40
* @Desc: M3U8Downloader 监听器
*/
public interface OnM3U8DownloadListener {
/**
* 下载准备
* @param task 当前准备任务
*/
void onDownloadPrepare(M3U8Task task);
/**
* 等待下载
* @param task 等待的任务
*/
void onDownloadPending(M3U8Task task);
/**
* 下载进度
* 异步回调,不可以直接在UI线程调用
* @param task 当前下载任务
*/
void onDownloadProgress(M3U8DownloadTask task);
/**
* 完成一次下载任务
* @param task 下载任务
* @param itemFileSize 此任务的文件大小
* @param totalTs 总切片数
* @param curTs 已下载切片数
*/
void onDownloadItem(M3U8Task task, long itemFileSize, int totalTs, int curTs);
/**
* 下载成功
*/
void onDownloadSuccess(M3U8DownloadTask task);
/**
* 暂停下载
* @param task 暂停的任务
*/
void onDownloadPause(M3U8Task task);
/**
* 准备转成MP4
*/
void onConvert();
/**
* 下载失败
* @param url 失败的任务
* @param error 错误信息
*/
void onDownloadError(String url, final Throwable error);
/**
* 停止下载
* @param task 停止的任务
*/
void onStop(M3U8Task task);
}
@@ -0,0 +1,56 @@
package com.vincent.m3u8Downloader.listener;
import com.vincent.m3u8Downloader.bean.M3U8;
import com.vincent.m3u8Downloader.downloader.M3U8DownloadTask;
/**
* @Author: Vincent
* @CreateAt: 2021/08/25 22:37
* @Desc: 任务下载监听器
*/
public interface OnTaskDownloadListener {
/**
* 开始任务
*/
void onStart();
/**
* 开始下载
* @param totalTs ts总数
* @param curTs 当前下载完成的ts个数
*/
void onStartDownload(int totalTs, int curTs);
/**
* ts文件下载完成
* 注意:这个方法是异步的(子线程中执行),所以不能在此方法中回调,其他方法为主线程中回调
* @param itemFileSize 单个文件的大小
* @param totalTs ts总数
* @param curTs 当前下载完成的ts个数
*/
void onDownloadItem(long itemFileSize, int totalTs, int curTs);
/**
* 定时进度
* @param task
*/
void onProgress(M3U8DownloadTask task);
/**
* 正在转成MP4格式
*/
void onConvert();
/**
* 下载成功
*/
void onSuccess(M3U8 m3U8);
/**
* 错误的时候回调
* 线程环境无法保证,不可以直接在UI线程调用
* @param error 错误信息
*/
void onError(Throwable error);
}
@@ -0,0 +1,180 @@
package com.vincent.m3u8Downloader.utils;
import android.text.TextUtils;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* @Author: Vincent
* @CreateAt: 2021/08/25 16:42
* @Desc: 加解密工具
*/
public class EncryptUtil {
private final static String ENCODING = "UTF-8";
/**
* md5加密字符串
* @param str 待加密字符串
* @return 加密后的字符串
*/
public static String md5Encode(String str) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(str.getBytes());
return new BigInteger(1, md.digest()).toString(16);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return str;
}
/**
* 生成密钥
* 自动生成base64 编码后的AES128位密钥
*/
public static String getAESKey() throws Exception {
KeyGenerator kg = KeyGenerator.getInstance("AES");
kg.init(128);
SecretKey sk = kg.generateKey();
byte[] b = sk.getEncoded();
return parseByte2HexStr(b);
}
/**
* AES 加密
* @param base64Key base64编码后的 AES key
* @param text 待加密的字符串
* @return 加密后的byte[]
* @throws Exception 异常
*/
public static byte[] getAESEncode(String base64Key, String text) throws Exception{
return getAESEncode(base64Key, text.getBytes());
}
/**
* AES 加密
* @param base64Key base64编码后的 AES key
* @param bytes 待加密的bytes
* @return 加密后的byte[]
* @throws Exception 异常
*/
public static byte[] getAESEncode(String base64Key, byte[] bytes) throws Exception{
if (base64Key == null)return bytes;
byte[] key = parseHexStr2Byte(base64Key);
SecretKeySpec sKeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, sKeySpec);
return cipher.doFinal(bytes);
}
/**
* AES解密
* @param base64Key base64编码后的 AES key
* @param text 待解密的字符串
* @return 解密后的byte[]
* @throws Exception 异常
*/
public static byte[] getAESDecode(String base64Key, String text) throws Exception{
return getAESDecode(base64Key, text.getBytes());
}
/**
* AES解密
* @param base64Key base64编码后的 AES key
* @param bytes 待解密的字符串
* @return 解密后的byte[] 数组
* @throws Exception 异常
*/
public static byte[] getAESDecode(String base64Key, byte[] bytes) throws Exception{
if (base64Key == null)return bytes;
byte[] key = parseHexStr2Byte(base64Key);
SecretKeySpec sKeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, sKeySpec);
return cipher.doFinal(bytes);
}
/**
* 将二进制转换成16进制
* @param buf byte数组
* @return 16进制字符串
*/
public static String parseByte2HexStr(byte[] buf) {
StringBuilder sb = new StringBuilder();
for (byte b : buf) {
String hex = Integer.toHexString(b & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
/**
* 将16进制转换为二进制
* @param hexStr 16进制字符串
* @return byte[]
*/
public static byte[] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1)
return null;
byte[] result = new byte[hexStr.length()/2];
for (int i = 0; i< hexStr.length()/2; i++) {
int high = Integer.parseInt(hexStr.substring(i*2, i*2+1), 16);
int low = Integer.parseInt(hexStr.substring(i*2+1, i*2+2), 16);
result[i] = (byte) (high * 16 + low);
}
return result;
}
/**
* 解密ts文件
* @param bytes 文件字节流
* @param key m3u8的key
* @param iv m3u8的iv
* @return 解密后的byte[]
* @throws Exception 异常
*/
public static byte[] decryptTs(byte[] bytes, String key, String iv) throws Exception {
if (TextUtils.isEmpty(key)) {
return bytes;
}
if(key.length() != 16){
String utf8Key = key.getBytes(ENCODING).toString();
M3U8Log.e("key 长度不是16位--" + key.length() + "--" + utf8Key + "--" + utf8Key.length());
if (utf8Key.length() == 16) {
key = utf8Key;
}else {
// return bytes;
}
}
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
byte[] ivByte = new byte[16];
if (!TextUtils.isEmpty(iv)) {
if (iv.startsWith("0x"))
ivByte = parseHexStr2Byte(iv.substring(2));
else
ivByte = iv.getBytes();
if (ivByte == null || ivByte.length != 16)
ivByte = new byte[16];
}
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(ENCODING), "AES");
AlgorithmParameterSpec paramSpec = new IvParameterSpec(ivByte);
cipher.init(Cipher.DECRYPT_MODE, keySpec, paramSpec);
return cipher.doFinal(bytes);
}
}
@@ -0,0 +1,24 @@
package com.vincent.m3u8Downloader.utils;
import android.util.Log;
import com.vincent.m3u8Downloader.downloader.M3U8DownloadConfig;
/**
* @Author: Vincent
* @CreateAt: 2021/08/25 17:58
* @Desc: M3U8日志系统
*/
public class M3U8Log {
private static final boolean isDebugMode = M3U8DownloadConfig.isDebugMode();
private static final String TAG = "M3U8Log";
private static final String PREFIX = "M3U8Log cache error:====== ";
public static void d(String msg) {
if (isDebugMode) Log.d(TAG, TAG + msg);
}
public static void e(String msg) {
if (isDebugMode) Log.e(TAG, PREFIX + msg);
}
}
@@ -0,0 +1,249 @@
package com.vincent.m3u8Downloader.utils;
import android.annotation.SuppressLint;
import android.net.Uri;
import android.util.Log;
import com.vincent.m3u8Downloader.downloader.M3U8DownloadConfig;
import com.vincent.m3u8Downloader.bean.M3U8;
import com.vincent.m3u8Downloader.bean.M3U8Ts;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
/**
* @Author: Vincent
* @CreateAt: 2021/08/25 21:43
* @Desc: M3U8工具类
*/
public class M3U8Util {
/**
* 将Url转换为M3U8对象
* @param url url地址
* @return M3U8对象
* @throws IOException IO异常
*/
public static M3U8 parseIndex(String url, String filePath) throws IOException {
M3U8 ret = new M3U8();
URL baseUrl = new URL(url);
File remoteFile = new File(filePath);
BufferedReader reader;
Boolean fromLocalFile = false;
if (remoteFile.exists() && remoteFile.length() > 100) {
File dir = new File(remoteFile.getParent());
if (dir.isDirectory()) {
int count = dir.listFiles().length - 1;
if (count > 0){
ret.localTSFileCount = count;
}
}
fromLocalFile = true;
reader = new BufferedReader(new FileReader(filePath));
}else {
reader = new BufferedReader(new InputStreamReader(baseUrl.openStream()));
}
ret.setBaseUrl(url);
StringBuilder strBuilder = new StringBuilder();
String line;
float seconds = 0;
while ((line = reader.readLine()) != null) {
if (!fromLocalFile) {
strBuilder.append(line + "\n");
}
if (line.startsWith("#")) {
if (line.startsWith("#EXTINF:")) {
line = line.substring(8);
if (line.endsWith(",")) {
line = line.substring(0, line.length() - 1);
}
seconds = Float.parseFloat(line);
} else if (line.startsWith("#EXT-X-KEY:")) {
String[] lineInfoArr = line.split("#EXT-X-KEY:");
line = lineInfoArr[1];
String[] arr = line.split(",");
for (String s : arr) {
if (s.contains("=")) {
int eqIndex = s.indexOf("=");
String k = s.substring(0, eqIndex);
// 取第一个 '=' 之后的全部,不能用 split("=")[1]
// key 地址常带 query(?token=xxx)split 会把地址截成半截
String v = s.substring(eqIndex + 1);
if (k.equals("URI")) {
// 获取key。引号统一在这里去掉,留着的话上层 createLocalM3U8
// 再包一层就成了 URI=""http..."",取不出地址
v = v.replace("\"", "");
if (v.length() > 0 && !v.startsWith("http")) {
// 相对地址按 m3u8 自身地址补全(可能是同目录相对路径,不能只拼 scheme+host)
try {
v = new URL(baseUrl, v).toString();
} catch (Exception ignored) {
}
}
ret.methodKeyURL = v;
// if(Thread.currentThread() != Looper.getMainLooper().getThread()){
// // 只有查询信息才走主线程, key 本地化
// BufferedReader keyReader = new BufferedReader(new InputStreamReader(new URL(baseUrl, v).openStream()));
// ret.setKey(keyReader.readLine());
// M3U8Log.d("m3u8 key: " + ret.getKey());
// }
} else if (k.equals("IV")) {
// 获取IV
ret.setIv(v);
M3U8Log.d("m3u8 IV: " + v);
}else if(k.equals("METHOD")){
ret.methodCode = v;
}
}
}
}
continue;
}
if (line.endsWith("m3u8")) {
return parseIndex(new URL(baseUrl, line).toString(), filePath + "sub.m3u8");
}
ret.addTs(new M3U8Ts(line, seconds));
seconds = 0;
}
reader.close();
if(!fromLocalFile) {
BufferedWriter bfw = new BufferedWriter(new FileWriter(filePath, false));
bfw.write(strBuilder.toString());
bfw.close();
}
return ret;
}
/**
* 生成AES-128加密本地m3u8索引文件,ts切片和m3u8文件放在相同目录下即可
* @param m3U8 m3u8文件
* @param keyPath 加密key
*/
public static void createLocalM3U8(String fileName, M3U8 m3U8, String keyPath, String iv) throws IOException{
M3U8Log.d("createLocalM3U8: " + fileName);
String basePath = M3U8Util.getSaveFileDir(m3U8.getBaseUrl());
BufferedWriter bfw = new BufferedWriter(new FileWriter(fileName, false));
bfw.write("#EXTM3U\n");
bfw.write("#EXT-X-VERSION:3\n");
bfw.write("#EXT-X-MEDIA-SEQUENCE:0\n");
bfw.write("#EXT-X-TARGETDURATION:13\n");
if (keyPath != null) {
String keyContent = "#EXT-X-KEY:METHOD=" + m3U8.methodCode + ",URI=" ;
// keyContent = keyContent + basePath + "/" + keyPath + "\"";
keyContent = keyContent + "\"" + m3U8.methodKeyURL + "\"";
if(iv != null){
keyContent = keyContent + "," + "IV=" + iv;
}
keyContent = keyContent + "\n";
bfw.write(keyContent);
}
for (M3U8Ts m3U8Ts : m3U8.getTsList()) {
bfw.write("#EXTINF:" + m3U8Ts.getSeconds()+",\n");
bfw.write( "file://" + basePath + "/" + m3U8Ts.obtainEncodeTsFileName());
// M3U8Log.d("file://" + basePath + "/" + m3U8Ts.obtainEncodeTsFileName());
bfw.newLine();
}
bfw.write("#EXT-X-ENDLIST");
bfw.flush();
bfw.close();
}
/**
* 清空文件夹
* @param dir 文件夹/文件地址
* @return 删除状态
*/
public static boolean clearDir(File dir) {
if (dir.exists()) {
if (dir.isFile()) {
return dir.delete();
} else if (dir.isDirectory()) {
File[] files = dir.listFiles();
if (files != null && files.length > 0) {
for (File file : files) {
clearDir(file);
}
}
return dir.delete();
}
}
return true;
}
private static final float KB = 1024;
private static final float MB = 1024 * KB;
private static final float GB = 1024 * MB;
/**
* 格式化文件大小
* @param size 文件大小
* @return 格式化字符串
*/
@SuppressLint("DefaultLocale")
public static String formatFileSize(long size){
if (size >= GB) {
return String.format("%.1f GB", size / GB);
} else if (size >= MB) {
float value = size / MB;
return String.format(value > 100 ? "%.0f MB" : "%.1f MB", value);
} else if (size >= KB) {
float value = size / KB;
return String.format(value > 100 ? "%.0f KB" : "%.1f KB", value);
} else {
return String.format("%d B", size);
}
}
/**
* 生成本地m3u8索引文件,ts切片和m3u8文件放在相同目录下即可
* @param m3U8 m3u8文件
*/
public static void createLocalM3U8(String fileName, M3U8 m3U8) throws IOException{
createLocalM3U8(fileName, m3U8, null, null);
}
/**
* 保存文件
* @param text 文件内容
* @param fileName 文件名
* @throws IOException IO异常
*/
public static void saveFile(String text, String fileName) throws IOException{
File file = new File(fileName);
BufferedWriter out = new BufferedWriter(new FileWriter(file));
out.write(text);
out.flush();
out.close();
}
/**
* 获取url保存的地址
* @param url 请求地址
* @return 地址
*/
public static String getSaveFileDir(String url){
String readUrl = Uri.parse(url).getPath();
Log.d("M3U8::", "getSaveFileDir:" + url);
Log.d("M3U8::", "getSaveFileDir->readUrl" + readUrl);
return M3U8DownloadConfig.getSaveDir() + File.separator + EncryptUtil.md5Encode(readUrl);
}
public static String getSaveFileDirTmp(String url){
String readUrl = Uri.parse(url).getPath();
return M3U8DownloadConfig.getSaveDir() + File.separator + EncryptUtil.md5Encode(readUrl) + "_tmp";
}
public static String getSaveDir(){
return M3U8DownloadConfig.getSaveDir();
}
}
@@ -0,0 +1,160 @@
package com.vincent.m3u8Downloader.utils;
import android.annotation.SuppressLint;
import android.app.NotificationChannel;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.vincent.m3u8Downloader.bean.M3U8TaskState;
/**
* @Author: Vincent
* @CreateAt: 2021/08/26 16:47
* @Desc:
*/
public class NotificationUtil {
public static final int NOTIFICATION_ID = 9527;
public static final String NOTIFICATION_CHANNEL_ID = "M3U8_DOWNLOADER_NOTIFICATION";
public static final String ACTION_SELECT_NOTIFICATION = "SELECT_NOTIFICATION";
@SuppressLint("StaticFieldLeak")
private static NotificationUtil instance;
private NotificationCompat.Builder builder;
private android.app.NotificationManager notificationManager;
private Context context;
private int notificationProgress = -100;
public static NotificationUtil getInstance(){
if (null == instance) {
instance = new NotificationUtil();
}
return instance;
}
/**
* 构建通知
* @param c 上下文
*/
public void build(Context c) {
if (notificationManager != null) return;
this.context = c;
// Make a channel if necessary
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
CharSequence name = context.getApplicationInfo().loadLabel(context.getPackageManager());
int importance = android.app.NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance);
channel.setSound(null, null);
// Add the channel
notificationManager = context.getSystemService(android.app.NotificationManager.class);
if (notificationManager != null) {
notificationManager.createNotificationChannel(channel);
}
}
// Create the notification
builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
// .setSmallIcon(R.drawable.ic_download) // 通知图标
.setOnlyAlertOnce(true)
.setAutoCancel(true) // 默认不自动取消
.setPriority(NotificationCompat.PRIORITY_DEFAULT); // 默认优先级
}
/**
* 更新通知
* @param state 下载状态
* @param progress 下载进度
*/
public void updateNotification(String fileName, M3U8TaskState state, int progress) {
if (builder == null) return;
builder.setContentTitle(fileName == null || fileName.equals("") ? "下载M3U8文件" : fileName);
switch (state) {
case PREPARE:
notificationProgress = -100;
builder.setContentText("准备下载").setProgress(0, 0, true);
builder.setOngoing(true)
.setSmallIcon(android.R.drawable.stat_sys_download_done);
case PENDING:
notificationProgress = -100;
builder.setContentText("等待下载...").setProgress(0, 0, true);
builder.setOngoing(true)
.setSmallIcon(android.R.drawable.stat_sys_download_done);
break;
case DOWNLOADING:
// 控制刷新Notification频率
if (progress < 100 && (progress - notificationProgress < 2)) {
return;
}
notificationProgress = progress;
builder.setContentText("正在下载...")
.setProgress(100, progress, false);
builder.setOngoing(true)
.setSmallIcon(android.R.drawable.stat_sys_download);
break;
case PAUSE:
builder.setContentText("暂停下载");
builder.setOngoing(false)
.setSmallIcon(android.R.drawable.stat_sys_download);
break;
case SUCCESS:
// 点击跳转
Intent intent = new Intent(context, getMainActivityClass(context));
intent.setAction(ACTION_SELECT_NOTIFICATION);
PendingIntent pendingIntent = PendingIntent.getActivity(
context, NOTIFICATION_ID,
intent,
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
);
builder.setContentIntent(pendingIntent);
builder.setContentText("下载完成").setProgress(0, 0, false);
builder.setOngoing(false)
.setSmallIcon(android.R.drawable.stat_sys_download_done);
break;
case ERROR:
case ENOSPC:
builder.setContentText("下载失败").setProgress(0, 0, false);
builder.setOngoing(false)
.setSmallIcon(android.R.drawable.stat_sys_download_done);
break;
default:
break;
}
// Show the notification
NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, builder.build());
}
public NotificationCompat.Builder getBuilder() {
return builder;
}
public void cancel() {
if (notificationManager != null) {
notificationManager.cancel(NOTIFICATION_ID);
notificationManager = null;
}
}
private Class getMainActivityClass(Context context) {
String packageName = context.getPackageName();
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
String className = launchIntent.getComponent().getClassName();
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
}
@@ -0,0 +1,68 @@
package com.vincent.m3u8Downloader.utils;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Collections;
import java.util.Set;
/**
* @Author: Vincent
* @CreateAt: 2021/08/25 17:16
* @Desc: SharedPreferences帮助类
*/
public class SpHelper {
private static final String NULL_KEY = "NULL_KEY";
private static final String TAG_NAME = "M3U8PreferenceHelper";
private static SharedPreferences PREFERENCES;
public static void init(Context context) {
PREFERENCES = context.getSharedPreferences(TAG_NAME, Context.MODE_PRIVATE);
}
private static String checkKeyNonNull(String key) {
if (key == null) {
Log.e(NULL_KEY, "Key is null!!!");
return NULL_KEY;
}
return key;
}
private static SharedPreferences.Editor newEditor() {
return PREFERENCES.edit();
}
public static void putBoolean(@NonNull String key, boolean value) {
newEditor().putBoolean(checkKeyNonNull(key), value).apply();
}
public static boolean getBoolean(@NonNull String key, boolean defValue) {
return PREFERENCES.getBoolean(checkKeyNonNull(key), defValue);
}
public static void putInt(@NonNull String key, int value) {
newEditor().putInt(checkKeyNonNull(key), value).apply();
}
public static int getInt(@NonNull String key, int defValue) {
return PREFERENCES.getInt(checkKeyNonNull(key), defValue);
}
public static void putString(@NonNull String key, @Nullable String value) {
newEditor().putString(checkKeyNonNull(key), value).apply();
}
public static String getString(@NonNull String key, @Nullable String defValue) {
return PREFERENCES.getString(checkKeyNonNull(key), defValue);
}
}
+16
View File
@@ -0,0 +1,16 @@
# m3u8_downloader_example
Demonstrates how to use the m3u8_downloader plugin.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
@@ -0,0 +1,58 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
// throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 32
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.vincent.m3u8_downloader_example"
minSdkVersion 19
targetSdkVersion 31
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
dependencies {
implementation 'com.android.support:multidex:1.0.3' //加这个
}
flutter {
source '../..'
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.vincent.m3u8_downloader_example">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,49 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.vincent.m3u8_downloader_example">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.RECORD_VIDEO"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:label="m3u8_downloader_example"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of
Flutter's first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
@@ -0,0 +1,6 @@
package com.vincent.m3u8_downloader_example;
import io.flutter.embedding.android.FlutterActivity;
public class MainActivity extends FlutterActivity {
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.vincent.m3u8_downloader_example">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,27 @@
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.2'
}
}
allprojects {
repositories {
google()
jcenter()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
@@ -0,0 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
@@ -0,0 +1,11 @@
include ':app'
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
@@ -0,0 +1 @@
{"inputs":[],"outputs":[]}
@@ -0,0 +1 @@
{"inputs":["/Users/mac/Documents/workspace/hjllnew/m3u8_downloader-master/example/.dart_tool/package_config_subset"],"outputs":["/Users/mac/Documents/workspace/hjllnew/m3u8_downloader-master/example/.dart_tool/flutter_build/dart_plugin_registrant.dart"]}
@@ -0,0 +1,224 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:m3u8_downloader/m3u8_downloader.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'video_player_page.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
ReceivePort _port = ReceivePort();
String? _downloadingUrl;
//
//http://m3u8.afhklsdd.com/d1af20263f1b11cc685ba52e842f6659.m3u8
// 未加密的url地址(喜羊羊与灰太狼之决战次时代)
//String url1 = "http://m3u8.afhklsdd.com/2eb056a7a1fc2774aa536421939d73a49652eac7.m3u8"; //"https://cdn.605-zy.com/20210713/MiJecHrZ/index.m3u8";
// 加密的url地址(火影忍者疾风传)
//String url2 = "https://v3.dious.cc/20201116/SVGYv7Lo/index.m3u8";
String url2 =
"http://163.53.216.122:9898/api/media/m3u8/sp/ah/a9/nk/pr/441c94f4f57b4b1a9058217eaf3663d3.m3u8?Authorization=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJwdWJsaWMiLCJleHAiOjE2OTQzNDIzMTEsImlzc3VlciI6ImNvbS5idXR0ZXJmbHkiLCJzdWIiOiJhc2lnbiIsInVzZXJJZCI6OTk4MzMxMn0.u98UciEaat-JYiSx5GueWtYMM8gt61TSBy43maBdCvI"; // "http://m3u8.afhklsdd.com/7266e82a230a18ef457fe82b6c853fad.m3u8";
//"http://m3u8.afhklsdd.com/7266e82a230a18ef457fe82b6c853fad.m3u8";
//"https://m3u8.afhklsdd.com/fd1ff0da268f722901030204c0c1edbf.m3u8";//"http://m3u8.afhklsdd.com/d1af20263f1b11cc685ba52e842f6659.m3u8";
dynamic taskInfo;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
initAsync();
});
}
dynamic progress;
void initAsync() async {
String saveDir = await _findSavePath();
print(saveDir);
bool result = await M3u8Downloader.initialize(onSelect: () async {
print('下载成功点击');
return null;
});
print("initialize====$result");
await M3u8Downloader.config(
saveDir: saveDir,
threadCount: 5,
convertMp4: true,
debugMode: true,
progressCallback: progressCallback,
successCallback: successCallback,
errorCallback: errorCallback,
);
// 注册监听器
IsolateNameServer.registerPortWithName(_port.sendPort, 'downloader_send_port');
_port.listen((dynamic data) {
// 监听数据请求
print(data);
progress = data;
setState(() {});
});
}
Future<bool> _checkPermission() async {
var status = await Permission.storage.status;
if (!status.isGranted) {
status = await Permission.storage.request();
}
return status.isGranted;
}
Future<String> _findSavePath() async {
var directory = await getExternalStorageDirectory(); //getApplicationSupportDirectory();
String saveDir = "/sdcard/vPlayDownload"; //directory!.path + '/vPlayDownload';
Directory root = Directory(saveDir);
if (!root.existsSync()) {
await root.create();
}
print(saveDir);
return saveDir;
}
static progressCallback(dynamic args) {
//print("progressCallback====$args");
final SendPort? send = IsolateNameServer.lookupPortByName('downloader_send_port');
if (send != null) {
args["status"] = 1;
send.send(args);
}
}
static successCallback(dynamic args) {
print("successCallback====$args");
final SendPort? send = IsolateNameServer.lookupPortByName('downloader_send_port');
if (send != null) {
send.send({"status": 2, "url": args["url"], "filePath": args["filePath"], "dir": args["dir"]});
}
}
static errorCallback(dynamic args) {
print("errorCallback====$args");
final SendPort? send = IsolateNameServer.lookupPortByName('downloader_send_port');
if (send != null) {
send.send({"status": 3, "url": args["url"]});
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
useInheritedMediaQuery: true,
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Column(
children: <Widget>[
ElevatedButton(
child: Text("${_downloadingUrl == url2 ? '暂停' : '下载'}已加密m3u8"),
onPressed: () {
if (_downloadingUrl == url2) {
// 暂停
setState(() {
_downloadingUrl = null;
});
M3u8Downloader.pause(url2);
return;
}
// 下载
_checkPermission().then((hasGranted) async {
if (hasGranted) {
await M3u8Downloader.config(
convertMp4: true,
);
setState(() {
_downloadingUrl = url2;
});
final result = await M3u8Downloader.download(
url: url2,
name: "下载已加密m3u8",
);
print("=========download:$result");
if (result != null) {
setState(() {
_downloadingUrl = null;
});
}
}
});
},
),
ElevatedButton(
child: Text("打开已下载的已加密的文件"),
onPressed: () async {
final result = await M3u8Downloader.download(
url: url2,
name: "下载已加密m3u8",
progressCallback: progressCallback,
successCallback: successCallback,
errorCallback: errorCallback);
print("=========download:$result");
if (result != null) {
setState(() {
_downloadingUrl = null;
});
}
},
),
ElevatedButton(
child: Text("清空下载"),
onPressed: () async {
await M3u8Downloader.delete(url2);
print("清理完成");
},
),
ElevatedButton(
child: Text(taskInfo?.toString() ?? "查询"),
onPressed: () async {
taskInfo = await M3u8Downloader.searchInfo(url2);
print("=======taskInfo:");
print(taskInfo);
print("=======taskInfo:");
setState(() {});
},
),
Container(
child: Text(progress?.toString() ?? "progress"),
),
ElevatedButton(
child: Text("movie"),
onPressed: () {
print("Navigator push:${context}");
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => VideoM38UPlayerPage(),
),
);
// Navigator.of(context).push(MaterialPageRoute(builder: (context){
// print("Navigator push VideoPlayerPage");
// return VideoPlayerPage();
// }));
},
),
Container(
height: 300,
width: 400,
child: VideoM38UPlayerPage(),
),
],
),
),
);
}
}
@@ -0,0 +1,112 @@
import 'dart:io';
import 'package:m3u8_downloader/m3u8_downloader.dart';
import 'package:video_player/video_player.dart';
import 'package:flutter/material.dart';
//视频播放器封装需要使用动态类
class VideoM38UPlayerPage extends StatefulWidget {
@override
_VideoPlayerPageState createState() => _VideoPlayerPageState();
}
//继承VideoApp类
class _VideoPlayerPageState extends State<VideoM38UPlayerPage> {
//定义一个VideoPlayerController
VideoPlayerController? _controller;
//重写类方法initState(),初始化界面
@override
void initState() {
super.initState();
print("_VideoPlayerPageState init");
//设置视频参数 (..)是级联的意思
// initController();
}
void initController() {
String url = "http://163.53.216.122:9898/api/media/m3u8/sp/ah/a9/nk/pr/441c94f4f57b4b1a9058217eaf3663d3.m3u8?Authorization=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJwdWJsaWMiLCJleHAiOjE2OTQzNDIzMTEsImlzc3VlciI6ImNvbS5idXR0ZXJmbHkiLCJzdWIiOiJhc2lnbiIsInVzZXJJZCI6OTk4MzMxMn0.u98UciEaat-JYiSx5GueWtYMM8gt61TSBy43maBdCvI"; //"""https://m3u8.afhklsdd.com/5678954c76ece8b4345ae437e7344367e22e2G1o.m3u8";
M3u8Downloader.searchInfo(url).then((result) {
if (result is Map) {
Map fileInfoMap = result;
String? filePath = fileInfoMap["localPath"];
// filePath = filePath?.replaceAll("local.m3u8", "remote.m3u8");
print(filePath);
if (filePath?.isNotEmpty == true) {
_controller = VideoPlayerController.file(File(filePath!))
..initialize().then((value) {
setState(() {
if (_controller == null) {
_controller?.addListener(() {setState(() {
});});
print("_controller success local path");
_controller?.play();
}
});
});
}else {
_controller = VideoPlayerController.network(url)..initialize().then((value) {
setState(() {
if (_controller == null) {
_controller?.addListener(() {setState(() {
});});
print("_controller success");
_controller?.play();
}
});
});
}
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: Text("back"),
),
body: Center(
child: (_controller?.value.isInitialized ?? false)
? AspectRatio(
aspectRatio: _controller!.value.aspectRatio,
child: VideoPlayer(_controller!),
)
: Container(
child: Text("没有要播放的视频"),
),
),
//右下角图标按钮onPressed中需要调用setState方法,用于刷新界面
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
if (_controller != null) {
_controller!.value.isPlaying
? _controller!.pause() : _controller!.play();
}else{
initController();
}
});
},
child: Icon(
(_controller?.value.isPlaying ?? false)? Icons.pause : Icons.play_arrow,
),
),
);
}
//dispose():程序中是用来关闭一个GUI页面的
//视频播放完需要把页面关闭
@override
void dispose() {
super.dispose();
_controller?.dispose();
}
}
@@ -0,0 +1,75 @@
name: m3u8_downloader_example
description: Demonstrates how to use the m3u8_downloader plugin.
# The following line prevents the package from being accidentally published to
# pub.dev using `pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
path_provider: ^2.0.2
permission_handler: ^8.1.4+2
open_file: ^3.2.1
m3u8_downloader:
# When depending on this package from a real application you should use:
# m3u8_downloader: ^x.y.z
# See https://dart.dev/tools_base/pub/dependencies#version-constraints
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
video_player: ^2.2.19
dev_dependencies:
flutter_test:
sdk: flutter
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools_base/pub/pubspec
# The following section is specific to Flutter.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
@@ -0,0 +1,26 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility that Flutter provides. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../lib/main.dart';
void main() {
testWidgets('Verify Platform version', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());
// Verify that platform version is retrieved.
expect(
find.byWidgetPredicate(
(Widget widget) => widget is Text && widget.data!.startsWith('Running on:'),
),
findsOneWidget,
);
});
}
@@ -0,0 +1,18 @@
import Flutter
import UIKit
public final class M3u8DownloaderPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "m3u8_downloader",
binaryMessenger: registrar.messenger(),
codec: FlutterJSONMethodCodec.sharedInstance()
)
let instance = M3u8DownloaderPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
result(FlutterMethodNotImplemented)
}
}
@@ -0,0 +1,20 @@
Pod::Spec.new do |s|
s.name = 'm3u8_downloader'
s.version = '1.3.0'
s.summary = 'Minimal iOS stub for the m3u8_downloader Flutter plugin.'
s.description = <<-DESC
Provides an iOS registration stub so Flutter projects depending on the
m3u8_downloader plugin can build for iOS simulators and devices.
DESC
s.homepage = 'https://example.invalid/m3u8_downloader'
s.license = { :file => '../LICENSE' }
s.author = { 'Vincent' => 'vincent@example.invalid' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '13.0'
s.swift_version = '5.0'
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES'
}
end
@@ -0,0 +1,32 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
// 必须标注:原生启动后台 isolate 时要从 native 闭包化这个入口函数,
// 新版 Flutter/Dart 不加 @pragma('vm:entry-point') 会报 "To closurize ... it must be annotated"
// 后台 isolate 起不来 → 下载进度/成功回调永远到不了 Dart,进度条不刷新。
@pragma('vm:entry-point')
void callbackDispatcher() {
// Initialize state necessary for MethodChannels.
WidgetsFlutterBinding.ensureInitialized();
const MethodChannel backgroundChannel = MethodChannel('vincent/m3u8_downloader_background', JSONMethodCodec());
backgroundChannel.setMethodCallHandler((MethodCall call) async {
final dynamic args = call.arguments;
final CallbackHandle handle = CallbackHandle.fromRawHandle(args[0]);
final Function? closure = PluginUtilities.getCallbackFromHandle(handle);
if (closure == null) {
print('Fatal: could not find callback');
exit(-1);
}
closure(args[1]);
});
backgroundChannel.invokeMethod('didInitializeDispatcher');
}
@@ -0,0 +1,237 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/services.dart';
import 'callback_dispatcher.dart';
typedef CallbackHandle? _GetCallbackHandle(Function callback);
typedef SelectNotificationCallback = Future<dynamic> Function();
class M3u8Downloader {
static const MethodChannel _channel = const MethodChannel(
'm3u8_downloader', JSONMethodCodec());
static _GetCallbackHandle _getCallbackHandle = (Function callback) =>
PluginUtilities.getCallbackHandle(callback);
static SelectNotificationCallback? _onSelectNotification;
static bool _initialized = false;
static bool get isInitialized {
return _initialized;
}
/// 初始化下载器
/// 在使用之前必须调用
///
/// - [onSelect] 点击通知的回调
static Future<bool> initialize({
SelectNotificationCallback? onSelect
}) async {
assert(!_initialized, 'M3u8Downloader.initialize() must be called only once!');
final CallbackHandle? handle = _getCallbackHandle(callbackDispatcher);
if (handle == null) {
return false;
}
if (onSelect != null) {
_onSelectNotification = onSelect;
}
_channel.setMethodCallHandler((MethodCall call) {
switch (call.method) {
case 'selectNotification':
if (_onSelectNotification == null) {
return Future.value(false);
}
return _onSelectNotification!();
default:
return Future.error('method not defined');
}
});
final bool? r = await _channel.invokeMethod<bool?>('initialize', {
"handle": handle.toRawHandle(),
}) ?? false;
_initialized = r ?? false;
return _initialized;
}
/// 下载配置
///
/// - [saveDir] 文件保存位置
/// - [showNotification] 是否显示通知
/// - [convertMp4] 是否转成mp4
/// - [connTimeout] 网络连接超时时间
/// - [readTimeout] 文件读取超时时间
/// - [threadCount] 同时下载的线程数
/// - [debugMode] 调试模式
static Future<bool> config({
String? saveDir,
bool showNotification = true,
bool convertMp4 = false,
int? connTimeout,
int? readTimeout,
int? threadCount,
bool? debugMode = true,
Function? progressCallback,
Function? successCallback,
Function? errorCallback,
}) async {
Map<String, dynamic> params = {
"saveDir": saveDir,
"showNotification": showNotification,
"convertMp4": convertMp4,
"connTimeout": connTimeout,
"readTimeout": readTimeout,
"threadCount": threadCount,
"debugMode": debugMode,
};
if (progressCallback != null) {
final CallbackHandle? handle = _getCallbackHandle(progressCallback);
if (handle != null) {
params["progressCallback"] = handle.toRawHandle();
}
}
if (successCallback != null) {
final CallbackHandle? handle = _getCallbackHandle(successCallback);
if (handle != null) {
params["successCallback"] = handle.toRawHandle();
}
}
if (errorCallback != null) {
final CallbackHandle? handle = _getCallbackHandle(errorCallback);
if (handle != null) {
params["errorCallback"] = handle.toRawHandle();
}
}
final bool? r = await _channel.invokeMethod<bool>('config', params);
return r ?? false;
}
/// 下载文件
///
/// - [url] 下载链接地址
/// - [name] 下载文件名(通知标题)
/// - [progressCallback] 下载进度回调
/// - [successCallback] 下载成功回调
/// - [errorCallback] 下载失败回调
static Future<dynamic> download({
required String url,
required String name,
Function? progressCallback,
Function? successCallback,
Function? errorCallback
}) async {
assert(url.isNotEmpty && name.isNotEmpty);
assert(_initialized, 'M3u8Downloader.initialize() must be called first!');
Map<String, dynamic> params = {
"url": url,
"name": name,
};
if (progressCallback != null) {
final CallbackHandle? handle = _getCallbackHandle(progressCallback);
if (handle != null) {
params["progressCallback"] = handle.toRawHandle();
}
}
if (successCallback != null) {
final CallbackHandle? handle = _getCallbackHandle(successCallback);
if (handle != null) {
params["successCallback"] = handle.toRawHandle();
}
}
if (errorCallback != null) {
final CallbackHandle? handle = _getCallbackHandle(errorCallback);
if (handle != null) {
params["errorCallback"] = handle.toRawHandle();
}
}
return await _channel.invokeMethod("download", params);
}
/// 查询任务
///
/// - [url] 下载链接地址
static Future<dynamic> searchInfo(String url,{
Function? progressCallback,
Function? successCallback,
Function? errorCallback,}) async {
assert(url.isNotEmpty);
assert(_initialized, 'M3u8Downloader.initialize() must be called first!');
Map<String, dynamic> params = {
"url": url,
};
if (progressCallback != null) {
final CallbackHandle? handle = _getCallbackHandle(progressCallback);
if (handle != null) {
params["progressCallback"] = handle.toRawHandle();
}
}
if (successCallback != null) {
final CallbackHandle? handle = _getCallbackHandle(successCallback);
if (handle != null) {
params["successCallback"] = handle.toRawHandle();
}
}
if (errorCallback != null) {
final CallbackHandle? handle = _getCallbackHandle(errorCallback);
if (handle != null) {
params["errorCallback"] = handle.toRawHandle();
}
}
return await _channel.invokeMethod("searchInfo", params) ?? null;
}
/// 暂停下载
///
/// - [url] 暂停指定的链接地址
static dynamic pause(String url) async {
assert(_initialized, 'M3u8Downloader.initialize() must be called first!');
return await _channel.invokeMethod("pause", {
"url": url
});
}
static void pauseAll() async {
assert(_initialized, 'M3u8Downloader.initialize() must be called first!');
await _channel.invokeMethod("pauseAll", {});
}
/// 删除下载
///
/// - [url] 下载链接地址
static Future<bool> delete(String url) async {
assert(url.isNotEmpty);
assert(_initialized, 'M3u8Downloader.initialize() must be called first!');
return await _channel.invokeMethod("delete", {
"url": url
}) ?? false;
}
static Future<bool> deleteAll() async {
assert(_initialized, 'M3u8Downloader.initialize() must be called first!');
return await _channel.invokeMethod("deleteAll",{}) ?? false;
}
/// 下载状态
static Future<bool> isRunning() async {
assert(_initialized, 'M3u8Downloader.initialize() must be called first!');
bool isRunning = await _channel.invokeMethod("isRunning");
return isRunning;
}
/// 通过URL获取保存的路径
/// - [url] 请求的URL
/// baseDir - 基础文件保存路径
/// m3u8 - m3u8文件地址
/// mp4 - mp4存储位置
static Future<dynamic> getSavePath(String url) async {
return await _channel.invokeMethod("getSavePath", { "url": url});
}
}
+65
View File
@@ -0,0 +1,65 @@
name: m3u8_downloader
description: m3u8 downloader
version: 1.3.0
author: Vincent
homepage:
environment:
sdk: ">=2.12.0 <3.0.0"
flutter: ">=1.20.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools_base/pub/pubspec
# The following section is specific to Flutter.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' and Android 'package' identifiers should not ordinarily
# be modified. They are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.vincent.m3u8Downloader
pluginClass: M3U8DownloaderPlugin
ios:
pluginClass: M3u8DownloaderPlugin
# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.
# To add custom fonts to your plugin package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages
@@ -0,0 +1,22 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
const MethodChannel channel = MethodChannel('m3u8_downloader');
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
channel.setMockMethodCallHandler((MethodCall methodCall) async {
return '42';
});
});
tearDown(() {
channel.setMockMethodCallHandler(null);
});
test('getPlatformVersion', () async {
// expect(await M3u8Downloader.platformVersion, '42');
});
}