React Native Preview Screen Error and Bug

Hey there,

I am building an app using the React Native SDK. I am currently working on a preview screen where users can preview the camera, and test their mic and speaker, but I keep getting errors.

For the camera, when the screen first loads the camera starts and the video shows, but if I toggle it off and on again, I get ZoomVideoSDKError_Load_Module_Error.

For the Microphone test, I can start it and stop it without issue, but when I try to play it back using the testAudioDeviceHelper.playMicTest() function I get ZoomVideoSDKError_Wrong_Usage.

For the speaker test just get ZoomVideoSDKError_Internal_Error and nothing happens.

I am using "@zoom/react-native-videosdk": "^2.2.5" and Expo. I should note that the actual tour screen and functionality work great, without any issues; it is just the preview screen.

Here is the full code:

import React, { useState, useEffect } from 'react';
import { View, Text, Pressable } from 'react-native';
import { ZoomView, useZoom, ZoomVideoSDKTestMicStatus, EventType, VideoAspect, Errors } from '@zoom/react-native-videosdk';
import { Button } from '@/src/components/common/button';
import { ArrowLeft, Mic, MicOff, Video, VideoOff, Volume2, VolumeX, RotateCcw, Play } from 'lucide-react-native';
import { router } from 'expo-router';

interface PreviewScreenProps {
  sessionName: string;
  displayName: string;
  onJoin: (videoOn: boolean, audioOn: boolean) => void;
  initialVideoOn?: boolean;
  initialAudioOn?: boolean;
}

