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
57 changes: 46 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,18 @@

1. [Features](#features)
2. [Installation](#installation)
3. [Configuration](#configuration)
4. [Usage overview](#usage-overview)
5. [Voice Agent](#voice-agent-usedeepgramvoiceagent)
6. [Speech-to-Text](#speech-to-text-usedeepgramspeechtotext)
7. [Text-to-Speech](#text-to-speech-usedeepgramtexttospeech)
8. [Text Intelligence](#text-intelligence-usedeepgramtextintelligence)
9. [Management API](#management-api-usedeepgrammanagement)
10. [Example app](#example-app)
11. [Roadmap](#roadmap)
12. [Contributing](#contributing)
13. [License](#license)
3. [Expo config plugin](#expo-config-plugin)
4. [Configuration](#configuration)
5. [Usage overview](#usage-overview)
6. [Voice Agent](#voice-agent-usedeepgramvoiceagent)
7. [Speech-to-Text](#speech-to-text-usedeepgramspeechtotext)
8. [Text-to-Speech](#text-to-speech-usedeepgramtexttospeech)
9. [Text Intelligence](#text-intelligence-usedeepgramtextintelligence)
10. [Management API](#management-api-usedeepgrammanagement)
11. [Example app](#example-app)
12. [Roadmap](#roadmap)
13. [Contributing](#contributing)
14. [License](#license)

---

Expand Down Expand Up @@ -77,6 +78,40 @@ npx expo run:ios # or expo run:android

---

## Expo config plugin

The package ships with an Expo config plugin (exported from `app.plugin.js`) that keeps microphone permissions in sync for both
platforms:

- **Android** – automatically adds `android.permission.RECORD_AUDIO` to your manifest if it is missing.
- **iOS** – sets `NSMicrophoneUsageDescription` with the message you provide (or a sensible fallback).

### Options

You can customise the iOS prompt via the `microphonePermission` option:

```js
// app.config.js
module.exports = {
expo: {
plugins: [
[
'react-native-deepgram',
{
microphonePermission:
'Allow $(PRODUCT_NAME) to capture audio for real-time transcription.',
},
],
],
},
};
```

> 🧭 Need the plugin in a bare React Native project? Import it via
> `require('react-native-deepgram/app.plugin.js')` in your config plugin pipeline.

---

## Configuration

```ts
Expand Down
94 changes: 68 additions & 26 deletions android/src/main/java/com/deepgram/DeepgramModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,33 @@ class DeepgramModule(private val reactContext: ReactApplicationContext) :
}
}

@ReactMethod
fun stopRecording(promise: Promise) {
private fun stopRecordingInternal(throwOnError: Boolean = true) {
isRecording = false
recordingThread?.interrupt()
recordingThread = null

try {
isRecording = false
recordingThread?.interrupt()
recordingThread = null
audioRecord?.stop()
} catch (e: Exception) {
Log.w(TAG, "stopRecordingInternal: error stopping AudioRecord", e)
if (throwOnError) throw e
}

try {
audioRecord?.release()
audioRecord = null
releaseAudioEffects()
} catch (e: Exception) {
Log.w(TAG, "stopRecordingInternal: error releasing AudioRecord", e)
if (throwOnError) throw e
}

audioRecord = null
releaseAudioEffects()
}

@ReactMethod
fun stopRecording(promise: Promise) {
try {
stopRecordingInternal()
promise.resolve(null)
} catch (e: Exception) {
Log.e(TAG, "stopRecording error", e)
Expand Down Expand Up @@ -254,12 +271,28 @@ class DeepgramModule(private val reactContext: ReactApplicationContext) :
}
}

@ReactMethod
fun stopAudio(promise: Promise) {
private fun stopAudioInternal(throwOnError: Boolean = true) {
try {
audioTrack?.stop()
} catch (e: Exception) {
Log.w(TAG, "stopAudioInternal: error stopping AudioTrack", e)
if (throwOnError) throw e
}

try {
audioTrack?.release()
audioTrack = null
} catch (e: Exception) {
Log.w(TAG, "stopAudioInternal: error releasing AudioTrack", e)
if (throwOnError) throw e
}

audioTrack = null
}

@ReactMethod
fun stopAudio(promise: Promise) {
try {
stopAudioInternal()
promise.resolve(null)
} catch (e: Exception) {
Log.e(TAG, "stopAudio error", e)
Expand All @@ -282,8 +315,6 @@ class DeepgramModule(private val reactContext: ReactApplicationContext) :
audioBuffer = ByteArray(0)
}
isPlaying = false

Log.d(TAG, "Audio player initialized: ${sampleRate}Hz, ${channels} channels")
}

@ReactMethod
Expand All @@ -297,8 +328,6 @@ class DeepgramModule(private val reactContext: ReactApplicationContext) :
val audioData = Base64.decode(base64Audio, Base64.DEFAULT)
if (audioData.isEmpty()) return

Log.d(TAG, "Feeding ${audioData.size} bytes of audio for streaming")

// Add audio data to buffer
synchronized(bufferLock) {
audioBuffer = audioBuffer + audioData
Expand Down Expand Up @@ -343,7 +372,6 @@ class DeepgramModule(private val reactContext: ReactApplicationContext) :
}

if (dataToPlay.isNotEmpty()) {
Log.d(TAG, "Playing accumulated audio: ${dataToPlay.size} bytes")
val written = audioTrack?.write(dataToPlay, 0, dataToPlay.size) ?: 0

if (written < 0) {
Expand All @@ -360,47 +388,61 @@ class DeepgramModule(private val reactContext: ReactApplicationContext) :
Log.e(TAG, "Streaming playback error", e)
} finally {
isPlaying = false
Log.d(TAG, "Streaming playback finished")
}
}

playbackThread?.start()
}

private fun stopStreamingPlayback() {
private fun stopStreamingPlayback(throwOnError: Boolean = true) {
isPlaying = false
playbackThread?.interrupt()
playbackThread = null

audioTrack?.stop()
audioTrack?.release()
audioTrack = null

synchronized(bufferLock) {
audioBuffer = ByteArray(0)

try {
stopAudioInternal(throwOnError)
} catch (e: Exception) {
if (throwOnError) throw e else Log.w(TAG, "stopStreamingPlayback: error stopping audio", e)
} finally {
synchronized(bufferLock) {
audioBuffer = ByteArray(0)
}
}
}

@ReactMethod
fun stopPlayer(promise: Promise?) {
try {
stopStreamingPlayback()
Log.d(TAG, "Audio player stopped and cleaned up")
promise?.resolve(null)
} catch (e: Exception) {
Log.e(TAG, "stopPlayer error", e)
promise?.reject("stop_player_error", e)
}
}

override fun onCatalystInstanceDestroy() {
try {
stopRecordingInternal(false)
} catch (e: Exception) {
Log.w(TAG, "Error stopping recording on catalyst destroy", e)
}

try {
stopStreamingPlayback(false)
} catch (e: Exception) {
Log.w(TAG, "Error stopping playback on catalyst destroy", e)
}
super.onCatalystInstanceDestroy()
}

// -------------------------------------------------------------------
// One-shot playback (for HTTP synthesis)
// -------------------------------------------------------------------

@ReactMethod
fun playAudioChunk(chunk: String, promise: Promise) {
try {
Log.d(TAG, "Playing single audio chunk")

val audioData = Base64.decode(chunk, Base64.DEFAULT)
if (audioData.isEmpty()) {
Expand Down
10 changes: 5 additions & 5 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
PODS:
- boost (1.84.0)
- Deepgram (1.0.3):
- Deepgram (1.0.5):
- DoubleConversion
- glog
- hermes-engine
Expand Down Expand Up @@ -1798,9 +1798,9 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"

SPEC CHECKSUMS:
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
Deepgram: 02ac76cdb664bbcc8757144cc34abe8c7c12d089
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
boost: 1dca942403ed9342f98334bf4c3621f011aa7946
Deepgram: 234b0f4e42380dc2129dbc9d10533eb4019b5df0
DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
EXConstants: fcfc75800824ac2d5c592b5bc74130bad17b146b
Expo: 1687edb10c76b0c0f135306d6ae245379f50ed54
ExpoAsset: 48386d40d53a8c1738929b3ed509bcad595b5516
Expand All @@ -1811,7 +1811,7 @@ SPEC CHECKSUMS:
ExpoModulesCore: 98297c2cc7977c43740a2e52d850d94ac8dbf176
FBLazyVector: be509404b5de73a64a74284edcaf73a5d1e128b1
fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a
hermes-engine: 1949ca944b195a8bde7cbf6316b9068e19cf53c6
RCT-Folly: 84578c8756030547307e4572ab1947de1685c599
RCTDeprecation: 063fc281b30b7dc944c98fe53a7e266dab1a8706
Expand Down
26 changes: 20 additions & 6 deletions example/ios/ReactNativeDeepgramExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand All @@ -29,7 +29,7 @@
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-ReactNativeDeepgramExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeDeepgramExample.a"; sourceTree = BUILT_PRODUCTS_DIR; };
6C2E3173556A471DD304B334 /* Pods-ReactNativeDeepgramExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeDeepgramExample.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeDeepgramExample/Pods-ReactNativeDeepgramExample.debug.xcconfig"; sourceTree = "<group>"; };
7A4D352CD337FB3A3BF06240 /* Pods-ReactNativeDeepgramExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeDeepgramExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeDeepgramExample/Pods-ReactNativeDeepgramExample.release.xcconfig"; sourceTree = "<group>"; };
9518637219D6E071AB8BDAB0 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = ReactNativeDeepgramExample/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
9518637219D6E071AB8BDAB0 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ReactNativeDeepgramExample/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = ReactNativeDeepgramExample/SplashScreen.storyboard; sourceTree = "<group>"; };
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
C0773B8BEBC647858A0F7456 /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "ReactNativeDeepgramExample/noop-file.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -341,14 +341,18 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = ReactNativeDeepgramExample/ReactNativeDeepgramExample.entitlements;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ZG2CA7WRPY;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"FB_SONARKIT_ENABLED=1",
);
INFOPLIST_FILE = ReactNativeDeepgramExample/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"$(inherited)",
Expand All @@ -374,9 +378,13 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = ReactNativeDeepgramExample/ReactNativeDeepgramExample.entitlements;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ZG2CA7WRPY;
INFOPLIST_FILE = ReactNativeDeepgramExample/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"$(inherited)",
Expand Down Expand Up @@ -441,7 +449,10 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift,
"$(inherited)",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
Expand Down Expand Up @@ -497,7 +508,10 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift,
"$(inherited)",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_LDFLAGS = (
Expand Down
Loading
Loading