source_utils_deviceCapabilities.bs

import "pkg:/source/api/baserequest.bs"
import "pkg:/source/utils/misc.bs"

' Returns the Device Capabilities for Roku.
' Also prints out the device profile for debugging
function getDeviceCapabilities() as object
  deviceProfile = {
    "PlayableMediaTypes": [
      "Audio",
      "Video",
      "Photo"
    ],
    "SupportedCommands": [],
    "SupportsPersistentIdentifier": true,
    "SupportsMediaControl": false,
    "SupportsContentUploading": false,
    "SupportsSync": false,
    "DeviceProfile": getDeviceProfile(),
    "AppStoreUrl": "https://channelstore.roku.com/details/232f9e82db11ce628e3fe7e01382a330:a85d6e9e520567806e8dae1c0cabadd5/jellyrock"
  }

  return deviceProfile
end function

function getDeviceProfile() as object
  globalDevice = m.global.device
  return {
    "Name": "JellyRock",
    "Id": globalDevice.id,
    "Identification": {
      "FriendlyName": globalDevice.friendlyName,
      "ModelNumber": globalDevice.model,
      "ModelName": globalDevice.name,
      "ModelDescription": "Type: " + globalDevice.modelType,
      "Manufacturer": globalDevice.modelDetails.VendorName
    },
    "MaxStreamingBitrate": 140000000,
    "MaxStaticBitrate": 140000000,
    "MusicStreamingTranscodingBitrate": 192000, ' 192 kbps
    ' "MaxStaticMusicBitrate": 500000,
    "DirectPlayProfiles": GetDirectPlayProfiles(),
    "TranscodingProfiles": getTranscodingProfiles(),
    "ContainerProfiles": getContainerProfiles(),
    "CodecProfiles": getCodecProfiles(),
    "SubtitleProfiles": getSubtitleProfiles()
  }
end function

' Test if device can decode a specific codec at a given channel count
' This is a public wrapper for getActualCodecSupport that can be called from other files
' Returns true if the Roku can decode this codec at this channel count
function canDeviceDecodeCodec(codec as string, channelCount as integer) as boolean
  di = CreateObject("roDeviceInfo")
  return getActualCodecSupport(codec, channelCount, di)
end function

' Override false positives from Roku API using known hardware limits
' Returns true if codec can actually decode/passthrough the specified channel count
' For surround codecs (>2ch), prioritizes PassThru check to detect receiver support
function getActualCodecSupport(codec as string, channelCount as integer, di as object) as boolean
  ' Codecs that Roku can OUTPUT multichannel audio (not just decode)
  surroundOutputCodecs = ["eac3", "ac3", "dts"]

  ' For multichannel surround codecs (>2ch), prioritize PassThru check
  if channelCount > 2 and arrayHasValue(surroundOutputCodecs, codec)
    ' First: Check if receiver supports this via PassThru
    if di.CanDecodeAudio({ Codec: codec, ChCnt: channelCount, PassThru: 1 }).Result
      return true ' Receiver connected and supports this channel count!
    end if

    ' No PassThru support - check if Roku can natively decode (will downmix to stereo)
    if not di.CanDecodeAudio({ Codec: codec, ChCnt: channelCount }).Result
      return false ' API says can't decode at all
    end if

    ' API says yes - check for false positive using known hardware limits
    rokuDecodeMaxChannels = {
      "eac3": 6,
      "ac3": 6,
      "dts": 0 ' DTS is passthrough-only, cannot natively decode
    }

    ' Defensive: Ensure codec is a valid key in rokuDecodeMaxChannels
    ' This should always be true due to the surroundOutputCodecs check above,
    ' but this guards against future changes or typos.
    if not rokuDecodeMaxChannels.DoesExist(codec)
      ' Unexpected codec; treat as unsupported for >2ch
      return false
    end if

    if channelCount > rokuDecodeMaxChannels[codec]
      return false ' Exceeds native decode capability - false positive
    end if

    return true ' Can natively decode (will downmix to stereo for no-receiver users)
  end if

  ' For stereo (2ch) or non-surround codecs (aac, mp3, pcm, etc)
  if not di.CanDecodeAudio({ Codec: codec, ChCnt: channelCount }).Result
    return false ' API says no support
  end if

  ' API says yes - check for false positives on non-surround codecs
  rokuDecodeMaxChannels = {
    "aac": 6,
    "pcm": 2,
    "lpcm": 2
  }

  if rokuDecodeMaxChannels.DoesExist(codec) and channelCount > rokuDecodeMaxChannels[codec]
    return false ' False positive
  end if

  return true
