formatters, device profile, media matching, subtitle utils, syncplay, trakt
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* HDR support detection. Probes for the codec strings the major HDR
|
||||
* formats use:
|
||||
* - HDR10 / HLG via HEVC main10 level 5.1 (`hvc1.2.4.L153.B0`) or VP9.2
|
||||
* (`vp09.02.10.10.01.09.16.09.00`)
|
||||
* - Dolby Vision via the `dvh1.05.06` codec string
|
||||
* When a format is detected we add it to the `VideoRangeType` matrix on
|
||||
* the corresponding CodecProfile so the server direct-plays HDR content
|
||||
* instead of falling back to a tone-mapped SDR transcode.
|
||||
*/
|
||||
function supportedVideoRanges(): string {
|
||||
const ranges = ['SDR']
|
||||
if (
|
||||
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"')
|
||||
) {
|
||||
ranges.push('HDR10', 'HLG')
|
||||
}
|
||||
if (
|
||||
canPlayInMse('video/mp4; codecs="dvh1.05.06"') ||
|
||||
canPlayInMse('video/mp4; codecs="dvhe.05.06"')
|
||||
) {
|
||||
ranges.push('DOVI', 'DOVIWithHDR10', 'DOVIWithHLG', 'DOVIWithSDR')
|
||||
}
|
||||
return ranges.join('|')
|
||||
}
|
||||
|
||||
export function browserDeviceProfile() {
|
||||
const videoCodecs = supportedVideoCodecs()
|
||||
const videoCodecsCsv = videoCodecs.join(',')
|
||||
const videoRanges = supportedVideoRanges()
|
||||
|
||||
return {
|
||||
Name: 'Jellyfin Browser Client',
|
||||
MaxStreamingBitrate: 120_000_000,
|
||||
MaxStaticBitrate: 100_000_000,
|
||||
MusicStreamingTranscodingBitrate: 384_000,
|
||||
|
||||
DirectPlayProfiles: [
|
||||
{
|
||||
Container: 'mp4,m4v',
|
||||
Type: 'Video',
|
||||
VideoCodec: videoCodecsCsv,
|
||||
AudioCodec: 'aac,mp3,ac3,eac3,flac,opus',
|
||||
},
|
||||
{
|
||||
Container: 'webm',
|
||||
Type: 'Video',
|
||||
VideoCodec: 'vp8,vp9,av1',
|
||||
AudioCodec: 'opus,vorbis',
|
||||
},
|
||||
],
|
||||
|
||||
TranscodingProfiles: [
|
||||
{
|
||||
// fMP4 over HLS. Listing the full supported codec set lets the server
|
||||
// stream-copy (remux only) when the source already matches one of
|
||||
// them - that's what makes h264-in-mkv "play instantly" instead of
|
||||
// burning CPU. The browser plays this through MSE via hls.js.
|
||||
Container: 'mp4',
|
||||
Type: 'Video',
|
||||
VideoCodec: videoCodecsCsv,
|
||||
AudioCodec: 'aac,mp3',
|
||||
Protocol: 'hls',
|
||||
Context: 'Streaming',
|
||||
MaxAudioChannels: '6',
|
||||
MinSegments: 1,
|
||||
BreakOnNonKeyFrames: true,
|
||||
},
|
||||
{
|
||||
Container: 'aac',
|
||||
Type: 'Audio',
|
||||
AudioCodec: 'aac',
|
||||
Context: 'Streaming',
|
||||
Protocol: 'http',
|
||||
MaxAudioChannels: '2',
|
||||
},
|
||||
],
|
||||
|
||||
ContainerProfiles: [],
|
||||
CodecProfiles: [
|
||||
// Bound h264 to profiles/levels MSE actually decodes - avoids the
|
||||
// server stream-copying high-10 or level 5.2 footage that the browser
|
||||
// would silently refuse.
|
||||
{
|
||||
Type: 'Video',
|
||||
Codec: 'h264',
|
||||
Conditions: [
|
||||
{
|
||||
Condition: 'EqualsAny',
|
||||
Property: 'VideoProfile',
|
||||
Value: 'high|main|baseline|constrained baseline|high 10',
|
||||
IsRequired: false,
|
||||
},
|
||||
{
|
||||
Condition: 'LessThanEqual',
|
||||
Property: 'VideoLevel',
|
||||
Value: '52',
|
||||
IsRequired: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
// HEVC: cap at main / main10 + level 5.1 - matches what hardware
|
||||
// decoders on most consumer GPUs can chew through. VideoRangeType
|
||||
// advertises whichever HDR formats the browser actually supports so
|
||||
// the server direct-plays HDR sources instead of tone-mapping to SDR.
|
||||
{
|
||||
Type: 'Video',
|
||||
Codec: 'hevc',
|
||||
Conditions: [
|
||||
{
|
||||
Condition: 'EqualsAny',
|
||||
Property: 'VideoProfile',
|
||||
Value: 'main|main 10',
|
||||
IsRequired: false,
|
||||
},
|
||||
{
|
||||
Condition: 'LessThanEqual',
|
||||
Property: 'VideoLevel',
|
||||
Value: '153',
|
||||
IsRequired: false,
|
||||
},
|
||||
{
|
||||
Condition: 'EqualsAny',
|
||||
Property: 'VideoRangeType',
|
||||
Value: videoRanges,
|
||||
IsRequired: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
// VP9 + AV1 also support HDR transport; advertise the same range
|
||||
// matrix so 4K HDR YouTube-style sources direct-play.
|
||||
{
|
||||
Type: 'Video',
|
||||
Codec: 'vp9',
|
||||
Conditions: [
|
||||
{
|
||||
Condition: 'EqualsAny',
|
||||
Property: 'VideoRangeType',
|
||||
Value: videoRanges,
|
||||
IsRequired: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
Type: 'Video',
|
||||
Codec: 'av1',
|
||||
Conditions: [
|
||||
{
|
||||
Condition: 'EqualsAny',
|
||||
Property: 'VideoRangeType',
|
||||
Value: videoRanges,
|
||||
IsRequired: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
SubtitleProfiles: [
|
||||
{ Format: 'vtt', Method: 'External' },
|
||||
{ Format: 'subrip', Method: 'External' },
|
||||
{ Format: 'ass', Method: 'External' },
|
||||
{ Format: 'ssa', Method: 'External' },
|
||||
],
|
||||
|
||||
ResponseProfiles: [],
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user