2023-11-11 13:41:20 +00:00
<!DOCTYPE html> < html lang = "en" style = "font-size:16px" > < head > < meta charset = "utf-8" > < meta name = "viewport" content = "width=device-width,initial-scale=1" > < title > Source: source/utils/deviceCapabilities.bs< / title > <!-- [if lt IE 9]>
< script src = "//html5shiv.googlecode.com/svn/trunk/html5.js" > < / script >
2023-12-05 16:56:00 +00:00
<![endif]--> < script src = "scripts/third-party/hljs.js" defer = "defer" > < / script > < script src = "scripts/third-party/hljs-line-num.js" defer = "defer" > < / script > < script src = "scripts/third-party/popper.js" defer = "defer" > < / script > < script src = "scripts/third-party/tippy.js" defer = "defer" > < / script > < script src = "scripts/third-party/tocbot.min.js" > < / script > < script > var baseURL = "/" , locationPathname = "" ; baseURL = ( baseURL = ( baseURL = "https://jellyfin.github.io/jellyfin-roku/" ) . replace ( /https?:\/\//i , "" ) ) . substr ( baseURL . indexOf ( "/" ) ) < / script > < link rel = "stylesheet" href = "styles/clean-jsdoc-theme.min.css" > < svg aria-hidden = "true" version = "1.1" xmlns = "http://www.w3.org/2000/svg" xmlns:xlink = "http://www.w3.org/1999/xlink" style = "display:none" > < defs > < symbol id = "copy-icon" viewbox = "0 0 488.3 488.3" > < g > < path d = "M314.25,85.4h-227c-21.3,0-38.6,17.3-38.6,38.6v325.7c0,21.3,17.3,38.6,38.6,38.6h227c21.3,0,38.6-17.3,38.6-38.6V124 C352.75,102.7,335.45,85.4,314.25,85.4z M325.75,449.6c0,6.4-5.2,11.6-11.6,11.6h-227c-6.4,0-11.6-5.2-11.6-11.6V124 c0-6.4,5.2-11.6,11.6-11.6h227c6.4,0,11.6,5.2,11.6,11.6V449.6z" / > < path d = "M401.05,0h-227c-21.3,0-38.6,17.3-38.6,38.6c0,7.5,6,13.5,13.5,13.5s13.5-6,13.5-13.5c0-6.4,5.2-11.6,11.6-11.6h227 c6.4,0,11.6,5.2,11.6,11.6v325.7c0,6.4-5.2,11.6-11.6,11.6c-7.5,0-13.5,6-13.5,13.5s6,13.5,13.5,13.5c21.3,0,38.6-17.3,38.6-38.6 V38.6C439.65,17.3,422.35,0,401.05,0z" / > < / g > < / symbol > < symbol id = "search-icon" viewBox = "0 0 512 512" > < g > < g > < path d = "M225.474,0C101.151,0,0,101.151,0,225.474c0,124.33,101.151,225.474,225.474,225.474 c124.33,0,225.474-101.144,225.474-225.474C450.948,101.151,349.804,0,225.474,0z M225.474,409.323 c-101.373,0-183.848-82.475-183.848-183.848S124.101,41.626,225.474,41.626s183.848,82.475,183.848,183.848 S326.847,409.323,225.474,409.323z" / > < / g > < / g > < g > < g > < path d = "M505.902,476.472L386.574,357.144c-8.131-8.131-21.299-8.131-29.43,0c-8.131,8.124-8.131,21.306,0,29.43l119.328,119.328 c4.065,4.065,9.387,6.098,14.715,6.098c5.321,0,10.649-2.033,14.715-6.098C514.033,497.778,514.033,484.596,505.902,476.472z" / > < / g > < / g > < / symbol > < symbol id = "font-size-icon" viewBox = "0 0 24 24" > < path fill = "none" d = "M0 0h24v24H0z" / > < path d = "M11.246 15H4.754l-2 5H.6L7 4h2l6.4 16h-2.154l-2-5zm-.8-2L8 6.885 5.554 13h4.892zM21 12.535V12h2v8h-2v-.535a4 4 0 1 1 0-6.93zM19 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z" / > < / symbol > < symbol id = "add-icon" viewBox = "0 0 24 24" > < path fill = "none" d = "M0 0h24v24H0z" / > < path d = "M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z" / > < / symbol > < symbol id = "minus-icon" viewBox = "0 0 24 24" > < path fill = "none" d = "M0 0h24v24H0z" / > < path d = "M5 11h14v2H5z" / > < / symbol > < symbol id = "dark-theme-icon" viewBox = "0 0 24 24" > < path fill = "none" d = "M0 0h24v24H0z" / > < path d = "M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z" / > < / symbol > < symbol id = "light-theme-icon" viewBox = "0 0 24 24" > < path fill = "none" d = "M0 0h24v24H0z" / > < path d = "M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z" / > < / symbol > < symbol id = "reset-icon" viewBox = "0 0 24 24" > < path fill = "none" d = "M0 0h24v24H0z" / > < path d = "M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z" / > < / symbol > < symbol id = "down-icon" viewBox = "0 0 16 16" > < path fill-rule = "evenodd" clip-rule = "evenodd" d = "M12.7803 6.21967C13.0732 6.51256 13.0732 6.98744 12.7803 7.28033L8.53033 11.5303C8.23744 11.8232 7.76256 11.8232 7.46967 11.5303L3.21967 7.28033C2.92678 6.98744 2.92678 6.51256 3.21967 6.21967C3.51256 5.92678 3.98744 5.92678 4.28033 6.21967L8 9.93934L11.7197 6.21967C12.0126 5.92678 12.4874 5.92678 12.7803 6.21967Z" > < / path > < / symbol > < symbol id = "codepen-icon" viewBox = "0 0 24 24" > < path fill = "none" d =
2023-11-11 02:08:52 +00:00
import "pkg:/source/api/baserequest.bs"
2023-10-06 03:18:36 +00:00
2023-11-11 00:30:32 +00:00
' Returns the Device Capabilities for Roku.
' Also prints out the device profile for debugging
2023-10-06 03:18:36 +00:00
function getDeviceCapabilities() as object
2023-11-11 00:30:32 +00:00
deviceProfile = {
2023-10-06 03:18:36 +00:00
"PlayableMediaTypes": [
"Audio",
"Video",
"Photo"
],
"SupportedCommands": [],
"SupportsPersistentIdentifier": true,
"SupportsMediaControl": false,
"SupportsContentUploading": false,
"SupportsSync": false,
"DeviceProfile": getDeviceProfile(),
"AppStoreUrl": "https://channelstore.roku.com/details/cc5e559d08d9ec87c5f30dcebdeebc12/jellyfin"
}
2023-11-11 00:30:32 +00:00
printDeviceProfile(deviceProfile)
return deviceProfile
end function
2023-10-06 03:18:36 +00:00
function getDeviceProfile() as object
2023-11-04 19:11:14 +00:00
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(),
"ContainerProfiles": getContainerProfiles(),
"CodecProfiles": getCodecProfiles(),
"SubtitleProfiles": getSubtitleProfiles()
}
end function
function GetDirectPlayProfiles() as object
globalUserSettings = m.global.session.user.settings
directPlayProfiles = []
2023-10-06 03:18:36 +00:00
di = CreateObject("roDeviceInfo")
2023-11-04 19:11:14 +00:00
' 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
2023-10-06 03:18:36 +00:00
2023-11-04 19:11:14 +00:00
' 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
for each container in supportedCodecs
for each audioCodec in audioCodecs
if di.CanDecodeAudio({ Codec: audioCodec, Container: container }).Result
supportedCodecs[container]["audio"].push(audioCodec)
end if
end for
end for
2023-11-05 20:36:11 +00:00
' 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 = ["aac", "mp3", "mp2", "pcm", "lpcm", "wav", "ac3", "ac4", "aiff", "wma", "flac", "alac", "aac", "dts", "wmapro", "vorbis", "eac3", "mpg123"]
2023-11-04 19:11:14 +00:00
' 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() as object
globalUserSettings = m.global.session.user.settings
transcodingProfiles = []
di = CreateObject("roDeviceInfo")
transcodingContainers = ["mp4", "ts"]
2023-10-06 03:18:36 +00:00
' use strings to preserve order
mp4AudioCodecs = "aac"
mp4VideoCodecs = "h264"
tsAudioCodecs = "aac"
tsVideoCodecs = "h264"
' does the users setup support surround sound?
maxAudioChannels = "2" ' jellyfin expects this as a string
' in order of preference from left to right
2023-11-04 19:11:14 +00:00
audioCodecs = ["mp3", "vorbis", "opus", "flac", "alac", "ac4", "pcm", "wma", "wmapro"]
2023-10-06 03:18:36 +00:00
surroundSoundCodecs = ["eac3", "ac3", "dts"]
2023-11-04 19:11:14 +00:00
if globalUserSettings["playback.forceDTS"] = true
2023-10-06 03:18:36 +00:00
surroundSoundCodecs = ["dts", "eac3", "ac3"]
end if
2023-11-04 19:11:14 +00:00
surroundSoundCodec = invalid
2023-10-06 03:18:36 +00:00
if di.GetAudioOutputChannel() = "5.1 surround"
maxAudioChannels = "6"
for each codec in surroundSoundCodecs
if di.CanDecodeAudio({ Codec: codec, ChCnt: 6 }).Result
2023-11-04 19:11:14 +00:00
surroundSoundCodec = codec
2023-10-06 03:18:36 +00:00
if di.CanDecodeAudio({ Codec: codec, ChCnt: 8 }).Result
maxAudioChannels = "8"
end if
exit for
end if
end for
end if
' VIDEO CODECS
'
' AVC / h264 / MPEG4 AVC
2023-11-04 19:11:14 +00:00
for each container in transcodingContainers
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"
2023-10-06 03:18:36 +00:00
end if
2023-11-04 19:11:14 +00:00
else if container = "ts"
' check for codec string before adding it
if tsVideoCodecs.Instr(0, ",h264") = -1
tsVideoCodecs = tsVideoCodecs + ",h264"
2023-10-06 03:18:36 +00:00
end if
2023-11-04 19:11:14 +00:00
end if
end if
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
2023-10-06 03:18:36 +00:00
end for
2023-11-04 19:11:14 +00:00
' HEVC / h265
if globalUserSettings["playback.compatibility.disablehevc"] = false
for each container in transcodingContainers
if di.CanDecodeVideo({ Codec: "hevc", Container: container }).Result
2023-10-06 03:18:36 +00:00
if container = "mp4"
' check for codec string before adding it
2023-11-04 19:11:14 +00:00
if mp4VideoCodecs.Instr(0, "h265,") = -1
mp4VideoCodecs = "h265," + mp4VideoCodecs
end if
if mp4VideoCodecs.Instr(0, "hevc,") = -1
mp4VideoCodecs = "hevc," + mp4VideoCodecs
2023-10-06 03:18:36 +00:00
end if
else if container = "ts"
' check for codec string before adding it
2023-11-04 19:11:14 +00:00
if tsVideoCodecs.Instr(0, "h265,") = -1
tsVideoCodecs = "h265," + tsVideoCodecs
end if
if tsVideoCodecs.Instr(0, "hevc,") = -1
tsVideoCodecs = "hevc," + tsVideoCodecs
2023-10-06 03:18:36 +00:00
end if
end if
end if
end for
2023-11-04 19:11:14 +00:00
end if
' VP9
for each container in transcodingContainers
if di.CanDecodeAudio({ 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
2023-10-06 03:18:36 +00:00
end for
' MPEG2
2023-11-04 19:11:14 +00:00
if globalUserSettings["playback.mpeg2"]
for each container in transcodingContainers
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"
2023-10-06 03:18:36 +00:00
end if
end if
2023-11-04 19:11:14 +00:00
end if
2023-10-06 03:18:36 +00:00
end for
end if
' AV1
2023-11-04 19:11:14 +00:00
for each container in transcodingContainers
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 for
2023-10-06 03:18:36 +00:00
' AUDIO CODECS
2023-11-04 19:11:14 +00:00
for each container in transcodingContainers
2023-10-06 03:18:36 +00:00
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
2023-11-05 20:36:11 +00:00
' add aac to TranscodingProfile for stereo audio
' NOTE: multichannel aac is not supported. only decode to stereo on some devices
2023-11-04 19:11:14 +00:00
transcodingProfiles.push({
2023-11-05 20:36:11 +00:00
"Container": "aac",
2023-10-06 03:18:36 +00:00
"Type": "Audio",
2023-11-05 20:36:11 +00:00
"AudioCodec": "aac",
2023-10-06 03:18:36 +00:00
"Context": "Streaming",
"Protocol": "http",
2023-11-05 20:36:11 +00:00
"MaxAudioChannels": "2"
2023-10-06 03:18:36 +00:00
})
2023-11-04 19:11:14 +00:00
transcodingProfiles.push({
2023-11-05 20:36:11 +00:00
"Container": "aac",
2023-10-06 03:18:36 +00:00
"Type": "Audio",
2023-11-05 20:36:11 +00:00
"AudioCodec": "aac",
2023-10-06 03:18:36 +00:00
"Context": "Static",
"Protocol": "http",
2023-11-05 20:36:11 +00:00
"MaxAudioChannels": "2"
2023-10-06 03:18:36 +00:00
})
2023-11-05 20:36:11 +00:00
' add mp3 to TranscodingProfile for multichannel music
2023-11-04 19:11:14 +00:00
transcodingProfiles.push({
2023-11-05 20:36:11 +00:00
"Container": "mp3",
2023-10-06 03:18:36 +00:00
"Type": "Audio",
2023-11-05 20:36:11 +00:00
"AudioCodec": "mp3",
2023-10-06 03:18:36 +00:00
"Context": "Streaming",
"Protocol": "http",
2023-11-05 20:36:11 +00:00
"MaxAudioChannels": maxAudioChannels
2023-10-06 03:18:36 +00:00
})
2023-11-04 19:11:14 +00:00
transcodingProfiles.push({
2023-11-05 20:36:11 +00:00
"Container": "mp3",
2023-10-06 03:18:36 +00:00
"Type": "Audio",
2023-11-05 20:36:11 +00:00
"AudioCodec": "mp3",
2023-10-06 03:18:36 +00:00
"Context": "Static",
"Protocol": "http",
2023-11-05 20:36:11 +00:00
"MaxAudioChannels": maxAudioChannels
2023-10-06 03:18:36 +00:00
})
tsArray = {
"Container": "ts",
"Context": "Streaming",
"Protocol": "hls",
"Type": "Video",
"AudioCodec": tsAudioCodecs,
"VideoCodec": tsVideoCodecs,
"MaxAudioChannels": maxAudioChannels,
"MinSegments": 1,
"BreakOnNonKeyFrames": false
}
mp4Array = {
"Container": "mp4",
"Context": "Streaming",
"Protocol": "hls",
"Type": "Video",
"AudioCodec": mp4AudioCodecs,
"VideoCodec": mp4VideoCodecs,
"MaxAudioChannels": maxAudioChannels,
"MinSegments": 1,
"BreakOnNonKeyFrames": false
}
2023-11-04 19:11:14 +00:00
' apply max res to transcoding profile
if globalUserSettings["playback.resolution.max"] < > "off"
tsArray.Conditions = [getMaxHeightArray(), getMaxWidthArray()]
mp4Array.Conditions = [getMaxHeightArray(), getMaxWidthArray()]
end if
' surround sound
if surroundSoundCodec < > invalid
' add preferred surround sound codec to TranscodingProfile
transcodingProfiles.push({
"Container": surroundSoundCodec,
"Type": "Audio",
"AudioCodec": surroundSoundCodec,
"Context": "Streaming",
"Protocol": "http",
"MaxAudioChannels": maxAudioChannels
})
transcodingProfiles.push({
"Container": surroundSoundCodec,
"Type": "Audio",
"AudioCodec": surroundSoundCodec,
"Context": "Static",
"Protocol": "http",
"MaxAudioChannels": maxAudioChannels
})
' put codec in front of AudioCodec string
if tsArray.AudioCodec = ""
tsArray.AudioCodec = surroundSoundCodec
else
tsArray.AudioCodec = surroundSoundCodec + "," + tsArray.AudioCodec
end if
if mp4Array.AudioCodec = ""
mp4Array.AudioCodec = surroundSoundCodec
else
mp4Array.AudioCodec = surroundSoundCodec + "," + mp4Array.AudioCodec
end if
end if
transcodingProfiles.push(tsArray)
transcodingProfiles.push(mp4Array)
return transcodingProfiles
end function
function getContainerProfiles() as object
containerProfiles = []
return containerProfiles
end function
function getCodecProfiles() 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"]
audioChannels = [8, 6, 2] ' highest first
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"]
2023-11-05 20:36:11 +00:00
if audioCodec = "opus" and codecType = "Audio"
' opus audio files not supported by roku
else
codecProfiles.push({
"Type": codecType,
"Codec": audioCodec,
"Conditions": [
{
"Condition": "LessThanEqual",
"Property": "AudioChannels",
"Value": audioChannel,
"IsRequired": true
}
]
})
end if
2023-11-04 19:11:14 +00:00
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
h264Profiles = ["main", "high"]
h264Levels = ["4.1", "4.2"]
for each profile in h264Profiles
for each level in h264Levels
if di.CanDecodeVideo({ Codec: "h264", Profile: profile, Level: level }).Result
profileSupport = updateProfileArray(profileSupport, "h264", profile, level)
end if
if di.CanDecodeVideo({ Codec: "mpeg4 avc", Profile: profile, Level: level }).Result
profileSupport = updateProfileArray(profileSupport, "mpeg4 avc", profile, level)
end if
end for
end for
' HEVC / h265
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
' 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
2023-10-06 03:18:36 +00:00
2023-11-04 19:11:14 +00:00
' 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
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
2023-10-06 03:18:36 +00:00
2023-11-04 19:11:14 +00:00
' 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
2023-10-06 03:18:36 +00:00
2023-11-04 19:11:14 +00:00
' HDR SUPPORT
h264VideoRangeTypes = "SDR"
hevcVideoRangeTypes = "SDR"
vp9VideoRangeTypes = "SDR"
av1VideoRangeTypes = "SDR"
2023-10-06 03:18:36 +00:00
2023-11-04 19:11:14 +00:00
dp = di.GetDisplayProperties()
if dp.Hdr10
hevcVideoRangeTypes = hevcVideoRangeTypes + "|HDR10"
vp9VideoRangeTypes = vp9VideoRangeTypes + "|HDR10"
av1VideoRangeTypes = av1VideoRangeTypes + "|HDR10"
end if
if dp.Hdr10Plus
av1VideoRangeTypes = av1VideoRangeTypes + "|HDR10+"
end if
if dp.HLG
hevcVideoRangeTypes = hevcVideoRangeTypes + "|HLG"
vp9VideoRangeTypes = vp9VideoRangeTypes + "|HLG"
av1VideoRangeTypes = av1VideoRangeTypes + "|HLG"
end if
if dp.DolbyVision
h264VideoRangeTypes = h264VideoRangeTypes + "|DOVI"
hevcVideoRangeTypes = hevcVideoRangeTypes + "|DOVI"
'vp9VideoRangeTypes = vp9VideoRangeTypes + ",DOVI" no evidence that vp9 can hold DOVI
av1VideoRangeTypes = av1VideoRangeTypes + "|DOVI"
end if
2023-10-06 03:18:36 +00:00
' H264
2023-11-04 19:11:14 +00:00
h264LevelSupported = 0.0
2023-10-06 03:18:36 +00:00
h264AssProfiles = {}
2023-11-04 19:11:14 +00:00
for each profile in profileSupport["h264"]
h264AssProfiles.AddReplace(profile, true)
for each level in profileSupport["h264"][profile]
levelFloat = level.ToFloat()
if levelFloat > h264LevelSupported
h264LevelSupported = levelFloat
end if
2023-10-06 03:18:36 +00:00
end for
end for
' convert to string
2023-11-04 19:11:14 +00:00
h264LevelString = h264LevelSupported.ToStr()
2023-10-06 03:18:36 +00:00
' remove decimals
h264LevelString = removeDecimals(h264LevelString)
2023-11-04 19:11:14 +00:00
h264ProfileArray = {
2023-10-06 03:18:36 +00:00
"Type": "Video",
"Codec": "h264",
"Conditions": [
{
"Condition": "NotEquals",
"Property": "IsAnamorphic",
"Value": "true",
"IsRequired": false
},
2023-11-04 19:11:14 +00:00
{
"Condition": "LessThanEqual",
"Property": "VideoBitDepth",
"Value": "8",
"IsRequired": false
},
2023-10-06 03:18:36 +00:00
{
"Condition": "EqualsAny",
"Property": "VideoProfile",
"Value": h264AssProfiles.Keys().join("|"),
"IsRequired": false
},
{
"Condition": "EqualsAny",
"Property": "VideoRangeType",
"Value": h264VideoRangeTypes,
"IsRequired": false
}
]
}
2023-11-04 19:11:14 +00:00
2023-10-06 03:18:36 +00:00
' check user setting before adding video level restrictions
2023-11-04 19:11:14 +00:00
if not globalUserSettings["playback.tryDirect.h264ProfileLevel"]
h264ProfileArray.Conditions.push({
2023-10-06 03:18:36 +00:00
"Condition": "LessThanEqual",
"Property": "VideoLevel",
"Value": h264LevelString,
"IsRequired": false
})
end if
2023-11-04 19:11:14 +00:00
' set max resolution
if globalUserSettings["playback.resolution.mode"] = "everything" and maxResSetting < > "off"
h264ProfileArray.Conditions.push(maxHeightArray)
h264ProfileArray.Conditions.push(maxWidthArray)
end if
2023-10-06 03:18:36 +00:00
' set bitrate restrictions based on user settings
bitRateArray = GetBitRateLimit("h264")
if bitRateArray.count() > 0
2023-11-04 19:11:14 +00:00
h264ProfileArray.Conditions.push(bitRateArray)
2023-10-06 03:18:36 +00:00
end if
2023-11-04 19:11:14 +00:00
codecProfiles.push(h264ProfileArray)
2023-10-06 03:18:36 +00:00
' MPEG2
' NOTE: the mpeg2 levels are being saved in the profileSupport array as if they were profiles
2023-11-04 19:11:14 +00:00
if globalUserSettings["playback.mpeg2"]
2023-10-06 03:18:36 +00:00
mpeg2Levels = []
2023-11-04 19:11:14 +00:00
for each level in profileSupport["mpeg2"]
if not arrayHasValue(mpeg2Levels, level)
mpeg2Levels.push(level)
end if
2023-10-06 03:18:36 +00:00
end for
2023-11-04 19:11:14 +00:00
mpeg2ProfileArray = {
2023-10-06 03:18:36 +00:00
"Type": "Video",
"Codec": "mpeg2",
"Conditions": [
{
"Condition": "EqualsAny",
"Property": "VideoLevel",
"Value": mpeg2Levels.join("|"),
"IsRequired": false
}
]
}
' set max resolution
2023-11-04 19:11:14 +00:00
if globalUserSettings["playback.resolution.mode"] = "everything" and maxResSetting < > "off"
mpeg2ProfileArray.Conditions.push(maxHeightArray)
mpeg2ProfileArray.Conditions.push(maxWidthArray)
2023-10-06 03:18:36 +00:00
end if
' set bitrate restrictions based on user settings
bitRateArray = GetBitRateLimit("mpeg2")
if bitRateArray.count() > 0
2023-11-04 19:11:14 +00:00
mpeg2ProfileArray.Conditions.push(bitRateArray)
2023-10-06 03:18:36 +00:00
end if
2023-11-04 19:11:14 +00:00
codecProfiles.push(mpeg2ProfileArray)
2023-10-06 03:18:36 +00:00
end if
2023-11-04 19:11:14 +00:00
if di.CanDecodeVideo({ Codec: "av1" }).Result
av1LevelSupported = 0.0
2023-10-27 02:20:25 +00:00
av1AssProfiles = {}
2023-11-04 19:11:14 +00:00
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
2023-10-06 03:18:36 +00:00
end for
end for
2023-11-04 19:11:14 +00:00
av1ProfileArray = {
2023-10-06 03:18:36 +00:00
"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",
2023-11-04 19:11:14 +00:00
"Value": (120 * av1LevelSupported).ToStr(),
2023-10-06 03:18:36 +00:00
"IsRequired": false
}
]
}
' set max resolution
2023-11-04 19:11:14 +00:00
if globalUserSettings["playback.resolution.mode"] = "everything" and maxResSetting < > "off"
av1ProfileArray.Conditions.push(maxHeightArray)
av1ProfileArray.Conditions.push(maxWidthArray)
2023-10-06 03:18:36 +00:00
end if
' set bitrate restrictions based on user settings
bitRateArray = GetBitRateLimit("av1")
if bitRateArray.count() > 0
2023-11-04 19:11:14 +00:00
av1ProfileArray.Conditions.push(bitRateArray)
2023-10-06 03:18:36 +00:00
end if
2023-11-04 19:11:14 +00:00
codecProfiles.push(av1ProfileArray)
2023-10-06 03:18:36 +00:00
end if
2023-11-04 19:11:14 +00:00
if not globalUserSettings["playback.compatibility.disablehevc"] and di.CanDecodeVideo({ Codec: "hevc" }).Result
hevcLevelSupported = 0.0
2023-10-06 03:18:36 +00:00
hevcAssProfiles = {}
2023-11-04 19:11:14 +00:00
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
2023-10-06 03:18:36 +00:00
end for
end for
hevcLevelString = "120"
2023-11-04 19:11:14 +00:00
if hevcLevelSupported = 5.1
2023-10-06 03:18:36 +00:00
hevcLevelString = "153"
end if
2023-11-04 19:11:14 +00:00
hevcProfileArray = {
2023-10-06 03:18:36 +00:00
"Type": "Video",
"Codec": "hevc",
"Conditions": [
{
"Condition": "NotEquals",
"Property": "IsAnamorphic",
"Value": "true",
"IsRequired": false
},
{
"Condition": "EqualsAny",
"Property": "VideoProfile",
2023-11-04 19:11:14 +00:00
"Value": profileSupport["hevc"].Keys().join("|"),
2023-10-06 03:18:36 +00:00
"IsRequired": false
},
{
"Condition": "EqualsAny",
"Property": "VideoRangeType",
"Value": hevcVideoRangeTypes,
"IsRequired": false
}
]
}
' check user setting before adding VideoLevel restrictions
2023-11-04 19:11:14 +00:00
if not globalUserSettings["playback.tryDirect.hevcProfileLevel"]
hevcProfileArray.Conditions.push({
2023-10-06 03:18:36 +00:00
"Condition": "LessThanEqual",
"Property": "VideoLevel",
"Value": hevcLevelString,
"IsRequired": false
})
end if
2023-11-04 19:11:14 +00:00
' set max resolution
if globalUserSettings["playback.resolution.mode"] = "everything" and maxResSetting < > "off"
hevcProfileArray.Conditions.push(maxHeightArray)
hevcProfileArray.Conditions.push(maxWidthArray)
end if
2023-10-06 03:18:36 +00:00
' set bitrate restrictions based on user settings
bitRateArray = GetBitRateLimit("h265")
if bitRateArray.count() > 0
2023-11-04 19:11:14 +00:00
hevcProfileArray.Conditions.push(bitRateArray)
2023-10-06 03:18:36 +00:00
end if
2023-11-04 19:11:14 +00:00
codecProfiles.push(hevcProfileArray)
2023-10-06 03:18:36 +00:00
end if
2023-11-04 19:11:14 +00:00
if di.CanDecodeVideo({ Codec: "vp9" }).Result
2023-10-06 03:18:36 +00:00
vp9Profiles = []
2023-11-04 19:11:14 +00:00
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
2023-10-06 03:18:36 +00:00
end if
end for
end for
2023-11-04 19:11:14 +00:00
vp9LevelString = "120"
if vp9LevelSupported = 5.1
vp9LevelString = "153"
end if
vp9ProfileArray = {
2023-10-06 03:18:36 +00:00
"Type": "Video",
"Codec": "vp9",
"Conditions": [
{
"Condition": "EqualsAny",
2023-11-04 19:11:14 +00:00
"Property": "VideoProfile",
2023-10-06 03:18:36 +00:00
"Value": vp9Profiles.join("|"),
"IsRequired": false
},
{
"Condition": "EqualsAny",
"Property": "VideoRangeType",
"Value": vp9VideoRangeTypes,
"IsRequired": false
2023-11-04 19:11:14 +00:00
},
{
"Condition": "LessThanEqual",
"Property": "VideoLevel",
"Value": vp9LevelString,
"IsRequired": false
2023-10-06 03:18:36 +00:00
}
]
}
' set max resolution
2023-11-04 19:11:14 +00:00
if globalUserSettings["playback.resolution.mode"] = "everything" and maxResSetting < > "off"
vp9ProfileArray.Conditions.push(maxHeightArray)
vp9ProfileArray.Conditions.push(maxWidthArray)
2023-10-06 03:18:36 +00:00
end if
' set bitrate restrictions based on user settings
bitRateArray = GetBitRateLimit("vp9")
if bitRateArray.count() > 0
2023-11-04 19:11:14 +00:00
vp9ProfileArray.Conditions.push(bitRateArray)
2023-10-06 03:18:36 +00:00
end if
2023-11-04 19:11:14 +00:00
codecProfiles.push(vp9ProfileArray)
2023-10-06 03:18:36 +00:00
end if
2023-11-04 19:11:14 +00:00
return codecProfiles
2023-10-06 03:18:36 +00:00
end function
2023-11-04 19:11:14 +00:00
function getSubtitleProfiles() as object
subtitleProfiles = []
2023-10-06 03:18:36 +00:00
2023-11-04 19:11:14 +00:00
subtitleProfiles.push({
"Format": "vtt",
"Method": "External"
})
subtitleProfiles.push({
"Format": "srt",
"Method": "External"
})
subtitleProfiles.push({
"Format": "ttml",
"Method": "External"
2023-10-06 03:18:36 +00:00
})
2023-11-04 19:11:14 +00:00
subtitleProfiles.push({
"Format": "sub",
"Method": "External"
})
return subtitleProfiles
2023-10-06 03:18:36 +00:00
end function
function GetBitRateLimit(codec as string) as object
2023-11-04 19:11:14 +00:00
globalUserSettings = m.global.session.user.settings
if globalUserSettings["playback.bitrate.maxlimited"]
userSetLimit = globalUserSettings["playback.bitrate.limit"].ToInt()
2023-10-06 03:18:36 +00:00
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
2023-11-04 19:11:14 +00:00
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
2023-10-06 03:18:36 +00:00
' 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
2023-10-27 02:20:25 +00:00
2023-11-11 00:30:32 +00:00
' Print out the deviceProfile for debugging
sub printDeviceProfile(profile as object)
print "profile =", profile
print "profile.DeviceProfile =", profile.DeviceProfile
print "profile.DeviceProfile.CodecProfiles ="
for each prof in profile.DeviceProfile.CodecProfiles
print prof
for each cond in prof.Conditions
print cond
end for
end for
print "profile.DeviceProfile.ContainerProfiles =", profile.DeviceProfile.ContainerProfiles
print "profile.DeviceProfile.DirectPlayProfiles ="
for each prof in profile.DeviceProfile.DirectPlayProfiles
print prof
end for
print "profile.DeviceProfile.SubtitleProfiles ="
for each prof in profile.DeviceProfile.SubtitleProfiles
print prof
end for
print "profile.DeviceProfile.TranscodingProfiles ="
for each prof in profile.DeviceProfile.TranscodingProfiles
print prof
if isValid(prof.Conditions)
for each condition in prof.Conditions
print condition
end for
end if
end for
print "profile.PlayableMediaTypes =", profile.PlayableMediaTypes
print "profile.SupportedCommands =", profile.SupportedCommands
end sub
2023-10-27 02:20:25 +00:00
' 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
2023-12-05 16:56:00 +00:00
< / code > < / pre > < / article > < / section > < footer class = "footer" id = "PeOAagUepe" > < div class = "wrapper" > < span class = "jsdoc-message" > Automatically generated using < a href = "https://github.com/jsdoc/jsdoc" target = "_blank" > JSDoc< / a > and the < a href = "https://github.com/ankitskvmdam/clean-jsdoc-theme" target = "_blank" > clean-jsdoc-theme< / a > .< / span > < / div > < / footer > < / div > < / div > < / div > < div class = "search-container" id = "PkfLWpAbet" style = "display:none" > < div class = "wrapper" id = "iCxFxjkHbP" > < button class = "icon-button search-close-button" id = "VjLlGakifb" aria-label = "close search" > < svg > < use xlink:href = "#close-icon" > < / use > < / svg > < / button > < div class = "search-box-c" > < svg > < use xlink:href = "#search-icon" > < / use > < / svg > < input type = "text" id = "vpcKVYIppa" class = "search-input" placeholder = "Search..." autofocus > < / div > < div class = "search-result-c" id = "fWwVHRuDuN" > < span class = "search-result-c-text" > Type anything to view search result< / span > < / div > < / div > < / div > < div class = "mobile-menu-icon-container" > < button class = "icon-button" id = "mobile-menu" data-isopen = "false" aria-label = "menu" > < svg > < use xlink:href = "#menu-icon" > < / use > < / svg > < / button > < / div > < div id = "mobile-sidebar" class = "mobile-sidebar-container" > < div class = "mobile-sidebar-wrapper" > < a href = "/" class = "sidebar-title sidebar-title-anchor" > jellyfin-roku Code Documentation< / a > < div class = "mobile-nav-links" > < div class = "external-link navbar-item" > < a id = "jellyfin-link-mobile" href = "https://jellyfin.org/" target = "_blank" > Jellyfin< / a > < / div > < div class = "external-link navbar-item" > < a id = "github-link-mobile" href = "https://github.com/jellyfin/jellyfin-roku" target = "_blank" > GitHub< / a > < / div > < div class = "external-link navbar-item" > < a id = "forum-link-mobile" href = "https://forum.jellyfin.org/f-roku-development" target = "_blank" > Forum< / a > < / div > < div class = "external-link navbar-item" > < a id = "matrix-link-mobile" href = "https://matrix.to/#/#jellyfin-dev-roku:matrix.org" target = "_blank" > Matrix< / a > < / div > < / div > < div class = "mobile-sidebar-items-c" > < div class = "sidebar-section-title with-arrow" data-isopen = "false" id = "sidebar-modules" > < div > Modules< / div > < svg > < use xlink:href = "#down-icon" > < / use > < / svg > < / div > < div class = "sidebar-section-children-container" > < div class = "sidebar-section-children" > < a href = "module-AlbumData.html" > AlbumData< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-AlbumGrid.html" > AlbumGrid< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-AlbumTrackList.html" > AlbumTrackList< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-AlbumView.html" > AlbumView< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-Alpha.html" > Alpha< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-ArtistView.html" > ArtistView< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-AudioPlayer.html" > AudioPlayer< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-AudioPlayerView.html" > AudioPlayerView< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-AudioTrackListItem.html" > AudioTrackListItem< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-ButtonGroupHoriz.html" > ButtonGroupHoriz< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-ButtonGroupVert.html" > ButtonGroupVert< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-ChannelData.html" > ChannelData< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-Clock.html" > Clock< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-CollectionData.html" > CollectionData< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-ConfigData.html" > ConfigData< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-ConfigItem.html" > ConfigItem< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-ConfigList.html" > ConfigList< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-ExtrasItem.html" > ExtrasItem< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-ExtrasRowList.html" > ExtrasRowList< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-FavoriteItemsTask.html" > FavoriteItemsTask< / a > < / div > < div class = "sidebar-section-children" > < a href = "module-Folder