How to pass and play specific queue position media item from playlist in audio_service flutter? - flutter

I am using flutter audio_service and just_audio package for music player. I want to play specific queue position media item from playlist when I initialize the music player. It is always playing first item of the playlist when I called AudioService.start() method. How can I pass and play specific queue position media item from playlist when I start the audio service?
AudioService start
AudioService.start(
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
androidNotificationChannelName: 'Zenmind',
androidNotificationColor: 0xFF2196f3,
androidNotificationIcon: 'mipmap/ic_launcher',
androidEnableQueue: true,
params: params); // [params contains playlist ]
_audioPlayerTaskEntrypoint code
void _audioPlayerTaskEntrypoint() async {
AudioServiceBackground.run(() => AudioPlayerTask());
}
AudioPlayerTask class
class AudioPlayerTask extends BackgroundAudioTask {
var _queue = <MediaItem>[];
AudioPlayer _player = new AudioPlayer();
AudioProcessingState _skipState;
Seeker _seeker;
StreamSubscription<PlaybackEvent> _eventSubscription;
List<MediaItem> get queue => _queue;
int get index => _player.currentIndex;
MediaItem get mediaItem => index == null ? null : queue[index];
#override
Future<void> onStart(Map<String, dynamic> params) async {
_queue.clear();
List mediaItems = params['data'];
// print(params['data']);
for (int i = 0; i < mediaItems.length; i++) {
MediaItem mediaItem = MediaItem.fromJson(mediaItems[i]);
_queue.add(mediaItem);
}
_player.currentIndexStream.listen((index) {
print("index value is $index");
if (index != null) {
AudioServiceBackground.setMediaItem(queue[index]);
}
});
_eventSubscription = _player.playbackEventStream.listen((event) {
_broadcastState();
});
_player.processingStateStream.listen((state) {
switch (state) {
case ProcessingState.completed:
onStop();
break;
case ProcessingState.ready:
_skipState = null;
break;
default:
break;
}
});
AudioServiceBackground.setQueue(queue);
try {
await _player.setAudioSource(ConcatenatingAudioSource(
children:
queue.map((item) => AudioSource.uri(Uri.parse(item.id))).toList(),
));
onSkipToQueueItem(queue[1].id);
onPlay();
} catch (e) {
print("Error: $e");
onStop();
}
}
#override
Future<void> onSkipToQueueItem(String mediaId) async {
final newIndex = queue.indexWhere((item) => item.id == mediaId);
if (newIndex == -1) return;
_skipState = newIndex > index
? AudioProcessingState.skippingToNext
: AudioProcessingState.skippingToPrevious;
_player.seek(Duration.zero, index: newIndex);
AudioServiceBackground.sendCustomEvent('skip to $newIndex');
}
#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();
await super.onStop();
}
Future<void> _seekRelative(Duration offset) async {
var newPosition = _player.position + offset;
if (newPosition < Duration.zero) newPosition = Duration.zero;
if (newPosition > mediaItem.duration) newPosition = mediaItem.duration;
// if (newPosition > _player.duration) newPosition = _player.duration;
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)
Duration(seconds: 1),
queue[_player.currentIndex])
..start();
}
}
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() {
if (_skipState != null) return _skipState;
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}");
}
}
}

