Skip to content

Commit 1e94582

Browse files
add flushAudio
1 parent 4e8c11a commit 1e94582

File tree

7 files changed

+157
-1
lines changed

7 files changed

+157
-1
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,37 @@ subscription?.remove();
172172
playbackSubscription?.remove();
173173
```
174174

175+
### Stopping Audio Playback
176+
177+
```typescript
178+
import { ExpoStreamer } from 'expo-streamer';
179+
180+
// Graceful stop - allows buffered audio to finish
181+
await ExpoStreamer.stopAudio();
182+
183+
// Immediate flush - clears buffer and stops mid-stream
184+
await ExpoStreamer.flushAudio();
185+
```
186+
187+
**When to use `stopAudio()` vs `flushAudio()`:**
188+
189+
- **`stopAudio()`**: Use when you want to gracefully stop playback, allowing any buffered audio to finish playing. Good for natural conversation endings.
190+
191+
- **`flushAudio()`**: Use when you need to immediately stop all audio output, such as when the user interrupts or cancels playback. This clears all scheduled audio buffers without waiting for them to drain.
192+
193+
```typescript
194+
// Example: User interruption handling
195+
async function handleUserInterrupt() {
196+
// Immediately stop all audio
197+
await ExpoStreamer.flushAudio();
198+
199+
// Clear the turn queue
200+
await ExpoStreamer.clearSoundQueueByTurnId(currentTurnId);
201+
202+
console.log('Audio interrupted and flushed');
203+
}
204+
```
205+
175206
## 📋 API Reference
176207

177208
### Core Types
@@ -243,6 +274,7 @@ const EncodingTypes = {
243274
| `playAudio(data: string, turnId: string, encoding?: Encoding)` | `Promise<void>` | Play base64 audio data |
244275
| `pauseAudio()` | `Promise<void>` | Pause current playback |
245276
| `stopAudio()` | `Promise<void>` | Stop all audio playback |
277+
| `flushAudio()` | `Promise<void>` | Immediately flush audio buffer and stop mid-stream |
246278
| `clearPlaybackQueueByTurnId(turnId: string)` | `Promise<void>` | Clear queue for specific turn |
247279

248280
### Configuration Methods

android/src/main/java/expo/modules/audiostream/AudioPlaybackManager.kt

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,66 @@ class AudioPlaybackManager(private val eventSender: EventSender? = null) {
287287
}
288288
}
289289

290+
fun flushAudio(promise: Promise? = null) {
291+
Log.d("ExpoPlayStreamModule", "Flushing audio buffer")
292+
isPlaying = false
293+
coroutineScope.launch {
294+
try {
295+
// Immediately stop and flush audio track
296+
Log.d("ExpoPlayStreamModule", "Flushing audioTrack")
297+
if (::audioTrack.isInitialized && audioTrack.state != AudioTrack.STATE_UNINITIALIZED) {
298+
try {
299+
audioTrack.pause()
300+
audioTrack.flush()
301+
audioTrack.stop()
302+
} catch (e: Exception) {
303+
Log.e("ExpoPlayStreamModule", "Error flushing AudioTrack: ${e.message}", e)
304+
}
305+
}
306+
307+
// Cancel jobs immediately
308+
if (currentPlaybackJob != null) {
309+
Log.d("ExpoPlayStreamModule", "Cancelling currentPlaybackJob")
310+
currentPlaybackJob?.cancelAndJoin()
311+
currentPlaybackJob = null
312+
}
313+
314+
if (processingJob != null) {
315+
Log.d("ExpoPlayStreamModule", "Cancelling processingJob")
316+
processingJob?.cancelAndJoin()
317+
processingJob = null
318+
}
319+
320+
// Clear all pending chunks
321+
Log.d("ExpoPlayStreamModule", "Clearing all pending chunks")
322+
for (chunk in playbackChannel) {
323+
if (!chunk.isPromiseSettled) {
324+
chunk.isPromiseSettled = true
325+
chunk.promise.resolve(null)
326+
}
327+
}
328+
329+
// Close channels
330+
if (!processingChannel.isClosedForSend) {
331+
processingChannel.close()
332+
}
333+
if (!playbackChannel.isClosedForSend) {
334+
playbackChannel.close()
335+
}
336+
337+
// Reset state
338+
hasSentSoundStartedEvent = false
339+
segmentsLeftToPlay = 0
340+
341+
Log.d("ExpoPlayStreamModule", "Audio buffer flushed")
342+
promise?.resolve(null)
343+
} catch (e: Exception) {
344+
Log.e("ExpoPlayStreamModule", "Error in flushAudio: ${e.message}", e)
345+
promise?.reject("ERR_FLUSH_AUDIO", e.message, e)
346+
}
347+
}
348+
}
349+
290350
fun stopPlayback(promise: Promise? = null) {
291351
Log.d("ExpoPlayStreamModule", "Stopping playback")
292352
if (!isPlaying || playbackChannel.isEmpty ) {

android/src/main/java/expo/modules/audiostream/ExpoPlayAudioStreamModule.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ class ExpoPlayAudioStreamModule : Module(), EventSender {
179179

180180
AsyncFunction("stopAudio") { promise: Promise -> audioPlaybackManager.stopPlayback(promise) }
181181

182+
AsyncFunction("flushAudio") { promise: Promise -> audioPlaybackManager.flushAudio(promise) }
183+
182184
AsyncFunction("clearAudioFiles") { promise: Promise ->
183185
audioRecorderManager.clearAudioStorage(promise)
184186
}

ios/ExpoPlayAudioStreamModule.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,14 @@ public class ExpoPlayAudioStreamModule: Module, AudioStreamManagerDelegate, Micr
210210
}
211211
}
212212

213+
AsyncFunction("flushAudio") { (promise: Promise) in
214+
if case .success(let soundPlayer) = componentManager.getSoundPlayer() {
215+
soundPlayer.flushAudio(promise)
216+
} else {
217+
promise.reject("SOUND_PLAYER_UNAVAILABLE", "Sound player is not available")
218+
}
219+
}
220+
213221
AsyncFunction("listAudioFiles") { (promise: Promise) in
214222
let result = listAudioFiles()
215223
promise.resolve(result)

ios/SoundPlayer.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,45 @@ class SoundPlayer: SoundPlayerManaging {
371371
promise.resolve(nil)
372372
}
373373

374+
/// Immediately flushes the audio buffer and stops playback mid-stream
375+
/// - Parameter promise: Promise to resolve when flushed
376+
func flushAudio(_ promise: Promise) {
377+
Logger.debug("[SoundPlayer] Flushing Audio Buffer")
378+
379+
// Thread-safe queue clearing
380+
bufferAccessQueue.sync {
381+
if !self.audioQueue.isEmpty {
382+
Logger.debug("[SoundPlayer] Queue is not empty, clearing \(self.audioQueue.count) items")
383+
self.audioQueue.removeAll()
384+
}
385+
}
386+
387+
// Immediately stop the audio player node and reset it
388+
if let playerNode = self.audioPlayerNode {
389+
if playerNode.isPlaying {
390+
Logger.debug("[SoundPlayer] Player is playing, stopping and resetting")
391+
playerNode.stop()
392+
// Reset the player node to clear any scheduled buffers
393+
playerNode.reset()
394+
}
395+
}
396+
397+
// Stop the engine and disable voice processing if in voice processing mode
398+
if config.playbackMode == .voiceProcessing {
399+
if let engine = self.audioEngine, engine.isRunning {
400+
engine.stop()
401+
try? self.disableVoiceProcessing()
402+
self.isAudioEngineIsSetup = false
403+
}
404+
}
405+
406+
self.segmentsLeftToPlay = 0
407+
self.isPlaying = false
408+
409+
Logger.debug("[SoundPlayer] Audio buffer flushed")
410+
promise.resolve(nil)
411+
}
412+
374413
/// Interrupts audio playback
375414
/// - Parameter promise: Promise to resolve when interrupted
376415
func interrupt(_ promise: Promise) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "expo-streamer",
3-
"version": "1.0.7",
3+
"version": "1.1.0",
44
"description": "Realtime Audio Streaming for Expo",
55
"main": "build/index.js",
66
"types": "build/index.d.ts",

src/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,21 @@ export class ExpoPlayAudioStream {
199199
}
200200
}
201201

202+
/**
203+
* Immediately flushes the audio buffer and stops playback mid-stream.
204+
* Unlike stopAudio(), this method doesn't wait for the buffer to drain.
205+
* @returns {Promise<void>}
206+
* @throws {Error} If the audio buffer fails to flush.
207+
*/
208+
static async flushAudio(): Promise<void> {
209+
try {
210+
return await ExpoPlayAudioStreamModule.flushAudio();
211+
} catch (error) {
212+
console.error(error);
213+
throw new Error(`Failed to flush audio: ${error}`);
214+
}
215+
}
216+
202217
/**
203218
* Clears the playback queue by turn ID.
204219
* @param {string} turnId - The turn ID.

0 commit comments

Comments
 (0)