Skip to content

Commit

Permalink
Add volume state to playback rewrite
Browse files Browse the repository at this point in the history
  • Loading branch information
nielsvanvelzen committed Jun 3, 2023
1 parent 9657743 commit b6f3531
Show file tree
Hide file tree
Showing 12 changed files with 259 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.jellyfin.androidtv.data.eventhandling

import android.content.Context
import android.media.AudioManager
import android.widget.Toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -42,6 +43,7 @@ class SocketHandler(
private val mediaManager: MediaManager,
private val playbackControllerContainer: PlaybackControllerContainer,
private val navigationRepository: NavigationRepository,
private val audioManager: AudioManager,
) {
private val coroutineScope = CoroutineScope(Dispatchers.IO)
val state = socketInstance.state
Expand All @@ -51,11 +53,23 @@ class SocketHandler(
api.sessionApi.postCapabilities(
playableMediaTypes = listOf(MediaType.Video, MediaType.Audio),
supportsMediaControl = true,
supportedCommands = listOf(
GeneralCommandType.DISPLAY_MESSAGE,
GeneralCommandType.SEND_STRING,
GeneralCommandType.DISPLAY_CONTENT,
),
supportedCommands = buildList {
add(GeneralCommandType.DISPLAY_CONTENT)

add(GeneralCommandType.DISPLAY_MESSAGE)
add(GeneralCommandType.SEND_STRING)

// Note: These are used in the PlaySessionSocketService
if (!audioManager.isVolumeFixed) {
add(GeneralCommandType.VOLUME_UP)
add(GeneralCommandType.VOLUME_DOWN)
add(GeneralCommandType.SET_VOLUME)

add(GeneralCommandType.MUTE)
add(GeneralCommandType.UNMUTE)
add(GeneralCommandType.TOGGLE_MUTE)
}
},
)
} catch (err: ApiClientException) {
Timber.e(err, "Unable to update capabilities")
Expand Down Expand Up @@ -163,6 +177,7 @@ class SocketHandler(
val item by api.userLibraryApi.getItem(itemId = itemId)
ItemLauncher.launchUserView(item)
}

else -> navigationRepository.navigate(Destinations.itemDetails(itemId))
}
}
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/org/jellyfin/androidtv/di/AndroidModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.jellyfin.androidtv.di

