You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native中byte[]与JS Bridge互传及音频流卡顿问题咨询

Answers to Your React Native + Java Audio & Custom View Questions

Let's tackle your questions one by one—you've got two main goals here: cutting out the inefficient byte[] ↔ String conversions between React Native's JS Bridge and Java, plus building a custom native View that handles your audio/WebSocket logic and integrates smoothly with React Navigation. Here's how to make it happen:

1. Bidirectional byte[] Transfer Without String/WritableArray Conversions

The root cause of your GC thrashing and playback lag is the constant Base64 encoding/decoding and array conversions—these create tons of temporary objects that overwhelm low-memory devices. Here's how to pass raw byte[] data directly between JS and Java:

Sending byte[] from Java to JS

Instead of converting to a String, wrap the byte[] in a ByteBuffer and pass it to JS as a Uint8Array (JS's native equivalent of a byte array):

import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.UiThreadUtil;
import java.nio.ByteBuffer;

public class AudioModule extends ReactContextBaseJavaModule {
    public AudioModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

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

    // Example: Send raw audio bytes from Java (WebSocket) to JS
    private void onWebSocketAudioReceived(byte[] audioBytes) {
        UiThreadUtil.runOnUiThread(() -> {
            // Get the JS callback to send data
            Callback dataCallback = getReactApplicationContext().getJSModule(Callback.class);
            ByteBuffer buffer = ByteBuffer.wrap(audioBytes);
            dataCallback.invoke(null, buffer); // JS receives this as Uint8Array
        });
    }
}

In JS, you'll receive the raw data directly as a Uint8Array:

import { NativeModules } from 'react-native';

// Set up listener for audio data
NativeModules.AudioModule.setAudioDataCallback((audioBytes) => {
  // audioBytes is a Uint8Array—no conversion needed
  console.log("Received raw audio chunk:", audioBytes.length);
});

Receiving byte[] from JS to Java

Accept a Uint8Array in your Java method, which maps directly to a byte[] without conversion:

import com.facebook.react.bridge.Uint8Array;
import com.facebook.react.bridge.Promise;

@ReactMethod
public void sendAudioToWebSocket(Uint8Array audioBytes, Promise promise) {
    byte[] rawBytes = audioBytes.getByteArray();
    // Send raw bytes directly to your WebSocket endpoint
    yourWebSocketClient.send(rawBytes);
    promise.resolve(null);
}

In JS, pass the raw Uint8Array directly:

// Assume you have raw audio data as Uint8Array
const rawAudioChunk = new Uint8Array(yourRecordingData);
await NativeModules.AudioModule.sendAudioToWebSocket(rawAudioChunk);

2. Building a Custom Java View with WebSocket/STOMP/Audio Logic + React Navigation Integration

Moving all your WebSocket, STOMP, and audio logic to a custom native View is the best way to eliminate JS Bridge overhead entirely. Here's a step-by-step implementation:

Step 1: Create the Custom View Class

Build a View that handles all native logic and manages lifecycle to avoid resource leaks:

import android.content.Context;
import android.view.ViewGroup;
import android.media.AudioRecorder;
import android.media.AudioTrack;
import okhttp3.WebSocket;

public class AudioWebSocketView extends ViewGroup {
    private WebSocket webSocketClient;
    private AudioRecorder audioRecorder;
    private AudioTrack audioTrack;

    public AudioWebSocketView(Context context) {
        super(context);
        initWebSocket();
        initAudioComponents();
    }

    private void initWebSocket() {
        // Initialize your WebSocket/STOMP client here (e.g., using OkHttp)
        webSocketClient = new WebSocket.Builder()
            .url("your_websocket_endpoint")
            .build();
        
        // Handle incoming audio data directly
        webSocketClient.setListener(new WebSocket.Listener() {
            @Override
            public void onMessage(WebSocket webSocket, byte[] bytes) {
                // Play audio immediately without JS Bridge
                audioTrack.write(bytes, 0, bytes.length);
            }
        });
    }

    private void initAudioComponents() {
        // Initialize AudioRecorder and AudioTrack with your desired parameters
        audioTrack = new AudioTrack(...);
        audioRecorder = new AudioRecorder(...);
        
        // Send recorded bytes directly to WebSocket
        audioRecorder.setOnDataAvailableListener((byte[] data) -> {
            webSocketClient.send(data);
        });
    }

    // Expose control methods for JS
    public void startRecording() {
        audioRecorder.startRecording();
    }

    public void stopRecording() {
        audioRecorder.stopRecording();
    }

    public void startPlaying() {
        audioTrack.play();
    }

    public void stopPlaying() {
        audioTrack.stop();
    }

    // Clean up resources when view is detached
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (webSocketClient != null) webSocketClient.close(1000, "View detached");
        if (audioTrack != null) audioTrack.release();
        if (audioRecorder != null) audioRecorder.release();
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // Required for ViewGroup—leave empty if no child views
    }
}

