基于Flutter的m3u8下载器

Flutter m3u8下载器。后台任务下载,支持加密下载。
只实现了Android端,并且只支持单m3u8视频下载(m3u8文件包含了多个ts文件,本质是多个ts同时下载)。

项目地址:m3u8_downloader

一、m3u8文件内容分析

用http://youku.cdn-iqiyi.com/20180523/11112_b1fb9d8b/index.m3u8地址举例。提取内容:

#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=800000,RESOLUTION=1080x608
1000k/hls/index.m3u8

可以看出该链接重定向到1000k/hls/index.m3u8,拼接链接得到http://youku.cdn-iqiyi.com/20180523/11112_b1fb9d8b/1000k/hls/index.m3u8。继续提取:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:9
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:4.276000,
65f7a658c87000.ts
#EXTINF:4.170000,
65f7a658c87001.ts
#EXTINF:5.754600,
65f7a658c87002.ts
#EXTINF:4.170000,
65f7a658c87003.ts
.....

该文件中每一行#EXTINF下面都是一些.ts的文件,其实这些就是视频片段。同样的,通过URL拼接,可以下载ts文件。

需要注意的是,有的m3u8文件中包含加密的key,也声明了加密的方式#EXT-X-KEY:METHOD=AES-128,URI="key.key"。比如:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:13
#EXT-X-KEY:METHOD=AES-128,URI="key.key"
#EXTINF:3.92,
223f43bef60c77c7bd5aa8008e4d8604.ts
#EXTINF:3.0,
736898a19f24587ee61579cc74ac2e98.ts
#EXTINF:3.0,
b9e5e152c12629813502b61218e3aa8a.ts
#EXTINF:3.0,
40d1fe66edb627bf2e76dce9afe588be.ts
......