import android.accounts.AccountManager
import android.app.UiModeManager
import android.media.AudioManager
import androidx.core.content.getSystemService
import androidx.work.WorkManager
import org.koin.android.ext.koin.androidApplication
Expand All @@ -13,5 +14,6 @@ import org.koin.dsl.module
val androidModule = module {
factory { androidApplication().getSystemService<AccountManager>()!! }
factory { androidApplication().getSystemService<UiModeManager>()!! }
factory { androidApplication().getSystemService<AudioManager>()!! }
factory { WorkManager.getInstance(get()) }
}
2 changes: 1 addition & 1 deletion app/src/main/java/org/jellyfin/androidtv/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ val appModule = module {

single { get<ApiClient>().ws() }

single { SocketHandler(get(), get(), get(), get(), get(), get(), get()) }
single { SocketHandler(get(), get(), get(), get(), get(), get(), get(), get()) }

// Old apiclient
single {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import org.jellyfin.playback.core.mediasession.mediaSessionPlugin
import org.jellyfin.playback.core.playbackManager
import org.jellyfin.playback.exoplayer.exoPlayerPlugin
import org.jellyfin.playback.jellyfin.jellyfinPlugin
import org.koin.android.ext.koin.androidContext
import org.koin.core.scope.Scope
import org.koin.dsl.module
import org.jellyfin.androidtv.ui.playback.PlaybackManager as LegacyPlaybackManager
Expand Down Expand Up @@ -49,7 +50,7 @@ val playbackModule = module {
single { createPlaybackManager() }
}

fun Scope.createPlaybackManager() = playbackManager {
fun Scope.createPlaybackManager() = playbackManager(androidContext()) {
install(exoPlayerPlugin(get()))
install(jellyfinPlugin(get(), get()))

Expand Down
14 changes: 10 additions & 4 deletions playback/core/src/main/kotlin/PlaybackManagerBuilder.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package org.jellyfin.playback.core

import android.content.Context
import android.os.Build
import androidx.core.content.getSystemService
import org.jellyfin.playback.core.backend.PlayerBackend
import org.jellyfin.playback.core.mediastream.MediaStreamResolver
import org.jellyfin.playback.core.plugin.PlaybackPlugin
import org.jellyfin.playback.core.plugin.PlayerService

class PlaybackManagerBuilder {
class PlaybackManagerBuilder(context: Context) {
private val factories = mutableListOf<PlaybackPlugin>()
val options = PlaybackManagerOptions()
private val volumeState = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) NoOpPlayerVolumeState()
else AndroidPlayerVolumeState(audioManager = requireNotNull(context.getSystemService()))

val options = PlaybackManagerOptions(volumeState)

fun install(pluginFactory: PlaybackPlugin) {
factories.add(pluginFactory)
Expand Down Expand Up @@ -40,5 +46,5 @@ class PlaybackManagerBuilder {
}
}

fun playbackManager(init: PlaybackManagerBuilder.() -> Unit): PlaybackManager =
PlaybackManagerBuilder().apply { init() }.build()
fun playbackManager(context: Context, init: PlaybackManagerBuilder.() -> Unit): PlaybackManager =
PlaybackManagerBuilder(context).apply { init() }.build()
5 changes: 4 additions & 1 deletion playback/core/src/main/kotlin/PlaybackManagerOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package org.jellyfin.playback.core
import kotlinx.coroutines.flow.MutableStateFlow
import kotlin.time.Duration.Companion.seconds

class PlaybackManagerOptions {
class PlaybackManagerOptions(
val playerVolumeState: PlayerVolumeState,
) {
var defaultRewindAmount = MutableStateFlow(10.seconds)
var defaultFastForwardAmount = MutableStateFlow(10.seconds)

}
3 changes: 3 additions & 0 deletions playback/core/src/main/kotlin/PlayerState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import kotlin.time.Duration

interface PlayerState {
val queue: PlayerQueueState
val volume: PlayerVolumeState
val playState: StateFlow<PlayState>
val speed: StateFlow<Float>
val videoSize: StateFlow<VideoSize>
Expand Down Expand Up @@ -63,6 +64,7 @@ class MutablePlayerState(
private val backendService: BackendService,
) : PlayerState {
override val queue: PlayerQueueState
override val volume: PlayerVolumeState

private val _playState = MutableStateFlow(PlayState.STOPPED)
override val playState: StateFlow<PlayState> get() = _playState.asStateFlow()
Expand Down Expand Up @@ -96,6 +98,7 @@ class MutablePlayerState(
})

queue = DefaultPlayerQueueState(this, scope, backendService)
volume = options.playerVolumeState
}

override fun play(playQueue: Queue) {
Expand Down
131 changes: 131 additions & 0 deletions playback/core/src/main/kotlin/PlayerVolumeState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package org.jellyfin.playback.core

import android.media.AudioManager
import android.os.Build
import androidx.annotation.FloatRange
import androidx.annotation.RequiresApi
import timber.log.Timber
import kotlin.math.roundToInt

/**
* The state of the device volume. No flows are used due to platform limitations.
*/
interface PlayerVolumeState {
/**
* Whether the volume of the device is muted or not.
*/
val muted: Boolean

/**
* The current volume level of the device between 0f and 1f.
*/
val volume: Float

/**
* Whether the volume and mute state can be changed or not.
* Changing the volume/mute state will do nothing when false.
*/
val modifyable: Boolean

/**
* Mute the device.
*/
fun mute()

/**
* Unmute the device.
*/
fun unmute()

/**
* Increase the device volume by the device prefered amount.
*/
fun increaseVolume()

/**
* Decrease the device volume by the device prefered amount.
*/
fun decreaseVolume()

/**
* Set the device volume to a level between 0f and 1f.
*/
fun setVolume(volume: Float)
}

/**
* Implementation of [PlayerVolumeState] that does nothing. Useful for platforms that do not
* support changing the volume properties.
*/
class NoOpPlayerVolumeState : PlayerVolumeState {
override val muted = false
override val volume = 1f
override val modifyable = false

override fun mute() = Unit
override fun unmute() = Unit

override fun increaseVolume() = Unit
override fun decreaseVolume() = Unit
override fun setVolume(volume: Float) = Unit
}

@RequiresApi(Build.VERSION_CODES.M)
class AndroidPlayerVolumeState(
private val audioManager: AudioManager,
) : PlayerVolumeState {
private val stream = AudioManager.STREAM_MUSIC

override val muted: Boolean
get() = audioManager.isStreamMute(stream)
override val volume: Float
get() = audioManager.getStreamVolume(stream).toFloat() / audioManager.getStreamMaxVolume(stream)

override val modifyable: Boolean
get() = !audioManager.isVolumeFixed

override fun mute() {
if (!modifyable) return
audioManager.adjustStreamVolume(
stream,
AudioManager.ADJUST_MUTE,
AudioManager.FLAG_SHOW_UI
)
}

override fun unmute() {
if (!modifyable) return
audioManager.adjustStreamVolume(
stream,
AudioManager.ADJUST_UNMUTE,
AudioManager.FLAG_SHOW_UI
)
}

override fun increaseVolume() {
if (!modifyable) return
audioManager.adjustStreamVolume(
stream,
AudioManager.ADJUST_RAISE,
AudioManager.FLAG_SHOW_UI
)
}

override fun decreaseVolume() {
if (!modifyable) return
audioManager.adjustStreamVolume(
stream,
AudioManager.ADJUST_LOWER,
AudioManager.FLAG_SHOW_UI
)
}

override fun setVolume(@FloatRange(0.0, 1.0) volume: Float) {
require(volume in 0f..1f)
if (!modifyable) return
val maxVolume = audioManager.getStreamMaxVolume(stream)
val index = (volume * maxVolume).roundToInt()
Timber.d("volume=$volume, maxVolume=$maxVolume, index=$index")
audioManager.setStreamVolume(stream, index, AudioManager.FLAG_SHOW_UI)
}
}
23 changes: 20 additions & 3 deletions playback/core/src/main/kotlin/mediasession/MediaSessionPlayer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,10 @@ internal class MediaSessionPlayer(
// add(COMMAND_CHANGE_MEDIA_ITEMS)
// add(COMMAND_GET_AUDIO_ATTRIBUTES)
// add(COMMAND_GET_VOLUME)
// add(COMMAND_GET_DEVICE_VOLUME)
add(COMMAND_GET_DEVICE_VOLUME)
// add(COMMAND_SET_VOLUME)
// add(COMMAND_SET_DEVICE_VOLUME)
// add(COMMAND_ADJUST_DEVICE_VOLUME)
add(COMMAND_SET_DEVICE_VOLUME)
add(COMMAND_ADJUST_DEVICE_VOLUME)
// add(COMMAND_SET_VIDEO_SURFACE)
// add(COMMAND_GET_TEXT)
// add(COMMAND_SET_TRACK_SELECTION_PARAMETERS)
Expand Down Expand Up @@ -120,6 +120,8 @@ internal class MediaSessionPlayer(
setShuffleModeEnabled(state.playbackOrder.value != PlaybackOrder.DEFAULT)
setRepeatMode(if (state.repeatMode.value == RepeatMode.NONE) REPEAT_MODE_OFF else REPEAT_MODE_ALL)
setVideoSize(state.videoSize.value.let { VideoSize(it.width, it.height) })
setDeviceVolume((state.volume.volume * 100).toInt())
setIsDeviceMuted(state.volume.muted)
}.build()

override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> {
Expand Down Expand Up @@ -193,6 +195,21 @@ internal class MediaSessionPlayer(
return Futures.immediateVoidFuture()
}

override fun handleIncreaseDeviceVolume(): ListenableFuture<*> {
state.volume.increaseVolume()
return Futures.immediateVoidFuture()
}

override fun handleDecreaseDeviceVolume(): ListenableFuture<*> {
state.volume.decreaseVolume()
return Futures.immediateVoidFuture()
}

override fun handleSetDeviceVolume(deviceVolume: Int): ListenableFuture<*> {
state.volume.setVolume(deviceVolume / 100f)
return Futures.immediateVoidFuture()
}

override fun handleRelease(): ListenableFuture<*> {
return Futures.immediateVoidFuture()
}
Expand Down
5 changes: 3 additions & 2 deletions playback/jellyfin/src/main/kotlin/JellyfinPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ fun jellyfinPlugin(
) = playbackPlugin {
provide(UniversalAudioMediaStreamResolver(api))

provide(PlaySessionService(api))
provide(PlaySessionSocketService(socketInstance))
val playSessionService = PlaySessionService(api)
provide(playSessionService)
provide(PlaySessionSocketService(socketInstance, playSessionService))
}
Loading

0 comments on commit b6f3531

Please sign in to comment.