QYMediaScanner 是专为 Android HiFi 播放设备设计的高性能多线程媒体扫描引擎。用于扫描本地存储、SD 卡、U 盘、移动硬盘或局域网共享目录中的音频文件,自动提取完整歌曲标签与封面,支持流式批量持久化和秒级增量复扫。客户统一使用 com.qytech.mediascanner.QYMediaScanner。
arm64-v8a在模块级 build.gradle.kts 中添加依赖:
dependencies {
implementation("io.github.qytech:qymediascanner:0.0.2-snapshot")
}
这是最推荐的核心用法。扫描引擎通过 onBatch 定期将一组曲目(如 300 首)回调给宿主,宿主可以直接在数据库中以单个事务批量写入。这样既保证了万首曲库扫描时内存零堆积,又保证了界面进度条的流畅更新:
import com.qytech.mediascanner.QYMediaScanner
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class MediaLibraryViewModel : ViewModel() {
// 实例化扫描器:工作线程数推荐配置为 2 ~ 4(平衡性能与发热)
private var scanner: QYMediaScanner? = null
fun startScan(directoryPath: String) {
viewModelScope.launch {
val mediaScanner = QYMediaScanner(maxWorkers = 2)
scanner = mediaScanner
val report = mediaScanner.scanDirectoryStreaming(
directoryPath = directoryPath,
batchSize = 300, // 推荐每批 300 首,与数据库事务深度契合
onDiscovery = { totalFiles ->
// 1. 发现文件总数回调:可用于初始化进度条最大值
println("在目录中发现音频文件: $totalFiles 个")
},
onProgress = { percent, currentFile ->
// 2. 扫描实时进度:百分比 (0~100) 与当前正在处理的文件路径
println("进度: $percent%, 正在处理: $currentFile")
},
onBatch = { batchTracks ->
// 3. 批量音轨分发:将该批次批量写入 Room / 本地数据库
println("收到批次音轨: ${batchTracks.size} 首,执行批量持久化")
myMediaRepository.insertTracksBatch(batchTracks)
}
)
// 4. 扫描完成汇总
println("扫描结束!耗时: ${report.elapsedMs} ms, 成功入库: ${report.scannedTracks} 首")
println("跳过未变动: ${report.unchangedTracks} 首, 失败文件数: ${report.failedFiles}")
}
}
fun stopScan() {
// 用户点击“取消扫描”时,立即原子中断
scanner?.cancel()
}
}
如果只需要快速扫描一个包含几十首歌曲的小文件夹(如单张专辑目录),可使用单次扫描接口:
val (report, trackList) = scanner.scanDirectory(
directoryPath = "/storage/USB_DISK/Jay_Chou_Album",
onProgress = { percent, currentFile ->
println("正在扫描: $percent%")
}
)
println("扫描完成,共找到 ${trackList.size} 首歌曲")
当用户再次点击“扫描”或接入已有磁盘时,可以通过传入已有数据库中的文件修改时间与伴生资源修改时间,引擎会自动比对并直接跳过未变动文件,十万首大曲库也能在数秒内完成复扫:
// 1. 从宿主数据库中查询现有文件的修改时间快照
val fileModTimes: Map<String, Long> = myDatabase.getAllFileModifiedTimes()
val resourceModTimes: Map<String, Long> = myDatabase.getAllResourceModifiedTimes()
// 2. 编码为版本比对快照
val versionBuffer = QYMediaScanner.encodeVersionCache(
versions = fileModTimes,
resourceVersions = resourceModTimes
)
// 3. 传入快照发起增量扫描
val report = scanner.scanDirectoryStreaming(
directoryPath = "/storage/emulated/0/Music",
batchSize = 300,
versionCacheBuffer = versionBuffer,
onBatch = { batchTracks ->
// 每首音轨的 isUnchanged 标识表示该文件是否未变动
// 增量复扫时,只需对发生变化或新增的曲目进行数据库更新
val changedTracks = batchTracks.filterNot { it.isUnchanged }
if (changedTracks.isNotEmpty()) {
myMediaRepository.insertTracksBatch(changedTracks)
}
}
)
| 方法 | 类型 | 说明 |
|---|---|---|
scanDirectoryStreaming(...) |
挂起函数 | 流式批量扫描指定目录,支持背压与增量比对快照,返回 MediaScanReport |
scanDirectory(directoryPath, onProgress) |
挂起函数 | 单次全量扫描指定目录,返回 (MediaScanReport, List<CanonicalScannedTrack>) |
cancel() |
普通方法 | 立即原子取消当前正在执行的扫描任务 |
encodeVersionCache(versions, resourceVersions) |
伴生工具 | 将文件修改时间字典编码为高效版本快照,用于增量复扫 |
CanonicalScannedTrack(扫描音轨实体)| 字段 | 类型 | 说明 |
|---|---|---|
sourcePath |
String |
音频文件在磁盘上的物理绝对路径 |
uri |
String |
文件的 URI 形式 |
title |
String |
歌曲标题(音频标签为空时自动回退为文件名) |
artist |
String |
艺术家名称 |
album |
String |
专辑名称 |
albumArtist |
String |
专辑艺术家 |
genre |
String |
音乐流派(如 Pop, Classical, Rock) |
year |
Int? |
发行年份 |
trackNumber |
Int? |
音轨序号(如 1, 2, 3) |
discNumber |
Int? |
盘片序号(如 1, 2) |
durationMs |
Long |
歌曲时长(毫秒) |
sampleRate |
Int |
采样率(Hz),如 44100, 192000 |
bitDepth |
Int |
采样位深(bit),如 16, 24 |
channels |
Int |
声道数(1 为单声道,2 为双声道) |
bitrateKbps |
Long |
比特率(kbps) |
format |
String |
格式名称(如 FLAC, DSD, WAV, MP3) |
isDsd |
Boolean |
是否为 DSD 编码音频 |
isCueTrack |
Boolean |
是否由 CUE 分轨表生成的独立音轨 |
cuesheetPath |
String? |
关联的 CUE 文件路径 |
lyrics |
String? |
内嵌歌词或同目录伴生歌词 |
embeddedPicturePath |
String? |
内嵌封面提取保存的本地图片路径 |
externalPicturePath |
String? |
同级目录中发现的封面图片路径(如 cover.jpg, folder.jpg) |
isUnchanged |
Boolean |
增量扫描中文件是否未发生修改 |
MediaScanReport(扫描结果报告)| 字段 | 类型 | 说明 |
|---|---|---|
totalFiles |
Int |
发现的音频文件总数 |
scannedTracks |
Int |
成功解析并输出的有效音轨总数 |
unchangedTracks |
Int |
命中增量缓存跳过重复解析的音轨数 |
failedFiles |
Int |
解析失败或损坏的文件总数 |
traversalFailures |
Int |
因权限不足或磁盘 I/O 错误无法访问的目录/文件数 |
probeFailedFiles |
Int |
物理文件可访问但音频元数据损坏的文件数 |
elapsedMs |
Long |
扫描总耗时(毫秒) |
cancelled |
Boolean |
用户是否主动取消了扫描 |
complete |
Boolean |
目录遍历是否完整完成(无意外中断) |
maxWorkers):
2 ~ 4。在 4 核设备实测中,4 线程可获得 2.3× 综合加速,总扫描耗时大幅缩减,且设备平均 CPU 占用率仅约 26%,温升仅 2.7 °C,处于极佳的能效与温控平衡状态。2,兼顾扫描速度与续航发热。batchSize):
200 ~ 500 首/批(默认 300 首最佳),配合 Room 的 @Transaction 批量写入,可彻底杜绝频繁的小事务导致的主线程 UI 卡顿。