如何将数据发送到从 UI 扩展 BackgroundAudioTask 的 AudioServiceTask class
How to send data to AudioServiceTask class which extends BackgroundAudioTask from UI
好吧,我被这个问题困住了。我有一个 audioservice
(audioplayer.dart
) 的代码,需要 queue
才能播放。我使用 ModalRoute
从 playlist.dart
in audioplayer.dart
获取队列并保存在全局变量 queue
中。然后,我初始化 AudioPlayerService。现在到这里为止一切都很好,但是在扩展 BackgroundAudioTask
的 AudioPlayerTask
class 内部,当我尝试访问变量(在 onStart
内部)时,它变成了一个空列表.我不知道问题出在哪里,而且我对 BackgroundAudioTask
class 不是很熟悉。这是它的样子:
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}");
}
}
}
This 是 AudioService 的完整代码,以备不时之需。
(答案更新:从v0.18开始,由于共享隔离中的UI和后台代码运行,这种陷阱不存在。下面的答案仅与v0相关.17 及更早版本。)
audio_service 运行 将您的 BackgroundAudioTask
放在单独的隔离区中。在README中是这样写的:
Note that your UI and background task run in separate isolates and do not share memory. The only way they communicate is via message passing. Your Flutter UI will only use the AudioService
API to communicate with the background task, while your background task will only use the AudioServiceBackground
API to interact with the UI and other clients.
这里的关键点是 isolate 不共享内存。如果您在 UI isolate 中设置了一个“全局”变量,它不会在后台 isolate 中设置,因为后台 isolate 有自己独立的内存块。这就是为什么您的全局 queue
变量为空。它实际上不是同一个变量,因为现在你实际上有两个变量副本:一个在 UI isolate 中已经设置了一个值,另一个在背景 isolate 中(尚未)设置一个值。
现在,您的后台 isolate 确实“稍后”将其自己的队列变量副本设置为某物,这是通过消息传递 API 发生的,您从 UI isolate 传递队列到 updateQueue
中,后台 isolate 收到该消息并将其存储到 onUpdateQueue
中它自己的变量副本中。如果您在此之后打印出队列,它将不再为空。
您的 onStart
中也有一行是您尝试设置队列的地方,尽管您可能应该删除该代码并让队列仅在 onUpdateQueue
中设置。您不应尝试访问 onStart
中的队列,因为您的队列要到 onUpdateQueue
才会收到它的值。如果你想在设置之前避免任何空指针异常,你可以在后台隔离中将队列初始化为一个空列表,它最终将被 onUpdateQueue
中的非空列表替换,而不会为空。
我还建议您避免将 queue
设为全局变量。全局变量通常是不好的,但在这种情况下,它实际上可能会让您感到困惑,认为队列变量在 UI 和背景隔离区中都是相同的,而实际上每个隔离区都有自己的副本可能具有不同值的变量。因此,如果您制作两个单独的“本地”变量,您的代码将更加清晰。一个在 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.
好吧,我被这个问题困住了。我有一个 audioservice
(audioplayer.dart
) 的代码,需要 queue
才能播放。我使用 ModalRoute
从 playlist.dart
in audioplayer.dart
获取队列并保存在全局变量 queue
中。然后,我初始化 AudioPlayerService。现在到这里为止一切都很好,但是在扩展 BackgroundAudioTask
的 AudioPlayerTask
class 内部,当我尝试访问变量(在 onStart
内部)时,它变成了一个空列表.我不知道问题出在哪里,而且我对 BackgroundAudioTask
class 不是很熟悉。这是它的样子:
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}");
}
}
}
This 是 AudioService 的完整代码,以备不时之需。
(答案更新:从v0.18开始,由于共享隔离中的UI和后台代码运行,这种陷阱不存在。下面的答案仅与v0相关.17 及更早版本。)
audio_service 运行 将您的 BackgroundAudioTask
放在单独的隔离区中。在README中是这样写的:
Note that your UI and background task run in separate isolates and do not share memory. The only way they communicate is via message passing. Your Flutter UI will only use the
AudioService
API to communicate with the background task, while your background task will only use theAudioServiceBackground
API to interact with the UI and other clients.
这里的关键点是 isolate 不共享内存。如果您在 UI isolate 中设置了一个“全局”变量,它不会在后台 isolate 中设置,因为后台 isolate 有自己独立的内存块。这就是为什么您的全局 queue
变量为空。它实际上不是同一个变量,因为现在你实际上有两个变量副本:一个在 UI isolate 中已经设置了一个值,另一个在背景 isolate 中(尚未)设置一个值。
现在,您的后台 isolate 确实“稍后”将其自己的队列变量副本设置为某物,这是通过消息传递 API 发生的,您从 UI isolate 传递队列到 updateQueue
中,后台 isolate 收到该消息并将其存储到 onUpdateQueue
中它自己的变量副本中。如果您在此之后打印出队列,它将不再为空。
您的 onStart
中也有一行是您尝试设置队列的地方,尽管您可能应该删除该代码并让队列仅在 onUpdateQueue
中设置。您不应尝试访问 onStart
中的队列,因为您的队列要到 onUpdateQueue
才会收到它的值。如果你想在设置之前避免任何空指针异常,你可以在后台隔离中将队列初始化为一个空列表,它最终将被 onUpdateQueue
中的非空列表替换,而不会为空。
我还建议您避免将 queue
设为全局变量。全局变量通常是不好的,但在这种情况下,它实际上可能会让您感到困惑,认为队列变量在 UI 和背景隔离区中都是相同的,而实际上每个隔离区都有自己的副本可能具有不同值的变量。因此,如果您制作两个单独的“本地”变量,您的代码将更加清晰。一个在 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.