先来说说布景吧。前段时刻,我用Flutter
断断续续的开发了一款桌面版的网易云音乐,制品仍是比较满意的。已完成的功能包含推荐音乐、私家FM、我喜欢的音乐、我的保藏、歌曲谈论、我的下载等。当然了,有关音乐的中心功能播映下载也都完成了。
前文引导:
根据Flutter开发的桌面版网易云音乐(一)
根据Flutter开发的桌面版网易云音乐(二)
一、MP3+JSON
问题就出在下载这儿。当时关于音频文件下载的完成方法是经过MP3+JSON
的方法完成的。有人就要问了,为什么要这么完成?一起下载一个JSON文件又是什么鬼?别急,且听我渐渐道来。
首要,我开发的DreamMusic项目播映音乐时运用的是外链,它长这样"https://music.163.com/song/media/outer/url?id=$songId.mp3"
。至于为何不用歌曲概况中的链接而直接运用外链,由于不是本文要点,我就不解释了。咱们仍是回到播映和下载上。经过这个音乐URL,咱们能够播映在线音乐,当然也能够经过这个URL直接下载MP3文件。它是一个不包含任何媒体信息的原始音频文件(这儿的媒体信息指的是歌名,歌手,专辑信息等)。
现在咱们来想一下,在做音乐下载模块时需求做什么?
- 下载中进度展现。
- 下载完展现在列表上。
- 运用启动时能读取到音乐信息并展现出来。
其间1和2都是在运转中进行的,因此,音乐信息是能够直接从操作的音乐模型中获取到。那么,3应该怎么完成。我上面有说到,从URL中下载的音频文件是一个原始的MP3文件,不包含其他任何媒体信息。假如要完成运用启动时展现已下载音乐列表,并能够展现出歌曲封面,歌名,歌手,专辑等信息,那么咱们在下载的时分还需求一起存取一份音乐的媒体信息才行。因此才会有我第一版中说到的MP3+JSON
方法。
上图中,那一串数字是云音乐平台的歌曲ID,其间的json文件就存储了歌曲的根本信息。用于在运用启动时加载歌曲信息。流程便是先找到JSON文件,读取JSON信息,转成已下载音乐的模型。写成代码便是下面这样。
String name =
directory.uri.pathSegments[directory.uri.pathSegments.length - 2];
final jsonFile = File("${directory.path}/$name.json");
final exist = await jsonFile.exists();
if (exist) {
final content = await jsonFile.readAsString();
final data = json.decode(content);
if (data is Map<String, dynamic>) {
final song = DownloadSongModel.fromJson(data);
return song;
}
} else {
debugPrint("[download]音乐[$name]json文件没有找到,删掉对应文件夹内容");
await directory.delete(recursive: true);
}
那么,这样写会有什么问题?
一个显而易见的问题便是音频文件和媒体信息分离了,不便办理。这儿或许有人就要说了,你这不是P话吗,媒体信息不分敞开莫非放MP3里?诶~还真能够,那便是运用ID3,这个我后面会说到。咱们仍是持续说MP3+JSON
这种方法。还有没有其他问题?有的,比如用户能够随意独自删去JSON或MP3,或打开JSON文件,修正其间的信息,导致音乐和媒体信息不一致。其间随意修正JSON信息真是致命的。
那么我在之前是怎么处理上述问题的呢。咱们接着往下看。
针对删去文件
场景如下,用户打开着运用,然后直接操作下载文件夹,独自删去了JSON,或MP3,或整个DreamMusic目录。假如咱们要做同步,那就需求监听这些文件的变动。Flutter
文件体系为咱们提供了这个方法:
Stream<FileSystemEvent> watch(
{int events = FileSystemEvent.all, bool recursive = false})
这个方法会监听文件的事情FileSystemEvent
,并经过回调的方法告知咱们。所以,咱们能够很容易的写出下列代码,加入文件/文件夹删去监听。
/// 监听下载目录的改变,主要看文件有没有减少
void _addFileDeleteObserverIfNeeded() async {
if (!FileSystemEntity.isWatchSupported) {
return;
}
if (hasDirectoryObserver) {
return;
}
hasDirectoryObserver = true;
final directory = Directory(fileCacheDirectorPath);
if (!directory.existsSync()) {
await directory.create();
}
final stream =
directory.watch(events: FileSystemEvent.delete, recursive: true);
stream.listen((event) async {
// debugPrint("[download]$event");
String path = event.path;
if (path == fileCacheDirectorPath) {
// 删去了整个下载目录
_downloadedSongModels.clear();
hasDirectoryObserver = false;
debugPrint("[download]删去整个下载目录");
} else {
// 删去其间某个文件,这会导致信息不完整,因此直接悉数删去即可
final lastSegment = Uri(path: path).pathSegments.last;
final fileName = lastSegment.split('.').first;
final songId = int.tryParse(fileName);
if (songId != null) {
final path = "$fileCacheDirectorPath/$songId";
final dir = Directory(path);
final exist = await dir.exists();
if (exist) {
await dir.delete(recursive: true);
}
final key = SongDownloadTask.createTaskId(songId);
_downloadedSongModels.remove(key);
}
debugPrint("[download]删去文件$lastSegment,songId-$songId");
}
notifyListeners();
});
debugPrint("[download]开端监听$fileCacheDirectorPath目录的改变");
}
逻辑处理很简略,假如用户单单删去了JSON或MP3,这就导致下载的音频文件不完整,所以,直接删去整个音乐文件夹就好**(这儿指的是上面说到的那一串歌曲ID的文件夹,不是最外层的DreamMusic目录)**。假如用户是删去了整个DreamMusic下载目录,那么不多说,悉数删去。
针对修正文件
很抱愧,我没做这个处理。因为我懒。其实是想出了更好的方法。那便是MP3+ID3
的方法。
当然,我仍是能够提供下思路。原理仍是运用上述说到的监听文件修正的方法,这儿咱们监听修正JSON文件,一单文件的内容经过修正,体系会回调一个FileSystemModifyEvent
对象给咱们,里边有个特点叫contentChanged
,咱们判别下内容是否真的变了,变了就删掉,谁让你乱改下载文件的。当然最主要的原因是,文件体系没告知我改了什么,改变前和改变后又是什么,实在不好判别呀~
二、MP3+ID3
所以,我就抛弃持续在JSON
上转牛角尖的想法,转而考虑是否能够将媒体信息放入到音频文件内部。所以,顺理成章的了解到了ID3
(真的是问题不可怕,它是行进的动力)。
ID3维基百科。不了解ID3
的能够先看看这是何物,有何效果。简略来说,ID3便是存在于音频文件中用于存放媒体信息的一段内容。它有自己的格局,现在流行的是ID3v2.3
和ID3v2.4
版别。
了解了ID3
根本的信息后,我就去找对应的三方库呀,看看有没有现成的能够协助我处理问题的id3解析库存在。很快的,我就找到了一个排名靠前的ID3库完成,id3。然后,我又别离实验了下自己下载的mp3文件和Mac版网易云音乐下载的mp3文件,里边都有些什么。发现公然有些东西。
下面是网易云下载的歌曲读取出来的ID3信息:
{
Version: v2.3.0,
Settings: Lavf57.25.100,
TPOS: 1,
Track: 12, Artist: 大壮,
APIC: {mime: image/jpg, textEncoding: 0, picType: Other, description: , base64: iVBORw0K...},
Title: 为你我受冷风吹,
Album: 大壮首张限量定制翻唱
}
而我自己下载的歌曲文件的ID3中没有任何媒体信息:
{
Version: v2.3.0,
Settings: Lavf57.71.100
}
其实,在Mac上,咱们平时方便预览MP3文件时也会出现一些媒体信息,而这些信息便是mac桌面体系经过读取ID3
显示出来的。
这下,咱们总算知道要将媒体信息存到哪里去了。那便是ID3
中。可问题来了,id3
这个三方库它不支撑修改啊。先不说它有没有bug,它不支撑修改啊。
所以,我检查了ID3有关v1,v2的一切版别的官方信息。又在网上看了不少前辈的文章讲解,心里有了明悟,我为什么不自己写呢?
所以前后经历一个月时刻,一个支撑ID3
解码和编码的id3_codec总算完成了。而且还支撑ID3一切版别(编码这块v2.2不做支撑,因为根本没人用)。
有关id3_codec完成能够看我下列文章:
- Flutter ID3解码完成- v1、v1.1、v2.2、v2.3
- Flutter ID3解码完成-v2.4
- Flutter下 ID3 编码完成-超详细
有了ID3
的支撑,咱们就能够将媒体信息存入MP3中了,这样就处理了上面一切的问题。再也不忧虑用户乱改了(当然,假如有用户用编码器修正ID3信息,那我服了)。
咱们只需求将写入JSON的逻辑改成写入MP3原文件即可。看代码:
/// 将歌曲信息写入
void _writeSongInfoAsync(DownloadSongModel song) async {
if (_cacheMode == DownloadCacheMode.json) {
// 略
} else if (_cacheMode == DownloadCacheMode.id3) {
final path = _generateSongId3SavePath(song.name);
final file = File(path);
bool exist = await file.exists();
if (exist) {
final bytes = await file.readAsBytes();
final encoder = ID3Encoder(bytes);
final al = json.encode(song.al.toJson());
final resultBytes = encoder.encodeSync(MetadataV2_3Body(
title: song.name,
artist: song.authorNmae,
album: song.al.name,
userDefines: {
"duration": song.time.toString(),
"songId": song.songId.toString(),
"ar": json.encode(song.ar.map((e) => e.toJson()).toList()),
"al": al,
}));
file.writeAsBytes(resultBytes, mode: FileMode.write);
debugPrint("[download]finish encode id3 info: ${song.name}");
}
}
}
我下载了一首歌“是你”,桌面预览能直接看到歌曲标题等信息。假如要看详细点,咱们能够直接经过id3_codec的ID3Decoder
,当然还能够运用其他东西,这儿我运用一款叫MediaInfo的东西,还看到了咱们自定义存储的ar
、al
、duration
和songId
信息。
其实剩下的就没有啥悬念了。咱们经过id3_codec的ID3Decoder
读取对应的信息,组装成模型展现出来即可。代码都在项目的download_manager.dart
的_loadSongModelFromPath
下,感兴趣的自行取检查。
总结
本文主要讲解了音乐下载中存储的方法和期间遇到的问题,以及最后的处理方法。也简略介绍了ID3
,它的运用,以及相应的编解码库id3_codec。本文涉及到的项目地址点我检查DreamMusic,感谢支撑。