AC3 audio in an MP4 container is an invalid codec/container combination. Restrict the surround passthrough fallback to containers other than mp4 to avoid producing invalid direct play profiles.
1164 lines
41 KiB
Text
1164 lines
41 KiB
Text
import "pkg:/source/api/baserequest.bs"
|
|
import "pkg:/source/enums/PlaybackMethod.bs"
|
|
import "pkg:/source/enums/String.bs"
|
|
import "pkg:/source/utils/misc.bs"
|
|
|
|
const h264Profiles = ["baseline", "main", "high"]
|
|
|
|
' Returns the Device Capabilities for Roku.
|
|
function getDeviceCapabilities(videoCodec = string.EMPTY as string) as object
|
|
deviceProfile = {
|
|
"PlayableMediaTypes": [
|
|
"Audio",
|
|
"Video",
|
|
"Photo"
|
|
],
|
|
"SupportedCommands": [],
|
|
"SupportsPersistentIdentifier": true,
|
|
"SupportsMediaControl": false,
|
|
"SupportsContentUploading": false,
|
|
"SupportsSync": false,
|
|
"DeviceProfile": getDeviceProfile(videoCodec)
|
|
}
|
|
|
|
return deviceProfile
|
|
end function
|
|
|
|
function getDeviceProfile(videoCodec = string.EMPTY as string) as object
|
|
globalDevice = m.global.device
|
|
return {
|
|
"Name": "Official Roku Client",
|
|
"Id": globalDevice.id,
|
|
"Identification": {
|
|
"FriendlyName": globalDevice.friendlyName,
|
|
"ModelNumber": globalDevice.model,
|
|
"SerialNumber": "string",
|
|
"ModelName": globalDevice.name,
|
|
"ModelDescription": "Type: " + globalDevice.modelType,
|
|
"Manufacturer": globalDevice.modelDetails.VendorName
|
|
},
|
|
"FriendlyName": globalDevice.friendlyName,
|
|
"Manufacturer": globalDevice.modelDetails.VendorName,
|
|
"ModelName": globalDevice.name,
|
|
"ModelDescription": "Type: " + globalDevice.modelType,
|
|
"ModelNumber": globalDevice.model,
|
|
"SerialNumber": globalDevice.serial,
|
|
"MaxStreamingBitrate": 120000000,
|
|
"MaxStaticBitrate": 100000000,
|
|
"MusicStreamingTranscodingBitrate": 192000,
|
|
"DirectPlayProfiles": GetDirectPlayProfiles(),
|
|
"TranscodingProfiles": getTranscodingProfiles(videoCodec),
|
|
"ContainerProfiles": getContainerProfiles(),
|
|
"CodecProfiles": getCodecProfiles(videoCodec),
|
|
"SubtitleProfiles": getSubtitleProfiles()
|
|
}
|
|
end function
|
|
|
|
function GetDirectPlayProfiles() as object
|
|
globalUserSettings = m.global.session.user.settings
|
|
directPlayProfiles = []
|
|
|
|
if not isStringEqual(PlaybackMethod.PLAYNORMALLY, chainLookupReturn(globalUserSettings, "`playback.media.forceTranscode`", PlaybackMethod.PLAYNORMALLY)) then return directPlayProfiles
|
|
if not isStringEqual(m.global.queueManager.callFunc("getForceTranscode"), PlaybackMethod.PLAYNORMALLY) then return directPlayProfiles
|
|
|
|
di = CreateObject("roDeviceInfo")
|
|
' all possible containers
|
|
supportedCodecs = {
|
|
mp4: {
|
|
audio: [],
|
|
video: []
|
|
},
|
|
hls: {
|
|
audio: [],
|
|
video: []
|
|
},
|
|
mkv: {
|
|
audio: [],
|
|
video: []
|
|
},
|
|
ism: {
|
|
audio: [],
|
|
video: []
|
|
},
|
|
dash: {
|
|
audio: [],
|
|
video: []
|
|
},
|
|
ts: {
|
|
audio: [],
|
|
video: []
|
|
}
|
|
}
|
|
' all possible codecs (besides those restricted by user settings)
|
|
videoCodecs = ["h264", "mpeg4 avc", "vp8", "vp9", "h263", "mpeg1"]
|
|
audioCodecs = ["mp3", "mp2", "pcm", "lpcm", "wav", "ac3", "ac4", "aiff", "wma", "flac", "alac", "aac", "opus", "dts", "wmapro", "vorbis", "eac3", "mpg123"]
|
|
|
|
' check if hevc is disabled
|
|
if globalUserSettings["playback.compatibility.disablehevc"] = false
|
|
videoCodecs.push("hevc")
|
|
end if
|
|
|
|
' check video codecs for each container
|
|
for each container in supportedCodecs
|
|
for each videoCodec in videoCodecs
|
|
if di.CanDecodeVideo({ Codec: videoCodec, Container: container }).Result
|
|
if videoCodec = "hevc"
|
|
supportedCodecs[container]["video"].push("hevc")
|
|
supportedCodecs[container]["video"].push("h265")
|
|
else
|
|
' device profile string matches codec string
|
|
supportedCodecs[container]["video"].push(videoCodec)
|
|
end if
|
|
end if
|
|
end for
|
|
end for
|
|
|
|
' user setting overrides
|
|
if globalUserSettings["playback.mpeg4"]
|
|
for each container in supportedCodecs
|
|
supportedCodecs[container]["video"].push("mpeg4")
|
|
end for
|
|
end if
|
|
if globalUserSettings["playback.mpeg2"]
|
|
for each container in supportedCodecs
|
|
supportedCodecs[container]["video"].push("mpeg2video")
|
|
end for
|
|
end if
|
|
|
|
' video codec overrides
|
|
' these codecs play fine but are not correctly detected using CanDecodeVideo()
|
|
if di.CanDecodeVideo({ Codec: "av1" }).Result
|
|
' codec must be checked by itself or the result will always be false
|
|
for each container in supportedCodecs
|
|
supportedCodecs[container]["video"].push("av1")
|
|
end for
|
|
end if
|
|
|
|
' check audio codecs for each container
|
|
' Work around potential AC3 false negatives on some surround configurations.
|
|
' When 5.1 surround output is enabled, assume AC3 support even if
|
|
' CanDecodeAudio() reports otherwise.
|
|
isSurround = di.GetAudioOutputChannel() = "5.1 surround"
|
|
|
|
for each container in supportedCodecs
|
|
for each audioCodec in audioCodecs
|
|
canDecode = di.CanDecodeAudio({
|
|
Codec: audioCodec,
|
|
Container: container
|
|
}).Result
|
|
|
|
if canDecode or (isSurround and isStringEqual(audioCodec, "ac3") and not isStringEqual(container, "mp4"))
|
|
supportedCodecs[container]["audio"].push(audioCodec)
|
|
end if
|
|
end for
|
|
end for
|
|
|
|
' remove audio codecs not supported as standalone audio files (opus)
|
|
' also add aac back to the list so it gets added to the direct play profile
|
|
audioCodecs = ["mp3", "mp2", "pcm", "lpcm", "wav", "ac3", "ac4", "aiff", "wma", "flac", "alac", "aac", "dts", "wmapro", "vorbis", "eac3", "mpg123"]
|
|
|
|
' check audio codecs with no container
|
|
supportedAudio = []
|
|
for each audioCodec in audioCodecs
|
|
if di.CanDecodeAudio({ Codec: audioCodec }).Result
|
|
supportedAudio.push(audioCodec)
|
|
end if
|
|
end for
|
|
|
|
' build return array
|
|
for each container in supportedCodecs
|
|
videoCodecString = supportedCodecs[container]["video"].Join(",")
|
|
if videoCodecString <> ""
|
|
containerString = container
|
|
|
|
if container = "mp4"
|
|
containerString = "mp4,mov,m4v"
|
|
else if container = "mkv"
|
|
containerString = "mkv,webm"
|
|
end if
|
|
|
|
directPlayProfiles.push({
|
|
"Container": containerString,
|
|
"Type": "Video",
|
|
"VideoCodec": videoCodecString,
|
|
"AudioCodec": supportedCodecs[container]["audio"].Join(",")
|
|
})
|
|
end if
|
|
end for
|
|
|
|
directPlayProfiles.push({
|
|
"Container": supportedAudio.Join(","),
|
|
"Type": "Audio"
|
|
})
|
|
return directPlayProfiles
|
|
end function
|
|
|
|
function getTranscodingProfiles(videoCodec = string.EMPTY as string) as object
|
|
globalUserSettings = m.global.session.user.settings
|
|
transcodingProfiles = []
|
|
|
|
di = CreateObject("roDeviceInfo")
|
|
|
|
transcodingContainers = ["mp4", "ts"]
|
|
' use strings to preserve order
|
|
mp4AudioCodecs = "AC3"
|
|
mp4VideoCodecs = isStringEqual(videoCodec, "h264") ? string.EMPTY : "h264"
|
|
tsAudioCodecs = "AC3"
|
|
tsVideoCodecs = isStringEqual(videoCodec, "h264") ? string.EMPTY : "h264"
|
|
|
|
' does the users setup support surround sound?
|
|
maxAudioChannels = "2" ' jellyfin expects this as a string
|
|
' in order of preference from left to right
|
|
audioCodecs = ["eac3", "ac3", "dts", "mp3", "vorbis", "opus", "flac", "alac", "ac4", "pcm", "wma", "wmapro"]
|
|
|
|
if di.GetAudioOutputChannel() = "5.1 surround"
|
|
maxAudioChannels = "6"
|
|
for each codec in audioCodecs
|
|
if di.CanDecodeAudio({ Codec: codec, ChCnt: 8 }).Result
|
|
maxAudioChannels = "8"
|
|
exit for
|
|
end if
|
|
end for
|
|
end if
|
|
|
|
' VIDEO CODECS
|
|
'
|
|
' AVC / h264 / MPEG4 AVC
|
|
for each container in transcodingContainers
|
|
if not isStringEqual(videoCodec, "h264")
|
|
if di.CanDecodeVideo({ Codec: "h264", Container: container }).Result
|
|
if container = "mp4"
|
|
' check for codec string before adding it
|
|
if mp4VideoCodecs.Instr(0, ",h264") = -1
|
|
mp4VideoCodecs = mp4VideoCodecs + ",h264"
|
|
end if
|
|
else if container = "ts"
|
|
' check for codec string before adding it
|
|
if tsVideoCodecs.Instr(0, ",h264") = -1
|
|
tsVideoCodecs = tsVideoCodecs + ",h264"
|
|
end if
|
|
end if
|
|
end if
|
|
end if
|
|
if not isStringEqual(videoCodec, "mpeg4 avc")
|
|
if di.CanDecodeVideo({ Codec: "mpeg4 avc", Container: container }).Result
|
|
if container = "mp4"
|
|
' check for codec string before adding it
|
|
if mp4VideoCodecs.Instr(0, ",mpeg4 avc") = -1
|
|
mp4VideoCodecs = mp4VideoCodecs + ",mpeg4 avc"
|
|
end if
|
|
else if container = "ts"
|
|
' check for codec string before adding it
|
|
if tsVideoCodecs.Instr(0, ",mpeg4 avc") = -1
|
|
tsVideoCodecs = tsVideoCodecs + ",mpeg4 avc"
|
|
end if
|
|
end if
|
|
end if
|
|
end if
|
|
end for
|
|
|
|
' HEVC / h265
|
|
if globalUserSettings["playback.compatibility.disablehevc"] = false
|
|
for each container in transcodingContainers
|
|
if not isStringEqual(videoCodec, "hevc")
|
|
if di.CanDecodeVideo({ Codec: "hevc", Container: container }).Result
|
|
if container = "mp4"
|
|
' check for codec string before adding it
|
|
if mp4VideoCodecs.Instr(0, "h265,") = -1
|
|
mp4VideoCodecs = "h265," + mp4VideoCodecs
|
|
end if
|
|
if mp4VideoCodecs.Instr(0, "hevc,") = -1
|
|
mp4VideoCodecs = "hevc," + mp4VideoCodecs
|
|
end if
|
|
else if container = "ts"
|
|
' check for codec string before adding it
|
|
if tsVideoCodecs.Instr(0, "h265,") = -1
|
|
tsVideoCodecs = "h265," + tsVideoCodecs
|
|
end if
|
|
if tsVideoCodecs.Instr(0, "hevc,") = -1
|
|
tsVideoCodecs = "hevc," + tsVideoCodecs
|
|
end if
|
|
end if
|
|
end if
|
|
end if
|
|
end for
|
|
end if
|
|
|
|
' VP9
|
|
for each container in transcodingContainers
|
|
if not isStringEqual(videoCodec, "vp9")
|
|
if di.CanDecodeVideo({ Codec: "vp9", Container: container }).Result
|
|
if container = "mp4"
|
|
' check for codec string before adding it
|
|
if mp4VideoCodecs.Instr(0, ",vp9") = -1
|
|
mp4VideoCodecs = mp4VideoCodecs + ",vp9"
|
|
end if
|
|
else if container = "ts"
|
|
' check for codec string before adding it
|
|
if tsVideoCodecs.Instr(0, ",vp9") = -1
|
|
tsVideoCodecs = tsVideoCodecs + ",vp9"
|
|
end if
|
|
end if
|
|
end if
|
|
end if
|
|
end for
|
|
|
|
' MPEG2
|
|
if globalUserSettings["playback.mpeg2"]
|
|
for each container in transcodingContainers
|
|
if not isStringEqual(videoCodec, "mpeg2")
|
|
if di.CanDecodeVideo({ Codec: "mpeg2", Container: container }).Result
|
|
if container = "mp4"
|
|
' check for codec string before adding it
|
|
if mp4VideoCodecs.Instr(0, ",mpeg2video") = -1
|
|
mp4VideoCodecs = mp4VideoCodecs + ",mpeg2video"
|
|
end if
|
|
else if container = "ts"
|
|
' check for codec string before adding it
|
|
if tsVideoCodecs.Instr(0, ",mpeg2video") = -1
|
|
tsVideoCodecs = tsVideoCodecs + ",mpeg2video"
|
|
end if
|
|
end if
|
|
end if
|
|
end if
|
|
end for
|
|
end if
|
|
|
|
' AV1
|
|
for each container in transcodingContainers
|
|
if not isStringEqual(videoCodec, "av1")
|
|
if di.CanDecodeVideo({ Codec: "av1", Container: container }).Result
|
|
if container = "mp4"
|
|
' check for codec string before adding it
|
|
if mp4VideoCodecs.Instr(0, ",av1") = -1
|
|
mp4VideoCodecs = mp4VideoCodecs + ",av1"
|
|
end if
|
|
else if container = "ts"
|
|
' check for codec string before adding it
|
|
if tsVideoCodecs.Instr(0, ",av1") = -1
|
|
tsVideoCodecs = tsVideoCodecs + ",av1"
|
|
end if
|
|
end if
|
|
end if
|
|
end if
|
|
end for
|
|
|
|
' AUDIO CODECS
|
|
for each container in transcodingContainers
|
|
for each codec in audioCodecs
|
|
if di.CanDecodeAudio({ Codec: codec, Container: container }).result
|
|
if container = "mp4"
|
|
mp4AudioCodecs = mp4AudioCodecs + "," + codec
|
|
else if container = "ts"
|
|
tsAudioCodecs = tsAudioCodecs + "," + codec
|
|
end if
|
|
end if
|
|
end for
|
|
end for
|
|
|
|
' add aac to TranscodingProfile for stereo audio
|
|
' NOTE: multichannel aac is not supported. only decode to stereo on some devices
|
|
transcodingProfiles.push({
|
|
"Container": "aac",
|
|
"Type": "Audio",
|
|
"AudioCodec": "aac",
|
|
"Context": "Streaming",
|
|
"Protocol": "http",
|
|
"MaxAudioChannels": "2"
|
|
})
|
|
transcodingProfiles.push({
|
|
"Container": "aac",
|
|
"Type": "Audio",
|
|
"AudioCodec": "aac",
|
|
"Context": "Static",
|
|
"Protocol": "http",
|
|
"MaxAudioChannels": "2"
|
|
})
|
|
' add mp3 to TranscodingProfile for multichannel music
|
|
transcodingProfiles.push({
|
|
"Container": "mp3",
|
|
"Type": "Audio",
|
|
"AudioCodec": "mp3",
|
|
"Context": "Streaming",
|
|
"Protocol": "http",
|
|
"MaxAudioChannels": maxAudioChannels
|
|
})
|
|
transcodingProfiles.push({
|
|
"Container": "mp3",
|
|
"Type": "Audio",
|
|
"AudioCodec": "mp3",
|
|
"Context": "Static",
|
|
"Protocol": "http",
|
|
"MaxAudioChannels": maxAudioChannels
|
|
})
|
|
|
|
tsArray = {
|
|
"Container": "ts",
|
|
"Context": "Streaming",
|
|
"Protocol": "hls",
|
|
"Type": "Video",
|
|
"AudioCodec": tsAudioCodecs,
|
|
"VideoCodec": tsVideoCodecs,
|
|
"MaxAudioChannels": maxAudioChannels,
|
|
"MinSegments": 1,
|
|
"SegmentLength": 6,
|
|
"BreakOnNonKeyFrames": false
|
|
}
|
|
mp4Array = {
|
|
"Container": "mp4",
|
|
"Context": "Streaming",
|
|
"Protocol": "hls",
|
|
"Type": "Video",
|
|
"AudioCodec": mp4AudioCodecs,
|
|
"VideoCodec": mp4VideoCodecs,
|
|
"MaxAudioChannels": maxAudioChannels,
|
|
"MinSegments": 1,
|
|
"SegmentLength": 6,
|
|
"BreakOnNonKeyFrames": false
|
|
}
|
|
|
|
' apply max res to transcoding profile
|
|
if globalUserSettings["playback.resolution.max"] <> "off"
|
|
tsArray.Conditions = [getMaxHeightArray(), getMaxWidthArray()]
|
|
mp4Array.Conditions = [getMaxHeightArray(), getMaxWidthArray()]
|
|
end if
|
|
|
|
' add user-selected preferred codec to the front of the list
|
|
if globalUserSettings["playback.preferredAudioCodec"] <> "auto"
|
|
tsArray.AudioCodec = globalUserSettings["playback.preferredAudioCodec"] + "," + tsArray.AudioCodec
|
|
mp4Array.AudioCodec = globalUserSettings["playback.preferredAudioCodec"] + "," + mp4Array.AudioCodec
|
|
end if
|
|
|
|
transcodingProfiles.push(tsArray)
|
|
transcodingProfiles.push(mp4Array)
|
|
|
|
return transcodingProfiles
|
|
end function
|
|
|
|
function getContainerProfiles() as object
|
|
containerProfiles = []
|
|
|
|
return containerProfiles
|
|
end function
|
|
|
|
function getCodecProfiles(videoCodec = string.EMPTY as string) as object
|
|
globalUserSettings = m.global.session.user.settings
|
|
codecProfiles = []
|
|
profileSupport = {
|
|
"h264": {},
|
|
"mpeg4 avc": {},
|
|
"h265": {},
|
|
"hevc": {},
|
|
"vp9": {},
|
|
"mpeg2": {},
|
|
"av1": {}
|
|
}
|
|
|
|
maxResSetting = globalUserSettings["playback.resolution.max"]
|
|
di = CreateObject("roDeviceInfo")
|
|
maxHeightArray = getMaxHeightArray()
|
|
maxWidthArray = getMaxWidthArray()
|
|
|
|
' AUDIO
|
|
' test each codec to see how many channels are supported
|
|
audioCodecs = ["aac", "mp3", "mp2", "opus", "pcm", "lpcm", "wav", "flac", "alac", "ac3", "ac4", "aiff", "dts", "wmapro", "vorbis", "eac3", "mpg123"]
|
|
|
|
' get maximum audio channels
|
|
maxAudioChannelsSetting = globalUserSettings["playback.media.maxAudioChannels"]
|
|
audioChannels = [8, 6, 2] ' highest first
|
|
|
|
if maxAudioChannelsSetting = "6"
|
|
audioChannels = [6, 2] ' highest first
|
|
end if
|
|
if maxAudioChannelsSetting = "2"
|
|
audioChannels = [2]
|
|
end if
|
|
|
|
for each audioCodec in audioCodecs
|
|
for each audioChannel in audioChannels
|
|
channelSupportFound = false
|
|
if di.CanDecodeAudio({ Codec: audioCodec, ChCnt: audioChannel }).Result
|
|
channelSupportFound = true
|
|
for each codecType in ["VideoAudio", "Audio"]
|
|
if audioCodec = "opus" and codecType = "Audio"
|
|
' opus audio files not supported by roku
|
|
else if audioCodec = "aac"
|
|
codecProfiles.push({
|
|
"Type": codecType,
|
|
"Codec": audioCodec,
|
|
"Conditions": [
|
|
{
|
|
"Condition": "LessThanEqual",
|
|
"Property": "AudioChannels",
|
|
"Value": audioChannel,
|
|
"IsRequired": true
|
|
},
|
|
{
|
|
"Condition": "NotEquals",
|
|
"Property": "AudioProfile",
|
|
"Value": "Main",
|
|
"IsRequired": true
|
|
}
|
|
]
|
|
})
|
|
else
|
|
codecProfiles.push({
|
|
"Type": codecType,
|
|
"Codec": audioCodec,
|
|
"Conditions": [
|
|
{
|
|
"Condition": "LessThanEqual",
|
|
"Property": "AudioChannels",
|
|
"Value": audioChannel,
|
|
"IsRequired": true
|
|
}
|
|
]
|
|
})
|
|
end if
|
|
end for
|
|
end if
|
|
if channelSupportFound
|
|
' if 8 channels are supported we don't need to test for 6 or 2
|
|
' if 6 channels are supported we don't need to test 2
|
|
exit for
|
|
end if
|
|
end for
|
|
end for
|
|
|
|
' check device for codec profile and level support
|
|
' AVC / h264 / MPEG4 AVC
|
|
h264Levels = ["3", "3.1", "4", "4.1", "4.2", "5", "5.1", "5.2", "6", "6.1", "6.2"]
|
|
for each profile in h264Profiles
|
|
for each level in h264Levels
|
|
if not isStringEqual(videoCodec, "h264")
|
|
if di.CanDecodeVideo({ Codec: "h264", Profile: profile, Level: level }).Result
|
|
profileSupport = updateProfileArray(profileSupport, "h264", profile, level)
|
|
end if
|
|
end if
|
|
if not isStringEqual(videoCodec, "mpeg4 avc")
|
|
if di.CanDecodeVideo({ Codec: "mpeg4 avc", Profile: profile, Level: level }).Result
|
|
profileSupport = updateProfileArray(profileSupport, "mpeg4 avc", profile, level)
|
|
end if
|
|
end if
|
|
end for
|
|
end for
|
|
|
|
' HEVC / h265
|
|
if not isStringEqual(videoCodec, "hevc")
|
|
hevcProfiles = ["main", "main 10"]
|
|
hevcLevels = ["4.1", "5.0", "5.1"]
|
|
for each profile in hevcProfiles
|
|
for each level in hevcLevels
|
|
if di.CanDecodeVideo({ Codec: "hevc", Profile: profile, Level: level }).Result
|
|
profileSupport = updateProfileArray(profileSupport, "h265", profile, level)
|
|
profileSupport = updateProfileArray(profileSupport, "hevc", profile, level)
|
|
end if
|
|
end for
|
|
end for
|
|
end if
|
|
|
|
' VP9
|
|
if not isStringEqual(videoCodec, "vp9")
|
|
vp9Profiles = ["profile 0", "profile 2"]
|
|
vp9Levels = ["4.1", "5.0", "5.1"]
|
|
for each profile in vp9Profiles
|
|
for each level in vp9Levels
|
|
if di.CanDecodeVideo({ Codec: "vp9", Profile: profile, Level: level }).Result
|
|
profileSupport = updateProfileArray(profileSupport, "vp9", profile, level)
|
|
end if
|
|
end for
|
|
end for
|
|
end if
|
|
|
|
' MPEG2
|
|
' mpeg2 uses levels with no profiles. see https://developer.roku.com/en-ca/docs/references/brightscript/interfaces/ifdeviceinfo.md#candecodevideovideo_format-as-object-as-object
|
|
' NOTE: the mpeg2 levels are being saved in the profileSupport array as if they were profiles
|
|
if not isStringEqual(videoCodec, "mpeg2")
|
|
mpeg2Levels = ["main", "high"]
|
|
for each level in mpeg2Levels
|
|
if di.CanDecodeVideo({ Codec: "mpeg2", Level: level }).Result
|
|
profileSupport = updateProfileArray(profileSupport, "mpeg2", level)
|
|
end if
|
|
end for
|
|
end if
|
|
|
|
' AV1
|
|
if not isStringEqual(videoCodec, "av1")
|
|
av1Profiles = ["main", "main 10"]
|
|
av1Levels = ["4.1", "5.0", "5.1"]
|
|
for each profile in av1Profiles
|
|
for each level in av1Levels
|
|
if di.CanDecodeVideo({ Codec: "av1", Profile: profile, Level: level }).Result
|
|
profileSupport = updateProfileArray(profileSupport, "av1", profile, level)
|
|
end if
|
|
end for
|
|
end for
|
|
end if
|
|
|
|
if not isStringEqual(videoCodec, string.EMPTY)
|
|
profileSupport.addreplace(videoCodec, {})
|
|
end if
|
|
|
|
' HDR SUPPORT
|
|
' See https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Data/Enums/VideoRangeType.cs for valid range types
|
|
h264VideoRangeTypes = "SDR|DOVIWithSDR"
|
|
hevcVideoRangeTypes = "SDR|DOVIWithSDR"
|
|
vp9VideoRangeTypes = "SDR|DOVIWithSDR"
|
|
av1VideoRangeTypes = "SDR|DOVIWithSDR"
|
|
|
|
dp = di.GetDisplayProperties()
|
|
if dp.Hdr10
|
|
hevcVideoRangeTypes = hevcVideoRangeTypes + "|HDR10|DOVIWithHDR10"
|
|
vp9VideoRangeTypes = vp9VideoRangeTypes + "|HDR10|DOVIWithHDR10"
|
|
av1VideoRangeTypes = av1VideoRangeTypes + "|HDR10|DOVIWithHDR10"
|
|
end if
|
|
if dp.Hdr10Plus
|
|
hevcVideoRangeTypes = hevcVideoRangeTypes + "|HDR10Plus|DOVIWithHDR10Plus"
|
|
vp9VideoRangeTypes = vp9VideoRangeTypes + "|HDR10Plus|DOVIWithHDR10Plus"
|
|
av1VideoRangeTypes = av1VideoRangeTypes + "|HDR10Plus|DOVIWithHDR10Plus"
|
|
end if
|
|
if dp.HLG
|
|
hevcVideoRangeTypes = hevcVideoRangeTypes + "|HLG|DOVIWithHLG"
|
|
vp9VideoRangeTypes = vp9VideoRangeTypes + "|HLG|DOVIWithHLG"
|
|
av1VideoRangeTypes = av1VideoRangeTypes + "|HLG|DOVIWithHLG"
|
|
end if
|
|
if dp.DolbyVision
|
|
h264VideoRangeTypes = h264VideoRangeTypes + "|DOVI|DOVIWithHDR10|DOVIWithHDR10Plus|DOVIWithHLG"
|
|
hevcVideoRangeTypes = hevcVideoRangeTypes + "|DOVI|DOVIWithHDR10|DOVIWithHDR10Plus|DOVIWithHLG"
|
|
'vp9VideoRangeTypes = vp9VideoRangeTypes + ",DOVI" no evidence that vp9 can hold DOVI
|
|
av1VideoRangeTypes = av1VideoRangeTypes + "|DOVI|DOVIWithHDR10|DOVIWithHDR10Plus|DOVIWithHLG"
|
|
end if
|
|
|
|
' H264
|
|
h264LevelSupported = 0.0
|
|
h264AssProfiles = {}
|
|
for each profile in profileSupport["h264"]
|
|
h264AssProfiles.AddReplace(profile, true)
|
|
if inArray(h264Profiles, profile)
|
|
h264AssProfiles.AddReplace("baseline", true)
|
|
h264AssProfiles.AddReplace("constrained baseline", true)
|
|
h264AssProfiles.AddReplace("constrainedbaseline", true)
|
|
end if
|
|
for each level in profileSupport["h264"][profile]
|
|
levelFloat = level.ToFloat()
|
|
if levelFloat > h264LevelSupported
|
|
h264LevelSupported = levelFloat
|
|
end if
|
|
end for
|
|
end for
|
|
|
|
' convert to string
|
|
h264LevelString = h264LevelSupported.ToStr()
|
|
' remove decimals
|
|
h264LevelString = removeDecimals(h264LevelString)
|
|
|
|
h264ProfileArray = {
|
|
"Type": "Video",
|
|
"Codec": "h264",
|
|
"Conditions": [
|
|
{
|
|
"Condition": "LessThanEqual",
|
|
"Property": "VideoBitDepth",
|
|
"Value": "8",
|
|
"IsRequired": false
|
|
},
|
|
{
|
|
"Condition": "EqualsAny",
|
|
"Property": "VideoProfile",
|
|
"Value": h264AssProfiles.Keys().join("|"),
|
|
"IsRequired": false
|
|
},
|
|
{
|
|
"Condition": "EqualsAny",
|
|
"Property": "VideoRangeType",
|
|
"Value": h264VideoRangeTypes,
|
|
"IsRequired": false
|
|
}
|
|
|
|
]
|
|
}
|
|
|
|
if not globalUserSettings["playback.media.directPlayAnamorphic"]
|
|
h264ProfileArray.Conditions.push({
|
|
"Condition": "NotEquals",
|
|
"Property": "IsAnamorphic",
|
|
"Value": "true",
|
|
"IsRequired": false
|
|
})
|
|
end if
|
|
|
|
' check user setting before adding video level restrictions
|
|
if not globalUserSettings["playback.tryDirect.h264ProfileLevel"]
|
|
h264ProfileArray.Conditions.push({
|
|
"Condition": "LessThanEqual",
|
|
"Property": "VideoLevel",
|
|
"Value": h264LevelString,
|
|
"IsRequired": false
|
|
})
|
|
end if
|
|
|
|
' set max resolution
|
|
if globalUserSettings["playback.resolution.mode"] = "everything" and maxResSetting <> "off"
|
|
h264ProfileArray.Conditions.push(maxHeightArray)
|
|
h264ProfileArray.Conditions.push(maxWidthArray)
|
|
end if
|
|
|
|
' set bitrate restrictions based on user settings
|
|
bitRateArray = GetBitRateLimit("h264")
|
|
if bitRateArray.count() > 0
|
|
h264ProfileArray.Conditions.push(bitRateArray)
|
|
end if
|
|
|
|
if not isStringEqual(videoCodec, "h264")
|
|
codecProfiles.push(h264ProfileArray)
|
|
end if
|
|
|
|
' MPEG2
|
|
' NOTE: the mpeg2 levels are being saved in the profileSupport array as if they were profiles
|
|
if globalUserSettings["playback.mpeg2"]
|
|
mpeg2Levels = []
|
|
for each level in profileSupport["mpeg2"]
|
|
if not inArray(mpeg2Levels, level)
|
|
mpeg2Levels.push(level)
|
|
end if
|
|
end for
|
|
|
|
mpeg2ProfileArray = {
|
|
"Type": "Video",
|
|
"Codec": "mpeg2",
|
|
"Conditions": [
|
|
{
|
|
"Condition": "EqualsAny",
|
|
"Property": "VideoLevel",
|
|
"Value": mpeg2Levels.join("|"),
|
|
"IsRequired": false
|
|
}
|
|
]
|
|
}
|
|
|
|
if not globalUserSettings["playback.media.directPlayAnamorphic"]
|
|
mpeg2ProfileArray.Conditions.push({
|
|
"Condition": "NotEquals",
|
|
"Property": "IsAnamorphic",
|
|
"Value": "true",
|
|
"IsRequired": false
|
|
})
|
|
end if
|
|
|
|
' set max resolution
|
|
if globalUserSettings["playback.resolution.mode"] = "everything" and maxResSetting <> "off"
|
|
mpeg2ProfileArray.Conditions.push(maxHeightArray)
|
|
mpeg2ProfileArray.Conditions.push(maxWidthArray)
|
|
end if
|
|
|
|
' set bitrate restrictions based on user settings
|
|
bitRateArray = GetBitRateLimit("mpeg2")
|
|
if bitRateArray.count() > 0
|
|
mpeg2ProfileArray.Conditions.push(bitRateArray)
|
|
end if
|
|
|
|
if not isStringEqual(videoCodec, "mpeg2")
|
|
codecProfiles.push(mpeg2ProfileArray)
|
|
end if
|
|
end if
|
|
|
|
if di.CanDecodeVideo({ Codec: "av1" }).Result
|
|
av1LevelSupported = 0.0
|
|
av1AssProfiles = {}
|
|
for each profile in profileSupport["av1"]
|
|
av1AssProfiles.AddReplace(profile, true)
|
|
for each level in profileSupport["av1"][profile]
|
|
levelFloat = level.ToFloat()
|
|
if levelFloat > av1LevelSupported
|
|
av1LevelSupported = levelFloat
|
|
end if
|
|
end for
|
|
end for
|
|
|
|
av1ProfileArray = {
|
|
"Type": "Video",
|
|
"Codec": "av1",
|
|
"Conditions": [
|
|
{
|
|
"Condition": "EqualsAny",
|
|
"Property": "VideoProfile",
|
|
"Value": av1AssProfiles.Keys().join("|"),
|
|
"IsRequired": false
|
|
},
|
|
{
|
|
"Condition": "EqualsAny",
|
|
"Property": "VideoRangeType",
|
|
"Value": av1VideoRangeTypes,
|
|
"IsRequired": false
|
|
},
|
|
{
|
|
"Condition": "LessThanEqual",
|
|
"Property": "VideoLevel",
|
|
"Value": (120 * av1LevelSupported).ToStr(),
|
|
"IsRequired": false
|
|
}
|
|
]
|
|
}
|
|
|
|
if not globalUserSettings["playback.media.directPlayAnamorphic"]
|
|
av1ProfileArray.Conditions.push({
|
|
"Condition": "NotEquals",
|
|
"Property": "IsAnamorphic",
|
|
"Value": "true",
|
|
"IsRequired": false
|
|
})
|
|
end if
|
|
|
|
' set max resolution
|
|
if globalUserSettings["playback.resolution.mode"] = "everything" and maxResSetting <> "off"
|
|
av1ProfileArray.Conditions.push(maxHeightArray)
|
|
av1ProfileArray.Conditions.push(maxWidthArray)
|
|
end if
|
|
|
|
' set bitrate restrictions based on user settings
|
|
bitRateArray = GetBitRateLimit("av1")
|
|
if bitRateArray.count() > 0
|
|
av1ProfileArray.Conditions.push(bitRateArray)
|
|
end if
|
|
|
|
if not isStringEqual(videoCodec, "av1")
|
|
codecProfiles.push(av1ProfileArray)
|
|
end if
|
|
end if
|
|
|
|
if not globalUserSettings["playback.compatibility.disablehevc"] and di.CanDecodeVideo({ Codec: "hevc" }).Result
|
|
hevcLevelSupported = 0.0
|
|
hevcAssProfiles = {}
|
|
|
|
for each profile in profileSupport["hevc"]
|
|
hevcAssProfiles.AddReplace(profile, true)
|
|
for each level in profileSupport["hevc"][profile]
|
|
levelFloat = level.ToFloat()
|
|
if levelFloat > hevcLevelSupported
|
|
hevcLevelSupported = levelFloat
|
|
end if
|
|
end for
|
|
end for
|
|
|
|
hevcLevelString = "120"
|
|
if hevcLevelSupported = 5.1
|
|
hevcLevelString = "153"
|
|
end if
|
|
|
|
hevcProfileArray = {
|
|
"Type": "Video",
|
|
"Codec": "hevc",
|
|
"Conditions": [
|
|
{
|
|
"Condition": "EqualsAny",
|
|
"Property": "VideoProfile",
|
|
"Value": profileSupport["hevc"].Keys().join("|"),
|
|
"IsRequired": false
|
|
},
|
|
{
|
|
"Condition": "EqualsAny",
|
|
"Property": "VideoRangeType",
|
|
"Value": hevcVideoRangeTypes,
|
|
"IsRequired": false
|
|
}
|
|
]
|
|
}
|
|
|
|
if not globalUserSettings["playback.media.directPlayAnamorphic"]
|
|
hevcProfileArray.Conditions.push({
|
|
"Condition": "NotEquals",
|
|
"Property": "IsAnamorphic",
|
|
"Value": "true",
|
|
"IsRequired": false
|
|
})
|
|
end if
|
|
|
|
' check user setting before adding VideoLevel restrictions
|
|
if not globalUserSettings["playback.tryDirect.hevcProfileLevel"]
|
|
hevcProfileArray.Conditions.push({
|
|
"Condition": "LessThanEqual",
|
|
"Property": "VideoLevel",
|
|
"Value": hevcLevelString,
|
|
"IsRequired": false
|
|
})
|
|
end if
|
|
|
|
' set max resolution
|
|
if globalUserSettings["playback.resolution.mode"] = "everything" and maxResSetting <> "off"
|
|
hevcProfileArray.Conditions.push(maxHeightArray)
|
|
hevcProfileArray.Conditions.push(maxWidthArray)
|
|
end if
|
|
|
|
' set bitrate restrictions based on user settings
|
|
bitRateArray = GetBitRateLimit("h265")
|
|
if bitRateArray.count() > 0
|
|
hevcProfileArray.Conditions.push(bitRateArray)
|
|
end if
|
|
|
|
if not isStringEqual(videoCodec, "h265")
|
|
codecProfiles.push(hevcProfileArray)
|
|
end if
|
|
end if
|
|
|
|
if di.CanDecodeVideo({ Codec: "vp9" }).Result
|
|
vp9Profiles = []
|
|
vp9LevelSupported = 0.0
|
|
|
|
for each profile in profileSupport["vp9"]
|
|
vp9Profiles.push(profile)
|
|
for each level in profileSupport["vp9"][profile]
|
|
levelFloat = level.ToFloat()
|
|
if levelFloat > vp9LevelSupported
|
|
vp9LevelSupported = levelFloat
|
|
end if
|
|
end for
|
|
end for
|
|
|
|
vp9LevelString = "120"
|
|
if vp9LevelSupported = 5.1
|
|
vp9LevelString = "153"
|
|
end if
|
|
|
|
vp9ProfileArray = {
|
|
"Type": "Video",
|
|
"Codec": "vp9",
|
|
"Conditions": [
|
|
{
|
|
"Condition": "EqualsAny",
|
|
"Property": "VideoProfile",
|
|
"Value": vp9Profiles.join("|"),
|
|
"IsRequired": false
|
|
},
|
|
{
|
|
"Condition": "EqualsAny",
|
|
"Property": "VideoRangeType",
|
|
"Value": vp9VideoRangeTypes,
|
|
"IsRequired": false
|
|
},
|
|
{
|
|
"Condition": "LessThanEqual",
|
|
"Property": "VideoLevel",
|
|
"Value": vp9LevelString,
|
|
"IsRequired": false
|
|
}
|
|
]
|
|
}
|
|
|
|
if not globalUserSettings["playback.media.directPlayAnamorphic"]
|
|
vp9ProfileArray.Conditions.push({
|
|
"Condition": "NotEquals",
|
|
"Property": "IsAnamorphic",
|
|
"Value": "true",
|
|
"IsRequired": false
|
|
})
|
|
end if
|
|
|
|
' set max resolution
|
|
if globalUserSettings["playback.resolution.mode"] = "everything" and maxResSetting <> "off"
|
|
vp9ProfileArray.Conditions.push(maxHeightArray)
|
|
vp9ProfileArray.Conditions.push(maxWidthArray)
|
|
end if
|
|
|
|
' set bitrate restrictions based on user settings
|
|
bitRateArray = GetBitRateLimit("vp9")
|
|
if bitRateArray.count() > 0
|
|
vp9ProfileArray.Conditions.push(bitRateArray)
|
|
end if
|
|
|
|
if not isStringEqual(videoCodec, "vp9")
|
|
codecProfiles.push(vp9ProfileArray)
|
|
end if
|
|
end if
|
|
|
|
return codecProfiles
|
|
end function
|
|
|
|
function getSubtitleProfiles() as object
|
|
subtitleProfiles = []
|
|
|
|
subtitleProfiles.push({
|
|
"Format": "vtt",
|
|
"Method": "External"
|
|
})
|
|
subtitleProfiles.push({
|
|
"Format": "srt",
|
|
"Method": "External"
|
|
})
|
|
subtitleProfiles.push({
|
|
"Format": "ttml",
|
|
"Method": "External"
|
|
})
|
|
subtitleProfiles.push({
|
|
"Format": "sub",
|
|
"Method": "External"
|
|
})
|
|
|
|
return subtitleProfiles
|
|
end function
|
|
|
|
function GetBitRateLimit(codec as string) as object
|
|
globalUserSettings = m.global.session.user.settings
|
|
if globalUserSettings["playback.bitrate.maxlimited"]
|
|
userSetLimit = globalUserSettings["playback.bitrate.limit"].ToInt()
|
|
if isValid(userSetLimit) and type(userSetLimit) = "Integer" and userSetLimit > 0
|
|
userSetLimit *= 1000000
|
|
return {
|
|
"Condition": "LessThanEqual",
|
|
"Property": "VideoBitrate",
|
|
"Value": userSetLimit.ToStr(),
|
|
"IsRequired": true
|
|
}
|
|
else
|
|
codec = Lcase(codec)
|
|
' Some repeated values (e.g. same "40mbps" for several codecs)
|
|
' but this makes it easy to update in the future if the bitrates start to deviate.
|
|
if codec = "h264"
|
|
' Roku only supports h264 up to 10Mpbs
|
|
return {
|
|
"Condition": "LessThanEqual",
|
|
"Property": "VideoBitrate",
|
|
"Value": "10000000",
|
|
"IsRequired": true
|
|
}
|
|
else if codec = "av1"
|
|
' Roku only supports AV1 up to 40Mpbs
|
|
return {
|
|
"Condition": "LessThanEqual",
|
|
"Property": "VideoBitrate",
|
|
"Value": "40000000",
|
|
"IsRequired": true
|
|
}
|
|
else if codec = "h265"
|
|
' Roku only supports h265 up to 40Mpbs
|
|
return {
|
|
"Condition": "LessThanEqual",
|
|
"Property": "VideoBitrate",
|
|
"Value": "40000000",
|
|
"IsRequired": true
|
|
}
|
|
else if codec = "vp9"
|
|
' Roku only supports VP9 up to 40Mpbs
|
|
return {
|
|
"Condition": "LessThanEqual",
|
|
"Property": "VideoBitrate",
|
|
"Value": "40000000",
|
|
"IsRequired": true
|
|
}
|
|
end if
|
|
end if
|
|
end if
|
|
return {}
|
|
end function
|
|
|
|
function getMaxHeightArray() as object
|
|
myGlobal = m.global
|
|
|
|
maxResSetting = myGlobal.session.user.settings["playback.resolution.max"]
|
|
if maxResSetting = "off" then return {}
|
|
|
|
maxVideoHeight = maxResSetting
|
|
|
|
if maxResSetting = "auto"
|
|
maxVideoHeight = myGlobal.device.videoHeight
|
|
end if
|
|
|
|
return {
|
|
"Condition": "LessThanEqual",
|
|
"Property": "Height",
|
|
"Value": maxVideoHeight,
|
|
"IsRequired": true
|
|
}
|
|
end function
|
|
|
|
function getMaxWidthArray() as object
|
|
myGlobal = m.global
|
|
|
|
maxResSetting = myGlobal.session.user.settings["playback.resolution.max"]
|
|
if maxResSetting = "off" then return {}
|
|
|
|
maxVideoWidth = invalid
|
|
|
|
if maxResSetting = "auto"
|
|
maxVideoWidth = myGlobal.device.videoWidth
|
|
else if maxResSetting = "360"
|
|
maxVideoWidth = "480"
|
|
else if maxResSetting = "480"
|
|
maxVideoWidth = "640"
|
|
else if maxResSetting = "720"
|
|
maxVideoWidth = "1280"
|
|
else if maxResSetting = "1080"
|
|
maxVideoWidth = "1920"
|
|
else if maxResSetting = "2160"
|
|
maxVideoWidth = "3840"
|
|
else if maxResSetting = "4320"
|
|
maxVideoWidth = "7680"
|
|
end if
|
|
|
|
return {
|
|
"Condition": "LessThanEqual",
|
|
"Property": "Width",
|
|
"Value": maxVideoWidth,
|
|
"IsRequired": true
|
|
}
|
|
end function
|
|
|
|
' Recieves and returns an assArray of supported profiles and levels for each video codec
|
|
function updateProfileArray(profileArray as object, videoCodec as string, videoProfile as string, profileLevel = "" as string) as object
|
|
' validate params
|
|
if profileArray = invalid then return {}
|
|
if videoCodec = "" or videoProfile = "" then return profileArray
|
|
|
|
if profileArray[videoCodec] = invalid
|
|
profileArray[videoCodec] = {}
|
|
end if
|
|
|
|
if profileArray[videoCodec][videoProfile] = invalid
|
|
profileArray[videoCodec][videoProfile] = {}
|
|
end if
|
|
|
|
' add profileLevel if a value was provided
|
|
if profileLevel <> ""
|
|
if profileArray[videoCodec][videoProfile][profileLevel] = invalid
|
|
profileArray[videoCodec][videoProfile].AddReplace(profileLevel, true)
|
|
end if
|
|
end if
|
|
|
|
return profileArray
|
|
end function
|
|
|
|
' Remove all decimals from a string
|
|
function removeDecimals(value as string) as string
|
|
r = CreateObject("roRegex", "\.", "")
|
|
value = r.ReplaceAll(value, "")
|
|
return value
|
|
end function
|
|
|
|
' Takes and returns a comma delimited string of codecs.
|
|
' Moves the preferred codec to the front of the string
|
|
function setPreferredCodec(codecString as string, preferredCodec as string) as string
|
|
if preferredCodec = "" then return ""
|
|
if codecString = "" then return preferredCodec
|
|
|
|
preferredCodecSize = Len(preferredCodec)
|
|
|
|
' is the codec already in front?
|
|
if Left(codecString, preferredCodecSize) = preferredCodec
|
|
return codecString
|
|
else
|
|
' convert string to array
|
|
codecArray = codecString.Split(",")
|
|
' remove preferred codec from array
|
|
newArray = []
|
|
for each codec in codecArray
|
|
if codec <> preferredCodec
|
|
newArray.push(codec)
|
|
end if
|
|
end for
|
|
' convert newArray to string
|
|
newCodecString = newArray.Join(",")
|
|
' add preferred codec to front of newCodecString
|
|
newCodecString = preferredCodec + "," + newCodecString
|
|
|
|
return newCodecString
|
|
end if
|
|
end function
|