mirror of
				https://github.com/johrpan/musicus_mobile.git
				synced 2025-10-26 18:57:25 +01:00 
			
		
		
		
	Make playback controls and seeking functional
This commit is contained in:
		
							parent
							
								
									066e46a3e7
								
							
						
					
					
						commit
						5344f16f53
					
				
					 1 changed files with 125 additions and 66 deletions
				
			
		|  | @ -1,5 +1,7 @@ | ||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
|  | import 'dart:isolate'; | ||||||
|  | import 'dart:ui'; | ||||||
| 
 | 
 | ||||||
| import 'package:audio_service/audio_service.dart'; | import 'package:audio_service/audio_service.dart'; | ||||||
| import 'package:musicus_player/musicus_player.dart'; | import 'package:musicus_player/musicus_player.dart'; | ||||||
|  | @ -7,15 +9,14 @@ import 'package:rxdart/rxdart.dart'; | ||||||
| 
 | 
 | ||||||
| import 'music_library.dart'; | import 'music_library.dart'; | ||||||
| 
 | 
 | ||||||
|  | const _portName = 'playbackService'; | ||||||
|  | 
 | ||||||
| /// Entrypoint for the playback service. | /// Entrypoint for the playback service. | ||||||
| void _playbackServiceEntrypoint() { | void _playbackServiceEntrypoint() { | ||||||
|   AudioServiceBackground.run(() => _PlaybackService()); |   AudioServiceBackground.run(() => _PlaybackService()); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class Player { | class Player { | ||||||
|   /// The interval between playback position updates in milliseconds. |  | ||||||
|   static const positionUpdateInterval = 250; |  | ||||||
| 
 |  | ||||||
|   /// Whether the player is active. |   /// Whether the player is active. | ||||||
|   /// |   /// | ||||||
|   /// This means, that there is at least one item in the queue and the playback |   /// This means, that there is at least one item in the queue and the playback | ||||||
|  | @ -40,16 +41,12 @@ class Player { | ||||||
|   /// Playback position normalized to the range from zero to one. |   /// Playback position normalized to the range from zero to one. | ||||||
|   final normalizedPosition = BehaviorSubject.seeded(0.0); |   final normalizedPosition = BehaviorSubject.seeded(0.0); | ||||||
| 
 | 
 | ||||||
|   /// The current position in milliseconds. |   StreamSubscription _playbackServiceStateSubscription; | ||||||
|   int _positionMs = 0; |  | ||||||
| 
 | 
 | ||||||
|   StreamSubscription<PlaybackState> _stateStreamSubscription; |   /// Update [position] and [normalizedPosition] from position in milliseconds. | ||||||
|   StreamSubscription<MediaItem> _mediaItemStreamSubscription; |   void _updatePosition(int positionMs) { | ||||||
| 
 |     position.add(Duration(milliseconds: positionMs)); | ||||||
|   /// Update [position] and [normalizedPosition] according to [_positionMs]. |     normalizedPosition.add(positionMs / duration.value.inMilliseconds); | ||||||
|   void _updatePosition() { |  | ||||||
|     position.add(Duration(milliseconds: _positionMs)); |  | ||||||
|     normalizedPosition.add(_positionMs / duration.value.inMilliseconds); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Set everything to its default because the playback service was stopped. |   /// Set everything to its default because the playback service was stopped. | ||||||
|  | @ -59,9 +56,6 @@ class Player { | ||||||
|     position.add(const Duration()); |     position.add(const Duration()); | ||||||
|     duration.add(const Duration(seconds: 1)); |     duration.add(const Duration(seconds: 1)); | ||||||
|     normalizedPosition.add(0.0); |     normalizedPosition.add(0.0); | ||||||
|     _positionMs = 0; |  | ||||||
|     _stateStreamSubscription.cancel(); |  | ||||||
|     _mediaItemStreamSubscription.cancel(); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Start playback service. |   /// Start playback service. | ||||||
|  | @ -75,40 +69,39 @@ class Player { | ||||||
|         androidNotificationIcon: 'drawable/ic_notification', |         androidNotificationIcon: 'drawable/ic_notification', | ||||||
|       ); |       ); | ||||||
| 
 | 
 | ||||||
|       setup(); |       active.add(true); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Connect listeners and initialize streams. |   /// Connect listeners and initialize streams. | ||||||
|   void setup() { |   void setup() { | ||||||
|  |     if (_playbackServiceStateSubscription == null) { | ||||||
|  |       // We will receive updated state information from the playback service, | ||||||
|  |       // which runs in its own isolate, through this port. | ||||||
|  |       final receivePort = ReceivePort(); | ||||||
|  |       _playbackServiceStateSubscription = receivePort.listen((msg) { | ||||||
|  |         // If state is null, the background audio service has stopped. | ||||||
|  |         if (msg == null) { | ||||||
|  |           _stop(); | ||||||
|  |         } else { | ||||||
|  |           final state = msg as PlaybackServiceState; | ||||||
|  | 
 | ||||||
|  |           // TODO: Consider checking, whether values have actually changed. | ||||||
|  |           playing.add(state.playing); | ||||||
|  |           position.add(Duration(milliseconds: state.positionMs)); | ||||||
|  |           duration.add(Duration(milliseconds: state.durationMs)); | ||||||
|  |           normalizedPosition.add(state.positionMs / state.durationMs); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |       IsolateNameServer.registerPortWithName(receivePort.sendPort, _portName); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     if (AudioService.running) { |     if (AudioService.running) { | ||||||
|       active.add(true); |       active.add(true); | ||||||
| 
 | 
 | ||||||
|       _stateStreamSubscription = |       // Instruct the background service to send its current state. This will | ||||||
|           AudioService.playbackStateStream.listen((playbackState) { |       // by handled in the listeners, that were already set in the constructor. | ||||||
|         if (playbackState != null) { |       AudioService.customAction('sendState'); | ||||||
|           if (playbackState.basicState == BasicPlaybackState.stopped) { |  | ||||||
|             _stop(); |  | ||||||
|           } else { |  | ||||||
|             if (playbackState.basicState == BasicPlaybackState.playing) { |  | ||||||
|               playing.add(true); |  | ||||||
|               _play(); |  | ||||||
|             } else { |  | ||||||
|               playing.add(false); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             _positionMs = playbackState.currentPosition; |  | ||||||
|             _updatePosition(); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       _mediaItemStreamSubscription = |  | ||||||
|           AudioService.currentMediaItemStream.listen((mediaItem) { |  | ||||||
|         if (mediaItem?.duration != null) { |  | ||||||
|           duration.add(Duration(milliseconds: mediaItem.duration)); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -134,16 +127,6 @@ class Player { | ||||||
|     await AudioService.customAction('addTracks', jsonEncode(tracks)); |     await AudioService.customAction('addTracks', jsonEncode(tracks)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Regularly update [_positionMs] while playing. |  | ||||||
|   // TODO: Maybe find a better approach on handling this. |  | ||||||
|   Future<void> _play() async { |  | ||||||
|     while (playing.value) { |  | ||||||
|       await Future.delayed(Duration(milliseconds: positionUpdateInterval)); |  | ||||||
|       _positionMs += positionUpdateInterval; |  | ||||||
|       _updatePosition(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /// Seek to [pos], which is a value between (and including) zero and one. |   /// Seek to [pos], which is a value between (and including) zero and one. | ||||||
|   /// |   /// | ||||||
|   /// If the player is not active or an invalid value is provided, this will do |   /// If the player is not active or an invalid value is provided, this will do | ||||||
|  | @ -157,9 +140,7 @@ class Player { | ||||||
| 
 | 
 | ||||||
|   /// Tidy up. |   /// Tidy up. | ||||||
|   void dispose() { |   void dispose() { | ||||||
|     _stateStreamSubscription.cancel(); |     _playbackServiceStateSubscription.cancel(); | ||||||
|     _mediaItemStreamSubscription.cancel(); |  | ||||||
| 
 |  | ||||||
|     active.close(); |     active.close(); | ||||||
|     playing.close(); |     playing.close(); | ||||||
|     position.close(); |     position.close(); | ||||||
|  | @ -168,7 +149,55 @@ class Player { | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// Bundle of the current state of the playback service. | ||||||
|  | class PlaybackServiceState { | ||||||
|  |   /// The current playlist. | ||||||
|  |   final List<InternalTrack> playlist; | ||||||
|  | 
 | ||||||
|  |   /// The index of the currentTrack. | ||||||
|  |   final int currentTrack; | ||||||
|  | 
 | ||||||
|  |   /// Whether the player is playing (or paused). | ||||||
|  |   final bool playing; | ||||||
|  | 
 | ||||||
|  |   /// The current playback position in milliseconds. | ||||||
|  |   final int positionMs; | ||||||
|  | 
 | ||||||
|  |   /// The duration of the currently played track in milliseconds. | ||||||
|  |   final int durationMs; | ||||||
|  | 
 | ||||||
|  |   PlaybackServiceState({ | ||||||
|  |     this.playlist, | ||||||
|  |     this.currentTrack, | ||||||
|  |     this.playing, | ||||||
|  |     this.positionMs, | ||||||
|  |     this.durationMs, | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   factory PlaybackServiceState.fromJson(Map<String, dynamic> json) => | ||||||
|  |       PlaybackServiceState( | ||||||
|  |         playlist: json['playlist'] | ||||||
|  |             .map<InternalTrack>((j) => InternalTrack.fromJson(j)) | ||||||
|  |             .toList(), | ||||||
|  |         currentTrack: json['currentTrack'], | ||||||
|  |         playing: json['playing'], | ||||||
|  |         positionMs: json['positionMs'], | ||||||
|  |         durationMs: json['durationMs'], | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |   Map<String, dynamic> toJson() => { | ||||||
|  |         'playlist': playlist.map((t) => t.toJson()), | ||||||
|  |         'currentTrack': currentTrack, | ||||||
|  |         'playing': playing, | ||||||
|  |         'positionMs': positionMs, | ||||||
|  |         'durationMs': durationMs, | ||||||
|  |       }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| class _PlaybackService extends BackgroundAudioTask { | class _PlaybackService extends BackgroundAudioTask { | ||||||
|  |   /// The interval between playback position updates in milliseconds. | ||||||
|  |   static const positionUpdateInterval = 250; | ||||||
|  | 
 | ||||||
|   static const playControl = MediaControl( |   static const playControl = MediaControl( | ||||||
|     androidIcon: 'drawable/ic_play', |     androidIcon: 'drawable/ic_play', | ||||||
|     label: 'Play', |     label: 'Play', | ||||||
|  | @ -199,9 +228,8 @@ class _PlaybackService extends BackgroundAudioTask { | ||||||
| 
 | 
 | ||||||
|   MusicusPlayer _player; |   MusicusPlayer _player; | ||||||
|   int _currentTrack = 0; |   int _currentTrack = 0; | ||||||
|   int _position; |  | ||||||
|   int _updateTime; |  | ||||||
|   bool _playing = false; |   bool _playing = false; | ||||||
|  |   int _durationMs = 1000; | ||||||
| 
 | 
 | ||||||
|   _PlaybackService() { |   _PlaybackService() { | ||||||
|     _player = MusicusPlayer(onComplete: () { |     _player = MusicusPlayer(onComplete: () { | ||||||
|  | @ -209,27 +237,48 @@ class _PlaybackService extends BackgroundAudioTask { | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   void _setPosition(int position) { |   Future<void> _sendMsg(dynamic msg) { | ||||||
|     _position = position; |     final sendPort = IsolateNameServer.lookupPortByName(_portName); | ||||||
|     _updateTime = DateTime.now().millisecondsSinceEpoch; |     sendPort?.send(msg); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   void _setState() { |   Future<void> _setState() async { | ||||||
|  |     final positionMs = await _player.getPosition() ?? 0; | ||||||
|  |     final updateTime = DateTime.now().millisecondsSinceEpoch; | ||||||
|  | 
 | ||||||
|     AudioServiceBackground.setState( |     AudioServiceBackground.setState( | ||||||
|       controls: |       controls: | ||||||
|           _playing ? [pauseControl, stopControl] : [playControl, stopControl], |           _playing ? [pauseControl, stopControl] : [playControl, stopControl], | ||||||
|       basicState: |       basicState: | ||||||
|           _playing ? BasicPlaybackState.playing : BasicPlaybackState.paused, |           _playing ? BasicPlaybackState.playing : BasicPlaybackState.paused, | ||||||
|       position: _position, |       position: positionMs, | ||||||
|       updateTime: _updateTime, |       updateTime: updateTime, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     AudioServiceBackground.setMediaItem(dummyMediaItem); |     AudioServiceBackground.setMediaItem(dummyMediaItem); | ||||||
|  | 
 | ||||||
|  |     _sendMsg(PlaybackServiceState( | ||||||
|  |       playlist: _playlist, | ||||||
|  |       currentTrack: _currentTrack, | ||||||
|  |       playing: _playing, | ||||||
|  |       positionMs: positionMs, | ||||||
|  |       durationMs: _durationMs, | ||||||
|  |     )); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<void> _updatePosition() async { | ||||||
|  |     while (_playing) { | ||||||
|  |       await Future.delayed( | ||||||
|  |           const Duration(milliseconds: positionUpdateInterval)); | ||||||
|  | 
 | ||||||
|  |       // TODO: Consider seperating position updates from general state updates | ||||||
|  |       // and/or estimating the position instead of asking the player. | ||||||
|  |       _setState(); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Future<void> onStart() async { |   Future<void> onStart() async { | ||||||
|     _setPosition(0); |  | ||||||
|     _setState(); |     _setState(); | ||||||
|     await _completer.future; |     await _completer.future; | ||||||
|   } |   } | ||||||
|  | @ -244,7 +293,13 @@ class _PlaybackService extends BackgroundAudioTask { | ||||||
|       final List<InternalTrack> tracks = List.castFrom( |       final List<InternalTrack> tracks = List.castFrom( | ||||||
|           tracksJson.map((j) => InternalTrack.fromJson(j)).toList()); |           tracksJson.map((j) => InternalTrack.fromJson(j)).toList()); | ||||||
|       _playlist.addAll(tracks); |       _playlist.addAll(tracks); | ||||||
|       _player.setUri(tracks.first.uri); |       _player.setUri(tracks.first.uri).then((newDurationMs) { | ||||||
|  |         _durationMs = newDurationMs; | ||||||
|  |         _setState(); | ||||||
|  |       }); | ||||||
|  |     } else if (name == 'sendState') { | ||||||
|  |       // Send the current state to the main isolate. | ||||||
|  |       _setState(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -254,6 +309,7 @@ class _PlaybackService extends BackgroundAudioTask { | ||||||
| 
 | 
 | ||||||
|     _player.play(); |     _player.play(); | ||||||
|     _playing = true; |     _playing = true; | ||||||
|  |     _updatePosition(); | ||||||
|     _setState(); |     _setState(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -270,8 +326,9 @@ class _PlaybackService extends BackgroundAudioTask { | ||||||
|   void onSeekTo(int position) { |   void onSeekTo(int position) { | ||||||
|     super.onSeekTo(position); |     super.onSeekTo(position); | ||||||
| 
 | 
 | ||||||
|     _setPosition(position); |     _player.seekTo(position).then((_) { | ||||||
|     _setState(); |       _setState(); | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|  | @ -283,6 +340,8 @@ class _PlaybackService extends BackgroundAudioTask { | ||||||
|       basicState: BasicPlaybackState.stopped, |       basicState: BasicPlaybackState.stopped, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|  |     _sendMsg(null); | ||||||
|  | 
 | ||||||
|     // This will end onStart. |     // This will end onStart. | ||||||
|     _completer.complete(); |     _completer.complete(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Elias Projahn
						Elias Projahn