├── ios ├── Assets │ └── .gitkeep ├── Classes │ ├── M3u8DownloaderPlugin.h │ ├── SwiftM3u8DownloaderPlugin.swift │ └── M3u8DownloaderPlugin.m ├── .gitignore └── m3u8_downloader.podspec ├── LICENSE ├── android ├── settings.gradle ├── gradle.properties ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── vincent │ │ └── m3u8Downloader │ │ ├── listener │ │ ├── OnInfoCallback.java │ │ ├── OnTaskDownloadListener.java │ │ └── OnM3U8DownloadListener.java │ │ ├── bean │ │ ├── M3U8TaskState.java │ │ ├── M3U8Ts.java │ │ ├── M3U8Task.java │ │ └── M3U8.java │ │ ├── utils │ │ ├── M3U8Log.java │ │ ├── SpHelper.java │ │ ├── EncryptUtil.java │ │ ├── NotificationUtil.java │ │ └── M3U8Util.java │ │ ├── downloader │ │ ├── M3U8DownloadConfig.java │ │ ├── M3U8Downloader.java │ │ ├── M3U8DownloadTask.java │ │ └── WeakHandler.java │ │ ├── FlutterBackgroundExecutor.java │ │ └── M3U8DownloaderPlugin.java ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── build.gradle ├── example ├── ios │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── xcshareddata │ │ │ └── xcschemes │ │ │ │ └── Runner.xcscheme │ │ └── project.pbxproj │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── .gitignore ├── android │ ├── gradle.properties │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── java │ │ │ │ │ └── com │ │ │ │ │ │ └── vincent │ │ │ │ │ │ └── m3u8_downloader_example │ │ │ │ │ │ └── MainActivity.java │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── settings.gradle │ └── build.gradle ├── .metadata ├── README.md ├── .gitignore ├── test │ └── widget_test.dart ├── pubspec.yaml ├── lib │ └── main.dart └── pubspec.lock ├── .idea ├── .gitignore ├── vcs.xml ├── libraries │ ├── Flutter_Plugins.xml │ └── Dart_SDK.xml ├── runConfigurations │ └── example_lib_main_dart.xml ├── misc.xml ├── modules.xml └── workspace.xml ├── .gitignore ├── .metadata ├── README.md ├── test └── m3u8_downloader_test.dart ├── m3u8_downloader.iml ├── lib ├── callback_dispatcher.dart └── m3u8_downloader.dart ├── pubspec.yaml └── pubspec.lock /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | TODO: Add your license here. 2 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'm3u8Downloader' 2 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /ios/Classes/M3u8DownloaderPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface M3U8DownloaderPlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .atom/ 3 | .buildlog/ 4 | .history 5 | .svn/ 6 | .dart_tool/ 7 | .idea/ 8 | 9 | .packages 10 | .pub/ 11 | 12 | build/ 13 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lytian/m3u8_downloader/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 6 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/vincent/m3u8_downloader_example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.vincent.m3u8_downloader_example; 2 | 3 | import io.flutter.embedding.android.FlutterActivity; 4 | 5 | public class MainActivity extends FlutterActivity { 6 | } 7 | -------------------------------------------------------------------------------- /.idea/libraries/Flutter_Plugins.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: f4abaa0735eba4dfd8f33f73363911d63931fe03 8 | channel: stable 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # m3u8_downloader 2 | 3 | m3u8下载器 4 | 5 | 实现思路请查看文章: [基于Flutter的m3u8下载器](https://blog.csdn.net/tly599167/article/details/105141528) 6 | 7 | ## Getting Started 8 | 9 | 在 pubspec.yaml 中新增依赖: 10 | ```yaml 11 | m3u8_downloader: 12 | git: 13 | url: https://github.com/lytian/m3u8_downloader.git 14 | ``` 15 | 16 | 或者Fork项目,引用自己的仓库。 17 | 18 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: f4abaa0735eba4dfd8f33f73363911d63931fe03 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /android/src/main/java/com/vincent/m3u8Downloader/listener/OnInfoCallback.java: -------------------------------------------------------------------------------- 1 | package com.vincent.m3u8Downloader.listener; 2 | 3 | import com.vincent.m3u8Downloader.bean.M3U8; 4 | 5 | /** 6 | * @Author: Vincent 7 | * @CreateAt: 2021/08/26 9:58 8 | * @Desc: 获取M3U8文件信息的回调函数 9 | */ 10 | public interface OnInfoCallback { 11 | 12 | void success(M3U8 m3u8); 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/runConfigurations/example_lib_main_dart.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.vincent.m3u8Downloader' 2 | version '1.0' 3 | 4 | buildscript { 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:4.1.0' 12 | } 13 | } 14 | 15 | rootProject.allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | apply plugin: 'com.android.library' 23 | 24 | android { 25 | compileSdkVersion 30 26 | 27 | defaultConfig { 28 | minSdkVersion 16 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:4.1.0' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | project.evaluationDependsOn(':app') 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | /Flutter/ephemeral/ 38 | /Flutter/flutter_export_environment.sh -------------------------------------------------------------------------------- /ios/Classes/SwiftM3u8DownloaderPlugin.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | public class SwiftM3u8DownloaderPlugin: NSObject, FlutterPlugin { 5 | public static func register(with registrar: FlutterPluginRegistrar) { 6 | let channel = FlutterMethodChannel(name: "m3u8_downloader", binaryMessenger: registrar.messenger()) 7 | let instance = SwiftM3u8DownloaderPlugin() 8 | registrar.addMethodCallDelegate(instance, channel: channel) 9 | } 10 | 11 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 12 | result("iOS " + UIDevice.current.systemVersion) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # m3u8_downloader_example 2 | 3 | Demonstrates how to use the m3u8_downloader plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /test/m3u8_downloader_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:m3u8_downloader/m3u8_downloader.dart'; 4 | 5 | void main() { 6 | const MethodChannel channel = MethodChannel('m3u8_downloader'); 7 | 8 | TestWidgetsFlutterBinding.ensureInitialized(); 9 | 10 | setUp(() { 11 | channel.setMockMethodCallHandler((MethodCall methodCall) async { 12 | return '42'; 13 | }); 14 | }); 15 | 16 | tearDown(() { 17 | channel.setMockMethodCallHandler(null); 18 | }); 19 | 20 | test('getPlatformVersion', () async { 21 | expect(await M3u8Downloader.platformVersion, '42'); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /ios/Classes/M3u8DownloaderPlugin.m: -------------------------------------------------------------------------------- 1 | #import "M3U8DownloaderPlugin.h" 2 | #if __has_include() 3 | #import 4 | #else 5 | // Support project import fallback if the generated compatibility header 6 | // is not copied when this plugin is created as a library. 7 | // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 8 | #import "m3u8_downloader-Swift.h" 9 | #endif 10 | 11 | @implementation M3U8DownloaderPlugin 12 | + (void)registerWithRegistrar:(NSObject*)registrar { 13 | [SwiftM3u8DownloaderPlugin registerWithRegistrar:registrar]; 14 | } 15 | @end 16 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/ephemeral/ 22 | Flutter/app.flx 23 | Flutter/app.zip 24 | Flutter/flutter_assets/ 25 | Flutter/flutter_export_environment.sh 26 | ServiceDefinitions.json 27 | Runner/GeneratedPluginRegistrant.* 28 | 29 | # Exceptions to above rules. 30 | !default.mode1v3 31 | !default.mode2v3 32 | !default.pbxuser 33 | !default.perspectivev3 34 | -------------------------------------------------------------------------------- /android/src/main/java/com/vincent/m3u8Downloader/bean/M3U8TaskState.java: -------------------------------------------------------------------------------- 1 | package com.vincent.m3u8Downloader.bean; 2 | 3 | /** 4 | * @Author: Vincent 5 | * @CreateAt: 2021/08/25 17:18 6 | * @Desc: 下载任务状态 7 | */ 8 | public enum M3U8TaskState { 9 | /** 10 | * 默认状态 11 | */ 12 | DEFAULT, 13 | /** 14 | * 下载排队中 15 | */ 16 | PENDING, 17 | /** 18 | * 下载准备中 19 | */ 20 | PREPARE, 21 | /** 22 | * 正在下载中 23 | */ 24 | DOWNLOADING, 25 | /** 26 | * 下载成功 27 | */ 28 | SUCCESS, 29 | /** 30 | * 下载失败 31 | */ 32 | ERROR, 33 | /** 34 | * 暂停下载 35 | */ 36 | PAUSE, 37 | /** 38 | * 存储空间不足 39 | */ 40 | ENOSPC, 41 | } 42 | -------------------------------------------------------------------------------- /android/src/main/java/com/vincent/m3u8Downloader/utils/M3U8Log.java: -------------------------------------------------------------------------------- 1 | package com.vincent.m3u8Downloader.utils; 2 | 3 | import android.util.Log; 4 | 5 | import com.vincent.m3u8Downloader.downloader.M3U8DownloadConfig; 6 | 7 | /** 8 | * @Author: Vincent 9 | * @CreateAt: 2021/08/25 17:58 10 | * @Desc: M3U8日志系统 11 | */ 12 | public class M3U8Log { 13 | private static final boolean isDebugMode = M3U8DownloadConfig.isDebugMode(); 14 | private static final String TAG = "M3U8Log"; 15 | private static final String PREFIX = "====== "; 16 | 17 | public static void d(String msg){ 18 | if (isDebugMode) Log.d(TAG, PREFIX + msg); 19 | } 20 | 21 | public static void e(String msg){ 22 | if (isDebugMode) Log.e(TAG, PREFIX + msg); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/m3u8_downloader.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint m3u8_downloader.podspec` to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'm3u8_downloader' 7 | s.version = '0.0.1' 8 | s.summary = 'm3u8下载器' 9 | s.description = <<-DESC 10 | m3u8下载器 11 | DESC 12 | s.homepage = 'http://example.com' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Your Company' => 'email@example.com' } 15 | s.source = { :path => '.' } 16 | s.source_files = 'Classes/**/*' 17 | s.dependency 'Flutter' 18 | s.platform = :ios, '8.0' 19 | 20 | # Flutter.framework does not contain a i386 slice. 21 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } 22 | s.swift_version = '5.0' 23 | end 24 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /m3u8_downloader.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:m3u8_downloader_example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Verify Platform version', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that platform version is retrieved. 19 | expect( 20 | find.byWidgetPredicate( 21 | (Widget widget) => widget is Text && 22 | widget.data!.startsWith('Running on:'), 23 | ), 24 | findsOneWidget, 25 | ); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/libraries/Dart_SDK.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /lib/callback_dispatcher.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:ui'; 3 | 4 | import 'package:flutter/services.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | /// Pragma annotation is needed to avoid tree shaking in release mode. See 8 | /// https://github.com/dart-lang/sdk/blob/master/runtime/docs/compiler/aot/entry_point_pragma.md 9 | @pragma('vm:entry-point') 10 | void callbackDispatcher() { 11 | 12 | // Initialize state necessary for MethodChannels. 13 | WidgetsFlutterBinding.ensureInitialized(); 14 | const MethodChannel backgroundChannel = MethodChannel('vincent/m3u8_downloader_background', JSONMethodCodec()); 15 | 16 | backgroundChannel.setMethodCallHandler((MethodCall call) async { 17 | final dynamic args = call.arguments; 18 | final CallbackHandle handle = CallbackHandle.fromRawHandle(args[0]); 19 | 20 | final Function? closure = PluginUtilities.getCallbackFromHandle(handle); 21 | 22 | if (closure == null) { 23 | print('Fatal: could not find callback'); 24 | exit(-1); 25 | } 26 | 27 | closure(args[1]); 28 | }); 29 | 30 | backgroundChannel.invokeMethod('didInitializeDispatcher'); 31 | } -------------------------------------------------------------------------------- /android/src/main/java/com/vincent/m3u8Downloader/listener/OnTaskDownloadListener.java: -------------------------------------------------------------------------------- 1 | package com.vincent.m3u8Downloader.listener; 2 | 3 | import com.vincent.m3u8Downloader.bean.M3U8; 4 | 5 | /** 6 | * @Author: Vincent 7 | * @CreateAt: 2021/08/25 22:37 8 | * @Desc: 任务下载监听器 9 | */ 10 | public interface OnTaskDownloadListener { 11 | 12 | /** 13 | * 开始任务 14 | */ 15 | void onStart(); 16 | 17 | /** 18 | * 开始下载 19 | * @param totalTs ts总数 20 | * @param curTs 当前下载完成的ts个数 21 | */ 22 | void onStartDownload(int totalTs, int curTs); 23 | 24 | /** 25 | * ts文件下载完成 26 | * 注意:这个方法是异步的(子线程中执行),所以不能在此方法中回调,其他方法为主线程中回调 27 | * @param itemFileSize 单个文件的大小 28 | * @param totalTs ts总数 29 | * @param curTs 当前下载完成的ts个数 30 | */ 31 | void onDownloadItem(long itemFileSize, int totalTs, int curTs); 32 | 33 | /** 34 | * 定时进度 35 | * @param curLength 下载大小 36 | */ 37 | void onProgress(long curLength); 38 | 39 | /** 40 | * 正在转成MP4格式 41 | */ 42 | void onConvert(); 43 | 44 | /** 45 | * 下载成功 46 | */ 47 | void onSuccess(M3U8 m3U8); 48 | 49 | /** 50 | * 错误的时候回调 51 | * 线程环境无法保证,不可以直接在UI线程调用 52 | * @param error 错误信息 53 | */ 54 | void onError(Throwable error); 55 | } 56 | -------------------------------------------------------------------------------- /android/src/main/java/com/vincent/m3u8Downloader/listener/OnM3U8DownloadListener.java: -------------------------------------------------------------------------------- 1 | package com.vincent.m3u8Downloader.listener; 2 | 3 | import com.vincent.m3u8Downloader.bean.M3U8Task; 4 | 5 | /** 6 | * @Author: Vincent 7 | * @CreateAt: 2021/08/25 22:40 8 | * @Desc: M3U8Downloader 监听器 9 | */ 10 | public interface OnM3U8DownloadListener { 11 | 12 | /** 13 | * 下载准备 14 | * @param task 当前准备任务 15 | */ 16 | void onDownloadPrepare(M3U8Task task); 17 | 18 | /** 19 | * 等待下载 20 | * @param task 等待的任务 21 | */ 22 | void onDownloadPending(M3U8Task task); 23 | 24 | /** 25 | * 下载进度 26 | * 异步回调,不可以直接在UI线程调用 27 | * @param task 当前下载任务 28 | */ 29 | void onDownloadProgress(M3U8Task task); 30 | 31 | /** 32 | * 完成一次下载任务 33 | * @param task 下载任务 34 | * @param itemFileSize 此任务的文件大小 35 | * @param totalTs 总切片数 36 | * @param curTs 已下载切片数 37 | */ 38 | void onDownloadItem(M3U8Task task, long itemFileSize, int totalTs, int curTs); 39 | 40 | /** 41 | * 下载成功 42 | */ 43 | void onDownloadSuccess(M3U8Task task); 44 | 45 | /** 46 | * 暂停下载 47 | * @param task 暂停的任务 48 | */ 49 | void onDownloadPause(M3U8Task task); 50 | 51 | /** 52 | * 准备转成MP4 53 | */ 54 | void onConvert(); 55 | 56 | /** 57 | * 下载失败 58 | * @param task 失败的任务 59 | * @param error 错误信息 60 | */ 61 | void onDownloadError(M3U8Task task, Throwable error); 62 | 63 | /** 64 | * 停止下载 65 | * @param task 停止的任务 66 | */ 67 | void onStop(M3U8Task task); 68 | } 69 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | m3u8_downloader_example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | def keystoreProperties = new Properties() 28 | def keystorePropertiesFile = rootProject.file('key.properties') 29 | if (keystorePropertiesFile.exists()) { 30 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 31 | } 32 | 33 | android { 34 | compileSdkVersion 31 35 | 36 | defaultConfig { 37 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 38 | applicationId "com.vincent.m3u8_downloader_example" 39 | minSdkVersion 16 40 | targetSdkVersion 31 41 | versionCode flutterVersionCode.toInteger() 42 | versionName flutterVersionName 43 | } 44 | signingConfigs { 45 | release { 46 | keyAlias keystoreProperties['keyAlias'] 47 | keyPassword keystoreProperties['keyPassword'] 48 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 49 | storePassword keystoreProperties['storePassword'] 50 | } 51 | } 52 | 53 | buildTypes { 54 | release { 55 | signingConfig signingConfigs.release 56 | } 57 | } 58 | 59 | } 60 | 61 | flutter { 62 | source '../..' 63 | } 64 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: m3u8_downloader 2 | description: m3u8 downloader 3 | version: 1.3.1 4 | author: Vincent 5 | homepage: 6 | 7 | environment: 8 | sdk: ">=2.12.0 <3.0.0" 9 | flutter: ">=1.20.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | 19 | # For information on the generic Dart part of this file, see the 20 | # following page: https://dart.dev/tools/pub/pubspec 21 | 22 | # The following section is specific to Flutter. 23 | flutter: 24 | # This section identifies this Flutter project as a plugin project. 25 | # The 'pluginClass' and Android 'package' identifiers should not ordinarily 26 | # be modified. They are used by the tooling to maintain consistency when 27 | # adding or updating assets for this project. 28 | plugin: 29 | platforms: 30 | android: 31 | package: com.vincent.m3u8Downloader 32 | pluginClass: M3U8DownloaderPlugin 33 | ios: 34 | pluginClass: M3U8DownloaderPlugin 35 | 36 | # To add assets to your plugin package, add an assets section, like this: 37 | # assets: 38 | # - images/a_dot_burr.jpeg 39 | # - images/a_dot_ham.jpeg 40 | # 41 | # For details regarding assets in packages, see 42 | # https://flutter.dev/assets-and-images/#from-packages 43 | # 44 | # An image asset can refer to one or more resolution-specific "variants", see 45 | # https://flutter.dev/assets-and-images/#resolution-aware. 46 | 47 | # To add custom fonts to your plugin package, add a fonts section here, 48 | # in this "flutter" section. Each entry in this list should have a 49 | # "family" key with the font family name, and a "fonts" key with a 50 | # list giving the asset and other descriptors for the font. For 51 | # example: 52 | # fonts: 53 | # - family: Schyler 54 | # fonts: 55 | # - asset: fonts/Schyler-Regular.ttf 56 | # - asset: fonts/Schyler-Italic.ttf 57 | # style: italic 58 | # - family: Trajan Pro 59 | # fonts: 60 | # - asset: fonts/TrajanPro.ttf 61 | # - asset: fonts/TrajanPro_Bold.ttf 62 | # weight: 700 63 | # 64 | # For details regarding fonts in packages, see 65 | # https://flutter.dev/custom-fonts/#from-packages 66 | -------------------------------------------------------------------------------- /android/src/main/java/com/vincent/m3u8Downloader/bean/M3U8Ts.java: -------------------------------------------------------------------------------- 1 | package com.vincent.m3u8Downloader.bean; 2 | 3 | import com.vincent.m3u8Downloader.utils.EncryptUtil; 4 | 5 | import java.net.MalformedURLException; 6 | import java.net.URL; 7 | 8 | /** 9 | * @Author: Vincent 10 | * @CreateAt: 2021/08/25 16:36 11 | * @Desc: m3u8切片 12 | */ 13 | public class M3U8Ts implements Comparable { 14 | /** 15 | * ts网络请求地址(完整的网络请求地址请使用obtainFullUrl) 16 | */ 17 | private String url; 18 | /** 19 | * 文件大小 20 | */ 21 | private long fileSize; 22 | /** 23 | * ts秒数 24 | */ 25 | private float seconds; 26 | 27 | public M3U8Ts(String url, float seconds) { 28 | this.url = url; 29 | this.seconds = seconds; 30 | } 31 | 32 | public String getUrl() { 33 | return url; 34 | } 35 | 36 | public void setUrl(String url) { 37 | this.url = url; 38 | } 39 | 40 | public long getFileSize() { 41 | return fileSize; 42 | } 43 | 44 | public void setFileSize(long fileSize) { 45 | this.fileSize = fileSize; 46 | } 47 | 48 | public float getSeconds() { 49 | return seconds; 50 | } 51 | 52 | public void setSeconds(float seconds) { 53 | this.seconds = seconds; 54 | } 55 | 56 | @Override 57 | public int compareTo(M3U8Ts m3U8Ts) { 58 | return url.compareTo(m3U8Ts.url); 59 | } 60 | 61 | @Override 62 | public String toString() { 63 | return "M3U8Ts{" + 64 | "url='" + url + '\'' + 65 | ", fileSize=" + fileSize + 66 | ", seconds=" + seconds + 67 | '}'; 68 | } 69 | 70 | /** 71 | * 获取加密后的文件名 72 | * @return ts文件名 73 | */ 74 | public String obtainEncodeTsFileName(){ 75 | if (url == null) return "error.ts"; 76 | 77 | return EncryptUtil.md5Encode(url).concat(".ts"); 78 | } 79 | 80 | /** 81 | * 获取完整的URL地址 82 | * @param hostUrl host地址 83 | * @return URL地址 84 | */ 85 | public URL obtainFullUrl(String hostUrl) throws MalformedURLException { 86 | if (url == null || hostUrl == null) { 87 | return null; 88 | } 89 | URL host = new URL(hostUrl); 90 | return new URL(host, url); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /android/src/main/java/com/vincent/m3u8Downloader/bean/M3U8Task.java: -------------------------------------------------------------------------------- 1 | package com.vincent.m3u8Downloader.bean; 2 | 3 | import com.vincent.m3u8Downloader.utils.M3U8Util; 4 | 5 | /** 6 | * @Author: Vincent 7 | * @CreateAt: 2021/08/25 17:20 8 | * @Desc: M3U8下载任务 9 | */ 10 | public class M3U8Task { 11 | 12 | private String url; 13 | private M3U8TaskState state = M3U8TaskState.DEFAULT; 14 | private long speed; 15 | private float progress; 16 | private M3U8 m3U8; 17 | 18 | private M3U8Task() {} 19 | 20 | public M3U8Task(String url){ 21 | this.url = url; 22 | } 23 | 24 | public String getUrl() { 25 | return url; 26 | } 27 | 28 | public void setUrl(String url) { 29 | this.url = url; 30 | } 31 | 32 | public M3U8TaskState getState() { 33 | return state; 34 | } 35 | 36 | public void setState(M3U8TaskState state) { 37 | this.state = state; 38 | } 39 | 40 | public long getSpeed() { 41 | return speed; 42 | } 43 | 44 | public void setSpeed(long speed) { 45 | this.speed = speed; 46 | } 47 | 48 | public float getProgress() { 49 | return progress; 50 | } 51 | 52 | public void setProgress(float progress) { 53 | this.progress = progress; 54 | } 55 | 56 | public M3U8 getM3U8() { 57 | return m3U8; 58 | } 59 | 60 | public void setM3U8(M3U8 m3U8) { 61 | this.m3U8 = m3U8; 62 | } 63 | 64 | @Override 65 | public boolean equals(Object o) { 66 | if (this == o) return true; 67 | if (o == null || getClass() != o.getClass()) return false; 68 | M3U8Task m3U8Task = (M3U8Task) o; 69 | return url.equals(m3U8Task.url); 70 | } 71 | 72 | public String getFormatSpeed() { 73 | if (speed == 0) return ""; 74 | return M3U8Util.formatFileSize(speed) + "/s"; 75 | } 76 | 77 | public long getTotalSize() { 78 | if (m3U8 == null) return 0; 79 | return m3U8.getTotalFileSize(); 80 | } 81 | 82 | public String getFormatTotalSize() { 83 | if (m3U8 == null) return ""; 84 | long fileSize = getTotalSize(); 85 | if (fileSize == 0) return ""; 86 | return M3U8Util.formatFileSize(fileSize); 87 | } 88 | 89 | public String getFormatCurrentSize() { 90 | if (m3U8 == null)return ""; 91 | return M3U8Util.formatFileSize((long)(progress * m3U8.getTotalFileSize())); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 20 | 24 | 28 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: m3u8_downloader_example 2 | description: Demonstrates how to use the m3u8_downloader plugin. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | environment: 9 | sdk: ">=2.12.0 <3.0.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | path_provider: ^2.0.10 16 | permission_handler: ^9.2.0 17 | open_file: ^3.2.1 18 | m3u8_downloader: 19 | # When depending on this package from a real application you should use: 20 | # m3u8_downloader: ^x.y.z 21 | # See https://dart.dev/tools/pub/dependencies#version-constraints 22 | # The example app is bundled with the plugin so we use a path dependency on 23 | # the parent directory to use the current plugin's version. 24 | path: ../ 25 | 26 | # The following adds the Cupertino Icons font to your application. 27 | # Use with the CupertinoIcons class for iOS style icons. 28 | cupertino_icons: ^1.0.2 29 | 30 | dev_dependencies: 31 | flutter_test: 32 | sdk: flutter 33 | 34 | # For information on the generic Dart part of this file, see the 35 | # following page: https://dart.dev/tools/pub/pubspec 36 | 37 | # The following section is specific to Flutter. 38 | flutter: 39 | 40 | # The following line ensures that the Material Icons font is 41 | # included with your application, so that you can use the icons in 42 | # the material Icons class. 43 | uses-material-design: true 44 | 45 | # To add assets to your application, add an assets section, like this: 46 | # assets: 47 | # - images/a_dot_burr.jpeg 48 | # - images/a_dot_ham.jpeg 49 | 50 | # An image asset can refer to one or more resolution-specific "variants", see 51 | # https://flutter.dev/assets-and-images/#resolution-aware. 52 | 53 | # For details regarding adding assets from package dependencies, see 54 | # https://flutter.dev/assets-and-images/#from-packages 55 | 56 | # To add custom fonts to your application, add a fonts section here, 57 | # in this "flutter" section. Each entry in this list should have a 58 | # "family" key with the font family name, and a "fonts" key with a 59 | # list giving the asset and other descriptors for the font. For 60 | # example: 61 | # fonts: 62 | # - family: Schyler 63 | # fonts: 64 | # - asset: fonts/Schyler-Regular.ttf 65 | # - asset: fonts/Schyler-Italic.ttf 66 | # style: italic 67 | # - family: Trajan Pro 68 | # fonts: 69 | # - asset: fonts/TrajanPro.ttf 70 | # - asset: fonts/TrajanPro_Bold.ttf 71 | # weight: 700 72 | # 73 | # For details regarding fonts from package dependencies, 74 | # see https://flutter.dev/custom-fonts/#from-packages 75 | -------------------------------------------------------------------------------- /android/src/main/java/com/vincent/m3u8Downloader/bean/M3U8.java: -------------------------------------------------------------------------------- 1 | package com.vincent.m3u8Downloader.bean; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * @Author: Vincent 8 | * @CreateAt: 2021/08/25 16:31 9 | * @Desc: m3u8实体类 10 | */ 11 | public class M3U8 { 12 | private String baseUrl; 13 | private String dirPath; 14 | private String localPath; 15 | private String key; 16 | private String iv; 17 | 18 | private List tsList = new ArrayList<>(); 19 | 20 | public String getBaseUrl() { 21 | return baseUrl; 22 | } 23 | 24 | public void setBaseUrl(String baseUrl) { 25 | this.baseUrl = baseUrl; 26 | } 27 | 28 | public String getDirPath() { 29 | return dirPath; 30 | } 31 | 32 | public void setDirPath(String dirPath) { 33 | this.dirPath = dirPath; 34 | } 35 | 36 | public String getLocalPath() { 37 | return localPath; 38 | } 39 | 40 | public void setLocalPath(String localPath) { 41 | this.localPath = localPath; 42 | } 43 | 44 | public String getKey() { 45 | return key; 46 | } 47 | 48 | public void setKey(String key) { 49 | this.key = key; 50 | } 51 | 52 | public String getIv() { 53 | return iv; 54 | } 55 | 56 | public void setIv(String iv) { 57 | this.iv = iv; 58 | } 59 | 60 | public List getTsList() { 61 | return tsList; 62 | } 63 | 64 | public void setTsList(List tsList) { 65 | this.tsList = tsList; 66 | } 67 | 68 | public void addTs(M3U8Ts ts) { 69 | this.tsList.add(ts); 70 | } 71 | 72 | public long getTotalFileSize() { 73 | long fileSize = 0; 74 | for (M3U8Ts m3U8Ts : tsList){ 75 | fileSize = fileSize + m3U8Ts.getFileSize(); 76 | } 77 | return fileSize; 78 | } 79 | 80 | public long getTotalTime() { 81 | long totalTime = 0; 82 | for (M3U8Ts m3U8Ts : tsList){ 83 | totalTime = totalTime + (int)(m3U8Ts.getSeconds() * 1000); 84 | } 85 | return totalTime; 86 | } 87 | 88 | @Override 89 | public String toString() { 90 | return "M3U8{" + 91 | "basePath='" + baseUrl + '\'' + 92 | ", dirPath='" + dirPath + '\'' + 93 | ", localPath='" + localPath + '\'' + 94 | ", key='" + key + '\'' + 95 | ", iv='" + iv + '\'' + 96 | ", totalFileSize=" + getTotalFileSize() + 97 | ", totalTime=" + getTotalTime() + 98 | '}'; 99 | } 100 | 101 | @Override 102 | public boolean equals(Object o) { 103 | if (this == o) return true; 104 | if (o == null || getClass() != o.getClass()) return false; 105 | M3U8 m3U8 = (M3U8) o; 106 | return baseUrl.equals(m3U8.baseUrl); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /android/src/main/java/com/vincent/m3u8Downloader/downloader/M3U8DownloadConfig.java: -------------------------------------------------------------------------------- 1 | package com.vincent.m3u8Downloader.downloader; 2 | 3 | import android.content.Context; 4 | import android.os.Environment; 5 | 6 | import com.vincent.m3u8Downloader.utils.SpHelper; 7 | 8 | import java.io.File; 9 | 10 | /** 11 | * @Author: Vincent 12 | * @CreateAt: 2021/08/25 21:37 13 | * @Desc: 配置类 14 | */ 15 | public class M3U8DownloadConfig { 16 | private static final String TAG_SAVE_DIR = "TAG_SAVE_DIR_M3U8"; 17 | private static final String TAG_THREAD_COUNT = "TAG_THREAD_COUNT_M3U8"; 18 | private static final String TAG_CONN_TIMEOUT = "TAG_CONN_TIMEOUT_M3U8"; 19 | private static final String TAG_READ_TIMEOUT = "TAG_READ_TIMEOUT_M3U8"; 20 | private static final String TAG_DEBUG = "TAG_DEBUG_M3U8"; 21 | private static final String TAG_SHOW_NOTIFICATION = "TAG_SHOW_NOTIFICATION_M3U8"; 22 | private static final String TAG_CONVERT_MP4 = "TAG_CONVERT_MP4"; 23 | 24 | public static M3U8DownloadConfig build(Context context){ 25 | SpHelper.init(context); 26 | return new M3U8DownloadConfig(); 27 | } 28 | 29 | public M3U8DownloadConfig setSaveDir(String saveDir){ 30 | SpHelper.putString(TAG_SAVE_DIR, saveDir); 31 | return this; 32 | } 33 | 34 | @SuppressWarnings("deprecation") 35 | public static String getSaveDir(){ 36 | return SpHelper.getString(TAG_SAVE_DIR, Environment.getExternalStorageDirectory().getPath() + File.separator + "M3u8Downloader"); 37 | } 38 | 39 | public M3U8DownloadConfig setThreadCount(int threadCount){ 40 | if (threadCount > 5) threadCount = 5; 41 | if (threadCount <= 0) threadCount = 1; 42 | SpHelper.putInt(TAG_THREAD_COUNT, threadCount); 43 | return this; 44 | } 45 | 46 | public static int getThreadCount(){ 47 | return SpHelper.getInt(TAG_THREAD_COUNT, 3); 48 | } 49 | 50 | public M3U8DownloadConfig setConnTimeout(int connTimeout){ 51 | SpHelper.putInt(TAG_CONN_TIMEOUT, connTimeout); 52 | return this; 53 | } 54 | 55 | public static int getConnTimeout(){ 56 | return SpHelper.getInt(TAG_CONN_TIMEOUT, 10 * 1000); 57 | } 58 | 59 | public M3U8DownloadConfig setReadTimeout(int readTimeout){ 60 | SpHelper.putInt(TAG_READ_TIMEOUT, readTimeout); 61 | return this; 62 | } 63 | 64 | public static int getReadTimeout(){ 65 | return SpHelper.getInt(TAG_READ_TIMEOUT, 30 * 60 * 1000); 66 | } 67 | 68 | 69 | public M3U8DownloadConfig setDebugMode(boolean debug){ 70 | SpHelper.putBoolean(TAG_DEBUG, debug); 71 | return this; 72 | } 73 | 74 | public static boolean isDebugMode(){ 75 | return SpHelper.getBoolean(TAG_DEBUG, false); 76 | } 77 | 78 | public M3U8DownloadConfig setShowNotification(boolean show){ 79 | SpHelper.putBoolean(TAG_SHOW_NOTIFICATION, show); 80 | return this; 81 | } 82 | 83 | public static boolean isShowNotification(){ 84 | return SpHelper.getBoolean(TAG_SHOW_NOTIFICATION, true); 85 | } 86 | 87 | public M3U8DownloadConfig setConvertMp4(boolean convertMp4){ 88 | SpHelper.putBoolean(TAG_CONVERT_MP4, convertMp4); 89 | return this; 90 | } 91 | 92 | public static boolean isConvertMp4(){ 93 | return SpHelper.getBoolean(TAG_CONVERT_MP4, false); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.flutter-io.cn" 9 | source: hosted 10 | version: "2.9.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.flutter-io.cn" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.flutter-io.cn" 23 | source: hosted 24 | version: "1.2.1" 25 | clock: 26 | dependency: transitive 27 | description: 28 | name: clock 29 | url: "https://pub.flutter-io.cn" 30 | source: hosted 31 | version: "1.1.1" 32 | collection: 33 | dependency: transitive 34 | description: 35 | name: collection 36 | url: "https://pub.flutter-io.cn" 37 | source: hosted 38 | version: "1.16.0" 39 | fake_async: 40 | dependency: transitive 41 | description: 42 | name: fake_async 43 | url: "https://pub.flutter-io.cn" 44 | source: hosted 45 | version: "1.3.1" 46 | flutter: 47 | dependency: "direct main" 48 | description: flutter 49 | source: sdk 50 | version: "0.0.0" 51 | flutter_test: 52 | dependency: "direct dev" 53 | description: flutter 54 | source: sdk 55 | version: "0.0.0" 56 | matcher: 57 | dependency: transitive 58 | description: 59 | name: matcher 60 | url: "https://pub.flutter-io.cn" 61 | source: hosted 62 | version: "0.12.12" 63 | material_color_utilities: 64 | dependency: transitive 65 | description: 66 | name: material_color_utilities 67 | url: "https://pub.flutter-io.cn" 68 | source: hosted 69 | version: "0.1.5" 70 | meta: 71 | dependency: transitive 72 | description: 73 | name: meta 74 | url: "https://pub.flutter-io.cn" 75 | source: hosted 76 | version: "1.8.0" 77 | path: 78 | dependency: transitive 79 | description: 80 | name: path 81 | url: "https://pub.flutter-io.cn" 82 | source: hosted 83 | version: "1.8.2" 84 | sky_engine: 85 | dependency: transitive 86 | description: flutter 87 | source: sdk 88 | version: "0.0.99" 89 | source_span: 90 | dependency: transitive 91 | description: 92 | name: source_span 93 | url: "https://pub.flutter-io.cn" 94 | source: hosted 95 | version: "1.9.0" 96 | stack_trace: 97 | dependency: transitive 98 | description: 99 | name: stack_trace 100 | url: "https://pub.flutter-io.cn" 101 | source: hosted 102 | version: "1.10.0" 103 | stream_channel: 104 | dependency: transitive 105 | description: 106 | name: stream_channel 107 | url: "https://pub.flutter-io.cn" 108 | source: hosted 109 | version: "2.1.0" 110 | string_scanner: 111 | dependency: transitive 112 | description: 113 | name: string_scanner 114 | url: "https://pub.flutter-io.cn" 115 | source: hosted 116 | version: "1.1.1" 117 | term_glyph: 118 | dependency: transitive 119 | description: 120 | name: term_glyph 121 | url: "https://pub.flutter-io.cn" 122 | source: hosted 123 | version: "1.2.1" 124 | test_api: 125 | dependency: transitive 126 | description: 127 | name: test_api 128 | url: "https://pub.flutter-io.cn" 129 | source: hosted 130 | version: "0.4.12" 131 | vector_math: 132 | dependency: transitive 133 | description: 134 | name: vector_math 135 | url: "https://pub.flutter-io.cn" 136 | source: hosted 137 | version: "2.1.2" 138 | sdks: 139 | dart: ">=2.17.0-0 <3.0.0" 140 | flutter: ">=1.20.0" 141 | -------------------------------------------------------------------------------- /android/src/main/java/com/vincent/m3u8Downloader/utils/SpHelper.java: -------------------------------------------------------------------------------- 1 | package com.vincent.m3u8Downloader.utils; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.util.Log; 6 | 7 | import androidx.annotation.NonNull; 8 | import androidx.annotation.Nullable; 9 | 10 | import java.util.Collections; 11 | import java.util.Set; 12 | 13 | /** 14 | * @Author: Vincent 15 | * @CreateAt: 2021/08/25 17:16 16 | * @Desc: SharedPreferences帮助类 17 | */ 18 | public class SpHelper { 19 | 20 | private static final String NULL_KEY = "NULL_KEY"; 21 | private static final String TAG_NAME = "M3U8PreferenceHelper"; 22 | 23 | private static SharedPreferences PREFERENCES; 24 | 25 | 26 | public static void init(Context context) { 27 | PREFERENCES = context.getSharedPreferences(TAG_NAME, Context.MODE_PRIVATE); 28 | } 29 | 30 | public static void onSetPrefBoolSetting(String Tag, Boolean Value, Context activityContext) { 31 | if (Tag != null && Value != null && activityContext != null) { 32 | SharedPreferences settings = activityContext.getSharedPreferences(TAG_NAME, 0); 33 | settings.edit().putBoolean(Tag, Value).apply(); 34 | } 35 | } 36 | 37 | private static String checkKeyNonNull(String key) { 38 | if (key == null) { 39 | Log.e(NULL_KEY, "Key is null!!!"); 40 | return NULL_KEY; 41 | } 42 | return key; 43 | } 44 | 45 | private static SharedPreferences.Editor newEditor() { 46 | return PREFERENCES.edit(); 47 | } 48 | 49 | public static void putBoolean(@NonNull String key, boolean value) { 50 | newEditor().putBoolean(checkKeyNonNull(key), value).apply(); 51 | } 52 | 53 | public static boolean getBoolean(@NonNull String key, boolean defValue) { 54 | return PREFERENCES.getBoolean(checkKeyNonNull(key), defValue); 55 | } 56 | 57 | public static void putInt(@NonNull String key, int value) { 58 | newEditor().putInt(checkKeyNonNull(key), value).apply(); 59 | } 60 | 61 | public static int getInt(@NonNull String key, int defValue) { 62 | return PREFERENCES.getInt(checkKeyNonNull(key), defValue); 63 | } 64 | 65 | public static void putLong(@NonNull String key, long value) { 66 | newEditor().putLong(checkKeyNonNull(key), value).apply(); 67 | } 68 | 69 | public static long getLong(@NonNull String key, long defValue) { 70 | return PREFERENCES.getLong(checkKeyNonNull(key), defValue); 71 | } 72 | 73 | public static void putFloat(@NonNull String key, float value) { 74 | newEditor().putFloat(checkKeyNonNull(key), value).apply(); 75 | } 76 | 77 | public static float getFloat(@NonNull String key, float defValue) { 78 | return PREFERENCES.getFloat(checkKeyNonNull(key), defValue); 79 | } 80 | 81 | public static void putString(@NonNull String key, @Nullable String value) { 82 | newEditor().putString(checkKeyNonNull(key), value).apply(); 83 | } 84 | 85 | public static String getString(@NonNull String key, @Nullable String defValue) { 86 | return PREFERENCES.getString(checkKeyNonNull(key), defValue); 87 | } 88 | 89 | public static void putStringSet(@NonNull String key, @Nullable Set values) { 90 | newEditor().putStringSet(checkKeyNonNull(key), values).apply(); 91 | } 92 | 93 | public static Set getStringSet(@NonNull String key, @Nullable Set defValues) { 94 | Set result = PREFERENCES.getStringSet(checkKeyNonNull(key), defValues); 95 | return result == null ? null : Collections.unmodifiableSet(result); 96 | } 97 | 98 | public static void increaseCount(String key) { 99 | int count = getInt(key, 0); 100 | putInt(key, ++count); 101 | } 102 | 103 | public static void remove(String key) { 104 | newEditor().remove(key).apply(); 105 | } 106 | 107 | public static void clearPreference() { 108 | newEditor().clear().commit(); 109 | } 110 | 111 | } -------------------------------------------------------------------------------- /android/src/main/java/com/vincent/m3u8Downloader/FlutterBackgroundExecutor.java: -------------------------------------------------------------------------------- 1 | package com.vincent.m3u8Downloader; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.content.res.AssetManager; 6 | 7 | import androidx.annotation.NonNull; 8 | 9 | import java.util.concurrent.atomic.AtomicBoolean; 10 | 11 | import io.flutter.Log; 12 | import io.flutter.embedding.engine.FlutterEngine; 13 | import io.flutter.embedding.engine.dart.DartExecutor; 14 | import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistry; 15 | import io.flutter.plugin.common.BinaryMessenger; 16 | import io.flutter.plugin.common.JSONMethodCodec; 17 | import io.flutter.plugin.common.MethodCall; 18 | import io.flutter.plugin.common.MethodChannel; 19 | import io.flutter.plugin.common.PluginRegistry; 20 | import io.flutter.view.FlutterCallbackInformation; 21 | 22 | /** 23 | * @Author: Vincent 24 | * @CreateAt: 2021/08/26 16:19 25 | * @Desc: 初始化运行回调调度程序的后台隔离,用于在后台启动时调用Dart回调。 26 | */ 27 | public class FlutterBackgroundExecutor implements MethodChannel.MethodCallHandler { 28 | public static final String SHARED_PREFERENCES_KEY = "vincent.m3u8.downloader.pref"; 29 | public static final String CALLBACK_DISPATCHER_HANDLE_KEY = "callback_dispatcher_handle_key"; 30 | private static final String TAG = "M3u8Downloader background"; 31 | @SuppressWarnings("deprecation") 32 | private static PluginRegistry.PluginRegistrantCallback pluginRegistrantCallback; 33 | private MethodChannel backgroundChannel; 34 | private FlutterEngine backgroundFlutterEngine; 35 | private final AtomicBoolean isCallbackDispatcherReady = new AtomicBoolean(false); 36 | 37 | public static void setCallbackDispatcher(Context context, long callbackHandle) { 38 | SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0); 39 | prefs.edit().putLong(CALLBACK_DISPATCHER_HANDLE_KEY, callbackHandle).apply(); 40 | } 41 | 42 | public boolean isRunning() { 43 | return isCallbackDispatcherReady.get(); 44 | } 45 | 46 | private void onInitialized() { 47 | isCallbackDispatcherReady.set(true); 48 | } 49 | 50 | @Override 51 | public void onMethodCall(MethodCall call, @NonNull MethodChannel.Result result) { 52 | String method = call.method; 53 | if (method.equals("didInitializeDispatcher")) { 54 | onInitialized(); 55 | result.success(true); 56 | } else { 57 | result.notImplemented(); 58 | } 59 | } 60 | 61 | void startBackgroundIsolate(Context context) { 62 | if (!isRunning()) { 63 | SharedPreferences p = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0); 64 | long callbackHandle = p.getLong(CALLBACK_DISPATCHER_HANDLE_KEY, 0); 65 | startBackgroundIsolate(context, callbackHandle); 66 | } 67 | } 68 | 69 | public void startBackgroundIsolate(Context context, long callbackHandle) { 70 | if (backgroundFlutterEngine != null) { 71 | Log.e(TAG, "Background isolate already started"); 72 | return; 73 | } 74 | Log.i(TAG, "Starting Background isolate..."); 75 | @SuppressWarnings("deprecation") 76 | String appBundlePath = io.flutter.view.FlutterMain.findAppBundlePath(context); 77 | AssetManager assets = context.getAssets(); 78 | if (appBundlePath != null && !isRunning()) { 79 | backgroundFlutterEngine = new FlutterEngine(context); 80 | FlutterCallbackInformation flutterCallback = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle); 81 | if (flutterCallback == null) { 82 | Log.e(TAG, "Fatal: failed to find callback"); 83 | return; 84 | } 85 | DartExecutor executor = backgroundFlutterEngine.getDartExecutor(); 86 | initializeMethodChannel(executor); 87 | DartExecutor.DartCallback dartCallback = new DartExecutor.DartCallback(assets, appBundlePath, flutterCallback); 88 | 89 | executor.executeDartCallback(dartCallback); 90 | 91 | if (pluginRegistrantCallback != null) { 92 | pluginRegistrantCallback.registerWith(new ShimPluginRegistry(backgroundFlutterEngine)); 93 | } 94 | } 95 | } 96 | 97 | private void initializeMethodChannel(BinaryMessenger isolate) { 98 | backgroundChannel = new MethodChannel(isolate, "vincent/m3u8_downloader_background", JSONMethodCodec.INSTANCE); 99 | backgroundChannel.setMethodCallHandler(this); 100 | } 101 | 102 | public void executeDartCallbackInBackgroundIsolate(long callbackHandle, Object args) { 103 | backgroundChannel.invokeMethod("", new Object[]{callbackHandle, args}); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/m3u8_downloader.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui'; 3 | 4 | import 'package:flutter/services.dart'; 5 | 6 | import 'callback_dispatcher.dart'; 7 | 8 | typedef CallbackHandle? _GetCallbackHandle(Function callback); 9 | typedef SelectNotificationCallback = Future Function(); 10 | 11 | class M3u8Downloader { 12 | static const MethodChannel _channel = const MethodChannel('vincent/m3u8_downloader', JSONMethodCodec()); 13 | static _GetCallbackHandle _getCallbackHandle = (Function callback) => PluginUtilities.getCallbackHandle(callback); 14 | static SelectNotificationCallback? _onSelectNotification; 15 | static bool _initialized = false; 16 | 17 | 18 | /// 初始化下载器 19 | /// 在使用之前必须调用 20 | /// 21 | /// - [onSelect] 点击通知的回调 22 | static Future initialize({ 23 | SelectNotificationCallback? onSelect 24 | }) async { 25 | assert(!_initialized, 'M3u8Downloader.initialize() must be called only once!'); 26 | 27 | final CallbackHandle? handle = _getCallbackHandle(callbackDispatcher); 28 | if (handle == null) { 29 | return false; 30 | } 31 | if (onSelect != null) { 32 | _onSelectNotification = onSelect; 33 | } 34 | _channel.setMethodCallHandler((MethodCall call) { 35 | switch (call.method) { 36 | case 'selectNotification': 37 | if (_onSelectNotification == null) { 38 | return Future.value(false); 39 | } 40 | return _onSelectNotification!(); 41 | default: 42 | return Future.error('method not defined'); 43 | } 44 | }); 45 | 46 | final bool? r = await _channel.invokeMethod('initialize',{ 47 | "handle": handle.toRawHandle(), 48 | }); 49 | _initialized = r ?? false; 50 | return _initialized; 51 | } 52 | 53 | /// 下载配置 54 | /// 55 | /// - [saveDir] 文件保存位置 56 | /// - [showNotification] 是否显示通知 57 | /// - [convertMp4] 是否转成mp4 58 | /// - [connTimeout] 网络连接超时时间 59 | /// - [readTimeout] 文件读取超时时间 60 | /// - [threadCount] 同时下载的线程数 61 | /// - [debugMode] 调试模式 62 | static Future config({ 63 | String? saveDir, 64 | bool showNotification = true, 65 | bool convertMp4 = false, 66 | int? connTimeout, 67 | int? readTimeout, 68 | int? threadCount, 69 | bool? debugMode, 70 | }) async { 71 | final bool? r = await _channel.invokeMethod('config',{ 72 | "saveDir": saveDir, 73 | "showNotification": showNotification, 74 | "convertMp4": convertMp4, 75 | "connTimeout": connTimeout, 76 | "readTimeout": readTimeout, 77 | "threadCount": threadCount, 78 | "debugMode": debugMode, 79 | }); 80 | return r ?? false; 81 | } 82 | 83 | /// 下载文件 84 | /// 85 | /// - [url] 下载链接地址 86 | /// - [name] 下载文件名(通知标题) 87 | /// - [progressCallback] 下载进度回调 88 | /// - [successCallback] 下载成功回调 89 | /// - [errorCallback] 下载失败回调 90 | static void download({ 91 | required String url, 92 | required String name, 93 | Function? progressCallback, 94 | Function? successCallback, 95 | Function? errorCallback 96 | }) async { 97 | assert(url.isNotEmpty && name.isNotEmpty); 98 | assert(_initialized, 'M3u8Downloader.initialize() must be called first!'); 99 | 100 | Map params = { 101 | "url": url, 102 | "name": name, 103 | }; 104 | if (progressCallback != null) { 105 | final CallbackHandle? handle = _getCallbackHandle(progressCallback); 106 | if (handle != null) { 107 | params["progressCallback"] = handle.toRawHandle(); 108 | } 109 | } 110 | if (successCallback != null) { 111 | final CallbackHandle? handle = _getCallbackHandle(successCallback); 112 | if (handle != null) { 113 | params["successCallback"] = handle.toRawHandle(); 114 | } 115 | } 116 | if (errorCallback != null) { 117 | final CallbackHandle? handle = _getCallbackHandle(errorCallback); 118 | if (handle != null) { 119 | params["errorCallback"] = handle.toRawHandle(); 120 | } 121 | } 122 | 123 | await _channel.invokeMethod("download", params); 124 | } 125 | 126 | /// 暂停下载 127 | /// 128 | /// - [url] 暂停指定的链接地址 129 | static void pause(String url) async { 130 | assert(_initialized, 'M3u8Downloader.initialize() must be called first!'); 131 | await _channel.invokeMethod("pause", { 132 | "url": url 133 | }); 134 | } 135 | 136 | /// 删除下载 137 | /// 138 | /// - [url] 下载链接地址 139 | static Future delete(String url) async { 140 | assert(url.isNotEmpty); 141 | assert(_initialized, 'M3u8Downloader.initialize() must be called first!'); 142 | 143 | return await _channel.invokeMethod("delete", { 144 | "url": url 145 | }) ?? false; 146 | } 147 | 148 | /// 下载状态 149 | static Future isRunning() async { 150 | assert(_initialized, 'M3u8Downloader.initialize() must be called first!'); 151 | bool isRunning = await _channel.invokeMethod("isRunning"); 152 | return isRunning; 153 | } 154 | 155 | /// 通过URL获取保存的路径 156 | /// - [url] 请求的URL 157 | /// baseDir - 基础文件保存路径 158 | /// m3u8 - m3u8文件地址 159 | /// mp4 - mp4存储位置 160 | static Future getSavePath(String url) async { 161 | return await _channel.invokeMethod("getSavePath", { "url": url }); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /android/src/main/java/com/vincent/m3u8Downloader/utils/EncryptUtil.java: -------------------------------------------------------------------------------- 1 | package com.vincent.m3u8Downloader.utils; 2 | 3 | import android.text.TextUtils; 4 | 5 | import java.math.BigInteger; 6 | import java.security.MessageDigest; 7 | import java.security.NoSuchAlgorithmException; 8 | import java.security.spec.AlgorithmParameterSpec; 9 | 10 | import javax.crypto.Cipher; 11 | import javax.crypto.KeyGenerator; 12 | import javax.crypto.SecretKey; 13 | import javax.crypto.spec.IvParameterSpec; 14 | import javax.crypto.spec.SecretKeySpec; 15 | 16 | /** 17 | * @Author: Vincent 18 | * @CreateAt: 2021/08/25 16:42 19 | * @Desc: 加解密工具 20 | */ 21 | public class EncryptUtil { 22 | 23 | private final static String ENCODING = "UTF-8"; 24 | 25 | /** 26 | * md5加密字符串 27 | * @param str 待加密字符串 28 | * @return 加密后的字符串 29 | */ 30 | public static String md5Encode(String str) { 31 | try { 32 | MessageDigest md = MessageDigest.getInstance("MD5"); 33 | md.update(str.getBytes()); 34 | return new BigInteger(1, md.digest()).toString(16); 35 | } catch (NoSuchAlgorithmException e) { 36 | e.printStackTrace(); 37 | } 38 | return str; 39 | } 40 | 41 | /** 42 | * 生成密钥 43 | * 自动生成base64 编码后的AES128位密钥 44 | */ 45 | public static String getAESKey() throws Exception { 46 | KeyGenerator kg = KeyGenerator.getInstance("AES"); 47 | kg.init(128); 48 | SecretKey sk = kg.generateKey(); 49 | byte[] b = sk.getEncoded(); 50 | return parseByte2HexStr(b); 51 | } 52 | 53 | /** 54 | * AES 加密 55 | * @param base64Key base64编码后的 AES key 56 | * @param text 待加密的字符串 57 | * @return 加密后的byte[] 58 | * @throws Exception 异常 59 | */ 60 | public static byte[] getAESEncode(String base64Key, String text) throws Exception{ 61 | return getAESEncode(base64Key, text.getBytes()); 62 | } 63 | 64 | /** 65 | * AES 加密 66 | * @param base64Key base64编码后的 AES key 67 | * @param bytes 待加密的bytes 68 | * @return 加密后的byte[] 69 | * @throws Exception 异常 70 | */ 71 | public static byte[] getAESEncode(String base64Key, byte[] bytes) throws Exception{ 72 | if (base64Key == null)return bytes; 73 | byte[] key = parseHexStr2Byte(base64Key); 74 | SecretKeySpec sKeySpec = new SecretKeySpec(key, "AES"); 75 | Cipher cipher = Cipher.getInstance("AES"); 76 | cipher.init(Cipher.ENCRYPT_MODE, sKeySpec); 77 | return cipher.doFinal(bytes); 78 | } 79 | 80 | /** 81 | * AES解密 82 | * @param base64Key base64编码后的 AES key 83 | * @param text 待解密的字符串 84 | * @return 解密后的byte[] 85 | * @throws Exception 异常 86 | */ 87 | public static byte[] getAESDecode(String base64Key, String text) throws Exception{ 88 | return getAESDecode(base64Key, text.getBytes()); 89 | } 90 | 91 | /** 92 | * AES解密 93 | * @param base64Key base64编码后的 AES key 94 | * @param bytes 待解密的字符串 95 | * @return 解密后的byte[] 数组 96 | * @throws Exception 异常 97 | */ 98 | public static byte[] getAESDecode(String base64Key, byte[] bytes) throws Exception{ 99 | if (base64Key == null)return bytes; 100 | byte[] key = parseHexStr2Byte(base64Key); 101 | SecretKeySpec sKeySpec = new SecretKeySpec(key, "AES"); 102 | Cipher cipher = Cipher.getInstance("AES"); 103 | cipher.init(Cipher.DECRYPT_MODE, sKeySpec); 104 | return cipher.doFinal(bytes); 105 | } 106 | 107 | /** 108 | * 将二进制转换成16进制 109 | * @param buf byte数组 110 | * @return 16进制字符串 111 | */ 112 | public static String parseByte2HexStr(byte[] buf) { 113 | StringBuilder sb = new StringBuilder(); 114 | for (byte b : buf) { 115 | String hex = Integer.toHexString(b & 0xFF); 116 | if (hex.length() == 1) { 117 | hex = '0' + hex; 118 | } 119 | sb.append(hex.toUpperCase()); 120 | } 121 | return sb.toString(); 122 | } 123 | 124 | /** 125 | * 将16进制转换为二进制 126 | * @param hexStr 16进制字符串 127 | * @return byte[] 128 | */ 129 | public static byte[] parseHexStr2Byte(String hexStr) { 130 | if (hexStr.length() < 1) 131 | return null; 132 | byte[] result = new byte[hexStr.length()/2]; 133 | for (int i = 0; i< hexStr.length()/2; i++) { 134 | int high = Integer.parseInt(hexStr.substring(i*2, i*2+1), 16); 135 | int low = Integer.parseInt(hexStr.substring(i*2+1, i*2+2), 16); 136 | result[i] = (byte) (high * 16 + low); 137 | } 138 | return result; 139 | } 140 | 141 | /** 142 | * 解密ts文件 143 | * @param bytes 文件字节流 144 | * @param key m3u8的key 145 | * @param iv m3u8的iv 146 | * @return 解密后的byte[] 147 | * @throws Exception 异常 148 | */ 149 | public static byte[] decryptTs(byte[] bytes, String key, String iv) throws Exception { 150 | if (TextUtils.isEmpty(key)) { 151 | return bytes; 152 | } 153 | Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); 154 | byte[] ivByte = new byte[16]; 155 | if (!TextUtils.isEmpty(iv)) { 156 | if (iv.startsWith("0x")) 157 | ivByte = parseHexStr2Byte(iv.substring(2)); 158 | else 159 | ivByte = iv.getBytes(); 160 | 161 | if (ivByte == null || ivByte.length != 16) 162 | ivByte = new byte[16]; 163 | } 164 | SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(ENCODING), "AES"); 165 | AlgorithmParameterSpec paramSpec = new IvParameterSpec(ivByte); 166 | cipher.init(Cipher.DECRYPT_MODE, keySpec, paramSpec); 167 | return cipher.doFinal(bytes); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /android/src/main/java/com/vincent/m3u8Downloader/utils/NotificationUtil.java: -------------------------------------------------------------------------------- 1 | package com.vincent.m3u8Downloader.utils; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.NotificationChannel; 5 | import android.app.PendingIntent; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.os.Build; 9 | 10 | import androidx.core.app.NotificationCompat; 11 | import androidx.core.app.NotificationManagerCompat; 12 | 13 | import com.vincent.m3u8Downloader.bean.M3U8TaskState; 14 | 15 | /** 16 | * @Author: Vincent 17 | * @CreateAt: 2021/08/26 16:47 18 | * @Desc: 19 | */ 20 | public class NotificationUtil { 21 | public static final int NOTIFICATION_ID = 9527; 22 | public static final String NOTIFICATION_CHANNEL_ID = "M3U8_DOWNLOADER_NOTIFICATION"; 23 | public static final String ACTION_SELECT_NOTIFICATION = "SELECT_NOTIFICATION"; 24 | 25 | @SuppressLint("StaticFieldLeak") 26 | private static NotificationUtil instance; 27 | private NotificationCompat.Builder builder; 28 | private android.app.NotificationManager notificationManager; 29 | private Context context; 30 | private int notificationProgress = -100; 31 | 32 | public static NotificationUtil getInstance(){ 33 | if (null == instance) { 34 | instance = new NotificationUtil(); 35 | } 36 | return instance; 37 | } 38 | 39 | /** 40 | * 构建通知 41 | * @param c 上下文 42 | */ 43 | public void build(Context c) { 44 | if (notificationManager != null) return; 45 | this.context = c; 46 | 47 | // Make a channel if necessary 48 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 49 | // Create the NotificationChannel, but only on API 26+ because 50 | // the NotificationChannel class is new and not in the support library 51 | 52 | CharSequence name = context.getApplicationInfo().loadLabel(context.getPackageManager()); 53 | int importance = android.app.NotificationManager.IMPORTANCE_DEFAULT; 54 | NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance); 55 | channel.setSound(null, null); 56 | 57 | // Add the channel 58 | notificationManager = context.getSystemService(android.app.NotificationManager.class); 59 | 60 | if (notificationManager != null) { 61 | notificationManager.createNotificationChannel(channel); 62 | } 63 | } 64 | 65 | // Create the notification 66 | builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) 67 | // .setSmallIcon(R.drawable.ic_download) // 通知图标 68 | .setOnlyAlertOnce(true) 69 | .setAutoCancel(true) // 默认不自动取消 70 | .setPriority(NotificationCompat.PRIORITY_DEFAULT); // 默认优先级 71 | } 72 | 73 | /** 74 | * 更新通知 75 | * @param state 下载状态 76 | * @param progress 下载进度 77 | */ 78 | public void updateNotification(String fileName, M3U8TaskState state, int progress) { 79 | if (builder == null) return; 80 | 81 | builder.setContentTitle(fileName == null || fileName.equals("") ? "下载M3U8文件" : fileName); 82 | switch (state) { 83 | case PREPARE: 84 | notificationProgress = -100; 85 | builder.setContentText("准备下载").setProgress(0, 0, true); 86 | builder.setOngoing(true) 87 | .setSmallIcon(android.R.drawable.stat_sys_download_done); 88 | case PENDING: 89 | notificationProgress = -100; 90 | builder.setContentText("等待下载...").setProgress(0, 0, true); 91 | builder.setOngoing(true) 92 | .setSmallIcon(android.R.drawable.stat_sys_download_done); 93 | break; 94 | case DOWNLOADING: 95 | // 控制刷新Notification频率 96 | if (progress < 100 && (progress - notificationProgress < 2)) { 97 | return; 98 | } 99 | notificationProgress = progress; 100 | builder.setContentText("正在下载...") 101 | .setProgress(100, progress, false); 102 | builder.setOngoing(true) 103 | .setSmallIcon(android.R.drawable.stat_sys_download); 104 | break; 105 | case PAUSE: 106 | builder.setContentText("暂停下载"); 107 | builder.setOngoing(false) 108 | .setSmallIcon(android.R.drawable.stat_sys_download); 109 | break; 110 | case SUCCESS: 111 | // 点击跳转 112 | Intent intent = new Intent(context, getMainActivityClass(context)); 113 | intent.setAction(ACTION_SELECT_NOTIFICATION); 114 | PendingIntent pendingIntent = PendingIntent.getActivity(context, NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT); 115 | builder.setContentIntent(pendingIntent); 116 | 117 | builder.setContentText("下载完成").setProgress(0, 0, false); 118 | builder.setOngoing(false) 119 | .setSmallIcon(android.R.drawable.stat_sys_download_done); 120 | break; 121 | case ERROR: 122 | case ENOSPC: 123 | builder.setContentText("下载失败").setProgress(0, 0, false); 124 | builder.setOngoing(false) 125 | .setSmallIcon(android.R.drawable.stat_sys_download_done); 126 | break; 127 | default: 128 | break; 129 | } 130 | // Show the notification 131 | NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, builder.build()); 132 | } 133 | 134 | public NotificationCompat.Builder getBuilder() { 135 | return builder; 136 | } 137 | 138 | public void cancel() { 139 | if (notificationManager != null) { 140 | notificationManager.cancel(NOTIFICATION_ID); 141 | notificationManager = null; 142 | } 143 | } 144 | 145 | private Class getMainActivityClass(Context context) { 146 | String packageName = context.getPackageName(); 147 | Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName); 148 | String className = launchIntent.getComponent().getClassName(); 149 | try { 150 | return Class.forName(className); 151 | } catch (ClassNotFoundException e) { 152 | e.printStackTrace(); 153 | return null; 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 78 | 79 | 80 | 81 | 82 | 83 | 1629878938285 84 | 88 | 89 | 1630052401707 90 | 95 | 96 | 1630053548666 97 | 102 | 103 | 1630055784654 104 | 109 | 110 | 1656409925087 111 | 116 | 119 | 120 | 123 | 124 | 125 | 126 | 127 | 128 | 130 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:isolate'; 3 | import 'dart:ui'; 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'dart:async'; 7 | 8 | import 'package:m3u8_downloader/m3u8_downloader.dart'; 9 | import 'package:open_file/open_file.dart'; 10 | import 'package:path_provider/path_provider.dart'; 11 | import 'package:permission_handler/permission_handler.dart'; 12 | 13 | void main() => runApp(MyApp()); 14 | 15 | class MyApp extends StatefulWidget { 16 | @override 17 | State createState() => _MyAppState(); 18 | } 19 | 20 | class _MyAppState extends State { 21 | ReceivePort _port = ReceivePort(); 22 | String? _downloadingUrl; 23 | String? _printData; 24 | 25 | // 未加密的url地址(喜羊羊与灰太狼之决战次时代) 26 | String url1 = "https://new.iskcd.com/20220420/XNihn9Om/index.m3u8"; 27 | // 加密的url地址(火影忍者疾风传) 28 | String url2 = "https://v3.dious.cc/20201116/SVGYv7Lo/index.m3u8"; 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | initAsync(); 34 | } 35 | 36 | void initAsync() async { 37 | String saveDir = await _findSavePath(); 38 | M3u8Downloader.initialize( 39 | onSelect: () async { 40 | print('下载成功点击'); 41 | return null; 42 | } 43 | ); 44 | M3u8Downloader.config( 45 | saveDir: saveDir, 46 | threadCount: 2, 47 | convertMp4: true, 48 | debugMode: true 49 | ); 50 | // 注册监听器 51 | IsolateNameServer.registerPortWithName(_port.sendPort, 'downloader_send_port'); 52 | _port.listen((dynamic data) { 53 | // 监听数据请求 54 | setState(() { 55 | _printData = '$data'; 56 | }); 57 | }); 58 | } 59 | 60 | Future _checkPermission() async { 61 | var status = await Permission.storage.status; 62 | if (!status.isGranted) { 63 | status = await Permission.storage.request(); 64 | } 65 | return status.isGranted; 66 | } 67 | 68 | Future _findSavePath() async { 69 | final directory = Platform.isAndroid 70 | ? await getExternalStorageDirectory() 71 | : await getApplicationDocumentsDirectory(); 72 | String saveDir = directory!.path + '/vPlayDownload'; 73 | Directory root = Directory(saveDir); 74 | if (!root.existsSync()) { 75 | await root.create(); 76 | } 77 | print(saveDir); 78 | return saveDir; 79 | } 80 | 81 | @pragma('vm:entry-point') 82 | static progressCallback(dynamic args) { 83 | final SendPort? send = IsolateNameServer.lookupPortByName('downloader_send_port'); 84 | if (send != null) { 85 | args["status"] = 1; 86 | send.send(args); 87 | } 88 | } 89 | @pragma('vm:entry-point') 90 | static successCallback(dynamic args) { 91 | final SendPort? send = IsolateNameServer.lookupPortByName('downloader_send_port'); 92 | if (send != null) { 93 | send.send({ 94 | "status": 2, 95 | "url": args["url"], 96 | "filePath": args["filePath"], 97 | "dir": args["dir"] 98 | }); 99 | } 100 | } 101 | @pragma('vm:entry-point') 102 | static errorCallback(dynamic args) { 103 | final SendPort? send = IsolateNameServer.lookupPortByName('downloader_send_port'); 104 | if (send != null) { 105 | send.send({"status": 3, "url": args["url"]}); 106 | } 107 | } 108 | 109 | @override 110 | Widget build(BuildContext context) { 111 | return MaterialApp( 112 | home: Scaffold( 113 | appBar: AppBar( 114 | title: const Text('Plugin example app'), 115 | ), 116 | body: SizedBox( 117 | width: double.infinity, 118 | child: Column( 119 | children: [ 120 | ElevatedButton( 121 | child: Text("${_downloadingUrl == url1 ? '暂停' : '下载'}未加密m3u8"), 122 | onPressed: () { 123 | if (_downloadingUrl == url1) { 124 | // 暂停 125 | setState(() { 126 | _downloadingUrl = null; 127 | }); 128 | M3u8Downloader.pause(url1); 129 | return; 130 | } 131 | // 下载 132 | _checkPermission().then((hasGranted) async { 133 | if (hasGranted) { 134 | await M3u8Downloader.config( 135 | convertMp4: false, 136 | ); 137 | setState(() { 138 | _downloadingUrl = url1; 139 | }); 140 | M3u8Downloader.download( 141 | url: url1, 142 | name: "下载未加密m3u8", 143 | progressCallback: progressCallback, 144 | successCallback: successCallback, 145 | errorCallback: errorCallback 146 | ); 147 | } 148 | }); 149 | }), 150 | ElevatedButton( 151 | child: Text("${_downloadingUrl == url2 ? '暂停' : '下载'}已加密m3u8"), 152 | onPressed: () { 153 | if (_downloadingUrl == url2) { 154 | // 暂停 155 | setState(() { 156 | _downloadingUrl = null; 157 | }); 158 | M3u8Downloader.pause(url2); 159 | return; 160 | } 161 | // 下载 162 | _checkPermission().then((hasGranted) async { 163 | if (hasGranted) { 164 | await M3u8Downloader.config( 165 | convertMp4: false, 166 | ); 167 | setState(() { 168 | _downloadingUrl = url2; 169 | }); 170 | M3u8Downloader.download( 171 | url: url2, 172 | name: "下载已加密m3u8", 173 | progressCallback: progressCallback, 174 | successCallback: successCallback, 175 | errorCallback: errorCallback 176 | ); 177 | } 178 | }); 179 | }, 180 | ), 181 | ElevatedButton( 182 | child: Text("打开已下载的未加密的文件"), 183 | onPressed: () async { 184 | var res = await M3u8Downloader.getSavePath(url1); 185 | print(res); 186 | File mp4 = File(res['mp4']); 187 | if (mp4.existsSync()) { 188 | OpenFile.open(res['mp4']); 189 | } 190 | }, 191 | ), 192 | ElevatedButton( 193 | child: Text("打开已下载的已加密的文件"), 194 | onPressed: () async { 195 | var res = await M3u8Downloader.getSavePath(url2); 196 | print(res); 197 | File mp4 = File(res['mp4']); 198 | print(mp4); 199 | if (mp4.existsSync()) { 200 | OpenFile.open(res['mp4']); 201 | } 202 | }, 203 | ), 204 | ElevatedButton( 205 | child: Text("清空下载"), 206 | onPressed: () async { 207 | await M3u8Downloader.delete(url1); 208 | await M3u8Downloader.delete(url2); 209 | print("清理完成"); 210 | }, 211 | ), 212 | Text(_printData ?? '') 213 | ], 214 | ), 215 | ), 216 | ), 217 | ); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /android/src/main/java/com/vincent/m3u8Downloader/utils/M3U8Util.java: -------------------------------------------------------------------------------- 1 | package com.vincent.m3u8Downloader.utils; 2 | 3 | import android.annotation.SuppressLint; 4 | 5 | import com.vincent.m3u8Downloader.downloader.M3U8DownloadConfig; 6 | import com.vincent.m3u8Downloader.bean.M3U8; 7 | import com.vincent.m3u8Downloader.bean.M3U8Ts; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.BufferedWriter; 11 | import java.io.File; 12 | import java.io.FileInputStream; 13 | import java.io.FileOutputStream; 14 | import java.io.FileWriter; 15 | import java.io.IOException; 16 | import java.io.InputStreamReader; 17 | import java.net.URL; 18 | 19 | /** 20 | * @Author: Vincent 21 | * @CreateAt: 2021/08/25 21:43 22 | * @Desc: M3U8工具类 23 | */ 24 | public class M3U8Util { 25 | 26 | /** 27 | * 将Url转换为M3U8对象 28 | * @param url url地址 29 | * @return M3U8对象 30 | * @throws IOException IO异常 31 | */ 32 | public static M3U8 parseIndex(String url) throws IOException { 33 | URL baseUrl = new URL(url); 34 | BufferedReader reader = new BufferedReader(new InputStreamReader(baseUrl.openStream())); 35 | 36 | M3U8 ret = new M3U8(); 37 | ret.setBaseUrl(url); 38 | 39 | String line; 40 | float seconds = 0; 41 | while ((line = reader.readLine()) != null) { 42 | if (line.startsWith("#")) { 43 | if (line.startsWith("#EXTINF:")) { 44 | line = line.substring(8); 45 | if (line.endsWith(",")) { 46 | line = line.substring(0, line.length() - 1); 47 | } 48 | seconds = Float.parseFloat(line); 49 | } else if (line.startsWith("#EXT-X-KEY:")) { 50 | line = line.split("#EXT-X-KEY:")[1]; 51 | String[] arr = line.split(","); 52 | for (String s : arr) { 53 | if (s.contains("=")) { 54 | String k = s.split("=")[0]; 55 | String v = s.split("=")[1]; 56 | if (k.equals("URI")) { 57 | // 获取key 58 | v = v.replaceAll("\"", ""); 59 | v = v.replaceAll("'", ""); 60 | BufferedReader keyReader = new BufferedReader(new InputStreamReader(new URL(baseUrl, v).openStream())); 61 | ret.setKey(keyReader.readLine()); 62 | M3U8Log.d("m3u8 key: " + ret.getKey()); 63 | } else if (k.equals("IV")) { 64 | // 获取IV 65 | ret.setIv(v); 66 | M3U8Log.d("m3u8 IV: " + v); 67 | } 68 | } 69 | } 70 | } 71 | continue; 72 | } 73 | if (line.endsWith("m3u8")) { 74 | return parseIndex(new URL(baseUrl, line).toString()); 75 | } 76 | ret.addTs(new M3U8Ts(line, seconds)); 77 | seconds = 0; 78 | } 79 | reader.close(); 80 | 81 | return ret; 82 | } 83 | 84 | 85 | /** 86 | * 清空文件夹 87 | * @param dir 文件夹/文件地址 88 | * @return 删除状态 89 | */ 90 | public static boolean clearDir(File dir) { 91 | if (dir.exists()) { 92 | if (dir.isFile()) { 93 | return dir.delete(); 94 | } else if (dir.isDirectory()) { 95 | File[] files = dir.listFiles(); 96 | if (files != null && files.length > 0) { 97 | for (File file : files) { 98 | clearDir(file); 99 | } 100 | } 101 | return dir.delete(); 102 | } 103 | } 104 | return true; 105 | } 106 | 107 | 108 | private static final float KB = 1024; 109 | private static final float MB = 1024 * KB; 110 | private static final float GB = 1024 * MB; 111 | 112 | /** 113 | * 格式化文件大小 114 | * @param size 文件大小 115 | * @return 格式化字符串 116 | */ 117 | @SuppressLint("DefaultLocale") 118 | public static String formatFileSize(long size){ 119 | if (size >= GB) { 120 | return String.format("%.1f GB", size / GB); 121 | } else if (size >= MB) { 122 | float value = size / MB; 123 | return String.format(value > 100 ? "%.0f MB" : "%.1f MB", value); 124 | } else if (size >= KB) { 125 | float value = size / KB; 126 | return String.format(value > 100 ? "%.0f KB" : "%.1f KB", value); 127 | } else { 128 | return String.format("%d B", size); 129 | } 130 | } 131 | 132 | /** 133 | * 生成本地m3u8索引文件,ts切片和m3u8文件放在相同目录下即可 134 | * @param m3U8 m3u8文件 135 | */ 136 | public static void createLocalM3U8(String fileName, M3U8 m3U8) throws IOException{ 137 | createLocalM3U8(fileName, m3U8, null); 138 | } 139 | 140 | /** 141 | * 生成AES-128加密本地m3u8索引文件,ts切片和m3u8文件放在相同目录下即可 142 | * @param m3U8 m3u8文件 143 | * @param keyPath 加密key 144 | */ 145 | public static void createLocalM3U8(String fileName, M3U8 m3U8, String keyPath) throws IOException{ 146 | M3U8Log.d("createLocalM3U8: " + fileName); 147 | BufferedWriter bfw = new BufferedWriter(new FileWriter(fileName, false)); 148 | bfw.write("#EXTM3U\n"); 149 | bfw.write("#EXT-X-VERSION:3\n"); 150 | bfw.write("#EXT-X-MEDIA-SEQUENCE:0\n"); 151 | bfw.write("#EXT-X-TARGETDURATION:13\n"); 152 | if (keyPath != null) bfw.write("#EXT-X-KEY:METHOD=AES-128,URI=\""+keyPath+"\"\n"); 153 | for (M3U8Ts m3U8Ts : m3U8.getTsList()) { 154 | bfw.write("#EXTINF:" + m3U8Ts.getSeconds()+",\n"); 155 | bfw.write(m3U8Ts.obtainEncodeTsFileName()); 156 | bfw.newLine(); 157 | } 158 | bfw.write("#EXT-X-ENDLIST"); 159 | bfw.flush(); 160 | bfw.close(); 161 | } 162 | 163 | /** 164 | * 获取文件流 165 | * @param fileName 文件名 166 | * @return 文件字节数组 167 | * @throws IOException IO异常 168 | */ 169 | public static byte[] readFile(String fileName) throws IOException{ 170 | File file = new File(fileName); 171 | FileInputStream fis = new FileInputStream(file); 172 | int length = fis.available(); 173 | byte[] buffer = new byte[length]; 174 | fis.read(buffer); 175 | fis.close(); 176 | return buffer; 177 | } 178 | 179 | /** 180 | * 保存文件 181 | * @param bytes 字节数组 182 | * @param fileName 文件名 183 | * @throws IOException IO异常 184 | */ 185 | public static void saveFile(byte[] bytes, String fileName) throws IOException{ 186 | File file = new File(fileName); 187 | FileOutputStream outputStream = new FileOutputStream(file); 188 | outputStream.write(bytes); 189 | outputStream.flush(); 190 | outputStream.close(); 191 | } 192 | 193 | /** 194 | * 保存文件 195 | * @param text 文件内容 196 | * @param fileName 文件名 197 | * @throws IOException IO异常 198 | */ 199 | public static void saveFile(String text, String fileName) throws IOException{ 200 | File file = new File(fileName); 201 | BufferedWriter out = new BufferedWriter(new FileWriter(file)); 202 | out.write(text); 203 | out.flush(); 204 | out.close(); 205 | } 206 | 207 | /** 208 | * 获取url保存的地址 209 | * @param url 请求地址 210 | * @return 地址 211 | */ 212 | public static String getSaveFileDir(String url){ 213 | return M3U8DownloadConfig.getSaveDir() + File.separator + EncryptUtil.md5Encode(url); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /android/src/main/java/com/vincent/m3u8Downloader/downloader/M3U8Downloader.java: -------------------------------------------------------------------------------- 1 | package com.vincent.m3u8Downloader.downloader; 2 | 3 | import android.text.TextUtils; 4 | 5 | import com.vincent.m3u8Downloader.bean.M3U8; 6 | import com.vincent.m3u8Downloader.bean.M3U8Task; 7 | import com.vincent.m3u8Downloader.bean.M3U8TaskState; 8 | import com.vincent.m3u8Downloader.listener.OnM3U8DownloadListener; 9 | import com.vincent.m3u8Downloader.listener.OnTaskDownloadListener; 10 | import com.vincent.m3u8Downloader.utils.M3U8Log; 11 | import com.vincent.m3u8Downloader.utils.M3U8Util; 12 | 13 | import java.io.File; 14 | 15 | /** 16 | * @Author: Vincent 17 | * @CreateAt: 2021/08/25 22:17 18 | * @Desc: M3U8下载器 19 | */ 20 | public class M3U8Downloader { 21 | private static M3U8Downloader instance; 22 | 23 | private final M3U8DownloadTask m3U8DownLoadTask; 24 | private M3U8Task currentM3U8Task; 25 | private OnM3U8DownloadListener onM3U8DownloadListener; 26 | private long currentTime; 27 | 28 | private M3U8Downloader() { 29 | m3U8DownLoadTask = new M3U8DownloadTask(); 30 | } 31 | 32 | public static M3U8Downloader getInstance(){ 33 | if (null == instance) { 34 | instance = new M3U8Downloader(); 35 | } 36 | return instance; 37 | } 38 | 39 | public void setOnM3U8DownloadListener(OnM3U8DownloadListener onM3U8DownloadListener) { 40 | this.onM3U8DownloadListener = onM3U8DownloadListener; 41 | } 42 | 43 | /** 44 | * 防止快速点击引起ThreadPoolExecutor频繁创建销毁引起crash 45 | * @return 是否快速点击 46 | */ 47 | private boolean isQuicklyClick(){ 48 | boolean result = false; 49 | if (System.currentTimeMillis() - currentTime <= 100){ 50 | result = true; 51 | M3U8Log.d("is too quickly click!"); 52 | } 53 | currentTime = System.currentTimeMillis(); 54 | return result; 55 | } 56 | 57 | /** 58 | * 下载m3u8 59 | * @param url m3u8下载地址 60 | */ 61 | public void download(String url) { 62 | if (TextUtils.isEmpty(url) || isQuicklyClick()) return; 63 | 64 | // 暂停之前的 65 | if (currentM3U8Task != null && !currentM3U8Task.getUrl().equals(url)) { 66 | pauseCurrent(); 67 | } 68 | 69 | // 开启新的下载 70 | M3U8Task task = new M3U8Task(url); 71 | // 准备任务 72 | pendingTask(task); 73 | try { 74 | currentM3U8Task = task; 75 | M3U8Log.d("start downloading: " + task.getUrl()); 76 | m3U8DownLoadTask.download(task.getUrl(), onTaskDownloadListener); 77 | } catch (Exception e){ 78 | e.printStackTrace(); 79 | M3U8Log.e("startDownloadTask Error:"+e.getMessage()); 80 | } 81 | } 82 | 83 | /** 84 | * 挂起任务 85 | * @param task 下载任务 86 | */ 87 | private void pendingTask(M3U8Task task){ 88 | task.setState(M3U8TaskState.PENDING); 89 | if (onM3U8DownloadListener != null){ 90 | onM3U8DownloadListener.onDownloadPending(task); 91 | } 92 | } 93 | 94 | /** 95 | * 暂停任务(非当前任务) 96 | */ 97 | public void pause(String url){ 98 | M3U8Log.d("pause download: " + url); 99 | if (currentM3U8Task == null || url == null) return; 100 | if (currentM3U8Task.getUrl().equals(url)) { 101 | pauseCurrent(); 102 | } 103 | 104 | } 105 | 106 | /** 107 | * 暂停当前任务 108 | */ 109 | private void pauseCurrent() { 110 | if (currentM3U8Task == null || currentM3U8Task.getState() != M3U8TaskState.DOWNLOADING) return; 111 | 112 | currentM3U8Task.setState(M3U8TaskState.PAUSE); 113 | if (onM3U8DownloadListener != null) { 114 | onM3U8DownloadListener.onDownloadPause(currentM3U8Task); 115 | } 116 | m3U8DownLoadTask.stop(); 117 | } 118 | 119 | /** 120 | * 删除下载文件。非线程安全 121 | * @param url 下载地址 122 | * @return 删除状态 123 | */ 124 | public boolean delete(final String url){ 125 | if (currentM3U8Task != null && currentM3U8Task.getUrl().equals(url)) { 126 | currentM3U8Task.setState(M3U8TaskState.DEFAULT); 127 | m3U8DownLoadTask.stop(); 128 | if (onM3U8DownloadListener != null) { 129 | onM3U8DownloadListener.onStop(currentM3U8Task); 130 | } 131 | } 132 | String saveDir = M3U8Util.getSaveFileDir(url); 133 | // 删除文件夹 134 | boolean isDelete = M3U8Util.clearDir(new File(saveDir)); 135 | // 删除mp4文件 136 | if (isDelete) { 137 | isDelete = M3U8Util.clearDir(new File(saveDir + ".mp4")); 138 | } 139 | return isDelete; 140 | } 141 | 142 | /** 143 | * 是否正在下载 144 | * @return 运行状态 145 | */ 146 | public boolean isRunning(){ 147 | return m3U8DownLoadTask.isRunning(); 148 | } 149 | 150 | /** 151 | * 下载任务监听器 152 | */ 153 | private final OnTaskDownloadListener onTaskDownloadListener = new OnTaskDownloadListener() { 154 | private long lastLength; 155 | private float downloadProgress; 156 | 157 | @Override 158 | public void onStart() { 159 | currentM3U8Task.setState(M3U8TaskState.PREPARE); 160 | if (onM3U8DownloadListener != null){ 161 | onM3U8DownloadListener.onDownloadPrepare(currentM3U8Task); 162 | } 163 | M3U8Log.d("onDownloadPrepare: "+ currentM3U8Task.getUrl()); 164 | } 165 | 166 | @Override 167 | public void onStartDownload(int totalTs, int curTs) { 168 | M3U8Log.d("onStartDownload: "+totalTs+"|"+curTs); 169 | 170 | currentM3U8Task.setState(M3U8TaskState.DOWNLOADING); 171 | if (totalTs > 0) { 172 | downloadProgress = 1.0f * curTs / totalTs; 173 | } 174 | } 175 | 176 | @Override 177 | public void onDownloadItem(long itemFileSize, int totalTs, int curTs) { 178 | if (!m3U8DownLoadTask.isRunning())return; 179 | M3U8Log.d("onDownloadItem: "+currentM3U8Task.getTotalSize()+"|"+itemFileSize+"|"+totalTs+"|"+curTs); 180 | 181 | if (totalTs > 0) { 182 | downloadProgress = 1.0f * curTs / totalTs; 183 | } 184 | if (onM3U8DownloadListener != null){ 185 | onM3U8DownloadListener.onDownloadItem(currentM3U8Task, itemFileSize, totalTs, curTs); 186 | } 187 | } 188 | 189 | @Override 190 | public void onProgress(long curLength) { 191 | if (curLength - lastLength > 0) { 192 | currentM3U8Task.setProgress(downloadProgress); 193 | currentM3U8Task.setSpeed(curLength - lastLength); 194 | if (onM3U8DownloadListener != null ){ 195 | onM3U8DownloadListener.onDownloadProgress(currentM3U8Task); 196 | } 197 | lastLength = curLength; 198 | } 199 | } 200 | 201 | @Override 202 | public void onConvert() { 203 | M3U8Log.d("onConvert!"); 204 | if (onM3U8DownloadListener != null){ 205 | onM3U8DownloadListener.onConvert(); 206 | } 207 | } 208 | 209 | @Override 210 | public void onSuccess(M3U8 m3U8) { 211 | M3U8Log.d("m3u8 Downloader onSuccess: "+ m3U8); 212 | m3U8DownLoadTask.stop(); 213 | currentM3U8Task.setM3U8(m3U8); 214 | currentM3U8Task.setState(M3U8TaskState.SUCCESS); 215 | if (onM3U8DownloadListener != null) { 216 | onM3U8DownloadListener.onDownloadSuccess(currentM3U8Task); 217 | } 218 | } 219 | 220 | @Override 221 | public void onError(Throwable error) { 222 | error.printStackTrace(); 223 | if (error.getMessage() != null && error.getMessage().contains("ENOSPC")){ 224 | currentM3U8Task.setState(M3U8TaskState.ENOSPC); 225 | }else { 226 | currentM3U8Task.setState(M3U8TaskState.ERROR); 227 | } 228 | if (onM3U8DownloadListener != null) { 229 | onM3U8DownloadListener.onDownloadError(currentM3U8Task, error); 230 | } 231 | M3U8Log.e("onError: " + error.getMessage()); 232 | } 233 | }; 234 | } 235 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.flutter-io.cn" 9 | source: hosted 10 | version: "2.9.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.flutter-io.cn" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.flutter-io.cn" 23 | source: hosted 24 | version: "1.2.1" 25 | clock: 26 | dependency: transitive 27 | description: 28 | name: clock 29 | url: "https://pub.flutter-io.cn" 30 | source: hosted 31 | version: "1.1.1" 32 | collection: 33 | dependency: transitive 34 | description: 35 | name: collection 36 | url: "https://pub.flutter-io.cn" 37 | source: hosted 38 | version: "1.16.0" 39 | cupertino_icons: 40 | dependency: "direct main" 41 | description: 42 | name: cupertino_icons 43 | url: "https://pub.flutter-io.cn" 44 | source: hosted 45 | version: "1.0.4" 46 | fake_async: 47 | dependency: transitive 48 | description: 49 | name: fake_async 50 | url: "https://pub.flutter-io.cn" 51 | source: hosted 52 | version: "1.3.1" 53 | ffi: 54 | dependency: transitive 55 | description: 56 | name: ffi 57 | url: "https://pub.flutter-io.cn" 58 | source: hosted 59 | version: "1.2.1" 60 | file: 61 | dependency: transitive 62 | description: 63 | name: file 64 | url: "https://pub.flutter-io.cn" 65 | source: hosted 66 | version: "6.1.2" 67 | flutter: 68 | dependency: "direct main" 69 | description: flutter 70 | source: sdk 71 | version: "0.0.0" 72 | flutter_test: 73 | dependency: "direct dev" 74 | description: flutter 75 | source: sdk 76 | version: "0.0.0" 77 | m3u8_downloader: 78 | dependency: "direct main" 79 | description: 80 | path: ".." 81 | relative: true 82 | source: path 83 | version: "1.3.1" 84 | matcher: 85 | dependency: transitive 86 | description: 87 | name: matcher 88 | url: "https://pub.flutter-io.cn" 89 | source: hosted 90 | version: "0.12.12" 91 | material_color_utilities: 92 | dependency: transitive 93 | description: 94 | name: material_color_utilities 95 | url: "https://pub.flutter-io.cn" 96 | source: hosted 97 | version: "0.1.5" 98 | meta: 99 | dependency: transitive 100 | description: 101 | name: meta 102 | url: "https://pub.flutter-io.cn" 103 | source: hosted 104 | version: "1.8.0" 105 | open_file: 106 | dependency: "direct main" 107 | description: 108 | name: open_file 109 | url: "https://pub.flutter-io.cn" 110 | source: hosted 111 | version: "3.2.1" 112 | path: 113 | dependency: transitive 114 | description: 115 | name: path 116 | url: "https://pub.flutter-io.cn" 117 | source: hosted 118 | version: "1.8.2" 119 | path_provider: 120 | dependency: "direct main" 121 | description: 122 | name: path_provider 123 | url: "https://pub.flutter-io.cn" 124 | source: hosted 125 | version: "2.0.10" 126 | path_provider_android: 127 | dependency: transitive 128 | description: 129 | name: path_provider_android 130 | url: "https://pub.flutter-io.cn" 131 | source: hosted 132 | version: "2.0.14" 133 | path_provider_ios: 134 | dependency: transitive 135 | description: 136 | name: path_provider_ios 137 | url: "https://pub.flutter-io.cn" 138 | source: hosted 139 | version: "2.0.9" 140 | path_provider_linux: 141 | dependency: transitive 142 | description: 143 | name: path_provider_linux 144 | url: "https://pub.flutter-io.cn" 145 | source: hosted 146 | version: "2.1.6" 147 | path_provider_macos: 148 | dependency: transitive 149 | description: 150 | name: path_provider_macos 151 | url: "https://pub.flutter-io.cn" 152 | source: hosted 153 | version: "2.0.6" 154 | path_provider_platform_interface: 155 | dependency: transitive 156 | description: 157 | name: path_provider_platform_interface 158 | url: "https://pub.flutter-io.cn" 159 | source: hosted 160 | version: "2.0.4" 161 | path_provider_windows: 162 | dependency: transitive 163 | description: 164 | name: path_provider_windows 165 | url: "https://pub.flutter-io.cn" 166 | source: hosted 167 | version: "2.0.6" 168 | permission_handler: 169 | dependency: "direct main" 170 | description: 171 | name: permission_handler 172 | url: "https://pub.flutter-io.cn" 173 | source: hosted 174 | version: "9.2.0" 175 | permission_handler_android: 176 | dependency: transitive 177 | description: 178 | name: permission_handler_android 179 | url: "https://pub.flutter-io.cn" 180 | source: hosted 181 | version: "9.0.2+1" 182 | permission_handler_apple: 183 | dependency: transitive 184 | description: 185 | name: permission_handler_apple 186 | url: "https://pub.flutter-io.cn" 187 | source: hosted 188 | version: "9.0.4" 189 | permission_handler_platform_interface: 190 | dependency: transitive 191 | description: 192 | name: permission_handler_platform_interface 193 | url: "https://pub.flutter-io.cn" 194 | source: hosted 195 | version: "3.7.0" 196 | permission_handler_windows: 197 | dependency: transitive 198 | description: 199 | name: permission_handler_windows 200 | url: "https://pub.flutter-io.cn" 201 | source: hosted 202 | version: "0.1.0" 203 | platform: 204 | dependency: transitive 205 | description: 206 | name: platform 207 | url: "https://pub.flutter-io.cn" 208 | source: hosted 209 | version: "3.1.0" 210 | plugin_platform_interface: 211 | dependency: transitive 212 | description: 213 | name: plugin_platform_interface 214 | url: "https://pub.flutter-io.cn" 215 | source: hosted 216 | version: "2.1.2" 217 | process: 218 | dependency: transitive 219 | description: 220 | name: process 221 | url: "https://pub.flutter-io.cn" 222 | source: hosted 223 | version: "4.2.4" 224 | sky_engine: 225 | dependency: transitive 226 | description: flutter 227 | source: sdk 228 | version: "0.0.99" 229 | source_span: 230 | dependency: transitive 231 | description: 232 | name: source_span 233 | url: "https://pub.flutter-io.cn" 234 | source: hosted 235 | version: "1.9.0" 236 | stack_trace: 237 | dependency: transitive 238 | description: 239 | name: stack_trace 240 | url: "https://pub.flutter-io.cn" 241 | source: hosted 242 | version: "1.10.0" 243 | stream_channel: 244 | dependency: transitive 245 | description: 246 | name: stream_channel 247 | url: "https://pub.flutter-io.cn" 248 | source: hosted 249 | version: "2.1.0" 250 | string_scanner: 251 | dependency: transitive 252 | description: 253 | name: string_scanner 254 | url: "https://pub.flutter-io.cn" 255 | source: hosted 256 | version: "1.1.1" 257 | term_glyph: 258 | dependency: transitive 259 | description: 260 | name: term_glyph 261 | url: "https://pub.flutter-io.cn" 262 | source: hosted 263 | version: "1.2.1" 264 | test_api: 265 | dependency: transitive 266 | description: 267 | name: test_api 268 | url: "https://pub.flutter-io.cn" 269 | source: hosted 270 | version: "0.4.12" 271 | vector_math: 272 | dependency: transitive 273 | description: 274 | name: vector_math 275 | url: "https://pub.flutter-io.cn" 276 | source: hosted 277 | version: "2.1.2" 278 | win32: 279 | dependency: transitive 280 | description: 281 | name: win32 282 | url: "https://pub.flutter-io.cn" 283 | source: hosted 284 | version: "2.6.1" 285 | xdg_directories: 286 | dependency: transitive 287 | description: 288 | name: xdg_directories 289 | url: "https://pub.flutter-io.cn" 290 | source: hosted 291 | version: "0.2.0+1" 292 | sdks: 293 | dart: ">=2.17.0 <3.0.0" 294 | flutter: ">=2.8.1" 295 | -------------------------------------------------------------------------------- /android/src/main/java/com/vincent/m3u8Downloader/M3U8DownloaderPlugin.java: -------------------------------------------------------------------------------- 1 | package com.vincent.m3u8Downloader; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.os.Looper; 7 | 8 | import androidx.annotation.NonNull; 9 | import androidx.core.app.NotificationCompat; 10 | import androidx.core.app.NotificationManagerCompat; 11 | 12 | import com.vincent.m3u8Downloader.bean.M3U8Task; 13 | import com.vincent.m3u8Downloader.bean.M3U8TaskState; 14 | import com.vincent.m3u8Downloader.downloader.M3U8DownloadConfig; 15 | import com.vincent.m3u8Downloader.downloader.M3U8DownloadTask; 16 | import com.vincent.m3u8Downloader.downloader.M3U8Downloader; 17 | import com.vincent.m3u8Downloader.downloader.WeakHandler; 18 | import com.vincent.m3u8Downloader.listener.OnM3U8DownloadListener; 19 | import com.vincent.m3u8Downloader.utils.M3U8Log; 20 | import com.vincent.m3u8Downloader.utils.M3U8Util; 21 | import com.vincent.m3u8Downloader.utils.NotificationUtil; 22 | 23 | import org.json.JSONObject; 24 | 25 | import java.io.File; 26 | import java.util.HashMap; 27 | import java.util.Map; 28 | 29 | import io.flutter.embedding.engine.plugins.FlutterPlugin; 30 | import io.flutter.embedding.engine.plugins.activity.ActivityAware; 31 | import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; 32 | import io.flutter.plugin.common.BinaryMessenger; 33 | import io.flutter.plugin.common.JSONMethodCodec; 34 | import io.flutter.plugin.common.MethodCall; 35 | import io.flutter.plugin.common.MethodChannel; 36 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler; 37 | import io.flutter.plugin.common.MethodChannel.Result; 38 | import io.flutter.plugin.common.PluginRegistry; 39 | import io.flutter.plugin.common.PluginRegistry.Registrar; 40 | 41 | /** M3U8DownloaderPlugin */ 42 | public class M3U8DownloaderPlugin implements FlutterPlugin, MethodCallHandler, PluginRegistry.NewIntentListener, ActivityAware { 43 | private static final String CHANNEL_NAME = "vincent/m3u8_downloader"; 44 | 45 | private MethodChannel channel; 46 | private Context context; 47 | private Activity mainActivity; 48 | private WeakHandler handler; 49 | private final Object initializationLock = new Object(); 50 | private boolean showNotification = true; 51 | private final FlutterBackgroundExecutor backgroundExecutor = new FlutterBackgroundExecutor(); 52 | 53 | private String fileName = ""; 54 | private long progressCallbackHandle = -1; 55 | private long successCallbackHandle = -1; 56 | private long errorCallbackHandle = -1; 57 | 58 | @Override 59 | public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { 60 | onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger()); 61 | } 62 | 63 | private void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger) { 64 | synchronized (initializationLock) { 65 | if (channel != null) { 66 | return; 67 | } 68 | this.context = applicationContext; 69 | handler = new WeakHandler(Looper.getMainLooper()); 70 | 71 | channel = new MethodChannel( messenger, CHANNEL_NAME, JSONMethodCodec.INSTANCE); 72 | channel.setMethodCallHandler(this); 73 | } 74 | } 75 | 76 | @SuppressWarnings("ConstantConditions") 77 | @Override 78 | public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) { 79 | String method = call.method; 80 | switch (method) { 81 | case "initialize": 82 | long callbackHandle = call.argument("handle"); 83 | FlutterBackgroundExecutor.setCallbackDispatcher(context, callbackHandle); 84 | backgroundExecutor.startBackgroundIsolate(context); 85 | 86 | result.success(true); 87 | break; 88 | case "config": 89 | M3U8DownloadConfig config = M3U8DownloadConfig.build(context); 90 | if (call.hasArgument("saveDir") && call.argument("saveDir") != JSONObject.NULL) { 91 | String saveDir = call.argument("saveDir"); 92 | config.setSaveDir(saveDir); 93 | } 94 | if (call.hasArgument("showNotification") && call.argument("showNotification") != JSONObject.NULL) { 95 | boolean show = call.argument("showNotification"); 96 | showNotification = show; 97 | config.setShowNotification(show); 98 | } 99 | if (call.hasArgument("connTimeout") && call.argument("connTimeout") != JSONObject.NULL) { 100 | int connTimeout = call.argument("connTimeout"); 101 | config.setConnTimeout(connTimeout); 102 | } 103 | if (call.hasArgument("readTimeout") && call.argument("readTimeout") != JSONObject.NULL) { 104 | int readTimeout = call.argument("readTimeout"); 105 | config.setReadTimeout(readTimeout); 106 | } 107 | if (call.hasArgument("threadCount") && call.argument("threadCount") != JSONObject.NULL) { 108 | int threadCount = call.argument("threadCount"); 109 | config.setThreadCount(threadCount); 110 | } 111 | if (call.hasArgument("debugMode") && call.argument("debugMode") != JSONObject.NULL) { 112 | boolean debugMode = call.argument("debugMode"); 113 | config.setDebugMode(debugMode); 114 | } 115 | if (call.hasArgument("convertMp4") && call.argument("convertMp4") != JSONObject.NULL) { 116 | boolean convertMp4 = call.argument("convertMp4"); 117 | config.setConvertMp4(convertMp4); 118 | } 119 | result.success(true); 120 | break; 121 | case "download": 122 | if (!call.hasArgument("url")) { 123 | result.error("1", "url必传", ""); 124 | return; 125 | } 126 | if (!call.hasArgument("name")) { 127 | result.error("1", "name必传", ""); 128 | return; 129 | } 130 | showNotification = M3U8DownloadConfig.isShowNotification(); 131 | String url = call.argument("url"); 132 | fileName = call.argument("name"); 133 | NotificationUtil.getInstance().cancel(); 134 | if (showNotification) { 135 | NotificationUtil.getInstance().build(context); 136 | } 137 | progressCallbackHandle = call.hasArgument("progressCallback") && call.argument("progressCallback") != JSONObject.NULL ? (long)call.argument("progressCallback") : -1; 138 | successCallbackHandle = call.hasArgument("successCallback") && call.argument("successCallback") != JSONObject.NULL ? (long)call.argument("successCallback") : -1; 139 | errorCallbackHandle = call.hasArgument("errorCallback") && call.argument("errorCallback") != JSONObject.NULL ? (long)call.argument("errorCallback") : -1; 140 | 141 | M3U8Downloader.getInstance().download(url); 142 | M3U8Downloader.getInstance().setOnM3U8DownloadListener(mDownloadListener); 143 | result.success(null); 144 | break; 145 | case "pause": 146 | if (!call.hasArgument("url")) { 147 | result.error("1", "url必传", ""); 148 | return; 149 | } 150 | String pauseUrl = call.argument("url"); 151 | 152 | M3U8Downloader.getInstance().pause(pauseUrl); 153 | result.success(null); 154 | break; 155 | case "delete": 156 | if (!call.hasArgument("url")) { 157 | result.error("1", "url必传", ""); 158 | return; 159 | } 160 | final String deleteUrl = call.argument("url"); 161 | handler.post(new Runnable() { 162 | @Override 163 | public void run() { 164 | boolean flag = M3U8Downloader.getInstance().delete(deleteUrl); 165 | result.success(flag); 166 | } 167 | }); 168 | break; 169 | case "isRunning": 170 | result.success(M3U8Downloader.getInstance().isRunning()); 171 | break; 172 | case "getSavePath": 173 | if (!call.hasArgument("url")) { 174 | result.error("1", "url必传", ""); 175 | return; 176 | } 177 | String saveUrl = call.argument("url"); 178 | Map res = new HashMap<>(); 179 | res.put("baseDir", M3U8Util.getSaveFileDir(saveUrl)); 180 | res.put("m3u8", M3U8DownloadTask.getM3U8Path(saveUrl)); 181 | res.put("mp4", M3U8DownloadTask.getMp4Path(saveUrl)); 182 | result.success(res); 183 | break; 184 | default: 185 | result.notImplemented(); 186 | break; 187 | } 188 | } 189 | 190 | @Override 191 | public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { 192 | channel.setMethodCallHandler(null); 193 | } 194 | 195 | final OnM3U8DownloadListener mDownloadListener = new OnM3U8DownloadListener() { 196 | @Override 197 | public void onDownloadPrepare(M3U8Task task) { 198 | if (showNotification) { 199 | NotificationUtil.getInstance().updateNotification(fileName, M3U8TaskState.PREPARE, 0); 200 | } 201 | } 202 | 203 | @Override 204 | public void onDownloadPending(M3U8Task task) { 205 | if (showNotification) { 206 | NotificationUtil.getInstance().updateNotification(fileName, M3U8TaskState.PENDING, Math.round(task.getProgress() * 100)); 207 | } 208 | } 209 | 210 | @Override 211 | public void onDownloadProgress(M3U8Task task) { 212 | if (showNotification) { 213 | NotificationUtil.getInstance().updateNotification(fileName, M3U8TaskState.DOWNLOADING, Math.round(task.getProgress() * 100)); 214 | } 215 | if (progressCallbackHandle != -1) { 216 | final Map args = new HashMap<>(); 217 | args.put("url", task.getUrl()); 218 | args.put("state", task.getState()); 219 | args.put("progress", task.getProgress()); 220 | args.put("speed", task.getSpeed()); 221 | args.put("formatSpeed", task.getFormatSpeed()); 222 | args.put("totalSize", task.getTotalSize()); 223 | args.put("currentFormatSize", task.getFormatCurrentSize()); 224 | args.put("totalFormatSize", task.getFormatTotalSize()); 225 | handler.post(new Runnable() { 226 | @Override 227 | public void run() { 228 | backgroundExecutor.executeDartCallbackInBackgroundIsolate(progressCallbackHandle, args); 229 | } 230 | }); 231 | } 232 | } 233 | 234 | @Override 235 | public void onDownloadItem(M3U8Task task, long itemFileSize, int totalTs, int curTs) { 236 | } 237 | 238 | @Override 239 | public void onDownloadSuccess(M3U8Task task) { 240 | if (showNotification) { 241 | NotificationUtil.getInstance().updateNotification(fileName, M3U8TaskState.SUCCESS, 100); 242 | } 243 | String saveDir = M3U8Util.getSaveFileDir(task.getUrl()); 244 | String filePath; 245 | if (task.getM3U8() != null) { 246 | filePath = task.getM3U8().getLocalPath(); 247 | } else { 248 | File mp4File = new File(M3U8DownloadTask.getMp4Path(task.getUrl())); 249 | if (mp4File.exists()) { 250 | filePath = mp4File.getPath(); 251 | } else { 252 | filePath = M3U8DownloadTask.getMp4Path(task.getUrl()); 253 | } 254 | } 255 | final Map args = new HashMap<>(); 256 | args.put("url", task.getUrl()); 257 | args.put("dir", saveDir); 258 | args.put("filePath", filePath); 259 | 260 | //下载成功 261 | if (successCallbackHandle != -1) { 262 | handler.post(new Runnable() { 263 | @Override 264 | public void run() { 265 | backgroundExecutor.executeDartCallbackInBackgroundIsolate(successCallbackHandle, args); 266 | } 267 | }); 268 | } 269 | } 270 | 271 | @Override 272 | public void onDownloadPause(M3U8Task task) { 273 | if (showNotification) { 274 | NotificationUtil.getInstance().updateNotification(fileName, M3U8TaskState.PAUSE, Math.round(task.getProgress() * 100)); 275 | } 276 | } 277 | 278 | @Override 279 | public void onConvert() { 280 | if (showNotification) { 281 | NotificationCompat.Builder builder = NotificationUtil.getInstance().getBuilder(); 282 | if (builder == null) return; 283 | 284 | builder.setContentText("正在转成MP4") 285 | .setProgress(100, 100, true) 286 | .setOngoing(true) 287 | .setSmallIcon(android.R.drawable.stat_sys_download); 288 | NotificationManagerCompat.from(context).notify(NotificationUtil.NOTIFICATION_ID, builder.build()); 289 | } 290 | } 291 | 292 | @Override 293 | public void onDownloadError(M3U8Task task, Throwable error) { 294 | error.printStackTrace(); 295 | if (showNotification) { 296 | NotificationUtil.getInstance().updateNotification(fileName, task.getState(), Math.round(task.getProgress() * 100)); 297 | } 298 | if (errorCallbackHandle != -1) { 299 | final Map args = new HashMap<>(); 300 | args.put("url", task.getUrl()); 301 | handler.post(new Runnable() { 302 | @Override 303 | public void run() { 304 | backgroundExecutor.executeDartCallbackInBackgroundIsolate(errorCallbackHandle, args); 305 | } 306 | }); 307 | } 308 | } 309 | 310 | @Override 311 | public void onStop(M3U8Task task) { 312 | if (showNotification) { 313 | NotificationUtil.getInstance().cancel(); 314 | } 315 | } 316 | }; 317 | 318 | @Override 319 | public boolean onNewIntent(Intent intent) { 320 | if (NotificationUtil.ACTION_SELECT_NOTIFICATION.equals(intent.getAction())) { 321 | M3U8Log.d("selectNotification"); 322 | channel.invokeMethod("selectNotification", null); 323 | if (mainActivity != null) { 324 | mainActivity.setIntent(intent); 325 | } 326 | return true; 327 | } 328 | return false; 329 | } 330 | 331 | @Override 332 | public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { 333 | binding.addOnNewIntentListener(this); 334 | mainActivity = binding.getActivity(); 335 | } 336 | 337 | @Override 338 | public void onDetachedFromActivityForConfigChanges() { 339 | this.mainActivity = null; 340 | } 341 | 342 | @Override 343 | public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { 344 | onAttachedToActivity(binding); 345 | } 346 | 347 | @Override 348 | public void onDetachedFromActivity() { 349 | this.mainActivity = null; 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /android/src/main/java/com/vincent/m3u8Downloader/downloader/M3U8DownloadTask.java: -------------------------------------------------------------------------------- 1 | package com.vincent.m3u8Downloader.downloader; 2 | import android.text.TextUtils; 3 | 4 | import com.vincent.m3u8Downloader.bean.M3U8; 5 | import com.vincent.m3u8Downloader.bean.M3U8Ts; 6 | import com.vincent.m3u8Downloader.listener.OnInfoCallback; 7 | import com.vincent.m3u8Downloader.listener.OnTaskDownloadListener; 8 | import com.vincent.m3u8Downloader.utils.EncryptUtil; 9 | import com.vincent.m3u8Downloader.utils.M3U8Log; 10 | import com.vincent.m3u8Downloader.utils.M3U8Util; 11 | 12 | import java.io.File; 13 | import java.io.FileInputStream; 14 | import java.io.FileNotFoundException; 15 | import java.io.FileOutputStream; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.io.InterruptedIOException; 19 | import java.net.HttpURLConnection; 20 | import java.net.MalformedURLException; 21 | import java.net.URL; 22 | import java.util.Timer; 23 | import java.util.TimerTask; 24 | import java.util.concurrent.CountDownLatch; 25 | import java.util.concurrent.ExecutorService; 26 | import java.util.concurrent.Executors; 27 | import java.util.concurrent.atomic.AtomicInteger; 28 | import java.util.concurrent.atomic.AtomicLong; 29 | 30 | /** 31 | * @Author: Vincent 32 | * @CreateAt: 2021/08/25 22:26 33 | * @Desc: M3U8下载任务 34 | */ 35 | public class M3U8DownloadTask { 36 | 37 | public static final String LOCAL_FILE_NAME = "local.m3u8"; 38 | public static final String M3U8_KEY_NAME = "key.key"; 39 | 40 | // 文件保存地址 41 | private String saveDir; 42 | // 当前M3U8 43 | private M3U8 currentM3U8; 44 | // 线程池 45 | private ExecutorService executor; 46 | // 网速定时任务 47 | private Timer netSpeedTimer; 48 | // 任务是否正在运行 49 | private boolean isRunning = false; 50 | // 当前下载完成的文件个数 51 | private final AtomicInteger curTs = new AtomicInteger(0); 52 | // 当前已经在下完成的大小 53 | private final AtomicLong curLength = new AtomicLong(0); 54 | // 总文件个数 55 | private volatile int totalTs = 0; 56 | // 单个文件的大小 57 | private volatile long itemFileSize = 0; 58 | // 下载任务监听器 59 | private OnTaskDownloadListener onTaskDownloadListener; 60 | int connTimeout; 61 | int readTimeout; 62 | int threadCount; 63 | 64 | public M3U8DownloadTask() { 65 | connTimeout = M3U8DownloadConfig.getConnTimeout(); 66 | readTimeout = M3U8DownloadConfig.getReadTimeout(); 67 | threadCount = M3U8DownloadConfig.getThreadCount(); 68 | } 69 | 70 | public boolean isRunning() { 71 | return isRunning; 72 | } 73 | 74 | /** 75 | * 开始下载 76 | * @param url m3u8下载地址 77 | * @param onTaskDownloadListener 任务下载监听器 78 | */ 79 | public void download(final String url, final OnTaskDownloadListener onTaskDownloadListener) { 80 | this.saveDir = M3U8Util.getSaveFileDir(url); 81 | this.onTaskDownloadListener = onTaskDownloadListener; 82 | onTaskDownloadListener.onStart(); 83 | if (M3U8DownloadConfig.isConvertMp4()) { 84 | File file = new File(saveDir + ".mp4"); 85 | // 已存在MP4文件,则已完成 86 | if (file.exists()) { 87 | if (netSpeedTimer != null) { 88 | netSpeedTimer.cancel(); 89 | } 90 | onTaskDownloadListener.onSuccess(currentM3U8); 91 | return; 92 | } 93 | } 94 | if (!isRunning()) { 95 | // 获取m3u8 96 | getM3U8Info(url, new OnInfoCallback() { 97 | @Override 98 | public void success(M3U8 m3u8) { 99 | start(m3u8); 100 | } 101 | }); 102 | } else { 103 | handlerError(new Throwable("Task running")); 104 | } 105 | } 106 | 107 | /** 108 | * 获取m3u8信息 109 | * @param url m3u8地址 110 | * @param callback 回调函数 111 | */ 112 | private synchronized void getM3U8Info(final String url, final OnInfoCallback callback) { 113 | new Thread(new Runnable() { 114 | @Override 115 | public void run() { 116 | try { 117 | M3U8 m3u8 = M3U8Util.parseIndex(url); 118 | callback.success(m3u8); 119 | } catch (Exception e) { 120 | handlerError(e); 121 | } 122 | } 123 | }).start(); 124 | } 125 | 126 | /** 127 | * 开始下载 128 | */ 129 | private void start(final M3U8 m3u8) { 130 | currentM3U8 = m3u8; 131 | onTaskDownloadListener.onStartDownload(totalTs, curTs.get()); 132 | M3U8Log.d("start download, save dir: " + saveDir); 133 | new Thread() { 134 | @Override 135 | public void run() { 136 | try { 137 | final CountDownLatch latch = new CountDownLatch(m3u8.getTsList().size()); 138 | // 开始下载 139 | batchDownloadTs(m3u8, latch); 140 | 141 | // 等待线程执行完毕 142 | latch.await(); 143 | 144 | // 关闭线程池 145 | if (executor != null) { 146 | executor.shutdown(); 147 | } 148 | if (isRunning) { 149 | currentM3U8.setDirPath(saveDir); 150 | if (M3U8DownloadConfig.isConvertMp4()) { 151 | // 转成mp4 152 | convertMP4(); 153 | } else { 154 | // 否则生成local.m3u8文件 155 | String m3u8Path = saveDir + File.separator + LOCAL_FILE_NAME; 156 | if (TextUtils.isEmpty(currentM3U8.getKey())) { 157 | M3U8Util.createLocalM3U8(m3u8Path, currentM3U8); 158 | } else { 159 | M3U8Util.createLocalM3U8(m3u8Path, currentM3U8, M3U8_KEY_NAME); 160 | } 161 | currentM3U8.setLocalPath(m3u8Path); 162 | } 163 | 164 | if (netSpeedTimer != null) { 165 | netSpeedTimer.cancel(); 166 | } 167 | onTaskDownloadListener.onSuccess(currentM3U8); 168 | isRunning = false; 169 | } 170 | } catch (InterruptedIOException e) { 171 | // 被中断了,使用stop时会抛出这个,不需要处理 172 | } catch (IOException e) { 173 | handlerError(e); 174 | } catch (InterruptedException e) { 175 | handlerError(e); 176 | } catch (Exception e) { 177 | handlerError(e); 178 | } 179 | } 180 | }.start(); 181 | } 182 | 183 | /** 184 | * 批量下载ts切片 185 | * @param m3u8 M3U8对象 186 | * @param latch 锁存器 187 | */ 188 | private void batchDownloadTs(final M3U8 m3u8, final CountDownLatch latch) { 189 | final File dir = new File(saveDir); 190 | if (!dir.exists()) { 191 | dir.mkdirs(); 192 | } 193 | if (!TextUtils.isEmpty(m3u8.getKey())) { 194 | // 保存key文件 195 | try { 196 | M3U8Util.saveFile(m3u8.getKey(), saveDir + File.separator + M3U8_KEY_NAME); 197 | } catch (IOException e) { 198 | handlerError(e); 199 | } 200 | } 201 | totalTs = m3u8.getTsList().size(); 202 | // 重置线程池 203 | if (executor != null) { 204 | executor.shutdownNow(); 205 | M3U8Log.d("executor is shutDown !"); 206 | } 207 | M3U8Log.d("Downloading !"); 208 | executor = Executors.newFixedThreadPool(threadCount); 209 | 210 | curTs.set(0); 211 | curLength.set(0); 212 | isRunning = true; 213 | // 重置网速定时器 214 | if (netSpeedTimer != null) { 215 | netSpeedTimer.cancel(); 216 | } 217 | netSpeedTimer = new Timer(); 218 | netSpeedTimer.schedule(new TimerTask() { 219 | @Override 220 | public void run() { 221 | onTaskDownloadListener.onProgress(curLength.get()); 222 | } 223 | }, 0, 1500); 224 | 225 | final String basePath = m3u8.getBaseUrl(); 226 | for (final M3U8Ts m3u8Ts : m3u8.getTsList()) { 227 | // 每个TS文件下载单独一个线程 228 | executor.execute(new Runnable() { 229 | @Override 230 | public void run() { 231 | File file; 232 | try { 233 | file = new File(dir + File.separator + m3u8Ts.obtainEncodeTsFileName()); 234 | } catch (Exception e) { 235 | file = new File(dir + File.separator + m3u8Ts.getUrl()); 236 | } 237 | 238 | if (!file.exists()) { 239 | FileOutputStream fos = null; 240 | InputStream inputStream = null; 241 | boolean readFinished = false; 242 | try { 243 | URL url = m3u8Ts.obtainFullUrl(basePath); 244 | HttpURLConnection conn = (HttpURLConnection) url.openConnection(); 245 | // conn.addRequestProperty("Referer", "http://xxxxxxxx.com/"); 246 | conn.setConnectTimeout(connTimeout); 247 | conn.setReadTimeout(readTimeout); 248 | if (conn.getResponseCode() == 200) { 249 | inputStream = conn.getInputStream(); 250 | fos = new FileOutputStream(file);//会自动创建文件 251 | int len; 252 | byte[] buf = new byte[1024]; 253 | while ((len = inputStream.read(buf)) != -1) { 254 | fos.write(buf, 0, len);//写入流中 255 | } 256 | } else { 257 | handlerError(new Throwable(String.valueOf(conn.getResponseCode()))); 258 | } 259 | readFinished = true; 260 | } catch (MalformedURLException e) { 261 | handlerError(e); 262 | } catch (IOException e) { 263 | handlerError(e); 264 | } finally { 265 | // 如果没有读取完,则删除 266 | if (!readFinished && file.exists()) { 267 | file.delete(); 268 | } 269 | // 关流 270 | if (inputStream != null) { 271 | try { 272 | inputStream.close(); 273 | } catch (IOException e) { 274 | e.printStackTrace(); 275 | } 276 | } 277 | if (fos != null) { 278 | try { 279 | fos.close(); 280 | } catch (IOException e) { 281 | e.printStackTrace(); 282 | } 283 | } 284 | } 285 | curLength.set(curLength.get() + file.length()); 286 | onTaskDownloadListener.onDownloadItem(itemFileSize, totalTs, curTs.get()); 287 | } 288 | itemFileSize = file.length(); 289 | m3u8Ts.setFileSize(itemFileSize); 290 | curTs.incrementAndGet(); 291 | latch.countDown(); 292 | } 293 | }); 294 | } 295 | } 296 | 297 | /** 298 | * M3U8转MP4 299 | */ 300 | private void convertMP4() { 301 | final File dir = new File(saveDir); 302 | 303 | FileOutputStream fos = null; 304 | InputStream inputStream = null; 305 | String mp4FilePath = saveDir + ".mp4"; 306 | File mp4File = null; 307 | int len; 308 | try { 309 | mp4File = new File(mp4FilePath); 310 | if (mp4File.exists()) { 311 | mp4File.delete(); 312 | } 313 | fos = new FileOutputStream(mp4File); 314 | byte[] bytes = new byte[1024]; 315 | for (final M3U8Ts m3U8Ts : currentM3U8.getTsList()) { 316 | File file; 317 | try { 318 | file = new File(dir + File.separator + m3U8Ts.obtainEncodeTsFileName()); 319 | } catch (Exception e) { 320 | file = new File(dir + File.separator + m3U8Ts.getUrl()); 321 | } 322 | // ts片段不存在,直接跳过 323 | if(!file.exists()) 324 | continue; 325 | inputStream = new FileInputStream(file); 326 | if (!TextUtils.isEmpty(currentM3U8.getKey())) { 327 | int available = inputStream.available(); 328 | if (bytes.length < available) 329 | bytes = new byte[available]; 330 | inputStream.read(bytes); 331 | // 解密,追加到mp4文件中 332 | fos.write(EncryptUtil.decryptTs(bytes, currentM3U8.getKey(), currentM3U8.getIv())); 333 | } else { 334 | // 追加到mp4文件中 335 | while ((len = inputStream.read(bytes)) != -1) { 336 | fos.write(bytes, 0, len); 337 | } 338 | } 339 | // 关闭流 340 | inputStream.close(); 341 | } 342 | // 设置文件路径 343 | currentM3U8.setLocalPath(mp4FilePath); 344 | // 合并成功,删除m3u8和ts文件 345 | M3U8Util.clearDir(dir); 346 | } catch (FileNotFoundException e) { 347 | e.printStackTrace(); 348 | handlerError(e); 349 | } catch (IOException e) { 350 | e.printStackTrace(); 351 | handlerError(e); 352 | } catch (Exception e) { 353 | e.printStackTrace(); 354 | handlerError(e); 355 | } finally { 356 | // 关流 357 | if (inputStream != null) { 358 | try { 359 | inputStream.close(); 360 | } catch (IOException e) { 361 | e.printStackTrace(); 362 | } 363 | } 364 | if (fos != null) { 365 | try { 366 | fos.close(); 367 | } catch (IOException e) { 368 | e.printStackTrace(); 369 | } 370 | } 371 | if (mp4File != null && mp4File.exists() && mp4File.length() == 0) { 372 | mp4File.delete(); 373 | } 374 | } 375 | onTaskDownloadListener.onConvert(); 376 | } 377 | 378 | /** 379 | * 停止任务 380 | */ 381 | public void stop() { 382 | // 停止网速定时器 383 | if (netSpeedTimer != null) { 384 | netSpeedTimer.cancel(); 385 | netSpeedTimer = null; 386 | } 387 | isRunning = false; 388 | // 关闭线程池 389 | if (executor != null) { 390 | executor.shutdownNow(); 391 | } 392 | } 393 | 394 | /** 395 | * 处理异常 396 | * @param e 异常信息 397 | */ 398 | private void handlerError(Throwable e) { 399 | if (!"Task running".equals(e.getMessage())) { 400 | stop(); 401 | } 402 | // 不提示被中断的情况 403 | if ("thread interrupted".equals(e.getMessage())) { 404 | return; 405 | } 406 | e.printStackTrace(); 407 | onTaskDownloadListener.onError(e); 408 | } 409 | 410 | /** 411 | * 获取m3u8本地路径 412 | * @param url m3u8地址 413 | * @return 文件路径 414 | */ 415 | public static String getM3U8Path(String url) { 416 | return M3U8Util.getSaveFileDir(url) + File.separator + LOCAL_FILE_NAME; 417 | } 418 | 419 | /** 420 | * 获取m3u8本地路径 421 | * @param url m3u8地址 422 | * @return 文件路径 423 | */ 424 | public static String getMp4Path(String url) { 425 | return M3U8Util.getSaveFileDir(url) + ".mp4"; 426 | } 427 | 428 | /** 429 | * 获取m3u8本地文件 430 | * @param url m3u8网络地址 431 | * @return m3u8本地文件 432 | */ 433 | public static File getM3U8File(String url) { 434 | try { 435 | return new File(M3U8Util.getSaveFileDir(url), LOCAL_FILE_NAME); 436 | } catch (Exception e){ 437 | M3U8Log.e(e.getMessage()); 438 | } 439 | return null; 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXCopyFilesBuildPhase section */ 19 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 20 | isa = PBXCopyFilesBuildPhase; 21 | buildActionMask = 2147483647; 22 | dstPath = ""; 23 | dstSubfolderSpec = 10; 24 | files = ( 25 | ); 26 | name = "Embed Frameworks"; 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXCopyFilesBuildPhase section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 33 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 34 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 35 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 36 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 38 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 39 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 40 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 42 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 44 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | 9740EEB11CF90186004384FC /* Flutter */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 62 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 63 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 64 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 65 | ); 66 | name = Flutter; 67 | sourceTree = ""; 68 | }; 69 | 97C146E51CF9000F007C117D = { 70 | isa = PBXGroup; 71 | children = ( 72 | 9740EEB11CF90186004384FC /* Flutter */, 73 | 97C146F01CF9000F007C117D /* Runner */, 74 | 97C146EF1CF9000F007C117D /* Products */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 97C146EF1CF9000F007C117D /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 97C146EE1CF9000F007C117D /* Runner.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 97C146F01CF9000F007C117D /* Runner */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 90 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 91 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 92 | 97C147021CF9000F007C117D /* Info.plist */, 93 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 94 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 95 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 96 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 97 | ); 98 | path = Runner; 99 | sourceTree = ""; 100 | }; 101 | /* End PBXGroup section */ 102 | 103 | /* Begin PBXNativeTarget section */ 104 | 97C146ED1CF9000F007C117D /* Runner */ = { 105 | isa = PBXNativeTarget; 106 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 107 | buildPhases = ( 108 | 9740EEB61CF901F6004384FC /* Run Script */, 109 | 97C146EA1CF9000F007C117D /* Sources */, 110 | 97C146EB1CF9000F007C117D /* Frameworks */, 111 | 97C146EC1CF9000F007C117D /* Resources */, 112 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 113 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 114 | ); 115 | buildRules = ( 116 | ); 117 | dependencies = ( 118 | ); 119 | name = Runner; 120 | productName = Runner; 121 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 122 | productType = "com.apple.product-type.application"; 123 | }; 124 | /* End PBXNativeTarget section */ 125 | 126 | /* Begin PBXProject section */ 127 | 97C146E61CF9000F007C117D /* Project object */ = { 128 | isa = PBXProject; 129 | attributes = { 130 | LastUpgradeCheck = 1020; 131 | ORGANIZATIONNAME = ""; 132 | TargetAttributes = { 133 | 97C146ED1CF9000F007C117D = { 134 | CreatedOnToolsVersion = 7.3.1; 135 | LastSwiftMigration = 1100; 136 | }; 137 | }; 138 | }; 139 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 140 | compatibilityVersion = "Xcode 9.3"; 141 | developmentRegion = en; 142 | hasScannedForEncodings = 0; 143 | knownRegions = ( 144 | en, 145 | Base, 146 | ); 147 | mainGroup = 97C146E51CF9000F007C117D; 148 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 149 | projectDirPath = ""; 150 | projectRoot = ""; 151 | targets = ( 152 | 97C146ED1CF9000F007C117D /* Runner */, 153 | ); 154 | }; 155 | /* End PBXProject section */ 156 | 157 | /* Begin PBXResourcesBuildPhase section */ 158 | 97C146EC1CF9000F007C117D /* Resources */ = { 159 | isa = PBXResourcesBuildPhase; 160 | buildActionMask = 2147483647; 161 | files = ( 162 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 163 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 164 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 165 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXResourcesBuildPhase section */ 170 | 171 | /* Begin PBXShellScriptBuildPhase section */ 172 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 173 | isa = PBXShellScriptBuildPhase; 174 | buildActionMask = 2147483647; 175 | files = ( 176 | ); 177 | inputPaths = ( 178 | ); 179 | name = "Thin Binary"; 180 | outputPaths = ( 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | shellPath = /bin/sh; 184 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 185 | }; 186 | 9740EEB61CF901F6004384FC /* Run Script */ = { 187 | isa = PBXShellScriptBuildPhase; 188 | buildActionMask = 2147483647; 189 | files = ( 190 | ); 191 | inputPaths = ( 192 | ); 193 | name = "Run Script"; 194 | outputPaths = ( 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | shellPath = /bin/sh; 198 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 199 | }; 200 | /* End PBXShellScriptBuildPhase section */ 201 | 202 | /* Begin PBXSourcesBuildPhase section */ 203 | 97C146EA1CF9000F007C117D /* Sources */ = { 204 | isa = PBXSourcesBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 208 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 209 | ); 210 | runOnlyForDeploymentPostprocessing = 0; 211 | }; 212 | /* End PBXSourcesBuildPhase section */ 213 | 214 | /* Begin PBXVariantGroup section */ 215 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 216 | isa = PBXVariantGroup; 217 | children = ( 218 | 97C146FB1CF9000F007C117D /* Base */, 219 | ); 220 | name = Main.storyboard; 221 | sourceTree = ""; 222 | }; 223 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 224 | isa = PBXVariantGroup; 225 | children = ( 226 | 97C147001CF9000F007C117D /* Base */, 227 | ); 228 | name = LaunchScreen.storyboard; 229 | sourceTree = ""; 230 | }; 231 | /* End PBXVariantGroup section */ 232 | 233 | /* Begin XCBuildConfiguration section */ 234 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 235 | isa = XCBuildConfiguration; 236 | buildSettings = { 237 | ALWAYS_SEARCH_USER_PATHS = NO; 238 | CLANG_ANALYZER_NONNULL = YES; 239 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 240 | CLANG_CXX_LIBRARY = "libc++"; 241 | CLANG_ENABLE_MODULES = YES; 242 | CLANG_ENABLE_OBJC_ARC = YES; 243 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 244 | CLANG_WARN_BOOL_CONVERSION = YES; 245 | CLANG_WARN_COMMA = YES; 246 | CLANG_WARN_CONSTANT_CONVERSION = YES; 247 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 248 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 249 | CLANG_WARN_EMPTY_BODY = YES; 250 | CLANG_WARN_ENUM_CONVERSION = YES; 251 | CLANG_WARN_INFINITE_RECURSION = YES; 252 | CLANG_WARN_INT_CONVERSION = YES; 253 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 254 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 255 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 256 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 257 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 258 | CLANG_WARN_STRICT_PROTOTYPES = YES; 259 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 260 | CLANG_WARN_UNREACHABLE_CODE = YES; 261 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 262 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 263 | COPY_PHASE_STRIP = NO; 264 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 265 | ENABLE_NS_ASSERTIONS = NO; 266 | ENABLE_STRICT_OBJC_MSGSEND = YES; 267 | GCC_C_LANGUAGE_STANDARD = gnu99; 268 | GCC_NO_COMMON_BLOCKS = YES; 269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 271 | GCC_WARN_UNDECLARED_SELECTOR = YES; 272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 273 | GCC_WARN_UNUSED_FUNCTION = YES; 274 | GCC_WARN_UNUSED_VARIABLE = YES; 275 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 276 | MTL_ENABLE_DEBUG_INFO = NO; 277 | SDKROOT = iphoneos; 278 | SUPPORTED_PLATFORMS = iphoneos; 279 | TARGETED_DEVICE_FAMILY = "1,2"; 280 | VALIDATE_PRODUCT = YES; 281 | }; 282 | name = Profile; 283 | }; 284 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 285 | isa = XCBuildConfiguration; 286 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 287 | buildSettings = { 288 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 289 | CLANG_ENABLE_MODULES = YES; 290 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 291 | ENABLE_BITCODE = NO; 292 | INFOPLIST_FILE = Runner/Info.plist; 293 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 294 | PRODUCT_BUNDLE_IDENTIFIER = com.vincent.m3u8DownloaderExample; 295 | PRODUCT_NAME = "$(TARGET_NAME)"; 296 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 297 | SWIFT_VERSION = 5.0; 298 | VERSIONING_SYSTEM = "apple-generic"; 299 | }; 300 | name = Profile; 301 | }; 302 | 97C147031CF9000F007C117D /* Debug */ = { 303 | isa = XCBuildConfiguration; 304 | buildSettings = { 305 | ALWAYS_SEARCH_USER_PATHS = NO; 306 | CLANG_ANALYZER_NONNULL = YES; 307 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 308 | CLANG_CXX_LIBRARY = "libc++"; 309 | CLANG_ENABLE_MODULES = YES; 310 | CLANG_ENABLE_OBJC_ARC = YES; 311 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 312 | CLANG_WARN_BOOL_CONVERSION = YES; 313 | CLANG_WARN_COMMA = YES; 314 | CLANG_WARN_CONSTANT_CONVERSION = YES; 315 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 316 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 317 | CLANG_WARN_EMPTY_BODY = YES; 318 | CLANG_WARN_ENUM_CONVERSION = YES; 319 | CLANG_WARN_INFINITE_RECURSION = YES; 320 | CLANG_WARN_INT_CONVERSION = YES; 321 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 322 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 323 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 324 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 325 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 326 | CLANG_WARN_STRICT_PROTOTYPES = YES; 327 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 328 | CLANG_WARN_UNREACHABLE_CODE = YES; 329 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 330 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 331 | COPY_PHASE_STRIP = NO; 332 | DEBUG_INFORMATION_FORMAT = dwarf; 333 | ENABLE_STRICT_OBJC_MSGSEND = YES; 334 | ENABLE_TESTABILITY = YES; 335 | GCC_C_LANGUAGE_STANDARD = gnu99; 336 | GCC_DYNAMIC_NO_PIC = NO; 337 | GCC_NO_COMMON_BLOCKS = YES; 338 | GCC_OPTIMIZATION_LEVEL = 0; 339 | GCC_PREPROCESSOR_DEFINITIONS = ( 340 | "DEBUG=1", 341 | "$(inherited)", 342 | ); 343 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 344 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 345 | GCC_WARN_UNDECLARED_SELECTOR = YES; 346 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 347 | GCC_WARN_UNUSED_FUNCTION = YES; 348 | GCC_WARN_UNUSED_VARIABLE = YES; 349 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 350 | MTL_ENABLE_DEBUG_INFO = YES; 351 | ONLY_ACTIVE_ARCH = YES; 352 | SDKROOT = iphoneos; 353 | TARGETED_DEVICE_FAMILY = "1,2"; 354 | }; 355 | name = Debug; 356 | }; 357 | 97C147041CF9000F007C117D /* Release */ = { 358 | isa = XCBuildConfiguration; 359 | buildSettings = { 360 | ALWAYS_SEARCH_USER_PATHS = NO; 361 | CLANG_ANALYZER_NONNULL = YES; 362 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 363 | CLANG_CXX_LIBRARY = "libc++"; 364 | CLANG_ENABLE_MODULES = YES; 365 | CLANG_ENABLE_OBJC_ARC = YES; 366 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 367 | CLANG_WARN_BOOL_CONVERSION = YES; 368 | CLANG_WARN_COMMA = YES; 369 | CLANG_WARN_CONSTANT_CONVERSION = YES; 370 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 371 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 372 | CLANG_WARN_EMPTY_BODY = YES; 373 | CLANG_WARN_ENUM_CONVERSION = YES; 374 | CLANG_WARN_INFINITE_RECURSION = YES; 375 | CLANG_WARN_INT_CONVERSION = YES; 376 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 377 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 378 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 379 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 380 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 381 | CLANG_WARN_STRICT_PROTOTYPES = YES; 382 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 383 | CLANG_WARN_UNREACHABLE_CODE = YES; 384 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 385 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 386 | COPY_PHASE_STRIP = NO; 387 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 388 | ENABLE_NS_ASSERTIONS = NO; 389 | ENABLE_STRICT_OBJC_MSGSEND = YES; 390 | GCC_C_LANGUAGE_STANDARD = gnu99; 391 | GCC_NO_COMMON_BLOCKS = YES; 392 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 393 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 394 | GCC_WARN_UNDECLARED_SELECTOR = YES; 395 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 396 | GCC_WARN_UNUSED_FUNCTION = YES; 397 | GCC_WARN_UNUSED_VARIABLE = YES; 398 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 399 | MTL_ENABLE_DEBUG_INFO = NO; 400 | SDKROOT = iphoneos; 401 | SUPPORTED_PLATFORMS = iphoneos; 402 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 403 | TARGETED_DEVICE_FAMILY = "1,2"; 404 | VALIDATE_PRODUCT = YES; 405 | }; 406 | name = Release; 407 | }; 408 | 97C147061CF9000F007C117D /* Debug */ = { 409 | isa = XCBuildConfiguration; 410 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 411 | buildSettings = { 412 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 413 | CLANG_ENABLE_MODULES = YES; 414 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 415 | ENABLE_BITCODE = NO; 416 | INFOPLIST_FILE = Runner/Info.plist; 417 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 418 | PRODUCT_BUNDLE_IDENTIFIER = com.vincent.m3u8DownloaderExample; 419 | PRODUCT_NAME = "$(TARGET_NAME)"; 420 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 421 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 422 | SWIFT_VERSION = 5.0; 423 | VERSIONING_SYSTEM = "apple-generic"; 424 | }; 425 | name = Debug; 426 | }; 427 | 97C147071CF9000F007C117D /* Release */ = { 428 | isa = XCBuildConfiguration; 429 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 430 | buildSettings = { 431 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 432 | CLANG_ENABLE_MODULES = YES; 433 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 434 | ENABLE_BITCODE = NO; 435 | INFOPLIST_FILE = Runner/Info.plist; 436 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 437 | PRODUCT_BUNDLE_IDENTIFIER = com.vincent.m3u8DownloaderExample; 438 | PRODUCT_NAME = "$(TARGET_NAME)"; 439 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 440 | SWIFT_VERSION = 5.0; 441 | VERSIONING_SYSTEM = "apple-generic"; 442 | }; 443 | name = Release; 444 | }; 445 | /* End XCBuildConfiguration section */ 446 | 447 | /* Begin XCConfigurationList section */ 448 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 449 | isa = XCConfigurationList; 450 | buildConfigurations = ( 451 | 97C147031CF9000F007C117D /* Debug */, 452 | 97C147041CF9000F007C117D /* Release */, 453 | 249021D3217E4FDB00AE95B9 /* Profile */, 454 | ); 455 | defaultConfigurationIsVisible = 0; 456 | defaultConfigurationName = Release; 457 | }; 458 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 459 | isa = XCConfigurationList; 460 | buildConfigurations = ( 461 | 97C147061CF9000F007C117D /* Debug */, 462 | 97C147071CF9000F007C117D /* Release */, 463 | 249021D4217E4FDB00AE95B9 /* Profile */, 464 | ); 465 | defaultConfigurationIsVisible = 0; 466 | defaultConfigurationName = Release; 467 | }; 468 | /* End XCConfigurationList section */ 469 | }; 470 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 471 | } 472 | -------------------------------------------------------------------------------- /android/src/main/java/com/vincent/m3u8Downloader/downloader/WeakHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Badoo Trading Limited 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy 4 | * of this software and associated documentation files (the "Software"), to deal 5 | * in the Software without restriction, including without limitation the rights 6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | * copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in 11 | * all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | * THE SOFTWARE. 20 | * 21 | * Portions of documentation in this code are modifications based on work created and 22 | * shared by Android Open Source Project and used according to terms described in the 23 | * Apache License, Version 2.0 24 | */ 25 | package com.vincent.m3u8Downloader.downloader; 26 | 27 | import android.os.Handler; 28 | import android.os.Looper; 29 | import android.os.Message; 30 | 31 | import androidx.annotation.NonNull; 32 | import androidx.annotation.Nullable; 33 | import androidx.annotation.VisibleForTesting; 34 | 35 | import java.lang.ref.WeakReference; 36 | import java.util.concurrent.locks.Lock; 37 | import java.util.concurrent.locks.ReentrantLock; 38 | 39 | /** 40 | * Memory safer implementation of android.os.Handler 41 | *

