gitee project address github project address
If you like, please click star
Let's talk about the player selection of the project today
Check out pub The main player plug-ins on dev are audioplayers and just_audio
Mention the configuration of android and ios
android
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.netease_app"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <application android:label="netease_app" android:icon="@mipmap/ic_launcher" android:usesCleartextTraffic="true" android:networkSecurityConfig="@xml/network_security_config" > <activity android:name="com.ryanheise.audioservice.AudioServiceActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:supportsPictureInPicture="true" android:resizeableActivity="true" android:windowSoftInputMode="adjustResize"> <!-- Specifies an Android theme to apply to this Activity as soon as the Android process has started. This theme is visible to the user while the Flutter UI initializes. After that, this theme continues to determine the Window background behind the Flutter UI. --> <meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> <!-- Displays an Android View that continues showing the launch screen Drawable until Flutter paints its first frame, then this splash screen fades out. A splash screen is useful to avoid any visual gap between the end of Android's launch screen and the painting of Flutter's first frame. --> <meta-data android:name="io.flutter.embedding.android.SplashScreenDrawable" android:resource="@drawable/launch_background" /> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <service android:name="com.ryanheise.audioservice.AudioService"> <intent-filter> <action android:name="android.media.browse.MediaBrowserService" /> </intent-filter> </service> <receiver android:name="com.ryanheise.audioservice.MediaButtonReceiver" > <intent-filter> <action android:name="android.intent.action.MEDIA_BUTTON" /> </intent-filter> </receiver> <!-- Don't delete the meta-data below. This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> <meta-data android:name="flutterEmbedding" android:value="2" /> </application> </manifest>
ios
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>CFBundleDevelopmentRegion</key> <string>$(DEVELOPMENT_LANGUAGE)</string> <key>CFBundleExecutable</key> <string>$(EXECUTABLE_NAME)</string> <key>CFBundleIdentifier</key> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> <key>CFBundleName</key> <string>Imitation Netease cloud</string> <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleShortVersionString</key> <string>$(FLUTTER_BUILD_NAME)</string> <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> <string>$(FLUTTER_BUILD_NUMBER)</string> <key>LSRequiresIPhoneOS</key> <true /> <key>UILaunchStoryboardName</key> <string>LaunchScreen</string> <key>UIMainStoryboardFile</key> <string>Main</string> <key>UISupportedInterfaceOrientations</key> <array> <string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeRight</string> </array> <key>UISupportedInterfaceOrientations~ipad</key> <array> <string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeRight</string> </array> <key>UIViewControllerBasedStatusBarAppearance</key> <false /> <key>UIBackgroundModes</key> <array> <string>audio</string> </array> <key>NSAppTransportSecurity</key> <dict> <key>NSExceptionMinimumTLSVersion</key> <string>TLSv1.0</string> <key>NSAllowsArbitraryLoads</key> <true/> <key>NSAllowsArbitraryLoadsInWebContent</key> <true/> <key>NSAllowsArbitraryLoadsForMedia</key> <true/> <key>NSExceptionRequiresForwardSecrecy</key> <true/> <key>NSIncludesSubdomains</key> <true/> <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key> <true/> </dict> </dict> </plist>
It's mainly the http protocol. I don't understand ios. Anyway, I can't configure it, so I looked for it on the Internet and changed Netease cloud http to https to play it. If anyone understands it, you can communicate
I have used both players in this project. At the beginning, I chose to use audiolayers, which is easy to operate, but considering the collocation audio_service To realize the background control of music, I pondered for some time. audioplayers found that I was too good at using o(╥﹏╥) o, so I chose to work with audio_ Just matching service_ Audio, via audio_ The official instance of service can be used almost, Official tutorial examples
According to the official documentation, the dependencies that need to be installed are {just_audio,audio_service,get_it
Create directory services, notifiers and page files_ manager. dart
First say service from services_ locator. dart
// Initialize audio background processing import 'package:get_it/get_it.dart'; import '../page_manager.dart'; import 'audio_handler.dart'; import 'playlist_repository.dart'; GetIt getIt = GetIt.instance; Future<void> setupServiceLocator() async { getIt.registerSingleton<AudioPlayerHandler>(await initAudioService()); getIt.registerLazySingleton<PlaylistRepository>(() => DemoPlaylist()); getIt.registerLazySingleton<PageManager>(() => PageManager()); }
Git is used here_ It packages can be controlled globally. I don't understand the details. I also see the official
AudioPlayerHandler first creates audio_service
The second step of PlaylistRepository is to create an initialized playlist, which is mainly to load the last playlist in the cache later
The third one of PageManager is mainly used to control audio_service and just_audio play audio
// playlist import 'package:netease_app/model/songs.dart'; abstract class PlaylistRepository { Future<List<Map<String, String>>> fetchInitialPlaylist(); Future<Song> fetchAnotherSong(Song song); } class DemoPlaylist extends PlaylistRepository { @override Future<List<Map<String, String>>> fetchInitialPlaylist( {int length = 3}) async { return []; } @override Future<Song> fetchAnotherSong(Song song) async { return song; } }
This is the playlist_repository.dart
fetchInitialPlaylist is used to initialize the playlist, which is not used here for the time being
fetchAnotherSong is used to return a song audio file
Here, a song is created to save each audio file
// Construct class songs class Song { int id; // Song id String name; // Song name String artists; // artist String picUrl; // Song pictures Duration timer; Song(this.id, this.timer, {this.name = '', this.artists = '', this.picUrl = ''}); Song.fromJson(Map<String, dynamic> json) : id = int.parse(json['id']), name = json['name'], artists = json['artists'], timer = Duration(milliseconds: int.parse(json["timer"])), picUrl = json['picUrl']; Map<String, dynamic> toJson() => { 'id': id, 'name': name, 'artists': artists, 'picUrl': picUrl, 'timer': timer, }; @override String toString() { return '{"id": "$id", "name": "$name", "artists": "$artists","picUrl": "$picUrl","timer": "${timer.inMilliseconds}"}'; } }
audio_handler.dart
First, let's talk about the logic I understand. MediaItem refers to the currently playing song / queue refers to the playlist
Therefore, the parameters of mediaitem need to be modified when updating, and the queue list needs to be changed when the playlist is changed
Let's talk about the functions triggered by the buttons playing in the background. play() pause() seek() skipToNext() skipToPrevious() stop() better not do other processing in it
Due to the official just_ The next logic of audio doesn't have the heart mode of Netease cloud, so it doesn't use its own logic
Another reason why I don't need this is that I need to get the audio url. I can't directly setAudioSource. If you have a better way, you can teach me
Then I customized several methods I need to use
abstract class AudioPlayerHandler implements AudioHandler { // Add public method Future<void> changeQueueLists(List<MediaItem> list, {int index = 0}); // Change playlist Future<void> readySongUrl(); // Get song url Future<void> playIndex(int index); // Play from subscript Future<void> addFmItems(List<MediaItem> mediaItems, bool isAddcurIndex); // Private fm }
Audio is initialized here_ service
Future<AudioPlayerHandler> initAudioService() async { return await AudioService.init( builder: () => MyAudioHandler(), config: AudioServiceConfig( androidNotificationChannelId: 'com.mycompany.myapp.audio', androidNotificationChannelName: 'NetEase cloud music', androidNotificationOngoing: true, androidStopForegroundOnPause: true, ), ); }
Then create a custom audio_handler
class MyAudioHandler extends BaseAudioHandler with SeekHandler implements AudioPlayerHandler { final _player = AudioPlayer(); // player final _playlist = ConcatenatingAudioSource(children: []); // playlist final _songlist = <Song>[]; // This is a playlist int _curIndex = 0; // Playlist index MyAudioHandler() { // initialization // _ loadEmptyPlaylist(); // Load playlist _notifyAudioHandlerAboutPlaybackEvents(); // Background status change _listenForDurationChanges(); // Update background when time changes _listenPlayEnd(); // _ listenForCurrentSongIndexChanges(); // This is also the background } UriAudioSource _createAudioSource(MediaItem mediaItem) { return AudioSource.uri( Uri.parse(mediaItem.id), tag: mediaItem, ); } Future<void> _loadEmptyPlaylist() async { final session = await AudioSession.instance; await session.configure(AudioSessionConfiguration.speech()); try { await _player.setAudioSource(_playlist); } catch (e) { print("error:$e"); } } void _listenPlayEnd() { _player.playerStateStream.listen((state) { if (state.playing) { } else {} switch (state.processingState) { case ProcessingState.idle: break; case ProcessingState.loading: break; case ProcessingState.buffering: break; case ProcessingState.ready: break; case ProcessingState.completed: skipToNext(); break; } }); } void _listenForDurationChanges() { // When the time transmission changes (that is, the song transmission changes), this is the update time _player.durationStream.listen((duration) { // final index = _player.currentIndex; // Current play subscript final newQueue = queue.value; if (_curIndex == null || newQueue.isEmpty) return; final oldMediaItem = newQueue[_curIndex]; final newMediaItem = oldMediaItem.copyWith(duration: duration); newQueue[_curIndex] = newMediaItem; queue.add(newQueue); mediaItem.add(newMediaItem); }); } void _notifyAudioHandlerAboutPlaybackEvents() { _player.playbackEventStream.listen((PlaybackEvent event) { final playing = _player.playing; playbackState.add(playbackState.value.copyWith( controls: [ MediaControl.skipToPrevious, if (playing) MediaControl.pause else MediaControl.play, MediaControl.stop, MediaControl.skipToNext, ], systemActions: const { MediaAction.seek, }, androidCompactActionIndices: const [0, 1, 3], processingState: const { ProcessingState.idle: AudioProcessingState.idle, ProcessingState.loading: AudioProcessingState.loading, ProcessingState.buffering: AudioProcessingState.buffering, ProcessingState.ready: AudioProcessingState.ready, ProcessingState.completed: AudioProcessingState.completed, }[_player.processingState]!, shuffleMode: (_player.shuffleModeEnabled) ? AudioServiceShuffleMode.all : AudioServiceShuffleMode.none, playing: playing, updatePosition: _player.position, bufferedPosition: _player.bufferedPosition, speed: _player.speed, queueIndex: _curIndex, )); }); } // void _listenForCurrentSongIndexChanges() { // _player.currentIndexStream.listen((index) { // final playlist = queue.value; // if (index == null || playlist.isEmpty) return; // mediaItem.add(playlist[index]); // }); // } @override Future<void> addQueueItems(List<MediaItem> mediaItems) async { final audioSource = mediaItems.map(_futterSongItem); if (_songlist.length > 0) { // Judge whether the current song is in the last position _songlist.insertAll(_curIndex + 1, audioSource.toList()); final newQueue = queue.value..insertAll(_curIndex + 1, mediaItems); // _curIndex++; queue.add(newQueue); } else { _songlist.insertAll(_curIndex, audioSource.toList()); final newQueue = queue.value..insertAll(_curIndex, mediaItems); queue.add(newQueue); } } // Addition of private Fm @override Future<void> addFmItems(List<MediaItem> mediaItems, bool isadd) async { final audioSource = mediaItems.map(_futterSongItem); if (_songlist.length > 0) { // Judge whether the current song is in the last position _songlist.insertAll(_curIndex + 1, audioSource.toList()); final newQueue = queue.value..insertAll(_curIndex + 1, mediaItems); if (isadd) { _curIndex++; } queue.add(newQueue); } else { _songlist.insertAll(_curIndex, audioSource.toList()); final newQueue = queue.value..insertAll(_curIndex, mediaItems); queue.add(newQueue); } } Song _futterSongItem(MediaItem mediaItem) { return Song( int.parse(mediaItem.id), mediaItem.duration!, artists: mediaItem.artist ?? '', picUrl: mediaItem.extras?["picUrl"] ?? '', name: mediaItem.title, ); } @override Future<void> changeQueueLists(List<MediaItem> mediaitems, {int index = 0}) async { // Here is the replacement playlist final audioSource = mediaitems.map(_futterSongItem); // _playlist.clear(); // _ playlist.addAll(audioSource.toList()); // Add to Playlist _songlist.clear(); _songlist.addAll(audioSource.toList()); _curIndex = index; // Changed the playlist and set the index to 0 // notify system queue.value.clear(); final newQueue = queue.value..addAll(mediaitems); queue.add(newQueue); // Add to background playlist } @override Future<void> playIndex(int index) async { // Subscript received _curIndex = index; readySongUrl(); } @override Future<void> removeQueueItemAt(int index) async { // manage Just Audio _playlist.removeAt(index); // notify system final newQueue = queue.value..removeAt(index); queue.add(newQueue); } @override Future<void> addQueueItem(MediaItem mediaItem) async { // manage Just Audio if (_songlist.length > 0) { // Judge whether the current song is in the last position _songlist.insert(_curIndex + 1, _futterSongItem(mediaItem)); final newQueue = queue.value..insert(_curIndex + 1, mediaItem); _curIndex++; queue.add(newQueue); } else { _songlist.insert(_curIndex, _futterSongItem(mediaItem)); final newQueue = queue.value..insert(_curIndex, mediaItem); queue.add(newQueue); } // notify system } @override Future<void> readySongUrl() async { // Here is the url to get the song var song = this._songlist[_curIndex]; String url = await getSongUrl({"id": '${song.id}'}); if (url.isNotEmpty) { // Load music url = url.replaceFirst('http', 'https'); try { await _player.setAudioSource(AudioSource.uri( Uri.parse(url), tag: MediaItem( id: '${song.id}', title: song.name, artist: song.artists, duration: song.timer, artUri: Uri.parse(song.picUrl), ), )); } catch (e) { print('error======$e'); } // It needs to be updated again final playlist = queue.value; if (_curIndex == null || playlist.isEmpty) return; mediaItem.add(playlist[_curIndex]); play(); // play } } @override Future<void> play() async { _player.play(); } @override Future<void> pause() => _player.pause(); @override Future<void> seek(Duration position) => _player.seek(position); @override Future<void> skipToNext() async { // When triggered, play the next song if (_curIndex >= _songlist.length - 1) { _curIndex = 0; } else { _curIndex++; } // Then trigger to get the url readySongUrl(); final model = getIt<PageManager>(); print('Trigger to play the next song'); if (model.isPersonFm.value) { // If it's private fm print('private fm========$_curIndex'); if (_curIndex == _songlist.length - 1) { // Judge if it's the last song print('trigger'); model.getPersonFmList(); } } } @override Future<void> skipToPrevious() async { if (_curIndex <= 0) { _curIndex = _songlist.length - 1; } else { _curIndex--; } readySongUrl(); } @override Future<void> stop() async { await _player.stop(); return super.stop(); } }
_ Player comfort player_ The playlist has been abandoned and has not been used_ songlist playlist_ curIndex playback index
MyAudioHandler() { // initialization // _ loadEmptyPlaylist(); // Load playlist _notifyAudioHandlerAboutPlaybackEvents(); // Background status change _listenForDurationChanges(); // Update background when time changes _listenPlayEnd(); // _ listenForCurrentSongIndexChanges(); // This is also the background }
I'll just talk about the above code with a few comments
readySongUrl # is my customized url for loading audio triggered before play
@override Future<void> readySongUrl() async { // Here is the url to get the song var song = this._songlist[_curIndex]; String url = await getSongUrl({"id": '${song.id}'}); if (url.isNotEmpty) { // Load music url = url.replaceFirst('http', 'https'); try { await _player.setAudioSource(AudioSource.uri( Uri.parse(url), tag: MediaItem( id: '${song.id}', title: song.name, artist: song.artists, duration: song.timer, artUri: Uri.parse(song.picUrl), ), )); } catch (e) { print('error======$e'); } // It needs to be updated again final playlist = queue.value; if (_curIndex == null || playlist.isEmpty) return; mediaItem.add(playlist[_curIndex]); play(); // play } }
How to control playback on a page
// Global processing import 'dart:async'; import 'package:audio_service/audio_service.dart'; import 'package:flutter/foundation.dart'; import 'package:netease_app/http/request.dart'; import 'package:netease_app/model/songs.dart'; import 'package:netease_app/notifiers/play_button_notifier.dart'; import 'package:netease_app/notifiers/play_item_notifier.dart'; import 'package:netease_app/notifiers/progress_notifier.dart'; import 'package:netease_app/services/audio_handler.dart'; import 'package:netease_app/services/playlist_repository.dart'; import 'package:netease_app/services/service_locator.dart'; class PageManager { final currentSongTitleNotifier = PlayItemNotifier(); final playlistNotifier = ValueNotifier<List>([]); // playlist final progressNotifier = ProgressNotifier(); // Time progress bar // final repeatButtonNotifier = RepeatButtonNotifier(); final isFirstSongNotifier = ValueNotifier<bool>(true); // Is it the first song final playButtonNotifier = PlayButtonNotifier(); final isLastSongNotifier = ValueNotifier<bool>(true); // Is it the last song final isShuffleModeEnabledNotifier = ValueNotifier<bool>(false); final _audioHandler = getIt<AudioPlayerHandler>(); final isPersonFm = ValueNotifier<bool>(false); // Is it currently private fm final waitPersonFm = ValueNotifier<bool>(false); // initialization void init() async { await _loadPlaylist(); _listenToChangesInPlaylist(); _listenToPlaybackState(); _listenToCurrentPosition(); _listenToBufferedPosition(); _listenToTotalDuration(); _listenToChangesInSong(); } Future<void> _loadPlaylist() async { final songRepository = getIt<PlaylistRepository>(); final playlist = await songRepository.fetchInitialPlaylist(); final mediaItems = playlist .map((song) => MediaItem( id: song['id'] ?? '', album: song['album'] ?? '', title: song['title'] ?? '', extras: {'url': song['url']}, )) .toList(); _audioHandler.addQueueItems(mediaItems); } void _listenToChangesInPlaylist() { _audioHandler.queue.listen((playlist) { if (playlist.isEmpty) { playlistNotifier.value = []; currentSongTitleNotifier.value = MediaItem(id: '', title: ''); } else { final newList = playlist; playlistNotifier.value = newList; } _updateSkipButtons(); }); } void _listenToPlaybackState() { // Monitor playback status _audioHandler.playbackState.listen((playbackState) { final isPlaying = playbackState.playing; final processingState = playbackState.processingState; print(processingState); if (processingState == AudioProcessingState.loading || processingState == AudioProcessingState.buffering) { // Loading playButtonNotifier.value = ButtonState.loading; } else if (!isPlaying) { // No play playButtonNotifier.value = ButtonState.paused; } else if (processingState != AudioProcessingState.completed) { // Playing playButtonNotifier.value = ButtonState.playing; } else if (isPlaying) { // playButtonNotifier.value = ButtonState.playing; } else { // Reset // print('reset trigger '); // _audioHandler.seek(Duration.zero); // _audioHandler.pause(); } }); } // Update playback time void _listenToCurrentPosition() { AudioService.position.listen((position) { // print('playback time $position '); final oldState = progressNotifier.value; progressNotifier.value = ProgressBarState( current: position, buffered: oldState.buffered, total: oldState.total, ); }); } // Update buffer location void _listenToBufferedPosition() { _audioHandler.playbackState.listen((playbackState) { final oldState = progressNotifier.value; progressNotifier.value = ProgressBarState( current: oldState.current, buffered: playbackState.bufferedPosition, total: oldState.total, ); }); } // Total update time void _listenToTotalDuration() { _audioHandler.mediaItem.listen((mediaItem) { final oldState = progressNotifier.value; progressNotifier.value = ProgressBarState( current: oldState.current, buffered: oldState.buffered, total: mediaItem?.duration ?? Duration.zero, ); }); } // Update songs displays the latest songs void _listenToChangesInSong() { _audioHandler.mediaItem.listen((mediaItem) { currentSongTitleNotifier.value = mediaItem!; print('Item$mediaItem'); _updateSkipButtons(); }); } void _updateSkipButtons() { final mediaItem = _audioHandler.mediaItem.value; final playlist = _audioHandler.queue.value; if (playlist.length < 2 || mediaItem == null) { isFirstSongNotifier.value = true; isLastSongNotifier.value = true; } else { isFirstSongNotifier.value = playlist.first == mediaItem; isLastSongNotifier.value = playlist.last == mediaItem; } // if (isPersonFm.value) { // //If this is a private Fm, when the song played is the last song, it is necessary to obtain a new song and add it to the song list // print( // 'object============${isLastSongNotifier.value}===========${waitPersonFm.value}'); // if (isLastSongNotifier.value && !waitPersonFm.value) { // waitPersonFm.value = true; // print('trigger ================ '; // this.getPersonFmList(); // } // } } // Is the change private Fm void changeIsPersonFm(bool personFm) { isPersonFm.value = personFm; } // Get private Fm Future<void> getPersonFmList() async { Map<String, dynamic> params = { "timestamp": '${DateTime.now().microsecondsSinceEpoch}', }; getPersonaFm(params).then((value) { if (value["code"] == 200) { List<Song> playsongslist = <Song>[]; value["data"].forEach((element) { playsongslist.add(Song( element["id"], Duration(milliseconds: element["duration"]), name: element["name"], picUrl: element["album"]["picUrl"], artists: createArtistString(element["artists"]), )); }); addSongs(playsongslist); } }); } createArtistString(List list) { List artistString = []; list.forEach((element) { artistString.add(element["name"]); }); return artistString.join('/'); } Future<void> play() async { await _audioHandler.readySongUrl(); } Future<void> resumePlay() async { await _audioHandler.play(); } sinkProgress(int timer) async { final oldState = progressNotifier.value; progressNotifier.value = ProgressBarState( current: Duration(milliseconds: timer), buffered: oldState.buffered, total: oldState.total, ); } void pasue() { _audioHandler.pause(); } void togglePlay() { if (playButtonNotifier.value == ButtonState.paused) { // Perform playback resumePlay(); } else { // Execution pause pasue(); } } void playInex(int index) async { await _audioHandler.playIndex(index); } void seek(Duration position) => _audioHandler.seek(position); // Time jump void previous() => _audioHandler.skipToPrevious(); // Last song void next() => _audioHandler.skipToNext(); // Next song void repeat() {} void shuffle() {} // Add a song Future<void> add(Song song) async { final songRepository = getIt<PlaylistRepository>(); final songitem = await songRepository.fetchAnotherSong(song); final mediaItem = MediaItem( id: songitem.id.toString(), album: '', artist: songitem.artists, duration: songitem.timer, title: songitem.name, artUri: Uri.parse(songitem.picUrl), extras: { 'picUrl': songitem.picUrl, }, ); _audioHandler.addQueueItem(mediaItem); } // Add a list to the playlist Future<void> addSongs(List<Song> songlist, {int index = 0}) async { final List<MediaItem> mediaItems = <MediaItem>[]; songlist.forEach((element) { mediaItems.add(MediaItem( id: element.id.toString(), album: '', artist: element.artists, duration: element.timer, title: element.name, artUri: Uri.parse(element.picUrl), extras: { 'picUrl': element.picUrl, }, )); }); await _audioHandler.addQueueItems(mediaItems); } Future<void> changesonglistplay(List<Song> list, {int index = 0}) async { final List<MediaItem> mediaItems = <MediaItem>[]; list.forEach((element) { mediaItems.add(MediaItem( id: element.id.toString(), album: '', artist: element.artists, duration: element.timer, title: element.name, artUri: Uri.parse(element.picUrl), extras: { 'picUrl': element.picUrl, }, )); }); await _audioHandler.changeQueueLists(mediaItems, index: index); } void remove() { final lastIndex = _audioHandler.queue.value.length - 1; if (lastIndex < 0) return; _audioHandler.removeQueueItemAt(lastIndex); } Future<void> changelist(list) async {} void dispose() { _audioHandler.customAction('dispose'); } void stop() { _audioHandler.stop(); } }