end function

' Get list of surround codecs that support passthrough at specified channel count
' Returns array of codec strings in priority order: eac3, ac3, dts
function getSupportedPassthruCodecs(di as object, channelCount as integer) as object
  ' Codecs that Roku can OUTPUT multichannel audio (not just decode)
  surroundOutputCodecs = ["eac3", "ac3", "dts"]
  supportedCodecs = []

  for each codec in surroundOutputCodecs
    if getActualCodecSupport(codec, channelCount, di)
      supportedCodecs.push(codec)
    end if
  end for

  return supportedCodecs
end function

function GetDirectPlayProfiles() as object
  ' ONE rendezvous to get user settings
  globalUserSettings = m.global.user.settings

  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", "hevc", "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 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.playbackMpeg4
    for each container in supportedCodecs
      supportedCodecs[container]["video"].push("mpeg4")
    end for
  end if
  if globalUserSettings.playbackMpeg2
    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

  ' 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"]

  ' 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
  ' ONE rendezvous to get user settings
  globalUserSettings = m.global.user.settings
  transcodingProfiles = []
  di = CreateObject("roDeviceInfo")

  ' ========================================
  ' AUDIO CAPABILITY DETECTION
  ' ========================================

  ' Detect actual passthrough support for multichannel audio
  sixChannelPassthruCodecs = getSupportedPassthruCodecs(di, 6)
  eightChannelPassthruCodecs = getSupportedPassthruCodecs(di, 8)

  ' Get user's preferred codec for ordering (applied later in transcoding profile)
  preferredCodec = globalUserSettings.playbackPreferredMultichannelCodec

  ' ========================================
  ' AUDIO-ONLY TRANSCODING PROFILES
  ' ========================================

  ' AAC for stereo audio (always 2 channels)
  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"
  })

  ' MP3 for stereo audio (fixed from incorrect maxAudioChannels value)
  transcodingProfiles.push({
    "Container": "mp3",
    "Type": "Audio",
    "AudioCodec": "mp3",
    "Context": "Streaming",
    "Protocol": "http",
    "MaxAudioChannels": "2"
  })
  transcodingProfiles.push({
    "Container": "mp3",
    "Type": "Audio",
    "AudioCodec": "mp3",
    "Context": "Static",
    "Protocol": "http",
    "MaxAudioChannels": "2"
  })

  ' ========================================
  ' VIDEO CODEC DETECTION (per container)
  ' ========================================

  transcodingContainers = ["ts", "mp4"]
  containerVideoCodecs = {}

  for each container in transcodingContainers
    ' Video codecs
    videoCodecList = "h264"

    if di.CanDecodeVideo({ Codec: "hevc", Container: container }).Result
      videoCodecList = "hevc,h265," + videoCodecList
    end if

    if di.CanDecodeVideo({ Codec: "mpeg4 avc", Container: container }).Result
      if videoCodecList.Instr("mpeg4 avc") = -1
        videoCodecList = videoCodecList + ",mpeg4 avc"
      end if
    end if

    if di.CanDecodeVideo({ Codec: "vp9", Container: container }).Result
      if videoCodecList.Instr("vp9") = -1
        videoCodecList = videoCodecList + ",vp9"
      end if
    end if

    if globalUserSettings.playbackMpeg2
      if di.CanDecodeVideo({ Codec: "mpeg2", Container: container }).Result
        videoCodecList = videoCodecList + ",mpeg2video"
      end if
    end if

    containerVideoCodecs[container] = videoCodecList
  end for

  ' ========================================
  ' VIDEO TRANSCODING PROFILES
  ' Single profile per container with codecs in optimal order
  ' ========================================

  ' Determine max supported audio channels
  maxAudioChannels = "2" ' default stereo
  allSurroundCodecs = []

  if eightChannelPassthruCodecs.count() > 0
    maxAudioChannels = "8"
    allSurroundCodecs = eightChannelPassthruCodecs
  else if sixChannelPassthruCodecs.count() > 0
    maxAudioChannels = "6"
    allSurroundCodecs = sixChannelPassthruCodecs
  end if

  ' Build optimal audio codec list for transcoding
  ' Order: AAC (stereo), passthrough surround, multichannel decode, stereo fallbacks
  for each container in transcodingContainers
    audioCodecList = []

    ' 1. AAC always first (efficient for stereo, removed at playback time for multichannel sources with passthrough)
    audioCodecList.push("aac")

    ' 2. Add surround passthrough codecs if supported (in preference order)
    if allSurroundCodecs.count() > 0
      ' Apply user's preferred codec ordering
      if isValid(preferredCodec) and preferredCodec <> "" and preferredCodec <> "auto"
        ' Preferred codec first (if supported)
        if arrayHasValue(allSurroundCodecs, preferredCodec)
          audioCodecList.push(preferredCodec)
        end if

        ' Then other surround codecs in priority order: eac3 > ac3 > dts
        surroundPriority = ["eac3", "ac3", "dts"]
        for each codec in surroundPriority
          if arrayHasValue(allSurroundCodecs, codec) and codec <> preferredCodec
            audioCodecList.push(codec)
          end if
        end for
      else
        ' Auto mode: use default priority order (eac3 > ac3 > dts)
        surroundPriority = ["eac3", "ac3", "dts"]
        for each codec in surroundPriority
          if arrayHasValue(allSurroundCodecs, codec)
            audioCodecList.push(codec)
          end if
        end for
      end if
    end if

    ' 3. Add stereo fallback codecs if device supports them (MP3 most compatible, then lossless as fallbacks)
    stereoFallbacks = ["mp3", "flac", "alac", "pcm"]
    for each codec in stereoFallbacks
      if not arrayHasValue(audioCodecList, codec)
        ' Validate device can decode this codec at 2 channels in this container
        if di.CanDecodeAudio({ Codec: codec, ChCnt: 2, Container: container }).Result
          audioCodecList.push(codec)
        end if
      end if
    end for

    ' Create single profile per container
    profile = {
      "Container": container,
      "Context": "Streaming",
      "Protocol": "hls",
      "Type": "Video",
      "VideoCodec": containerVideoCodecs[container],
      "AudioCodec": audioCodecList.join(","),
      "MaxAudioChannels": maxAudioChannels,
      "MinSegments": 1,
      "BreakOnNonKeyFrames": false,
      "SegmentLength": 6
    }

    ' Add height restriction if configured
    maxHeightArray = getMaxHeightArray(true)
    if maxHeightArray.count() > 0
      profile.Conditions = [maxHeightArray]
    end if

    transcodingProfiles.push(profile)
  end for

  return transcodingProfiles
