@clarionhq/recorder 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/android/CMakeLists.txt +9 -0
- package/android/build.gradle +97 -0
- package/android/gradle.properties +4 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/cpp/cpp-adapter.cpp +7 -0
- package/android/src/main/java/com/clarionhq/recorder/AudioLevelMeter.kt +38 -0
- package/android/src/main/java/com/clarionhq/recorder/ClarionRecorderPackage.kt +27 -0
- package/android/src/main/java/com/clarionhq/recorder/EncodeFile.kt +62 -0
- package/android/src/main/java/com/clarionhq/recorder/RecorderConfig.kt +33 -0
- package/android/src/main/java/com/clarionhq/recorder/RecorderConstants.kt +16 -0
- package/android/src/main/java/com/clarionhq/recorder/RecorderSession.kt +336 -0
- package/android/src/main/java/com/clarionhq/recorder/RecorderTypes.kt +29 -0
- package/android/src/main/java/com/margelo/nitro/clarion/recorder/HybridClarionRecorder.kt +174 -0
- package/clarionhq-recorder.podspec +31 -0
- package/ios/AudioLevelMeter.swift +37 -0
- package/ios/EncodeFile.swift +69 -0
- package/ios/HybridClarionRecorder.swift +186 -0
- package/ios/RecorderConstants.swift +11 -0
- package/ios/RecorderSession.swift +278 -0
- package/ios/RecorderTypes.swift +41 -0
- package/lib/RecorderEngine.d.ts +31 -0
- package/lib/RecorderEngine.d.ts.map +1 -0
- package/lib/RecorderEngine.js +245 -0
- package/lib/RecorderEngine.js.map +1 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -0
- package/lib/native.d.ts +3 -0
- package/lib/native.d.ts.map +1 -0
- package/lib/native.js +3 -0
- package/lib/native.js.map +1 -0
- package/lib/specs/ClarionRecorder.nitro.d.ts +49 -0
- package/lib/specs/ClarionRecorder.nitro.d.ts.map +1 -0
- package/lib/specs/ClarionRecorder.nitro.js +2 -0
- package/lib/specs/ClarionRecorder.nitro.js.map +1 -0
- package/nitro.json +24 -0
- package/nitrogen/generated/android/ClarionRecorder+autolinking.cmake +81 -0
- package/nitrogen/generated/android/ClarionRecorder+autolinking.gradle +27 -0
- package/nitrogen/generated/android/ClarionRecorderOnLoad.cpp +62 -0
- package/nitrogen/generated/android/ClarionRecorderOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JFunc_void_NativeRecorderError.hpp +78 -0
- package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string_double_double_double.hpp +76 -0
- package/nitrogen/generated/android/c++/JHybridClarionRecorderSpec.cpp +207 -0
- package/nitrogen/generated/android/c++/JHybridClarionRecorderSpec.hpp +75 -0
- package/nitrogen/generated/android/c++/JNativeRecorderConfig.hpp +90 -0
- package/nitrogen/generated/android/c++/JNativeRecorderError.hpp +65 -0
- package/nitrogen/generated/android/c++/JNativeRecorderResult.hpp +77 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/clarion/recorder/ClarionRecorderOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/clarion/recorder/Func_void_NativeRecorderError.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/clarion/recorder/Func_void_double_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/clarion/recorder/Func_void_std__string.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/clarion/recorder/Func_void_std__string_double_double_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/clarion/recorder/HybridClarionRecorderSpec.kt +125 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/clarion/recorder/NativeRecorderConfig.kt +91 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/clarion/recorder/NativeRecorderError.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/clarion/recorder/NativeRecorderResult.kt +76 -0
- package/nitrogen/generated/ios/ClarionRecorder+autolinking.rb +62 -0
- package/nitrogen/generated/ios/ClarionRecorder-Swift-Cxx-Bridge.cpp +89 -0
- package/nitrogen/generated/ios/ClarionRecorder-Swift-Cxx-Bridge.hpp +297 -0
- package/nitrogen/generated/ios/ClarionRecorder-Swift-Cxx-Umbrella.hpp +56 -0
- package/nitrogen/generated/ios/ClarionRecorderAutolinking.mm +33 -0
- package/nitrogen/generated/ios/ClarionRecorderAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridClarionRecorderSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridClarionRecorderSpecSwift.hpp +188 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_NativeRecorderError.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_NativeRecorderResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string_double_double_double.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridClarionRecorderSpec.swift +67 -0
- package/nitrogen/generated/ios/swift/HybridClarionRecorderSpec_cxx.swift +354 -0
- package/nitrogen/generated/ios/swift/NativeRecorderConfig.swift +108 -0
- package/nitrogen/generated/ios/swift/NativeRecorderError.swift +39 -0
- package/nitrogen/generated/ios/swift/NativeRecorderResult.swift +54 -0
- package/nitrogen/generated/shared/c++/HybridClarionRecorderSpec.cpp +34 -0
- package/nitrogen/generated/shared/c++/HybridClarionRecorderSpec.hpp +84 -0
- package/nitrogen/generated/shared/c++/NativeRecorderConfig.hpp +116 -0
- package/nitrogen/generated/shared/c++/NativeRecorderError.hpp +91 -0
- package/nitrogen/generated/shared/c++/NativeRecorderResult.hpp +103 -0
- package/package.json +68 -8
- package/react-native.config.js +10 -0
- package/src/RecorderEngine.ts +298 -0
- package/src/index.ts +8 -0
- package/src/native.ts +5 -0
- package/src/specs/ClarionRecorder.nitro.ts +58 -0
- package/index.js +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Clarion
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# @clarionhq/recorder
|
|
2
|
+
|
|
3
|
+
React Native microphone capture → AAC `.m4a`. Built on the New Architecture with [Nitro Modules](https://nitro.margelo.com). 16 KB page-size compliant.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { RecorderEngine } from '@clarionhq/recorder';
|
|
7
|
+
|
|
8
|
+
const engine = new RecorderEngine({ emitAudioLevel: true });
|
|
9
|
+
engine.on(e => {
|
|
10
|
+
if (e.type === 'state') setState(e.state);
|
|
11
|
+
if (e.type === 'audio-level') setRms(e.rms);
|
|
12
|
+
if (e.type === 'recording-complete') console.log(e.result.uri);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
await engine.start(); // permission, prepare and record — one call
|
|
16
|
+
// later…
|
|
17
|
+
await engine.stop(); // finalizes the m4a and resets to `idle`
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
pnpm add @clarionhq/recorder @clarionhq/core react-native-nitro-modules
|
|
24
|
+
cd ios && pod install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
| | |
|
|
30
|
+
|---|---|
|
|
31
|
+
| React Native | 0.77+ with New Architecture + Hermes |
|
|
32
|
+
| Android | API 26+ |
|
|
33
|
+
| iOS | 15.1+ |
|
|
34
|
+
| Expo | bare workflow only (prebuild) |
|
|
35
|
+
|
|
36
|
+
## Permissions
|
|
37
|
+
|
|
38
|
+
**Android** — `RECORD_AUDIO` is auto-merged into your manifest. Request at runtime:
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.RECORD_AUDIO);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**iOS** — add `NSMicrophoneUsageDescription` to `Info.plist`. The library prompts the user automatically on first `start()`.
|
|
45
|
+
|
|
46
|
+
## API
|
|
47
|
+
|
|
48
|
+
### `new RecorderEngine(options?)`
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
type RecorderEngineOptions = {
|
|
52
|
+
outputDirectory?: string; // default: app cache dir / 'clarion-recorder'
|
|
53
|
+
filenamePrefix?: string; // default: 'clarion'
|
|
54
|
+
rotateAfterMs?: number; // split into chunks every N ms
|
|
55
|
+
emitAudioLevel?: boolean; // default: false
|
|
56
|
+
audioLevelIntervalMs?: number; // default: 50
|
|
57
|
+
aacBitrate?: number; // default: 32_000
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Lifecycle
|
|
62
|
+
|
|
63
|
+
| Method | Behavior |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `start()` | Begin recording. Auto-handles permission + `prepare()` from `idle`/`error`. |
|
|
66
|
+
| `pause()` / `resume()` | Gapless pause — audio session stays warm. |
|
|
67
|
+
| `stop()` | Finalize the m4a → fire `recording-complete` → state → `idle`. |
|
|
68
|
+
| `discard()` | Abort and delete the partial file → state → `idle`. |
|
|
69
|
+
| `release()` | Permanently dispose. Engine cannot be reused after this. |
|
|
70
|
+
| `prepare()` | Optional. Pre-warm the audio session before showing your record UI. |
|
|
71
|
+
|
|
72
|
+
### Events
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
type ClarionEvent =
|
|
76
|
+
| { type: 'state'; state: EngineState }
|
|
77
|
+
| { type: 'audio-level'; rms: number; peak: number } // 0..1 linear (not dB)
|
|
78
|
+
| { type: 'chunk'; uri: string; startMs: number; endMs: number; sizeBytes: number }
|
|
79
|
+
| { type: 'recording-complete'; result: RecorderResult }
|
|
80
|
+
| { type: 'error'; error: ClarionError };
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
States: `idle → preparing → ready → starting → recording → paused → stopping → idle`, plus `error` and `released` as terminals. Method calls from invalid states throw `ClarionError({ code: 'INVALID_STATE' })`; runtime errors arrive via the `error` event. See [`@clarionhq/core/errors`](https://github.com/Th4nderG0d/clarion/blob/main/packages/core/src/errors.ts) for the full code list.
|
|
84
|
+
|
|
85
|
+
### React cleanup
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const engine = new RecorderEngine();
|
|
90
|
+
const off = engine.on(handle);
|
|
91
|
+
return () => { off(); engine.release(); }; // important — frees the mic
|
|
92
|
+
}, []);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Limitations
|
|
96
|
+
|
|
97
|
+
- **No background recording.** App suspending mid-recording will truncate the file. Callers who need background must configure `UIBackgroundModes: audio` (iOS) + a foreground service (Android) themselves; opt-in support may land in v0.2.
|
|
98
|
+
- **No playback.** Use [`expo-av`](https://docs.expo.dev/versions/latest/sdk/audio/) or [`react-native-track-player`](https://rntp.dev) — Clarion produces m4a files that play cleanly in both.
|
|
99
|
+
- **No managed Expo support.** Bare workflow / prebuild only until a config plugin ships.
|
|
100
|
+
|
|
101
|
+
## Troubleshooting
|
|
102
|
+
|
|
103
|
+
| Symptom | Fix |
|
|
104
|
+
|---|---|
|
|
105
|
+
| iOS build: `fmt` library `consteval` errors on Xcode 26 | Add `FMT_USE_CONSTEVAL=0` patch to your `Podfile` post_install (see `example/ios/Podfile` in the repo). |
|
|
106
|
+
| `PERMISSION_DENIED` fires repeatedly | iOS: confirm `NSMicrophoneUsageDescription` is in Info.plist. Android: confirm `PermissionsAndroid.request(RECORD_AUDIO)` returned `granted` before calling `start()`. |
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
project(ClarionRecorder)
|
|
2
|
+
cmake_minimum_required(VERSION 3.9.0)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 20)
|
|
5
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
6
|
+
|
|
7
|
+
add_library(${PROJECT_NAME} SHARED src/main/cpp/cpp-adapter.cpp)
|
|
8
|
+
|
|
9
|
+
include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/ClarionRecorder+autolinking.cmake)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.getExtOrDefault = { name ->
|
|
3
|
+
return rootProject.ext.has(name)
|
|
4
|
+
? rootProject.ext.get(name)
|
|
5
|
+
: project.properties["ClarionRecorder_" + name]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
repositories {
|
|
9
|
+
google()
|
|
10
|
+
mavenCentral()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
dependencies {
|
|
14
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
15
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
apply plugin: "com.android.library"
|
|
20
|
+
apply plugin: "kotlin-android"
|
|
21
|
+
|
|
22
|
+
apply from: "../nitrogen/generated/android/ClarionRecorder+autolinking.gradle"
|
|
23
|
+
|
|
24
|
+
android {
|
|
25
|
+
namespace "com.clarionhq.recorder"
|
|
26
|
+
|
|
27
|
+
compileSdkVersion getExtOrDefault("compileSdkVersion").toInteger()
|
|
28
|
+
|
|
29
|
+
defaultConfig {
|
|
30
|
+
minSdkVersion getExtOrDefault("minSdkVersion").toInteger()
|
|
31
|
+
targetSdkVersion getExtOrDefault("targetSdkVersion").toInteger()
|
|
32
|
+
consumerProguardFiles "proguard-rules.pro"
|
|
33
|
+
|
|
34
|
+
externalNativeBuild {
|
|
35
|
+
cmake {
|
|
36
|
+
cppFlags "-O2 -frtti -fexceptions -Wall -Wno-unused-variable -fstack-protector-all"
|
|
37
|
+
// -Wl,-z,max-page-size=16384 makes the produced .so 16 KB page-aligned —
|
|
38
|
+
// required by Google Play for new Android 15+ uploads.
|
|
39
|
+
arguments "-DANDROID_STL=c++_shared",
|
|
40
|
+
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
|
|
41
|
+
"-DCMAKE_SHARED_LINKER_FLAGS=-Wl,-z,max-page-size=16384"
|
|
42
|
+
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
externalNativeBuild {
|
|
48
|
+
cmake {
|
|
49
|
+
path "CMakeLists.txt"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
buildFeatures {
|
|
54
|
+
buildConfig false
|
|
55
|
+
prefab true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
compileOptions {
|
|
59
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
60
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
kotlinOptions {
|
|
64
|
+
jvmTarget = "17"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
sourceSets {
|
|
68
|
+
main {
|
|
69
|
+
manifest.srcFile "src/main/AndroidManifest.xml"
|
|
70
|
+
java.srcDirs += [
|
|
71
|
+
"src/main/java",
|
|
72
|
+
"$projectDir/../nitrogen/generated/android/kotlin",
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
packagingOptions {
|
|
78
|
+
excludes = [
|
|
79
|
+
"META-INF/DEPENDENCIES",
|
|
80
|
+
"META-INF/LICENSE",
|
|
81
|
+
"META-INF/LICENSE.txt",
|
|
82
|
+
"META-INF/NOTICE",
|
|
83
|
+
"META-INF/NOTICE.txt"
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
repositories {
|
|
89
|
+
mavenCentral()
|
|
90
|
+
google()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
dependencies {
|
|
94
|
+
implementation "com.facebook.react:react-android"
|
|
95
|
+
implementation project(":react-native-nitro-modules")
|
|
96
|
+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1"
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
-keep class com.clarionhq.recorder.** { *; }
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package com.clarionhq.recorder
|
|
2
|
+
|
|
3
|
+
import kotlin.math.abs
|
|
4
|
+
import kotlin.math.min
|
|
5
|
+
import kotlin.math.sqrt
|
|
6
|
+
|
|
7
|
+
internal data class AudioLevels(val rms: Double, val peak: Double)
|
|
8
|
+
|
|
9
|
+
internal object AudioLevelMeter {
|
|
10
|
+
private const val PCM_16_MAX = 32_768.0
|
|
11
|
+
|
|
12
|
+
fun compute(pcm: ByteArray, length: Int): AudioLevels {
|
|
13
|
+
if (length < 2) return AudioLevels(0.0, 0.0)
|
|
14
|
+
|
|
15
|
+
val sampleCount = length / 2
|
|
16
|
+
var sumSquares = 0.0
|
|
17
|
+
var peakAbs = 0
|
|
18
|
+
|
|
19
|
+
var i = 0
|
|
20
|
+
while (i < length - 1) {
|
|
21
|
+
val low = pcm[i].toInt() and 0xFF
|
|
22
|
+
val high = pcm[i + 1].toInt()
|
|
23
|
+
val sample = (high shl 8) or low
|
|
24
|
+
val signed = if (sample > 32_767) sample - 65_536 else sample
|
|
25
|
+
|
|
26
|
+
val abs = abs(signed)
|
|
27
|
+
if (abs > peakAbs) peakAbs = abs
|
|
28
|
+
|
|
29
|
+
val normalized = signed.toDouble()
|
|
30
|
+
sumSquares += normalized * normalized
|
|
31
|
+
i += 2
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
val rms = sqrt(sumSquares / sampleCount) / PCM_16_MAX
|
|
35
|
+
val peak = peakAbs.toDouble() / PCM_16_MAX
|
|
36
|
+
return AudioLevels(min(rms, 1.0), min(peak, 1.0))
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package com.clarionhq.recorder
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
import com.margelo.nitro.clarion.recorder.ClarionRecorderOnLoad
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Stub ReactPackage so React Native's autolinker discovers this library.
|
|
11
|
+
* Nitro registers the actual HybridObject via JNI inside the C++ library — this
|
|
12
|
+
* class triggers the System.loadLibrary call (idempotent) at startup so the
|
|
13
|
+
* registration runs before any JS code calls `createHybridObject('ClarionRecorder')`.
|
|
14
|
+
*/
|
|
15
|
+
class ClarionRecorderPackage : ReactPackage {
|
|
16
|
+
init {
|
|
17
|
+
ClarionRecorderOnLoad.initializeNative()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override fun createNativeModules(
|
|
21
|
+
reactContext: ReactApplicationContext,
|
|
22
|
+
): List<NativeModule> = emptyList()
|
|
23
|
+
|
|
24
|
+
override fun createViewManagers(
|
|
25
|
+
reactContext: ReactApplicationContext,
|
|
26
|
+
): List<ViewManager<*, *>> = emptyList()
|
|
27
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
package com.clarionhq.recorder
|
|
2
|
+
|
|
3
|
+
import android.media.MediaCodec
|
|
4
|
+
import android.media.MediaCodecInfo
|
|
5
|
+
import android.media.MediaFormat
|
|
6
|
+
import android.media.MediaMuxer
|
|
7
|
+
import java.io.File
|
|
8
|
+
import java.util.UUID
|
|
9
|
+
|
|
10
|
+
internal class EncodeFile private constructor(
|
|
11
|
+
val path: String,
|
|
12
|
+
val codec: MediaCodec,
|
|
13
|
+
val muxer: MediaMuxer,
|
|
14
|
+
) {
|
|
15
|
+
var muxerTrackIndex: Int = -1
|
|
16
|
+
var muxerStarted: Boolean = false
|
|
17
|
+
var bytesWritten: Long = 0L
|
|
18
|
+
|
|
19
|
+
fun close() {
|
|
20
|
+
runCatching { codec.stop() }
|
|
21
|
+
runCatching { codec.release() }
|
|
22
|
+
runCatching { if (muxerStarted) muxer.stop() }
|
|
23
|
+
runCatching { muxer.release() }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
companion object {
|
|
27
|
+
fun open(
|
|
28
|
+
outputDir: File,
|
|
29
|
+
filenamePrefix: String,
|
|
30
|
+
sampleRate: Int,
|
|
31
|
+
channels: Int,
|
|
32
|
+
aacBitrate: Int,
|
|
33
|
+
maxInputSize: Int,
|
|
34
|
+
): EncodeFile {
|
|
35
|
+
if (!outputDir.exists()) outputDir.mkdirs()
|
|
36
|
+
|
|
37
|
+
val file = File(
|
|
38
|
+
outputDir,
|
|
39
|
+
"$filenamePrefix-${UUID.randomUUID()}.${RecorderConstants.OUTPUT_EXTENSION}",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
val format = MediaFormat.createAudioFormat(
|
|
43
|
+
MediaFormat.MIMETYPE_AUDIO_AAC,
|
|
44
|
+
sampleRate,
|
|
45
|
+
channels,
|
|
46
|
+
).apply {
|
|
47
|
+
setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
|
|
48
|
+
setInteger(MediaFormat.KEY_BIT_RATE, aacBitrate)
|
|
49
|
+
setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
val codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC).apply {
|
|
53
|
+
configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
|
54
|
+
start()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
val muxer = MediaMuxer(file.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
|
|
58
|
+
|
|
59
|
+
return EncodeFile(path = file.absolutePath, codec = codec, muxer = muxer)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.clarionhq.recorder
|
|
2
|
+
|
|
3
|
+
internal data class RecorderConfig(
|
|
4
|
+
val sampleRate: Int,
|
|
5
|
+
val channels: Int,
|
|
6
|
+
val bitDepth: Int,
|
|
7
|
+
val outputDirectory: String?,
|
|
8
|
+
val filenamePrefix: String?,
|
|
9
|
+
val rotateAfterMs: Long?,
|
|
10
|
+
val emitAudioLevel: Boolean,
|
|
11
|
+
val audioLevelIntervalMs: Int,
|
|
12
|
+
val aacBitrate: Int,
|
|
13
|
+
) {
|
|
14
|
+
init {
|
|
15
|
+
require(sampleRate in setOf(8_000, 16_000, 22_050, 44_100, 48_000)) {
|
|
16
|
+
"Unsupported sample rate: $sampleRate"
|
|
17
|
+
}
|
|
18
|
+
require(channels in 1..2) { "Channels must be 1 or 2, got $channels" }
|
|
19
|
+
require(bitDepth == 16) { "Only 16-bit PCM is supported (got $bitDepth)" }
|
|
20
|
+
require(aacBitrate in 16_000..256_000) { "AAC bitrate out of range: $aacBitrate" }
|
|
21
|
+
require(audioLevelIntervalMs >= 10) { "audioLevelIntervalMs too low: $audioLevelIntervalMs" }
|
|
22
|
+
rotateAfterMs?.let { require(it >= 1_000) { "rotateAfterMs must be >= 1000ms" } }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
internal data class RecorderResult(
|
|
27
|
+
val uri: String,
|
|
28
|
+
val durationMs: Long,
|
|
29
|
+
val sizeBytes: Long,
|
|
30
|
+
val sampleRate: Int,
|
|
31
|
+
val channels: Int,
|
|
32
|
+
val bitDepth: Int,
|
|
33
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package com.clarionhq.recorder
|
|
2
|
+
|
|
3
|
+
internal object RecorderConstants {
|
|
4
|
+
const val CODEC_DEQUEUE_TIMEOUT_US = 10_000L
|
|
5
|
+
const val PCM_QUEUE_POLL_TIMEOUT_MS = 200L
|
|
6
|
+
const val ENCODE_THREAD_JOIN_TIMEOUT_MS = 3_000L
|
|
7
|
+
const val CAPTURE_THREAD_JOIN_TIMEOUT_MS = 1_000L
|
|
8
|
+
const val CANCEL_ENCODE_JOIN_TIMEOUT_MS = 1_000L
|
|
9
|
+
const val CANCEL_CAPTURE_JOIN_TIMEOUT_MS = 500L
|
|
10
|
+
const val PCM_QUEUE_CAPACITY = 128
|
|
11
|
+
const val CAPTURE_BUFFER_FRACTION_OF_SECOND = 5
|
|
12
|
+
const val DEFAULT_CACHE_SUBDIR = "clarion-recorder"
|
|
13
|
+
const val DEFAULT_FILENAME_PREFIX = "clarion"
|
|
14
|
+
const val OUTPUT_EXTENSION = "m4a"
|
|
15
|
+
const val URI_SCHEME = "file://"
|
|
16
|
+
}
|