export function PreviewScreen({ 
  sessionName, 
  displayName, 
  onJoin, 
  initialVideoOn = false, 
  initialAudioOn = false 
}: PreviewScreenProps) {
  const zoom = useZoom();

  const [videoOn, setVideoOn] = useState(initialVideoOn);
  const [audioOn, setAudioOn] = useState(initialAudioOn);
  
  const [micStatus, setMicStatus] = useState<ZoomVideoSDKTestMicStatus>(ZoomVideoSDKTestMicStatus.CanTest);
  const [isSpeakerTesting, setIsSpeakerTesting] = useState(false);

  useEffect(() => {
    const testMicListener = zoom.addListener(
      EventType.onTestMicStatusChanged,
      (params: { status: ZoomVideoSDKTestMicStatus }) => {
        console.log('Mic status changed:', params.status);
        setMicStatus(params.status);
      }
    );

    return () => {
      testMicListener.remove();
    }
  }, [zoom]);

  const handleMicAction = async () => {
    try {
      if (micStatus === ZoomVideoSDKTestMicStatus.CanTest) {
        const res = await zoom.testAudioDeviceHelper.startMicTest();
        console.log('Start Mic Test', res);
        if (res === Errors.Success) {
          setMicStatus(ZoomVideoSDKTestMicStatus.Recording);
        }
      } else if (micStatus === ZoomVideoSDKTestMicStatus.Recording) {
        const res = await zoom.testAudioDeviceHelper.stopMicTest();
        console.log('Stop Mic Test', res);
        if (res === Errors.Success) {
          setMicStatus(ZoomVideoSDKTestMicStatus.CanPlay);
        }
      } else if (micStatus === ZoomVideoSDKTestMicStatus.CanPlay) {
        const res = await zoom.testAudioDeviceHelper.playMicTest();
        console.log('Play Mic Test', res);
        if (res === Errors.Success) {
          setMicStatus(ZoomVideoSDKTestMicStatus.CanTest);
        }
      }
    } catch (e) {
      console.error("Mic test error", e);
    }
  };

  const resetMicTest = async () => {
    try {
      if (micStatus !== ZoomVideoSDKTestMicStatus.CanPlay) return;
      const res = await zoom.testAudioDeviceHelper.startMicTest();
      console.log('resetMicTest', res);
      if (res === Errors.Success) {
        setMicStatus(ZoomVideoSDKTestMicStatus.Recording);
      }
    } catch (e) {
      console.error('resetMicTest error', e);
    }
  };

  const toggleSpeakerTest = async () => {
    try {
      if (isSpeakerTesting) {
        const res = await zoom.testAudioDeviceHelper.stopSpeakerTest();
        console.log('Stop Speaker Test', res);
        if (res === Errors.Success) {
          setIsSpeakerTesting(false);
        }
      } else {
        const res = await zoom.testAudioDeviceHelper.startSpeakerTest();
        console.log('Start Speaker Test', res);
        if (res === Errors.Success) {
          setIsSpeakerTesting(true);
        }
      }
    } catch (e) {
      console.error("Speaker test error", e);
    }
  };

  const toggleVideo = async () => {
    try {
      if (videoOn) {
        const res = await zoom.videoHelper.stopVideo();
        console.log('Stop Video', res);
      } else {
        const res = await zoom.videoHelper.startVideo();
        console.log('Start Video', res);
      }
    } catch (e) {}
    setVideoOn(!videoOn);
  }

  useEffect(() => {
    return () => {
      zoom.videoHelper.stopVideo().catch(() => {});
    }
  }, []);

  useEffect(() => {
    return () => {
      if (isSpeakerTesting) zoom.testAudioDeviceHelper.stopSpeakerTest().catch(() => {});
    };
  }, [isSpeakerTesting, zoom]);

  return (
    <View className="flex-1 relative px-6 py-4 gap-2 bg-[#FAF3EA]">
      {/* Close Button */}
      <View className="flex-row items-center gap-4 ">
        <Pressable onPress={() => router.back()}>
          <ArrowLeft color="#1D1C1B" size={28}/>
        </Pressable>
        <Text className="text-3xl font-medium text-black">Preview</Text>
      </View>

      <View className="flex-1 flex-row gap-2">
        {/* Video Preview */}
        <View className="flex-1 bg-black relative rounded-2xl overflow-hidden">
          {videoOn ? (
            <ZoomView
              style={{ width: '100%', height: '100%' }}
              userId=""
              preview={true}
              fullScreen={false}
              sharing={false}
              hasMultiCamera={false}
              multiCameraIndex={'0'}
              videoAspect={VideoAspect.PanAndScan}
            />
          ) : (
            <View className="flex-1 items-center justify-center bg-[#232323]">
              <View className="w-24 h-24 rounded-full bg-gray-600 items-center justify-center">
                 <Text className="text-white text-2xl font-bold">{displayName.charAt(0)}</Text>
              </View>
              <Text className="text-white mt-4 text-lg">Camera is off</Text>
            </View>
          )}

          {/* Controls Overlay */}
          <View className="absolute bottom-6 left-0 right-0 flex-row justify-center gap-6">
            <View className="flex-row gap-4">
              <Button variant="overlayIcon" onPress={() => setAudioOn(!audioOn)}>
                {audioOn ? (
                  <Mic size={28} color="#FFF" />
                ) : (
                  <MicOff size={28} color="#FFF" />
                )}
              </Button>

              <Button variant="overlayIcon" onPress={toggleVideo}>
                {videoOn ? (
                  <Video size={28} color="#FFF" />
                ) : (
                  <VideoOff size={28} color="#FFF" />
                )}
              </Button>
            </View>
          </View>
        </View>

        <View className="justify-between bg-white rounded-2xl p-6">
          <View>
            <Text className="text-xl font-bold text-black mb-6">{sessionName}</Text>
            <View className="gap-4">
              {/* Mic Test Control */}
              <View className="gap-2">
                <Text className="text-sm font-medium text-gray-500 uppercase">Microphone</Text>
                <View className="flex-row gap-2">
                  <Pressable 
                    onPress={handleMicAction}
                    className={`flex-1 py-2.5 rounded-lg flex-row items-center justify-center gap-2 ${
                      micStatus === ZoomVideoSDKTestMicStatus.Recording 
                        ? 'bg-red-50 border border-red-100' 
                        : micStatus === ZoomVideoSDKTestMicStatus.CanPlay
                        ? 'bg-green-50 border border-green-100'
                        : 'bg-gray-50 border border-gray-200'
                    }`}
                  >
                    {micStatus === ZoomVideoSDKTestMicStatus.Recording ? (
                      <>
                        <View className="w-2 h-2 bg-red-500 rounded-sm" />
                        <Text className="text-red-600 font-medium text-sm">Stop</Text>
                      </>
                    ) : micStatus === ZoomVideoSDKTestMicStatus.CanPlay ? (
                      <>
                        <Play size={14} color="#10B981" fill="#10B981" />
                        <Text className="text-green-600 font-medium text-sm">Play</Text>
                      </>
                    ) : (
                      <Text className="text-gray-700 font-medium text-sm">Test Mic</Text>
                    )}
                  </Pressable>
                  
                  {micStatus === ZoomVideoSDKTestMicStatus.CanPlay && (
                    <Pressable 
                      onPress={resetMicTest}
                      className="w-10 items-center justify-center rounded-lg bg-gray-100 border border-gray-200"
                    >
                      <RotateCcw size={16} color="#6B7280" />
                    </Pressable>
                  )}
                </View>
              </View>

              {/* Speaker Test Control */}
              <View className="gap-2 mt-2">
                <Text className="text-sm font-medium text-gray-500 uppercase">Speaker</Text>
                <Pressable 
                  onPress={toggleSpeakerTest}
                  className={`py-2.5 rounded-lg flex-row items-center justify-center gap-2 ${
                    isSpeakerTesting 
                      ? 'bg-blue-50 border border-blue-100' 
                      : 'bg-gray-50 border border-gray-200'
                  }`}
                >
                  {isSpeakerTesting ? (
                    <>
                      <VolumeX size={16} color="#2563EB" />
                      <Text className="text-blue-600 font-medium text-sm">Stop Sound</Text>
                    </>
                  ) : (
                    <>
                      <Volume2 size={16} color="#374151" />
                      <Text className="text-gray-700 font-medium text-sm">Test Speaker</Text>
                    </>
                  )}
                </Pressable>
              </View>
            </View>
          </View>

          <Button
            title="Join Session"
            onPress={() => onJoin(videoOn, audioOn)}
          />
        </View>
      </View>
    </View>
  );
}

Note that the entire component above is wrapped in the ZoomVideoSdkProvider with the following configs: { domain: "zoom.us", enableLog: true }
And here are the full logs from a test:

Start Mic Test ZoomVideoSDKError_Success
Stop Mic Test ZoomVideoSDKError_Success
Play Mic Test ZoomVideoSDKError_Wrong_Usage
resetMicTest ZoomVideoSDKError_Success
Stop Mic Test ZoomVideoSDKError_Success
Start Speaker Test ZoomVideoSDKError_Internal_Error
Start Speaker Test ZoomVideoSDKError_Internal_Error
Stop Video ZoomVideoSDKError_Load_Module_Error
Start Video ZoomVideoSDKError_Load_Module_Error

Any help or guidance is much appreciated! Thanks.