-
-
Notifications
You must be signed in to change notification settings - Fork 321
[iOS] Calling speak() after stop() does not work – AVSpeechSynthesizer never speaks again until app restart #627
Description
Environment
- flutter_tts version: 4.2.5 (or your version)
- Platform: iOS (device/simulator, e.g. iOS 17)
- Flutter: (e.g. 3.24.x)
Summary
On iOS, after calling stop() (or pause() then stop()), any subsequent call to speak() has no effect: no sound, no error. TTS only works again after killing and reopening the app.
Root cause (from debugging)
The plugin uses a single AVSpeechSynthesizer instance (in SwiftFlutterTtsPlugin.swift: let synthesizer = AVSpeechSynthesizer()). On iOS, after stopSpeaking(at: .immediate), this synthesizer can end up in a state where it no longer accepts speak(utterance) – a known limitation / bug with AVSpeechSynthesizer in some iOS versions. Creating a new FlutterTts() in Dart does not help because all Dart instances share the same MethodChannel('flutter_tts') and thus the same native synthesizer.
Steps to reproduce
- Create a screen with TTS: play, then stop, then play again.
- Call
await flutterTts.speak("First sentence");, wait or let it play. - Call
await flutterTts.stop();(or pause then stop). - Call
await flutterTts.speak("Second sentence");. - Expected: Second sentence is spoken.
Actual: No sound. TTS only works again after restarting the app.
Suggested fix
On the iOS side, allow recreating the synthesizer so the next speak() uses a fresh instance:
- Change
synthesizerfromlettovarinSwiftFlutterTtsPlugin.swift. - Add a new method (e.g.
resetSynthesizer) that does:self.synthesizer = AVSpeechSynthesizer()self.synthesizer.delegate = self
- Expose it via the method channel (e.g. a new method
resetSynthesizer) so that apps can call it before the nextspeak()when they have just calledstop().
Alternatively, the plugin could call this reset internally after each stop() on iOS, so that the next speak() always uses a new synthesizer.
Workaround (for maintainers / other users)
Until the plugin supports this, apps can patch the plugin’s iOS Swift file (e.g. with a post–pub get script) to add the above logic and call the new method from Dart via MethodChannel('flutter_tts').invokeMethod('resetSynthesizer') before each speak() on iOS. This workaround has been tested and fixes the issue.
References
- Similar issues: .stop() doesn't work on iOS 15 and above #298 (stop doesn’t work on iOS 15+), Calling
speak()after callingstop()does not work #364 (speak after stop doesn’t work). - Apple:
AVSpeechSynthesizerbehavior afterstopSpeaking(at:)can leave the instance in a state where it does not speak again.