diff --git a/components/ItemGrid/LoadVideoContentTask.brs b/components/ItemGrid/LoadVideoContentTask.brs index 50a978557..25588c309 100644 --- a/components/ItemGrid/LoadVideoContentTask.brs +++ b/components/ItemGrid/LoadVideoContentTask.brs @@ -141,7 +141,6 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s ' 'TODO: allow user selection of subtitle track before playback initiated, for now set to no subtitles - video.directPlaySupported = m.playbackInfo.MediaSources[0].SupportsDirectPlay fully_external = false @@ -164,8 +163,8 @@ sub LoadItems_AddVideoContent(video as object, mediaSourceId as dynamic, audio_s end if if video.directPlaySupported - addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external) video.isTranscoded = false + addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external) else if m.playbackInfo.MediaSources[0].TranscodingUrl = invalid ' If server does not provide a transcode URL, display a message to the user @@ -203,15 +202,13 @@ sub addVideoContentURL(video, mediaSourceId, audio_stream_idx, fully_external) fully_external = true video.content.url = m.playbackInfo.MediaSources[0].Path end if - else: - params = {} - - params.append({ + else + params = { "Static": "true", "Container": video.container, "PlaySessionId": video.PlaySessionId, "AudioStreamIndex": audio_stream_idx - }) + } if mediaSourceId <> "" params.MediaSourceId = mediaSourceId diff --git a/source/utils/deviceCapabilities.brs b/source/utils/deviceCapabilities.brs index 10f522f6b..c019d1c5d 100644 --- a/source/utils/deviceCapabilities.brs +++ b/source/utils/deviceCapabilities.brs @@ -59,36 +59,184 @@ sub PostDeviceProfile() end sub function getDeviceProfile() as object - playMpeg2 = m.global.session.user.settings["playback.mpeg2"] + 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 = [] 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 - ' TRANSCODING + ' 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 + + ' 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"] ' use strings to preserve order mp4AudioCodecs = "aac" mp4VideoCodecs = "h264" tsAudioCodecs = "aac" tsVideoCodecs = "h264" - ' profileSupport["mp4"]["hevc"]["profile name"]["profile level"] - profileSupport = { - mp4: {}, - ts: {} - } ' 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"] + audioCodecs = ["mp3", "vorbis", "opus", "flac", "alac", "ac4", "pcm", "wma", "wmapro"] surroundSoundCodecs = ["eac3", "ac3", "dts"] - if m.global.session.user.settings["playback.forceDTS"] = true + if globalUserSettings["playback.forceDTS"] = true surroundSoundCodecs = ["dts", "eac3", "ac3"] end if - preferredSurroundSoundCodec = invalid + surroundSoundCodec = invalid if di.GetAudioOutputChannel() = "5.1 surround" maxAudioChannels = "6" for each codec in surroundSoundCodecs if di.CanDecodeAudio({ Codec: codec, ChCnt: 6 }).Result - preferredSurroundSoundCodec = codec + surroundSoundCodec = codec if di.CanDecodeAudio({ Codec: codec, ChCnt: 8 }).Result maxAudioChannels = "8" end if @@ -100,149 +248,115 @@ function getDeviceProfile() as object ' VIDEO CODECS ' ' AVC / h264 / MPEG4 AVC - h264Profiles = ["main", "high"] - h264Levels = ["4.1", "4.2"] - for each container in profileSupport - for each profile in h264Profiles - for each level in h264Levels - if di.CanDecodeVideo({ Codec: "h264", Container: container, Profile: profile, Level: level }).Result - profileSupport[container] = updateProfileArray(profileSupport[container], "h264", profile, level) + 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" end if - if di.CanDecodeVideo({ Codec: "mpeg4 avc", Container: container, Profile: profile, Level: level }).Result - profileSupport[container] = updateProfileArray(profileSupport[container], "mpeg4 avc", profile, level) - 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 + else if container = "ts" + ' check for codec string before adding it + if tsVideoCodecs.Instr(0, ",h264") = -1 + tsVideoCodecs = tsVideoCodecs + ",h264" end if - end for - end for + 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 end for - addHevc = false - if m.global.session.user.settings["playback.compatibility.disablehevc"] = false - ' HEVC / h265 - hevcProfiles = ["main", "main 10"] - hevcLevels = ["4.1", "5.0", "5.1"] - for each container in profileSupport - for each profile in hevcProfiles - for each level in hevcLevels - if di.CanDecodeVideo({ Codec: "hevc", Container: container, Profile: profile, Level: level }).Result - addHevc = true - profileSupport[container] = updateProfileArray(profileSupport[container], "hevc", profile, level) - profileSupport[container] = updateProfileArray(profileSupport[container], "h265", profile, level) - 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 for - end for - end for - end if - - ' VP9 - vp9Profiles = ["profile 0", "profile 2"] - addVp9 = false - for each container in profileSupport - for each profile in vp9Profiles - if di.CanDecodeAudio({ Codec: "vp9", Container: container, Profile: profile }).Result - addVp9 = true - profileSupport[container] = updateProfileArray(profileSupport[container], "vp9", profile) - + ' HEVC / h265 + if globalUserSettings["playback.compatibility.disablehevc"] = false + for each container in transcodingContainers + if di.CanDecodeVideo({ Codec: "hevc", Container: container }).Result if container = "mp4" ' check for codec string before adding it - if mp4VideoCodecs.Instr(0, ",vp9") = -1 - mp4VideoCodecs = mp4VideoCodecs + ",vp9" + 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, ",vp9") = -1 - tsVideoCodecs = tsVideoCodecs + ",vp9" + 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 for + 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 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"] - addMpeg2 = false - if playMpeg2 - for each container in profileSupport - for each level in mpeg2Levels - if di.CanDecodeVideo({ Codec: "mpeg2", Container: container, Level: level }).Result - addMpeg2 = true - profileSupport[container] = updateProfileArray(profileSupport[container], "mpeg2", level) - 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 + 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" end if end if - end for + end if end for end if ' AV1 - addAv1 = di.CanDecodeVideo({ Codec: "av1" }).Result - av1Profiles = ["main", "main 10"] - av1Levels = ["4.1", "5.0", "5.1"] - - if addAv1 - for each container in profileSupport - for each profile in av1Profiles - for each level in av1Levels - ' av1 doesn't support checking for container - if di.CanDecodeVideo({ Codec: "av1", Profile: profile, Level: level }).Result - profileSupport[container] = updateProfileArray(profileSupport[container], "av1", profile, level) - 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 - end for - end for - end if + 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 ' AUDIO CODECS - for each container in profileSupport + for each container in transcodingContainers for each codec in audioCodecs if di.CanDecodeAudio({ Codec: codec, Container: container }).result if container = "mp4" @@ -254,132 +368,8 @@ function getDeviceProfile() as object end for end for - ' HDR SUPPORT - h264VideoRangeTypes = "SDR" - hevcVideoRangeTypes = "SDR" - vp9VideoRangeTypes = "SDR" - av1VideoRangeTypes = "SDR" - - 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 - - DirectPlayProfile = GetDirectPlayProfiles() - - deviceProfile = { - "Name": "Official Roku Client", - "Id": m.global.device.id, - "Identification": { - "FriendlyName": m.global.device.friendlyName, - "ModelNumber": m.global.device.model, - "SerialNumber": "string", - "ModelName": m.global.device.name, - "ModelDescription": "Type: " + m.global.device.modelType, - "Manufacturer": m.global.device.modelDetails.VendorName - }, - "FriendlyName": m.global.device.friendlyName, - "Manufacturer": m.global.device.modelDetails.VendorName, - "ModelName": m.global.device.name, - "ModelDescription": "Type: " + m.global.device.modelType, - "ModelNumber": m.global.device.model, - "SerialNumber": m.global.device.serial, - "MaxStreamingBitrate": 120000000, - "MaxStaticBitrate": 100000000, - "MusicStreamingTranscodingBitrate": 192000, - "DirectPlayProfiles": DirectPlayProfile, - "TranscodingProfiles": [], - "ContainerProfiles": [], - "CodecProfiles": [ - { - "Type": "VideoAudio", - "Conditions": [ - { - "Condition": "LessThanEqual", - "Property": "AudioChannels", - "Value": maxAudioChannels, - "IsRequired": false - } - ] - } - ], - "SubtitleProfiles": [ - { - "Format": "vtt", - "Method": "External" - }, - { - "Format": "srt", - "Method": "External" - }, - { - "Format": "ttml", - "Method": "External" - }, - { - "Format": "sub", - "Method": "External" - } - ] - } - - ' build TranscodingProfiles - ' max resolution - maxResSetting = m.global.session.user.settings["playback.resolution.max"] - maxResMode = m.global.session.user.settings["playback.resolution.mode"] - maxVideoHeight = maxResSetting - maxVideoWidth = invalid - - if maxResSetting = "auto" - maxVideoHeight = m.global.device.videoHeight - maxVideoWidth = m.global.device.videoWidth - else if maxResSetting <> "off" - 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 - end if - - maxVideoHeightArray = { - "Condition": "LessThanEqual", - "Property": "Width", - "Value": maxVideoWidth, - "IsRequired": true - } - maxVideoWidthArray = { - "Condition": "LessThanEqual", - "Property": "Height", - "Value": maxVideoHeight, - "IsRequired": true - } - ' ' add mp3 to TranscodingProfile for music - deviceProfile.TranscodingProfiles.push({ + transcodingProfiles.push({ "Container": "mp3", "Type": "Audio", "AudioCodec": "mp3", @@ -387,7 +377,7 @@ function getDeviceProfile() as object "Protocol": "http", "MaxAudioChannels": maxAudioChannels }) - deviceProfile.TranscodingProfiles.push({ + transcodingProfiles.push({ "Container": "mp3", "Type": "Audio", "AudioCodec": "mp3", @@ -397,7 +387,7 @@ function getDeviceProfile() as object }) ' add aac to TranscodingProfile for stereo audio ' NOTE: multichannel aac is not supported. only decode to stereo on some devices - deviceProfile.TranscodingProfiles.push({ + transcodingProfiles.push({ "Container": "ts", "Type": "Audio", "AudioCodec": "aac", @@ -405,7 +395,7 @@ function getDeviceProfile() as object "Protocol": "http", "MaxAudioChannels": "2" }) - deviceProfile.TranscodingProfiles.push({ + transcodingProfiles.push({ "Container": "ts", "Type": "Audio", "AudioCodec": "aac", @@ -437,75 +427,213 @@ function getDeviceProfile() as object "BreakOnNonKeyFrames": false } - ' apply max res to transcoding profile - if maxResSetting <> "off" - tsArray.Conditions = [maxVideoHeightArray, maxVideoWidthArray] - mp4Array.Conditions = [maxVideoHeightArray, maxVideoWidthArray] - end if + ' 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"] + codecProfiles.push({ + "Type": codecType, + "Codec": audioCodec, + "Conditions": [ + { + "Condition": "LessThanEqual", + "Property": "AudioChannels", + "Value": audioChannel, + "IsRequired": true + } + ] + }) + + 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 - ' surround sound - if preferredSurroundSoundCodec <> invalid - ' add preferred surround sound codec to TranscodingProfile - deviceProfile.TranscodingProfiles.push({ - "Container": preferredSurroundSoundCodec, - "Type": "Audio", - "AudioCodec": preferredSurroundSoundCodec, - "Context": "Streaming", - "Protocol": "http", - "MaxAudioChannels": maxAudioChannels - }) - deviceProfile.TranscodingProfiles.push({ - "Container": preferredSurroundSoundCodec, - "Type": "Audio", - "AudioCodec": preferredSurroundSoundCodec, - "Context": "Static", - "Protocol": "http", - "MaxAudioChannels": maxAudioChannels - }) + ' 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 - ' move preferred codec to front of AudioCodec string - tsArray.AudioCodec = setPreferredCodec(tsArray.AudioCodec, preferredSurroundSoundCodec) - mp4Array.AudioCodec = setPreferredCodec(mp4Array.AudioCodec, preferredSurroundSoundCodec) - end if + ' 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 - deviceProfile.TranscodingProfiles.push(tsArray) - deviceProfile.TranscodingProfiles.push(mp4Array) + ' HDR SUPPORT + h264VideoRangeTypes = "SDR" + hevcVideoRangeTypes = "SDR" + vp9VideoRangeTypes = "SDR" + av1VideoRangeTypes = "SDR" - ' Build CodecProfiles + 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 ' H264 - h264Mp4LevelSupported = 0.0 - h264TsLevelSupported = 0.0 + h264LevelSupported = 0.0 h264AssProfiles = {} - h264LevelString = invalid - for each container in profileSupport - for each profile in profileSupport[container]["h264"] - h264AssProfiles.AddReplace(profile, true) - for each level in profileSupport[container]["h264"][profile] - levelFloat = level.ToFloat() - if container = "mp4" - if levelFloat > h264Mp4LevelSupported - h264Mp4LevelSupported = levelFloat - end if - else if container = "ts" - if levelFloat > h264TsLevelSupported - h264TsLevelSupported = levelFloat - end if - end if - end for + 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 - h264LevelString = h264Mp4LevelSupported - if h264TsLevelSupported > h264Mp4LevelSupported - h264LevelString = h264TsLevelSupported - end if ' convert to string - h264LevelString = h264LevelString.ToStr() + h264LevelString = h264LevelSupported.ToStr() ' remove decimals h264LevelString = removeDecimals(h264LevelString) - codecProfileArray = { + h264ProfileArray = { "Type": "Video", "Codec": "h264", "Conditions": [ @@ -515,6 +643,12 @@ function getDeviceProfile() as object "Value": "true", "IsRequired": false }, + { + "Condition": "LessThanEqual", + "Property": "VideoBitDepth", + "Value": "8", + "IsRequired": false + }, { "Condition": "EqualsAny", "Property": "VideoProfile", @@ -530,14 +664,10 @@ function getDeviceProfile() as object ] } - ' set max resolution - if maxResMode = "everything" and maxResSetting <> "off" - codecProfileArray.Conditions.push(maxVideoHeightArray) - codecProfileArray.Conditions.push(maxVideoWidthArray) - end if + ' check user setting before adding video level restrictions - if not m.global.session.user.settings["playback.tryDirect.h264ProfileLevel"] - codecProfileArray.Conditions.push({ + if not globalUserSettings["playback.tryDirect.h264ProfileLevel"] + h264ProfileArray.Conditions.push({ "Condition": "LessThanEqual", "Property": "VideoLevel", "Value": h264LevelString, @@ -545,27 +675,31 @@ function getDeviceProfile() as object }) 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 - codecProfileArray.Conditions.push(bitRateArray) + h264ProfileArray.Conditions.push(bitRateArray) end if - deviceProfile.CodecProfiles.push(codecProfileArray) + codecProfiles.push(h264ProfileArray) ' MPEG2 ' NOTE: the mpeg2 levels are being saved in the profileSupport array as if they were profiles - if addMpeg2 + if globalUserSettings["playback.mpeg2"] mpeg2Levels = [] - for each container in profileSupport - for each level in profileSupport[container]["mpeg2"] - if not arrayHasValue(mpeg2Levels, level) - mpeg2Levels.push(level) - end if - end for + for each level in profileSupport["mpeg2"] + if not arrayHasValue(mpeg2Levels, level) + mpeg2Levels.push(level) + end if end for - codecProfileArray = { + mpeg2ProfileArray = { "Type": "Video", "Codec": "mpeg2", "Conditions": [ @@ -579,49 +713,34 @@ function getDeviceProfile() as object } ' set max resolution - if maxResMode = "everything" and maxResSetting <> "off" - codecProfileArray.Conditions.push(maxVideoHeightArray) - codecProfileArray.Conditions.push(maxVideoWidthArray) + 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 - codecProfileArray.Conditions.push(bitRateArray) + mpeg2ProfileArray.Conditions.push(bitRateArray) end if - deviceProfile.CodecProfiles.push(codecProfileArray) + codecProfiles.push(mpeg2ProfileArray) end if - if addAv1 - av1Mp4LevelSupported = 0.0 - av1TsLevelSupported = 0.0 + if di.CanDecodeVideo({ Codec: "av1" }).Result + av1LevelSupported = 0.0 av1AssProfiles = {} - av1HighestLevel = 0.0 - for each container in profileSupport - for each profile in profileSupport[container]["av1"] - av1AssProfiles.AddReplace(profile, true) - for each level in profileSupport[container]["av1"][profile] - levelFloat = level.ToFloat() - if container = "mp4" - if levelFloat > av1Mp4LevelSupported - av1Mp4LevelSupported = levelFloat - end if - else if container = "ts" - if levelFloat > av1TsLevelSupported - av1TsLevelSupported = levelFloat - end if - end if - end for + 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 - av1HighestLevel = av1Mp4LevelSupported - if av1TsLevelSupported > av1Mp4LevelSupported - av1HighestLevel = av1TsLevelSupported - end if - - codecProfileArray = { + av1ProfileArray = { "Type": "Video", "Codec": "av1", "Conditions": [ @@ -640,61 +759,47 @@ function getDeviceProfile() as object { "Condition": "LessThanEqual", "Property": "VideoLevel", - "Value": (120 * av1HighestLevel).ToStr(), + "Value": (120 * av1LevelSupported).ToStr(), "IsRequired": false } ] } ' set max resolution - if maxResMode = "everything" and maxResSetting <> "off" - codecProfileArray.Conditions.push(maxVideoHeightArray) - codecProfileArray.Conditions.push(maxVideoWidthArray) + 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 - codecProfileArray.Conditions.push(bitRateArray) + av1ProfileArray.Conditions.push(bitRateArray) end if - deviceProfile.CodecProfiles.push(codecProfileArray) + codecProfiles.push(av1ProfileArray) end if - if addHevc - hevcMp4LevelSupported = 0.0 - hevcTsLevelSupported = 0.0 + if not globalUserSettings["playback.compatibility.disablehevc"] and di.CanDecodeVideo({ Codec: "hevc" }).Result + hevcLevelSupported = 0.0 hevcAssProfiles = {} - hevcHighestLevel = 0.0 - for each container in profileSupport - for each profile in profileSupport[container]["hevc"] - hevcAssProfiles.AddReplace(profile, true) - for each level in profileSupport[container]["hevc"][profile] - levelFloat = level.ToFloat() - if container = "mp4" - if levelFloat > hevcMp4LevelSupported - hevcMp4LevelSupported = levelFloat - end if - else if container = "ts" - if levelFloat > hevcTsLevelSupported - hevcTsLevelSupported = levelFloat - end if - end if - end for + + 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 - hevcHighestLevel = hevcMp4LevelSupported - if hevcTsLevelSupported > hevcMp4LevelSupported - hevcHighestLevel = hevcTsLevelSupported - end if - hevcLevelString = "120" - if hevcHighestLevel = 5.1 + if hevcLevelSupported = 5.1 hevcLevelString = "153" end if - codecProfileArray = { + hevcProfileArray = { "Type": "Video", "Codec": "hevc", "Conditions": [ @@ -707,7 +812,7 @@ function getDeviceProfile() as object { "Condition": "EqualsAny", "Property": "VideoProfile", - "Value": profileSupport["ts"]["hevc"].Keys().join("|"), + "Value": profileSupport["hevc"].Keys().join("|"), "IsRequired": false }, { @@ -719,15 +824,9 @@ function getDeviceProfile() as object ] } - ' set max resolution - if maxResMode = "everything" and maxResSetting <> "off" - codecProfileArray.Conditions.push(maxVideoHeightArray) - codecProfileArray.Conditions.push(maxVideoWidthArray) - end if - ' check user setting before adding VideoLevel restrictions - if not m.global.session.user.settings["playback.tryDirect.hevcProfileLevel"] - codecProfileArray.Conditions.push({ + if not globalUserSettings["playback.tryDirect.hevcProfileLevel"] + hevcProfileArray.Conditions.push({ "Condition": "LessThanEqual", "Property": "VideoLevel", "Value": hevcLevelString, @@ -735,32 +834,47 @@ function getDeviceProfile() as object }) 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 - codecProfileArray.Conditions.push(bitRateArray) + hevcProfileArray.Conditions.push(bitRateArray) end if - deviceProfile.CodecProfiles.push(codecProfileArray) + codecProfiles.push(hevcProfileArray) end if - if addVp9 + if di.CanDecodeVideo({ Codec: "vp9" }).Result vp9Profiles = [] - for each container in profileSupport - for each profile in profileSupport[container]["vp9"] - if vp9Profiles[profile] = invalid - vp9Profiles.push(profile) + 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 - codecProfileArray = { + vp9LevelString = "120" + if vp9LevelSupported = 5.1 + vp9LevelString = "153" + end if + + vp9ProfileArray = { "Type": "Video", "Codec": "vp9", "Conditions": [ { "Condition": "EqualsAny", - "Property": "VideoLevel", + "Property": "VideoProfile", "Value": vp9Profiles.join("|"), "IsRequired": false }, @@ -769,151 +883,61 @@ function getDeviceProfile() as object "Property": "VideoRangeType", "Value": vp9VideoRangeTypes, "IsRequired": false + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": vp9LevelString, + "IsRequired": false } ] } ' set max resolution - if maxResMode = "everything" and maxResSetting <> "off" - codecProfileArray.Conditions.push(maxVideoHeightArray) - codecProfileArray.Conditions.push(maxVideoWidthArray) + 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 - codecProfileArray.Conditions.push(bitRateArray) + vp9ProfileArray.Conditions.push(bitRateArray) end if - deviceProfile.CodecProfiles.push(codecProfileArray) + codecProfiles.push(vp9ProfileArray) end if - return deviceProfile + return codecProfiles end function -function GetDirectPlayProfiles() as object - 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 m.global.session.user.settings["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 m.global.session.user.settings["playback.mpeg4"] - for each container in supportedCodecs - supportedCodecs[container]["video"].push("mpeg4") - end for - end if - if m.global.session.user.settings["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 - - ' 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 - returnArray = [] - 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 - - returnArray.push({ - "Container": containerString, - "Type": "Video", - "VideoCodec": videoCodecString, - "AudioCodec": supportedCodecs[container]["audio"].Join(",") - }) - end if - end for +function getSubtitleProfiles() as object + subtitleProfiles = [] - returnArray.push({ - "Container": supportedAudio.Join(","), - "Type": "Audio" + subtitleProfiles.push({ + "Format": "vtt", + "Method": "External" + }) + subtitleProfiles.push({ + "Format": "srt", + "Method": "External" + }) + subtitleProfiles.push({ + "Format": "ttml", + "Method": "External" }) - return returnArray + subtitleProfiles.push({ + "Format": "sub", + "Method": "External" + }) + + return subtitleProfiles end function function GetBitRateLimit(codec as string) as object - if m.global.session.user.settings["playback.bitrate.maxlimited"] = true - userSetLimit = m.global.session.user.settings["playback.bitrate.limit"].ToInt() + 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 { @@ -964,6 +988,58 @@ function GetBitRateLimit(codec as string) as object 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 @@ -985,7 +1061,6 @@ function updateProfileArray(profileArray as object, videoCodec as string, videoP end if end if - ' profileSupport[container][codec][profile][level] return profileArray end function