I’m working on a custom React Native module for my EXPO EAS Build Android app that involves streaming a raw H264 Drone footage live. The module includes a custom view (StreamingView) that manages a SurfaceView for rendering the video stream. The SurfaceView is supposed to trigger surfaceCreated, surfaceChanged, and surfaceDestroyed callbacks which are essential for managing the stream lifecycle.
However, I’m facing an issue where the surfaceCreated callback is not being triggered, even though the SurfaceHolder appears to be valid and everything else is set up correctly. This issue is preventing me from starting the video stream as expected.
Here’s are the snippets from my code:
StreamingView.java
package com.anonymous.Kizuna;
import android.content.Context;
import android.util.Log;
import android.view.SurfaceView;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.view.SurfaceHolder;
public class StreamingView extends FrameLayout {
private SurfaceView surfaceView;
private static final String TAG = "StreamingView";
public StreamingView(Context context) {
super(context);
surfaceView = new SurfaceView(context);
Log.d(TAG, "SurfaceView created");
}
public SurfaceView getSurfaceView() {
return surfaceView;
}
public void removeSurfaceViewFromParent() {
ViewGroup parent = (ViewGroup) surfaceView.getParent();
if (parent != null) {
parent.removeView(surfaceView);
Log.d(TAG, "SurfaceView removed from parent");
}
}
public void addSurfaceViewToParent() {
if (!isAttachedToWindow()) {
Log.d(TAG, "StreamingView is not attached to the window");
return;
}
if (getWidth() == 0 || getHeight() == 0) {
Log.d(TAG, "StreamingView has not been laid out");
return;
}
ViewGroup parent = (ViewGroup) surfaceView.getParent();
if (parent != null) {
parent.removeView(surfaceView);
Log.d(TAG, "SurfaceView removed from old parent");
}
surfaceView.requestLayout();
surfaceView.invalidate();
addView(surfaceView);
Log.d(TAG, "SurfaceView added to StreamingView");
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeSurfaceViewFromParent();
Log.d(TAG, "StreamingView detached from window");
}
public void initializeAndAddSurfaceView() {
addSurfaceViewToParent();
Log.d(TAG, "SurfaceView initialized and added to StreamingView");
}
}
TelloStreamModule.java
public class TelloStreamModule extends ReactContextBaseJavaModule {
private UDPReceiver receiver;
private H264Decoder decoder;
private volatile boolean isStreaming = false;
private SurfaceView surfaceView;
private SurfaceHolder surfaceHolder;
private StreamingView streamingView;
private ReactApplicationContext reactContext;
private Thread streamThread;
private static final String TAG = "TelloStreamModule";
private int streamingViewId;
public TelloStreamModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@Override
public String getName() {
return "TelloStreamModule";
}
@ReactMethod
public void startStream(int viewId) {
streamingViewId = viewId;
Log.d(TAG, "startStream called");
Activity activity = reactContext.getCurrentActivity();
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
Log.d(TAG, "Start stream is running on UI thread");
UIManagerModule uiManager = reactContext.getNativeModule(UIManagerModule.class);
StreamingView newStreamingView = (StreamingView) uiManager.resolveView(viewId);
if (newStreamingView == null) {
Log.e(TAG, "Failed to resolve StreamingView");
return;
}
Log.d(TAG, "Initializing and adding SurfaceView");
newStreamingView.initializeAndAddSurfaceView();
SurfaceView newSurfaceView = newStreamingView.getSurfaceView();
surfaceView = newSurfaceView;
surfaceHolder = surfaceView.getHolder();
if (surfaceHolder != null) {
Log.d(TAG, "getHolder() returned a valid SurfaceHolder.");
surfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.d(TAG, "Surface created successfully!");
try {
if (receiver == null) {
receiver = new UDPReceiver(11111);
}
if (decoder == null) {
decoder = new H264Decoder(holder);
decoder.init();
}
startReceiving();
} catch (Exception e) {
Log.e(TAG, "Exception in surfaceCreated", e);
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Log.d(TAG, "Surface changed: format=" + format + ", width=" + width + ", height=" + height);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.d(TAG, "Surface destroyed");
}
});
} else {
Log.d(TAG, "getHolder() returned null.");
}
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
newSurfaceView.setLayoutParams(params);
newSurfaceView.setVisibility(View.VISIBLE);
newSurfaceView.requestLayout();
Log.d(TAG, "SurfaceView visibility set to VISIBLE");
streamingView = newStreamingView;
}
});
} else {
Log.d(TAG, "Current activity is null or finishing or destroyed");
}
}
.
.
.
.
.
DroneScreen.js
.
.
.
useEffect(() => {
const timeoutId = setTimeout(() => {
console.log('Ref current value:', streamingViewRef.current);
if (streamingViewRef.current) {
const handle = findNodeHandle(streamingViewRef.current);
console.log('Attempting to start stream with handle:', handle);
try {
TelloStreamModule.startStream(handle);
console.log('Stream started successfully');
} catch (error) {
console.error('Failed to start stream:', error);
}
} else {
console.warn('streamingViewRef.current is not set');
}
}, 3000);
return () => clearTimeout(timeoutId); // Cleanup function to clear the timeout
}, [streamingViewRef.current]);
.
.
.
.
{showPlayer && (
<View style={styles.videoContainer}>
{console.log("Rendering StreamingViewJava")}
<StreamingViewJava
ref={streamingViewRef}
style={{ width: '100%', height: 300 }}
/>
</View>
)}
The SurfaceView is added to the layout and should be ready to display content, but the surfaceCreated callback never gets called, which is critical for initializing the streaming components.
I've checked that the SurfaceView is properly attached to the window and that its layout is set correctly. The rest of the app, including other React Native components and modules, works properly.
Has anyone encountered a similar issue or can offer insights into why the surfaceCreated callback might not be triggered in this context? Any suggestions on how to debug or fix this problem would be greatly appreciated!