42 | * Original implementation of Handlers always keeps hard reference to handler in queue of execution. 43 | * If you create anonymous handler and post delayed message into it, it will keep all parent class 44 | * for that time in memory even if it could be cleaned. 45 | *

46 | * This implementation is trickier, it will keep WeakReferences to runnables and messages, 47 | * and GC could collect them once WeakHandler instance is not referenced any more 48 | *

49 | * 50 | * @see android.os.Handler 51 | * 52 | * Created by Dmytro Voronkevych on 17/06/2014. 53 | */ 54 | public class WeakHandler { 55 | private final Handler.Callback mCallback; // hard reference to Callback. We need to keep callback in memory 56 | private final ExecHandler mExec; 57 | private Lock mLock = new ReentrantLock(); 58 | @SuppressWarnings("ConstantConditions") 59 | @VisibleForTesting 60 | final ChainedRef mRunnables = new ChainedRef(mLock, null); 61 | 62 | /** 63 | * Default constructor associates this handler with the {@link Looper} for the 64 | * current thread. 65 | * 66 | * If this thread does not have a looper, this handler won't be able to receive messages 67 | * so an exception is thrown. 68 | */ 69 | public WeakHandler() { 70 | mCallback = null; 71 | mExec = new ExecHandler(); 72 | } 73 | 74 | /** 75 | * Constructor associates this handler with the {@link Looper} for the 76 | * current thread and takes a callback interface in which you can handle 77 | * messages. 78 | * 79 | * If this thread does not have a looper, this handler won't be able to receive messages 80 | * so an exception is thrown. 81 | * 82 | * @param callback The callback interface in which to handle messages, or null. 83 | */ 84 | public WeakHandler(@Nullable Handler.Callback callback) { 85 | mCallback = callback; // Hard referencing body 86 | mExec = new ExecHandler(new WeakReference<>(callback)); // Weak referencing inside ExecHandler 87 | } 88 | 89 | /** 90 | * Use the provided {@link Looper} instead of the default one. 91 | * 92 | * @param looper The looper, must not be null. 93 | */ 94 | public WeakHandler(@NonNull Looper looper) { 95 | mCallback = null; 96 | mExec = new ExecHandler(looper); 97 | } 98 | 99 | /** 100 | * Use the provided {@link Looper} instead of the default one and take a callback 101 | * interface in which to handle messages. 102 | * 103 | * @param looper The looper, must not be null. 104 | * @param callback The callback interface in which to handle messages, or null. 105 | */ 106 | public WeakHandler(@NonNull Looper looper, @NonNull Handler.Callback callback) { 107 | mCallback = callback; 108 | mExec = new ExecHandler(looper, new WeakReference<>(callback)); 109 | } 110 | 111 | /** 112 | * Causes the Runnable r to be added to the message queue. 113 | * The runnable will be run on the thread to which this handler is 114 | * attached. 115 | * 116 | * @param r The Runnable that will be executed. 117 | * 118 | * @return Returns true if the Runnable was successfully placed in to the 119 | * message queue. Returns false on failure, usually because the 120 | * looper processing the message queue is exiting. 121 | */ 122 | public final boolean post(@NonNull Runnable r) { 123 | return mExec.post(wrapRunnable(r)); 124 | } 125 | 126 | /** 127 | * Causes the Runnable r to be added to the message queue, to be run 128 | * at a specific time given by uptimeMillis. 129 | * The time-base is {@link android.os.SystemClock#uptimeMillis}. 130 | * The runnable will be run on the thread to which this handler is attached. 131 | * 132 | * @param r The Runnable that will be executed. 133 | * @param uptimeMillis The absolute time at which the callback should run, 134 | * using the {@link android.os.SystemClock#uptimeMillis} time-base. 135 | * 136 | * @return Returns true if the Runnable was successfully placed in to the 137 | * message queue. Returns false on failure, usually because the 138 | * looper processing the message queue is exiting. Note that a 139 | * result of true does not mean the Runnable will be processed -- if 140 | * the looper is quit before the delivery time of the message 141 | * occurs then the message will be dropped. 142 | */ 143 | public final boolean postAtTime(@NonNull Runnable r, long uptimeMillis) { 144 | return mExec.postAtTime(wrapRunnable(r), uptimeMillis); 145 | } 146 | 147 | /** 148 | * Causes the Runnable r to be added to the message queue, to be run 149 | * at a specific time given by uptimeMillis. 150 | * The time-base is {@link android.os.SystemClock#uptimeMillis}. 151 | * The runnable will be run on the thread to which this handler is attached. 152 | * 153 | * @param r The Runnable that will be executed. 154 | * @param uptimeMillis The absolute time at which the callback should run, 155 | * using the {@link android.os.SystemClock#uptimeMillis} time-base. 156 | * 157 | * @return Returns true if the Runnable was successfully placed in to the 158 | * message queue. Returns false on failure, usually because the 159 | * looper processing the message queue is exiting. Note that a 160 | * result of true does not mean the Runnable will be processed -- if 161 | * the looper is quit before the delivery time of the message 162 | * occurs then the message will be dropped. 163 | * 164 | * @see android.os.SystemClock#uptimeMillis 165 | */ 166 | public final boolean postAtTime(Runnable r, Object token, long uptimeMillis) { 167 | return mExec.postAtTime(wrapRunnable(r), token, uptimeMillis); 168 | } 169 | 170 | /** 171 | * Causes the Runnable r to be added to the message queue, to be run 172 | * after the specified amount of time elapses. 173 | * The runnable will be run on the thread to which this handler 174 | * is attached. 175 | * 176 | * @param r The Runnable that will be executed. 177 | * @param delayMillis The delay (in milliseconds) until the Runnable 178 | * will be executed. 179 | * 180 | * @return Returns true if the Runnable was successfully placed in to the 181 | * message queue. Returns false on failure, usually because the 182 | * looper processing the message queue is exiting. Note that a 183 | * result of true does not mean the Runnable will be processed -- 184 | * if the looper is quit before the delivery time of the message 185 | * occurs then the message will be dropped. 186 | */ 187 | public final boolean postDelayed(Runnable r, long delayMillis) { 188 | return mExec.postDelayed(wrapRunnable(r), delayMillis); 189 | } 190 | 191 | /** 192 | * Posts a message to an object that implements Runnable. 193 | * Causes the Runnable r to executed on the next iteration through the 194 | * message queue. The runnable will be run on the thread to which this 195 | * handler is attached. 196 | * This method is only for use in very special circumstances -- it 197 | * can easily starve the message queue, cause ordering problems, or have 198 | * other unexpected side-effects. 199 | * 200 | * @param r The Runnable that will be executed. 201 | * 202 | * @return Returns true if the message was successfully placed in to the 203 | * message queue. Returns false on failure, usually because the 204 | * looper processing the message queue is exiting. 205 | */ 206 | public final boolean postAtFrontOfQueue(Runnable r) { 207 | return mExec.postAtFrontOfQueue(wrapRunnable(r)); 208 | } 209 | 210 | /** 211 | * Remove any pending posts of Runnable r that are in the message queue. 212 | */ 213 | public final void removeCallbacks(Runnable r) { 214 | final WeakRunnable runnable = mRunnables.remove(r); 215 | if (runnable != null) { 216 | mExec.removeCallbacks(runnable); 217 | } 218 | } 219 | 220 | /** 221 | * Remove any pending posts of Runnable r with Object 222 | * token that are in the message queue. If token is null, 223 | * all callbacks will be removed. 224 | */ 225 | public final void removeCallbacks(Runnable r, Object token) { 226 | final WeakRunnable runnable = mRunnables.remove(r); 227 | if (runnable != null) { 228 | mExec.removeCallbacks(runnable, token); 229 | } 230 | } 231 | 232 | /** 233 | * Pushes a message onto the end of the message queue after all pending messages 234 | * before the current time. It will be received in callback, 235 | * in the thread attached to this handler. 236 | * 237 | * @return Returns true if the message was successfully placed in to the 238 | * message queue. Returns false on failure, usually because the 239 | * looper processing the message queue is exiting. 240 | */ 241 | public final boolean sendMessage(Message msg) { 242 | return mExec.sendMessage(msg); 243 | } 244 | 245 | /** 246 | * Sends a Message containing only the what value. 247 | * 248 | * @return Returns true if the message was successfully placed in to the 249 | * message queue. Returns false on failure, usually because the 250 | * looper processing the message queue is exiting. 251 | */ 252 | public final boolean sendEmptyMessage(int what) { 253 | return mExec.sendEmptyMessage(what); 254 | } 255 | 256 | /** 257 | * Sends a Message containing only the what value, to be delivered 258 | * after the specified amount of time elapses. 259 | * @see #sendMessageDelayed(android.os.Message, long) 260 | * 261 | * @return Returns true if the message was successfully placed in to the 262 | * message queue. Returns false on failure, usually because the 263 | * looper processing the message queue is exiting. 264 | */ 265 | public final boolean sendEmptyMessageDelayed(int what, long delayMillis) { 266 | return mExec.sendEmptyMessageDelayed(what, delayMillis); 267 | } 268 | 269 | /** 270 | * Sends a Message containing only the what value, to be delivered 271 | * at a specific time. 272 | * @see #sendMessageAtTime(android.os.Message, long) 273 | * 274 | * @return Returns true if the message was successfully placed in to the 275 | * message queue. Returns false on failure, usually because the 276 | * looper processing the message queue is exiting. 277 | */ 278 | public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) { 279 | return mExec.sendEmptyMessageAtTime(what, uptimeMillis); 280 | } 281 | 282 | /** 283 | * Enqueue a message into the message queue after all pending messages 284 | * before (current time + delayMillis). You will receive it in 285 | * callback, in the thread attached to this handler. 286 | * 287 | * @return Returns true if the message was successfully placed in to the 288 | * message queue. Returns false on failure, usually because the 289 | * looper processing the message queue is exiting. Note that a 290 | * result of true does not mean the message will be processed -- if 291 | * the looper is quit before the delivery time of the message 292 | * occurs then the message will be dropped. 293 | */ 294 | public final boolean sendMessageDelayed(Message msg, long delayMillis) { 295 | return mExec.sendMessageDelayed(msg, delayMillis); 296 | } 297 | 298 | /** 299 | * Enqueue a message into the message queue after all pending messages 300 | * before the absolute time (in milliseconds) uptimeMillis. 301 | * The time-base is {@link android.os.SystemClock#uptimeMillis}. 302 | * You will receive it in callback, in the thread attached 303 | * to this handler. 304 | * 305 | * @param uptimeMillis The absolute time at which the message should be 306 | * delivered, using the 307 | * {@link android.os.SystemClock#uptimeMillis} time-base. 308 | * 309 | * @return Returns true if the message was successfully placed in to the 310 | * message queue. Returns false on failure, usually because the 311 | * looper processing the message queue is exiting. Note that a 312 | * result of true does not mean the message will be processed -- if 313 | * the looper is quit before the delivery time of the message 314 | * occurs then the message will be dropped. 315 | */ 316 | public boolean sendMessageAtTime(Message msg, long uptimeMillis) { 317 | return mExec.sendMessageAtTime(msg, uptimeMillis); 318 | } 319 | 320 | /** 321 | * Enqueue a message at the front of the message queue, to be processed on 322 | * the next iteration of the message loop. You will receive it in 323 | * callback, in the thread attached to this handler. 324 | * This method is only for use in very special circumstances -- it 325 | * can easily starve the message queue, cause ordering problems, or have 326 | * other unexpected side-effects. 327 | * 328 | * @return Returns true if the message was successfully placed in to the 329 | * message queue. Returns false on failure, usually because the 330 | * looper processing the message queue is exiting. 331 | */ 332 | public final boolean sendMessageAtFrontOfQueue(Message msg) { 333 | return mExec.sendMessageAtFrontOfQueue(msg); 334 | } 335 | 336 | /** 337 | * Remove any pending posts of messages with code 'what' that are in the 338 | * message queue. 339 | */ 340 | public final void removeMessages(int what) { 341 | mExec.removeMessages(what); 342 | } 343 | 344 | /** 345 | * Remove any pending posts of messages with code 'what' and whose obj is 346 | * 'object' that are in the message queue. If object is null, 347 | * all messages will be removed. 348 | */ 349 | public final void removeMessages(int what, Object object) { 350 | mExec.removeMessages(what, object); 351 | } 352 | 353 | /** 354 | * Remove any pending posts of callbacks and sent messages whose 355 | * obj is token. If token is null, 356 | * all callbacks and messages will be removed. 357 | */ 358 | public final void removeCallbacksAndMessages(Object token) { 359 | mExec.removeCallbacksAndMessages(token); 360 | } 361 | 362 | /** 363 | * Check if there are any pending posts of messages with code 'what' in 364 | * the message queue. 365 | */ 366 | public final boolean hasMessages(int what) { 367 | return mExec.hasMessages(what); 368 | } 369 | 370 | /** 371 | * Check if there are any pending posts of messages with code 'what' and 372 | * whose obj is 'object' in the message queue. 373 | */ 374 | public final boolean hasMessages(int what, Object object) { 375 | return mExec.hasMessages(what, object); 376 | } 377 | 378 | public final Looper getLooper() { 379 | return mExec.getLooper(); 380 | } 381 | 382 | private WeakRunnable wrapRunnable(@NonNull Runnable r) { 383 | //noinspection ConstantConditions 384 | if (r == null) { 385 | throw new NullPointerException("Runnable can't be null"); 386 | } 387 | final ChainedRef hardRef = new ChainedRef(mLock, r); 388 | mRunnables.insertAfter(hardRef); 389 | return hardRef.wrapper; 390 | } 391 | 392 | private static class ExecHandler extends Handler { 393 | private final WeakReference mCallback; 394 | 395 | ExecHandler() { 396 | mCallback = null; 397 | } 398 | 399 | ExecHandler(WeakReference callback) { 400 | mCallback = callback; 401 | } 402 | 403 | ExecHandler(Looper looper) { 404 | super(looper); 405 | mCallback = null; 406 | } 407 | 408 | ExecHandler(Looper looper, WeakReference callback) { 409 | super(looper); 410 | mCallback = callback; 411 | } 412 | 413 | @Override 414 | public void handleMessage(@NonNull Message msg) { 415 | if (mCallback == null) { 416 | return; 417 | } 418 | final Handler.Callback callback = mCallback.get(); 419 | if (callback == null) { // Already disposed 420 | return; 421 | } 422 | callback.handleMessage(msg); 423 | } 424 | } 425 | 426 | static class WeakRunnable implements Runnable { 427 | private final WeakReference mDelegate; 428 | private final WeakReference mReference; 429 | 430 | WeakRunnable(WeakReference delegate, WeakReference reference) { 431 | mDelegate = delegate; 432 | mReference = reference; 433 | } 434 | 435 | @Override 436 | public void run() { 437 | final Runnable delegate = mDelegate.get(); 438 | final ChainedRef reference = mReference.get(); 439 | if (reference != null) { 440 | reference.remove(); 441 | } 442 | if (delegate != null) { 443 | delegate.run(); 444 | } 445 | } 446 | } 447 | 448 | static class ChainedRef { 449 | @Nullable 450 | ChainedRef next; 451 | @Nullable 452 | ChainedRef prev; 453 | @NonNull 454 | final Runnable runnable; 455 | @NonNull 456 | final WeakRunnable wrapper; 457 | 458 | @NonNull 459 | Lock lock; 460 | 461 | public ChainedRef(@NonNull Lock lock, @NonNull Runnable r) { 462 | this.runnable = r; 463 | this.lock = lock; 464 | this.wrapper = new WeakRunnable(new WeakReference<>(r), new WeakReference<>(this)); 465 | } 466 | 467 | public WeakRunnable remove() { 468 | lock.lock(); 469 | try { 470 | if (prev != null) { 471 | prev.next = next; 472 | } 473 | if (next != null) { 474 | next.prev = prev; 475 | } 476 | prev = null; 477 | next = null; 478 | } finally { 479 | lock.unlock(); 480 | } 481 | return wrapper; 482 | } 483 | 484 | public void insertAfter(@NonNull ChainedRef candidate) { 485 | lock.lock(); 486 | try { 487 | if (this.next != null) { 488 | this.next.prev = candidate; 489 | } 490 | 491 | candidate.next = this.next; 492 | this.next = candidate; 493 | candidate.prev = this; 494 | } finally { 495 | lock.unlock(); 496 | } 497 | } 498 | 499 | @Nullable 500 | public WeakRunnable remove(Runnable obj) { 501 | lock.lock(); 502 | try { 503 | ChainedRef curr = this.next; // Skipping head 504 | while (curr != null) { 505 | if (curr.runnable == obj) { // We do comparison exactly how Handler does inside 506 | return curr.remove(); 507 | } 508 | curr = curr.next; 509 | } 510 | } finally { 511 | lock.unlock(); 512 | } 513 | return null; 514 | } 515 | } 516 | } --------------------------------------------------------------------------------