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
129 changes: 112 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,136 @@
# react-native-opus
# rn-stream-opus

A library for decoding Opus audio packets in React Native.
A React Native library optimized for real-time Opus audio streaming with frame-by-frame decoding capabilities. Built specifically for streaming audio applications that require low-latency, persistent decoder state.

## Installation

```sh
npm install react-native-opus
npm install rn-stream-opus
```

## Usage

The `react-native-opus` library provides a function to decode Opus audio packets from base64-encoded strings.
The `rn-stream-opus` library provides efficient frame-by-frame Opus decoding with persistent decoder state, perfect for real-time audio streaming applications.

### Example: Decoding an Opus Packet
## API Methods

```js
import { decodeMultipleOpusPackets } from 'react-native-opus';
### Primary Streaming API

- **`initializeStreamDecoder(sampleRate: number, channels: number)`**: Initializes the stream decoder with specified sample rate and channel count.
- **`decodeOpusFrame(frameData: Uint8Array)`**: Decodes a single Opus frame and returns Float32Array PCM data.
- **`resetOpusStreamDecoder()`**: Resets the stream decoder state.

### Legacy Batch API

// Hardcoded base64 string (100 characters)
const base64String = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
- **`decodeMultipleOpusPackets(base64String: string, frameSize: number)`**: Decodes multiple Opus packets from a base64-encoded string. The `frameSize` parameter specifies the frame size in bytes.

// Decode the Opus packet
async function decodeAudio() {
## Examples

### Real-Time Streaming (Primary Use Case)

```js
import {
initializeStreamDecoder,
decodeOpusFrame,
resetOpusStreamDecoder
} from 'rn-stream-opus';

async function streamDecodeAudio() {
try {
const decodedPackets = await decodeMultipleOpusPackets(base64String, 40);
console.log("Decoded packets:", decodedPackets);
// Initialize stream decoder for 16kHz mono audio
const initResult = await initializeStreamDecoder(16000, 1);
if (!initResult.success) {
throw new Error(initResult.error);
}

// Decode individual frames as they arrive
const frameData = new Uint8Array([/* your opus frame bytes */]);
const decodeResult = await decodeOpusFrame(frameData);

if (decodeResult.success && decodeResult.pcmData) {
console.log(`Decoded ${decodeResult.samplesDecoded} samples`);
console.log('PCM data:', decodeResult.pcmData); // Float32Array

// Process PCM data for real-time playback
processAudioData(decodeResult.pcmData);
}

// Reset decoder when stream ends
await resetOpusStreamDecoder();
} catch (error) {
console.error("Error decoding audio:", error);
console.error("Error streaming decode:", error);
}
}

decodeAudio();
streamDecodeAudio();
```

### Key Function
### Streaming with Audio Processing

```js
import {
initializeStreamDecoder,
decodeOpusFrame
} from 'rn-stream-opus';

class OpusStreamer {
constructor() {
this.initialized = false;
}

async initialize(sampleRate = 16000, channels = 1) {
const result = await initializeStreamDecoder(sampleRate, channels);
this.initialized = result.success;
return result;
}

async processFrame(opusFrame) {
if (!this.initialized) {
throw new Error('Decoder not initialized');
}

const result = await decodeOpusFrame(opusFrame);

if (result.success && result.pcmData) {
// Direct access to Float32Array PCM data
return {
pcm: result.pcmData,
samples: result.samplesDecoded,
timestamp: Date.now()
};
}

throw new Error(result.error || 'Decode failed');
}
}

// Usage
const streamer = new OpusStreamer();
await streamer.initialize(16000, 1);

