初始化
This commit is contained in:
@@ -0,0 +1 @@
|
||||
TODO: Add your license here.
|
||||
@@ -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>
|
||||
+100
@@ -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});
|
||||
}
|
||||
}
|
||||
+389
@@ -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;
|
||||
}
|
||||
}
|
||||
+111
@@ -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);
|
||||
}
|
||||
}
|
||||
+93
@@ -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()));
|
||||
}
|
||||
}
|
||||
+41
@@ -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,
|
||||
}
|
||||
+95
@@ -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);
|
||||
}
|
||||
}
|
||||
+92
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+338
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+273
@@ -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());
|
||||
}
|
||||
};
|
||||
}
|
||||
+294
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -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);
|
||||
}
|
||||
+69
@@ -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);
|
||||
}
|
||||
+56
@@ -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);
|
||||
}
|
||||
+180
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+24
@@ -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);
|
||||
}
|
||||
}
|
||||
+249
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+160
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.vincent.m3u8_downloader_example;
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity;
|
||||
|
||||
public class MainActivity extends FlutterActivity {
|
||||
}
|
||||
+12
@@ -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>
|
||||
+12
@@ -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 |
BIN
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
|
||||
+6
@@ -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":[]}
|
||||
+1
@@ -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});
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user