Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion automotive/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,11 @@ dependencies {

implementation 'androidx.media:media:1.7.1'
implementation 'com.android.car.ui:car-ui-lib:2.6.0'
implementation 'us.berkovitz:kotlin-plexapi:0.1.18'
implementation 'us.berkovitz:kotlin-plexapi:0.1.19'
implementation 'io.github.g0dkar:qrcode-kotlin-android:4.5.0'

api "androidx.media3:media3-exoplayer:1.9.0"
implementation "androidx.media3:media3-exoplayer-dash:1.9.0"
api "androidx.media3:media3-ui:1.9.0"
implementation "androidx.media3:media3-session:1.9.0"

Expand Down
33 changes: 32 additions & 1 deletion automotive/src/main/java/us/berkovitz/plexaaos/AndroidStorage.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
package us.berkovitz.plexaaos

import android.content.Context
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

object AndroidStorage {
const val MAXIMUM_AUDIO_QUALITY = -1 // Maximum (never transcode)
const val DEFAULT_TRANSCODE_QUALITY = 128
private const val SHARED_PREFS_NAME = "plexaaos"
private const val SERVER = "server"
private const val LAST_MEDIA_ID = "last_media_id"
private const val LAST_POSITION = "last_position"
private const val SHUFFLE_ENABLED = "shuffle_enabled"
private const val REPEAT_MODE = "repeat_mode"
private const val AUDIO_QUALITY = "audio_quality"
private const val TRANSCODE_QUALITY = "transcode_quality"

fun getKeySync(key: String, context: Context): String? {
return context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
?.getString(key, null)
}

suspend fun getKey(key: String, context: Context): String? {
return withContext(Dispatchers.IO) {
Expand Down Expand Up @@ -78,4 +86,27 @@ object AndroidStorage {
return setKey(SERVER, server, context)
}

suspend fun getAudioQuality(context: Context): Int {
return getKey(AUDIO_QUALITY, context)?.toIntOrNull() ?: MAXIMUM_AUDIO_QUALITY
}

fun getAudioQualitySync(context: Context): Int {
return getKeySync(AUDIO_QUALITY, context)?.toIntOrNull() ?: MAXIMUM_AUDIO_QUALITY
}

suspend fun setAudioQuality(quality: Int, context: Context){
return setKey(AUDIO_QUALITY, quality.toString(), context)
}

suspend fun getTranscodeQuality(context: Context): Int {
return getKey(TRANSCODE_QUALITY, context)?.toIntOrNull() ?: DEFAULT_TRANSCODE_QUALITY
}

fun getTranscodeQualitySync(context: Context): Int {
return getKeySync(TRANSCODE_QUALITY, context)?.toIntOrNull() ?: DEFAULT_TRANSCODE_QUALITY
}

suspend fun setTranscodeQuality(quality: Int, context: Context){
return setKey(TRANSCODE_QUALITY, quality.toString(), context)
}
}
48 changes: 37 additions & 11 deletions automotive/src/main/java/us/berkovitz/plexaaos/MediaService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.content.Intent
import android.os.Bundle
import android.support.v4.media.MediaMetadataCompat
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.media3.common.AudioAttributes
Expand All @@ -27,7 +26,6 @@ import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.LoadControl
import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadRequest
Expand All @@ -41,8 +39,6 @@ import androidx.media3.session.MediaSession
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionError
import androidx.media3.session.SessionResult
import androidx.media3.session.legacy.MediaSessionCompat
import androidx.media3.session.legacy.PlaybackStateCompat
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
Expand All @@ -51,9 +47,7 @@ import com.google.common.util.concurrent.SettableFuture
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import us.berkovitz.plexaaos.extensions.id
import us.berkovitz.plexaaos.library.BrowseTree
import us.berkovitz.plexaaos.library.MusicSource
import us.berkovitz.plexaaos.library.PlexSource
Expand All @@ -63,7 +57,6 @@ import us.berkovitz.plexaaos.library.buildMeta
import us.berkovitz.plexapi.media.Track
import us.berkovitz.plexapi.myplex.AuthorizationException
import java.io.File
import java.util.UUID
import java.util.concurrent.Executors
import kotlin.math.ceil
import kotlin.math.min
Expand Down Expand Up @@ -173,8 +166,6 @@ class PlexMediaService : MediaLibraryService() {
AndroidPlexApi.initPlexApi(this)
plexUtil = PlexUtil(this)



player = newPlayer()
mediaLibrarySession = newLibrarySession()

Expand Down Expand Up @@ -476,7 +467,7 @@ class PlexMediaService : MediaLibraryService() {
}

mediaSource.playlistWhenReady(playlistId) { plist ->
logger.error("plist when ready")
logger.debug("plist when ready")
browseTree.storePlaylist(plist)
if (plist != null && pageNum == null && plist.leafCount > PAGE_SIZE) {
val numPages = ceil(plist.leafCount.toDouble() / PAGE_SIZE).toInt()
Expand Down Expand Up @@ -516,7 +507,7 @@ class PlexMediaService : MediaLibraryService() {
if (item !is Track) {
return@forEach
}
children += MediaItem.Builder().buildMeta(item, playlistId, pageNum?.toString())
children += MediaItem.Builder().buildMeta(item, browseTree.audioQuality, browseTree.transcodeQuality, playlistId, pageNum?.toString())
}
logger.info("Sending playlist results: ${children.size} ${playlistId}")
future.set(children)
Expand Down Expand Up @@ -920,6 +911,15 @@ class PlexMediaService : MediaLibraryService() {
val uri = meta.localConfiguration?.uri ?: continue
val id = meta.mediaId

// Only prefetch tracks that wouldn't be transcoded. The Plex Transcoding API
// only supports a single transcoding session per device.
val extras = meta.mediaMetadata.extras
val transcodeUri = extras?.getString("TRANSCODE_URI")
if (transcodeUri != null && uri.toString() == transcodeUri) {
logger.warn("Skipping prefetch for transcoded track at index $windowIndex: $id")
continue
}

try {
// Create download request for the track
val downloadRequest = DownloadRequest.Builder(id, uri)
Expand Down Expand Up @@ -1015,6 +1015,32 @@ class PlexMediaService : MediaLibraryService() {
) {
message = "media not found";
}

val mediaItem = player.currentMediaItem
if (mediaItem != null) {
val extras = mediaItem.mediaMetadata.extras
val transcodeUri = extras?.getString("TRANSCODE_URI")
val rawUri = extras?.getString("URI")
val currentUri = mediaItem.localConfiguration?.uri?.toString()

if (transcodeUri != null && rawUri != null && currentUri == transcodeUri) {
logger.warn("Transcoding failed for ${currentUri}, falling back to raw stream at $rawUri")
message = "transcoding failed"

val newMediaItem = mediaItem.buildUpon()
.setUri(rawUri)
.build()

val currentIndex = player.currentMediaItemIndex
val playbackPosition = player.currentPosition

player.replaceMediaItem(currentIndex, newMediaItem)
player.seekTo(currentIndex, playbackPosition)
player.prepare()
player.play()
}
}

Toast.makeText(
applicationContext,
message,
Expand Down
34 changes: 34 additions & 0 deletions automotive/src/main/java/us/berkovitz/plexaaos/SettingsFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,17 @@ class SettingsFragment : PreferenceFragment() {

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupAudioQualityPreference()
setupTranscodeQualityPreference()
setupServerPreference()
setupUserPreference()
setupSignOutPreference()

// Observe view model
val serverPref = findPreference<ListPreference>("pref_server")
val userPref = findPreference<ListPreference>("pref_switch_user")
val audioPref = findPreference<ListPreference>("pref_audio_quality")
val transcodePref = findPreference<ListPreference>("pref_transcode_quality")
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
Expand Down Expand Up @@ -132,6 +136,16 @@ class SettingsFragment : PreferenceFragment() {
activity?.finish()
}
}
launch {
viewModel.audioQuality.collect { quality ->
audioPref?.value = quality.toString()
}
}
launch {
viewModel.transcodeQuality.collect { quality ->
transcodePref?.value = quality.toString()
}
}
}
}
}
Expand Down Expand Up @@ -202,4 +216,24 @@ class SettingsFragment : PreferenceFragment() {
true
}
}

private fun setupAudioQualityPreference() {
val audioPref = findPreference<ListPreference>("pref_audio_quality")
audioPref?.setOnPreferenceChangeListener { _, newValue ->
val qualityStr = newValue as? String ?: return@setOnPreferenceChangeListener true
val quality = qualityStr.toIntOrNull() ?: AndroidStorage.MAXIMUM_AUDIO_QUALITY
viewModel.setAudioQuality(quality)
true
}
}

private fun setupTranscodeQualityPreference() {
val transcodePref = findPreference<ListPreference>("pref_transcode_quality")
transcodePref?.setOnPreferenceChangeListener { _, newValue ->
val qualityStr = newValue as? String ?: return@setOnPreferenceChangeListener true
val quality = qualityStr.toIntOrNull() ?: AndroidStorage.DEFAULT_TRANSCODE_QUALITY
viewModel.setTranscodeQuality(quality)
true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import android.content.ComponentName
import android.os.Bundle
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -45,6 +44,12 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
private val _currentServerId = MutableStateFlow<String?>(SERVER_ID_AUTO)
val currentServerId: StateFlow<String?> = _currentServerId.asStateFlow()

private val _audioQuality = MutableStateFlow<Int>(AndroidStorage.MAXIMUM_AUDIO_QUALITY)
val audioQuality: StateFlow<Int> = _audioQuality.asStateFlow()

private val _transcodeQuality = MutableStateFlow<Int>(AndroidStorage.DEFAULT_TRANSCODE_QUALITY)
val transcodeQuality: StateFlow<Int> = _transcodeQuality.asStateFlow()

private val _userSwitchStatus = MutableStateFlow<UserSwitchStatus>(UserSwitchStatus.Idle)
val userSwitchStatus: StateFlow<UserSwitchStatus> = _userSwitchStatus.asStateFlow()

Expand All @@ -64,6 +69,18 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
}
_currentServerId.value = savedServerId ?: SERVER_ID_AUTO
}
launch {
val savedAudioQuality = withContext(Dispatchers.IO) {
AndroidStorage.getAudioQuality(getApplication())
}
_audioQuality.value = savedAudioQuality
}
launch {
val savedTranscodeQuality = withContext(Dispatchers.IO) {
AndroidStorage.getTranscodeQuality(getApplication())
}
_transcodeQuality.value = savedTranscodeQuality
}
launch {
try {
val fetchedServers = withContext(Dispatchers.IO) {
Expand Down Expand Up @@ -101,6 +118,32 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
}
}

fun setAudioQuality(quality: Int) {
if (quality == _audioQuality.value) return
applicationScope.launch {
withContext(Dispatchers.IO) {
AndroidStorage.setAudioQuality(quality, getApplication())
}
_audioQuality.value = quality
withContext(Dispatchers.Main) {
notifyRefresh()
}
}
}

fun setTranscodeQuality(quality: Int) {
if (quality == _transcodeQuality.value) return
applicationScope.launch {
withContext(Dispatchers.IO) {
AndroidStorage.setTranscodeQuality(quality, getApplication())
}
_transcodeQuality.value = quality
withContext(Dispatchers.Main) {
notifyRefresh()
}
}
}

fun switchUser(userId: String, userTitle: String, pin: String?) {
val token = plexToken ?: return
_userSwitchStatus.value = UserSwitchStatus.Switching
Expand Down
Loading