Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,6 @@ buck-out/
**/.settings
**/.factorypath
.vscode

# TestApp (local test application)
TestApp/
1 change: 1 addition & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def safeExtGet(prop, fallback) {
}

android {
namespace "com.imxiqi.rnliveaudiostream"
compileSdkVersion safeExtGet("compileSdkVersion", DEFAULT_COMPILE_SDK_VERSION)
buildToolsVersion safeExtGet("buildToolsVersion", DEFAULT_BUILD_TOOLS_VERSION)

Expand Down
3 changes: 1 addition & 2 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.imxiqi.rnliveaudiostream">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class RNLiveAudioStreamPackage implements ReactPackage {
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new RNLiveAudioStreamModule(reactContext));
modules.add(new RNLiveAudioStreamPlaybackModule(reactContext));
return modules;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package com.imxiqi.rnliveaudiostream;

import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.util.Base64;
import android.util.Log;

import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;

import java.util.ArrayList;
import java.util.List;

public class RNLiveAudioStreamPlaybackModule extends ReactContextBaseJavaModule {

private final ReactApplicationContext reactContext;
private AudioTrack audioTrack = null;
private boolean isPlaying = false;
private Thread playbackThread = null;
private int sampleRateInHz = 44100;
private int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
private int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
private int bufferSize = 0;

public RNLiveAudioStreamPlaybackModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}

@Override
public String getName() {
return "RNLiveAudioStreamPlayback";
}

@ReactMethod
public void init(ReadableMap options, Promise promise) {
try {
sampleRateInHz = 44100;
if (options.hasKey("sampleRate")) {
sampleRateInHz = options.getInt("sampleRate");
}

channelConfig = AudioFormat.CHANNEL_OUT_MONO;
if (options.hasKey("channels")) {
if (options.getInt("channels") == 2) {
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
}
}

audioFormat = AudioFormat.ENCODING_PCM_16BIT;
if (options.hasKey("bitsPerSample")) {
if (options.getInt("bitsPerSample") == 8) {
audioFormat = AudioFormat.ENCODING_PCM_8BIT;
}
}

bufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);

if (options.hasKey("bufferSize")) {
bufferSize = Math.max(bufferSize, options.getInt("bufferSize"));
}

// Store config for later use, but don't create AudioTrack yet
// AudioTrack will be created when play() is called
promise.resolve(null);
} catch (Exception e) {
promise.reject("AudioTrack initialization error", e.getMessage());
}
}

private void createAudioTrack() {
if (audioTrack != null) {
return; // Already created
}
audioTrack = new AudioTrack(
AudioManager.STREAM_MUSIC,
sampleRateInHz,
channelConfig,
audioFormat,
bufferSize,
AudioTrack.MODE_STREAM
);
}

@ReactMethod
public void play(ReadableArray base64Chunks, Promise promise) {
if (isPlaying) {
promise.reject("Playback error", "Already playing");
return;
}

try {
// Create AudioTrack if needed
createAudioTrack();

if (audioTrack == null || audioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
promise.reject("Playback error", "AudioTrack not initialized");
return;
}

isPlaying = true;
audioTrack.play();

playbackThread = new Thread(new Runnable() {
public void run() {
try {
List<byte[]> audioDataList = new ArrayList<>();

// Decode all base64 chunks
for (int i = 0; i < base64Chunks.size(); i++) {
String base64Data = base64Chunks.getString(i);
byte[] audioData = Base64.decode(base64Data, Base64.NO_WRAP);
audioDataList.add(audioData);
}

// Write all audio data to AudioTrack
for (byte[] audioData : audioDataList) {
if (!isPlaying) break;
int written = audioTrack.write(audioData, 0, audioData.length);
if (written < 0) {
Log.e("RNLiveAudioStreamPlayback", "Error writing audio data: " + written);
break;
}
}

// Wait for all data to be played
// AudioTrack.write() is blocking, so we just need to wait for thread completion

} catch (Exception e) {
Log.e("RNLiveAudioStreamPlayback", "Playback error: " + e.getMessage());
e.printStackTrace();
} finally {
// Playback completed - emit event to JS
DeviceEventManagerModule.RCTDeviceEventEmitter eventEmitter =
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
eventEmitter.emit("playbackFinished", null);
stopPlayback();
}
}
});

playbackThread.start();
promise.resolve(null);
} catch (Exception e) {
isPlaying = false;
promise.reject("Playback error", e.getMessage());
}
}

@ReactMethod
public void stop() {
stopPlayback();
}

private void stopPlayback() {
isPlaying = false;
if (audioTrack != null) {
try {
if (audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
audioTrack.stop();
}
audioTrack.release();
} catch (Exception e) {
Log.e("RNLiveAudioStreamPlayback", "Error stopping playback: " + e.getMessage());
}
audioTrack = null;
}
if (playbackThread != null) {
playbackThread.interrupt();
playbackThread = null;
}
}
}
10 changes: 10 additions & 0 deletions react-native.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
dependency: {
platforms: {
android: {
sourceDir: '../android',
packageImportPath: 'import com.imxiqi.rnliveaudiostream.RNLiveAudioStreamPackage;',
},
},
},
};