Files
memby/server/internal/emby/device_profile.go
T

189 lines
6.1 KiB
Go

// 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
//
// Modifications Copyright (C) 2026 Memby contributors
// SPDX-License-Identifier: GPL-2.0-only
package emby
import "strconv"
// 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
}
func androidTVDeviceProfile(capabilities PlaybackCapabilities) map[string]any {
videoCodecs := directPlayVideoCodecs(capabilities.HEVC)
return map[string]any{
"Name": "Memby Android TV", "SupportedMediaTypes": "Video",
"DirectPlayProfiles": []map[string]string{
{
"Container": "mkv,mp4,m4v,mov,ts,mpegts", "VideoCodec": videoCodecs,
"AudioCodec": "aac,mp3", "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": "aac",
"Protocol": "hls", "Type": "Video", "Context": "Streaming",
},
},
"CodecProfiles": videoCodecProfiles(capabilities),
"SubtitleProfiles": androidTVSubtitleProfiles(),
}
}
// 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
}