Flutter - How to play ringtone from background task and let the user be able to stop it?

Viewed 2387

I want to play a ringtone, while my flutter app is in the background, and let the user stop it.

I want to play a ringtone after a specific interval that the user chooses (for example 3 minutes or 58 minutes), and then the user should be able to stop the ringtone. This works when the flutter app is in the foreground, but not when the app is in the background(For example when the screen is closed).

I'm currently using the callbackDispatcher fron the Workmanager plugin for flutter, which creates a new Isolate. Let's call it isolate B, to distinguish it from isolate A that is created when flutter starts my app.

Here is the callbackDispatcher:

void callbackDispatcher() {
  print('callbackDispatcher');

    FlutterRingtonePlayer.play(
      android: AndroidSounds.ringtone,
      ios: IosSounds.glass,
      looping: false,
      // Android only - API >= 28
      volume: 1,
      // Android only - API >= 28
      asAlarm: true, // Android only - all APIs
    );

    return Future.value(true);
  });
}

The problems is that I can start playing the ringtone from Isolate B, but I don't know how to let the user stop it.

So far I've tried:

  • Calling FlutterRingtonePlayer.stop() from Isolate A when a notification is clicked, but that doesn't stop the ringtone that is playing in Isolate B.
  • Starting the notification from Isolate B, and then stop the ringtone in Isolate B once clicked. But for some reason the notification doesn't popup when it's created in Isolate B.
  • Calling FlutterRingtonePlayer.stop() from Isolate A, but that doesn't stop the ringtone that is playing in Isolate B.
  • Making a global class with a method for playing and stopping ringtone and using it in Isolate B to play the ringtone and using it in Isolate A to stop the ringtone, but this also doesn't work...

I don't know how to communicate with Isolate B that Workmanager creates, and it seems like their documentation doesn't specify how to communicate with the new Isolate that is created...

What else can I try? My only goal is to play a Ringtone in background mode and let the user stop it.

3 Answers

I had the same issue (but, with the android alarm manager package and not workmanager - but, I believe the concept is still the same). I am able to solve it as follows.

Isolates are like threads but don't share memory - In that sense, they are similar to Linux/Unix processes. So, the globals/static inside FlutterRingtonePlayer is not the same in both the isolates - they are two different instances. So, we need to use messages to communicate between the isolates.

We can use ReceivePort and SendPort to communicate between both the isolates. We need to define a ReceivePort (dart:isolate package) in the callback that runs in background isolate. We need to register the send port corresponding to that receive port (<receive_port_object>.sendPort) in the IsolateNameServer (dart:ui). Then, we need to listen on that ReceivePort in the background isolate. From the foreground isolate (when the user clicks the notification as in your example), we can use IsolateNameServer.lookup to find that SendPort and send a message to the background isolate. When that message is processed in background isolate, we can do the FlutterRingtonePlayer.stop().

I was able to solve this issue by sending messages between Isolates using Port Communication.

Listen for Port messages probably in main method

final String portName = 'myUniquePortName';

ReceivePort receiver = ReceivePort();
  IsolateNameServer.registerPortWithName(receiver.sendPort, portName);

  receiver.listen((message) async {
    if (message == "stop") {
      await FlutterRingtonePlayer.stop() 
    }
  });

Send a stop message to Isolate in stopAudio method.

Future<void> stopAudio() async {
  IsolateNameServer.lookupPortByName(isolateName)?.send("stop");
  await FlutterRingtonePlayer.stop();
}

Originally answered here: https://github.com/inway/flutter_ringtone_player/issues/7#issuecomment-1012971949

Me too I had this issue with android alarm manager and audio player but I fixed it like this:

    stopTheSound() async {
    await AndroidAlarmManager.oneShot(Duration(seconds: 0), 4, stopSound,
        exact: true, wakeup: true, alarmClock: true, allowWhileIdle: true);
  }
  
  static stopSound() async {
    if (MyApp.isPlaying) {
      var res = await audioPlugin.stop();
      if (res == 1) {
        MyApp.isPlaying = false;
      }
    }
  }

I used alarm manager to call the sound so I used it again to stop it.

Related