// Process frames as they arrive
const audioData = await streamer.processFrame(frameBytes);
playAudio(audioData.pcm);
```

## Key Features

- **🎯 Real-Time Streaming**: Optimized for frame-by-frame decoding with persistent decoder state
- **⚡ Low Latency**: Minimal processing overhead for real-time audio applications
- **🔄 Float32Array Output**: Direct PCM data ready for audio processing and playback
- **📱 Cross-Platform**: Native TurboModule implementation for iOS and Android
- **🧠 Memory Efficient**: Optimized buffer management for mobile devices
- **🔧 Easy Integration**: Simple API designed for streaming audio applications
- **🔄 Persistent State**: Decoder maintains state across frames for continuous streams

## Use Cases

- **`decodeMultipleOpusPackets(base64String: string, frameSize: number)`**: Decodes a base64-encoded Opus packet. The `frameSize` parameter specifies the frame size in milliseconds (e.g., 40ms).
- **Real-time voice communication apps**
- **Audio streaming applications**
- **Voice memo recording/playback**
- **Live audio processing**
- **Bluetooth audio streaming**
- **WebRTC audio pipelines**

## Contributing

Expand Down
126 changes: 126 additions & 0 deletions cpp/NativeOpusTurboModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ NativeOpusTurboModule::~NativeOpusTurboModule() {
opus_decoder_destroy(opusDecoder);
opusDecoder = nullptr;
}
if (streamDecoder) {
opus_decoder_destroy(streamDecoder);
streamDecoder = nullptr;
}
}

// Base64 encoding/decoding utility methods
Expand Down Expand Up @@ -286,4 +290,126 @@ jsi::Value NativeOpusTurboModule::saveDecodedDataAsWav(jsi::Runtime &rt, std::st
return result;
}

// Frame-by-frame streaming methods
jsi::Value NativeOpusTurboModule::initializeStreamDecoder(jsi::Runtime &rt, double sampleRate, double channels) {
jsi::Object result = jsi::Object(rt);

try {
// Clean up existing stream decoder if any
if (streamDecoder) {
opus_decoder_destroy(streamDecoder);
streamDecoder = nullptr;
}

// Create new stream decoder with specified parameters
streamSampleRate = static_cast<opus_int32>(sampleRate);
streamChannels = static_cast<int>(channels);

int error = 0;
streamDecoder = opus_decoder_create(streamSampleRate, streamChannels, &error);

if (error != OPUS_OK || !streamDecoder) {
result.setProperty(rt, "success", false);
result.setProperty(rt, "error", jsi::String::createFromUtf8(rt, opus_strerror(error)));
streamInitialized = false;
return result;
}

streamInitialized = true;
result.setProperty(rt, "success", true);

} catch (const std::exception& e) {
result.setProperty(rt, "success", false);
result.setProperty(rt, "error", jsi::String::createFromUtf8(rt, e.what()));
streamInitialized = false;
}

return result;
}

jsi::Value NativeOpusTurboModule::decodeOpusFrame(jsi::Runtime &rt, std::string base64Frame) {
jsi::Object result = jsi::Object(rt);

if (!streamInitialized || !streamDecoder) {
result.setProperty(rt, "success", false);
result.setProperty(rt, "error", jsi::String::createFromUtf8(rt, "Stream decoder not initialized"));
return result;
}

try {
// Decode base64 frame data
std::vector<uint8_t> frameData = base64_decode(base64Frame);

if (frameData.empty() && !base64Frame.empty()) {
result.setProperty(rt, "success", false);
result.setProperty(rt, "error", jsi::String::createFromUtf8(rt, "Invalid base64 frame data"));
return result;
}

// Prepare output buffer for decoded PCM data
float pcmBuffer[MAX_FRAME_SIZE * streamChannels];

// Decode the Opus frame
int samplesDecoded = opus_decode_float(
streamDecoder,
frameData.data(),
static_cast<opus_int32>(frameData.size()),
pcmBuffer,
MAX_FRAME_SIZE,
0
);

if (samplesDecoded < 0) {
result.setProperty(rt, "success", false);
result.setProperty(rt, "error", jsi::String::createFromUtf8(rt, opus_strerror(samplesDecoded)));
return result;
}

// Create Float32Array from the decoded PCM data
size_t totalSamples = samplesDecoded * streamChannels;
size_t bufferSize = totalSamples * sizeof(float);

auto arrayBuffer = jsi::ArrayBuffer(rt, bufferSize);
std::memcpy(arrayBuffer.data(rt), pcmBuffer, bufferSize);

jsi::Object typedArray = rt.global().getPropertyAsObject(rt, "Float32Array");
jsi::Value args[] = {
jsi::Value(rt, arrayBuffer),
jsi::Value(0),
jsi::Value(static_cast<double>(totalSamples))
};
jsi::Object float32Array = typedArray.asFunction(rt).callAsConstructor(rt, args, 3).asObject(rt);

result.setProperty(rt, "success", true);
result.setProperty(rt, "pcmData", float32Array);
result.setProperty(rt, "samplesDecoded", samplesDecoded);

} catch (const std::exception& e) {
result.setProperty(rt, "success", false);
result.setProperty(rt, "error", jsi::String::createFromUtf8(rt, e.what()));
}

return result;
}

jsi::Value NativeOpusTurboModule::resetOpusStreamDecoder(jsi::Runtime &rt) {
jsi::Object result = jsi::Object(rt);

if (!streamInitialized || !streamDecoder) {
result.setProperty(rt, "success", false);
result.setProperty(rt, "error", jsi::String::createFromUtf8(rt, "Stream decoder not initialized"));
return result;
}

int error = opus_decoder_ctl(streamDecoder, OPUS_RESET_STATE);
if (error == OPUS_OK) {
result.setProperty(rt, "success", true);
} else {
result.setProperty(rt, "success", false);
result.setProperty(rt, "error", jsi::String::createFromUtf8(rt, opus_strerror(error)));
}

return result;
}

} // namespace facebook::react
12 changes: 12 additions & 0 deletions cpp/NativeOpusTurboModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ class NativeOpusTurboModule: public NativeOpusTurboModuleCxxSpec<NativeOpusTurbo
jsi::Value decodeMultipleOpusPackets(jsi::Runtime &rt, std::string packetsBase64, double packetSize);
jsi::Value resetDecoderState(jsi::Runtime &rt);
jsi::Value saveDecodedDataAsWav(jsi::Runtime &rt, std::string decodedDataBase64, std::string filepath, double sampleRate, double channels);

// Frame-by-frame streaming methods
jsi::Value initializeStreamDecoder(jsi::Runtime &rt, double sampleRate, double channels);
jsi::Value decodeOpusFrame(jsi::Runtime &rt, std::string base64Frame);
jsi::Value resetOpusStreamDecoder(jsi::Runtime &rt);

private:
static std::string base64_encode(const std::vector<uint8_t>& input);
Expand All @@ -41,6 +46,13 @@ class NativeOpusTurboModule: public NativeOpusTurboModuleCxxSpec<NativeOpusTurbo
OpusDecoder* opusDecoder = nullptr;
static constexpr opus_int32 DEFAULT_SAMPLE_RATE = 16000;
static constexpr int DEFAULT_CHANNELS = 1;

// Streaming decoder state
OpusDecoder* streamDecoder = nullptr;
opus_int32 streamSampleRate = DEFAULT_SAMPLE_RATE;
int streamChannels = DEFAULT_CHANNELS;
bool streamInitialized = false;
static constexpr int MAX_FRAME_SIZE = 5760; // Max frame size for 48kHz, 120ms frame
};

} // namespace facebook::react
Loading
Loading