Skip to content

Commit

Permalink
Migrate WebSockets to new SocketApi from SDK
Browse files Browse the repository at this point in the history
  • Loading branch information
nielsvanvelzen committed Feb 19, 2022
1 parent c0875f0 commit a9aee39
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 241 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import org.acra.data.StringFormat
import org.acra.ktx.initAcra
import org.acra.sender.HttpSender
import org.jellyfin.androidtv.auth.SessionRepository
import org.jellyfin.androidtv.data.eventhandling.SocketListener
import org.jellyfin.androidtv.integration.LeanbackChannelWorker
import org.jellyfin.androidtv.util.AutoBitrate
import org.koin.android.ext.android.get
Expand Down Expand Up @@ -60,6 +61,7 @@ class JellyfinApplication : TvApp(), LifecycleObserver {
suspend fun onSessionStart() {
val workManager by inject<WorkManager>()
val autoBitrate by inject<AutoBitrate>()
val socketListener by inject<SocketListener>()

// Cancel all current workers
workManager.cancelAllWork().await()
Expand All @@ -75,6 +77,8 @@ class JellyfinApplication : TvApp(), LifecycleObserver {

// Detect auto bitrate
GlobalScope.launch(Dispatchers.IO) { autoBitrate.detect() }

socketListener.updateSession()
}

override fun attachBaseContext(base: Context?) {
Expand Down
19 changes: 0 additions & 19 deletions app/src/main/java/org/jellyfin/androidtv/auth/ApiBinder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,10 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.jellyfin.androidtv.JellyfinApplication
import org.jellyfin.androidtv.util.apiclient.callApi
import org.jellyfin.androidtv.util.apiclient.callApiEmpty
import org.jellyfin.androidtv.util.sdk.legacy
import org.jellyfin.apiclient.interaction.ApiClient
import org.jellyfin.apiclient.model.apiclient.ServerInfo
import org.jellyfin.apiclient.model.dto.UserDto
import org.jellyfin.apiclient.model.entities.MediaType
import org.jellyfin.apiclient.model.session.ClientCapabilities
import org.jellyfin.apiclient.model.session.GeneralCommandType
import org.jellyfin.sdk.model.DeviceInfo
import timber.log.Timber

Expand Down Expand Up @@ -61,21 +57,6 @@ class ApiBinder(
val user = callApi<UserDto> { callback -> api.GetUserAsync(session.userId.toString(), callback) }
application.currentUser = user

callApiEmpty { callback ->
api.ReportCapabilities(ClientCapabilities().apply {
playableMediaTypes = arrayListOf(MediaType.Video, MediaType.Audio)
supportsMediaControl = true
supportedCommands = arrayListOf(
GeneralCommandType.DisplayContent.toString(),
GeneralCommandType.DisplayMessage.toString(),
)
}, callback)
}

// Connect to WebSocket AFTER HTTP connection confirmed working
// to catch exceptions not catchable with the legacy websocket client
api.ensureWebSocket()

return true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package org.jellyfin.androidtv.data.eventhandling

import android.content.Context
import android.widget.Toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.jellyfin.androidtv.TvApp
import org.jellyfin.androidtv.data.model.DataRefreshService
import org.jellyfin.androidtv.ui.playback.MediaManager
import org.jellyfin.androidtv.util.apiclient.PlaybackHelper
import org.jellyfin.apiclient.model.entities.MediaType
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.sessionApi
import org.jellyfin.sdk.api.client.extensions.socket
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.api.sockets.SocketInstance
import org.jellyfin.sdk.api.sockets.addGeneralCommandsListener
import org.jellyfin.sdk.api.sockets.addListener
import org.jellyfin.sdk.model.api.GeneralCommandType
import org.jellyfin.sdk.model.api.LibraryUpdateInfo
import org.jellyfin.sdk.model.api.PlaystateCommand
import org.jellyfin.sdk.model.extensions.getValue
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import org.jellyfin.sdk.model.socket.LibraryChangedMessage
import org.jellyfin.sdk.model.socket.PlayMessage
import org.jellyfin.sdk.model.socket.PlayStateMessage
import timber.log.Timber
import java.util.*

class SocketListener(
private val context: Context,
private val api: ApiClient,
private val dataRefreshService: DataRefreshService,
private val mediaManager: MediaManager,
) {
private var socketInstance: SocketInstance? = null

suspend fun updateSession() {
api.sessionApi.postCapabilities(
playableMediaTypes = listOf(MediaType.Video, MediaType.Audio),
supportsMediaControl = true,
supportedCommands = listOf(
GeneralCommandType.DISPLAY_MESSAGE,
GeneralCommandType.SEND_STRING,
),
)

if (socketInstance != null) socketInstance?.updateCredentials()
else socketInstance = createInstance()
}

private fun createInstance() = api.socket.createInstance().apply {
// Library
addListener<LibraryChangedMessage> { message -> onLibraryChanged(message.info) }

// Media playback
addListener<PlayMessage> { message -> onPlayMessage(message) }
addListener<PlayStateMessage> { message -> onPlayStateMessage(message) }

// General commands
addGeneralCommandsListener(setOf(GeneralCommandType.DISPLAY_CONTENT)) { message ->
val itemId by message
val itemUuid = itemId?.toUUIDOrNull()

if (itemUuid != null) onDisplayContent(itemUuid)
}
addGeneralCommandsListener(setOf(GeneralCommandType.DISPLAY_MESSAGE, GeneralCommandType.SEND_STRING)) { message ->
val header by message
val text by message
val string by message

onDisplayMessage(header, text ?: string)
}
}

private fun onLibraryChanged(info: LibraryUpdateInfo) {
Timber.d(buildString {
append("Library changed.")
append(" added=", info.itemsAdded!!.size)
append(" removed=", info.itemsRemoved!!.size)
append(" updated=", info.itemsUpdated!!.size)
})

if (info.itemsAdded!!.any() || info.itemsRemoved!!.any())
dataRefreshService.lastLibraryChange = System.currentTimeMillis()
}

private fun onPlayMessage(message: PlayMessage) {
val itemId = message.request.itemIds?.firstOrNull() ?: return

PlaybackHelper.retrieveAndPlay(
itemId.toString(),
false,
message.request.startPositionTicks,
context
)
}

private fun onPlayStateMessage(message: PlayStateMessage) = socketInstance?.launch(Dispatchers.Main) {
Timber.i("Received PlayStateMessage with command ${message.request.command}")

val playbackController = TvApp.getApplication()!!.playbackController
when (message.request.command) {
PlaystateCommand.STOP -> {
if (mediaManager.isAudioPlayerInitialized) mediaManager.stopAudio(true)
else playbackController?.endPlayback(true)
}
PlaystateCommand.PAUSE,
PlaystateCommand.UNPAUSE,
PlaystateCommand.PLAY_PAUSE -> {
if (mediaManager.isAudioPlayerInitialized) mediaManager.playPauseAudio()
else playbackController?.playPause()
}
PlaystateCommand.NEXT_TRACK -> {
if (mediaManager.isAudioPlayerInitialized) mediaManager.nextAudioItem()
else playbackController?.next()
}
PlaystateCommand.PREVIOUS_TRACK -> {
if (mediaManager.isAudioPlayerInitialized) mediaManager.prevAudioItem()
else playbackController?.prev()
}
PlaystateCommand.SEEK -> {
if (mediaManager.isAudioPlayerInitialized) return@launch
val pos = (message.request.seekPositionTicks ?: 0) / 10000
playbackController?.seek(pos)
}
PlaystateCommand.REWIND -> {
// FIXME get value from displayprefs
if (!mediaManager.isAudioPlayerInitialized) playbackController?.skip(-11000)
}
PlaystateCommand.FAST_FORWARD -> {
// FIXME get value from displayprefs
if (!mediaManager.isAudioPlayerInitialized) playbackController?.skip(30000)
}
}
}

private fun onDisplayContent(itemId: UUID) {
val playbackController = TvApp.getApplication()?.playbackController

if (playbackController?.isPlaying == true || playbackController?.isPaused == true) {
Timber.i("Not launching $itemId: playback in progress")
return
}

Timber.i("Launching $itemId")

socketInstance?.launch {
val item by api.userLibraryApi.getItem(itemId = itemId)
// FIXME: Add an ItemLauncher function that accepts SDK BaseItemDto
// Add "GeneralCommandType.DISPLAY_CONTENT" to supportedCommands to enable
// ItemLauncher.launch(item)
}
}

private fun onDisplayMessage(header: String?, text: String?) {
val toastMessage = buildString {
if (!header.isNullOrBlank()) append(header, ": ")
append(text)
}

runBlocking(Dispatchers.Main) {
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show()
}
}
}
Loading

0 comments on commit a9aee39

Please sign in to comment.