到底是谁的BUG呢?
一切的一切,要从 YesPlayMusic 这个播放器说起。
YesPlayMusic 篇
YesPlayMusic 是一款使用 Electron 编写的第三方网易云播放器,也是我目前认为在 Linux 上最好用的播放器。虽然这样说,但她仍然有很多的不足,比如不支持桌面歌词。后来我无意中发现了一款为 Linux 设计的桌面歌词软件:OSDLyrics。
这个软件的原理是通过 Linux 的 MPRIS 接口来获取当前播放媒体的曲目标题,然后在本地指定目录查找同名的 LRC 文件。如果没有找到,就联网进行查找。然而,联网查找的准确度很低,经常会找到错误的 LRC 或者干脆找不到。为了使 YesPlayMusic 能够搭配 OSDLyrics 使用,如果能让 YesPlayMusic 每次更新 Mpris 信息的时候把 LRC 文件下载到本地不就行了吗?
在进行了一番研究之后,最后算是勉强实现了这个功能。关于详细的代码细节这里就不在叙述了,相关的 commit 在这里:a2508589d372e849fb00996297e44a4a2a6b51a2。并且发布了一个可以直接使用的 Fork 版本:v0.4.5-1
不过上面的都是前传。某一天我尝试上传自己找的 CD 音乐资源到网易云的音乐云盘的时候,奇怪地发现 YesPlayMusic 无法播放。一开始我以为是网络问题,排查了一番后发现没问题。难道是播放器的问题?为了对比验证,我使用手机端网易云 APP 进行播放,没有任何问题。那看来就是 YesPlayMusic 的问题了。
既然都贡献代码了,不妨来排查一下到底是哪的问题吧,说不定可以再混一个 pr 呢。
打开控制台:
尝试 lyric 属性时发现变量是 undefined。来看看这里的代码:
1 | async saveLyricsFile(track) { |
这里在 lyricData 上读取了 lyric 属性,然而经过断电调试发现,lyricData 上虽然没有 lrc 属性,但经过判断之后应该不会报这个错才对。这说明问题不在这里。正在我想会是哪里有问题的时候,我随手点开了 Devtools 的 Network 面板,一看,一堆的 getLyrics 请求:
这就说明获取歌词的接口被循环调用,程序一直卡在了这个地方。那么到底是哪个地方一直在调用呢?于是我沿着上层的函数一个一个加断电,最终找到了下面的一段错误处理的代码:
1 | this._howler.on('loaderror', (_, errCode) => { |
在 this._replaceCurrentTrackAudio
函数里,程序会尝试重新加载当前的曲目,最终调用到 getLyrics 函数。然而每次加载的时候又会触发 howler 的 loaderror 于是就有进到了这个地方,导致了重复的调用。
那么为什么 howler 会出现 error 呢?让我们来打印一下这里的 errCode 看看。
打印出来发现这里的 errCode 是 4,MDN 里对它的定义是:
Name | Value | Description |
---|---|---|
MEDIA_ERR_SRC_NOT_SUPPORTED |
4 |
The associated resource or media provider object (such as a MediaStream ) has been found to be unsuitable. |
也就是说这里碰到了一个 MEDIA_ERR_SRC_NOT_SUPPORTED
的错误,翻译过来也就是不支持的媒体格式。奇怪,我在云盘里上传的 flac 文件有问题吗?可是本地播放器明明能正常播放的说。
不过为了先解决掉 YesPlayMusic 这边的问题,我在这里有加了一个判断,当 errCode 为 4 的时候也之间跳转到下一首曲目:
1 | if (errCode === 3) { |
Chromium 篇
暂时解决了 YesPlayMusic 了之后,我决定再研究一下我的文件到底为什么不支持播放。这里的 MEDIA_ERR_SRC_NOT_SUPPORTED
错误是一个浏览器内核级别的错误。鉴于 Electron 的内核就是 Chromium,也就是说是 Chromium 的问题了。为了验证,我把这个有问题的 FLAC 文件拖到了 Chrome 里,果然报了同样的错误。同时,为了验证是 Chromium 的问题,我还试了在 Firefox 里播放这个文件,结果是 Firefox 可以正常播放。
那 Chromium 为什么播放不了这个文件呢?这时我想起 Chrome 有一个 media-internal 的日志页面,用来检测媒体解码器的日志。
在 Chrome 中打开 chrome://media-internals
页面,找到要播放的标签条目,发现了下面的报错:
1 | error "FFmpegDemuxer: open context failed" |
也就是说 Chrome 尝试调用 ffmpeg 解码器来播放这个文件,结果 ffmpeg 说它没法打开这个文件。
为什么 ffmpeg 打不开这个文件呢?我尝试在本地使用 ffmpeg 的附属播放器 ffplay 来播放这个文件,结果是可以正常播放。
于是我当时认定为 Chrome 在调用 ffmpeg 的解码器的时候出了问题,导致 ffmpeg 打不开这个文件。鉴于只有这个文件在会出问题,我当时的想法是这个文件比较特殊,破坏了 Chrome 内部的什么机制。
于是我决定向 Chromium 项目汇报这个问题:#1431655
很快啊,Chromium 开发组的人回复,说我提供的 flac 本身是具有问题的,还附上了一份用 ffmpeg 检测错误的输出:
1 | ffmpeg -err_detect explode -i obj.flac out.wav |
惭愧的是我并不知道 ffmpeg 有错误检测的功能,在使用 ffmpeg 对问题文件执行同样的检查后,输出了同样的报错。
那就是这个 flac 文件确实有问题了,那到底有什么问题呢?为什么其他播放器都能播放呢?
观察 ffmpeg 的错误输出,它说无法读取附加图片的 mime-type。什么是 mime-type 呢?它是个用来指定媒体文件类型的通用标准。有了文件的 mime-type,就算不知道扩展名和文件头也可以正确地处理文件的数据。
为了验证问题所在,我使用 Kid3 给这个文件的嵌入封面加上了 mime type,然后拖到 chrome 里播放,果然可以正常播放了。
那为什么这个 flac 文件嵌入的图片封面没有 mime-type 呢?这个文件是哪里来的?这里我终于想起,这个 flac 文件是使用一个叫 flacon 的软件导出的。
Flacon 篇
Flacon 是一款用来对整个 CUE Track 和音频做分割的软件。在网上找到的 CD 音乐资源,下载下来大多数是一个特别大的 wav 文件和一个 cue 文件,有的还会带一张 cover.jpg 的专辑封面图片。CUE 格式的文件里记录着专辑里每个曲目的起始和终止位置。要想得到多个曲目的音频,就需要用专门的软件将它们分割开。分割之后的格式一般会选 wav 或 flac 格式,其中 flac 由于无损压缩的特性和丰富的标签支持在实际中更常见一些。
这里我使用的就是 flacon 来将专辑进行分割的。经过对比,发现用 flacon 导出的 flac 文件果然都缺少 MIME 信息,那么就得给 flacon 那边反馈了:#199
然而在进行了详细的反馈之后,开发者连续两天都没有理我。于是我斗胆决定把这个 C++ 的项目拉到本地进行调试,找找问题到底在哪。这里就要顺便提一下这种 Qt + CPP 的项目是真好编译,环境啥的随随便便就配了。以前跑别人的 Node 或是 Python 什么的项目需要配半天环境,最后也不见得能跑起来。
这里重点关注的地方就是这个 setCoverImage 函数:
1 | void FlacMetadataWriter::setCoverImage(const CoverImage &image) |
在 pic->setMimeType(image.mimeType().toStdString());
这里断点,然后看看 mimeType()
到底有没有被写到 pic 里。
然而尴尬的是,在调试的时候发现 mime 类型确实正确写入了,而且输出的 flac 文件也没有任何问题。这就邪了门了。这时我突然想起,我本地安装的 flacon 版本和源码的版本好像隔了挺久的,难道是 flacon 那边已经把这个问题修了?怪不得开发者不理我…
遗憾的时我翻了半天也没找到到底哪一条 commit 把这个问题修了,大概率是我确实没找到。毕竟我连 C++ 一点都没学过。
总而言之,这次因为这个问题纠结来纠结去了好几天,最后却以这种离谱的方式解决了,怎么想都很气啊!