前言
最近在研究视频下载,因此打算一边研究一边记录一下。方便以后使用时查看。
使用到的库有:
permission_handler 11.1.0 :权限请求
flutter_downloader 1.11.5:文件下载器
path_provider 2.1.1:路径处理
视频:
m3u8视频
:https://vip.lz-cdn5.com/20220402/3882_fcc71dbc/1800k/hls/mixed.m3u8
mp4视频
:https://www.runoob.com/try/demo_source/movie.mp4
开发配置Android Studio 2022.3.1
版本 、flutter 3.16.0
、dart 3.2.0
、win 10
这里只考虑安卓不考虑其他系统
实现
permission_handler
单个权限
import 'package:permission_handler/permission_handler.dart';
ElevatedButton(
onPressed: () async {
PermissionStatus status = await Permission.storage.request();
// 除了下面3个常见情况还有:isLimited (表示权限被限制。这种状态适用于一些敏感权限,例如相机或麦克风权限。)、isProvisional(表示权限处于临时授予状态。这种状态适用于一些敏感权限,例如位置权限。)
if (status.isGranted) {
permissionState = '存储权限已被授予';
} else if (status.isDenied) {
permissionState = '存储权限被拒绝';
} else if (status.isPermanentlyDenied) {
permissionState = '存储权限被永久拒绝';
}
setState(() {});
},
child: const Text("获取存储权限")),
ElevatedButton(
onPressed: () {
openAppSettings();
},
child: const Text("手动去设置"))
多权限
import 'package:permission_handler/permission_handler.dart';
void requestMultiplePermissions() async {
Map<Permission, PermissionStatus> statuses = await [
Permission.camera,
Permission.microphone,
Permission.location,
].request();
statuses.forEach((permission, status) {
if (status.isGranted) {
print('${permission.toString()} granted.');
} else if (status.isDenied) {
print('${permission.toString()} denied.');
} else if (status.isPermanentlyDenied) {
print('${permission.toString()} permanently denied.');
}
});
}
配置
项目/android/app/build.gradle
找到android
中的compileSdkVersion
设置为34
常用的权限
在项目/app/src/main/AndroidManifest.xml
添加对应的权限,主要是跟application
标签平级
常用权限如下:
<!-- 电话权限-->
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<!-- 相机权限-->
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<!-- 互联网权限-->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- 联系人权限 group -->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
<!--存储权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- Android 12及更低版本的读取存储权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- Android 13及更新版本的细粒度媒体权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<!-- 相机权限 -->
<uses-permission android:name="android.permission.CAMERA"/>
<!-- 短信权限组 -->
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_WAP_PUSH"/>
<uses-permission android:name="android.permission.RECEIVE_MMS"/>
<!-- 电话权限组 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission android:name="android.permission.ADD_VOICEMAIL"/>
<uses-permission android:name="android.permission.USE_SIP"/>
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
<uses-permission android:name="android.permission.BIND_CALL_REDIRECTION_SERVICE"
tools:ignore="ProtectedPermissions" />
<!-- 日历权限组 -->
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<!-- 位置权限组 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- 麦克风、语音权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- 传感器权限组 -->
<uses-permission android:name="android.permission.BODY_SENSORS" />
<uses-permission android:name="android.permission.BODY_SENSORS_BACKGROUND" />
<!-- “访问媒体位置”组的权限选项 -->
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<!-- “活动识别”组的权限选项 -->
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
<!-- “忽略电池优化”组的权限选项 -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!--“附近设备”组的权限选项 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
<!-- “管理外部存储”组的权限选项 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<!-- “系统警报窗口”组的权限选项 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- “请求安装程序包”组的权限选项 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- “访问通知策略”组的权限选项 -->
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY"/>
<!-- “通知”组的权限选项 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- “报警”组的权限选项 -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
效果图
不同的安卓版本可能会有不同的效果
模拟器
真机,demo app
真机,夸克浏览器下载视频时请求的权限
path_provider
这个之前介绍过了,简单使用见:flutter:文件系统目录、文件读写
创建文件夹
ElevatedButton(
onPressed: () async {
// 获取应用程序目录
Directory appDocumentDirectory =
await getApplicationSupportDirectory();
// 使用路径分隔符设置路径
String path =
"${appDocumentDirectory.path}${Platform.pathSeparator}myappVideoDownload";
// 判断文件夹是否存在,不存在则创建
var dir = Directory(path);
print("路径:$path");
if (!dir.existsSync()) {
await dir.create(recursive: true);
print("创建成功");
}
},
child: const Text("下载视频"))
查看应用程序目录
下面是在Android studio中进行的操作。
1、点击Device Explorer
2、在展开的设备目录中,找到data文件夹下的data文件夹,然后找到应用程序的包名文件夹(例如:com.example.myapp)。
3、在应用程序的包名文件夹中,找到files文件夹,这就是应用程序的文档目录。
注: files目录:这个目录用于存储应用程序在运行时生成或下载的文件,例如用户生成的数据、缓存文件等。这些文件只能被应用程序本身访问,并且可以在运行时进行读取、写入和修改。这个目录的访问权限是私有的,其他应用程序无法直接访问。
flutter_downloader
默认最大并发下载数是2,如果要修改具体见官方文档。
配置app/src/main/AndroidManifest.xml
的application
中添加如下配置
<provider
android:name="vn.hunghd.flutterdownloader.DownloadedFileProvider"
android:authorities="${applicationId}.flutter_downloader.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
在app/src/main/res/xml
目录下(没有的话自己手动创建),创建provider_paths.xml
文件,在文件里添加一下内容:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external-path" path="."/>
<external-cache-path name="external-cache-path" path="."/>
<external-files-path name="external-files-path" path="."/>
<files-path name="files_path" path="."/>
<cache-path name="cache-path" path="."/>
<root-path name="root" path="."/>
</paths>
案例代码
import 'dart:io';
import 'dart:isolate';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
void main() async {
// 初始化下载器
WidgetsFlutterBinding.ensureInitialized();
await FlutterDownloader.initialize(debug: true, ignoreSsl: false);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// 文件下载列表
List fileDownloadList = [];
// 下载进度
double downloadProgress = 0;
// 用于线程通信
final ReceivePort _port = ReceivePort();
@override
void initState() {
super.initState();
// 接收下载线程发来的消息
IsolateNameServer.registerPortWithName(
_port.sendPort, 'downloader_send_port');
_port.listen((dynamic data) {
var id = data[0];
var status = data[1];
int progress = data[2];
// Download progress: 15%,2
print("Download: $id,$status,$progress%");
// 更新进度
setState(() {
downloadProgress = progress.toDouble();
});
});
// 监听下载进度
FlutterDownloader.registerCallback(downloadCallback);
}
@override
dispose() {
// 关闭通信
IsolateNameServer.removePortNameMapping('downloader_send_port');
super.dispose();
}
// 下载回调函数,这个函数必须是静态或者顶级函数,具体见官方文档说明
static void downloadCallback(id, status, progress) {
final SendPort? send =
IsolateNameServer.lookupPortByName('downloader_send_port');
send?.send([id, status, progress]);
}
// 文件下载方法
Future<void> downloadFile(String url, [String? filename]) async {
// 判断是否存在存储权限
var hasStoragePermission = await Permission.storage.isGranted;
if (!hasStoragePermission) {
final status = await Permission.storage.request();
hasStoragePermission = status.isGranted;
}
// 获取应用程序目录
Directory appDocumentDirectory = await getApplicationSupportDirectory();
// 使用路径分隔符设置路径
String path =
"${appDocumentDirectory.path}${Platform.pathSeparator}myappVideoDownload";
// 判断文件夹是否存在,不存在则创建
var dir = Directory(path);
if (!dir.existsSync()) {
await dir.create(recursive: true);
print("创建成功,路径为:$path");
}
if (hasStoragePermission) {
// 创建下载任务
final taskId = await FlutterDownloader.enqueue(
url: url,
headers: {},
// optional: header send with url (auth token etc)
savedDir: path,
saveInPublicStorage: true,
fileName: filename);
// 存储下载的任务
fileDownloadList.add({
'taskId':taskId,
'filename':filename
});
print("下载任务ID是:$taskId");
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () async {
await downloadFile(
"https://scpic.chinaz.net/files/default/imgs/2023-12-01/8b0607ced040f71b_s.jpg",
"aa.jpg");
},
child: const Text("下载图片")),
Text("下载进度:$downloadProgress%"),
LinearProgressIndicator(
value: downloadProgress, //当前进度
minHeight: 20, //最新高度
color: Colors.red //当前进度的颜色
),
ElevatedButton(
onPressed: () {
final taskId = fileDownloadList[0]['taskId'];
FlutterDownloader.open(taskId: taskId);
},
child: const Text("打开下载的文件"))
],
),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
效果图