fix(ui): improve browser codec detection and limit Safari transcoding to mp3 (#5171)

* fix: update codec MIME types to support multiple variants for better compatibility

Signed-off-by: Deluan <deluan@navidrome.org>

* fix: limit Safari transcoding to mp3

Signed-off-by: Deluan <deluan@navidrome.org>

* style: format browserProfile test file with prettier

* fix: comment

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan Quintão 2026-03-12 08:21:49 -04:00 • committed by GitHub
commit 0312eb33f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 113 additions and 17 deletions

View file

@ -1,12 +1,16 @@
// Each entry: { codec name for the server, container, MIME to probe }
// Each entry: { codec name for the server, container, mime: [MIME probe strings] }
export const CODEC_PROBES = [
{ codec: 'mp3', container: 'mp3', mime: 'audio/mpeg' },
{ codec: 'aac', container: 'mp4', mime: 'audio/mp4; codecs="mp4a.40.2"' },
{ codec: 'opus', container: 'ogg', mime: 'audio/ogg; codecs="opus"' },
{ codec: 'vorbis', container: 'ogg', mime: 'audio/ogg; codecs="vorbis"' },
{ codec: 'flac', container: 'flac', mime: 'audio/flac' },
{ codec: 'wav', container: 'wav', mime: 'audio/wav' },
{ codec: 'alac', container: 'mp4', mime: 'audio/mp4; codecs="alac"' },
{ codec: 'mp3', container: 'mp3', mime: ['audio/mpeg; codecs="mp3"'] },
{ codec: 'opus', container: 'ogg', mime: ['audio/ogg; codecs="opus"'] },
{ codec: 'vorbis', container: 'ogg', mime: ['audio/ogg; codecs="vorbis"'] },
{
codec: 'flac',
container: 'flac',
mime: ['audio/flac', 'audio/flac; codecs="flac"'],
},
{ codec: 'wav', container: 'wav', mime: ['audio/wav; codecs="1"'] },
{ codec: 'alac', container: 'mp4', mime: ['audio/mp4; codecs="alac"'] },
{ codec: 'aac', container: 'mp4', mime: ['audio/mp4; codecs="mp4a.40.2"'] },
]
// Transcoding targets in preference order (lossless first, then lossy).
@ -14,8 +18,27 @@ export const CODEC_PROBES = [
// MP3 is always included as a universal fallback.
const TRANSCODE_CODECS = ['flac', 'opus', 'mp3']
// Safari transcoding is limited to mp3 only. Safari cannot reliably stream
// Ogg containers (reports canPlayType support but fails on non-seekable
// transcoded streams), and FLAC transcoding also fails in practice.
const SAFARI_TRANSCODE_CODECS = ['mp3']
function canPlay(audio, mimeList) {
return mimeList.some((m) => {
const result = audio.canPlayType(m)
return result === 'probably' || result === 'maybe'
})
}
function probeSupported(audio, probes) {
return probes.filter(({ mime }) => audio.canPlayType(mime) === 'probably')
return probes.filter(({ mime }) => canPlay(audio, mime))
}
function isSafari() {
const ua = navigator.userAgent
return (
ua.includes('Safari') && !ua.includes('Chrome') && !ua.includes('Chromium')
)
}
export function detectBrowserProfile() {
@ -29,10 +52,15 @@ export function detectBrowserProfile() {
}),
)
// Build transcoding profiles from supported codecs, always keeping mp3 as fallback
const transcodingProfiles = TRANSCODE_CODECS.reduce((profiles, codec) => {
// Build transcoding profiles from supported codecs, always keeping mp3 as fallback.
// Safari is limited to mp3 transcoding only.
const transcodeCodecs = isSafari()
? SAFARI_TRANSCODE_CODECS
: TRANSCODE_CODECS
const transcodingProfiles = transcodeCodecs.reduce((profiles, codec) => {
const probe = CODEC_PROBES.find((p) => p.codec === codec)
if (audio.canPlayType(probe.mime) === 'probably' || codec === 'mp3') {
if (!probe) return profiles
if (canPlay(audio, probe.mime) || codec === 'mp3') {
profiles.push({
container: probe.container,
audioCodec: codec,