// Playback profile generation is adapted from Wholphin's DeviceProfileUtils.kt, // itself derived from Jellyfin Android TV. // // Wholphin: https://github.com/damontecres/Wholphin // Jellyfin Android TV: https://github.com/jellyfin/jellyfin-androidtv // Moonfin audio profile: https://github.com/Moonfin-Client/Moonfin-Core // // Modifications Copyright (C) 2026 Memby contributors // SPDX-License-Identifier: GPL-2.0-only package emby import "strconv" var alwaysDecodableAudioCodecs = []string{ "aac", "mp3", "flac", "opus", "vorbis", "pcm_s16le", "pcm_s24le", } // PlaybackCapabilities is the Android decoder evidence captured for one TV session. // Zero values deliberately describe the legacy H.264-safe profile. type PlaybackCapabilities struct { H264Profiles []string H264Level int H264High10Level int H264MaxWidth int H264MaxHeight int HEVC bool HEVCMain bool HEVCMain10 bool HEVCMainLevel int HEVCMain10Level int HEVCMaxWidth int HEVCMaxHeight int HEVCHDR10 bool HEVCHDR10Plus bool HEVCDolbyVision bool AudioProfileV1 bool AudioPassthrough map[string]bool AudioDecode map[string]bool AudioMaxChannels int } func androidTVDeviceProfile(capabilities PlaybackCapabilities) map[string]any { videoCodecs := directPlayVideoCodecs(capabilities.HEVC) audioCodecs := directPlayAudioCodecs(capabilities) transcodeAudio := transcodeAudioCodecs(capabilities) return map[string]any{ "Name": "Memby Android TV", "SupportedMediaTypes": "Video", "DirectPlayProfiles": []map[string]string{ { "Container": "mkv,mp4,m4v,mov,ts,mpegts", "VideoCodec": videoCodecs, "AudioCodec": audioCodecs, "Type": "Video", }, }, "TranscodingProfiles": []map[string]string{ { // AllowVideoStreamCopy lets Emby keep a supported H.264/HEVC track and // convert only incompatible audio or subtitles into an HLS stream. "Container": "ts", "VideoCodec": videoCodecs, "AudioCodec": transcodeAudio, "Protocol": "hls", "Type": "Video", "Context": "Streaming", }, }, "CodecProfiles": append(videoCodecProfiles(capabilities), audioCodecProfile(capabilities)), "SubtitleProfiles": androidTVSubtitleProfiles(), } } func directPlayAudioCodecs(capabilities PlaybackCapabilities) string { if !capabilities.AudioProfileV1 { return "aac,mp3" } codecs := append([]string{}, alwaysDecodableAudioCodecs...) for _, codec := range []string{"ac3", "eac3", "atmos", "dts", "dts_hd", "truehd"} { if capabilities.AudioPassthrough[codec] || capabilities.AudioDecode[codec] { switch codec { case "dts": codecs = appendUnique(codecs, "dts", "dca") case "dts_hd": codecs = appendUnique(codecs, "dts", "dca", "dtshd") case "truehd": codecs = appendUnique(codecs, "truehd", "mlp") case "atmos": codecs = appendUnique(codecs, "eac3") default: codecs = appendUnique(codecs, codec) } } } return joinComma(codecs) } func transcodeAudioCodecs(capabilities PlaybackCapabilities) string { codecs := []string{} if capabilities.AudioPassthrough["eac3"] || capabilities.AudioPassthrough["atmos"] { codecs = append(codecs, "eac3") } if capabilities.AudioPassthrough["ac3"] { codecs = append(codecs, "ac3") } return joinComma(appendUnique(codecs, "aac", "mp3")) } func audioCodecProfile(capabilities PlaybackCapabilities) map[string]any { channels := capabilities.AudioMaxChannels if channels < 2 { channels = 2 } return map[string]any{ "Type": "VideoAudio", "Codec": "", "Conditions": []map[string]any{ profileCondition("LessThanEqual", "AudioChannels", strconv.Itoa(channels)), }, } } func appendUnique(values []string, additions ...string) []string { for _, addition := range additions { found := false for _, value := range values { if value == addition { found = true break } } if !found { values = append(values, addition) } } return values } func joinComma(values []string) string { result := "" for _, value := range values { if result != "" { result += "," } result += value } return result } // forceH264TranscodeProfile turns decoder recovery into an actual codec change. Merely // removing DirectPlayProfiles is insufficient: Emby may otherwise stream-copy HEVC into // the ordinary HLS transcoding profile and hand the failing decoder the same video again. func forceH264TranscodeProfile(profile map[string]any) { profile["DirectPlayProfiles"] = []map[string]string{} profile["TranscodingProfiles"] = []map[string]string{ { "Container": "ts", "VideoCodec": "h264", "AudioCodec": "aac", "Protocol": "hls", "Type": "Video", "Context": "Streaming", }, } profiles, _ := profile["CodecProfiles"].([]map[string]any) h264Profiles := make([]map[string]any, 0, len(profiles)) for _, codecProfile := range profiles { if codec, _ := codecProfile["Codec"].(string); codec == "h264" { h264Profiles = append(h264Profiles, codecProfile) } } profile["CodecProfiles"] = h264Profiles } func androidTVSubtitleProfiles() []map[string]string { return []map[string]string{ {"Format": "srt", "Method": "External"}, {"Format": "subrip", "Method": "External"}, {"Format": "ass", "Method": "External"}, {"Format": "ssa", "Method": "External"}, {"Format": "vtt", "Method": "External"}, {"Format": "webvtt", "Method": "External"}, {"Format": "mov_text", "Method": "External"}, {"Format": "tx3g", "Method": "External"}, {"Format": "pgs", "Method": "Encode"}, {"Format": "pgssub", "Method": "Encode"}, {"Format": "sup", "Method": "Encode"}, {"Format": "vobsub", "Method": "Encode"}, {"Format": "dvdsub", "Method": "Encode"}, } } func videoCodecProfiles(capabilities PlaybackCapabilities) []map[string]any { profiles := []map[string]any{} if len(capabilities.H264Profiles) > 0 { profiles = append(profiles, codecProfile("h264", []map[string]any{profileCondition("EqualsAny", "VideoProfile", joinProfiles(capabilities.H264Profiles))}, nil)) } profiles = appendLevelProfile( profiles, "h264", capabilities.H264Level, "baseline|constrained baseline|main|high", ) profiles = appendLevelProfile(profiles, "h264", capabilities.H264High10Level, "high 10") profiles = appendResolutionProfile( profiles, "h264", capabilities.H264MaxWidth, capabilities.H264MaxHeight, ) if !capabilities.HEVC { return profiles } hevcProfiles := []string{} if capabilities.HEVCMain { hevcProfiles = append(hevcProfiles, "main") } if capabilities.HEVCMain10 { hevcProfiles = append(hevcProfiles, "main 10") } if len(hevcProfiles) > 0 { profiles = append(profiles, codecProfile("hevc", []map[string]any{profileCondition("EqualsAny", "VideoProfile", joinProfiles(hevcProfiles))}, nil)) } profiles = appendLevelProfile(profiles, "hevc", capabilities.HEVCMainLevel, "main") profiles = appendLevelProfile(profiles, "hevc", capabilities.HEVCMain10Level, "main 10") profiles = appendResolutionProfile( profiles, "hevc", capabilities.HEVCMaxWidth, capabilities.HEVCMaxHeight, ) return profiles } func appendLevelProfile(profiles []map[string]any, codec string, level int, applyTo string) []map[string]any { if level <= 0 { return profiles } condition := "Equals" if containsPipe(applyTo) { condition = "EqualsAny" } return append(profiles, codecProfile(codec, []map[string]any{profileCondition("LessThanEqual", "VideoLevel", strconv.Itoa(level))}, []map[string]any{profileCondition(condition, "VideoProfile", applyTo)})) } func appendResolutionProfile(profiles []map[string]any, codec string, width, height int) []map[string]any { if width <= 0 || height <= 0 { return profiles } return append(profiles, codecProfile(codec, []map[string]any{ profileCondition("LessThanEqual", "Width", strconv.Itoa(width)), profileCondition("LessThanEqual", "Height", strconv.Itoa(height)), }, nil)) } func codecProfile(codec string, conditions, applyConditions []map[string]any) map[string]any { profile := map[string]any{ "Type": "Video", "Codec": codec, "Conditions": conditions, } if len(applyConditions) > 0 { profile["ApplyConditions"] = applyConditions } return profile } func profileCondition(condition, property, value string) map[string]any { return map[string]any{ "Condition": condition, "Property": property, "Value": value, "IsRequired": false, } } func joinProfiles(values []string) string { result := "" for _, value := range values { if result != "" { result += "|" } result += value } return result } func containsPipe(value string) bool { for _, char := range value { if char == '|' { return true } } return false }