m3u8是苹果公司推出的视频播放标准,是m3u的一种,只是编码格式采用的是UTF-8。
存储的信息类似于:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:6
#EXTINF:5,
abc_0123.ts
#EXTINF:5,
abc_0124.ts
#EXTINF:5,
abc_0125.ts
#EXT-X-ENDLIST
m3u8准确来说是一种索引文件,使用m3u8文件实际上是通过它来解析对应的放在服务器上的视频网络地址,从而实现在线播放。
在flutter中,要下载m3u8的视频,实际上是要将m3u8中列出的abc_0123.ts
视频文件全部下载下来,再合成一个mp4文件。
需要用到几个插件
flutter_hls_parser: ^2.0.0 // 解析m3u8文件
ffmpeg_kit_flutter: 4.5.1-LTS // 合成mp4文件
image_gallery_saver: ^1.7.1 // 保存到手机相册
具体代码如下
import "dart:io";
import "package:bot_toast/bot_toast.dart";
import "package:dio/dio.dart";
import "package:ffmpeg_kit_flutter/ffmpeg_kit.dart";
import "package:ffmpeg_kit_flutter/ffmpeg_session.dart";
import "package:ffmpeg_kit_flutter/log.dart";
import "package:ffmpeg_kit_flutter/statistics.dart";
import "package:flutter/material.dart";
import "package:flutter_hls_parser/flutter_hls_parser.dart";
import "package:image_gallery_saver/image_gallery_saver.dart";
import "package:path_provider/path_provider.dart";
class DownloadM3U8Util {
DownloadM3U8Util._();
static final DownloadM3U8Util instance = DownloadM3U8Util._();
List<String> downLoadUrl = [];
// 传入m3u8下载地址
Future<void> downloadM3u8(String url) async {
debugPrint("DownloadUtil, _url=$url");
if (downLoadUrl.contains(url)) {
print("已进入下载队列");
return;
}
final String savePath = await _getSavePath(); // 存储路径
downLoadUrl.add(url);
final String host = url.substring(0, url.lastIndexOf("/"));
final String m3u8FileName = url.substring(url.lastIndexOf("/") + 1); // m3u8文件名带后缀. xxx.m3u8
final String m3u8Path = "$savePath/$m3u8FileName"; // m3u8保存绝对路径 /abc/../xxx.m3u8
final String m3u8Name = m3u8FileName.split(".").first; // m3u8文件名不带后缀 xxx
final Response response = await Dio().download(url, m3u8Path);
if (response.statusCode != 200) {
print("下载 m3u8 失败 _url=$url");
downLoadUrl.remove(url);
return;
}
// video文件夹不存在则创建
Directory directory = Directory("$savePath/video/");
if (!directory.existsSync()) {
directory.createSync();
}
HlsPlaylist playList;
try {
playList = await HlsPlaylistParser.create()
.parse(Uri.parse(url), await File(m3u8Path).readAsLines());
} on ParserException catch (e) {
print(e);
}
if (playList is HlsMediaPlaylist) {
// 读取m3u8文件的URL信息
final mediaPlaylistUrls = playList.segments.map((it) => it.url);
for (var value in mediaPlaylistUrls) {
// value oux02488.ts
String tsUrl = "$host/$value";
File file = File("$savePath/$value");
if (!file.existsSync()) {
file.create();
}
print("下载ts地址 url=$tsUrl, file.path=${file.path}");
Response _response = await Dio().download(tsUrl, file.path);
if (_response.statusCode != 200) {
downLoadUrl.remove(url);
return;
}
}
print("已下载完所有ts视频");
String cmd = '-allowed_extensions ALL -i $m3u8Path "$savePath/video/$m3u8Name.mp4"';
///合并ts为mp4
FFmpegKit.executeAsync(cmd, (FFmpegSession session) {
BotToast.showText(text: "下载成功");
downLoadUrl.remove(url);
// 保存到相册
ImageGallerySaver.saveFile("$savePath/video/$m3u8Name.mp4").then((value) {
// 删除临时路径的文件
File file = File("$savePath/video/$m3u8Name.mp4");
file.deleteSync();
});
});
}
}
/// 获取存储路径
static Future<String> _getSavePath() async {
final directory = Platform.isAndroid
? await getExternalStorageDirectory()
: await getApplicationDocumentsDirectory();
return directory.path;
}
}