/**
* VStarCam React Native SDK
*
* This module provides a React Native bridge to the VStarCam P2P SDK,
* enabling direct communication with VStarCam cameras for:
* - P2P connection establishment
* - WiFi configuration
* - Video streaming
* - PTZ control
* - Camera settings
*/
import {
NativeModules,
NativeEventEmitter,
Platform,
requireNativeComponent,
ViewStyle,
UIManager,
findNodeHandle,
} from "react-native";
import React from "react";
// Video View Component
export interface VStarCamVideoViewProps {
/** Client pointer from create() call */
clientPtr: number;
/** Video resolution: 1=high, 2=general, 4=low, 100=superHD */
resolution?: 1 | 2 | 4 | 100;
/** Whether streaming is active */
streaming?: boolean;
/** Whether live audio should play (unmuted) */
audioEnabled?: boolean;
/** Style for the video view */
style?: ViewStyle;
/** Called when stream starts */
onStreamStarted?: () => void;
/** Called when stream stops */
onStreamStopped?: () => void;
/** Called on stream error */
onStreamError?: (error: { message: string }) => void;
}
/**
* Native video view component for VStarCam camera streaming.
*
* Usage:
* ```tsx
*
* ```
*/
// Video View Component registration with safety check
let VStarCamVideoViewNative = null;
if (Platform.OS === "android") {
try {
VStarCamVideoViewNative = requireNativeComponent("VStarCamVideoView");
} catch (e) {
console.warn("VStarCamVideoView could not be registered:", e.message);
}
}
export const VStarCamVideoView = VStarCamVideoViewNative || (() => null);
/**
* Capture a snapshot from the video view and save it to a file.
* @param viewRef Reference to the VStarCamVideoView component
* @param filePath Absolute path where the JPEG will be saved
*/
export function captureSnapshot(viewRef: any, filePath: string) {
const node = findNodeHandle(viewRef);
if (node) {
UIManager.dispatchViewManagerCommand(
node,
(UIManager as any).VStarCamVideoView.Commands.captureSnapshot,
[filePath]
);
}
}
const LINKING_ERROR =
`The package 'react-native-vstarcam' doesn't seem to be linked. Make sure: \n\n` +
Platform.select({ ios: "- You have run 'pod install'\n", default: "" }) +
"- You rebuilt the app after installing the package\n" +
"- You are not using Expo Go (requires development build)";
// Diagnostic log for native modules - using warn to make it stand out!
console.warn("[VStarCam] JS Initialization: Available NativeModules keys:", Object.keys(NativeModules));
console.warn("[VStarCam] Full NativeModules scan:", {
VStarCam: !!NativeModules.VStarCam,
WifiModule: !!NativeModules.WifiModule,
VStarCamModule: !!NativeModules.VStarCamModule
});
const VStarCamModule = NativeModules.VStarCam
? NativeModules.VStarCam
: new Proxy(
{},
{
get(target, prop) {
// Don't throw for internal JS/React checks
if (prop === '$$typeof' || prop === 'constructor' || typeof prop === 'symbol') {
return undefined;
}
// Log exactly what's being accessed when it fails
console.error(`[VStarCam] Attempted to access property "${String(prop)}" but the native module is missing.`);
throw new Error(LINKING_ERROR);
},
}
);
// Connection states
export enum ConnectionState {
CONNECTING = 1,
INITIALIZING = 2,
ONLINE = 3,
CONNECT_FAILED = 4,
DISCONNECTED = 5,
INVALID_ID = 6,
OFFLINE = 7,
TIMEOUT = 8,
WRONG_PWD = 9,
INVALID_CLIENT = 10,
MAX_SESSION = 11,
}
// Connection modes
export enum ConnectionMode {
NONE = 0,
P2P = 1, // Direct P2P connection
RELAY = 2, // Relay through cloud
SOCKET = 3, // Socket connection
}
// Channel types for P2P communication
export enum ChannelType {
CMD = 0, // Command channel
VIDEO = 1, // Video stream channel
AUDIO = 2, // Audio receive channel
TALK = 3, // Audio send (two-way audio)
PLAYBACK = 4, // Playback channel
ALARM = 5, // Alarm notification channel
}
// WiFi network info
export interface WiFiNetwork {
ssid: string;
mac: string;
security: string;
signalStrength: number;
channel: number;
mode: string;
}
// Device info
export interface DeviceInfo {
serialNumber: string;
model: string;
firmware: string;
manufacturer: string;
features: {
ptz: boolean;
audio: boolean;
twoWayAudio: boolean;
nightVision: boolean;
motionDetection: boolean;
wifi: boolean;
};
responsive?: boolean;
}
// Connection result
export interface ConnectionResult {
success: boolean;
state: ConnectionState;
mode?: ConnectionMode;
error?: string;
}
// Command result
export interface CommandResult {
success: boolean;
data?: Record;
error?: string;
}
// Event types
export type ConnectionEventListener = (
clientId: number,
state: ConnectionState
) => void;
export type CommandEventListener = (
clientId: number,
command: number,
data: Uint8Array
) => void;
export type VideoFrameListener = (
clientId: number,
frame: Uint8Array,
width: number,
height: number,
timestamp: number
) => void;
/**
* VStarCam P2P Client
*
* Main class for interacting with VStarCam cameras
*/
class VStarCamClient {
private clientPtr: number = 0;
private currentUsername: string = "admin";
private currentPassword: string = "888888";
private eventEmitter: NativeEventEmitter;
private connectionListeners: Set = new Set();
private commandListeners: Set = new Set();
private videoListeners: Set = new Set();
constructor() {
this.eventEmitter = new NativeEventEmitter(VStarCamModule);
this.setupEventListeners();
}
private setupEventListeners() {
this.eventEmitter.addListener("onConnectionStateChanged", (event) => {
if (event.clientId === this.clientPtr) {
this.connectionListeners.forEach((listener) => {
listener(event.clientId, event.state);
});
}
});
this.eventEmitter.addListener("onCommandReceived", (event) => {
if (event.clientId === this.clientPtr) {
this.commandListeners.forEach((listener) => {
listener(event.clientId, event.command, event.data);
});
}
});
this.eventEmitter.addListener("onVideoFrame", (event) => {
if (event.clientId === this.clientPtr) {
this.videoListeners.forEach((listener) => {
listener(
event.clientId,
event.frame,
event.width,
event.height,
event.timestamp
);
});
}
});
}
async testBridge(): Promise {
return VStarCamModule.testBridge();
}
/**
* Create a P2P client for a device
* @param deviceId The device ID (DID) from VStarCam
* @returns Client pointer (used internally)
*/
async create(deviceId: string): Promise {
this.clientPtr = await VStarCamModule.clientCreate(deviceId);
return this.clientPtr;
}
/**
* Connect to the camera via P2P
* @param lanScan Whether to scan LAN for the device
* @param serverParam Server connection parameters
* @param connectType Connection type (0 = P2P, 1 = Relay)
*/
async connect(
lanScan: boolean = true,
serverParam: string = "",
connectType: number = 0
): Promise {
if (!this.clientPtr) {
return {
success: false,
state: ConnectionState.INVALID_CLIENT,
error: "Client not created",
};
}
const state = await VStarCamModule.clientConnect(
this.clientPtr,
lanScan,
serverParam,
connectType
);
return {
success: state === ConnectionState.ONLINE,
state,
};
}
/**
* Check the current connection mode
* @returns Mode result including connection type and session handle
*/
async checkConnectionMode(): Promise {
if (!this.clientPtr) return { success: false, mode: 0 };
return VStarCamModule.clientCheckMode(this.clientPtr);
}
/**
* Get the current connection state of the client
*/
async getState(): Promise {
if (!this.clientPtr) return { state: 5, isConnected: false };
return VStarCamModule.clientGetState(this.clientPtr);
}
/**
* Login to the camera
* @param username Camera username (default: admin)
* @param password Camera password (default: 888888)
*/
async login(
username: string = "admin",
password: string = "888888"
): Promise {
if (!this.clientPtr) return false;
this.currentUsername = username;
this.currentPassword = password;
return VStarCamModule.clientLogin(this.clientPtr, username, password);
}
/**
* Send a CGI command to the camera
* @param cgi The CGI command (e.g., "get_status.cgi?")
* @param timeout Timeout in seconds
*/
async sendCommand(cgi: string, timeout: number = 5): Promise {
if (!this.clientPtr) return false;
return VStarCamModule.clientWriteCgi(this.clientPtr, cgi, timeout);
}
/**
* Get camera status/capabilities by sending get_status.cgi
* Returns parsed key-value pairs from the camera response.
* Includes fields like MaxZoomMultiple, support_WhiteLed_Ctrl, haveMotor, etc.
*/
async getStatus(): Promise | null> {
if (!this.clientPtr) return null;
const success = await this.sendCommand("get_status.cgi?");
if (!success) return null;
return new Promise((resolve) => {
const timeoutId = setTimeout(() => {
this.removeCommandListener(handler);
console.warn("[VStarCam] getStatus timed out");
resolve(null);
}, 8000);
const handler: CommandEventListener = (_, cmd, data) => {
// cmd 24577 = status response (same as login response)
if (cmd === 24577 || cmd === 24785) {
clearTimeout(timeoutId);
this.removeCommandListener(handler);
try {
const str = typeof data === "string" ? data : new TextDecoder().decode(data);
const parsed = this.parseResponse(str);
console.log("[VStarCam] getStatus response keys:", Object.keys(parsed).join(", "));
resolve(parsed);
} catch (e) {
console.error("[VStarCam] getStatus parse error:", e);
resolve(null);
}
}
};
this.addCommandListener(handler);
});
}
/**
* Scan for available WiFi networks
*/
async scanWiFi(): Promise {
if (!this.clientPtr) return [];
const success = await this.sendCommand("wifi_scan.cgi?");
if (!success) return [];
// Wait for response
return new Promise((resolve) => {
const timeout = setTimeout(() => resolve([]), 10000);
const handler: CommandEventListener = (_, cmd, data) => {
if (cmd === 24618 || cmd === 24584) {
clearTimeout(timeout);
this.removeCommandListener(handler);
// Parse WiFi list from data
const networks = this.parseWiFiList(data);
resolve(networks);
}
};
this.addCommandListener(handler);
});
}
private parseWiFiList(data: Uint8Array): WiFiNetwork[] {
// Implementation to parse CGI response
const str = new TextDecoder().decode(data);
const networks: WiFiNetwork[] = [];
// Parse key=value pairs
const params = this.parseResponse(str);
const apNumber = parseInt(params["ap_number"] || "0", 10);
for (let i = 0; i < apNumber; i++) {
networks.push({
ssid: params[`ap_ssid[${i}]`] || "",
mac: params[`ap_mac[${i}]`] || "",
security: params[`ap_security[${i}]`] || "",
signalStrength: parseInt(params[`ap_dbm0[${i}]`] || "0", 10),
channel: parseInt(params[`ap_channel[${i}]`] || "0", 10),
mode: params[`ap_mode[${i}]`] || "",
});
}
return networks;
}
private parseResponse(str: string): Record {
const result: Record = {};
const pairs = str.split(";");
for (const pair of pairs) {
const [key, value] = pair.split("=");
if (key && value) {
let cleanKey = key.trim();
// VStarCam often prefix variables with 'var '
if (cleanKey.startsWith("var ")) {
cleanKey = cleanKey.substring(4).trim();
}
let cleanValue = value.trim();
// Remove surrounding quotes if present
if (cleanValue.startsWith('"') && cleanValue.endsWith('"')) {
cleanValue = cleanValue.substring(1, cleanValue.length - 1);
}
result[cleanKey] = cleanValue;
}
}
return result;
}
/**
* Configure camera WiFi
* @param ssid WiFi network name
* @param password WiFi password
* @param security Security type (e.g., "WPA2")
* @param channel WiFi channel
*/
async configureWiFi(
ssid: string,
password: string,
security: string = "WPA2",
channel: number = 0
): Promise {
const encodedSsid = encodeURIComponent(ssid);
const encodedPassword = encodeURIComponent(password);
const cgi = `set_wifi.cgi?ssid=${encodedSsid}&channel=${channel}&authtype=${security}&wpa_psk=${encodedPassword}&enable=1&`;
const success = await this.sendCommand(cgi);
if (!success) return false;
// Wait for confirmation
return new Promise((resolve) => {
const waitTimeout = setTimeout(() => resolve(false), 10000);
const handler: CommandEventListener = (_, cmd, data) => {
if (cmd === 24593) {
clearTimeout(waitTimeout);
this.removeCommandListener(handler);
const params = this.parseResponse(new TextDecoder().decode(data));
resolve(params["result"] === "0");
}
};
this.addCommandListener(handler);
});
}
/**
* Rotate the camera's admin password via `set_users.cgi`.
*
* Use this immediately after pairing to replace the factory default
* (888888 / 88888888) with a per-camera random secret. This is the
* primary defense against the "VUID + factory password → public stream"
* attack: once rotated, possession of the printed serial alone is no
* longer enough to authenticate against the camera.
*
* Preconditions:
* - `login()` has succeeded with the CURRENT password. The CGI relies
* on the established P2P session, plus loginuse/loginpas URL params
* for the credential check (same pattern PTZ uses).
*
* On success:
* - The camera's admin password is now `newPassword`.
* - This client's cached `currentPassword` is updated, so subsequent
* PTZ / settings / video calls continue to work without re-login.
* - Caller MUST persist `newPassword` somewhere durable BEFORE the
* return value resolves true (vault + backend). If we lose the
* value, the camera becomes unreachable until a physical factory
* reset.
*
* Format note: the firmware variants we've tested accept the
* three-user slot form (user1/pwd1/pri1 + user2/user3 cleared). pri=2
* means admin. Other variants want different parameter names —
* if a particular camera fails with result=-1, capture the response
* here and iterate.
*
* @param newPassword The password to set. Caller should generate at
* least 16 random characters; the camera firmware accepts up to ~32.
* @returns true on confirmed success (camera replied result=0), false
* on explicit failure or timeout.
*/
async rotateAdminPassword(newPassword: string): Promise {
if (!this.clientPtr) return false;
if (!newPassword || newPassword.length === 0) {
console.warn("[VStarCam] rotateAdminPassword: empty password rejected");
return false;
}
const enc = encodeURIComponent;
// loginuse/loginpas mirror getPtzBaseParams() — some firmware
// revisions require URL-level auth on setters even after the
// session login. Including them is the safe choice.
const cgi =
`set_users.cgi?loginuse=${enc(this.currentUsername)}&loginpas=${enc(this.currentPassword)}` +
`&user1=admin&pwd1=${enc(newPassword)}&pri1=2` +
`&user2=&pwd2=&pri2=&user3=&pwd3=&pri3=&`;
const sent = await this.sendCommand(cgi);
if (!sent) {
console.warn("[VStarCam] rotateAdminPassword: sendCommand returned false");
return false;
}
// We don't know set_users.cgi's exact response cmd id, so listen for
// any subsequent command response that carries a `result=` field
// (the convention all setter CGIs follow). Anything within ~8s is
// fair game. Binary frames (video, etc.) are filtered by the
// result= substring check.
return new Promise((resolve) => {
let settled = false;
const finish = (ok: boolean) => {
if (settled) return;
settled = true;
clearTimeout(timeoutId);
this.removeCommandListener(handler);
if (ok) {
// Mutate cached creds AFTER caller knows we succeeded — they
// need this for the next CGI call to authenticate.
this.currentPassword = newPassword;
}
resolve(ok);
};
const timeoutId = setTimeout(() => {
console.warn("[VStarCam] rotateAdminPassword: no result in 8s");
finish(false);
}, 8000);
const handler: CommandEventListener = (_, _cmd, data) => {
try {
const str =
typeof data === "string" ? data : new TextDecoder().decode(data);
if (!str.includes("result=")) return;
const params = this.parseResponse(str);
const result = params["result"];
// result=0 → success on every VStarCam setter we've seen.
// Anything else (-1, -2, etc.) is an auth or validation failure.
finish(result === "0");
} catch {
// Parsing failure on a binary frame, etc. Ignore — we'll
// either hit a real ack later or time out.
}
};
this.addCommandListener(handler);
});
}
/**
* Get device information
*/
async getDeviceInfo(): Promise {
try {
// The native module handles credentials and basic command sending.
// It returns a promise that resolves with the device info and responsive: true.
return await VStarCamModule.clientGetDeviceInfo(this.clientPtr);
} catch (e: any) {
console.error("[VStarCam] getDeviceInfo failed:", e);
return null;
}
}
/**
* PTZ Control - Move camera
*/
private getPtzBaseParams(): string {
return `loginuse=${encodeURIComponent(this.currentUsername)}&loginpas=${encodeURIComponent(this.currentPassword)}`;
}
async ptzUp(continuous: boolean = false): Promise {
const cmd = continuous ? "command=0&onestep=0" : "command=0&onestep=1";
return this.sendCommand(`decoder_control.cgi?${this.getPtzBaseParams()}&${cmd}&`);
}
async ptzDown(continuous: boolean = false): Promise {
const cmd = continuous ? "command=2&onestep=0" : "command=2&onestep=1";
return this.sendCommand(`decoder_control.cgi?${this.getPtzBaseParams()}&${cmd}&`);
}
async ptzLeft(continuous: boolean = false): Promise {
const cmd = continuous ? "command=4&onestep=0" : "command=5&onestep=1";
return this.sendCommand(`decoder_control.cgi?${this.getPtzBaseParams()}&${cmd}&`);
}
async ptzRight(continuous: boolean = false): Promise {
const cmd = continuous ? "command=6&onestep=0" : "command=7&onestep=1";
return this.sendCommand(`decoder_control.cgi?${this.getPtzBaseParams()}&${cmd}&`);
}
async ptzStop(): Promise {
return this.sendCommand(`decoder_control.cgi?${this.getPtzBaseParams()}&command=1&onestep=0&`);
}
/**
* Start video stream
* @param streamType 0 = main stream, 1 = sub stream
*/
async startVideo(streamType: number = 0): Promise {
if (!this.clientPtr) return false;
return VStarCamModule.startVideoStream(this.clientPtr, streamType);
}
/**
* Stop video stream
*/
async stopVideo(): Promise {
if (!this.clientPtr) return false;
return VStarCamModule.stopVideoStream(this.clientPtr);
}
/**
* Take a snapshot
*/
async takeSnapshot(): Promise {
if (!this.clientPtr) return null;
return VStarCamModule.takeSnapshot(this.clientPtr);
}
/**
* Disconnect from camera
*/
async disconnect(): Promise {
if (!this.clientPtr) return false;
const result = await VStarCamModule.clientDisconnect(this.clientPtr);
return result;
}
/**
* Destroy client and release resources
*/
async destroy(): Promise {
if (this.clientPtr) {
await VStarCamModule.clientDestroy(this.clientPtr);
this.clientPtr = 0;
}
this.connectionListeners.clear();
this.commandListeners.clear();
this.videoListeners.clear();
}
// Event listener management
addConnectionListener(listener: ConnectionEventListener) {
this.connectionListeners.add(listener);
}
removeConnectionListener(listener: ConnectionEventListener) {
this.connectionListeners.delete(listener);
}
addCommandListener(listener: CommandEventListener) {
this.commandListeners.add(listener);
}
removeCommandListener(listener: CommandEventListener) {
this.commandListeners.delete(listener);
}
addVideoListener(listener: VideoFrameListener) {
this.videoListeners.add(listener);
}
removeVideoListener(listener: VideoFrameListener) {
this.videoListeners.delete(listener);
}
/**
* Get the current Wi-Fi SSID from the phone (Android only)
*/
async getPhoneSSID(): Promise {
if (Platform.OS === 'android') {
try {
return await VStarCamModule.getWiFiSSID();
} catch (e) {
console.warn("Failed to get phone SSID:", e);
return null;
}
}
return null;
}
get isConnected(): boolean {
return this.clientPtr > 0;
}
}
// Export singleton factory
export function createVStarCamClient(): VStarCamClient {
return new VStarCamClient();
}
export { VStarCamClient };
export default VStarCamModule;