Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Recording to file.** `useDeepgramSpeechToText` now supports `recordToFile`
option to persist microphone audio to a WAV file while simultaneously streaming
to Deepgram. The resulting `file://` URI is delivered via `onRecordingComplete`
callback and `recordingUri` state property once `stopListening` completes.
Recording format is uncompressed WAV (16 kHz PCM16 mono), mirroring the audio
streamed to Deepgram.
- **Audio-level metering.** Added microphone audio-level (RMS amplitude 0..1)
support via `metering` configuration in `useDeepgramSpeechToText`. When enabled,
the native module emits normalized audio levels at configurable intervals,
surfaced through `audioLevel` state and `onAudioLevel` callback. Useful for
implementing visual feedback like waveforms or voice activity indicators.
- **Auto-reconnect for live streaming.** Introduced `DeepgramReconnectOptions` for
automatic reconnection on unexpected socket close in Speech-to-Text and Voice
Agent. Configure max retries, initial/max backoff delays via `reconnect` prop.
Includes `onReconnecting` and `onReconnected` callbacks for monitoring reconnect
attempts.
- **Pause/resume streaming.** `useDeepgramSpeechToText` now exposes `pause()` and
`resume()` methods to temporarily stop forwarding microphone frames without
tearing down the WebSocket. Sends `Finalize` once (v1) to flush buffered audio
and starts periodic `KeepAlive` to maintain the connection during pause. State
tracked via `isPaused` property when `trackState` is enabled.
- **Mute/unmute for Voice Agent.** `useDeepgramVoiceAgent` now supports
`setMuted(boolean)` to control microphone input while maintaining an active
session. Keep-alive messages are sent while muted to prevent timeout.
- **Text-to-Speech `synthesizeToBytes` method.** Added method to fetch synthesized
audio as raw bytes (`ArrayBuffer`) with MIME type without automatically playing
it, enabling audio caching/persistence. Results are cached in-memory (LRU) keyed
by text + format to avoid duplicate network requests for identical prompts.
Accepts optional per-call `options` to override HTTP format.
- **Token-based authentication.** Introduced global `__DEEPGRAM_GET_TOKEN__`
function alongside existing `__DEEPGRAM_API_KEY__` for dynamic token
provisioning. The SDK automatically caches tokens with TTL-aware refresh and
deduplicates concurrent token requests. New `clearCachedAuthToken()` helper
forces fresh token retrieval. See `src/helpers/auth.ts`.
- **Audio device enumeration and selection.** Added `getAudioDevices()` and
`selectAudioDevice(deviceId)` methods for managing audio output devices,
including Bluetooth headsets and built-in options. Event listeners notify of
audio device changes. Requires Bluetooth permissions on Android 12+.
_(Note: audio route management methods were later removed in a refactor.)_
- **Structured error handling.** Introduced `DeepgramError` class with typed
`DeepgramErrorCode` for consistent error handling across all hooks. Native
rejections now use stable error codes (`permission_denied`, `init_failed`,
`start_error`, `stop_error`, `audio_start_error`, `audio_stop_error`,
`stop_player_error`, `invalid_data`, `playback_error`) so consumers can branch
programmatically instead of string-matching messages.
- **Configuration overrides.** New `configure()` function accepts `baseUrl`,
`websocketUrl`, and `agentUrl` overrides for custom endpoints or proxying.
Hooks dynamically resolve URLs based on configuration.
- **Live `measurements` option.** `DeepgramLiveListenOptions` now supports the
`measurements` flag, mirroring the existing prerecorded option. When enabled it
is serialized into the live/streaming query (`measurements=true`) for both the
v1 and v2 (Flux) realtime paths, converting spoken measurements into
abbreviated forms during live transcription.
- **iOS audio session management.** Comprehensive audio session lifecycle
management including activation, deactivation, configuration for recording and
playback, handling for audio route changes, media services resets, and audio
interruptions. Enhanced playback with AVAudioPlayerNode for TTS and Voice Agent
with echo cancellation support when voice processing is enabled.

### Fixed

Expand Down
238 changes: 231 additions & 7 deletions README.md

Large diffs are not rendered by default.

125 changes: 125 additions & 0 deletions android/src/main/java/com/deepgram/AudioFocusManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.deepgram

import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.os.Build
import android.os.Handler
import android.util.Log

/**
* Owns system audio-focus arbitration for the Deepgram module.
*
* Focus is requested with `AUDIOFOCUS_GAIN_TRANSIENT` so other audio apps
* (Spotify, podcasts, navigation, etc.) pause/duck and then automatically
* resume when we abandon focus. Focus-change events are forwarded to the owner
* through [Listener] so it can stop/pause the right audio components.
*/
internal class AudioFocusManager(
private val context: Context,
private val handler: Handler,
private val listener: Listener,
) {
/** Routes system focus transitions back to the owning module. */
interface Listener {
/** Permanent loss (another app took focus): tear everything down. */
fun onFocusLostPermanently()

/** Transient loss (e.g. incoming call): pause playback but keep state. */
fun onFocusLostTransiently()

/** Focus regained: resume playback if it was active. */
fun onFocusGained()
}

@Volatile
var hasFocus: Boolean = false
private set

private var audioManager: AudioManager? = null
private var audioFocusRequest: AudioFocusRequest? = null

private val focusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
when (focusChange) {
AudioManager.AUDIOFOCUS_LOSS -> {
Log.i(TAG, "AUDIOFOCUS_LOSS — stopping Deepgram audio")
hasFocus = false
listener.onFocusLostPermanently()
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT,
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
Log.i(TAG, "AUDIOFOCUS_LOSS_TRANSIENT — pausing playback")
listener.onFocusLostTransiently()
}
AudioManager.AUDIOFOCUS_GAIN -> {
Log.i(TAG, "AUDIOFOCUS_GAIN — resuming playback if active")
hasFocus = true
listener.onFocusGained()
}
}
}

fun requestFocus(): Boolean {
if (hasFocus) return true
try {
val manager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
?: return false
audioManager = manager

val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build()
)
.setOnAudioFocusChangeListener(focusChangeListener, handler)
.setWillPauseWhenDucked(true)
.build()
audioFocusRequest = request
manager.requestAudioFocus(request)
} else {
@Suppress("DEPRECATION")
manager.requestAudioFocus(
focusChangeListener,
AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
)
}

hasFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
if (!hasFocus) {
Log.w(TAG, "Audio focus request not granted (result=$result)")
}
return hasFocus
} catch (e: Exception) {
Log.w(TAG, "requestAudioFocus error", e)
hasFocus = false
return false
}
}

fun abandonFocus() {
if (!hasFocus) return
try {
val manager = audioManager ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
audioFocusRequest?.let { manager.abandonAudioFocusRequest(it) }
} else {
@Suppress("DEPRECATION")
manager.abandonAudioFocus(focusChangeListener)
}
} catch (e: Exception) {
Log.w(TAG, "abandonAudioFocus error", e)
} finally {
hasFocus = false
audioFocusRequest = null
}
}

companion object {
private const val TAG = "DeepgramFocus"
}
}
Loading
Loading