Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate WebSockets to new SocketApi from SDK #1456

Merged
merged 3 commits into from
Mar 28, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,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.SocketHandler
import org.jellyfin.androidtv.integration.LeanbackChannelWorker
import org.jellyfin.androidtv.util.AutoBitrate
import org.koin.android.ext.android.get
Expand Down Expand Up @@ -61,6 +62,7 @@ class JellyfinApplication : TvApp() {
suspend fun onSessionStart() {
val workManager by inject<WorkManager>()
val autoBitrate by inject<AutoBitrate>()
val socketListener by inject<SocketHandler>()

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

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

socketListener.updateSession()
}

override fun attachBaseContext(base: Context?) {
Expand Down
16 changes: 0 additions & 16 deletions app/src/main/java/org/jellyfin/androidtv/TvApp.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.jellyfin.androidtv;

import android.app.Activity;
import android.app.Application;

import androidx.annotation.Nullable;
Expand All @@ -24,8 +23,6 @@ public class TvApp extends Application {
private BaseItemDto lastPlayedItem;
private PlaybackController playbackController;

private Activity currentActivity;

@Override
public void onCreate() {
super.onCreate();
Expand Down Expand Up @@ -55,19 +52,6 @@ public void setCurrentUser(UserDto currentUser) {
TvManager.clearCache();
}

/**
* @deprecated This function is causing a **lot** of issues because not all activities will set their self as "currentactivity". Try to receive a Context instance instead.
*/
@Deprecated
@Nullable
public Activity getCurrentActivity() {
return currentActivity;
}

public void setCurrentActivity(Activity activity) {
currentActivity = activity;
}

@Nullable
public PlaybackController getPlaybackController() {
return playbackController;
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,164 @@
package org.jellyfin.androidtv.data.eventhandling

import android.content.Context
import android.widget.Toast
import kotlinx.coroutines.CoroutineScope
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.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.UUID

class SocketHandler(
private val context: Context,
private val api: ApiClient,
private val dataRefreshService: DataRefreshService,
private val mediaManager: MediaManager,
) {
private val coroutineScope = CoroutineScope(Dispatchers.IO)
private var socketInstance: SocketInstance? = null

suspend fun updateSession() {
api.sessionApi.postCapabilities(
playableMediaTypes = listOf(MediaType.Video, MediaType.Audio),
nielsvanvelzen marked this conversation as resolved.
Show resolved Hide resolved
supportsMediaControl = true,
supportedCommands = listOf(
GeneralCommandType.DISPLAY_MESSAGE,
GeneralCommandType.SEND_STRING,
),
)

if (socketInstance != null) socketInstance?.updateCredentials()
else socketInstance = createInstance()
nielsvanvelzen marked this conversation as resolved.
Show resolved Hide resolved
}

private fun createInstance() = api.ws().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
mueslimak3r marked this conversation as resolved.
Show resolved Hide resolved
val text by message
val string by message

onDisplayMessage(header, text ?: string)
}
}

private fun onLibraryChanged(info: LibraryUpdateInfo) {
Timber.d(buildString {
appendLine("Library changed.")
appendLine("Added ${info.itemsAdded!!.size} items")
appendLine("Removed ${info.itemsRemoved!!.size} items")
appendLine("Updated ${info.itemsUpdated!!.size} items")
})

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(
mueslimak3r marked this conversation as resolved.
Show resolved Hide resolved
mueslimak3r marked this conversation as resolved.
Show resolved Hide resolved
itemId.toString(),
false,
message.request.startPositionTicks,
context
)
}

@Suppress("ComplexMethod")
private fun onPlayStateMessage(message: PlayStateMessage) = coroutineScope.launch(Dispatchers.Main) {
Timber.i("Received PlayStateMessage with command ${message.request.command}")
val playbackController = TvApp.getApplication()?.playbackController
// Audio playback uses the mediaManager, video playback and live tv use the playbackController
if (mediaManager.isAudioPlayerInitialized) when (message.request.command) {
PlaystateCommand.STOP -> mediaManager.stopAudio(true)
PlaystateCommand.PAUSE, PlaystateCommand.UNPAUSE, PlaystateCommand.PLAY_PAUSE -> mediaManager.playPauseAudio()
PlaystateCommand.NEXT_TRACK -> mediaManager.nextAudioItem()
PlaystateCommand.PREVIOUS_TRACK -> mediaManager.prevAudioItem()
// Not implemented
PlaystateCommand.SEEK,
PlaystateCommand.REWIND,
PlaystateCommand.FAST_FORWARD -> Unit
} else when (message.request.command) {
PlaystateCommand.STOP -> playbackController?.endPlayback(true)
PlaystateCommand.PAUSE, PlaystateCommand.UNPAUSE, PlaystateCommand.PLAY_PAUSE -> playbackController?.playPause()
PlaystateCommand.NEXT_TRACK -> playbackController?.next()
PlaystateCommand.PREVIOUS_TRACK -> playbackController?.prev()
PlaystateCommand.SEEK -> playbackController?.seek(
(message.request.seekPositionTicks ?: 0) / TICKS_TO_MS
)
// FIXME get rewind/forward amount from displayprefs
PlaystateCommand.REWIND -> playbackController?.skip(REWIND_MS)
PlaystateCommand.FAST_FORWARD -> playbackController?.skip(FORWARD_MS)
nielsvanvelzen marked this conversation as resolved.
Show resolved Hide resolved
}
}

// FIXME: Add an ItemLauncher function that accepts SDK BaseItemDto
// Add "GeneralCommandType.DISPLAY_CONTENT" to supportedCommands in capabilities to receive
// these messages. Otherwise this function is never called.
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")

coroutineScope.launch {
val item by api.userLibraryApi.getItem(itemId = itemId)
// 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()
}
}

companion object {
const val TICKS_TO_MS = 10000L
const val REWIND_MS = -11000
const val FORWARD_MS = 30000
}
}
Loading