我被这个问题难住了。我有一个audioservice
(audioplayer.dart
)的代码,它需要一个queue
来玩。我使用ModalRoute
从audioplayer.dart
中的playlist.dart
获得队列,并保存在全局变量queue
中。然后,我初始化AudioPlayerService。现在一切直到这里都很好,但在扩展BackgroundAudioTask
的AudioPlayerTask
类中,当我尝试访问变量(在onStart
内部)时,它出现了一个空列表。我不知道问题在哪里,我对BackgroundAudioTask
类也不太熟悉。下面是它的样子:
import .....
List<MediaItem> queue = [];
class TempScreen extends StatefulWidget {
@override
_TempScreenState createState() => _TempScreenState();
}
class _TempScreenState extends State<TempScreen> {
@override
Widget build(BuildContext context) {
queue = ModalRoute.of(context).settings.arguments;
// NOW HERE THE QUEUE IS FINE
return Container(.....all ui code);
}
// I'm using this button to start the service
audioPlayerButton() {
AudioService.start(
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
androidNotificationChannelName: 'Audio Service Demo',
androidNotificationColor: 0xFF2196f3,
androidNotificationIcon: 'mipmap/ic_launcher',
androidEnableQueue: true,
);
AudioService.updateQueue(queue);
print('updated queue at the start');
print('queue now is $queue');
AudioService.setRepeatMode(AudioServiceRepeatMode.none);
AudioService.setShuffleMode(AudioServiceShuffleMode.none);
AudioService.play();
}
}
void _audioPlayerTaskEntrypoint() async {
AudioServiceBackground.run(() => AudioPlayerTask());
}
class AudioPlayerTask extends BackgroundAudioTask {
AudioPlayer _player = AudioPlayer();
Seeker _seeker;
StreamSubscription<PlaybackEvent> _eventSubscription;
String kUrl = '';
String key = "38346591";
String decrypt = "";
String preferredQuality = '320';
int get index => _player.currentIndex == null ? 0 : _player.currentIndex;
MediaItem get mediaItem => index == null ? queue[0] : queue[index];
// This is just a function i'm using to get song URLs
fetchSongUrl(songId) async {
print('starting fetching url');
String songUrl =
"https://www.jiosaavn.com/api.php?app_version=5.18.3&api_version=4&readable_version=5.18.3&v=79&_format=json&__call=song.getDetails&pids=" +
songId;
var res = await get(songUrl, headers: {"Accept": "application/json"});
var resEdited = (res.body).split("-->");
var getMain = jsonDecode(resEdited[1]);
kUrl = await DesPlugin.decrypt(
key, getMain[songId]["more_info"]["encrypted_media_url"]);
kUrl = kUrl.replaceAll('96', '$preferredQuality');
print('fetched url');
return kUrl;
}
@override
Future<void> onStart(Map<String, dynamic> params) async {
print('inside onStart of audioPlayertask');
print('queue now is $queue');
// NOW HERE QUEUE COMES OUT TO BE AN EMPTY LIST
final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration.speech());
if (queue.length == 0) {
print('queue is found to be null.........');
}
_player.currentIndexStream.listen((index) {
if (index != null) AudioServiceBackground.setMediaItem(queue[index]);
});
// Propagate all events from the audio player to AudioService clients.
_eventSubscription = _player.playbackEventStream.listen((event) {
_broadcastState();
});
// Special processing for state transitions.
_player.processingStateStream.listen((state) {
switch (state) {
case ProcessingState.completed:
AudioService.currentMediaItem != queue.last
? AudioService.skipToNext()
: AudioService.stop();
break;
case ProcessingState.ready:
break;
default:
break;
}
});
// Load and broadcast the queue
print('queue is');
print(queue);
print('Index is $index');
print('MediaItem is');
print(queue[index]);
try {
if (queue[index].extras == null) {
queue[index] = queue[index].copyWith(extras: {
'URL': await fetchSongUrl(queue[index].id),
});
}
await AudioServiceBackground.setQueue(queue);
await _player.setUrl(queue[index].extras['URL']);
onPlay();
} catch (e) {
print("Error: $e");
onStop();
}
}
@override
Future<void> onSkipToQueueItem(String mediaId) async {
// Then default implementations of onSkipToNext and onSkipToPrevious will
// delegate to this method.
final newIndex = queue.indexWhere((item) => item.id == mediaId);
if (newIndex == -1) return;
_player.pause();
if (queue[newIndex].extras == null) {
queue[newIndex] = queue[newIndex].copyWith(extras: {
'URL': await fetchSongUrl(queue[newIndex].id),
});
await AudioServiceBackground.setQueue(queue);
// AudioService.updateQueue(queue);
}
await _player.setUrl(queue[newIndex].extras['URL']);
_player.play();
await AudioServiceBackground.setMediaItem(queue[newIndex]);
}
@override
Future<void> onUpdateQueue(List<MediaItem> queue) {
AudioServiceBackground.setQueue(queue = queue);
return super.onUpdateQueue(queue);
}
@override
Future<void> onPlay() => _player.play();
@override
Future<void> onPause() => _player.pause();
@override
Future<void> onSeekTo(Duration position) => _player.seek(position);
@override
Future<void> onFastForward() => _seekRelative(fastForwardInterval);
@override
Future<void> onRewind() => _seekRelative(-rewindInterval);
@override
Future<void> onSeekForward(bool begin) async => _seekContinuously(begin, 1);
@override
Future<void> onSeekBackward(bool begin) async => _seekContinuously(begin, -1);
@override
Future<void> onStop() async {
await _player.dispose();
_eventSubscription.cancel();
await _broadcastState();
// Shut down this task
await super.onStop();
}
Future<void> _seekRelative(Duration offset) async {
var newPosition = _player.position + offset;
// Make sure we don't jump out of bounds.
if (newPosition < Duration.zero) newPosition = Duration.zero;
if (newPosition > mediaItem.duration) newPosition = mediaItem.duration;
// Perform the jump via a seek.
await _player.seek(newPosition);
}
void _seekContinuously(bool begin, int direction) {
_seeker?.stop();
if (begin) {
_seeker = Seeker(_player, Duration(seconds: 10 * direction),
Duration(seconds: 1), mediaItem)
..start();
}
}
/// Broadcasts the current state to all clients.
Future<void> _broadcastState() async {
await AudioServiceBackground.setState(
controls: [
MediaControl.skipToPrevious,
if (_player.playing) MediaControl.pause else MediaControl.play,
MediaControl.stop,
MediaControl.skipToNext,
],
systemActions: [
MediaAction.seekTo,
MediaAction.seekForward,
MediaAction.seekBackward,
],
androidCompactActions: [0, 1, 3],
processingState: _getProcessingState(),
playing: _player.playing,
position: _player.position,
bufferedPosition: _player.bufferedPosition,
speed: _player.speed,
);
}
AudioProcessingState _getProcessingState() {
switch (_player.processingState) {
case ProcessingState.idle:
return AudioProcessingState.stopped;
case ProcessingState.loading:
return AudioProcessingState.connecting;
case ProcessingState.buffering:
return AudioProcessingState.buffering;
case ProcessingState.ready:
return AudioProcessingState.ready;
case ProcessingState.completed:
return AudioProcessingState.completed;
default:
throw Exception("Invalid state: ${_player.processingState}");
}
}
}
如果需要,这是AudioService的完整代码。
(回答更新:自v0.18以来,由于UI和后台代码在共享隔离中运行,因此这种缺陷不存在。下面的答案只适用于v0.17和更早的版本。
audio_service在单独的隔离中运行BackgroundAudioTask
。在README中,它是这样写的:
请注意,您的UI和后台任务在单独的隔离中运行,并且不共享内存。它们唯一的通信方式是通过消息传递。您的Flutter UI将仅使用
AudioService
API与后台任务通信,而您的后台任务将仅使用AudioServiceBackground
API与UI和其他客户端交互。
这里的关键点是隔离不共享内存。如果你设置了"全局"变量,它不会在后台隔离中设置,因为后台隔离有自己单独的内存块。这就是为什么您的全局queue
变量为空。它实际上不是同一个变量,因为现在你实际上有两个变量的副本:一个在UI隔离中,已经设置了一个值,另一个在后台隔离中,还没有设置一个值。
现在,你的背景隔离'稍后';设置它自己的队列变量的副本,这是通过消息传递API发生的,你将队列从UI隔离传递到updateQueue
,后台隔离接收该消息并将其存储到onUpdateQueue
中它自己的变量副本中。如果在此点之后打印出队列,它将不再为空。
在onStart
中还有一行,您试图设置队列,尽管您可能应该删除该代码并让队列仅在onUpdateQueue
中设置。您不应该尝试访问onStart
中的队列,因为您的队列直到onUpdateQueue
才会接收到它的值。如果你想避免任何空指针异常之前,你可以在后台隔离初始化队列为空列表,它最终会被onUpdateQueue
中的非空列表所取代,而不会为空。
我还建议您避免将queue
作为全局变量。全局变量通常是不好的,但在这种情况下,它实际上可能会让你感到困惑,认为队列变量在UI和后台隔离中都是相同的,而实际上每个隔离都有自己的变量副本,可能具有不同的值。因此,如果您使用两个单独的"local"变量。一个在UI内,一个在后台任务内。
另一个建议是,您应该注意消息传递API中的方法是异步方法。您应该等待音频服务启动,然后再向它发送消息,例如设置队列。并且你应该等待队列设置好之后再尝试从队列中播放:
await AudioService.start(....);
// Now the service has started, it is safe to send messages.
await AudioService.updateQueue(...);
// Now the queue has been updated, it is safe to play from it.