/** * A Jellyfin DeviceProfile describing what this browser client can play. * * The server uses this to decide direct-play vs direct-stream vs transcode * and to choose codecs/containers that browsers can decode. * * Strategy mirrors jellyfin-web: * - DirectPlay any mp4/m4v with codecs the browser advertises support for. * - Transcode (or stream-copy via remux) into fMP4 over HLS, served through * hls.js + MSE. Listing multiple VideoCodec values on the TranscodingProfile * lets the server pick the source codec when it's already compatible - * that's the difference between "remux a file in 2 seconds" and "re-encode * every frame in real time". A Raspberry Pi can do the former; nothing in * this codebase makes it do the latter unless the source is genuinely * incompatible. * * MaxStreamingBitrate is intentionally high - we'd rather direct-stream the * source bitrate as-is than throttle it. The server only honours the cap when * a transcode is actually required. */ /** * Best-effort runtime probes for codec support in MSE. Browsers report this * accurately for VP9/AV1; HEVC support is hardware-gated and can vary even * on the same browser version, so we trust whatever isTypeSupported says. */ function canPlayInMse(mime: string): boolean { try { return typeof MediaSource !== 'undefined' && MediaSource.isTypeSupported(mime) } catch { return false } } async function canDecodeHdr(contentType: string, transferFunction: 'pq' | 'hlg' = 'pq'): Promise { if (!canPlayInMse(contentType)) return false if (typeof navigator.mediaCapabilities?.decodingInfo !== 'function') return false try { const info = await navigator.mediaCapabilities.decodingInfo({ type: 'media-source', video: { contentType, width: 3840, height: 2160, bitrate: 25_000_000, framerate: 24, colorGamut: 'rec2020', transferFunction, }, } as any) return !!info.supported } catch { return false } } function supportedVideoCodecs(): string[] { const codecs: string[] = ['h264'] // Universally supported in MSE if (canPlayInMse('video/mp4; codecs="hev1.1.6.L93.B0"') || canPlayInMse('video/mp4; codecs="hvc1.1.6.L93.B0"')) { codecs.push('hevc') } if (canPlayInMse('video/mp4; codecs="vp09.00.10.08"')) { codecs.push('vp9') } if (canPlayInMse('video/mp4; codecs="av01.0.04M.08"')) { codecs.push('av1') } return codecs } /** * Check whether the display is in HDR mode. On Windows this means the * user has "Use HDR" turned on in Display Settings. The media query * returns false on SDR-only panels and on HDR panels with HDR mode off. * * jellyfin-web uses browser-identification heuristics (hardcoding * `browser.chrome && !browser.mobile` etc.) because it doesn't control * the shell. We control the Tauri shell so we can use the actual media * query - same outcome, fewer moving parts. */ function displaySupportsHdr(): boolean { try { return window.matchMedia('(dynamic-range: high)').matches } catch { return false } } /** * HDR format detection. Two-stage gate: * 1. Codec probe - does the browser's MSE stack understand the codec * string for this HDR format? * 2. Display check - is the user's display actually in HDR mode? * * Both must be true. Advertising HDR10/HLG/DOVI in VideoRangeType when * the display is SDR causes the server to direct-play the file; the * browser then tone-maps it to SDR, which works but bypasses the * server's configurable FFmpeg tone-mapping and can produce different * (often worse) results. * * Codec strings probed: * - HDR10 / HDR10+ / HLG: HEVC main10 level 5.1 (`hvc1.2.4.L153.B0`) * or VP9.2 (`vp09.02.10.10.01.09.16.09.00`) * - Dolby Vision: `dvh1.05.06` / `dvhe.05.06` */ async function supportedVideoRanges(): Promise { const ranges = ['SDR'] if (!displaySupportsHdr()) return ranges.join('|') const hdrCodec = (await Promise.all([ canDecodeHdr('video/mp4; codecs="hvc1.2.4.L153.B0"'), canDecodeHdr('video/mp4; codecs="hev1.2.4.L153.B0"'), canDecodeHdr('video/mp4; codecs="av01.0.08M.10.0.110.09"'), canDecodeHdr('video/webm; codecs="vp09.02.10.10.01.09.16.09.00"'), ])).some(Boolean) if (hdrCodec) { ranges.push('HDR10', 'HDR10Plus') } const hlgCodec = (await Promise.all([ canDecodeHdr('video/mp4; codecs="hvc1.2.4.L153.B0"', 'hlg'), canDecodeHdr('video/mp4; codecs="hev1.2.4.L153.B0"', 'hlg'), canDecodeHdr('video/mp4; codecs="av01.0.08M.10.0.110.09"', 'hlg'), canDecodeHdr('video/webm; codecs="vp09.02.10.10.01.09.16.09.00"', 'hlg'), ])).some(Boolean) if (hlgCodec && !ranges.includes('HLG')) ranges.push('HLG') const dvCodec = (await Promise.all([ canDecodeHdr('video/mp4; codecs="dvh1.05.06"'), canDecodeHdr('video/mp4; codecs="dvhe.05.06"'), ])).some(Boolean) if (dvCodec) { ranges.push('DOVI', 'DOVIWithHDR10', 'DOVIWithHLG', 'DOVIWithSDR') } return ranges.join('|') } /** * Exported helper so StreamInfo and other diagnostics can show whether * the display is in HDR mode without re-probing the codec strings. */ export function isHdrDisplayActive(): boolean { return displaySupportsHdr() } export async function browserDeviceProfile(audioPassthrough = false) { const videoCodecs = supportedVideoCodecs() const videoCodecsCsv = videoCodecs.join(',') const videoRanges = await supportedVideoRanges() return { Name: 'Jellybloom Browser Client', MaxStreamingBitrate: 120_000_000, MaxStaticBitrate: 100_000_000, MusicStreamingTranscodingBitrate: 384_000, DirectPlayProfiles: [ { // Chromium can natively play mp4/m4v. Everything else (mkv, avi, etc) // goes through the HLS TranscodingProfile below, which remuxes into // fMP4 that hls.js + MSE can handle. Listing mkv here would make the // server return SupportsDirectPlay=true, but Chromium's