end function

function getContainerProfiles() as object
  containerProfiles = []

  return containerProfiles
end function

function getCodecProfiles() as object
  ' ONE rendezvous to get user settings
  globalUserSettings = m.global.user.settings

  codecProfiles = []
  profileSupport = {
    "h264": {},
    "mpeg4 avc": {},
    "h265": {},
    "hevc": {},
    "vp9": {},
    "mpeg2": {},
    "av1": {}
  }
  di = CreateObject("roDeviceInfo")
  maxHeightArray = getMaxHeightArray()

  ' ========================================
  ' DETECT SURROUND PASSTHROUGH SUPPORT
  ' ========================================

  sixChannelPassthruCodecs = getSupportedPassthruCodecs(di, 6)
  eightChannelPassthruCodecs = getSupportedPassthruCodecs(di, 8)
  hasSurroundPassthru = sixChannelPassthruCodecs.count() > 0 or eightChannelPassthruCodecs.count() > 0

  ' Check user setting for multichannel decode preference
  ' When disabled, force stereo-output codecs to 2ch max (same as passthrough behavior)
  userWantsDecodeLimit = not globalUserSettings.playbackDecodeMultichannelAudio

  ' ========================================
  ' CODEC CATEGORIES
  ' ========================================

  ' Codecs that Roku decodes multichannel but only OUTPUTS as stereo PCM
  ' When user has a receiver, we force these to 2ch max to trigger transcoding
  ' to surround output codecs (eac3/ac3/dts) instead of direct playing and downmixing
  stereoOutputCodecs = ["aac", "flac", "alac", "pcm", "lpcm", "wav", "opus", "vorbis"]

  ' ========================================
  ' AUDIO CODEC PROFILES
  ' ========================================

  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
    ' Special handling for stereo-output codecs when user has surround passthrough OR user disabled multichannel decode
    ' These codecs decode multichannel but Roku only outputs as stereo PCM
    if arrayHasValue(stereoOutputCodecs, audioCodec) and (hasSurroundPassthru or userWantsDecodeLimit)
      ' Force to 2 channels maximum to prevent Roku from downmixing multichannel to stereo
      ' This triggers server to transcode to surroundOutputCodecs (eac3/ac3/dts) or stereo instead
      for each codecType in ["VideoAudio", "Audio"]
        ' Special AAC profile restrictions (Main and HE-AAC not supported)
        if audioCodec = "aac"
          codecProfiles.push({
            "Type": codecType,
            "Codec": audioCodec,
            "Conditions": [
              {
                "Condition": "NotEquals",
                "Property": "AudioProfile",
                "Value": "Main",
                "IsRequired": true
              },
              {
                "Condition": "NotEquals",
                "Property": "AudioProfile",
                "Value": "HE-AAC",
                "IsRequired": true
              },
              {
                "Condition": "LessThanEqual",
                "Property": "AudioChannels",
                "Value": "2",
                "IsRequired": true
              }
            ]
          })
        else
          ' All other stereo-output codecs: just limit channels
          codecProfiles.push({
            "Type": codecType,
            "Codec": audioCodec,
            "Conditions": [
              {
                "Condition": "LessThanEqual",
                "Property": "AudioChannels",
                "Value": "2",
                "IsRequired": true
              }
            ]
          })
        end if
      end for
    else
      ' Standard logic for all other codecs (and AAC when no surround passthru)
      for each audioChannel in audioChannels
        ' Use override logic to catch false positives
        if getActualCodecSupport(audioCodec, audioChannel, di)
          ' Create codec profile for this channel count
          for each codecType in ["VideoAudio", "Audio"]
            if audioCodec = "aac"
              codecProfiles.push({
                "Type": codecType,
                "Codec": audioCodec,
                "Conditions": [
                  {
                    "Condition": "NotEquals",
                    "Property": "AudioProfile",
                    "Value": "Main",
                    "IsRequired": true
                  },
                  {
                    "Condition": "NotEquals",
                    "Property": "AudioProfile",
                    "Value": "HE-AAC",
                    "IsRequired": true
                  },
                  {
                    "Condition": "LessThanEqual",
                    "Property": "AudioChannels",
                    "Value": audioChannel.ToStr(),
                    "IsRequired": true
                  }
                ]
              })
            else if audioCodec = "opus" and codecType = "Audio"
              ' Opus audio files not supported by Roku - skip
            else
              codecProfiles.push({
                "Type": codecType,
                "Codec": audioCodec,
                "Conditions": [
                  {
                    "Condition": "LessThanEqual",
                    "Property": "AudioChannels",
                    "Value": audioChannel.ToStr(),
                    "IsRequired": true
                  }
                ]
              })
            end if
          end for
          ' Found highest supported channel count, stop testing lower
          exit for
        end if
      end for
    end if
  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

  ' 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

  ' 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

  ' HDR SUPPORT
  h264VideoRangeTypes = "SDR|DOVIWithSDR"
  hevcVideoRangeTypes = "SDR|DOVIWithSDR"
  vp9VideoRangeTypes = "SDR"
  av1VideoRangeTypes = "SDR|DOVIWithSDR"

  if canPlay4k()
    print "This device supports 4k video"
    dp = di.GetDisplayProperties()

    if dp.DolbyVision
      h264VideoRangeTypes = h264VideoRangeTypes + "|DOVI"
      hevcVideoRangeTypes = hevcVideoRangeTypes + "|DOVI"
      av1VideoRangeTypes = av1VideoRangeTypes + "|DOVI"
    end if

    if dp.Hdr10
      hevcVideoRangeTypes = hevcVideoRangeTypes + "|HDR10|DOVIWithHDR10"
      vp9VideoRangeTypes = vp9VideoRangeTypes + "|HDR10"
      av1VideoRangeTypes = av1VideoRangeTypes + "|HDR10|DOVIWithHDR10"
    end if

    if dp.Hdr10Plus
      av1VideoRangeTypes = av1VideoRangeTypes + "|HDR10Plus|DOVIWithHDR10Plus"
      hevcVideoRangeTypes = hevcVideoRangeTypes + "|HDR10Plus|DOVIWithHDR10Plus"
    end if

    if dp.HLG
      hevcVideoRangeTypes = hevcVideoRangeTypes + "|HLG|DOVIWithHLG"
      vp9VideoRangeTypes = vp9VideoRangeTypes + "|HLG"
      av1VideoRangeTypes = av1VideoRangeTypes + "|HLG|DOVIWithHLG"
    end if
  end if

  ' H264
  h264LevelSupported = 0.0
  h264AssProfiles = {}
  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
    end for
  end for

  ' convert to string
  h264LevelString = h264LevelSupported.ToStr()
  ' remove decimals
  h264LevelString = removeDecimals(h264LevelString)

  h264ProfileArray = {
    "Type": "Video",
    "Codec": "h264",
    "Conditions": [
      {
        "Condition": "NotEquals",
        "Property": "IsAnamorphic",
        "Value": "true",
        "IsRequired": false
      },
      {
        "Condition": "EqualsAny",
        "Property": "VideoProfile",
        "Value": h264AssProfiles.Keys().join("|"),
        "IsRequired": false
      },
      {
        "Condition": "EqualsAny",
        "Property": "VideoRangeType",
        "Value": h264VideoRangeTypes,
        "IsRequired": false
      }

    ]
  }

  ' check user setting before adding video level restrictions
  if not globalUserSettings.playbackTryDirectH264ProfileLevel
    h264ProfileArray.Conditions.push({
      "Condition": "LessThanEqual",
      "Property": "VideoLevel",
      "Value": h264LevelString,
      "IsRequired": false
    })
  end if

  ' set max resolution
  if maxHeightArray.count() > 0
    h264ProfileArray.Conditions.push(maxHeightArray)
  end if

  ' set bitrate restrictions based on user settings
  bitRateArray = GetBitRateLimit("h264")
  if bitRateArray.count() > 0
    h264ProfileArray.Conditions.push(bitRateArray)
  end if

  codecProfiles.push(h264ProfileArray)

  ' MPEG2
  ' NOTE: the mpeg2 levels are being saved in the profileSupport array as if they were profiles
  if globalUserSettings.playbackMpeg2
    mpeg2Levels = []
    for each level in profileSupport["mpeg2"]
      if not arrayHasValue(mpeg2Levels, level)
        mpeg2Levels.push(level)
      end if
    end for

    mpeg2ProfileArray = {
      "Type": "Video",
      "Codec": "mpeg2",
      "Conditions": [
        {
          "Condition": "EqualsAny",
          "Property": "VideoLevel",
          "Value": mpeg2Levels.join("|"),
          "IsRequired": false
        }
      ]
    }

    ' set max resolution
    if maxHeightArray.count() > 0
      mpeg2ProfileArray.Conditions.push(maxHeightArray)
    end if

    ' set bitrate restrictions based on user settings
    bitRateArray = GetBitRateLimit("mpeg2")
    if bitRateArray.count() > 0
      mpeg2ProfileArray.Conditions.push(bitRateArray)
    end if

    codecProfiles.push(mpeg2ProfileArray)
  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
        }
      ]
    }

    ' set max resolution
    if maxHeightArray.count() > 0
      av1ProfileArray.Conditions.push(maxHeightArray)
    end if

    ' set bitrate restrictions based on user settings
    bitRateArray = GetBitRateLimit("av1")
    if bitRateArray.count() > 0
      av1ProfileArray.Conditions.push(bitRateArray)
    end if

    codecProfiles.push(av1ProfileArray)
  end if

  if 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": "NotEquals",
          "Property": "IsAnamorphic",
          "Value": "true",
          "IsRequired": false
        },
        {
          "Condition": "EqualsAny",
          "Property": "VideoProfile",
          "Value": profileSupport["hevc"].Keys().join("|"),
          "IsRequired": false
        },
        {
          "Condition": "EqualsAny",
          "Property": "VideoRangeType",
          "Value": hevcVideoRangeTypes,
          "IsRequired": false
        }
      ]
    }

    ' check user setting before adding VideoLevel restrictions
    if not globalUserSettings.playbackTryDirectHevcProfileLevel
      hevcProfileArray.Conditions.push({
        "Condition": "LessThanEqual",
        "Property": "VideoLevel",
        "Value": hevcLevelString,
        "IsRequired": false
      })
    end if

    ' set max resolution
    if maxHeightArray.count() > 0
      hevcProfileArray.Conditions.push(maxHeightArray)
    end if

    ' set bitrate restrictions based on user settings
    bitRateArray = GetBitRateLimit("h265")
    if bitRateArray.count() > 0
      hevcProfileArray.Conditions.push(bitRateArray)
    end if

    codecProfiles.push(hevcProfileArray)
  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
        }
      ]
    }

    ' set max resolution
    if maxHeightArray.count() > 0
      vp9ProfileArray.Conditions.push(maxHeightArray)
    end if

    ' set bitrate restrictions based on user settings
    bitRateArray = GetBitRateLimit("vp9")
    if bitRateArray.count() > 0
      vp9ProfileArray.Conditions.push(bitRateArray)
    end if

    codecProfiles.push(vp9ProfileArray)
  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
  ' ONE rendezvous to get user settings
  globalUserSettings = m.global.user.settings

  if globalUserSettings.playbackBitrateMaxLimited
    userSetLimit = globalUserSettings.playbackBitrateLimit
    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(isRequired = false as boolean) as object
  userMaxHeight = m.global.user.settings.playbackResolutionMax
  if userMaxHeight = invalid or userMaxHeight = "" then userMaxHeight = "auto"
  if userMaxHeight = "off" then return {}

  deviceMaxHeight = m.global.device.videoHeight

  maxVideoHeight = 1080 ' default to 1080p in case all our validation checks fail

  if userMaxHeight = "auto"
    if isValid(deviceMaxHeight) and deviceMaxHeight <> 0 and deviceMaxHeight > maxVideoHeight
      maxVideoHeight = deviceMaxHeight
    end if
  else
    userMaxHeight = userMaxHeight.ToInt()
    if isValid(userMaxHeight) and userMaxHeight > 0 and userMaxHeight < maxVideoHeight
      maxVideoHeight = userMaxHeight
    end if
  end if

  return {
    "Condition": "LessThanEqual",
    "Property": "Height",
    "Value": maxVideoHeight.toStr(),
    "IsRequired": isRequired
  }
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 not isValid(profileArray) then return {}
  if videoCodec = "" or videoProfile = "" then return profileArray

  if not isValid(profileArray[videoCodec])
    profileArray[videoCodec] = {}
  end if

  if not isValid(profileArray[videoCodec][videoProfile])
    profileArray[videoCodec][videoProfile] = {}
  end if

  ' add profileLevel if a value was provided
  if profileLevel <> ""
    if not isValid(profileArray[videoCodec][videoProfile][profileLevel])
      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

' does this roku device support playing 4k video?
function canPlay4k() as boolean
  deviceInfo = CreateObject("roDeviceInfo")
  hdmiStatus = CreateObject("roHdmiStatus")

  ' Check if the output mode is 2160p or higher
  maxVideoHeight = m.global.device.videoHeight
  if not isValid(maxVideoHeight) then return false
  if maxVideoHeight < 2160
    print "maxVideoHeight is less than 2160p. Does the TV support 4K? If yes, then go to your Roku settings and set your display type to 4K"
    return false
  end if

  ' Check if HDCP 2.2 is enabled, skip check for TVs
  if deviceInfo.GetModelType() = "STB" and hdmiStatus.IsHdcpActive("2.2") <> true
    print "HDCP 2.2 is not active"
    return false
  end if

  ' Check if the Roku player can decode 4K 60fps HEVC streams
  if deviceInfo.CanDecodeVideo({ Codec: "hevc", Profile: "main", Level: "5.1" }).result <> true
    print "Device cannot decode 4K 60fps HEVC streams"
    return false
  end if

  return true
end function