From b30932a87ecdc64a49c92363293126eb6d84e0e0 Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Sun, 25 Aug 2024 23:27:34 +0200 Subject: [PATCH 01/22] Rough draft for playon compatibility for getting feedback --- lib/main.dart | 7 ++++ lib/services/playon_handler.dart | 59 ++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 lib/services/playon_handler.dart diff --git a/lib/main.dart b/lib/main.dart index 46bad0f5..6e6f9492 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,6 +18,7 @@ import 'package:finamp/services/finamp_settings_helper.dart'; import 'package:finamp/services/finamp_user_helper.dart'; import 'package:finamp/services/offline_listen_helper.dart'; import 'package:finamp/services/playback_history_service.dart'; +import 'package:finamp/services/playon_handler.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:finamp/services/theme_provider.dart'; import 'package:audio_service/audio_service.dart'; @@ -86,6 +87,7 @@ void main() async { await _setupDownloadsHelper(); await _setupOSIntegration(); await _setupPlaybackServices(); + await _setupPlayonHandler(); } catch (error, trace) { hasFailed = true; Logger("ErrorApp").severe(error, null, trace); @@ -151,6 +153,11 @@ Future<void> _setupDownloadsHelper() async { await downloadsService.startQueues(); } +Future<void> _setupPlayonHandler() async { + GetIt.instance.registerSingleton(PlayonHandler()); + GetIt.instance<PlayonHandler>().startListener(); +} + Future<void> setupHive() async { await Hive.initFlutter(); Hive.registerAdapter(BaseItemDtoAdapter()); diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart new file mode 100644 index 00000000..433d9004 --- /dev/null +++ b/lib/services/playon_handler.dart @@ -0,0 +1,59 @@ +import 'dart:async'; + +import 'package:finamp/models/finamp_models.dart'; +import 'package:finamp/services/queue_service.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; +import '../../services/music_player_background_task.dart'; +import '../../services/jellyfin_api_helper.dart'; +import 'dart:convert'; +import 'finamp_user_helper.dart'; + +import 'package:get_it/get_it.dart'; + + +class PlayonHandler { + Future<void> startListener() async { + final finampUserHelper = GetIt.instance<FinampUserHelper>(); + // final url="ws://192.168.1.30:8096/socket?api_key=${finampUserHelper.currentUser!.accessToken}&deviceId=AP2A.240705.005"; this should work but doesn't + final url="ws://192.168.1.30:8096/socket?api_key=${finampUserHelper.currentUser!.accessToken}&deviceId=TW96aWxsYS81LjAgKFgxMTsgTGludXggeDg2XzY0OyBydjoxMjguMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC8xMjguMHwxNzIyODczMDk3MDcy"; + final wsUrl = Uri.parse(url); + final channel = WebSocketChannel.connect(wsUrl); + final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>(); + final _jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>(); + final _queueService = GetIt.instance<QueueService>(); + + await channel.ready; + + // Will need to send a Keepalive message every 30 seconds, the code below didn't work + + // final Stream _myStream = + // Stream.periodic(const Duration(seconds: 30), (int count) { + // channel.sink.add('{"MessageType":"KeepAlive"}'); + // }); + + await for (final value in channel.stream) { + var response = jsonDecode(value); + + if (response['MessageType'] != 'ForceKeepAlive' && response['MessageType'] != 'KeepAlive') { + switch (response['Data']['PlayCommand']) { + case 'PlayNow': + channel.sink.add('{"MessageType":"KeepAlive"}'); + // print(response['Data']); + // print(response['Data']['ItemIds']); + var item = await _jellyfinApiHelper.getItemById(response['Data']['ItemIds'][0]); + unawaited(_queueService.startPlayback( + items: [item], + source: QueueItemSource( + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: item.name), + type: QueueItemSourceType.song, + id: item.id, + ), + )); + } + } + + } + } +} \ No newline at end of file From 3e38a9389bc6166dfd4fb0c90a5e497e6d8f5f3f Mon Sep 17 00:00:00 2001 From: Chaphasilor <ppp.u@web.de> Date: Wed, 14 Aug 2024 00:48:49 +0200 Subject: [PATCH 02/22] update client capabilities to allow for remote control, handle media commands --- lib/components/LoginScreen/login_flow.dart | 7 ++ lib/main.dart | 2 +- lib/models/jellyfin_models.dart | 4 +- lib/services/jellyfin_api.chopper.dart | 18 +++ lib/services/jellyfin_api.dart | 5 + lib/services/jellyfin_api_helper.dart | 5 + lib/services/playon_handler.dart | 126 ++++++++++++++++----- 7 files changed, 136 insertions(+), 31 deletions(-) diff --git a/lib/components/LoginScreen/login_flow.dart b/lib/components/LoginScreen/login_flow.dart index 4b5dd9da..8c12978e 100644 --- a/lib/components/LoginScreen/login_flow.dart +++ b/lib/components/LoginScreen/login_flow.dart @@ -120,6 +120,13 @@ class _LoginFlowState extends State<LoginFlow> { connectionState: connectionState, onAuthenticated: () { Navigator.of(context).popAndPushNamed(ViewSelector.routeName); + final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>(); + jellyfinApiHelper.updateCapabilities(ClientCapabilities( + supportsMediaControl: true, + supportsPersistentIdentifier: true, + playableMediaTypes: ["Audio"], + supportedCommands: ["MoveUp", "MoveDown", "MoveLeft", "MoveRight", "PageUp", "PageDown", "PreviousLetter", "NextLetter", "ToggleOsd", "ToggleContextMenu", "Select", "Back", "TakeScreenshot", "SendKey", "SendString", "GoHome", "GoToSettings", "VolumeUp", "VolumeDown", "Mute", "Unmute", "ToggleMute", "SetVolume", "SetAudioStreamIndex", "SetSubtitleStreamIndex", "ToggleFullscreen", "DisplayContent", "GoToSearch", "DisplayMessage", "SetRepeatMode", "ChannelUp", "ChannelDown", "Guide", "ToggleStats", "PlayMediaSource", "PlayTrailers", "SetShuffleQueue", "PlayState", "PlayNext", "ToggleOsdMenu", "Play", "SetMaxStreamingBitrate", "SetPlaybackOrder"], + )); }, )); break; diff --git a/lib/main.dart b/lib/main.dart index 6e6f9492..35a951ec 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -155,7 +155,7 @@ Future<void> _setupDownloadsHelper() async { Future<void> _setupPlayonHandler() async { GetIt.instance.registerSingleton(PlayonHandler()); - GetIt.instance<PlayonHandler>().startListener(); + unawaited(GetIt.instance<PlayonHandler>().initialize()); } Future<void> setupHive() async { diff --git a/lib/models/jellyfin_models.dart b/lib/models/jellyfin_models.dart index 03907a2f..84c4374c 100644 --- a/lib/models/jellyfin_models.dart +++ b/lib/models/jellyfin_models.dart @@ -772,10 +772,10 @@ class ClientCapabilities { this.supportedCommands, required this.supportsMediaControl, required this.supportsPersistentIdentifier, - required this.supportsSync, + this.supportsSync, this.deviceProfile, this.iconUrl, - required this.supportsContentUploading, + this.supportsContentUploading, this.messageCallbackUrl, this.appStoreUrl, }); diff --git a/lib/services/jellyfin_api.chopper.dart b/lib/services/jellyfin_api.chopper.dart index 636081af..ace4e2ad 100644 --- a/lib/services/jellyfin_api.chopper.dart +++ b/lib/services/jellyfin_api.chopper.dart @@ -343,6 +343,24 @@ final class _$JellyfinApi extends JellyfinApi { return $response.bodyOrThrow; } + @override + Future<dynamic> updateCapabilitiesFull( + ClientCapabilities clientCapabilities) async { + final Uri $url = Uri.parse('/Sessions/Capabilities/Full'); + final $body = clientCapabilities; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send<dynamic, dynamic>( + $request, + requestConverter: JsonConverter.requestFactory, + ); + return $response.bodyOrThrow; + } + @override Future<dynamic> startPlayback( PlaybackProgressInfo playbackProgressInfo) async { diff --git a/lib/services/jellyfin_api.dart b/lib/services/jellyfin_api.dart index 93240f9d..f5b5e6a9 100644 --- a/lib/services/jellyfin_api.dart +++ b/lib/services/jellyfin_api.dart @@ -260,6 +260,11 @@ abstract class JellyfinApi extends ChopperService { @Body() required BaseItemDto newItem, }); + @FactoryConverter(request: JsonConverter.requestFactory) + @Post(path: "/Sessions/Capabilities/Full") + Future<dynamic> updateCapabilitiesFull( + @Body() ClientCapabilities clientCapabilities); + @FactoryConverter(request: JsonConverter.requestFactory) @Post(path: "/Sessions/Playing") Future<dynamic> startPlayback( diff --git a/lib/services/jellyfin_api_helper.dart b/lib/services/jellyfin_api_helper.dart index d92648a9..3f643a37 100644 --- a/lib/services/jellyfin_api_helper.dart +++ b/lib/services/jellyfin_api_helper.dart @@ -469,6 +469,11 @@ class JellyfinApiHelper { return (QueryResult_BaseItemDto.fromJson(response).items); } + /// Updates capabilities for this client. + Future<void> updateCapabilities(ClientCapabilities capabilities) async { + await jellyfinApi.updateCapabilitiesFull(capabilities); + } + /// Tells the Jellyfin server that playback has started Future<void> reportPlaybackStart( PlaybackProgressInfo playbackProgressInfo) async { diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index 433d9004..94b0875a 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'package:finamp/models/finamp_models.dart'; +import 'package:finamp/models/jellyfin_models.dart'; import 'package:finamp/services/queue_service.dart'; +import 'package:logging/logging.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import '../../services/music_player_background_task.dart'; import '../../services/jellyfin_api_helper.dart'; @@ -10,19 +12,41 @@ import 'finamp_user_helper.dart'; import 'package:get_it/get_it.dart'; +final _playOnHandlerLogger = Logger("PlayOnHandler"); + class PlayonHandler { - Future<void> startListener() async { + + Future<void> initialize() async { + try { + await startListener(); + } catch (e) { + _playOnHandlerLogger.severe("Error initializing PlayOnHandler: $e"); + } + } + + Future<void> startListener() async { final finampUserHelper = GetIt.instance<FinampUserHelper>(); - // final url="ws://192.168.1.30:8096/socket?api_key=${finampUserHelper.currentUser!.accessToken}&deviceId=AP2A.240705.005"; this should work but doesn't - final url="ws://192.168.1.30:8096/socket?api_key=${finampUserHelper.currentUser!.accessToken}&deviceId=TW96aWxsYS81LjAgKFgxMTsgTGludXggeDg2XzY0OyBydjoxMjguMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC8xMjguMHwxNzIyODczMDk3MDcy"; - final wsUrl = Uri.parse(url); - final channel = WebSocketChannel.connect(wsUrl); + final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>(); + final queueService = GetIt.instance<QueueService>(); + + await jellyfinApiHelper.updateCapabilities(ClientCapabilities( + supportsMediaControl: true, + supportsPersistentIdentifier: true, + playableMediaTypes: ["Audio"], + supportedCommands: ["MoveUp", "MoveDown", "MoveLeft", "MoveRight", "PageUp", "PageDown", "PreviousLetter", "NextLetter", "ToggleOsd", "ToggleContextMenu", "Select", "Back", "TakeScreenshot", "SendKey", "SendString", "GoHome", "GoToSettings", "VolumeUp", "VolumeDown", "Mute", "Unmute", "ToggleMute", "SetVolume", "SetAudioStreamIndex", "SetSubtitleStreamIndex", "ToggleFullscreen", "DisplayContent", "GoToSearch", "DisplayMessage", "SetRepeatMode", "ChannelUp", "ChannelDown", "Guide", "ToggleStats", "PlayMediaSource", "PlayTrailers", "SetShuffleQueue", "PlayState", "PlayNext", "ToggleOsdMenu", "Play", "SetMaxStreamingBitrate", "SetPlaybackOrder"], + )); final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>(); - final _jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>(); - final _queueService = GetIt.instance<QueueService>(); + + final url="${finampUserHelper.currentUser!.baseUrl}/socket?api_key=${finampUserHelper.currentUser!.accessToken}"; + final parsedUrl = Uri.parse(url); + final wsUrl = parsedUrl.replace(scheme: parsedUrl.scheme == "https" ? "wss" : "ws"); + final channel = WebSocketChannel.connect(wsUrl); await channel.ready; + _playOnHandlerLogger.info("WebSocket connection to server established"); + + channel.sink.add('{"MessageType":"KeepAlive"}'); // Will need to send a Keepalive message every 30 seconds, the code below didn't work @@ -31,29 +55,75 @@ class PlayonHandler { // channel.sink.add('{"MessageType":"KeepAlive"}'); // }); + channel.stream.handleError((error, trace) { + _playOnHandlerLogger.severe("WebSocket Error: $error"); + }, + ); + await for (final value in channel.stream) { - var response = jsonDecode(value); - - if (response['MessageType'] != 'ForceKeepAlive' && response['MessageType'] != 'KeepAlive') { - switch (response['Data']['PlayCommand']) { - case 'PlayNow': - channel.sink.add('{"MessageType":"KeepAlive"}'); - // print(response['Data']); - // print(response['Data']['ItemIds']); - var item = await _jellyfinApiHelper.getItemById(response['Data']['ItemIds'][0]); - unawaited(_queueService.startPlayback( - items: [item], - source: QueueItemSource( - name: QueueItemSourceName( - type: QueueItemSourceNameType.preTranslated, - pretranslatedName: item.name), - type: QueueItemSourceType.song, - id: item.id, - ), - )); + + _playOnHandlerLogger.finest("Received message: $value"); + + var request = jsonDecode(value); + + if (request['MessageType'] != 'ForceKeepAlive' && request['MessageType'] != 'KeepAlive') { + _playOnHandlerLogger.info("Received a '${request['MessageType']}' message: ${request['Data']}"); + + switch (request['Data']['Command']) { + case "Stop": + await audioHandler.stop(); + break; + case "Pause": + await audioHandler.pause(); + break; + case "Unpause": + await audioHandler.play(); + break; + case "NextTrack": + await audioHandler.skipToNext(); + break; + case "PreviousTrack": + await audioHandler.skipToPrevious(); + break; + case "Seek": + // val to = message.data?.seekPositionTicks?.ticks ?: Duration.ZERO + final seekPosition = request['Data']['SeekPositionTicks'] != null ? Duration(milliseconds: ((request['Data']['SeekPositionTicks'] as int) / 10000).round()) : Duration.zero; + await audioHandler.seek(seekPosition); + break; + case "Rewind": + await audioHandler.rewind(); + break; + case "FastForward": + await audioHandler.fastForward(); + break; + case "PlayPause": + await audioHandler.togglePlayback(); + break; + + // Do nothing + default: + switch (request['Data']['PlayCommand']) { + case 'PlayNow': + channel.sink.add('{"MessageType":"KeepAlive"}'); + // print(request['Data']); + // print(request['Data']['ItemIds']); + var item = await jellyfinApiHelper.getItemById(request['Data']['ItemIds'][0]); + unawaited(queueService.startPlayback( + items: [item], + source: QueueItemSource( + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: item.name), + type: QueueItemSourceType.song, + id: item.id, + ), + )); + } + } } - } + // channel.sink.add('{"MessageType":"KeepAlive"}'); + } } -} \ No newline at end of file +} From a5c838f8e420ba7411024aa8d473431bfc851098 Mon Sep 17 00:00:00 2001 From: Chaphasilor <ppp.u@web.de> Date: Wed, 28 Aug 2024 22:05:17 +0200 Subject: [PATCH 03/22] support showing server messages --- lib/services/playon_handler.dart | 113 ++++++++++++++++++------------- 1 file changed, 65 insertions(+), 48 deletions(-) diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index 94b0875a..c274eb52 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:finamp/components/global_snackbar.dart'; import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/models/jellyfin_models.dart'; import 'package:finamp/services/queue_service.dart'; @@ -69,57 +70,73 @@ class PlayonHandler { if (request['MessageType'] != 'ForceKeepAlive' && request['MessageType'] != 'KeepAlive') { _playOnHandlerLogger.info("Received a '${request['MessageType']}' message: ${request['Data']}"); - switch (request['Data']['Command']) { - case "Stop": - await audioHandler.stop(); - break; - case "Pause": - await audioHandler.pause(); - break; - case "Unpause": - await audioHandler.play(); - break; - case "NextTrack": - await audioHandler.skipToNext(); - break; - case "PreviousTrack": - await audioHandler.skipToPrevious(); - break; - case "Seek": - // val to = message.data?.seekPositionTicks?.ticks ?: Duration.ZERO - final seekPosition = request['Data']['SeekPositionTicks'] != null ? Duration(milliseconds: ((request['Data']['SeekPositionTicks'] as int) / 10000).round()) : Duration.zero; - await audioHandler.seek(seekPosition); - break; - case "Rewind": - await audioHandler.rewind(); - break; - case "FastForward": - await audioHandler.fastForward(); + switch(request['MessageType']) { + case "GeneralCommand": + switch (request['Data']['Name']) { + case "DisplayMessage": + final messageFromServer = request['Data']['Arguments']['Text']; + final header = request['Data']['Arguments']['Header']; + final timeout = request['Data']['Arguments']['Timeout']; + _playOnHandlerLogger.info("Displaying message from server: '$messageFromServer'"); + GlobalSnackbar.message((context) => "$header: $messageFromServer"); + break; + } break; - case "PlayPause": - await audioHandler.togglePlayback(); + default: + switch (request['Data']['Command']) { + case "Stop": + await audioHandler.stop(); + break; + case "Pause": + await audioHandler.pause(); + break; + case "Unpause": + await audioHandler.play(); + break; + case "NextTrack": + await audioHandler.skipToNext(); + break; + case "PreviousTrack": + await audioHandler.skipToPrevious(); + break; + case "Seek": + // val to = message.data?.seekPositionTicks?.ticks ?: Duration.ZERO + final seekPosition = request['Data']['SeekPositionTicks'] != null ? Duration(milliseconds: ((request['Data']['SeekPositionTicks'] as int) / 10000).round()) : Duration.zero; + await audioHandler.seek(seekPosition); + break; + case "Rewind": + await audioHandler.rewind(); + break; + case "FastForward": + await audioHandler.fastForward(); + break; + case "PlayPause": + await audioHandler.togglePlayback(); + break; + + // Do nothing + default: + switch (request['Data']['PlayCommand']) { + case 'PlayNow': + channel.sink.add('{"MessageType":"KeepAlive"}'); + // print(request['Data']); + // print(request['Data']['ItemIds']); + var item = await jellyfinApiHelper.getItemById(request['Data']['ItemIds'][0]); + unawaited(queueService.startPlayback( + items: [item], + source: QueueItemSource( + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: item.name), + type: QueueItemSourceType.song, + id: item.id, + ), + )); + } + } break; + } - // Do nothing - default: - switch (request['Data']['PlayCommand']) { - case 'PlayNow': - channel.sink.add('{"MessageType":"KeepAlive"}'); - // print(request['Data']); - // print(request['Data']['ItemIds']); - var item = await jellyfinApiHelper.getItemById(request['Data']['ItemIds'][0]); - unawaited(queueService.startPlayback( - items: [item], - source: QueueItemSource( - name: QueueItemSourceName( - type: QueueItemSourceNameType.preTranslated, - pretranslatedName: item.name), - type: QueueItemSourceType.song, - id: item.id, - ), - )); - } - } } // channel.sink.add('{"MessageType":"KeepAlive"}'); From 218dcaefdc8657a684656e243ba3e89440ec53ec Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Thu, 19 Sep 2024 17:01:26 +0200 Subject: [PATCH 04/22] Fix wrong item being picked --- lib/services/playon_handler.dart | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index c274eb52..f527eaa2 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -3,11 +3,13 @@ import 'dart:async'; import 'package:finamp/components/global_snackbar.dart'; import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/models/jellyfin_models.dart'; +import 'package:finamp/services/audio_service_helper.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:logging/logging.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import '../../services/music_player_background_task.dart'; import '../../services/jellyfin_api_helper.dart'; +import '../../services/finamp_settings_helper.dart'; import 'dart:convert'; import 'finamp_user_helper.dart'; @@ -30,6 +32,7 @@ class PlayonHandler { final finampUserHelper = GetIt.instance<FinampUserHelper>(); final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>(); final queueService = GetIt.instance<QueueService>(); + final audioServiceHelper = GetIt.instance<AudioServiceHelper>(); await jellyfinApiHelper.updateCapabilities(ClientCapabilities( supportsMediaControl: true, @@ -120,18 +123,23 @@ class PlayonHandler { case 'PlayNow': channel.sink.add('{"MessageType":"KeepAlive"}'); // print(request['Data']); - // print(request['Data']['ItemIds']); - var item = await jellyfinApiHelper.getItemById(request['Data']['ItemIds'][0]); - unawaited(queueService.startPlayback( - items: [item], - source: QueueItemSource( - name: QueueItemSourceName( - type: QueueItemSourceNameType.preTranslated, - pretranslatedName: item.name), - type: QueueItemSourceType.song, - id: item.id, - ), - )); + // print(request['Data']['ItemIds']); + var item = await jellyfinApiHelper.getItemById(request['Data']['ItemIds'][request['Data']['StartIndex']]); + if (FinampSettingsHelper + .finampSettings.startInstantMixForIndividualTracks) { + unawaited(audioServiceHelper.startInstantMixForItem(item)); + } else { + unawaited(queueService.startPlayback( + items: [item], + source: QueueItemSource( + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: item.name), + type: QueueItemSourceType.song, + id: item.id, + ), + )); + } } } break; From 634ddde982165c366b39d666f97ccc55da950e31 Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Sat, 21 Sep 2024 10:47:40 +0200 Subject: [PATCH 05/22] Fix starting an album only queuing one song --- lib/services/playon_handler.dart | 43 ++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index f527eaa2..65805f7d 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -122,24 +122,41 @@ class PlayonHandler { switch (request['Data']['PlayCommand']) { case 'PlayNow': channel.sink.add('{"MessageType":"KeepAlive"}'); - // print(request['Data']); - // print(request['Data']['ItemIds']); - var item = await jellyfinApiHelper.getItemById(request['Data']['ItemIds'][request['Data']['StartIndex']]); + if (request['Data'].containsKey('StartIndex')) { // User started a single song + var item = await jellyfinApiHelper.getItemById(request['Data']['ItemIds'][request['Data']['StartIndex']]); if (FinampSettingsHelper .finampSettings.startInstantMixForIndividualTracks) { unawaited(audioServiceHelper.startInstantMixForItem(item)); - } else { + } else { + unawaited(queueService.startPlayback( + items: [item], + source: QueueItemSource( + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: item.name), + type: QueueItemSourceType.song, + id: item.id, + ), + )); + } + } else { // User asked to play an album + var items=<BaseItemDto>[]; + for (final itemId in request['Data']['ItemIds']) { + items.add(await jellyfinApiHelper.getItemById(itemId)); + } unawaited(queueService.startPlayback( - items: [item], - source: QueueItemSource( - name: QueueItemSourceName( - type: QueueItemSourceNameType.preTranslated, - pretranslatedName: item.name), - type: QueueItemSourceType.song, - id: item.id, - ), - )); + items: items, + source: QueueItemSource( + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: items[0].name), + type: QueueItemSourceType.song, + id: items[0].id, + ), + )); } + + } } break; From 2e253c144c2490baf65f35e6d595c88792e5a904 Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Sat, 21 Sep 2024 11:16:07 +0200 Subject: [PATCH 06/22] Send KeepAlive message every 30 seconds, apparently fix disconnection issues --- lib/services/playon_handler.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index 65805f7d..b1ada924 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -52,12 +52,13 @@ class PlayonHandler { channel.sink.add('{"MessageType":"KeepAlive"}'); - // Will need to send a Keepalive message every 30 seconds, the code below didn't work + final keepaliveSubscription = Stream.periodic(const Duration(seconds: 30), (count) { + return count; + }).listen((event) { + _playOnHandlerLogger.info("Sent KeepAlive message through websocket"); + channel.sink.add('{"MessageType":"KeepAlive"}'); + }); - // final Stream _myStream = - // Stream.periodic(const Duration(seconds: 30), (int count) { - // channel.sink.add('{"MessageType":"KeepAlive"}'); - // }); channel.stream.handleError((error, trace) { _playOnHandlerLogger.severe("WebSocket Error: $error"); From ef424d37b9462390be3de3adfbfa78b80177a446 Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Sat, 21 Sep 2024 12:32:16 +0200 Subject: [PATCH 07/22] WIP initial implementation of volume control --- lib/services/playon_handler.dart | 5 +++++ pubspec.yaml | 1 + 2 files changed, 6 insertions(+) diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index b1ada924..dd29cb62 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -7,6 +7,7 @@ import 'package:finamp/services/audio_service_helper.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:logging/logging.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; +import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import '../../services/music_player_background_task.dart'; import '../../services/jellyfin_api_helper.dart'; import '../../services/finamp_settings_helper.dart'; @@ -84,6 +85,10 @@ class PlayonHandler { _playOnHandlerLogger.info("Displaying message from server: '$messageFromServer'"); GlobalSnackbar.message((context) => "$header: $messageFromServer"); break; + case "SetVolume": + final desiredVolume=request['Data']['Arguments']['Volume']; + await FlutterVolumeController.setVolume(float.parse(desiredVolume)/100.0); + // We now have to report to jellyfin server, probably through playback_history_service, that we updated volume } break; default: diff --git a/pubspec.yaml b/pubspec.yaml index 495becbd..75190ba9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -101,6 +101,7 @@ dependencies: scroll_to_index: ^3.0.1 window_manager: ^0.3.8 url_launcher: ^6.2.6 + flutter_volume_controller: ^1.3.2 dev_dependencies: flutter_test: From 0b0722ad85924483e44a27788f296454f4058a0c Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Sat, 21 Sep 2024 13:37:05 +0200 Subject: [PATCH 08/22] Revert previous commit --- lib/services/playon_handler.dart | 5 ++--- pubspec.yaml | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index dd29cb62..365dca00 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -7,7 +7,6 @@ import 'package:finamp/services/audio_service_helper.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:logging/logging.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; -import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import '../../services/music_player_background_task.dart'; import '../../services/jellyfin_api_helper.dart'; import '../../services/finamp_settings_helper.dart'; @@ -86,8 +85,8 @@ class PlayonHandler { GlobalSnackbar.message((context) => "$header: $messageFromServer"); break; case "SetVolume": - final desiredVolume=request['Data']['Arguments']['Volume']; - await FlutterVolumeController.setVolume(float.parse(desiredVolume)/100.0); + _playOnHandlerLogger.info("Server requested a volume adjustment"); + // Waiting for planned issue #500 to be resolved to adjust volume // We now have to report to jellyfin server, probably through playback_history_service, that we updated volume } break; diff --git a/pubspec.yaml b/pubspec.yaml index 75190ba9..495becbd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -101,7 +101,6 @@ dependencies: scroll_to_index: ^3.0.1 window_manager: ^0.3.8 url_launcher: ^6.2.6 - flutter_volume_controller: ^1.3.2 dev_dependencies: flutter_test: From 9df5faf8872ba48eae949fbaa8eccabd5a0edfef Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Wed, 25 Sep 2024 15:46:27 +0200 Subject: [PATCH 09/22] Improve album queuing performance --- lib/services/playon_handler.dart | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index 365dca00..b1ab2b7b 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -145,16 +145,17 @@ class PlayonHandler { )); } } else { // User asked to play an album - var items=<BaseItemDto>[]; - for (final itemId in request['Data']['ItemIds']) { - items.add(await jellyfinApiHelper.getItemById(itemId)); - } + var items = await jellyfinApiHelper.getItems( + sortBy: "IndexNumber", + includeItemTypes: "Audio", + itemIds: List<String>.from(request['Data']['ItemIds'] as List), + ); unawaited(queueService.startPlayback( - items: items, + items: items!, source: QueueItemSource( name: QueueItemSourceName( type: QueueItemSourceNameType.preTranslated, - pretranslatedName: items[0].name), + pretranslatedName: items![0].name), type: QueueItemSourceType.song, id: items[0].id, ), From 76fcbc2301b7e9d84a2e84da173a86e7f8dbcbe6 Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Wed, 25 Sep 2024 16:45:01 +0200 Subject: [PATCH 10/22] Change queuing behaviour --- lib/services/playon_handler.dart | 58 ++++++++++++-------------------- 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index b1ab2b7b..45810605 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -127,44 +127,28 @@ class PlayonHandler { switch (request['Data']['PlayCommand']) { case 'PlayNow': channel.sink.add('{"MessageType":"KeepAlive"}'); - if (request['Data'].containsKey('StartIndex')) { // User started a single song - var item = await jellyfinApiHelper.getItemById(request['Data']['ItemIds'][request['Data']['StartIndex']]); - if (FinampSettingsHelper - .finampSettings.startInstantMixForIndividualTracks) { - unawaited(audioServiceHelper.startInstantMixForItem(item)); - } else { - unawaited(queueService.startPlayback( - items: [item], - source: QueueItemSource( - name: QueueItemSourceName( - type: QueueItemSourceNameType.preTranslated, - pretranslatedName: item.name), - type: QueueItemSourceType.song, - id: item.id, - ), - )); - } - } else { // User asked to play an album - var items = await jellyfinApiHelper.getItems( - sortBy: "IndexNumber", - includeItemTypes: "Audio", - itemIds: List<String>.from(request['Data']['ItemIds'] as List), - ); - unawaited(queueService.startPlayback( - items: items!, - source: QueueItemSource( - name: QueueItemSourceName( - type: QueueItemSourceNameType.preTranslated, - pretranslatedName: items![0].name), - type: QueueItemSourceType.song, - id: items[0].id, - ), - )); + if (!request['Data'].containsKey('StartIndex')) { + request['Data']['StartIndex']=0; } - - - } - } + var items = await jellyfinApiHelper.getItems( + sortBy: "IndexNumber", + includeItemTypes: "Audio", + itemIds: List<String>.from(request['Data']['ItemIds'] as List), + ); + unawaited(queueService.startPlayback( + items: items!, + source: QueueItemSource( + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: items![0].name), + type: QueueItemSourceType.song, + id: items[0].id, + ), + startingIndex: request['Data']['StartIndex'], + ) + ); + } + } break; } From 982a2c1e965fc7928938a9e8eda3cb353c4b917a Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Thu, 26 Sep 2024 14:22:09 +0200 Subject: [PATCH 11/22] Implement 'PlayNext' and 'PlayLast' play commands --- lib/services/playon_handler.dart | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index 45810605..68d7a0d3 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -147,6 +147,27 @@ class PlayonHandler { startingIndex: request['Data']['StartIndex'], ) ); + break; + case 'PlayNext': + var items = await jellyfinApiHelper.getItems( + sortBy: "IndexNumber", + includeItemTypes: "Audio", + itemIds: List<String>.from(request['Data']['ItemIds'] as List), + ); + unawaited(queueService.addToNextUp( + items: items!, + )); + break; + case 'PlayLast': + var items = await jellyfinApiHelper.getItems( + sortBy: "IndexNumber", + includeItemTypes: "Audio", + itemIds: List<String>.from(request['Data']['ItemIds'] as List), + ); + unawaited(queueService.addToQueue( + items: items!, + )); + break; } } break; From ba85206dff4a689b340e529e030e1de8ff19d8d6 Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Sun, 29 Sep 2024 18:37:15 +0200 Subject: [PATCH 12/22] Report playback to server when seeking --- lib/services/playon_handler.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index 68d7a0d3..174166df 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -4,6 +4,7 @@ import 'package:finamp/components/global_snackbar.dart'; import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/models/jellyfin_models.dart'; import 'package:finamp/services/audio_service_helper.dart'; +import 'package:finamp/services/playback_history_service.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:logging/logging.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; @@ -111,6 +112,10 @@ class PlayonHandler { // val to = message.data?.seekPositionTicks?.ticks ?: Duration.ZERO final seekPosition = request['Data']['SeekPositionTicks'] != null ? Duration(milliseconds: ((request['Data']['SeekPositionTicks'] as int) / 10000).round()) : Duration.zero; await audioHandler.seek(seekPosition); + final currentItem = queueService.getCurrentTrack(); + if (currentItem != null) { + unawaited(playbackHistoryService.onPlaybackStateChanged(currentItem, audioHandler.playbackState.value, null)); + } break; case "Rewind": await audioHandler.rewind(); From 7f8600a290dedaff643c42f628203a45fcb8394c Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Mon, 4 Nov 2024 12:19:30 +0100 Subject: [PATCH 13/22] fix playbackHistoryService not defined (forgot in last commit) --- lib/services/playon_handler.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index 174166df..c5fd1e82 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -34,6 +34,7 @@ class PlayonHandler { final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>(); final queueService = GetIt.instance<QueueService>(); final audioServiceHelper = GetIt.instance<AudioServiceHelper>(); + final playbackHistoryService = GetIt.instance<PlaybackHistoryService>(); await jellyfinApiHelper.updateCapabilities(ClientCapabilities( supportsMediaControl: true, From eda00bd00c88d43be349882672a2479b665ce453 Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Tue, 5 Nov 2024 13:53:42 +0100 Subject: [PATCH 14/22] Separate command handling code from the listener to allow (rough for now) websocket reconnection --- lib/services/playon_handler.dart | 50 ++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index c5fd1e82..7129a664 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:finamp/components/global_snackbar.dart'; import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/models/jellyfin_models.dart'; +import 'package:finamp/screens/active_downloads_screen.dart'; import 'package:finamp/services/audio_service_helper.dart'; import 'package:finamp/services/playback_history_service.dart'; import 'package:finamp/services/queue_service.dart'; @@ -17,6 +18,12 @@ import 'finamp_user_helper.dart'; import 'package:get_it/get_it.dart'; final _playOnHandlerLogger = Logger("PlayOnHandler"); +final finampUserHelper = GetIt.instance<FinampUserHelper>(); +final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>(); +final queueService = GetIt.instance<QueueService>(); +final audioServiceHelper = GetIt.instance<AudioServiceHelper>(); +final playbackHistoryService = GetIt.instance<PlaybackHistoryService>(); +final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>(); class PlayonHandler { @@ -30,19 +37,12 @@ class PlayonHandler { } Future<void> startListener() async { - final finampUserHelper = GetIt.instance<FinampUserHelper>(); - final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>(); - final queueService = GetIt.instance<QueueService>(); - final audioServiceHelper = GetIt.instance<AudioServiceHelper>(); - final playbackHistoryService = GetIt.instance<PlaybackHistoryService>(); - await jellyfinApiHelper.updateCapabilities(ClientCapabilities( supportsMediaControl: true, supportsPersistentIdentifier: true, playableMediaTypes: ["Audio"], supportedCommands: ["MoveUp", "MoveDown", "MoveLeft", "MoveRight", "PageUp", "PageDown", "PreviousLetter", "NextLetter", "ToggleOsd", "ToggleContextMenu", "Select", "Back", "TakeScreenshot", "SendKey", "SendString", "GoHome", "GoToSettings", "VolumeUp", "VolumeDown", "Mute", "Unmute", "ToggleMute", "SetVolume", "SetAudioStreamIndex", "SetSubtitleStreamIndex", "ToggleFullscreen", "DisplayContent", "GoToSearch", "DisplayMessage", "SetRepeatMode", "ChannelUp", "ChannelDown", "Guide", "ToggleStats", "PlayMediaSource", "PlayTrailers", "SetShuffleQueue", "PlayState", "PlayNext", "ToggleOsdMenu", "Play", "SetMaxStreamingBitrate", "SetPlaybackOrder"], )); - final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>(); final url="${finampUserHelper.currentUser!.baseUrl}/socket?api_key=${finampUserHelper.currentUser!.accessToken}"; final parsedUrl = Uri.parse(url); @@ -67,9 +67,26 @@ class PlayonHandler { }, ); - await for (final value in channel.stream) { + channel.stream.listen( + (dynamic message) { + _playOnHandlerLogger.severe("WebSocket connection to server established"); + unawaited(handleMessage(message)); + }, + onDone: () { + Future.delayed(const Duration(seconds: 10), () { + startListener(); + _playOnHandlerLogger.severe("Attempted to restart listener"); + }); + }, + onError: (error) { + _playOnHandlerLogger.severe("WebSocket connection to server established"); + }, + ); + } + - _playOnHandlerLogger.finest("Received message: $value"); + Future<void> handleMessage(value) async { + _playOnHandlerLogger.finest("Received message: $value"); var request = jsonDecode(value); @@ -98,10 +115,11 @@ class PlayonHandler { await audioHandler.stop(); break; case "Pause": - await audioHandler.pause(); + audioHandler.pause(); + _playOnHandlerLogger.severe("PAUSEEEEE !"); break; case "Unpause": - await audioHandler.play(); + audioHandler.play(); break; case "NextTrack": await audioHandler.skipToNext(); @@ -125,14 +143,13 @@ class PlayonHandler { await audioHandler.fastForward(); break; case "PlayPause": - await audioHandler.togglePlayback(); + audioHandler.togglePlayback(); break; // Do nothing default: switch (request['Data']['PlayCommand']) { case 'PlayNow': - channel.sink.add('{"MessageType":"KeepAlive"}'); if (!request['Data'].containsKey('StartIndex')) { request['Data']['StartIndex']=0; } @@ -178,11 +195,6 @@ class PlayonHandler { } break; } - - } - - // channel.sink.add('{"MessageType":"KeepAlive"}'); - - } + } } } From a5a45dc82e8602f366bb8d1aee943797ee0c323c Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Tue, 5 Nov 2024 15:27:28 +0100 Subject: [PATCH 15/22] close/open websocket connection when toggling offline mode --- lib/services/playon_handler.dart | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index 7129a664..2f1888a0 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -24,11 +24,20 @@ final queueService = GetIt.instance<QueueService>(); final audioServiceHelper = GetIt.instance<AudioServiceHelper>(); final playbackHistoryService = GetIt.instance<PlaybackHistoryService>(); final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>(); +var channel; class PlayonHandler { Future<void> initialize() async { + var settingsListener = FinampSettingsHelper.finampSettingsListener; + settingsListener.addListener(() { + if (FinampSettingsHelper.finampSettings.isOffline) { + closeListener(); + } else { + startListener(); + } + }); try { await startListener(); } catch (e) { @@ -47,7 +56,7 @@ class PlayonHandler { final url="${finampUserHelper.currentUser!.baseUrl}/socket?api_key=${finampUserHelper.currentUser!.accessToken}"; final parsedUrl = Uri.parse(url); final wsUrl = parsedUrl.replace(scheme: parsedUrl.scheme == "https" ? "wss" : "ws"); - final channel = WebSocketChannel.connect(wsUrl); + channel = WebSocketChannel.connect(wsUrl); await channel.ready; _playOnHandlerLogger.info("WebSocket connection to server established"); @@ -84,6 +93,10 @@ class PlayonHandler { ); } + Future<void> closeListener() async { + channel.sink.add('{"MessageType":"SessionsStop"}'); + channel.sink.close(); + } Future<void> handleMessage(value) async { _playOnHandlerLogger.finest("Received message: $value"); From 0f3b5589282e92ca3366ab36720c9897aa96b8d6 Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Tue, 5 Nov 2024 16:09:49 +0100 Subject: [PATCH 16/22] avoid crashing when remote client tries to play non audio files --- lib/services/playon_handler.dart | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index 2f1888a0..8892faea 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -171,18 +171,22 @@ class PlayonHandler { includeItemTypes: "Audio", itemIds: List<String>.from(request['Data']['ItemIds'] as List), ); - unawaited(queueService.startPlayback( - items: items!, - source: QueueItemSource( - name: QueueItemSourceName( - type: QueueItemSourceNameType.preTranslated, - pretranslatedName: items![0].name), - type: QueueItemSourceType.song, - id: items[0].id, - ), - startingIndex: request['Data']['StartIndex'], - ) - ); + if (items!.isNotEmpty) { + unawaited(queueService.startPlayback( + items: items, + source: QueueItemSource( + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: items[0].name), + type: QueueItemSourceType.song, + id: items[0].id, + ), + startingIndex: request['Data']['StartIndex'], + ) + ); + } else { + _playOnHandlerLogger.severe("Server asked to start an unplayable item"); + } break; case 'PlayNext': var items = await jellyfinApiHelper.getItems( From d1f9265d241ad9d106d0ee42751fba439a5a1c39 Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Wed, 6 Nov 2024 12:01:41 +0100 Subject: [PATCH 17/22] properly close the keepalive subscription --- lib/services/playon_handler.dart | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index 8892faea..6e738627 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -25,6 +25,7 @@ final audioServiceHelper = GetIt.instance<AudioServiceHelper>(); final playbackHistoryService = GetIt.instance<PlaybackHistoryService>(); final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>(); var channel; +var keepaliveSubscription; class PlayonHandler { @@ -63,32 +64,27 @@ class PlayonHandler { channel.sink.add('{"MessageType":"KeepAlive"}'); - final keepaliveSubscription = Stream.periodic(const Duration(seconds: 30), (count) { + keepaliveSubscription = Stream.periodic(const Duration(seconds: 30), (count) { return count; }).listen((event) { _playOnHandlerLogger.info("Sent KeepAlive message through websocket"); channel.sink.add('{"MessageType":"KeepAlive"}'); }); - - channel.stream.handleError((error, trace) { - _playOnHandlerLogger.severe("WebSocket Error: $error"); - }, - ); - channel.stream.listen( (dynamic message) { - _playOnHandlerLogger.severe("WebSocket connection to server established"); unawaited(handleMessage(message)); }, onDone: () { - Future.delayed(const Duration(seconds: 10), () { - startListener(); - _playOnHandlerLogger.severe("Attempted to restart listener"); - }); + if (!FinampSettingsHelper.finampSettings.isOffline) { + Future.delayed(const Duration(seconds: 1), () { + startListener(); + _playOnHandlerLogger.severe("Attempted to restart listener"); + }); + } }, onError: (error) { - _playOnHandlerLogger.severe("WebSocket connection to server established"); + _playOnHandlerLogger.severe("WebSocket Error: $error"); }, ); } @@ -96,6 +92,7 @@ class PlayonHandler { Future<void> closeListener() async { channel.sink.add('{"MessageType":"SessionsStop"}'); channel.sink.close(); + keepaliveSubscription.cancel(); } Future<void> handleMessage(value) async { From 66e23194e9323c5c217c7ceb54f276c737b0c58a Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Sat, 9 Nov 2024 14:38:09 +0100 Subject: [PATCH 18/22] handle websocket errors and disconnection --- lib/services/playon_handler.dart | 78 +++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index 6e738627..8239511b 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -26,34 +26,57 @@ final playbackHistoryService = GetIt.instance<PlaybackHistoryService>(); final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>(); var channel; var keepaliveSubscription; +var reconnectionSubscription = null; class PlayonHandler { Future<void> initialize() async { + + // Turn on/off when offline mode is toggled var settingsListener = FinampSettingsHelper.finampSettingsListener; - settingsListener.addListener(() { + settingsListener.addListener(() async { if (FinampSettingsHelper.finampSettings.isOffline) { - closeListener(); + await closeListener(); } else { - startListener(); + await startListener(); } }); + + await startListener(); + } + + Future<void> startListener() async { try { - await startListener(); + if (!FinampSettingsHelper.finampSettings.isOffline) { + await jellyfinApiHelper.updateCapabilities(ClientCapabilities( + supportsMediaControl: true, + supportsPersistentIdentifier: true, + playableMediaTypes: ["Audio"], + supportedCommands: ["MoveUp", "MoveDown", "MoveLeft", "MoveRight", "PageUp", "PageDown", "PreviousLetter", "NextLetter", "ToggleOsd", "ToggleContextMenu", "Select", "Back", "TakeScreenshot", "SendKey", "SendString", "GoHome", "GoToSettings", "VolumeUp", "VolumeDown", "Mute", "Unmute", "ToggleMute", "SetVolume", "SetAudioStreamIndex", "SetSubtitleStreamIndex", "ToggleFullscreen", "DisplayContent", "GoToSearch", "DisplayMessage", "SetRepeatMode", "ChannelUp", "ChannelDown", "Guide", "ToggleStats", "PlayMediaSource", "PlayTrailers", "SetShuffleQueue", "PlayState", "PlayNext", "ToggleOsdMenu", "Play", "SetMaxStreamingBitrate", "SetPlaybackOrder"], + )); + await connectWebsocket(); + } + reconnectionSubscription?.cancel(); + reconnectionSubscription = null; } catch (e) { - _playOnHandlerLogger.severe("Error initializing PlayOnHandler: $e"); + if (reconnectionSubscription == null) { + unawaited(startReconnectionLoop()); + _playOnHandlerLogger.severe("Error starting PlayOn listener: $e"); + } } } - - Future<void> startListener() async { - await jellyfinApiHelper.updateCapabilities(ClientCapabilities( - supportsMediaControl: true, - supportsPersistentIdentifier: true, - playableMediaTypes: ["Audio"], - supportedCommands: ["MoveUp", "MoveDown", "MoveLeft", "MoveRight", "PageUp", "PageDown", "PreviousLetter", "NextLetter", "ToggleOsd", "ToggleContextMenu", "Select", "Back", "TakeScreenshot", "SendKey", "SendString", "GoHome", "GoToSettings", "VolumeUp", "VolumeDown", "Mute", "Unmute", "ToggleMute", "SetVolume", "SetAudioStreamIndex", "SetSubtitleStreamIndex", "ToggleFullscreen", "DisplayContent", "GoToSearch", "DisplayMessage", "SetRepeatMode", "ChannelUp", "ChannelDown", "Guide", "ToggleStats", "PlayMediaSource", "PlayTrailers", "SetShuffleQueue", "PlayState", "PlayNext", "ToggleOsdMenu", "Play", "SetMaxStreamingBitrate", "SetPlaybackOrder"], - )); + Future<void> startReconnectionLoop() async { + reconnectionSubscription = Stream.periodic(const Duration(seconds: 1), (count) { + return count; + }).listen((count) { + startListener(); + _playOnHandlerLogger.info("Attempted to restart listener"); + }); + } + + Future<void> connectWebsocket() async { final url="${finampUserHelper.currentUser!.baseUrl}/socket?api_key=${finampUserHelper.currentUser!.accessToken}"; final parsedUrl = Uri.parse(url); final wsUrl = parsedUrl.replace(scheme: parsedUrl.scheme == "https" ? "wss" : "ws"); @@ -64,35 +87,36 @@ class PlayonHandler { channel.sink.add('{"MessageType":"KeepAlive"}'); - keepaliveSubscription = Stream.periodic(const Duration(seconds: 30), (count) { - return count; - }).listen((event) { - _playOnHandlerLogger.info("Sent KeepAlive message through websocket"); - channel.sink.add('{"MessageType":"KeepAlive"}'); - }); - channel.stream.listen( (dynamic message) { unawaited(handleMessage(message)); }, onDone: () { - if (!FinampSettingsHelper.finampSettings.isOffline) { - Future.delayed(const Duration(seconds: 1), () { - startListener(); - _playOnHandlerLogger.severe("Attempted to restart listener"); - }); - } + keepaliveSubscription?.cancel(); + startReconnectionLoop(); }, onError: (error) { _playOnHandlerLogger.severe("WebSocket Error: $error"); }, ); + + keepaliveSubscription = Stream.periodic(const Duration(seconds: 30), (count) { + return count; + }).listen((event) { + _playOnHandlerLogger.info("Sent KeepAlive message through websocket"); + channel.sink.add('{"MessageType":"KeepAlive"}'); + }); } Future<void> closeListener() async { + _playOnHandlerLogger.info("Closing playon session"); channel.sink.add('{"MessageType":"SessionsStop"}'); channel.sink.close(); - keepaliveSubscription.cancel(); + keepaliveSubscription?.cancel(); + + // In case offline mod is turned on while attempting to reconnect + reconnectionSubscription?.cancel(); + reconnectionSubscription = null; } Future<void> handleMessage(value) async { From 4e56faf1d21fd91ecf527256e7e5763872965fd4 Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Mon, 25 Nov 2024 13:26:13 +0100 Subject: [PATCH 19/22] small cleanup --- lib/services/playon_handler.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index 8239511b..ac0c3cc6 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -150,7 +150,6 @@ class PlayonHandler { break; case "Pause": audioHandler.pause(); - _playOnHandlerLogger.severe("PAUSEEEEE !"); break; case "Unpause": audioHandler.play(); From e3153a923a1bb0d90d5626d9a29cd69f8da7b5d5 Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Wed, 27 Nov 2024 13:27:07 +0100 Subject: [PATCH 20/22] temporarily downgrade just_audio_media_kit as the latest update breaks jellyfin's web client from showing up controls --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index ad7336c7..1d655a3e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,7 +31,7 @@ dependencies: chopper: ^8.0.0 get_it: ^7.2.0 just_audio: ^0.9.41 - just_audio_media_kit: ^2.0.6 + just_audio_media_kit: 2.0.6 media_kit_libs_linux: ^1.1.3 # Transcoding does not work on windows with current media-kit release. This fork uses the most recent From 026c1f5db2ca727aa12a6910d9ce01391e1ba7cc Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Wed, 27 Nov 2024 13:33:38 +0100 Subject: [PATCH 21/22] Remove duplicate log entry --- lib/services/playon_handler.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index ac0c3cc6..4187107f 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -125,7 +125,6 @@ class PlayonHandler { var request = jsonDecode(value); if (request['MessageType'] != 'ForceKeepAlive' && request['MessageType'] != 'KeepAlive') { - _playOnHandlerLogger.info("Received a '${request['MessageType']}' message: ${request['Data']}"); switch(request['MessageType']) { case "GeneralCommand": From 3e5016b10de62d434027c3d0396aed74bae98b0d Mon Sep 17 00:00:00 2001 From: "A. Pinsard" <a.pinsrd@proton.me> Date: Sun, 12 Jan 2025 21:07:18 +0100 Subject: [PATCH 22/22] handle favoriting a song from remote client and initialize playon handler from music_screen.dart instead of main.dart --- lib/main.dart | 1 - lib/screens/music_screen.dart | 6 ++++++ lib/services/favorite_provider.dart | 4 ++++ lib/services/playon_handler.dart | 13 +++++++++++-- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index e6cc2250..187bfe4b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -167,7 +167,6 @@ Future<void> _setupDownloadsHelper() async { Future<void> _setupPlayonHandler() async { GetIt.instance.registerSingleton(PlayonHandler()); - unawaited(GetIt.instance<PlayonHandler>().initialize()); } Future<void> _setupKeepScreenOnHelper() async { diff --git a/lib/screens/music_screen.dart b/lib/screens/music_screen.dart index 5ca5f1e4..0f57e693 100644 --- a/lib/screens/music_screen.dart +++ b/lib/screens/music_screen.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:finamp/services/playon_handler.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -28,6 +29,7 @@ final _musicScreenLogger = Logger("MusicScreen"); void postLaunchHook(WidgetRef ref) async { final downloadsService = GetIt.instance<DownloadsService>(); final queueService = GetIt.instance<QueueService>(); + final playonHandler = GetIt.instance<PlayonHandler>(); // make sure playlist info is downloaded for users upgrading from older versions and new installations AFTER logging in and selecting their libraries/views if (!FinampSettingsHelper.finampSettings.hasDownloadedPlaylistInfo) { @@ -38,6 +40,10 @@ void postLaunchHook(WidgetRef ref) async { FinampSettingsHelper.setHasDownloadedPlaylistInfo(true); } + // Initialize playon handler + unawaited(playonHandler.initialize()); + playonHandler.ref = ref; + // Restore queue unawaited(queueService .performInitialQueueLoad() diff --git a/lib/services/favorite_provider.dart b/lib/services/favorite_provider.dart index f73b080f..2b0dd71c 100644 --- a/lib/services/favorite_provider.dart +++ b/lib/services/favorite_provider.dart @@ -111,6 +111,10 @@ class IsFavorite extends _$IsFavorite { return state; } + void updateState(bool isFavorite) { + state = isFavorite; + } + void toggleFavorite() async { if (_initializing != null) { await _initializing; diff --git a/lib/services/playon_handler.dart b/lib/services/playon_handler.dart index 4187107f..a9f90c06 100644 --- a/lib/services/playon_handler.dart +++ b/lib/services/playon_handler.dart @@ -3,10 +3,11 @@ import 'dart:async'; import 'package:finamp/components/global_snackbar.dart'; import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/models/jellyfin_models.dart'; -import 'package:finamp/screens/active_downloads_screen.dart'; import 'package:finamp/services/audio_service_helper.dart'; import 'package:finamp/services/playback_history_service.dart'; import 'package:finamp/services/queue_service.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:finamp/services/favorite_provider.dart'; import 'package:logging/logging.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import '../../services/music_player_background_task.dart'; @@ -30,7 +31,8 @@ var reconnectionSubscription = null; class PlayonHandler { - + late WidgetRef ref; + Future<void> initialize() async { // Turn on/off when offline mode is toggled @@ -142,6 +144,13 @@ class PlayonHandler { // We now have to report to jellyfin server, probably through playback_history_service, that we updated volume } break; + case "UserDataChanged": + var item = await jellyfinApiHelper.getItemById(request['Data']['UserDataList'][0]['ItemId']); + + // Handle favoritig from remote client + _playOnHandlerLogger.info("Updating favorite ui state"); + ref.read(isFavoriteProvider(FavoriteRequest(item)).notifier).updateState(item.userData!.isFavorite); + break; default: switch (request['Data']['Command']) { case "Stop":