From 5814714e6a6d5a2d7a086ec0ae36000f651f14ce Mon Sep 17 00:00:00 2001 From: lashman Date: Thu, 4 Jun 2026 23:11:21 +0300 Subject: [PATCH] fix hdr tone mapping --- src/hooks/use-jellyfin.ts | 2 +- src/hooks/use-prebuffer.ts | 2 +- src/lib/device-profile.ts | 69 +++++++++++++++++++++++++++++--------- 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/hooks/use-jellyfin.ts b/src/hooks/use-jellyfin.ts index 709234d..9a7dfe5 100644 --- a/src/hooks/use-jellyfin.ts +++ b/src/hooks/use-jellyfin.ts @@ -647,7 +647,7 @@ export function usePlaybackInfo( // picks the default; when set, the returned TranscodingUrl muxes // that audio track into the stream. AudioStreamIndex: audioStreamIndex, - DeviceProfile: browserDeviceProfile(audioPassthrough) as any, + DeviceProfile: await browserDeviceProfile(audioPassthrough) as any, AutoOpenLiveStream: true, EnableDirectPlay: true, EnableDirectStream: true, diff --git a/src/hooks/use-prebuffer.ts b/src/hooks/use-prebuffer.ts index 1cfff49..6e20242 100644 --- a/src/hooks/use-prebuffer.ts +++ b/src/hooks/use-prebuffer.ts @@ -42,7 +42,7 @@ export function usePrebuffer(item: BaseItemDto | null | undefined, armed: boolea playbackInfoDto: { UserId: jellyfinClient.getAuthState()!.userId, MaxStreamingBitrate: 140_000_000, - DeviceProfile: browserDeviceProfile() as any, + DeviceProfile: await browserDeviceProfile() as any, AutoOpenLiveStream: true, EnableDirectPlay: true, EnableDirectStream: true, diff --git a/src/lib/device-profile.ts b/src/lib/device-profile.ts index 141f5de..ba752ea 100644 --- a/src/lib/device-profile.ts +++ b/src/lib/device-profile.ts @@ -32,6 +32,29 @@ function canPlayInMse(mime: string): boolean { } } +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"') || @@ -82,19 +105,33 @@ function displaySupportsHdr(): boolean { * or VP9.2 (`vp09.02.10.10.01.09.16.09.00`) * - Dolby Vision: `dvh1.05.06` / `dvhe.05.06` */ -function supportedVideoRanges(): string { +async function supportedVideoRanges(): Promise { const ranges = ['SDR'] - const hdrCodec = - canPlayInMse('video/mp4; codecs="hvc1.2.4.L153.B0"') || - canPlayInMse('video/mp4; codecs="hev1.2.4.L153.B0"') || - canPlayInMse('video/webm; codecs="vp09.02.10.10.01.09.16.09.00"') - if (hdrCodec && displaySupportsHdr()) { - ranges.push('HDR10', 'HDR10Plus', 'HLG') + 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 dvCodec = - canPlayInMse('video/mp4; codecs="dvh1.05.06"') || - canPlayInMse('video/mp4; codecs="dvhe.05.06"') - if (dvCodec && displaySupportsHdr()) { + + 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('|') @@ -108,10 +145,10 @@ export function isHdrDisplayActive(): boolean { return displaySupportsHdr() } -export function browserDeviceProfile(audioPassthrough = false) { +export async function browserDeviceProfile(audioPassthrough = false) { const videoCodecs = supportedVideoCodecs() const videoCodecsCsv = videoCodecs.join(',') - const videoRanges = supportedVideoRanges() + const videoRanges = await supportedVideoRanges() return { Name: 'Jellybloom Browser Client', @@ -215,7 +252,7 @@ export function browserDeviceProfile(audioPassthrough = false) { Condition: 'EqualsAny', Property: 'VideoRangeType', Value: videoRanges, - IsRequired: false, + IsRequired: true, }, ], }, @@ -229,7 +266,7 @@ export function browserDeviceProfile(audioPassthrough = false) { Condition: 'EqualsAny', Property: 'VideoRangeType', Value: videoRanges, - IsRequired: false, + IsRequired: true, }, ], }, @@ -241,7 +278,7 @@ export function browserDeviceProfile(audioPassthrough = false) { Condition: 'EqualsAny', Property: 'VideoRangeType', Value: videoRanges, - IsRequired: false, + IsRequired: true, }, ], },