二、实现下载的思路

  1. 解析m3u8文件。获取到.ts文件、.key文件的下载地址。
    /*** 将Url转换为M3U8对象** @param url* @return* @throws IOException*/public static M3U8 parseIndex(String url) throws IOException {// 获取m3u8的数据流BufferedReader reader = new BufferedReader(new InputStreamReader(new URL(url).openStream()));String basepath = url.substring(0, url.lastIndexOf("/") + 1);M3U8 ret = new M3U8();ret.setBasePath(basepath);String line;float seconds = 0;while ((line = reader.readLine()) != null) {if (line.startsWith("#")) {// TS 文件if (line.startsWith("#EXTINF:")) {line = line.substring(8);if (line.endsWith(",")) {line = line.substring(0, line.length() - 1);}seconds = Float.parseFloat(line);}continue;}// 包含了.m3u8文件if (line.endsWith("m3u8")) {return parseIndex(basepath + line);}ret.addTs(new M3U8Ts(line, seconds));seconds = 0;}reader.close();return ret;}
  1. 下载.ts .key文件。使用Executors、ExecutorService并发下载。
for (final M3U8Ts m3U8Ts : m3U8.getTsList()) {//循环下载TS切片文件executor.execute(new Runnable() {@Overridepublic void run() {File file;try {String fileName = M3U8EncryptHelper.encryptFileName(encryptKey, m3U8Ts.obtainEncodeTsFileName());file = new File(dir + File.separator + fileName);} catch (Exception e) {file = new File(dir + File.separator + m3U8Ts.getUrl());}if (!file.exists()) {//下载过的就不管了FileOutputStream fos = null;InputStream inputStream = null;try {URL url = new URL(m3U8Ts.obtainFullUrl(basePath));HttpURLConnection conn = (HttpURLConnection) url.openConnection();conn.setConnectTimeout(connTimeout);conn.setReadTimeout(readTimeout);if (conn.getResponseCode() == 200) {if (isStartDownload){isStartDownload = false;mHandler.sendEmptyMessage(WHAT_ON_START_DOWNLOAD);}inputStream = conn.getInputStream();fos = new FileOutputStream(file);//会自动创建文件int len = 0;byte[] buf = new byte[8 * 1024 * 1024];while ((len = inputStream.read(buf)) != -1) {curLength += len;fos.write(buf, 0, len);//写入流中}} else {handlerError(new Throwable(String.valueOf(conn.getResponseCode())));}} catch (MalformedURLException e) {handlerError(e);} catch (IOException e) {handlerError(e);} catch (Exception e) {handlerError(e);}finally{//关流if (inputStream != null) {try {inputStream.close();} catch (IOException e) {}}if (fos != null) {try {fos.close();} catch (IOException e) {}}}itemFileSize = file.length();m3U8Ts.setFileSize(itemFileSize);mHandler.sendEmptyMessage(WHAT_ON_PROGRESS);curTs++;}else {curTs ++;itemFileSize = file.length();m3U8Ts.setFileSize(itemFileSize);}}});
}
  1. 通过md5加密.m3u8文件 .ts文件、.key文件的下载地址。这样有两个好处,一是可以避免文件重复下载,二是有利于文件重组。
/*** 加密TS的URL地址*/
public String obtainEncodeTsFileName(){if (url == null)return "error.ts";return MD5Utils.encode(url).concat(".ts");
}
  1. 文件重组,重新定义根m3u8。创建根文件夹,把所有的.ts文件 .key文件集中起来。
 /*** 生成本地m3u8索引文件,ts切片和m3u8文件放在相同目录下即可* @param m3u8Dir* @param m3U8*/public static File createLocalM3U8(File m3u8Dir, String fileName, M3U8 m3U8, String keyPath) throws IOException{File m3u8File = new File(m3u8Dir, fileName);BufferedWriter bfw = new BufferedWriter(new FileWriter(m3u8File, 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");for (M3U8Ts m3U8Ts : m3U8.getTsList()) {// 如果m3u8加密,定义key文件if (keyPath != null) bfw.write("#EXT-X-KEY:METHOD=AES-128,URI=\""+keyPath+"\"\n");bfw.write("#EXTINF:" + m3U8Ts.getSeconds()+",\n");bfw.write(m3U8Ts.obtainEncodeTsFileName());bfw.newLine();}bfw.write("#EXT-X-ENDLIST");bfw.flush();bfw.close();return m3u8File;}

三、Flutter和原生相互通信

Flutter 提供 MethodChannel、EventChannel、BasicMessageChannel 三种方式。官方platform-channels说明

Android端的方法处理:

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {try {if (call.method.equals("initialize")) {// 初始化long callbackHandle = ((JSONArray) call.arguments).getLong(0);result.success(true);} else if (call.method.equals("config")) {// 配置下载器if (!call.hasArgument("saveDir")) {result.error("1", "saveDir必传", "");return;}M3U8DownloaderConfig config = M3U8DownloaderConfig.build(context);String saveDir = call.argument("saveDir");config.setSaveDir(saveDir);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("debugMode") && call.argument("debugMode") != JSONObject.NULL) {boolean debugMode = call.argument("debugMode");config.setDebugMode(debugMode);}result.success(true);} else if (call.method.equals("download")) {// 下载方法if (!call.hasArgument("url")) {result.error("1", "url必传", "");return;}String url = call.argument("url");M3U8Downloader.getInstance().download(url);result.success(null);} else if (call.method.equals("pause")) {// 暂停下载if (!call.hasArgument("url")) {result.error("1", "url必传", "");return;}String url = call.argument("url");M3U8Downloader.getInstance().pause(url);result.success(null);} else if (call.method.equals("cancel")) {// 取消下载if (!call.hasArgument("url")) {result.error("1", "url必传", "");return;}String url = call.argument("url");boolean isDelete = false;if (call.hasArgument("isDelete")) {isDelete = call.argument("isDelete");}if (isDelete) {M3U8Downloader.getInstance().cancelAndDelete(url, null);} else {M3U8Downloader.getInstance().pause(url);}result.success(null);} else if (call.method.equals("isRunning")) {// 获取运行状态result.success(M3U8Downloader.getInstance().isRunning());}  else if (call.method.equals("getM3U8Path")) {// 获取M3U8存储路径if (!call.hasArgument("url")) {result.error("1", "url必传", "");return;}String url = call.argument("url");result.success(M3U8Downloader.getInstance().getM3U8Path(url));}  else {result.notImplemented();}} catch (JSONException e) {result.error("error", "JSON error: " + e.getMessage(), null);} catch (Exception e) {result.error("error", "M3u8Downloader error: " + e.getMessage(), null);}
}

Flutter端消息处理:

class M3u8Downloader {static const MethodChannel _channel = const MethodChannel('vincent/m3u8_downloader', JSONMethodCodec());static _GetCallbackHandle _getCallbackHandle = (Function callback) => PluginUtilities.getCallbackHandle(callback);///  初始化下载器/// ///  在使用之前必须调用static Future<bool> initialize() async {final CallbackHandle handle = _getCallbackHandle(callbackDispatcher);if (handle == null) {return false;}final bool r = await _channel.invokeMethod<bool>('initialize', <dynamic>[handle.toRawHandle()]);return r ?? false;}/// 下载文件/// /// - [url] 下载链接地址/// - [callback] 回调函数static void download({String url, Function progressCallback, Function successCallback, Function errorCallback}) async {assert(url != null && url != "");Map<String, dynamic> params = {"url": url};await _channel.invokeMethod("download", params);}/// 配置方法////// - [saveDir] 文件保存位置/// - [connTimeout] 网络连接超时时间/// - [readTimeout] 文件读取超时时间/// - [debugMode] 调试模式static void config({ String saveDir, int connTimeout, int readTimeout, bool debugMode}) async {assert(Directory(saveDir).existsSync());await _channel.invokeMethod("config", {"saveDir": saveDir,"connTimeout": connTimeout,"readTimeout": readTimeout,"debugMode": debugMode});}/// 暂停下载/// /// - [url] 暂停指定的链接地址static void pause(String url) async {assert(url != null && url != "");await _channel.invokeMethod("pause", { "url": url });}/// 取消下载/// /// - [url] 下载链接地址/// - [isDelete] 取消时是否删除文件static void cancel(String url, { bool isDelete }) async {assert(url != null && url != "");await _channel.invokeMethod("cancel", { "url": url, "isDelete": isDelete });}/// 下载状态static Future<bool> isRunning() async {bool isRunning = await _channel.invokeMethod("isRunning");return isRunning;}/// 通过url获取保存的路径static Future<String> getM3U8Path(String url) async {String path = await _channel.invokeMethod("getM3U8Path", { "url": url });return path;}
}

四、下载的回调信息怎么发送到Flutter?

直接通过MethodChannel把回调消息发送到Flutter,这是不可取的。因为回调信息是异步的。

思路:再添加一条MethodChannel通道作为回调消息的通知。在Flutter中,定义下载的回调函数,然后把这个函数传给原生端,当原生端的下载的信息发生变化时,通过调用该回调函数,把消息通知出来。

原生 添加MethodChannel

public class FlutterM3U8BackgroundExecutor implements MethodChannel.MethodCallHandler {private static final  String TAG = "M3u8Downloader background";private static PluginRegistry.PluginRegistrantCallback pluginRegistrantCallback;private MethodChannel backgroundChannel;private FlutterEngine backgroundFlutterEngine;private AtomicBoolean isCallbackDispatcherReady = new AtomicBoolean(false);public static void setPluginRegistrant(PluginRegistry.PluginRegistrantCallback callback) {pluginRegistrantCallback = callback;}public static void setCallbackDispatcher(Context context, long callbackHandle) {SharedPreferences prefs = context.getSharedPreferences(M3u8DownloaderPlugin.SHARED_PREFERENCES_KEY, 0);prefs.edit().putLong(M3u8DownloaderPlugin.CALLBACK_DISPATCHER_HANDLE_KEY, callbackHandle).apply();}public boolean isRunning() {return isCallbackDispatcherReady.get();}private void onInitialized() {// TODO 初始化完成后}@Overridepublic void onMethodCall(MethodCall call, MethodChannel.Result result) {String method = call.method;Object arguments = call.arguments;try {// 初始化if (method.equals("didInitializeDispatcher")) {onInitialized();isCallbackDispatcherReady.set(true);}} catch(Exception e) {result.error("error", "M3u8Download error: " + e.getMessage(), null);}}// 启用后台Isolatevoid startBackgroundIsolate(Context context) {// 没有运行才启用if (!isRunning()) {SharedPreferences p = context.getSharedPreferences(M3u8DownloaderPlugin.SHARED_PREFERENCES_KEY, 0);long callbackHandle = p.getLong(M3u8DownloaderPlugin.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...");String appBundlePath = FlutterMain.findAppBundlePath(context);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);if (pluginRegistrantCallback != null) {pluginRegistrantCallback.registerWith(new ShimPluginRegistry(backgroundFlutterEngine));}}}private void initializeMethodChannel(BinaryMessenger isolate) {backgroundChannel = new MethodChannel(isolate, "vincent/m3u8_downloader_background", JSONMethodCodec.INSTANCE);backgroundChannel.setMethodCallHandler(this);}// 执行dart回调函数public void executeDartCallbackInBackgroundIsolate(long callbackHandle, Object args) {backgroundChannel.invokeMethod("", new Object[] {callbackHandle, args});}
}

Flutter添加MethodChannel

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');
}

需要注意的地方

  1. 原生端初始化时,初始化FlutterM3U8BackgroundExecutor
if (call.method.equals("initialize")) {long callbackHandle = ((JSONArray) call.arguments).getLong(0);flutterM3U8BackgroundExecutor.setCallbackDispatcher(context, callbackHandle);flutterM3U8BackgroundExecutor.startBackgroundIsolate(context);result.success(true);
}
  1. 下载函数中去回调方法
if (call.method.equals("download")) {if (!call.hasArgument("url")) {result.error("1", "url必传", "");return;}String url = call.argument("url");final long progressCallbackHandle = call.hasArgument("progressCallback") && call.argument("progressCallback") != JSONObject.NULL ? (long)call.argument("progressCallback") : -1;final long successCallbackHandle = call.hasArgument("successCallback") && call.argument("successCallback") != JSONObject.NULL ? (long)call.argument("successCallback") : -1;final long errorCallbackHandle = call.hasArgument("errorCallback") && call.argument("errorCallback") != JSONObject.NULL ? (long)call.argument("errorCallback") : -1;M3U8Downloader.getInstance().download(url);M3U8Downloader.getInstance().setOnM3U8DownloadListener(new OnM3U8DownloadListener() {@Overridepublic void onDownloadProgress(final M3U8Task task) {super.onDownloadProgress(task);if (progressCallbackHandle != -1) {//下载进度,非UI线程final Map<String, Object> args = new HashMap<>();args.put("url", task.getUrl());args.put("state", task.getState());args.put("progress", task.getProgress());args.put("speed", task.getSpeed());args.put("formatSpeed", task.getFormatSpeed());args.put("totalSize", task.getFormatTotalSize());handler.post(new Runnable() {@Overridepublic void run() {flutterM3U8BackgroundExecutor.executeDartCallbackInBackgroundIsolate(progressCallbackHandle, args);}});}}@Overridepublic void onDownloadItem(M3U8Task task, long itemFileSize, int totalTs, int curTs) {super.onDownloadItem(task, itemFileSize, totalTs, curTs);//下载切片监听,非UI线程
//          channel.invokeMethod();}@Overridepublic void onDownloadSuccess(M3U8Task task) {super.onDownloadSuccess(task);String saveDir = MUtils.getSaveFileDir(task.getUrl());final Map<String, Object> args = new HashMap<>();args.put("dir", saveDir);args.put("fileName", saveDir + File.separator + "local.m3u8");//下载成功if (successCallbackHandle != -1) {handler.post(new Runnable() {@Overridepublic void run() {flutterM3U8BackgroundExecutor.executeDartCallbackInBackgroundIsolate(successCallbackHandle, args);}});}}

基于Flutter的m3u8下载器相关推荐

  1. Protocol ‘https‘ not on whitelist ‘file,crypto‘ ——m3u8下载协议不在白名单,m3u8下载器推荐

    1.FFmpeg报错 接上一篇<cmd合并多个ts文件,ffmpeg快速转ts为mp4文件,通过m3u8合并文件> https://blog.csdn.net/qq_33957603/ar ...

  2. 基于GMap.NET地图下载器的开发和研究

    基于GMap.NET地图下载器的开发和研究 软件下载地址:https://pan.baidu.com/s/1ay0aOm3fiZ35vlfD8kFYFw 1.地图浏览功能 可以浏览谷歌地图.百度.ar ...

  3. 【CentOS7】在服务器上搭建基于Aria2的离线下载器

    说明   在平时从网上下载文件时,有时会遇到即使一个几十兆的文件都要下一天的困境,如果使用远程服务器先登录ssh,再通过wget.scp的方式下载文件又比较费事.参考网上有人用Aria2搭建离线下载器 ...

  4. NAS m3u8下载器 m3u8转mp4保存本地

    安装 docker安装 docker run -d -p 8081:8081 -v /Volumes/work/github-project/ffandown/media2:/app/media -v ...

  5. ALDownloadManager 基于Alamofire封装的下载器

    ALDownloadManager包含了断点续传,多文件顺序下载,多文件同时下载 同时下载 顺序下载 外层调用: 单文件下载 ALDownloadManager.shared.download(url ...

  6. 基于M3U8下载直播回放视频

    本次用到一个网页和一个软件. 基于知乎一个教程:先获得M3U8流,再通过M3U8下载器得到视频. 其他方法:方法1:Chrome插件ChromeVideo扩展 方法2:基于python环境的you-g ...

  7. 基于iOS 10、realm封装的下载器

    代码地址如下: http://www.demodashi.com/demo/11653.html 概要 在决定自己封装一个下载器前,我本以为没有那么复杂,可在实际开发过程中困难重重,再加上iOS10和 ...

  8. 基于iOS 10封装的下载器(支持存储读取、断点续传、后台下载、杀死APP重启后的断点续传等功能)

    原文 资源来自:http://www.cocoachina.com/ios/20170316/18901.html 概要 在决定自己封装一个下载器前,我本以为没有那么复杂,可在实际开发过程中困难重重, ...

  9. 转:使用Python写一个m3u8多线程下载器

    转载:使用Python写一个m3u8多线程下载器 可去看原文:https://blog.csdn.net/muslim377287976/article/details/104340242 文章目录 ...

  10. m3u8视频下载器,可下载各大视频网站资源(自动合并切片)

    简介: m3u8下载器让你轻松下载各种给不同类型的文件数据,支持离线下载还有强大的搜索引擎还有丰富的功能等你来体验,下载的同时还可以浏览视频文件,支持m3u8格式视频的转换还有强大的且流畅的播放体验, ...

最新文章

  1. python project_GitHub - DeqianBai/Python-Project: A series of python projects
  2. 微信好友个性标签词云--微信数据分析(四)
  3. 转 C# 串口编程遇到的问题以及解决方法
  4. 计算机二维全息图原理,三维信息加密如何使用计算全息进行
  5. java电子通讯录毕业设计_(C)JAVA001电子通讯录(带系统托盘)
  6. 关于vue中Cannot read property 'length' of undefined 导致:数据不显示问题【自己经验参考】
  7. Android* 操作系统上的应用程序远程调试
  8. python comprehension_Python从题目中学习:List comprehension
  9. 计算机监控系统必须有直流系统吗,变电站直流屏是否必须用蓄电池
  10. 九章算术卷第七 盈不足
  11. python多线程调用携程_《Python》线程池、携程
  12. java语言和python语言的区别_java和python的区别
  13. 日期时间编辑器(模拟QDateTimeEdit的自定义控件)——QML
  14. python hist bins_python – matplotlib中的Hist:Bins不居中,轴上的比例不正确
  15. 基于stm32的自动循迹及自动搬运物联网图传小车
  16. 代码随想录第十五天 二叉树层序遍历 226、101
  17. 计算机组成原理 好学吗,计算机组成原理太难了(计算机编程解释)
  18. 《读书是一辈子的事》中篇 了解未来
  19. UIToolBar实现高斯模糊
  20. 用Java抓取10年大乐透中奖数据

热门文章

  1. 如何使用移动端后台管理数据
  2. table固定表头、固定列
  3. 华为浏览器工具箱 html修改,华为手机修改浏览器模式为电脑浏览模式的方法
  4. 2021-07-28 cad贱人工具箱5.8
  5. 自动驾驶横向控制 LQR 算法推导及仿真学习笔记
  6. API网关之-协议转换原理
  7. SSH命令批量操作服务器
  8. 【滑动窗口协议模拟】
  9. CSS实现div垂直居中 div上下居中显示
  10. vue组件库(Element UI)