fix hdr tone mapping

This commit is contained in:
2026-06-04 23:11:21 +03:00
parent 6756bf9d40
commit 5814714e6a
3 changed files with 55 additions and 18 deletions
+1 -1
View File
@@ -647,7 +647,7 @@ export function usePlaybackInfo(
// picks the default; when set, the returned TranscodingUrl muxes // picks the default; when set, the returned TranscodingUrl muxes
// that audio track into the stream. // that audio track into the stream.
AudioStreamIndex: audioStreamIndex, AudioStreamIndex: audioStreamIndex,
DeviceProfile: browserDeviceProfile(audioPassthrough) as any, DeviceProfile: await browserDeviceProfile(audioPassthrough) as any,
AutoOpenLiveStream: true, AutoOpenLiveStream: true,
EnableDirectPlay: true, EnableDirectPlay: true,
EnableDirectStream: true, EnableDirectStream: true,
+1 -1
View File
@@ -42,7 +42,7 @@ export function usePrebuffer(item: BaseItemDto | null | undefined, armed: boolea
playbackInfoDto: { playbackInfoDto: {
UserId: jellyfinClient.getAuthState()!.userId, UserId: jellyfinClient.getAuthState()!.userId,
MaxStreamingBitrate: 140_000_000, MaxStreamingBitrate: 140_000_000,
DeviceProfile: browserDeviceProfile() as any, DeviceProfile: await browserDeviceProfile() as any,
AutoOpenLiveStream: true, AutoOpenLiveStream: true,
EnableDirectPlay: true, EnableDirectPlay: true,
EnableDirectStream: true, EnableDirectStream: true,
+53 -16
View File
@@ -32,6 +32,29 @@ function canPlayInMse(mime: string): boolean {
} }
} }
async function canDecodeHdr(contentType: string, transferFunction: 'pq' | 'hlg' = 'pq'): Promise<boolean> {
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[] { function supportedVideoCodecs(): string[] {
const codecs: string[] = ['h264'] // Universally supported in MSE const codecs: string[] = ['h264'] // Universally supported in MSE
if (canPlayInMse('video/mp4; codecs="hev1.1.6.L93.B0"') || 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`) * or VP9.2 (`vp09.02.10.10.01.09.16.09.00`)
* - Dolby Vision: `dvh1.05.06` / `dvhe.05.06` * - Dolby Vision: `dvh1.05.06` / `dvhe.05.06`
*/ */
function supportedVideoRanges(): string { async function supportedVideoRanges(): Promise<string> {
const ranges = ['SDR'] const ranges = ['SDR']
const hdrCodec = if (!displaySupportsHdr()) return ranges.join('|')
canPlayInMse('video/mp4; codecs="hvc1.2.4.L153.B0"') ||
canPlayInMse('video/mp4; codecs="hev1.2.4.L153.B0"') || const hdrCodec = (await Promise.all([
canPlayInMse('video/webm; codecs="vp09.02.10.10.01.09.16.09.00"') canDecodeHdr('video/mp4; codecs="hvc1.2.4.L153.B0"'),
if (hdrCodec && displaySupportsHdr()) { canDecodeHdr('video/mp4; codecs="hev1.2.4.L153.B0"'),
ranges.push('HDR10', 'HDR10Plus', 'HLG') 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"') || const hlgCodec = (await Promise.all([
canPlayInMse('video/mp4; codecs="dvhe.05.06"') canDecodeHdr('video/mp4; codecs="hvc1.2.4.L153.B0"', 'hlg'),
if (dvCodec && displaySupportsHdr()) { 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') ranges.push('DOVI', 'DOVIWithHDR10', 'DOVIWithHLG', 'DOVIWithSDR')
} }
return ranges.join('|') return ranges.join('|')
@@ -108,10 +145,10 @@ export function isHdrDisplayActive(): boolean {
return displaySupportsHdr() return displaySupportsHdr()
} }
export function browserDeviceProfile(audioPassthrough = false) { export async function browserDeviceProfile(audioPassthrough = false) {
const videoCodecs = supportedVideoCodecs() const videoCodecs = supportedVideoCodecs()
const videoCodecsCsv = videoCodecs.join(',') const videoCodecsCsv = videoCodecs.join(',')
const videoRanges = supportedVideoRanges() const videoRanges = await supportedVideoRanges()
return { return {
Name: 'Jellybloom Browser Client', Name: 'Jellybloom Browser Client',
@@ -215,7 +252,7 @@ export function browserDeviceProfile(audioPassthrough = false) {
Condition: 'EqualsAny', Condition: 'EqualsAny',
Property: 'VideoRangeType', Property: 'VideoRangeType',
Value: videoRanges, Value: videoRanges,
IsRequired: false, IsRequired: true,
}, },
], ],
}, },
@@ -229,7 +266,7 @@ export function browserDeviceProfile(audioPassthrough = false) {
Condition: 'EqualsAny', Condition: 'EqualsAny',
Property: 'VideoRangeType', Property: 'VideoRangeType',
Value: videoRanges, Value: videoRanges,
IsRequired: false, IsRequired: true,
}, },
], ],
}, },
@@ -241,7 +278,7 @@ export function browserDeviceProfile(audioPassthrough = false) {
Condition: 'EqualsAny', Condition: 'EqualsAny',
Property: 'VideoRangeType', Property: 'VideoRangeType',
Value: videoRanges, Value: videoRanges,
IsRequired: false, IsRequired: true,
}, },
], ],
}, },