| .. | .. |
|---|
| 28 | 28 | // Autoplay suppression |
|---|
| 29 | 29 | static bool _isBackgrounded = false; |
|---|
| 30 | 30 | |
|---|
| 31 | + // Track last played temp file so it can be cleaned up when the track ends |
|---|
| 32 | + static String? _lastPlaybackTempPath; |
|---|
| 33 | + |
|---|
| 34 | + // Lifecycle observer stored so we can remove it in dispose() |
|---|
| 35 | + static _LifecycleObserver? _lifecycleObserver; |
|---|
| 36 | + |
|---|
| 31 | 37 | /// Initialize the audio service and set up lifecycle observer. |
|---|
| 32 | 38 | static void init() { |
|---|
| 33 | | - WidgetsBinding.instance.addObserver(_LifecycleObserver()); |
|---|
| 39 | + _lifecycleObserver = _LifecycleObserver(); |
|---|
| 40 | + WidgetsBinding.instance.addObserver(_lifecycleObserver!); |
|---|
| 34 | 41 | |
|---|
| 35 | 42 | // Configure audio session for background playback |
|---|
| 36 | 43 | _player.setAudioContext(AudioContext( |
|---|
| .. | .. |
|---|
| 52 | 59 | } |
|---|
| 53 | 60 | |
|---|
| 54 | 61 | static void _onTrackComplete() { |
|---|
| 62 | + // Clean up the temp file that just finished playing |
|---|
| 63 | + final prev = _lastPlaybackTempPath; |
|---|
| 64 | + _lastPlaybackTempPath = null; |
|---|
| 65 | + if (prev != null) { |
|---|
| 66 | + File(prev).delete().ignore(); |
|---|
| 67 | + } |
|---|
| 68 | + |
|---|
| 55 | 69 | if (_queue.isNotEmpty) { |
|---|
| 56 | 70 | _playNextInQueue(); |
|---|
| 57 | 71 | } else { |
|---|
| .. | .. |
|---|
| 68 | 82 | } |
|---|
| 69 | 83 | |
|---|
| 70 | 84 | final path = _queue.removeAt(0); |
|---|
| 85 | + _lastPlaybackTempPath = path; |
|---|
| 71 | 86 | try { |
|---|
| 72 | 87 | // Brief pause between tracks — iOS audio player needs time to reset |
|---|
| 73 | 88 | await _player.stop(); |
|---|
| .. | .. |
|---|
| 143 | 158 | |
|---|
| 144 | 159 | if (source.startsWith('/')) { |
|---|
| 145 | 160 | await _player.play(DeviceFileSource(source)); |
|---|
| 161 | + // File path owned by caller — not tracked for deletion |
|---|
| 146 | 162 | } else { |
|---|
| 147 | 163 | // base64 data — write to temp file first |
|---|
| 148 | 164 | final path = await _base64ToFile(source); |
|---|
| 149 | 165 | if (path == null) return; |
|---|
| 166 | + _lastPlaybackTempPath = path; |
|---|
| 150 | 167 | await _player.play(DeviceFileSource(path)); |
|---|
| 151 | 168 | } |
|---|
| 152 | 169 | _isPlaying = true; |
|---|
| .. | .. |
|---|
| 159 | 176 | final path = await _base64ToFile(base64Audio); |
|---|
| 160 | 177 | if (path == null) return; |
|---|
| 161 | 178 | |
|---|
| 179 | + _lastPlaybackTempPath = path; |
|---|
| 162 | 180 | await _player.play(DeviceFileSource(path)); |
|---|
| 163 | 181 | _isPlaying = true; |
|---|
| 164 | 182 | onPlaybackStateChanged?.call(); |
|---|
| .. | .. |
|---|
| 177 | 195 | debugPrint('AudioService: queued (queue size: ${_queue.length})'); |
|---|
| 178 | 196 | } else { |
|---|
| 179 | 197 | // Nothing playing — start immediately |
|---|
| 198 | + _lastPlaybackTempPath = path; |
|---|
| 180 | 199 | try { |
|---|
| 181 | 200 | await _player.play(DeviceFileSource(path)); |
|---|
| 182 | 201 | _isPlaying = true; |
|---|
| .. | .. |
|---|
| 250 | 269 | } |
|---|
| 251 | 270 | |
|---|
| 252 | 271 | static Future<void> dispose() async { |
|---|
| 272 | + if (_lifecycleObserver != null) { |
|---|
| 273 | + WidgetsBinding.instance.removeObserver(_lifecycleObserver!); |
|---|
| 274 | + _lifecycleObserver = null; |
|---|
| 275 | + } |
|---|
| 253 | 276 | await cancelRecording(); |
|---|
| 254 | 277 | await stopPlayback(); |
|---|
| 255 | 278 | _recorder.dispose(); |
|---|