fix(remote-control): update connection handling and improve logging for remote control events

This commit is contained in:
Kingkor Roy Tirtho 2026-09-04 09:07:08 +06:00
parent df3a6dcbd2
commit c5d35bfc93
4 changed files with 27 additions and 10 deletions

View File

@ -101,13 +101,16 @@ class RemoteControlClient {
private suspend fun receiveLoop(host: String, port: Int) { private suspend fun receiveLoop(host: String, port: Int) {
val currentSession = session ?: return val currentSession = session ?: return
logger.d { "Starting receive loop for $host:$port" }
try { try {
for (frame in currentSession.incoming) { for (frame in currentSession.incoming) {
when (frame) { when (frame) {
is Frame.Text -> { is Frame.Text -> {
val text = frame.readText() val text = frame.readText()
logger.d { "Received frame: $text" }
try { try {
val event = json.decodeFromString(RemoteControlEvent.serializer(), text) val event = json.decodeFromString(RemoteControlEvent.serializer(), text)
logger.d { "Parsed event: $event" }
when (event) { when (event) {
is RemoteControlEvent.Connected -> { is RemoteControlEvent.Connected -> {
logger.i { "Connection authorized by server" } logger.i { "Connection authorized by server" }
@ -136,6 +139,7 @@ class RemoteControlClient {
else -> {} else -> {}
} }
} }
logger.d { "Receive loop exited normally" }
} catch (e: Exception) { } catch (e: Exception) {
logger.e(e) { "Error in receive loop" } logger.e(e) { "Error in receive loop" }
_connectionState.value = ConnectionState.Error(e.message ?: "Connection lost") _connectionState.value = ConnectionState.Error(e.message ?: "Connection lost")

View File

@ -74,7 +74,7 @@ class RemoteControlHandler(
val waitingMessage = RemoteControlEvent.WaitingForPermission( val waitingMessage = RemoteControlEvent.WaitingForPermission(
"Waiting for permission from $deviceName..." "Waiting for permission from $deviceName..."
) )
session.send(Frame.Text(json.encodeToString(RemoteControlEvent.WaitingForPermission.serializer(), waitingMessage))) session.send(Frame.Text(json.encodeToString(RemoteControlEvent.serializer(), waitingMessage)))
val request = ConnectionRequest( val request = ConnectionRequest(
deviceId = deviceId ?: "unknown", deviceId = deviceId ?: "unknown",
@ -99,14 +99,18 @@ class RemoteControlHandler(
} }
// Send connected message // Send connected message
session.send(Frame.Text(json.encodeToString(RemoteControlEvent.Connected.serializer(), RemoteControlEvent.Connected))) session.send(Frame.Text(json.encodeToString(RemoteControlEvent.serializer(), RemoteControlEvent.Connected)))
logger.i { "Remote control connection established from $deviceName ($deviceId)" } logger.i { "Remote control connection established from $deviceName ($deviceId)" }
// Broadcast initial player state so the controller shows current track info
broadcastState(session)
try { try {
handleControlLoop(session) handleControlLoop(session)
} catch (e: Exception) { } catch (e: Exception) {
logger.w(e) { "Error in remote control session" } logger.w(e) { "Error in remote control session" }
} finally { } finally {
logger.d { "Closing session in finally block" }
session.close() session.close()
} }
} }
@ -129,9 +133,11 @@ class RemoteControlHandler(
} }
private suspend fun handleControlLoop(session: WebSocketServerSession) { private suspend fun handleControlLoop(session: WebSocketServerSession) {
logger.d { "Starting control loop for session" }
for (frame in session.incoming) { for (frame in session.incoming) {
if (frame is Frame.Text) { if (frame is Frame.Text) {
val text = frame.readText() val text = frame.readText()
logger.d { "Received command: $text" }
try { try {
val envelope = json.decodeFromString(CommandEnvelope.serializer(), text) val envelope = json.decodeFromString(CommandEnvelope.serializer(), text)
handleCommand(session, envelope) handleCommand(session, envelope)
@ -141,6 +147,7 @@ class RemoteControlHandler(
} }
} }
} }
logger.d { "Control loop exited normally" }
} }
private suspend fun handleCommand(session: WebSocketServerSession, envelope: CommandEnvelope) { private suspend fun handleCommand(session: WebSocketServerSession, envelope: CommandEnvelope) {
@ -197,12 +204,12 @@ class RemoteControlHandler(
} }
private suspend fun sendAck(session: WebSocketServerSession, commandId: String) { private suspend fun sendAck(session: WebSocketServerSession, commandId: String) {
val text = json.encodeToString(RemoteControlEvent.Ack.serializer(), RemoteControlEvent.Ack(commandId)) val text = json.encodeToString(RemoteControlEvent.serializer(), RemoteControlEvent.Ack(commandId))
session.send(Frame.Text(text)) session.send(Frame.Text(text))
} }
private suspend fun sendError(session: WebSocketServerSession, message: String) { private suspend fun sendError(session: WebSocketServerSession, message: String) {
val text = json.encodeToString(RemoteControlEvent.Error.serializer(), RemoteControlEvent.Error(message)) val text = json.encodeToString(RemoteControlEvent.serializer(), RemoteControlEvent.Error(message))
session.send(Frame.Text(text)) session.send(Frame.Text(text))
} }
@ -221,7 +228,7 @@ class RemoteControlHandler(
currentTrackAlbum = current?.albumOrNull(), currentTrackAlbum = current?.albumOrNull(),
currentTrackCoverUrl = current?.coverUrlOrNull(), currentTrackCoverUrl = current?.coverUrlOrNull(),
) )
val text = json.encodeToString(RemoteControlEvent.PlayerState.serializer(), state) val text = json.encodeToString(RemoteControlEvent.serializer(), state)
session.send(Frame.Text(text)) session.send(Frame.Text(text))
} }

View File

@ -72,7 +72,8 @@ fun DevicesScreen(
viewModel.startDiscovery() viewModel.startDiscovery()
onDispose { onDispose {
viewModel.stopDiscovery() viewModel.stopDiscovery()
viewModel.disconnect() // Don't disconnect here - the connection should persist when navigating to RemoteControlScreen
// The RemoteControlViewModel will manage the connection lifecycle
} }
} }

View File

@ -57,6 +57,7 @@ import dev.krtirtho.spotube.core.ui.base.GhostIconButton
import dev.krtirtho.spotube.core.ui.base.IconButton import dev.krtirtho.spotube.core.ui.base.IconButton
import dev.krtirtho.spotube.core.ui.base.Slider import dev.krtirtho.spotube.core.ui.base.Slider
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset
import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxCloseSquare import dev.krtirtho.spotube.resources.iconsax.IconsaxCloseSquare
import dev.krtirtho.spotube.resources.iconsax.IconsaxNext import dev.krtirtho.spotube.resources.iconsax.IconsaxNext
@ -77,6 +78,7 @@ fun RemoteControlScreen(
val viewModel = koinViewModel<RemoteControlViewModel>() val viewModel = koinViewModel<RemoteControlViewModel>()
val playerState by viewModel.playerState.collectAsStateWithLifecycle() val playerState by viewModel.playerState.collectAsStateWithLifecycle()
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle() val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
val shellBottomInset = LocalAppShellBottomInset.current
Scaffold( Scaffold(
topBar = { topBar = {
@ -110,14 +112,15 @@ fun RemoteControlScreen(
onSetVolume = viewModel::setVolume, onSetVolume = viewModel::setVolume,
onToggleShuffle = viewModel::toggleShuffle, onToggleShuffle = viewModel::toggleShuffle,
onCycleLoopMode = viewModel::cycleLoopMode, onCycleLoopMode = viewModel::cycleLoopMode,
modifier = Modifier.padding(padding) modifier = Modifier.padding(padding).padding(bottom = shellBottomInset)
) )
} }
is ConnectionState.Connecting -> { is ConnectionState.Connecting -> {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(padding), .padding(padding)
.padding(bottom = shellBottomInset),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text("Connecting...") Text("Connecting...")
@ -127,7 +130,8 @@ fun RemoteControlScreen(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(padding), .padding(padding)
.padding(bottom = shellBottomInset),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text("Disconnected") Text("Disconnected")
@ -137,7 +141,8 @@ fun RemoteControlScreen(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(padding), .padding(padding)
.padding(bottom = shellBottomInset),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text("Connection error: ${(connectionState as ConnectionState.Error).message}") Text("Connection error: ${(connectionState as ConnectionState.Error).message}")