diff --git a/lib/audio/nplayer.dart b/lib/audio/nplayer.dart index d507ab8..f592df7 100644 --- a/lib/audio/nplayer.dart +++ b/lib/audio/nplayer.dart @@ -119,27 +119,34 @@ class NPlayer extends ChangeNotifier { bool _isServerOn = false; + bool _isPlayingFromServer = false; + bool get isPlayingFromServer => _isPlayingFromServer; + WebSocket? _serverSocket; + String? _serverIP; + Timer? _serverCheckTimer; + bool get isServerOn => _isServerOn; - final StreamController _songChangeController = StreamController.broadcast(); + final StreamController _songChangeController = + StreamController.broadcast(); Stream get songChangeStream => _songChangeController.stream; - void notifySongChange() { + void notifySongChange() { _songChangeController.add(null); } - - Future toggleServer() async { + + Future toggleServer() async { if (_isServerOn) { await _server?.stop(); _server = null; + _isServerOn = false; } else { _server = NServer(this); await _server!.start(); + _isServerOn = _server!.isRunning; } - _isServerOn = !_isServerOn; notifyListeners(); } - // SECTION: Constructor and Initialization NPlayer() { _log("Initializing NPlayer"); @@ -260,7 +267,7 @@ class NPlayer extends ChangeNotifier { await Settings.setLastPlayingSong(selectedSong.path); await _updateMetadata(); await _updatePlaybackState(playing: true); -notifySongChange(); + notifySongChange(); notifyListeners(); } @@ -352,7 +359,13 @@ notifySongChange(); await _updateMetadata(); await _updatePlaybackState(playing: true); await _audioHandler.play(); -notifySongChange(); + notifySongChange(); + if (_serverSocket != null) { + _serverSocket!.add(json.encode({ + 'type': 'skip', + 'direction': 'next', + })); + } notifyListeners(); } else { _log("No songs to play"); @@ -376,7 +389,13 @@ notifySongChange(); await _updateMetadata(); await _updatePlaybackState(playing: true); await _audioHandler.play(); -notifySongChange(); + notifySongChange(); + if (_serverSocket != null) { + _serverSocket!.add(json.encode({ + 'type': 'skip', + 'direction': 'previous', + })); + } notifyListeners(); } } else { @@ -485,6 +504,12 @@ notifySongChange(); _log("Seeking to position: $position"); await _audioPlayer.seek(position); _currentPosition = position; + if (_serverSocket != null) { + _serverSocket!.add(json.encode({ + 'type': 'seek', + 'position': position.inMilliseconds, + })); + } notifyListeners(); } @@ -809,22 +834,27 @@ notifySongChange(); } Future _handleSongCompletion() async { - _log("Handling song completion"); - switch (_repeatMode) { - case 'off': - await nextSong(); - break; - case 'one': - if (_currentSongIndex != null && _playingSongs.isNotEmpty) { - await _audioPlayer - .play(DeviceFileSource(_playingSongs[_currentSongIndex!].path)); - } - break; - case 'all': - await nextSong(); - break; + if (_isPlayingFromServer) { + await _fetchAndUpdateServerSong(); + notifySongChange(); + } else { + _log("Handling song completion"); + switch (_repeatMode) { + case 'off': + await nextSong(); + break; + case 'one': + if (_currentSongIndex != null && _playingSongs.isNotEmpty) { + await _audioPlayer + .play(DeviceFileSource(_playingSongs[_currentSongIndex!].path)); + } + break; + case 'all': + await nextSong(); + break; + } + notifySongChange(); } -notifySongChange(); } void _log(String message) { @@ -836,72 +866,175 @@ notifySongChange(); _log("Disposing NPlayer"); _songChangeController.close(); _audioPlayer.dispose(); + _serverCheckTimer?.cancel(); super.dispose(); } -Future> _fetchMetadata(String serverIP) async { - final url = 'http://$serverIP:8080/metadata'; - final response = await http.get(Uri.parse(url)); - if (response.statusCode == 200) { - return json.decode(response.body); - } else { - throw Exception('Failed to load metadata'); + Future> _fetchMetadata(String serverIP) async { + final url = 'http://$serverIP:8080/metadata'; + final response = await http.get(Uri.parse(url)); + if (response.statusCode == 200) { + return json.decode(response.body); + } else { + throw Exception('Failed to load metadata'); + } } -} -Future playFromServer(String serverIP) async { - if (serverIP.isEmpty) { - print('Error: Server IP is empty'); - return; + Future connectToServer(String serverIP) async { + print('Attempting to connect to server: $serverIP'); + try { + _serverIP = serverIP; + final wsUrl = Uri.parse('ws://$serverIP:8080/ws'); + print('Connecting to WebSocket URL: $wsUrl'); + _serverSocket = await WebSocket.connect(wsUrl.toString()) + .timeout(Duration(seconds: 10)); + print('WebSocket connected successfully'); + + _serverSocket!.listen( + _handleServerUpdate, + onError: (error) { + print('WebSocket error: $error'); + }, + onDone: () { + print('WebSocket connection closed'); + }, + ); + + // Request the current song from the server + _serverSocket!.add(json.encode({'type': 'getCurrentSong'})); + + // Enable stream playing using the old method + await enableStreamPlaying(serverIP); + + print('Connected to server: $serverIP'); + notifyListeners(); + } catch (e) { + print('Error connecting to server: $e'); + _serverIP = null; + _serverSocket = null; + rethrow; + } } - final streamUrl = 'http://$serverIP:8080/stream'; - final metadataUrl = 'http://$serverIP:8080/metadata'; - print('Attempting to play from URL: $streamUrl'); - - try { - // Fetch metadata first - final response = await http.get(Uri.parse(metadataUrl)); - if (response.statusCode == 200) { - final metadata = json.decode(response.body); - - // Create a Music object with the fetched metadata + void _handleServerUpdate(dynamic message) { + print('Received message from server: $message'); + if (message == null) { + print('Received null message from server'); + return; + } + + Map? data; + try { + data = json.decode(message); + } catch (e) { + print('Error decoding message: $e'); + return; + } + + if (data == null) { + print('Decoded data is null'); + return; + } + + switch (data['type']) { + case 'songChange': + print('Received songChange event: ${data['song']}'); + if (data['song'] != null) { + _handleSongChange(Music.fromJson(data['song'])); + } else { + print('Received songChange event with null song data'); + } + break; + case 'seek': + if (data['position'] != null) { + seek(Duration(milliseconds: data['position'])); + } + break; + default: + print('Unknown message type: ${data['type']}'); + } + } + + void _handleSongChange(Music newSong) { + // Keep playing the current song + Music? currentSong = getCurrentSong(); + + // Attempt to play the new song + _audioPlayer.play(UrlSource(newSong.path)); + + // Start a timer to check if the new song is playing + Timer(Duration(seconds: 1), () { + if (_audioPlayer.state == PlayerState.playing) { + // If the new song is playing after 1 second, update the player state + _playingSongs = [newSong]; + _currentSongIndex = 0; + _isPlaying = true; + _currentPosition = Duration.zero; + _updateMetadata(); + _updatePlaybackState(playing: true); + notifySongChange(); + notifyListeners(); + } else { + // If the new song isn't playing after 1 second, revert to the previous song + if (currentSong != null) { + _audioPlayer.play(DeviceFileSource(currentSong.path)); + } + print('Failed to play new song, reverting to previous song'); + } + }); + } + + Future enableStreamPlaying(String serverIP) async { + _serverIP = serverIP; + _isPlayingFromServer = true; + await _fetchAndUpdateServerSong(); + _serverCheckTimer = Timer.periodic( + Duration(seconds: 5), + (_) => _fetchAndUpdateServerSong(), + ); + notifyListeners(); + } + + Future disableStreamPlaying() async { + _isPlayingFromServer = false; + _serverIP = null; + _serverCheckTimer?.cancel(); + notifyListeners(); + } + + Future _fetchAndUpdateServerSong() async { + if (!_isPlayingFromServer || _serverIP == null) return; + try { + final metadata = await NServer.getRemoteMetadata(_serverIP!); final serverSong = Music( - path: streamUrl, + path: 'http://$_serverIP:8080/stream', folderName: 'Server', lastModified: DateTime.now(), title: metadata['title'] ?? 'Unknown Title', album: metadata['album'] ?? 'Unknown Album', artist: metadata['artist'] ?? 'Unknown Artist', - duration: (metadata['duration'] as num?)?.toInt() ?? 0, // Convert to int - picture: metadata['picture'] != null ? base64Decode(metadata['picture']) : null, + duration: (metadata['duration'] as num?)?.toInt() ?? 0, + picture: metadata['picture'] != null + ? base64Decode(metadata['picture']) + : null, year: metadata['year']?.toString() ?? '', genre: metadata['genre'] ?? 'Unknown Genre', - size: 0, // We don't have this information from the server + size: 0, ); - // Add the server song to the playing list - _playingSongs = [serverSong]; - _currentSongIndex = 0; - - // Play the stream - await _audioPlayer.play(UrlSource(streamUrl)); - _isPlaying = true; - _currentPosition = Duration.zero; - - // Update metadata and playback state - await _updateMetadata(); - await _updatePlaybackState(playing: true); - - notifyListeners(); - print('Successfully started playing from server'); - } else { - throw Exception('Failed to load metadata: ${response.statusCode}'); + if (_playingSongs.isEmpty || _playingSongs[0].path != serverSong.path) { + _playingSongs = [serverSong]; + _currentSongIndex = 0; + await _audioPlayer.play(UrlSource(serverSong.path)); + _isPlaying = true; + _currentPosition = Duration.zero; + await _updateMetadata(); + await _updatePlaybackState(playing: true); + notifySongChange(); + notifyListeners(); + } + } catch (e) { + print('Error fetching server song: $e'); } - } catch (e) { - print('Error playing from server: $e'); - // Handle the error (e.g., show an error message to the user) } } - -} diff --git a/lib/audio/nserver.dart b/lib/audio/nserver.dart index e2c1f37..2e68a03 100644 --- a/lib/audio/nserver.dart +++ b/lib/audio/nserver.dart @@ -1,5 +1,3 @@ -// nserver.dart - import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -7,6 +5,8 @@ import 'dart:io'; import 'package:blossom/audio/nplayer.dart'; import 'package:metadata_god/metadata_god.dart'; import 'package:http/http.dart' as http; +import 'package:web_socket_channel/io.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; class NServer { HttpServer? _server; @@ -14,6 +14,7 @@ class NServer { StreamSubscription? _songChangeSubscription; String? _currentIp; bool _isRunning = false; + List _clients = []; NServer(this._nplayer); @@ -30,39 +31,78 @@ class NServer { print('Server running on $_currentIp:${_server!.port}'); _songChangeSubscription = _nplayer.songChangeStream.listen((_) { - print('Song changed, updating server'); + print('Song changed, updating clients'); + _notifyClients('songChange', _nplayer.getCurrentSong()?.toJson()); }); - _server!.listen(_handleRequest, onError: (e) { + _server!.listen((HttpRequest request) { + if (request.uri.path == '/ws') { + _handleWebSocketConnection(request); + } else { + _handleHttpRequest(request); + } + }, onError: (e) { print('Error handling request: $e'); }); } catch (e) { print('Failed to start server: $e'); + _isRunning = false; } } - Future stop() async { - if (!_isRunning) { - print('Server is not running.'); - return; - } - - try { - await _songChangeSubscription?.cancel(); - await _server?.close(force: true); - _server = null; - _currentIp = null; - _isRunning = false; - print('Server stopped'); - } catch (e) { - print('Error stopping server: $e'); - } + void _handleWebSocketConnection(HttpRequest request) { + print('Handling WebSocket connection request'); + WebSocketTransformer.upgrade(request).then((WebSocket webSocket) { + print('WebSocket connection established'); + final channel = IOWebSocketChannel(webSocket); + _clients.add(channel); + _handleWebSocket(channel); + }).catchError((error) { + print('Error upgrading to WebSocket: $error'); + }); } - bool get isRunning => _isRunning; + void _handleWebSocket(WebSocketChannel channel) { + channel.stream.listen( + (message) { + final data = json.decode(message); + switch (data['type']) { + case 'getCurrentSong': + final currentSong = _nplayer.getCurrentSong(); + if (currentSong != null) { + channel.sink.add(json.encode({ + 'type': 'songChange', + 'song': currentSong.toJson(), + })); + } else { + channel.sink.add(json.encode({ + 'type': 'songChange', + 'song': null, + })); + } + break; + case 'seek': + _nplayer.seek(Duration(milliseconds: data['position'])); + _notifyClients('seek', {'position': data['position']}); + break; + case 'skip': + if (data['direction'] == 'next') { + _nplayer.nextSong(); + } else { + _nplayer.previousSong(); + } + _notifyClients('skip', {'direction': data['direction']}); + break; + } + }, + onDone: () { + _clients.remove(channel); + }, + ); + } - void _handleRequest(HttpRequest request) { - print('Received request for: ${request.uri.path}'); + void _handleHttpRequest(HttpRequest request) { + print('Received HTTP request for: ${request.uri.path}'); switch (request.uri.path) { case '/stream': @@ -104,7 +144,16 @@ class NServer { if (currentSong != null && await File(currentSong.path).exists()) { print('Serving stream for: ${currentSong.title}'); request.response.headers.contentType = ContentType('audio', 'mpeg'); - await File(currentSong.path).openRead().pipe(request.response); + + final file = File(currentSong.path); + final fileStream = file.openRead(); + + await for (var chunk in fileStream) { + request.response.add(chunk); + await request.response.flush(); + } + + await request.response.close(); print('Stream sent successfully'); } else { print('No current song or file not found'); @@ -154,6 +203,47 @@ class NServer { } } + void _notifyClients(String type, dynamic data) { + final update = json.encode({'type': type, 'data': data}); + print('Notifying clients: $update'); + for (var client in _clients) { + client.sink.add(update); + } + } + + void notifySongChange() { + final currentSong = _nplayer.getCurrentSong(); + if (currentSong != null) { + _notifyClients('songChange', {'song': currentSong.toJson()}); + } else { + print('No current song to notify clients about'); + _notifyClients('songChange', {'song': null}); + } + } + + Future stop() async { + if (!_isRunning) { + print('Server is not running.'); + return; + } + + try { + await _songChangeSubscription?.cancel(); + for (var client in _clients) { + await client.sink.close(); + } + _clients.clear(); + await _server?.close(force: true); + _server = null; + _currentIp = null; + _isRunning = false; + print('Server stopped'); + } catch (e) { + print('Error stopping server: $e'); + } + } + + bool get isRunning => _isRunning; String? get currentIp => _currentIp; static Future isValidServer(String ip) async { @@ -176,6 +266,4 @@ class NServer { throw Exception('Failed to load metadata'); } } - - void notifySongChange() {} } diff --git a/lib/main.dart b/lib/main.dart index 30361bd..63edcf8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:blossom/custom/custom_appbar.dart'; -import 'package:blossom/custom/custom_navbar.dart'; import 'package:blossom/pages/server_page.dart'; import 'package:blossom/tools/downloader.dart'; import 'package:blossom/pages/loading_page.dart'; @@ -247,10 +246,10 @@ class _MainStructureState extends State icon: Icon(Icons.person), label: 'Artists', ), - const BottomNavigationBarItem( - icon: Icon(Icons.wifi), - label: 'Server Scan', - ), + const BottomNavigationBarItem( + icon: Icon(Icons.wifi), + label: 'Server Scan', + ), if (isDesktop) const BottomNavigationBarItem( icon: Icon(Icons.download), diff --git a/lib/pages/server_page.dart b/lib/pages/server_page.dart index 7c5585c..80320ca 100644 --- a/lib/pages/server_page.dart +++ b/lib/pages/server_page.dart @@ -1,4 +1,3 @@ - import 'dart:async'; import 'package:flutter/material.dart'; @@ -20,8 +19,8 @@ class _ServerPageState extends State { bool _isScanning = false; final TextEditingController _ipController = TextEditingController(); StreamSubscription? _songChangeSubscription; - - @override + + @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -39,7 +38,7 @@ class _ServerPageState extends State { }); } - void _listenToSongChanges() { + void _listenToSongChanges() { final nplayer = Provider.of(context, listen: false); _songChangeSubscription = nplayer.songChangeStream.listen((_) { setState(() { @@ -48,11 +47,28 @@ class _ServerPageState extends State { }); } - Future _toggleServer() async { final nplayer = Provider.of(context, listen: false); - await nplayer.toggleServer(); - _updateServerStatus(); + try { + await nplayer.toggleServer(); + _updateServerStatus(); + if (nplayer.isServerOn) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('Server started on ${nplayer.server!.currentIp}:8080')), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Server stopped')), + ); + } + } catch (e) { + print('Error toggling server: $e'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to toggle server: $e')), + ); + } } Future _scanForServers() async { @@ -81,10 +97,17 @@ class _ServerPageState extends State { void _connectToServer(String ip) { final nplayer = Provider.of(context, listen: false); - nplayer.playFromServer(ip); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Connecting to server: $ip')), - ); + nplayer.connectToServer(ip).then((_) { + print('Successfully connected to server: $ip'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Connected to server: $ip')), + ); + }).catchError((error) { + print('Error connecting to server: $error'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to connect to server: $error')), + ); + }); } @override @@ -104,10 +127,14 @@ class _ServerPageState extends State { onPressed: _toggleServer, child: Text(nplayer.isServerOn ? 'Stop Server' : 'Start Server'), ), - if (nplayer.isServerOn && currentSong != null) + if (nplayer.isPlayingFromServer) Padding( padding: const EdgeInsets.all(8.0), - child: Text('Currently streaming: ${currentSong.title} by ${currentSong.artist}'), + child: Text( + currentSong != null + ? 'Currently streaming: ${currentSong.title} by ${currentSong.artist}' + : 'No song currently playing', + ), ), Padding( padding: const EdgeInsets.all(8.0), @@ -144,6 +171,7 @@ class _ServerPageState extends State { @override void dispose() { + _songChangeSubscription?.cancel(); super.dispose(); } -} \ No newline at end of file +}