diff --git a/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt b/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt index a7f4bebd07..fb418fe9f9 100644 --- a/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt +++ b/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt @@ -4,6 +4,7 @@ import android.content.Context import android.view.KeyEvent import androidx.preference.PreferenceManager import org.jellyfin.androidtv.preference.constant.AppTheme +import org.jellyfin.androidtv.preference.constant.AudioTranscodeTarget import org.jellyfin.androidtv.preference.constant.ClockBehavior import org.jellyfin.androidtv.preference.constant.NextUpBehavior import org.jellyfin.androidtv.preference.constant.PreferredVideoPlayer @@ -100,6 +101,11 @@ class UserPreferences(context: Context) : SharedPreferenceStore( */ var refreshRateSwitchingBehavior = enumPreference("refresh_rate_switching_behavior", RefreshRateSwitchingBehavior.DISABLED) + /** + * Enable 4k codecs for direct play and transcode target + */ + var enable4kSupport = booleanPreference("pref_enable_4k_support", DeviceUtils.has4kVideoSupport()) + /* Playback - Audio related */ /** * Preferred behavior for audio streaming. @@ -111,6 +117,21 @@ class UserPreferences(context: Context) : SharedPreferenceStore( */ var audioNightMode = enumPreference("audio_night_mode", false) + /** + * Enable mp2/mp3 + */ + var mpegEnabled = booleanPreference("pref_bitstream_mpeg", true) + + /** + * Enable PCM + */ + var pcmEnabled = booleanPreference("pref_bitstream_pcm", true) + + /** + * Enable AAC + */ + var aacEnabled = booleanPreference("pref_bitstream_aac", true) + /** * Enable DTS */ @@ -121,6 +142,26 @@ class UserPreferences(context: Context) : SharedPreferenceStore( */ var ac3Enabled = booleanPreference("pref_bitstream_ac3", !DeviceUtils.isFireTvStickGen1) + /** + * Enable EAC3 + */ + var eac3Enabled = booleanPreference("pref_bitstream_eac3", !DeviceUtils.isFireTvStickGen1) + + /** + * Enable TrueUD + */ + var thdEnabled = booleanPreference("pref_bitstream_thd", false) + + /** + * Enable Other + */ + var otherAudioEnabled = booleanPreference("pref_bitstream_other_audio", false) + + /** + * Preferred audio transcode target codec + */ + var audioTranscodeTarget = enumPreference("audio_transcode_target", AudioTranscodeTarget.AUTO) + /** * Default audio delay in milliseconds for libVLC */ diff --git a/app/src/main/java/org/jellyfin/androidtv/preference/constant/AudioTranscodeTarget.kt b/app/src/main/java/org/jellyfin/androidtv/preference/constant/AudioTranscodeTarget.kt new file mode 100644 index 0000000000..5df8ff818a --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/preference/constant/AudioTranscodeTarget.kt @@ -0,0 +1,23 @@ +package org.jellyfin.androidtv.preference.constant + +import org.jellyfin.androidtv.R +import org.jellyfin.preference.PreferenceEnum + +enum class AudioTranscodeTarget( + override val nameRes: Int, +) : PreferenceEnum { + /** + * Enable all codecs + */ + AUTO(R.string.lbl_audiotarget_auto), + + PCM(R.string.lbl_codec_pcm), + + AAC(R.string.lbl_codec_aac), + + AC3(R.string.lbl_codec_ac3), + + EAC3(R.string.lbl_codec_eac3), + + DTS(R.string.lbl_codec_dts) +} diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/LegacyMediaManager.java b/app/src/main/java/org/jellyfin/androidtv/ui/playback/LegacyMediaManager.java index 61df7f6810..0ad34e4f08 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/LegacyMediaManager.java +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/LegacyMediaManager.java @@ -19,10 +19,12 @@ import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import org.jellyfin.androidtv.R; +import org.jellyfin.androidtv.constant.Codec; import org.jellyfin.androidtv.constant.QueryType; import org.jellyfin.androidtv.data.compat.AudioOptions; import org.jellyfin.androidtv.data.compat.StreamInfo; import org.jellyfin.androidtv.data.model.DataRefreshService; +import org.jellyfin.androidtv.preference.UserPreferences; import org.jellyfin.androidtv.preference.UserSettingPreferences; import org.jellyfin.androidtv.ui.itemhandling.AudioQueueItem; import org.jellyfin.androidtv.ui.itemhandling.BaseRowItem; @@ -606,7 +608,10 @@ private void playInternal(final org.jellyfin.sdk.model.api.BaseItemDto item, fin options.setMediaSources(item.getMediaSources()); DeviceProfile profile; if (DeviceUtils.is60()) { - profile = new ExoPlayerProfile(context, false, false); + String[] audioDirectPlay = { Codec.Audio.AAC, Codec.Audio.MP3, Codec.Audio.MP2 }; + String[] audioTranscode = { Codec.Audio.AAC, Codec.Audio.MP3, Codec.Audio.MP2 }; + Boolean enable4kSupport = userPrefs.getValue().get(UserPreferences.Companion.getEnable4kSupport()); + profile = new ExoPlayerProfile(context, false, audioDirectPlay, audioTranscode, enable4kSupport); } else { profile = new LibVlcProfile(context, false); } diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java b/app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java index 402fca62f1..35227d9a4e 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/PlaybackController.java @@ -12,8 +12,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.acra.config.CoreConfiguration; import org.jellyfin.androidtv.R; import org.jellyfin.androidtv.auth.repository.UserRepository; +import org.jellyfin.androidtv.constant.Codec; import org.jellyfin.androidtv.data.compat.PlaybackException; import org.jellyfin.androidtv.data.compat.StreamInfo; import org.jellyfin.androidtv.data.compat.SubtitleStreamInfo; @@ -22,6 +24,7 @@ import org.jellyfin.androidtv.preference.SystemPreferences; import org.jellyfin.androidtv.preference.UserPreferences; import org.jellyfin.androidtv.preference.UserSettingPreferences; +import org.jellyfin.androidtv.preference.constant.AudioTranscodeTarget; import org.jellyfin.androidtv.preference.constant.NextUpBehavior; import org.jellyfin.androidtv.preference.constant.PreferredVideoPlayer; import org.jellyfin.androidtv.preference.constant.RefreshRateSwitchingBehavior; @@ -51,6 +54,8 @@ import org.jellyfin.sdk.model.api.PlayAccess; import org.koin.java.KoinJavaComponent; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import kotlin.Lazy; @@ -569,12 +574,84 @@ private VideoOptions buildExoPlayerOptions(@Nullable Integer forcedSubtitleIndex DeviceProfile internalProfile = new ExoPlayerProfile( mFragment.getContext(), isLiveTv && !userPreferences.getValue().get(UserPreferences.Companion.getLiveTvDirectPlayEnabled()), - userPreferences.getValue().get(UserPreferences.Companion.getAc3Enabled()) + getDirectPlayPreferences(), + getAudioTranscodeTarget(), + userPreferences.getValue().get(UserPreferences.Companion.getEnable4kSupport()) ); internalOptions.setProfile(internalProfile); return internalOptions; } + @NonNull + private String[] getDirectPlayPreferences() { + ArrayList codecs = new ArrayList<>(); + + if(userPreferences.getValue().get(UserPreferences.Companion.getMpegEnabled())) { + codecs.add(Codec.Audio.MP2); + codecs.add(Codec.Audio.MP3); + } + + if(userPreferences.getValue().get(UserPreferences.Companion.getAacEnabled())) { + codecs.add(Codec.Audio.AAC); + codecs.add(Codec.Audio.AAC_LATM); + } + + if(userPreferences.getValue().get(UserPreferences.Companion.getAc3Enabled())) { + codecs.add(Codec.Audio.AC3); + } + + if(userPreferences.getValue().get(UserPreferences.Companion.getEac3Enabled())) { + codecs.add(Codec.Audio.EAC3); + } + + if(userPreferences.getValue().get(UserPreferences.Companion.getDtsEnabled())) { + codecs.add(Codec.Audio.DTS); + } + + if(userPreferences.getValue().get(UserPreferences.Companion.getThdEnabled())) { + codecs.add(Codec.Audio.TRUEHD); + } + + if(userPreferences.getValue().get(UserPreferences.Companion.getPcmEnabled())) { + codecs.add(Codec.Audio.PCM_ALAW); + codecs.add(Codec.Audio.PCM_MULAW); + } + + if(userPreferences.getValue().get(UserPreferences.Companion.getOtherAudioEnabled())) { + codecs.add(Codec.Audio.DCA); + codecs.add(Codec.Audio.MLP); + codecs.add(Codec.Audio.OPUS); + codecs.add(Codec.Audio.FLAC); + } + + return codecs.toArray(new String[0]); + } + + @NonNull + private String[] getAudioTranscodeTarget() { + AudioTranscodeTarget target = userPreferences.getValue().get(UserPreferences.Companion.getAudioTranscodeTarget()); + switch (target) { + case AUTO: + String[] directPlayCodecs = getDirectPlayPreferences(); + // If any codecs are enabled, return those + if(directPlayCodecs.length > 0) return directPlayCodecs; + // Otherwise fallback to MP3 + return new String[]{ Codec.Audio.MP3 }; + case AAC: + return new String[] { Codec.Audio.AAC, Codec.Audio.AAC_LATM }; + case DTS: + return new String[]{ Codec.Audio.DTS }; + case PCM: + return new String[]{ Codec.Audio.PCM_ALAW, Codec.Audio.PCM_MULAW }; + case AC3: + return new String[]{ Codec.Audio.AC3 }; + case EAC3: + return new String[]{ Codec.Audio.EAC3 }; + default: + throw new IllegalArgumentException("Unknown audio transcode target preference"); + } + } + @NonNull private VideoOptions buildVLCOptions(@Nullable Integer forcedSubtitleIndex, org.jellyfin.sdk.model.api.BaseItemDto item, int maxBitrate) { VideoOptions vlcOptions = new VideoOptions(); diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/PlaybackPreferencesScreen.kt b/app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/PlaybackPreferencesScreen.kt index a8f9b032c2..6db4840234 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/PlaybackPreferencesScreen.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/PlaybackPreferencesScreen.kt @@ -5,6 +5,7 @@ import org.jellyfin.androidtv.R import org.jellyfin.androidtv.constant.getQualityProfiles import org.jellyfin.androidtv.preference.UserPreferences import org.jellyfin.androidtv.preference.constant.AudioBehavior +import org.jellyfin.androidtv.preference.constant.AudioTranscodeTarget import org.jellyfin.androidtv.preference.constant.NEXTUP_TIMER_DISABLED import org.jellyfin.androidtv.preference.constant.NextUpBehavior import org.jellyfin.androidtv.preference.constant.PreferredVideoPlayer @@ -107,6 +108,12 @@ class PlaybackPreferencesScreen : OptionsFragment() { bind(userPreferences, UserPreferences.refreshRateSwitchingBehavior) depends { DeviceUtils.is60() && userPreferences[UserPreferences.videoPlayer] != PreferredVideoPlayer.EXTERNAL } } + + checkbox { + setTitle(R.string.pref_enable_4k_support) + setContent(R.string.pref_enable_4k_support_description) + bind(userPreferences, UserPreferences.enable4kSupport) + } } category { @@ -171,6 +178,12 @@ class PlaybackPreferencesScreen : OptionsFragment() { depends { userPreferences[UserPreferences.videoPlayer] != PreferredVideoPlayer.EXTERNAL } } + enum { + setTitle(R.string.pref_audio_transcodetarget) + bind(userPreferences, UserPreferences.audioTranscodeTarget) + depends { userPreferences[UserPreferences.videoPlayer] != PreferredVideoPlayer.EXTERNAL } + } + checkbox { setTitle(R.string.pref_audio_night_mode) setContent(R.string.desc_audio_night_mode) @@ -178,20 +191,6 @@ class PlaybackPreferencesScreen : OptionsFragment() { depends { Build.VERSION.SDK_INT >= Build.VERSION_CODES.P } } - checkbox { - setTitle(R.string.lbl_bitstream_ac3) - setContent(R.string.desc_bitstream_ac3) - bind(userPreferences, UserPreferences.ac3Enabled) - depends { userPreferences[UserPreferences.videoPlayer] != PreferredVideoPlayer.EXTERNAL } - } - - checkbox { - setTitle(R.string.lbl_bitstream_dts) - setContent(R.string.desc_bitstream_ac3) - bind(userPreferences, UserPreferences.dtsEnabled) - depends { userPreferences[UserPreferences.videoPlayer] != PreferredVideoPlayer.EXTERNAL } - } - @Suppress("MagicNumber") seekbar { setTitle(R.string.pref_libvlc_audio_delay_title) @@ -204,6 +203,66 @@ class PlaybackPreferencesScreen : OptionsFragment() { } } + category { + setTitle(R.string.pref_audio_directplay) + + checkbox { + setTitle(R.string.lbl_codec_mpeg) + setContent(R.string.desc_bitstream_generic) + bind(userPreferences, UserPreferences.mpegEnabled) + depends { userPreferences[UserPreferences.videoPlayer] != PreferredVideoPlayer.EXTERNAL } + } + + checkbox { + setTitle(R.string.lbl_codec_pcm) + setContent(R.string.desc_bitstream_generic) + bind(userPreferences, UserPreferences.pcmEnabled) + depends { userPreferences[UserPreferences.videoPlayer] != PreferredVideoPlayer.EXTERNAL } + } + + checkbox { + setTitle(R.string.lbl_codec_aac) + setContent(R.string.desc_bitstream_generic) + bind(userPreferences, UserPreferences.aacEnabled) + depends { userPreferences[UserPreferences.videoPlayer] != PreferredVideoPlayer.EXTERNAL } + } + + checkbox { + setTitle(R.string.lbl_codec_dts) + setContent(R.string.desc_bitstream_generic) + bind(userPreferences, UserPreferences.dtsEnabled) + depends { userPreferences[UserPreferences.videoPlayer] != PreferredVideoPlayer.EXTERNAL } + } + + checkbox { + setTitle(R.string.lbl_codec_ac3) + setContent(R.string.desc_bitstream_generic) + bind(userPreferences, UserPreferences.ac3Enabled) + depends { userPreferences[UserPreferences.videoPlayer] != PreferredVideoPlayer.EXTERNAL } + } + + checkbox { + setTitle(R.string.lbl_codec_eac3) + setContent(R.string.desc_bitstream_generic) + bind(userPreferences, UserPreferences.eac3Enabled) + depends { userPreferences[UserPreferences.videoPlayer] != PreferredVideoPlayer.EXTERNAL } + } + + checkbox { + setTitle(R.string.lbl_codec_thd) + setContent(R.string.desc_bitstream_generic) + bind(userPreferences, UserPreferences.thdEnabled) + depends { userPreferences[UserPreferences.videoPlayer] != PreferredVideoPlayer.EXTERNAL } + } + + checkbox { + setTitle(R.string.lbl_codec_other) + setContent(R.string.desc_bitstream_generic) + bind(userPreferences, UserPreferences.otherAudioEnabled) + depends { userPreferences[UserPreferences.videoPlayer] != PreferredVideoPlayer.EXTERNAL } + } + } + category { setTitle(R.string.pref_music_cat) diff --git a/app/src/main/java/org/jellyfin/androidtv/util/profile/ExoPlayerProfile.kt b/app/src/main/java/org/jellyfin/androidtv/util/profile/ExoPlayerProfile.kt index c834697a10..61764bbc8b 100644 --- a/app/src/main/java/org/jellyfin/androidtv/util/profile/ExoPlayerProfile.kt +++ b/app/src/main/java/org/jellyfin/androidtv/util/profile/ExoPlayerProfile.kt @@ -28,7 +28,9 @@ import org.jellyfin.apiclient.model.dlna.TranscodingProfile class ExoPlayerProfile( context: Context, disableVideoDirectPlay: Boolean = false, - isAC3Enabled: Boolean = false, + audioDirectPlayCodecs : Array, + audioTranscodeTarget : Array, + enable4KSupport : Boolean ) : DeviceProfile() { private val downmixSupportedAudioCodecs = arrayOf( Codec.Audio.AAC, @@ -36,30 +38,6 @@ class ExoPlayerProfile( Codec.Audio.MP2 ) - /** - * Returns all audio codecs used commonly in video containers. - * This does not include containers / codecs found in audio files - */ - private val allSupportedAudioCodecs = buildList { - addAll(downmixSupportedAudioCodecs) - add(Codec.Audio.AAC_LATM) - add(Codec.Audio.ALAC) - if (isAC3Enabled) add(Codec.Audio.AC3) - if (isAC3Enabled) add(Codec.Audio.EAC3) - add(Codec.Audio.DCA) - add(Codec.Audio.DTS) - add(Codec.Audio.MLP) - add(Codec.Audio.TRUEHD) - add(Codec.Audio.PCM_ALAW) - add(Codec.Audio.PCM_MULAW) - add(Codec.Audio.OPUS) - add(Codec.Audio.FLAC) - }.toTypedArray() - - private val allSupportedAudioCodecsWithoutFFmpegExperimental = allSupportedAudioCodecs - .filterNot { it == Codec.Audio.DCA || it == Codec.Audio.TRUEHD } - .toTypedArray() - init { name = "AndroidTV-ExoPlayer" @@ -77,9 +55,9 @@ class ExoPlayerProfile( add(Codec.Video.H264) }.joinToString(",") audioCodec = when { - Utils.downMixAudio(context) -> downmixSupportedAudioCodecs - else -> allSupportedAudioCodecsWithoutFFmpegExperimental - }.joinToString(",") + Utils.downMixAudio(context) -> downmixSupportedAudioCodecs.joinToString(",") + else -> audioTranscodeTarget.joinToString(",") + } protocol = "hls" copyTimestamps = false }, @@ -124,12 +102,12 @@ class ExoPlayerProfile( audioCodec = when { Utils.downMixAudio(context) -> downmixSupportedAudioCodecs - else -> allSupportedAudioCodecs + else -> audioDirectPlayCodecs }.joinToString(",") }) } // Audio direct play - add(audioDirectPlayProfile(allSupportedAudioCodecs + arrayOf( + add(audioDirectPlayProfile(audioDirectPlayCodecs + arrayOf( Codec.Audio.MPA, Codec.Audio.WAV, Codec.Audio.WMA, @@ -150,7 +128,7 @@ class ExoPlayerProfile( conditions = buildList { add(h264VideoProfileCondition) add(h264VideoLevelProfileCondition) - if (!DeviceUtils.has4kVideoSupport()) addAll(max1080pProfileConditions) + if (!enable4KSupport) addAll(max1080pProfileConditions) }.toTypedArray() }) // H264 ref frames profile @@ -196,7 +174,7 @@ class ExoPlayerProfile( // AV1 profile add(deviceAV1CodecProfile) // Limit video resolution support for older devices - if (!DeviceUtils.has4kVideoSupport()) { + if (!enable4KSupport) { add(CodecProfile().apply { type = CodecType.Video conditions = max1080pProfileConditions diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index af11f2c1d4..2b653a94bb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,6 +8,8 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ Playback Max streaming bitrate + Enable 4k playback + Allow device to receive 4k streams Preferences Authentication Chapters @@ -175,8 +177,18 @@ " item added" Automatic playlist of favorite songs New premieres - Bitstream Dolby Digital audio - Requires capable hardware + Direct Play Audio Codecs + MP2/MP3 + PCM + AAC + DTS + Dolby Digital + Dolby Digital+ + Dolby TrueHD + Other audio codecs + Requires capable hardware + Audio transcode preference + Automatically choose Levels out audio volume automatically Zoom Auto crop @@ -194,7 +206,6 @@ Change background image to selected items Show season premieres Show a row of new episode pilots for any series you watch - Bitstream DTS audio In %d days Select date Just this once