MusicFree 源码级拆解:插件化音乐播放器如何用一份 JS 文件承载全部音源
【免费下载链接】MusicFree插件化、定制化、无广告的免费音乐播放器项目地址: https://gitcode.com/maotoumao/MusicFree
MusicFree 是一款插件化的免费音乐播放器,播放器本体不带任何音源,搜索、播放、歌词全部由 JS 插件按统一协议注入。当前版本 0.6.2(见 package.json),本文从src/core/pluginManager的源码切入,讲清插件如何被挂载、播放地址如何兜底、歌词如何按优先级回退。
架构鸟瞰:一个只做播放器的壳
整个工程可以拆成"播放器核心 + 插件运行时 + 页面层"三块。关键点在于:core层不直接访问任何第三方音源 API,所有与音源相关的请求都必须经过PluginMethodsWrapper这个包装层,插件只是其中可替换的一环。
| 模块 | 位置 | 职责 |
|---|---|---|
| 插件管理器 | src/core/pluginManager/index.ts | 插件的安装、卸载、排序、启用、懒加载缓存 |
| 插件实例 | src/core/pluginManager/plugin.ts | 挂载插件代码、计算 hash、包装并兜底每个能力方法 |
| 插件元信息 | src/core/pluginManager/meta.ts | 排序、禁用名单、替代插件、用户变量,存于 MMKV |
| 播放队列 | src/core/trackPlayer/index.ts | 队列管理、音源获取失败时的跨源切换 |
| 媒体缓存 | src/core/mediaCache.ts | 按platform@id缓存音源与歌词路径,上限 800 条 |
| 歌词管理 | src/core/lyricManager.ts | 歌词刷新、关联歌词、本地歌词读写 |
| 页面层 | src/pages/ | 搜索、歌单、设置、插件管理界面,只消费上面几层暴露的接口 |
核心链路拆解
插件加载:一个 .js 文件如何变成可调用的能力集
触发点是 App 启动时PluginManager.setup()(index.ts)读取插件目录(路径定义在 pathConst.ts 的pluginPath)下的所有.js文件。关键步骤:
- 逐个
readFile拿到源码字符串,new Plugin(funcCode, path); mountPlugin用Function构造器把源码包进一个 CommonJS 风格的闭包执行(plugin.ts),注入require、module、console、env、URL、process等参数;_require只放行内置包:cheerio、crypto-js、axios、dayjs、big-integer、qs、he、webdav(plugin.ts),插件无需自带依赖;checkValid用compare-versions的satisfies校验appVersion字段,不匹配则抛VersionNotMatch(plugin.ts);- 校验通过后,
hash = sha256(funcCode),状态置为Mounted,hash 即插件唯一 ID(plugin.ts)。
边界与兜底:解析失败进Error态,原因分VersionNotMatch和CannotParse(plugin.ts);开启basic.lazyLoadPlugin后启动不解析源码,元信息写进 MMKV 的plugin.cache,首次调用能力方法时才由ensureMounted真正挂载(index.ts、plugin.ts),另有 10 秒后的异步补挂载兜底(index.ts)。
播放链路:一首歌从点击到出声的音源解析
触发点是TrackPlayer.play()(trackPlayer/index.ts)。它先按basic.playQualityOrder生成音质序列,再对每档音质调用插件的getMediaSource,拿到源后交给react-native-track-player。真正的解析逻辑在PluginMethodsWrapper.getMediaSource(plugin.ts),按顺序过五道关卡:
- 本地文件:媒体项带
localPath且文件存在时,直接返回file://地址,跳过网络(plugin.ts); - 缓存命中:查
MediaCache,仅当插件声明cacheControl === "cache",或"no-cache"且当前离线(Network.isOffline)时才用缓存源(plugin.ts); - 替代插件:meta 里给某插件配置了
alternativePlugin时,改由替代插件解析(plugin.ts,配置存取见 meta.ts); - 插件解析:未实现
getMediaSource的插件直接回退到musicItem.qualities[quality].url ?? musicItem.url(plugin.ts);实现了的则调用它,URL 里若内嵌账号密码会被formatAuthUrl转成Authorization: Basic ...请求头(plugin.ts); - 写回缓存:除
no-store外,把该音质的{headers, userAgent, url}写入MediaCache.source[quality](plugin.ts)。
边界与兜底:插件调用抛错时按 150ms 间隔重试一次,消息为"NOT RETRY"则不重试(plugin.ts);若所有音质都拿不到源且开启了basic.tryChangeSourceWhenPlayFail,getSimilarMusic会用minDistance在其他插件里搜同名歌曲换源,搜索总预算 8 秒(trackPlayer/index.ts);仍失败则触发handlePlayFail,500ms 后自动切下一首(trackPlayer/index.ts)。.m3u8结尾的源会被打上type: "hls"标记交给底层播放器(trackPlayer/index.ts)。
歌词链路:关联、本地、缓存、插件四级回退
触发点是切歌事件CurrentMusicChanged,LyricManager.setup里监听它并调用refreshLyric(lyricManager.ts)。取词主体在PluginMethodsWrapper.getLyric(plugin.ts),优先级从高到低:
- 关联歌词:读媒体扩展属性
associatedLrc,若存在则换成被关联歌曲去取词,这就是 README 里 A→B→C 歌词链的实现,关联动作本身在associateLyric里patchMediaExtra(lyricManager.ts); - 本地手动歌词:按
MD5(platform)/MD5(id).lrc与.tran.lrc(翻译)查文件,路径常量见 pathConst.ts; - 缓存:
MediaCache里的远程歌词文本或已落地的本地歌词文件路径; - 插件拉取:调插件
getLyric,成功后把原文/翻译各写一个nanoid命名的文件到lrcCachePath,并把文件路径回写进缓存(plugin.ts); - 本地文件:走内置
localFilePlugin,先读 MP3 内嵌歌词,再读同名.lrc(plugin.ts)。
渲染侧由 lrcParser.ts 负责:[xx:xx.xx]时间戳与[offset:...]元信息分别用timeReg、metaReg提取(lrcParser.ts),getPosition用lastSearchIndex做双向线性查找定位当前句(lrcParser.ts),原文与翻译按双指针在相同时间轴上对齐合并(lrcParser.ts)。
契约与数据:IPluginDefine 字段速查
插件导出对象必须满足IPluginDefine(src/types/plugin.d.ts)。元信息字段决定插件如何被识别和管控,能力方法全部可选,未实现时包装层会给出空结果兜底。
| 字段 | 类型/取值 | 行为说明 |
|---|---|---|
platform | string | 来源名,必填;媒体项靠它反查插件,缺失则 hash 为空、进入 Error |
appVersion | string | 应用版本范围,satisfies不满足时报VersionNotMatch |
version | string | 安装时用compare-versions与已装同名插件比大小,旧版被拒 |
srcUrl | string | 远程更新地址,"更新全部插件"据此拉新(index.ts) |
primaryKey | string[] | 主键字段,随 media meta 存储,供getMusicInfo回查 |
cacheControl | cache/no-cache/no-store | 分别控制缓存读取、仅离线读、不写缓存,常量见 commonConst.ts |
userVariables | {key, name?, hint?}[] | 声明用户在设置页要填的变量,运行时经env.getUserVariables()读回 |
search/getMediaSource/getLyric | 方法 | 搜索、取播放地址(默认standard音质)、取歌词,签名见 plugin.d.ts |
getAlbumInfo/getMusicSheetInfo/getArtistWorks | 方法 | 专辑、歌单、作者作品,均为分页接口,返回isEnd |
importMusicSheet/importMusicItem | 方法 | 导入歌单 / 单曲,入参是形如 URL 的字符串 |
getTopLists/getTopListDetail/getRecommendSheetTags/getRecommendSheetsByTag/getMusicComments | 方法 | 榜单、推荐歌单、评论,均为可选能力 |
音质键值为IQualityKey = "low" | "standard" | "high" | "super"(src/types/music.d.ts),缓存键统一为platform@id(mediaUtils.ts)。
快速上手:从 clone 到装好第一个插件
- 克隆仓库:
git clone https://gitcode.com/maotoumao/MusicFree,预期得到含android/、src/、package.json的目录; - 安装依赖:Node 需 >= 18(package.json
engines),执行yarn install,预期无报错,生成node_modules; - 启动 Metro:
yarn start,预期看到Waiting on 0.0.0.0:8081; - 编译 Android:另开终端执行
yarn android(等价于react-native run-android),预期模拟器或真机上打开 App; - 安装插件:侧边栏「设置 → 插件设置」,点右下角
+,选「从本地安装插件」挑.js文件,或选「从网络安装插件」填一个 URL;预期出现"插件安装成功"提示,列表里出现新插件(入口代码见 pluginList.tsx); - 验证:在搜索页输入关键词,预期新插件出现在参与搜索的插件列表里,结果带有该插件名标记。
网络安装时若 URL 以.json结尾,会先解析其中的plugins[].url数组逐个安装,否则把输入本身当插件地址(pluginList.tsx)。
设计与坑:源码里能读到的四个决策
- hash 而不是版本号做身份。插件 ID 是
sha256(funcCode)(plugin.ts),安装去重、卸载、替换旧版本全部按 hash 匹配,version只用于"拒绝装旧版"判断(index.ts)。坑:同一插件只要改一个字符 hash 就变,旧文件被删后缓存也随之清理(index.ts),依赖 hash 做跨设备识别不成立。 - 能力方法全部可选,包装层负责空态。
PluginMethodsWrapper里每个方法开头都有ensurePluginIsMounted+ 未实现判断(如 plugin.ts),插件只需实现自己支持的部分。坑:supportedMethods是挂载后按"属性是函数"统计的(plugin.ts),所以懒加载插件在真正挂载前,其supportedMethods来自 MMKV 缓存的快照,若缓存丢失能力列表会缺项。 - 缓存三态而非布尔。
cache/no-cache/no-store把"能否读旧缓存"和"要不要写新缓存"拆开了,离线时no-cache插件也能播放缓存源(plugin.ts)。坑:缓存条目上限 800,满了直接按 key 顺序删一半(mediaCache.ts),不是 LRU,高频播放的插件条目也可能被先删。 - 播放队列里塞假 track 处理循环。
setQueue永远只放两首:真实歌曲 + 一条musicfree://fake-audio的假下一曲(trackPlayer/index.ts 与 L879-L903),监听PlaybackActiveTrackChanged切到假 track 时再真正加载下一首。这样单曲循环/列表循环的控制权留在 JS 层,但代价是队列长度在底层永远为 2,任何直接读getActiveTrack判断下一曲的逻辑都要防住假 track。
延伸阅读
MusicFree 的核心不在某个页面或组件,而在pluginManager这套"协议 + 包装层 + 兜底链":插件只承诺输入输出,分页、缓存、重试、换源、歌词回退全由宿主收口,这也是它能把音源相关风险隔离在插件边界之外的原因。进一步阅读可以从 src/core/pluginManager/ 三个文件开始,再顺着getMediaSource跳到 src/core/trackPlayer/index.ts 看队列侧如何消费结果。项目基于 AGPL 3.0 开源,使用约定与安全提示(含第三方插件需自行鉴别、应用市场同名 App 非本软件)见 readme.md,版本变更记录见 changelog.md,新需求可提 issue 讨论。
【免费下载链接】MusicFree插件化、定制化、无广告的免费音乐播放器项目地址: https://gitcode.com/maotoumao/MusicFree
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考