Step 2: Create a View Manager

Register your custom view with React Native using a ViewManager:

import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactMethod;

public class AudioWebSocketViewManager extends SimpleViewManager<AudioWebSocketView> {
    public static final String REACT_CLASS = "AudioWebSocketView";

    @Override
    public String getName() {
        return REACT_CLASS;
    }

    @Override
    protected AudioWebSocketView createViewInstance(ThemedReactContext reactContext) {
        return new AudioWebSocketView(reactContext);
    }

    // Expose control methods to JS
    @ReactMethod
    public void startRecording(AudioWebSocketView view) {
        view.startRecording();
    }

    @ReactMethod
    public void stopRecording(AudioWebSocketView view) {
        view.stopRecording();
    }

    @ReactMethod
    public void startPlaying(AudioWebSocketView view) {
        view.startPlaying();
    }

    @ReactMethod
    public void stopPlaying(AudioWebSocketView view) {
        view.stopPlaying();
    }
}

Step 3: Register the View in a React Package

Create a package to add your ViewManager to React Native:

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class AudioWebSocketPackage implements ReactPackage {
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        List<ViewManager> managers = new ArrayList<>();
        managers.add(new AudioWebSocketViewManager());
        return managers;
    }
}

Add this package to your MainApplication.java:

@Override
protected List<ReactPackage> getPackages() {
    List<ReactPackage> packages = new PackageList(this).getPackages();
    packages.add(new AudioWebSocketPackage()); // Add your custom package
    return packages;
}

Step 4: Integrate with React Navigation

Create a JS component for your custom view, then add it to your navigation stack:

import React, { useRef } from 'react';
import { requireNativeComponent, UIManager, findNodeHandle } from 'react-native';
import { Button, View } from 'react-native';

const AudioWebSocketView = requireNativeComponent('AudioWebSocketView');

export default function AudioScreen() {
    const viewRef = useRef(null);

    const triggerAction = (command) => {
        const viewId = findNodeHandle(viewRef.current);
        UIManager.dispatchViewManagerCommand(
            viewId,
            UIManager.AudioWebSocketView.Commands[command],
            []
        );
    };

    return (
        <View style={{ flex: 1 }}>
            <AudioWebSocketView ref={viewRef} style={{ flex: 1 }} />
            <View style={{ flexDirection: 'row', padding: 16, gap: 8 }}>
                <Button title="Start Recording" onPress={() => triggerAction('startRecording')} />
                <Button title="Stop Recording" onPress={() => triggerAction('stopRecording')} />
                <Button title="Start Playing" onPress={() => triggerAction('startPlaying')} />
                <Button title="Stop Playing" onPress={() => triggerAction('stopPlaying')} />
            </View>
        </View>
    );
}

Add the screen to your React Navigation stack:

import { createStackNavigator } from '@react-navigation/stack';
import { NavigationContainer } from '@react-navigation/native';
import AudioScreen from './AudioScreen';

const Stack = createStackNavigator();

export default function App() {
    return (
        <NavigationContainer>
            <Stack.Navigator>
                <Stack.Screen name="AudioStream" component={AudioScreen} />
                {/* Add other screens here */}
            </Stack.Navigator>
        </NavigationContainer>
    );
}

3. Fixing the Playback Lag Issue

With the custom view approach, you eliminate two major performance bottlenecks:

  • No more Base64 conversions: Raw byte[] data flows directly between the recorder, WebSocket, and player—no temporary objects to trigger GC.
  • No JS Bridge overhead: All audio and WebSocket logic runs natively, so there's no back-and-forth with JS that slows down playback.

This should give you smooth audio playback even on low-memory 512MB virtual devices.


内容的提问来源于stack exchange,提问作者Jason Rich Darmawan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 08:51:33