In audio_service 0.17, the params passed into start() were only intended for simple data types, not for lists of MediaItems. In fact there are other methods in the API specifically designed for that.
I suggest the following startup sequence instead:
// Set the playlist
await AudioService.updateQueue(playlist);
// Jump to the right item
await AudioService.skipToQueueItem(...);
// Play
AudioService.play(); // don't await!
Note: Replace AudioService. by audioHandler. if you use version 0.18.0 or later.
The await keyword above is important. These methods are asynchronous, and the later methods should not be called until the earlier ones have completed. For example, you don't want to skip to a particular queue item until after the queue has actually been set. But note the lack of await on the last step: you don't await the play call unless you want to wait for playback to complete.
In your background audio task (0.17) or audio handler (0.18), add the callback for updateQueue:
// 0.17 solution:
Future<void> onUpdateQueue(List<MediaItem> queue) async {
AudioServiceBackground.setQueue(_queue = queue);
await _player.setAudioSource(ConcatenatingAudioSource(
children:
queue.map((item) => AudioSource.uri(Uri.parse(item.id))).toList(),
));
// 0.18 solution:
Future<void> updateQueue(List<MediaItem> queue) async {
this.queue.add(_queue = queue);
await _player.setAudioSource(ConcatenatingAudioSource(
children:
queue.map((item) => AudioSource.uri(Uri.parse(item.id))).toList(),
));
}
You already have an onStart, but remember that using the suggested startup sequence above, the queue will be set in a later step, and the player will skip to the right queue item in a later step, so you can remove those parts from your onStart, and just keep the code that initialises the event listeners. (In 0.18, that logic would go in your audio handler constructor).

Related

Why I got : Error: MissingPluginException(No implementation found for method startScan on channel flutter_bluetooth_brazilian/methods)?

I'm trying to connect devices with Bluetooth and then share data. The problem now is that I always get: Missing Plugin Exception for flutter_bluetooth bluetooth_Brazilian.
this is bluetooth_manager.dart code:
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:rxdart/rxdart.dart';
import 'bluetooth_device.dart';
/// A BluetoothManager.
class BluetoothManager {
static const String NAMESPACE = 'flutter_bluetooth_brazilian';
static const int CONNECTED = 1;
static const int DISCONNECTED = 0;
static const MethodChannel _channel =
const MethodChannel('$NAMESPACE/methods');
static const EventChannel _stateChannel =
const EventChannel('$NAMESPACE/state');
Stream<MethodCall> get _methodStream => _methodStreamController.stream;
final StreamController<MethodCall> _methodStreamController =
StreamController.broadcast();
BluetoothManager._() {
_channel.setMethodCallHandler((MethodCall call) {
_methodStreamController.add(call);
return;
});
}
static BluetoothManager _instance = BluetoothManager._();
static BluetoothManager get instance => _instance;
// Future<bool> get isAvailable async =>
// await _channel.invokeMethod('isAvailable').then<bool>((d) => d);
// Future<bool> get isOn async =>
// await _channel.invokeMethod('isOn').then<bool>((d) => d);
Future<bool> get isConnected async =>
await _channel.invokeMethod('isConnected');
BehaviorSubject<bool> _isScanning = BehaviorSubject.seeded(false);
Stream<bool> get isScanning => _isScanning.stream;
BehaviorSubject<List<BluetoothDevice>> _scanResults =
BehaviorSubject.seeded([]);
Stream<List<BluetoothDevice>> get scanResults => _scanResults.stream;
PublishSubject _stopScanPill = new PublishSubject();
/// Gets the current state of the Bluetooth module
Stream<int> get state async* {
yield await _channel.invokeMethod('state').then((s) => s);
yield* _stateChannel.receiveBroadcastStream().map((s) => s);
}
/// Starts a scan for Bluetooth Low Energy devices
/// Timeout closes the stream after a specified [Duration]
Stream<BluetoothDevice> scan({
Duration timeout,
}) async* {
if (_isScanning.value == true) {
throw Exception('Another scan is already in progress.');
}
// Emit to isScanning
_isScanning.add(true);
final killStreams = <Stream>[];
killStreams.add(_stopScanPill);
if (timeout != null) {
killStreams.add(Rx.timer(null, timeout));
}
// Clear scan results list
_scanResults.add(<BluetoothDevice>[]);
try {
await _channel.invokeMethod('startScan');
} catch (e) {
print('Error starting scan.');
_stopScanPill.add(null);
_isScanning.add(false);
throw e;
}
yield* BluetoothManager.instance._methodStream
.where((m) => m.method == "ScanResult")
.map((m) => m.arguments)
.takeUntil(Rx.merge(killStreams))
.doOnDone(stopScan)
.map((map) {
final device = BluetoothDevice.fromJson(Map<String, dynamic>.from(map));
final List<BluetoothDevice> list = _scanResults.value;
int newIndex = -1;
list.asMap().forEach((index, e) {
if (e.address == device.address) {
newIndex = index;
}
});
if (newIndex != -1) {
list[newIndex] = device;
} else {
list.add(device);
}
_scanResults.add(list);
return device;
});
}
Future startScan({
Duration timeout,
}) async {
await scan(timeout: timeout).drain();
return _scanResults.value;
}
/// Stops a scan for Bluetooth Low Energy devices
Future stopScan() async {
await _channel.invokeMethod('stopScan');
_stopScanPill.add(null);
_isScanning.add(false);
}
Future<dynamic> connect(BluetoothDevice device) =>
_channel.invokeMethod('connect', device.toJson());
Future<dynamic> disconnect() => _channel.invokeMethod('disconnect');
Future<dynamic> destroy() => _channel.invokeMethod('destroy');
Future<dynamic> writeData(List<int> bytes) {
Map<String, Object> args = Map();
args['bytes'] = bytes;
args['length'] = bytes.length;
_channel.invokeMethod('writeData', args);
return Future.value(true);
}
}
flutter_bluetooth_brazilian doesn't support the web platform. You can check the supported platforms right on the plugin's page at https://pub.dev.

Is there a way to access data coming from BLE device from single file which keeps updating?

I want to access characteristic values of BLE from one dart file, What I am doing is that I am connecting the device from one activity and then sending the device info to all other activities. But to get values I have to write the same code again and again to all activities/dart files.
For example i am connecting device in an activity like this:
StreamBuilder<List<ScanResult>>(
stream: FlutterBlue.instance.scanResults,
initialData: [],
builder: (c, snapshot) => Column(
children: snapshot.data
.map(
(r) => ScanResultTile(
result: r,
onTap: () => Navigator.of(context)
.push(MaterialPageRoute(builder: (context) {
r.device.connect();
print('DEVICE CONNECTED');
return BluetoothConnectedSuccess(device: r.device);
Here device: r.device is the device that i have connected to my Flutter App. Now if i want to display device data i have to initilaze these lines of code everytime i jump to any screen/activity:
class BluetoothConnectedSuccess extends StatefulWidget {
const BluetoothConnectedSuccess({Key key, this.device}) : super(key: key);
final BluetoothDevice device;
#override
_BluetoothConnectedSuccessState createState() =>
_BluetoothConnectedSuccessState();
}
class _BluetoothConnectedSuccessState extends State<BluetoothConnectedSuccess> {
// BLE
final String SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b";
final String CHARACTERISTIC_UUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8";
bool isReady;
Stream<List<int>> stream;
List<int> lastValue;
List<double> traceDust = List();
#override
void initState() {
super.initState();
isReady = false;
connectToDevice();
}
connectToDevice() async {
await widget.device.connect();
discoverServices();
}
discoverServices() async {
List<BluetoothService> services = await widget.device.discoverServices();
services.forEach((service) {
if (service.uuid.toString() == SERVICE_UUID) {
service.characteristics.forEach((characteristic) {
if (characteristic.uuid.toString() == CHARACTERISTIC_UUID) {
characteristic.setNotifyValue(!characteristic.isNotifying);
stream = characteristic.value;
print(stream);
lastValue = characteristic.lastValue;
print(lastValue);
setState(() {
isReady = true;
});
}
});
}
});
}
_dataParser(List<int> data) {
var value = Uint8List.fromList(data);
print("stream.value: $value"); // stream.value: [33]
var hr = ByteData.sublistView(value, 0, 1);
print("Heart rate: ${hr.getUint8(0)}");
return hr.getUint8(0); // Heart rate: 33
}
It's creating a lot of mess to write the same code again and again to the activities where BLE data is needed.
Is there a way to only call this connected device from a single file instead of initializing the same code in every activity?
This is the link to my repo for a look at what I am doing on every activity/screen with BLE device data.
Please help me out as I am new to Flutter. Thank you
Firstly learn basic of state management using Get you can refer to my code here what happens is every time something changes or upadtes it will immediate show in UI using specific Get Widgets like Obx and GetX , these widgets listen to changes in value which are marked with obs (observable).
for exmaple :
Obx(
() => ble.isScan.value ? LinearProgressIndicator() : SizedBox(),
),
this will observe the changes in isScan value .
class BleServices extends GetxController {
FlutterBlue blue = FlutterBlue.instance;
BluetoothDevice d;
var connectedDevices = List<BluetoothDevice>().obs;
var scanResults = List<ScanResult>().obs;
var bleState = BluetoothState.off.obs;
var isScan = false.obs;
var scanRequire = false.obs;
var bluetoothServices = List<BluetoothService>().obs;
var discoveringServices = false.obs;
var characteristics = List<BluetoothCharacteristic>().obs;
#override
void onInit() async {
super.onInit();
final perb = await Permission.bluetooth.status.isGranted;
final perL = await Permission.location.status.isGranted;
if (perb && perL) {
getConnectedDevices();
} else {
await Permission.bluetooth.request();
await Permission.location.request();
}
isScanning();
state();
}
isDiscovering() async {}
getConnectedDevices() async {
final connectedDevice = await blue.connectedDevices;
connectedDevices.value = connectedDevice;
AppLogger.print('connected devices : $connectedDevice');
if (connectedDevice.length == 0) {
scanRequire.value = true;
searchDevices();
}
return connectedDevice;
}
searchDevices() {
// AppLogger.print('pppppppppppppp');
blue
.scan(timeout: Duration(seconds: 20))
.distinct()
.asBroadcastStream()
.listen((event) {
AppLogger.print(event.toString());
scanResults.addIf(!scanResults.contains(event), event);
});
Future.delayed(Duration(seconds: 20), () {
blue.stopScan();
Get.showSnackbar(GetBar(
message: 'scan is finished',
));
});
}
isScanning() {
blue.isScanning.listen((event) {
AppLogger.print(event.toString());
isScan.value = event;
});
}
state() {
blue.state.listen((event) {
AppLogger.print(event.toString());
bleState.value = event;
});
}
}

How to send data to AudioServiceTask class which extends BackgroundAudioTask from UI

Well, I'm stuck on this problem. I have a code for audioservice (audioplayer.dart) which takes a queue to play. I'm getting the queue from playlist.dart in audioplayer.dart using ModalRoute and save in a global variable queue. Then, I initialize the AudioPlayerService. Now everything till here is fine but inside the AudioPlayerTask class which extends BackgroundAudioTask, when I try to access the variable (inside onStart) it comes out to be an empty list. I don't know where the problem is and I'm not very much familier with the BackgroundAudioTask class. Here's how it looks like:
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 is the full code for AudioService in-case needed.
(Answer update: Since v0.18, this sort of pitfall doesn't exist since the UI and background code run in a shared isolate. The answer below is only relevant for v0.17 and earlier.)
audio_service runs your BackgroundAudioTask in a separate isolate. In the README, it is put this way:
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.
The key point there is that isolates do not share memory. If you set a "global" variable in the UI isolate, it will not be set in the background isolate because the background isolate has its own separate block of memory. That is why your global queue variable is null. It is not actually the same variable, because now you actually have two copies of the variable: one in the UI isolate which has been set with a value, and the other in the background isolate which has not (yet) been set with a value.
Now, your background isolate does "later" set its own copy of the queue variable to something, and this happens via the message passing API where you pass the queue from the UI isolate into updateQueue and the background isolate receive that message and stores it into its own copy of the variable in onUpdateQueue. If you were to print out the queue after this point it would no longer be null.
There is also a line in your onStart where you are attempting to set the queue, although you should probably delete that code and let the queue only be set in onUpdateQueue. You should not attempt to access the queue in onStart since your queue won't receive its value until onUpdateQueue. If you want to avoid any null pointer exception before its set, you can initialise the queue in the background isolate to an empty list, and it will eventually get replaced by a non-empty list in onUpdateQueue without ever being null.
I would also suggest you avoid making queue a global variable. Global variables are generally bad, but in this case, it may actually be confusing you into thinking that that queue variable is the same in both the UI and the background isolate when in reality each isolate will have its own copy of the variable perhaps with different values. Thus, your code will be clearer if you make two separate "local" variables. One inside the UI and one inside the background task.
One more suggestion is that you should note that the methods in the message passing API are asynchronous methods. You should wait for the audio service to start before you send messages to it, such as setting the queue. AND you should wait for the queue to be set before you try to play from the queue:
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.

Flutter AudioService plugin not working on Ios release

I'm using AudioService plugin and it works fine on Android and in debug mode in iOS. But once I test it on a real iOS device (in release mode) It's giving me exceptions
first at all
void quranStartListeningPoint() => AudioServiceBackground.run(() => QuranAudioService());
second this is my play Function
void startListeningToAyah({model.Ayah ayah, model.Surah surah}) {
if (AudioService.running) {
await AudioService.stop();
await Future.delayed(Duration(seconds: 1));
}
await AudioService.start(
androidNotificationColor: 0XFFB590EE,
backgroundTaskEntrypoint: quranStartListeningPoint,
params: {
'sheikhId': "$selectedReciter",
"ayahIndex": ayah.numberInSurah,
"surah": surah.number,
"quranModel": _quranDao.quranModelAsJson
},
);
}
Next I convert quranModelAsJson from a json to a model because I need the whole model in the Service Class and this is the only way to send it (as I think)
class QuranAudioService extends BackgroundAudioTask {
final _audioPlayer = AudioPlayer();
final String baseUrl = "https://cdn.alquran.cloud/media/audio/ayah/";
String ayahUrl;
int surahNumber;
int ayahIndex;
int renewSurah = 0;
QuranModel model;
Surah surah;
String sheikhId;
#override
Future<void> onStart(Map<String, dynamic> params) async {
await _audioPlayer.setReleaseMode(ReleaseMode.STOP);
implementParams(params);
onCompleteListener();
AudioServiceBackground.setState(
systemActions: [MediaAction.seekTo],
controls: getPlayControllers(),
playing: true,
processingState: AudioProcessingState.connecting);
ayahUrl = "$baseUrl$sheikhId/${surah.ayahs[ayahIndex].number}";
await _audioPlayer.play("$ayahUrl");
setMediaItem();
AudioServiceBackground.setState(
controls: getPlayControllers(),
playing: true,
processingState: AudioProcessingState.ready,
systemActions: [MediaAction.seekTo],
);
}
void setMediaItem() {
AudioServiceBackground.setMediaItem(
MediaItem(
extras: {"surahIndex": surahNumber, "renewSurah": renewSurah},
id: "$ayahIndex",
album: "${surah.englishName}",
title: "${surah.name}",
),
);
}
#override
Future<void> onPause() async {
// Broadcast that we're paused, and what controls are available.
AudioServiceBackground.setState(
controls: getPauseControllers(),
systemActions: [MediaAction.seekTo],
playing: false,
processingState: AudioProcessingState.ready);
// Pause the audio.
_audioPlayer.pause();
}
#override
Future<void> onStop() async {
_audioPlayer.stop();
if (ayahIndex == surah.ayahs.length) {
await AudioServiceBackground.setState(
controls: [replayControl],
playing: false,
processingState: AudioProcessingState.stopped);
} else {
await AudioServiceBackground.setState(
controls: [],
playing: false,
processingState: AudioProcessingState.stopped);
return super.onStop();
}
}
#override
Future<void> onPlay() async {
setMediaItem();
AudioServiceBackground.setState(
controls: getPlayControllers(),
playing: true,
processingState: AudioProcessingState.ready,
systemActions: [MediaAction.seekTo],
);
await _audioPlayer.play(ayahUrl);
renewSurah = 0;
}
#override
Future<void> onSkipToNext() async {
playNext();
}
#override
Future<void> onSkipToPrevious() async {
playPrevious();
}
// #override
// void onRewind() {
// ayahIndex = 0;
// ayahUrl = "$baseUrl$sheikhId/${surah.ayahs[ayahIndex].number}";
// this.onPlay();
// }
void implementParams(Map<String, dynamic> params) {
surahNumber = params["surah"] - 1;
ayahIndex = params["ayahIndex"] - 1;
sheikhId = params["sheikhId"];
if (model == null) model = QuranModel.fromJson(params["quranModel"]);
surah = model.surahs[surahNumber];
}
void onCompleteListener() {
_audioPlayer.onPlayerCompletion.listen((event) {
playNext();
});
}
void playNext() async {
ayahIndex++;
if (ayahIndex < surah.ayahs.length) {
ayahUrl = "$baseUrl$sheikhId/${surah.ayahs[ayahIndex].number}";
this.onPlay();
} else
changeNextSurahIndex();
}
void changeNextSurahIndex() {
renewSurah = 1;
if (surahNumber == 113) {
surahNumber = 0;
} else
surahNumber++;
ayahIndex = 0;
surah = model.surahs[surahNumber];
ayahUrl = "$baseUrl$sheikhId/${surah.ayahs[ayahIndex].number}";
this.onPlay();
}
void changePreviousSurahIndex() {
if (surahNumber == 0) {
surahNumber = 113;
} else
surahNumber--;
ayahIndex = 0;
renewSurah = 1;
surah = model.surahs[surahNumber];
ayahUrl = "$baseUrl$sheikhId/${surah.ayahs[ayahIndex].number}";
this.onPlay();
}
void playPrevious() async {
if (ayahIndex > 0) {
ayahIndex--;
ayahUrl = "$baseUrl$sheikhId/${surah.ayahs[ayahIndex].number}";
this.onPlay();
} else {
changePreviousSurahIndex();
}
}
List<MediaControl> getPlayControllers() {
return [
skipToNextControl,
pauseControl,
skipToPreviousControl,
stopControl
];
}
List<MediaControl> getPauseControllers() {
return [skipToNextControl, playControl, skipToPreviousControl, stopControl];
}
}
It was always giving me that ayahs was called on null
which means my json didnt converted to the model
so I deleted this implementation and added just a url to play
and the exception was
2020-08-31 17:58:08.458205-0400 Runner[700:75506] iOS => call startHeadlessService, playerId bb98efb6-a819-4ea7-a566-1dc6f0ff3df4
2020-08-31 17:58:08.471709-0400 Runner[700:76237] [VERBOSE-2:ui_dart_state.cc(166)] Unhandled Exception: NoSuchMethodError: The method '*' was called on null.
Receiver: null
Tried calling: *()
#0 AudioServiceBackground.run (package:audio_service/audio_service.dart:144)
<asynchronous suspension>

In flutter how can we use audio_service to fetch dynamic data

await AudioService.start(
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
androidNotificationChannelName: 'Audio Player',
androidNotificationColor: 0xFF2196f3,
androidNotificationIcon: 'mipmap/ic_launcher',
params: getParams(),
);
This is my code snippet where i am calling AudioService.start but i am unable to start service.
i am fetching the audio item from firebase and want to load those as a list view to audio_service..But i am unable to do that.My class where i have defined a audio service extending the background service.
import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart';
MediaControl playControl = MediaControl(
androidIcon: 'drawable/ic_action_play_arrow',
label: 'Play',
action: MediaAction.play,
);
MediaControl pauseControl = MediaControl(
androidIcon: 'drawable/ic_action_pause',
label: 'Pause',
action: MediaAction.pause,
);
MediaControl skipToNextControl = MediaControl(
androidIcon: 'drawable/ic_action_skip_next',
label: 'Next',
action: MediaAction.skipToNext,
);
MediaControl skipToPreviousControl = MediaControl(
androidIcon: 'drawable/ic_action_skip_previous',
label: 'Previous',
action: MediaAction.skipToPrevious,
);
MediaControl stopControl = MediaControl(
androidIcon: 'drawable/ic_action_stop',
label: 'Stop',
action: MediaAction.stop,
);
class AudioPlayerTask extends BackgroundAudioTask {
//
var _queue = <MediaItem>[];
int _queueIndex = -1;
AudioPlayer _audioPlayer = new AudioPlayer();
AudioProcessingState _skipState;
bool _playing;
bool get hasNext => _queueIndex + 1 < _queue.length;
bool get hasPrevious => _queueIndex > 0;
MediaItem get mediaItem => _queue[_queueIndex];
StreamSubscription<AudioPlaybackState> _playerStateSubscription;
StreamSubscription<AudioPlaybackEvent> _eventSubscription;
#override
void onStart(Map<String, dynamic> params) {
print("-------------------------------------started");
_queue.clear();
List mediaItems = params['data'];
for (int i = 0; i < mediaItems.length; i++) {
MediaItem mediaItem = MediaItem.fromJson(mediaItems[i]);
_queue.add(mediaItem);
}
_playerStateSubscription = _audioPlayer.playbackStateStream
.where((state) => state == AudioPlaybackState.completed)
.listen((state) {
_handlePlaybackCompleted();
});
_eventSubscription = _audioPlayer.playbackEventStream.listen((event) {
final bufferingState =
event.buffering ? AudioProcessingState.buffering : null;
switch (event.state) {
case AudioPlaybackState.paused:
_setState(
processingState: bufferingState ?? AudioProcessingState.ready,
position: event.position);
break;
case AudioPlaybackState.playing:
_setState(
processingState: bufferingState ?? AudioProcessingState.ready,
position: event.position);
break;
case AudioPlaybackState.connecting:
_setState(
processingState: _skipState ?? AudioProcessingState.connecting,
position: event.position);
break;
default:
}
});
AudioServiceBackground.setQueue(_queue);
onSkipToNext();
}
#override
void onPlay() {
if (_skipState == null) {
_playing = true;
_audioPlayer.play();
}
}
#override
void onPause() {
_playing = false;
_audioPlayer.pause();
}
#override
void onSkipToNext() async {
skip(1);
}
#override
void onSkipToPrevious() {
skip(-1);
}
void skip(int offset) async {
int newPos = _queueIndex + offset;
if (!(newPos >= 0 && newPos < _queue.length)) {
return;
}
if (null == _playing) {
_playing = true;
} else if (_playing) {
await _audioPlayer.stop();
}
_queueIndex = newPos;
_skipState = offset > 0
? AudioProcessingState.skippingToNext
: AudioProcessingState.skippingToPrevious;
AudioServiceBackground.setMediaItem(mediaItem);
await _audioPlayer.setUrl(mediaItem.id);
print(mediaItem.id);
_skipState = null;
if (_playing) {
onPlay();
} else {
_setState(processingState: AudioProcessingState.ready);
}
}
#override
Future<void> onStop() async {
_playing = false;
await _audioPlayer.stop();
await _audioPlayer.dispose();
_playerStateSubscription.cancel();
_eventSubscription.cancel();
return await super.onStop();
}
#override
void onSeekTo(Duration position) {
_audioPlayer.seek(position);
}
#override
void onClick(MediaButton button) {
playPause();
}
#override
Future<void> onFastForward() async {
await _seekRelative(fastForwardInterval);
}
#override
Future<void> onRewind() async {
await _seekRelative(rewindInterval);
}
Future<void> _seekRelative(Duration offset) async {
var newPosition = _audioPlayer.playbackEvent.position + offset;
if (newPosition < Duration.zero) {
newPosition = Duration.zero;
}
if (newPosition > mediaItem.duration) {
newPosition = mediaItem.duration;
}
await _audioPlayer.seek(_audioPlayer.playbackEvent.position + offset);
}
_handlePlaybackCompleted() {
if (hasNext) {
onSkipToNext();
} else {
onStop();
}
}
void playPause() {
if (AudioServiceBackground.state.playing)
onPause();
else
onPlay();
}
Future<void> _setState({
AudioProcessingState processingState,
Duration position,
Duration bufferedPosition,
}) async {
print('SetState $processingState');
if (position == null) {
position = _audioPlayer.playbackEvent.position;
}
await AudioServiceBackground.setState(
controls: getControls(),
systemActions: [MediaAction.seekTo],
processingState:
processingState ?? AudioServiceBackground.state.processingState,
playing: _playing,
position: position,
bufferedPosition: bufferedPosition ?? position,
speed: _audioPlayer.speed,
);
}
List<MediaControl> getControls() {
if (_playing) {
return [
skipToPreviousControl,
pauseControl,
stopControl,
skipToNextControl
];
} else {
return [
skipToPreviousControl,
playControl,
stopControl,
skipToNextControl
];
}
} this is my class
}
class AudioState {
final List<MediaItem> queue;
final MediaItem mediaItem;
final PlaybackState playbackState;
AudioState(this.queue, this.mediaItem, this.playbackState);
}
You should overridthis method inAudioPlayerTask` class:
#override
// ignore: missing_return
Future<Function> onAddQueueItem(MediaItem mediaItem) async{
// queue.add(mediaItem); or somthing like this to update your queue
await AudioServiceBackground.setQueue(queue);
try {
await _player.load(ConcatenatingAudioSource(
children:
queue.map((item) => AudioSource.uri(Uri.parse(item.id))).toList(),
));
// In this example, we automatically start playing on start.
onPlay();
} catch (e) {
print("Error: $e");
onStop();
}
}
Then you can call this function in your UI:
await AudioService.start(
backgroundTaskEntrypoint: audioPlayerTaskEntrypoint,
androidNotificationChannelName:
'Audio Service Demo',
// Enable this if you want the Android service to exit the foreground state on pause.
//androidStopForegroundOnPause: true,
androidNotificationColor: 0xFF2196f3,
androidNotificationIcon: 'mipmap/ic_launcher',
androidEnableQueue: true,
);
// call this func:
await AudioService.addQueueItem(m);
According to the author here the plugin originally didn't allow passing any arguments to the background task directly. It was designed to let the background task internally query what to play from internal storage via SQLite or tools like shared_preferences.
However today, the plugin allows to pass media data down to the background task via 3 different ways that I identified:
AudioService.addQueueItem(item) combined with onAddQueueItem as mentioned in the above answer as well as this one
AudioService.customAction('url', url) combined with onCustomAction as explained here
Use the Map<String, dynamic> params argument of the AudioService.start method. It is received as an argument of the background task's onStart method as explained here