Skip to content

Commit d481aea

Browse files
Experimental option to transcode surround sound audio to AC3 (#947)
## Description Adds an experimental setting to prefer transcoding audio with more than 2 channels to AC3. Stereo audio does not transcode. This is basically a workaround for HDMI ARC (not eARC) or optical audio devices because instead of 2 uncompressed PCM channels, 6 AC3 channels (ie 5.1) is possible. An downside of enabling this is that embedded subtitles be burned in causing a video transcode as well. ### Related issues Fixes #255 Incorporates some code from #1410 ### Testing Just on the emulator so far to observe if the settings trigger the transcode or not ## Screenshots N/A ## AI or LLM usage None --------- Co-authored-by: darkflame91 <darkflame91@gmail.com>
1 parent f2287a8 commit d481aea

11 files changed

Lines changed: 205 additions & 54 deletions

File tree

app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ sealed interface AppPreference<Pref, T> {
5353
value: T?,
5454
): String? = null
5555

56-
fun validate(value: T): PreferenceValidation = PreferenceValidation.Valid
56+
fun validate(
57+
prefs: Pref,
58+
value: T,
59+
): PreferenceValidation = PreferenceValidation.Valid
5760

5861
companion object {
5962
val SkipForward =
@@ -391,18 +394,25 @@ sealed interface AppPreference<Pref, T> {
391394
defaultValue = true,
392395
getter = { it.playbackPreferences.overrides.ac3Supported },
393396
setter = { prefs, value ->
394-
prefs.updatePlaybackOverrides { ac3Supported = value }
397+
if (!value) prefs.updateExperimentalPreferences { preferAc3Surround = false }
398+
prefs.updatePlaybackOverrides {
399+
ac3Supported = value
400+
}
395401
},
396402
summaryOn = R.string.enabled,
397403
summaryOff = R.string.disabled,
398404
)
405+
399406
val DownMixStereo =
400407
AppSwitchPreference<AppPreferences>(
401408
title = R.string.downmix_stereo,
402409
defaultValue = false,
403410
getter = { it.playbackPreferences.overrides.downmixStereo },
404411
setter = { prefs, value ->
405-
prefs.updatePlaybackOverrides { downmixStereo = value }
412+
if (value) prefs.updateExperimentalPreferences { preferAc3Surround = false }
413+
prefs.updatePlaybackOverrides {
414+
downmixStereo = value
415+
}
406416
},
407417
summaryOn = R.string.enabled,
408418
summaryOff = R.string.disabled,
@@ -1311,11 +1321,16 @@ data class AppSwitchPreference<Pref>(
13111321
override val defaultValue: Boolean,
13121322
override val getter: (prefs: Pref) -> Boolean,
13131323
override val setter: (prefs: Pref, value: Boolean) -> Pref,
1314-
val validator: (value: Boolean) -> PreferenceValidation = { PreferenceValidation.Valid },
1324+
val validator: (prefs: Pref, value: Boolean) -> PreferenceValidation = { _, _ -> PreferenceValidation.Valid },
13151325
@param:StringRes val summary: Int? = null,
13161326
@param:StringRes val summaryOn: Int? = null,
13171327
@param:StringRes val summaryOff: Int? = null,
13181328
) : AppPreference<Pref, Boolean> {
1329+
override fun validate(
1330+
prefs: Pref,
1331+
value: Boolean,
1332+
): PreferenceValidation = validator.invoke(prefs, value)
1333+
13191334
override fun summary(
13201335
context: Context,
13211336
value: Boolean?,

app/src/main/java/com/github/damontecres/wholphin/preferences/ExperimentalPreference.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.github.damontecres.wholphin.R
44
import com.github.damontecres.wholphin.ui.nav.Destination
55
import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup
66
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
7+
import com.github.damontecres.wholphin.ui.preferences.PreferenceValidation
78

89
object ExperimentalPreference {
910
val Enable =
@@ -35,6 +36,30 @@ object ExperimentalPreference {
3536
summaryOn = R.string.enabled,
3637
summaryOff = R.string.disabled,
3738
)
39+
40+
val PreferAc3ForSurround =
41+
AppSwitchPreference<AppPreferences>(
42+
title = R.string.prefer_ac3_for_surround,
43+
defaultValue = true,
44+
getter = { it.experimentalPreferences.preferAc3Surround },
45+
setter = { prefs, value ->
46+
prefs.updateExperimentalPreferences {
47+
preferAc3Surround = value
48+
}
49+
},
50+
summary = R.string.prefer_ac3_for_surround_summary,
51+
validator = { prefs, value ->
52+
prefs.playbackPreferences.overrides.let {
53+
if (value && !it.ac3Supported) {
54+
PreferenceValidation.Invalid("AC3 support is not enabled")
55+
} else if (value && it.downmixStereo) {
56+
PreferenceValidation.Invalid("Always downmixing to stereo")
57+
} else {
58+
PreferenceValidation.Valid
59+
}
60+
}
61+
},
62+
)
3863
}
3964

4065
val experimentalPreferences =
@@ -45,6 +70,7 @@ val experimentalPreferences =
4570
preferences =
4671
listOf(
4772
ExperimentalPreference.VideoTunneling,
73+
ExperimentalPreference.PreferAc3ForSurround,
4874
),
4975
),
5076
)

app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.github.damontecres.wholphin.services
22

33
import android.content.Context
4+
import com.github.damontecres.wholphin.preferences.AppPreferences
45
import com.github.damontecres.wholphin.preferences.AssPlaybackMode
5-
import com.github.damontecres.wholphin.preferences.PlaybackPreferences
6+
import com.github.damontecres.wholphin.preferences.ExperimentalPreferences
7+
import com.github.damontecres.wholphin.preferences.PlaybackOverrides
68
import com.github.damontecres.wholphin.util.WholphinDispatchers
79
import com.github.damontecres.wholphin.util.profile.MediaCodecCapabilitiesTest
810
import com.github.damontecres.wholphin.util.profile.createDeviceProfile
@@ -34,20 +36,17 @@ class DeviceProfileService
3436
private var deviceProfile: DeviceProfile? = null
3537

3638
suspend fun getOrCreateDeviceProfile(
37-
prefs: PlaybackPreferences,
39+
appPrefs: AppPreferences,
3840
serverVersion: ServerVersion?,
3941
): DeviceProfile =
4042
withContext(WholphinDispatchers.Default) {
43+
val prefs = appPrefs.playbackPreferences
4144
mutex.withLock {
4245
val newConfig =
4346
DeviceProfileConfiguration(
4447
maxBitrate = prefs.maxBitrate.toInt(),
45-
isAC3Enabled = prefs.overrides.ac3Supported,
46-
downMixAudio = prefs.overrides.downmixStereo,
47-
assPlaybackMode = prefs.overrides.assPlaybackMode,
48-
pgsDirectPlay = prefs.overrides.directPlayPgs,
49-
dolbyVisionELDirectPlay = prefs.overrides.directPlayDolbyVisionEL,
50-
decodeAv1 = prefs.overrides.decodeAv1,
48+
overrides = prefs.overrides,
49+
experimental = appPrefs.experimentalPreferences,
5150
jellyfinTenEleven =
5251
serverVersion != null && serverVersion >= ServerVersion(10, 11, 0),
5352
)
@@ -57,12 +56,13 @@ class DeviceProfileService
5756
createDeviceProfile(
5857
mediaTest = mediaCodecCapabilitiesTest,
5958
maxBitrate = newConfig.maxBitrate,
60-
isAC3Enabled = newConfig.isAC3Enabled,
61-
downMixAudio = newConfig.downMixAudio,
62-
assDirectPlay = newConfig.assPlaybackMode != AssPlaybackMode.ASS_TRANSCODE,
63-
pgsDirectPlay = newConfig.pgsDirectPlay,
64-
dolbyVisionELDirectPlay = newConfig.dolbyVisionELDirectPlay,
59+
isAC3Enabled = newConfig.overrides.ac3Supported,
60+
downMixAudio = newConfig.overrides.downmixStereo,
61+
assDirectPlay = newConfig.overrides.assPlaybackMode != AssPlaybackMode.ASS_TRANSCODE,
62+
pgsDirectPlay = newConfig.overrides.directPlayPgs,
63+
dolbyVisionELDirectPlay = newConfig.overrides.directPlayDolbyVisionEL,
6564
decodeAv1 = prefs.overrides.decodeAv1,
65+
preferAc3ForSurround = appPrefs.experimentalPreferences.preferAc3Surround,
6666
jellyfinTenEleven = newConfig.jellyfinTenEleven,
6767
)
6868
}
@@ -76,11 +76,7 @@ class DeviceProfileService
7676
*/
7777
data class DeviceProfileConfiguration(
7878
val maxBitrate: Int,
79-
val isAC3Enabled: Boolean,
80-
val downMixAudio: Boolean,
81-
val assPlaybackMode: AssPlaybackMode,
82-
val pgsDirectPlay: Boolean,
83-
val dolbyVisionELDirectPlay: Boolean,
84-
val decodeAv1: Boolean,
79+
val overrides: PlaybackOverrides,
80+
val experimental: ExperimentalPreferences,
8581
val jellyfinTenEleven: Boolean,
8682
)

app/src/main/java/com/github/damontecres/wholphin/services/MediaReportService.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,10 @@ class MediaReportService
6161
.getItem(itemId = item.id)
6262
.content.mediaSources
6363
val sourcesJson = json.encodeToString(sources)
64-
val playbackPrefs = userPreferencesService.getCurrent().appPreferences.playbackPreferences
64+
val appPreferences = userPreferencesService.getCurrent().appPreferences
6565
val serverVersion = serverRepository.currentServer?.serverVersion
6666
val deviceProfile =
67-
deviceProfileService.getOrCreateDeviceProfile(playbackPrefs, serverVersion)
67+
deviceProfileService.getOrCreateDeviceProfile(appPreferences, serverVersion)
6868
val deviceProfileJson = json.encodeToString(deviceProfile)
6969
val body =
7070
"""
@@ -76,7 +76,14 @@ class MediaReportService
7676
model=${Build.MODEL}
7777
apiLevel=${Build.VERSION.SDK_INT}
7878
79-
playbackPrefs=${playbackPrefs.toString().replace("\n", ", ").replace("\t", " ")}
79+
playbackPrefs=${
80+
appPreferences.playbackPreferences.toString().replace("\n", ", ")
81+
.replace("\t", " ")
82+
}
83+
experimental=${
84+
appPreferences.experimentalPreferences.toString().replace("\n", ", ")
85+
.replace("\t", " ")
86+
}
8087
8188
deviceProfile=$deviceProfileJson
8289

app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.github.damontecres.wholphin.data.model.AudioItem
1515
import com.github.damontecres.wholphin.data.model.BaseItem
1616
import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope
1717
import com.github.damontecres.wholphin.ui.DefaultItemFields
18+
import com.github.damontecres.wholphin.ui.gt
1819
import com.github.damontecres.wholphin.ui.main.settings.MoveDirection
1920
import com.github.damontecres.wholphin.ui.onMain
2021
import com.github.damontecres.wholphin.ui.seekBack
@@ -25,6 +26,7 @@ import com.github.damontecres.wholphin.util.LoadingState
2526
import com.github.damontecres.wholphin.util.PlaybackItemState
2627
import com.github.damontecres.wholphin.util.TrackActivityPlaybackListener
2728
import com.github.damontecres.wholphin.util.WholphinDispatchers
29+
import com.github.damontecres.wholphin.util.profile.Codec
2830
import com.github.damontecres.wholphin.util.profile.supportedAudioCodecs
2931
import dagger.hilt.android.qualifiers.ApplicationContext
3032
import kotlinx.coroutines.CoroutineScope
@@ -44,6 +46,7 @@ import org.jellyfin.sdk.api.client.extensions.universalAudioApi
4446
import org.jellyfin.sdk.api.sockets.subscribe
4547
import org.jellyfin.sdk.model.api.BaseItemKind
4648
import org.jellyfin.sdk.model.api.ImageType
49+
import org.jellyfin.sdk.model.api.MediaStreamType
4750
import org.jellyfin.sdk.model.api.PlayMethod
4851
import org.jellyfin.sdk.model.api.PlaystateCommand
4952
import org.jellyfin.sdk.model.api.PlaystateMessage
@@ -72,6 +75,7 @@ class MusicService
7275
private val playerFactory: PlayerFactory,
7376
private val serverRepository: ServerRepository,
7477
private val imageUrlService: ImageUrlService,
78+
private val userPreferencesService: UserPreferencesService,
7579
) {
7680
private val _state = MutableStateFlow(MusicServiceState.EMPTY)
7781
val state: StateFlow<MusicServiceState> = _state
@@ -92,6 +96,11 @@ class MusicService
9296
private var activityTracker: TrackActivityPlaybackListener? = null
9397
private var websocketJob: Job? = null
9498

99+
private suspend fun preferAc3Surround() =
100+
userPreferencesService
101+
.getCurrent()
102+
.appPreferences.experimentalPreferences.preferAc3Surround
103+
95104
/**
96105
* Start music playback
97106
*
@@ -195,10 +204,11 @@ class MusicService
195204
shuffled: Boolean,
196205
) {
197206
Timber.d("setQueue: %s items, shuffled=%s", items.size, shuffled)
207+
val preferAc3Surround = preferAc3Surround()
198208
val mediaItems =
199209
items
200210
.filter { it.type == BaseItemKind.AUDIO }
201-
.map(::convert)
211+
.map { convert(it, preferAc3Surround) }
202212
withContext(WholphinDispatchers.Main) {
203213
player.setMediaItems(mediaItems)
204214
player.shuffleModeEnabled = shuffled
@@ -215,7 +225,8 @@ class MusicService
215225
index: Int? = null,
216226
) {
217227
if (item.type == BaseItemKind.AUDIO) {
218-
val mediaItem = convert(item)
228+
val preferAc3Surround = preferAc3Surround()
229+
val mediaItem = convert(item, preferAc3Surround)
219230
withContext(WholphinDispatchers.Main) {
220231
if (index != null) {
221232
player.addMediaItem(index, mediaItem)
@@ -242,6 +253,7 @@ class MusicService
242253
list: BlockingList<BaseItem?>,
243254
startIndex: Int,
244255
) = loading {
256+
val preferAc3Surround = preferAc3Surround()
245257
var remaining = startIndex
246258
list.indices
247259
.chunked(25)
@@ -252,7 +264,7 @@ class MusicService
252264
list
253265
.getBlocking(it)
254266
?.takeIf { it.type == BaseItemKind.AUDIO }
255-
?.let(::convert)
267+
?.let { convert(it, preferAc3Surround) }
256268
} else {
257269
Timber.v("Skipping $remaining")
258270
remaining--
@@ -268,12 +280,36 @@ class MusicService
268280
/**
269281
* Converts a [BaseItem] into a [MediaItem] setting an [AudioItem] as its tag
270282
*/
271-
private fun convert(audio: BaseItem): MediaItem {
283+
private fun convert(
284+
audio: BaseItem,
285+
preferAc3Surround: Boolean,
286+
): MediaItem {
287+
val needsAc3Transcode =
288+
preferAc3Surround &&
289+
audio.data.mediaSources
290+
?.firstOrNull()
291+
?.mediaStreams
292+
?.any { stream ->
293+
stream.type == MediaStreamType.AUDIO &&
294+
stream.channels.gt(2)
295+
stream.codec != Codec.Audio.AC3
296+
} == true
272297
val url =
273-
api.universalAudioApi.getUniversalAudioStreamUrl(
274-
itemId = audio.id,
275-
container = audioFormats,
276-
)
298+
if (needsAc3Transcode) {
299+
api.universalAudioApi.getUniversalAudioStreamUrl(
300+
itemId = audio.id,
301+
container = listOf("mka"),
302+
transcodingContainer = "mka",
303+
maxAudioChannels = 6,
304+
transcodingAudioChannels = 6,
305+
audioCodec = Codec.Audio.AC3,
306+
)
307+
} else {
308+
api.universalAudioApi.getUniversalAudioStreamUrl(
309+
itemId = audio.id,
310+
container = audioFormats,
311+
)
312+
}
277313
Timber.i("url=%s", url)
278314
val imageUrl =
279315
audio.data.albumId?.let { albumId ->
@@ -354,7 +390,7 @@ class MusicService
354390
* Play this item next after the current, ie add the item as the next index in the queue
355391
*/
356392
suspend fun playNext(song: BaseItem) {
357-
val mediaItem = convert(song)
393+
val mediaItem = convert(song, preferAc3Surround())
358394
onMain {
359395
player.addMediaItem(state.value.currentIndex + 1, mediaItem)
360396
if (player.mediaItemCount == 1) {

app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import com.github.damontecres.wholphin.services.ScreensaverService
5454
import com.github.damontecres.wholphin.services.StreamChoiceService
5555
import com.github.damontecres.wholphin.services.UserPreferencesService
5656
import com.github.damontecres.wholphin.ui.formatBitrate
57+
import com.github.damontecres.wholphin.ui.gt
5758
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
5859
import com.github.damontecres.wholphin.ui.launchDefault
5960
import com.github.damontecres.wholphin.ui.launchIO
@@ -646,7 +647,7 @@ class PlaybackViewModel
646647
deviceProfile =
647648
if (currentPlayer.value!!.backend == PlayerBackend.EXO_PLAYER) {
648649
deviceProfileService.getOrCreateDeviceProfile(
649-
preferences.appPreferences.playbackPreferences,
650+
preferences.appPreferences,
650651
serverRepository.currentServer?.serverVersion,
651652
)
652653
} else {
@@ -855,9 +856,24 @@ class PlaybackViewModel
855856
// TODO there's probably no reason why we can't add external subtitles?
856857
Timber.v("changeStreams direct play")
857858

859+
// TODO Better way to handle unsupported types in general is needed
860+
// This is a workaround for switching to a non AC3 track when the user wants audio transcoded to AC3
861+
if (preferences.appPreferences.experimentalPreferences.preferAc3Surround && audioIndex != null) {
862+
currentPlayback.mediaSourceInfo.mediaStreams
863+
.orEmpty()
864+
.firstOrNull { it.index == audioIndex }
865+
?.let {
866+
if (it.channels.gt(2) && it.codec != Codec.Audio.AC3) {
867+
// User wants to transcode audio into AC3
868+
return@withContext false
869+
}
870+
}
871+
}
872+
858873
val source = currentPlayback.mediaSourceInfo
859874
val externalSubtitle = source.findExternalSubtitle(subtitleIndex)
860875

876+
// TODO there's probably no reason why we can't add external subtitles?
861877
if (externalSubtitle == null) {
862878
val result =
863879
withContext(WholphinDispatchers.Main) {

app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,7 @@ fun PreferencesContent(
621621
value = value,
622622
onNavigate = viewModel.navigationManager::navigateTo,
623623
onValueChange = { newValue ->
624-
val validation = pref.validate(newValue)
624+
val validation = pref.validate(preferences, newValue)
625625
when (validation) {
626626
is PreferenceValidation.Invalid -> {
627627
// TODO?

app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitlePreferencesContent.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ fun SubtitlePreferencesContent(
162162
value = value,
163163
onNavigate = viewModel.navigationManager::navigateTo,
164164
onValueChange = { newValue ->
165-
val validation = pref.validate(newValue)
165+
val validation = pref.validate(preferences, newValue)
166166
when (validation) {
167167
is PreferenceValidation.Invalid -> {
168168
// TODO?

0 commit comments

Comments
 (0)