☰
MusicFree 源码级拆解:插件化音乐播放器如何用一份 JS 文件承载全部音源
2026/10/29 10:52:55 网站建设 项目流程

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文件。关键步骤:

  1. 逐个readFile拿到源码字符串,new Plugin(funcCode, path);
  2. mountPlugin用Function构造器把源码包进一个 CommonJS 风格的闭包执行(plugin.ts),注入require、module、console、env、URL、process等参数;
  3. _require只放行内置包:cheerio、crypto-js、axios、dayjs、big-integer、qs、he、webdav(plugin.ts),插件无需自带依赖;
  4. checkValid用compare-versions的satisfies校验appVersion字段,不匹配则抛VersionNotMatch(plugin.ts);
  5. 校验通过后,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),按顺序过五道关卡:

  1. 本地文件:媒体项带localPath且文件存在时,直接返回file://地址,跳过网络(plugin.ts);
  2. 缓存命中:查MediaCache,仅当插件声明cacheControl === "cache",或"no-cache"且当前离线(Network.isOffline)时才用缓存源(plugin.ts);
  3. 替代插件:meta 里给某插件配置了alternativePlugin时,改由替代插件解析(plugin.ts,配置存取见 meta.ts);
  4. 插件解析:未实现getMediaSource的插件直接回退到musicItem.qualities[quality].url ?? musicItem.url(plugin.ts);实现了的则调用它,URL 里若内嵌账号密码会被formatAuthUrl转成Authorization: Basic ...请求头(plugin.ts);
  5. 写回缓存:除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),优先级从高到低:

  1. 关联歌词:读媒体扩展属性associatedLrc,若存在则换成被关联歌曲去取词,这就是 README 里 A→B→C 歌词链的实现,关联动作本身在associateLyric里patchMediaExtra(lyricManager.ts);
  2. 本地手动歌词:按MD5(platform)/MD5(id).lrc与.tran.lrc(翻译)查文件,路径常量见 pathConst.ts;
  3. 缓存:MediaCache里的远程歌词文本或已落地的本地歌词文件路径;
  4. 插件拉取:调插件getLyric,成功后把原文/翻译各写一个nanoid命名的文件到lrcCachePath,并把文件路径回写进缓存(plugin.ts);
  5. 本地文件:走内置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)。元信息字段决定插件如何被识别和管控,能力方法全部可选,未实现时包装层会给出空结果兜底。

字段类型/取值行为说明
platformstring来源名,必填;媒体项靠它反查插件,缺失则 hash 为空、进入 Error
appVersionstring应用版本范围,satisfies不满足时报VersionNotMatch
versionstring安装时用compare-versions与已装同名插件比大小,旧版被拒
srcUrlstring远程更新地址,"更新全部插件"据此拉新(index.ts)
primaryKeystring[]主键字段,随 media meta 存储,供getMusicInfo回查
cacheControlcache/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 到装好第一个插件

  1. 克隆仓库:git clone https://gitcode.com/maotoumao/MusicFree,预期得到含android/、src/、package.json的目录;
  2. 安装依赖:Node 需 >= 18(package.jsonengines),执行yarn install,预期无报错,生成node_modules;
  3. 启动 Metro:yarn start,预期看到Waiting on 0.0.0.0:8081;
  4. 编译 Android:另开终端执行yarn android(等价于react-native run-android),预期模拟器或真机上打开 App;
  5. 安装插件:侧边栏「设置 → 插件设置」,点右下角+,选「从本地安装插件」挑.js文件,或选「从网络安装插件」填一个 URL;预期出现"插件安装成功"提示,列表里出现新插件(入口代码见 pluginList.tsx);
  6. 验证:在搜索页输入关键词,预期新插件出现在参与搜索的插件列表里,结果带有该插件名标记。

网络安装时若 URL 以.json结尾,会先解析其中的plugins[].url数组逐个安装,否则把输入本身当插件地址(pluginList.tsx)。

设计与坑:源码里能读到的四个决策

  1. hash 而不是版本号做身份。插件 ID 是sha256(funcCode)(plugin.ts),安装去重、卸载、替换旧版本全部按 hash 匹配,version只用于"拒绝装旧版"判断(index.ts)。坑:同一插件只要改一个字符 hash 就变,旧文件被删后缓存也随之清理(index.ts),依赖 hash 做跨设备识别不成立。
  2. 能力方法全部可选,包装层负责空态。PluginMethodsWrapper里每个方法开头都有ensurePluginIsMounted+ 未实现判断(如 plugin.ts),插件只需实现自己支持的部分。坑:supportedMethods是挂载后按"属性是函数"统计的(plugin.ts),所以懒加载插件在真正挂载前,其supportedMethods来自 MMKV 缓存的快照,若缓存丢失能力列表会缺项。
  3. 缓存三态而非布尔。cache/no-cache/no-store把"能否读旧缓存"和"要不要写新缓存"拆开了,离线时no-cache插件也能播放缓存源(plugin.ts)。坑:缓存条目上限 800,满了直接按 key 顺序删一半(mediaCache.ts),不是 LRU,高频播放的插件条目也可能被先删。
  4. 播放队列里塞假 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询