@bits-innovate/react-native-vstarcam 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +203 -0
- package/android/build.gradle +56 -0
- package/android/libs/app_p2p_api-5.0.0.aar +0 -0
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/java/com/reactnativevstarcam/VStarCamModule.java +230 -0
- package/android/src/main/java/com/reactnativevstarcam/VStarCamPackage.java +28 -0
- package/ios/VStarCam.h +6 -0
- package/ios/VStarCam.m +170 -0
- package/package.json +46 -0
- package/react-native-vstarcam.podspec +20 -0
- package/src/index.ts +486 -0
package/README.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# react-native-vstarcam
|
|
2
|
+
|
|
3
|
+
React Native bridge for VStarCam P2P SDK - enables direct communication with VStarCam cameras for WiFi configuration, video streaming, PTZ control, and more.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ P2P Connection to VStarCam cameras
|
|
8
|
+
- ✅ WiFi configuration
|
|
9
|
+
- ✅ WiFi network scanning
|
|
10
|
+
- ✅ PTZ control (pan, tilt, zoom)
|
|
11
|
+
- ✅ Video streaming
|
|
12
|
+
- ✅ Snapshot capture
|
|
13
|
+
- ✅ Two-way audio (planned)
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install react-native-vstarcam
|
|
19
|
+
# or
|
|
20
|
+
yarn add react-native-vstarcam
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Expo Users
|
|
24
|
+
|
|
25
|
+
This package requires native code and won't work with Expo Go. You need to use a development build:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx expo prebuild
|
|
29
|
+
npx expo run:android
|
|
30
|
+
# or
|
|
31
|
+
npx expo run:ios
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Android Setup
|
|
35
|
+
|
|
36
|
+
The package automatically links the VStarCam AAR library. No additional setup required.
|
|
37
|
+
|
|
38
|
+
### iOS Setup
|
|
39
|
+
|
|
40
|
+
1. Run `pod install` in your iOS directory
|
|
41
|
+
2. The VStarCam iOS framework needs to be placed in `ios/Frameworks/`
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { createVStarCamClient, ConnectionState } from "react-native-vstarcam";
|
|
47
|
+
|
|
48
|
+
// Create a client
|
|
49
|
+
const client = createVStarCamClient();
|
|
50
|
+
|
|
51
|
+
// Connect to camera
|
|
52
|
+
async function connectToCamera(deviceId: string) {
|
|
53
|
+
// Create client with device ID
|
|
54
|
+
await client.create(deviceId);
|
|
55
|
+
|
|
56
|
+
// Connect via P2P
|
|
57
|
+
const result = await client.connect(true);
|
|
58
|
+
|
|
59
|
+
if (result.state === ConnectionState.ONLINE) {
|
|
60
|
+
console.log("Connected!");
|
|
61
|
+
|
|
62
|
+
// Login
|
|
63
|
+
await client.login("admin", "888888");
|
|
64
|
+
|
|
65
|
+
// Get device info
|
|
66
|
+
const info = await client.getDeviceInfo();
|
|
67
|
+
console.log("Device:", info);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Configure WiFi
|
|
72
|
+
async function configureWiFi() {
|
|
73
|
+
// Scan for networks
|
|
74
|
+
const networks = await client.scanWiFi();
|
|
75
|
+
console.log("Available networks:", networks);
|
|
76
|
+
|
|
77
|
+
// Configure WiFi
|
|
78
|
+
const success = await client.configureWiFi("MyWiFi", "MyPassword", "WPA2", 6);
|
|
79
|
+
|
|
80
|
+
if (success) {
|
|
81
|
+
console.log("WiFi configured successfully!");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// PTZ Control
|
|
86
|
+
async function moveCameraUp() {
|
|
87
|
+
await client.ptzUp(true); // Continuous movement
|
|
88
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
89
|
+
await client.ptzStop();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Cleanup
|
|
93
|
+
async function disconnect() {
|
|
94
|
+
await client.disconnect();
|
|
95
|
+
await client.destroy();
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## API Reference
|
|
100
|
+
|
|
101
|
+
### VStarCamClient
|
|
102
|
+
|
|
103
|
+
#### Methods
|
|
104
|
+
|
|
105
|
+
| Method | Description |
|
|
106
|
+
| ----------------------------------------------- | -------------------------------- |
|
|
107
|
+
| `create(deviceId)` | Create a P2P client for a device |
|
|
108
|
+
| `connect(lanScan?, serverParam?, connectType?)` | Connect to camera |
|
|
109
|
+
| `login(username?, password?)` | Login to camera |
|
|
110
|
+
| `disconnect()` | Disconnect from camera |
|
|
111
|
+
| `destroy()` | Cleanup and release resources |
|
|
112
|
+
|
|
113
|
+
#### WiFi Methods
|
|
114
|
+
|
|
115
|
+
| Method | Description |
|
|
116
|
+
| ---------------------------------------------------- | -------------------------------- |
|
|
117
|
+
| `scanWiFi()` | Scan for available WiFi networks |
|
|
118
|
+
| `configureWiFi(ssid, password, security?, channel?)` | Configure camera WiFi |
|
|
119
|
+
|
|
120
|
+
#### PTZ Methods
|
|
121
|
+
|
|
122
|
+
| Method | Description |
|
|
123
|
+
| ----------------------- | -------------------- |
|
|
124
|
+
| `ptzUp(continuous?)` | Move camera up |
|
|
125
|
+
| `ptzDown(continuous?)` | Move camera down |
|
|
126
|
+
| `ptzLeft(continuous?)` | Move camera left |
|
|
127
|
+
| `ptzRight(continuous?)` | Move camera right |
|
|
128
|
+
| `ptzStop()` | Stop camera movement |
|
|
129
|
+
|
|
130
|
+
#### Video Methods
|
|
131
|
+
|
|
132
|
+
| Method | Description |
|
|
133
|
+
| ------------------------- | ------------------ |
|
|
134
|
+
| `startVideo(streamType?)` | Start video stream |
|
|
135
|
+
| `stopVideo()` | Stop video stream |
|
|
136
|
+
| `takeSnapshot()` | Capture a snapshot |
|
|
137
|
+
|
|
138
|
+
### Enums
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
enum ConnectionState {
|
|
142
|
+
INVALID_CLIENT,
|
|
143
|
+
CONNECTING,
|
|
144
|
+
INITIALIZING,
|
|
145
|
+
ONLINE,
|
|
146
|
+
CONNECT_FAILED,
|
|
147
|
+
DISCONNECTED,
|
|
148
|
+
INVALID_ID,
|
|
149
|
+
OFFLINE,
|
|
150
|
+
TIMEOUT,
|
|
151
|
+
MAX_SESSION,
|
|
152
|
+
MAX,
|
|
153
|
+
REMOVE_CLOSE,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
enum ConnectionMode {
|
|
157
|
+
NONE,
|
|
158
|
+
P2P,
|
|
159
|
+
RELAY,
|
|
160
|
+
SOCKET,
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Events
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// Connection state changes
|
|
168
|
+
client.addConnectionListener((clientId, state) => {
|
|
169
|
+
console.log("Connection state:", ConnectionState[state]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Command responses
|
|
173
|
+
client.addCommandListener((clientId, command, data) => {
|
|
174
|
+
console.log("Command:", command, "Data:", data);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Video frames
|
|
178
|
+
client.addVideoListener((clientId, frame, width, height, timestamp) => {
|
|
179
|
+
// Handle video frame
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## VStarCam Device ID
|
|
184
|
+
|
|
185
|
+
The Device ID (DID) is typically printed on the camera or shown in the VStarCam app. Format: `VSTA-XXXXXX-XXXXX`
|
|
186
|
+
|
|
187
|
+
## Troubleshooting
|
|
188
|
+
|
|
189
|
+
### Connection fails
|
|
190
|
+
|
|
191
|
+
1. Ensure the camera is powered on and in pairing mode
|
|
192
|
+
2. Check that your phone is on the same network
|
|
193
|
+
3. Verify the Device ID is correct
|
|
194
|
+
|
|
195
|
+
### WiFi configuration fails
|
|
196
|
+
|
|
197
|
+
1. Ensure you're connected to the camera first
|
|
198
|
+
2. Check WiFi password is correct
|
|
199
|
+
3. Make sure the network is 2.4GHz (most VStarCam cameras don't support 5GHz)
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
repositories {
|
|
3
|
+
google()
|
|
4
|
+
mavenCentral()
|
|
5
|
+
}
|
|
6
|
+
dependencies {
|
|
7
|
+
classpath 'com.android.tools.build:gradle:7.4.2'
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
apply plugin: 'com.android.library'
|
|
12
|
+
|
|
13
|
+
android {
|
|
14
|
+
namespace "com.reactnativevstarcam"
|
|
15
|
+
compileSdkVersion 34
|
|
16
|
+
|
|
17
|
+
defaultConfig {
|
|
18
|
+
minSdkVersion 21
|
|
19
|
+
targetSdkVersion 34
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
buildTypes {
|
|
23
|
+
release {
|
|
24
|
+
minifyEnabled false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
lintOptions {
|
|
29
|
+
disable 'GradleCompatible'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
compileOptions {
|
|
33
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
34
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
repositories {
|
|
39
|
+
mavenCentral()
|
|
40
|
+
google()
|
|
41
|
+
// Look for AAR in the app's libs folder (copied by expo config plugin)
|
|
42
|
+
flatDir {
|
|
43
|
+
dirs rootProject.file('app/libs')
|
|
44
|
+
}
|
|
45
|
+
// Also check local libs folder
|
|
46
|
+
flatDir {
|
|
47
|
+
dirs 'libs'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
dependencies {
|
|
52
|
+
implementation "com.facebook.react:react-native:+"
|
|
53
|
+
implementation(name: 'app_p2p_api-5.0.0', ext: 'aar')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
+
package="com.reactnativevstarcam">
|
|
3
|
+
|
|
4
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
5
|
+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
6
|
+
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
|
7
|
+
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
|
8
|
+
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
|
9
|
+
|
|
10
|
+
</manifest>
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
package com.reactnativevstarcam;
|
|
2
|
+
|
|
3
|
+
import android.util.Log;
|
|
4
|
+
|
|
5
|
+
import androidx.annotation.NonNull;
|
|
6
|
+
import androidx.annotation.Nullable;
|
|
7
|
+
|
|
8
|
+
import com.facebook.react.bridge.Arguments;
|
|
9
|
+
import com.facebook.react.bridge.Promise;
|
|
10
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
11
|
+
import com.facebook.react.bridge.ReactContext;
|
|
12
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
13
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
14
|
+
import com.facebook.react.bridge.WritableMap;
|
|
15
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
|
16
|
+
|
|
17
|
+
import com.vstarcam.app_p2p_api.AppP2PApi;
|
|
18
|
+
import com.vstarcam.app_p2p_api.ClientConnectState;
|
|
19
|
+
import com.vstarcam.app_p2p_api.ConnectListener;
|
|
20
|
+
import com.vstarcam.app_p2p_api.CommandListener;
|
|
21
|
+
|
|
22
|
+
import java.util.HashMap;
|
|
23
|
+
import java.util.Map;
|
|
24
|
+
|
|
25
|
+
public class VStarCamModule extends ReactContextBaseJavaModule {
|
|
26
|
+
private static final String TAG = "VStarCamModule";
|
|
27
|
+
private static final String MODULE_NAME = "VStarCam";
|
|
28
|
+
|
|
29
|
+
private final ReactApplicationContext reactContext;
|
|
30
|
+
private AppP2PApi p2pApi;
|
|
31
|
+
private Map<Integer, ConnectListener> connectListeners = new HashMap<>();
|
|
32
|
+
private Map<Integer, CommandListener> commandListeners = new HashMap<>();
|
|
33
|
+
|
|
34
|
+
public VStarCamModule(ReactApplicationContext reactContext) {
|
|
35
|
+
super(reactContext);
|
|
36
|
+
this.reactContext = reactContext;
|
|
37
|
+
this.p2pApi = AppP2PApi.getInstance();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@Override
|
|
41
|
+
@NonNull
|
|
42
|
+
public String getName() {
|
|
43
|
+
return MODULE_NAME;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private void sendEvent(String eventName, @Nullable WritableMap params) {
|
|
47
|
+
reactContext
|
|
48
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
|
49
|
+
.emit(eventName, params);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a P2P client for a device
|
|
54
|
+
*/
|
|
55
|
+
@ReactMethod
|
|
56
|
+
public void clientCreate(String deviceId, Promise promise) {
|
|
57
|
+
try {
|
|
58
|
+
int clientPtr = p2pApi.clientCreate(deviceId, null);
|
|
59
|
+
|
|
60
|
+
if (clientPtr > 0) {
|
|
61
|
+
// Set up connection listener
|
|
62
|
+
ConnectListener connectListener = (state) -> {
|
|
63
|
+
WritableMap params = Arguments.createMap();
|
|
64
|
+
params.putInt("clientId", clientPtr);
|
|
65
|
+
params.putInt("state", state.ordinal());
|
|
66
|
+
sendEvent("onConnectionStateChanged", params);
|
|
67
|
+
};
|
|
68
|
+
connectListeners.put(clientPtr, connectListener);
|
|
69
|
+
p2pApi.setConnectListener(clientPtr, connectListener);
|
|
70
|
+
|
|
71
|
+
// Set up command listener
|
|
72
|
+
CommandListener commandListener = (cmd, data) -> {
|
|
73
|
+
WritableMap params = Arguments.createMap();
|
|
74
|
+
params.putInt("clientId", clientPtr);
|
|
75
|
+
params.putInt("command", cmd);
|
|
76
|
+
params.putString("data", new String(data));
|
|
77
|
+
sendEvent("onCommandReceived", params);
|
|
78
|
+
};
|
|
79
|
+
commandListeners.put(clientPtr, commandListener);
|
|
80
|
+
p2pApi.setCommandListener(clientPtr, commandListener);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
promise.resolve(clientPtr);
|
|
84
|
+
} catch (Exception e) {
|
|
85
|
+
Log.e(TAG, "clientCreate failed", e);
|
|
86
|
+
promise.reject("CREATE_ERROR", e.getMessage());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Connect to camera via P2P
|
|
92
|
+
*/
|
|
93
|
+
@ReactMethod
|
|
94
|
+
public void clientConnect(int clientPtr, boolean lanScan, String serverParam, int connectType, Promise promise) {
|
|
95
|
+
try {
|
|
96
|
+
ClientConnectState state = p2pApi.clientConnect(clientPtr, lanScan, serverParam, connectType, 0);
|
|
97
|
+
promise.resolve(state.ordinal());
|
|
98
|
+
} catch (Exception e) {
|
|
99
|
+
Log.e(TAG, "clientConnect failed", e);
|
|
100
|
+
promise.reject("CONNECT_ERROR", e.getMessage());
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Login to camera
|
|
106
|
+
*/
|
|
107
|
+
@ReactMethod
|
|
108
|
+
public void clientLogin(int clientPtr, String username, String password, Promise promise) {
|
|
109
|
+
try {
|
|
110
|
+
boolean success = p2pApi.clientLogin(clientPtr, username, password);
|
|
111
|
+
promise.resolve(success);
|
|
112
|
+
} catch (Exception e) {
|
|
113
|
+
Log.e(TAG, "clientLogin failed", e);
|
|
114
|
+
promise.reject("LOGIN_ERROR", e.getMessage());
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Send CGI command
|
|
120
|
+
*/
|
|
121
|
+
@ReactMethod
|
|
122
|
+
public void clientWriteCgi(int clientPtr, String cgi, int timeout, Promise promise) {
|
|
123
|
+
try {
|
|
124
|
+
boolean success = p2pApi.clientWriteCgi(clientPtr, cgi, timeout);
|
|
125
|
+
promise.resolve(success);
|
|
126
|
+
} catch (Exception e) {
|
|
127
|
+
Log.e(TAG, "clientWriteCgi failed", e);
|
|
128
|
+
promise.reject("CGI_ERROR", e.getMessage());
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Disconnect from camera
|
|
134
|
+
*/
|
|
135
|
+
@ReactMethod
|
|
136
|
+
public void clientDisconnect(int clientPtr, Promise promise) {
|
|
137
|
+
try {
|
|
138
|
+
boolean success = p2pApi.clientDisconnect(clientPtr);
|
|
139
|
+
promise.resolve(success);
|
|
140
|
+
} catch (Exception e) {
|
|
141
|
+
Log.e(TAG, "clientDisconnect failed", e);
|
|
142
|
+
promise.reject("DISCONNECT_ERROR", e.getMessage());
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Destroy client and cleanup
|
|
148
|
+
*/
|
|
149
|
+
@ReactMethod
|
|
150
|
+
public void clientDestroy(int clientPtr, Promise promise) {
|
|
151
|
+
try {
|
|
152
|
+
// Remove listeners
|
|
153
|
+
p2pApi.removeConnectListener(clientPtr);
|
|
154
|
+
p2pApi.removeCommandListener(clientPtr);
|
|
155
|
+
connectListeners.remove(clientPtr);
|
|
156
|
+
commandListeners.remove(clientPtr);
|
|
157
|
+
|
|
158
|
+
p2pApi.clientDestroy(clientPtr);
|
|
159
|
+
promise.resolve(true);
|
|
160
|
+
} catch (Exception e) {
|
|
161
|
+
Log.e(TAG, "clientDestroy failed", e);
|
|
162
|
+
promise.reject("DESTROY_ERROR", e.getMessage());
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Start video stream
|
|
168
|
+
*/
|
|
169
|
+
@ReactMethod
|
|
170
|
+
public void startVideoStream(int clientPtr, int streamType, Promise promise) {
|
|
171
|
+
try {
|
|
172
|
+
// Video streaming requires additional setup with AppPlayerPlugin
|
|
173
|
+
// This is a placeholder - actual implementation needs video decoder
|
|
174
|
+
String cgi = streamType == 0 ?
|
|
175
|
+
"videostream.cgi?resolution=32&rate=0&" :
|
|
176
|
+
"videostream.cgi?resolution=8&rate=0&";
|
|
177
|
+
boolean success = p2pApi.clientWriteCgi(clientPtr, cgi, 5);
|
|
178
|
+
promise.resolve(success);
|
|
179
|
+
} catch (Exception e) {
|
|
180
|
+
Log.e(TAG, "startVideoStream failed", e);
|
|
181
|
+
promise.reject("VIDEO_ERROR", e.getMessage());
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Stop video stream
|
|
187
|
+
*/
|
|
188
|
+
@ReactMethod
|
|
189
|
+
public void stopVideoStream(int clientPtr, Promise promise) {
|
|
190
|
+
try {
|
|
191
|
+
// Stop video streaming
|
|
192
|
+
promise.resolve(true);
|
|
193
|
+
} catch (Exception e) {
|
|
194
|
+
Log.e(TAG, "stopVideoStream failed", e);
|
|
195
|
+
promise.reject("VIDEO_ERROR", e.getMessage());
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Take snapshot
|
|
201
|
+
*/
|
|
202
|
+
@ReactMethod
|
|
203
|
+
public void takeSnapshot(int clientPtr, Promise promise) {
|
|
204
|
+
try {
|
|
205
|
+
boolean success = p2pApi.clientWriteCgi(clientPtr, "snapshot.cgi?", 10);
|
|
206
|
+
// Note: Actual snapshot data comes via command listener
|
|
207
|
+
promise.resolve(success);
|
|
208
|
+
} catch (Exception e) {
|
|
209
|
+
Log.e(TAG, "takeSnapshot failed", e);
|
|
210
|
+
promise.reject("SNAPSHOT_ERROR", e.getMessage());
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check connection mode
|
|
216
|
+
*/
|
|
217
|
+
@ReactMethod
|
|
218
|
+
public void clientCheckMode(int clientPtr, Promise promise) {
|
|
219
|
+
try {
|
|
220
|
+
// This would return the connection mode (P2P, Relay, etc.)
|
|
221
|
+
WritableMap result = Arguments.createMap();
|
|
222
|
+
result.putBoolean("success", true);
|
|
223
|
+
result.putInt("mode", 1); // P2P mode
|
|
224
|
+
promise.resolve(result);
|
|
225
|
+
} catch (Exception e) {
|
|
226
|
+
Log.e(TAG, "clientCheckMode failed", e);
|
|
227
|
+
promise.reject("MODE_ERROR", e.getMessage());
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package com.reactnativevstarcam;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.NonNull;
|
|
4
|
+
|
|
5
|
+
import com.facebook.react.ReactPackage;
|
|
6
|
+
import com.facebook.react.bridge.NativeModule;
|
|
7
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
8
|
+
import com.facebook.react.uimanager.ViewManager;
|
|
9
|
+
|
|
10
|
+
import java.util.ArrayList;
|
|
11
|
+
import java.util.Collections;
|
|
12
|
+
import java.util.List;
|
|
13
|
+
|
|
14
|
+
public class VStarCamPackage implements ReactPackage {
|
|
15
|
+
@NonNull
|
|
16
|
+
@Override
|
|
17
|
+
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
|
|
18
|
+
List<NativeModule> modules = new ArrayList<>();
|
|
19
|
+
modules.add(new VStarCamModule(reactContext));
|
|
20
|
+
return modules;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@NonNull
|
|
24
|
+
@Override
|
|
25
|
+
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
|
|
26
|
+
return Collections.emptyList();
|
|
27
|
+
}
|
|
28
|
+
}
|
package/ios/VStarCam.h
ADDED
package/ios/VStarCam.m
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#import "VStarCam.h"
|
|
2
|
+
#import <React/RCTLog.h>
|
|
3
|
+
|
|
4
|
+
// Note: iOS implementation requires the VStarCam iOS SDK framework
|
|
5
|
+
// The framework should be placed in ios/Frameworks/ directory
|
|
6
|
+
// For now, this is a stub implementation
|
|
7
|
+
|
|
8
|
+
@implementation VStarCam
|
|
9
|
+
{
|
|
10
|
+
bool hasListeners;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
RCT_EXPORT_MODULE()
|
|
14
|
+
|
|
15
|
+
- (NSArray<NSString *> *)supportedEvents {
|
|
16
|
+
return @[
|
|
17
|
+
@"onConnectionStateChanged",
|
|
18
|
+
@"onCommandReceived",
|
|
19
|
+
@"onVideoFrame"
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
- (void)startObserving {
|
|
24
|
+
hasListeners = YES;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
- (void)stopObserving {
|
|
28
|
+
hasListeners = NO;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#pragma mark - P2P Client Methods
|
|
32
|
+
|
|
33
|
+
RCT_EXPORT_METHOD(clientCreate:(NSString *)deviceId
|
|
34
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
35
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
36
|
+
{
|
|
37
|
+
// TODO: Implement with VStarCam iOS SDK
|
|
38
|
+
// This requires the iOS P2P library from VStarCam
|
|
39
|
+
|
|
40
|
+
RCTLogInfo(@"VStarCam: clientCreate called with deviceId: %@", deviceId);
|
|
41
|
+
|
|
42
|
+
// Placeholder - return mock client pointer
|
|
43
|
+
// In real implementation, call VStarCam iOS SDK
|
|
44
|
+
NSNumber *clientPtr = @(1);
|
|
45
|
+
resolve(clientPtr);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
RCT_EXPORT_METHOD(clientConnect:(nonnull NSNumber *)clientPtr
|
|
49
|
+
lanScan:(BOOL)lanScan
|
|
50
|
+
serverParam:(NSString *)serverParam
|
|
51
|
+
connectType:(nonnull NSNumber *)connectType
|
|
52
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
53
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
54
|
+
{
|
|
55
|
+
RCTLogInfo(@"VStarCam: clientConnect called");
|
|
56
|
+
|
|
57
|
+
// Placeholder - return ONLINE state (3)
|
|
58
|
+
resolve(@(3));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
RCT_EXPORT_METHOD(clientLogin:(nonnull NSNumber *)clientPtr
|
|
62
|
+
username:(NSString *)username
|
|
63
|
+
password:(NSString *)password
|
|
64
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
65
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
66
|
+
{
|
|
67
|
+
RCTLogInfo(@"VStarCam: clientLogin called with user: %@", username);
|
|
68
|
+
|
|
69
|
+
// Placeholder
|
|
70
|
+
resolve(@YES);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
RCT_EXPORT_METHOD(clientWriteCgi:(nonnull NSNumber *)clientPtr
|
|
74
|
+
cgi:(NSString *)cgi
|
|
75
|
+
timeout:(nonnull NSNumber *)timeout
|
|
76
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
77
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
78
|
+
{
|
|
79
|
+
RCTLogInfo(@"VStarCam: clientWriteCgi called with cgi: %@", cgi);
|
|
80
|
+
|
|
81
|
+
// Placeholder
|
|
82
|
+
resolve(@YES);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
RCT_EXPORT_METHOD(clientDisconnect:(nonnull NSNumber *)clientPtr
|
|
86
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
87
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
88
|
+
{
|
|
89
|
+
RCTLogInfo(@"VStarCam: clientDisconnect called");
|
|
90
|
+
|
|
91
|
+
// Placeholder
|
|
92
|
+
resolve(@YES);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
RCT_EXPORT_METHOD(clientDestroy:(nonnull NSNumber *)clientPtr
|
|
96
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
97
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
98
|
+
{
|
|
99
|
+
RCTLogInfo(@"VStarCam: clientDestroy called");
|
|
100
|
+
|
|
101
|
+
// Placeholder
|
|
102
|
+
resolve(@YES);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
RCT_EXPORT_METHOD(startVideoStream:(nonnull NSNumber *)clientPtr
|
|
106
|
+
streamType:(nonnull NSNumber *)streamType
|
|
107
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
108
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
109
|
+
{
|
|
110
|
+
RCTLogInfo(@"VStarCam: startVideoStream called");
|
|
111
|
+
|
|
112
|
+
// Placeholder
|
|
113
|
+
resolve(@YES);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
RCT_EXPORT_METHOD(stopVideoStream:(nonnull NSNumber *)clientPtr
|
|
117
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
118
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
119
|
+
{
|
|
120
|
+
RCTLogInfo(@"VStarCam: stopVideoStream called");
|
|
121
|
+
|
|
122
|
+
// Placeholder
|
|
123
|
+
resolve(@YES);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
RCT_EXPORT_METHOD(takeSnapshot:(nonnull NSNumber *)clientPtr
|
|
127
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
128
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
129
|
+
{
|
|
130
|
+
RCTLogInfo(@"VStarCam: takeSnapshot called");
|
|
131
|
+
|
|
132
|
+
// Placeholder
|
|
133
|
+
resolve(@YES);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
RCT_EXPORT_METHOD(clientCheckMode:(nonnull NSNumber *)clientPtr
|
|
137
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
138
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
139
|
+
{
|
|
140
|
+
RCTLogInfo(@"VStarCam: clientCheckMode called");
|
|
141
|
+
|
|
142
|
+
NSDictionary *result = @{
|
|
143
|
+
@"success": @YES,
|
|
144
|
+
@"mode": @1
|
|
145
|
+
};
|
|
146
|
+
resolve(result);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#pragma mark - Event Sending
|
|
150
|
+
|
|
151
|
+
- (void)sendConnectionEvent:(NSNumber *)clientId state:(NSNumber *)state {
|
|
152
|
+
if (hasListeners) {
|
|
153
|
+
[self sendEventWithName:@"onConnectionStateChanged" body:@{
|
|
154
|
+
@"clientId": clientId,
|
|
155
|
+
@"state": state
|
|
156
|
+
}];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
- (void)sendCommandEvent:(NSNumber *)clientId command:(NSNumber *)command data:(NSString *)data {
|
|
161
|
+
if (hasListeners) {
|
|
162
|
+
[self sendEventWithName:@"onCommandReceived" body:@{
|
|
163
|
+
@"clientId": clientId,
|
|
164
|
+
@"command": command,
|
|
165
|
+
@"data": data
|
|
166
|
+
}];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@end
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bits-innovate/react-native-vstarcam",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React Native bridge for VStarCam P2P SDK",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"react-native": "src/index.ts",
|
|
8
|
+
"source": "src/index.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"android",
|
|
12
|
+
"ios",
|
|
13
|
+
"*.podspec",
|
|
14
|
+
"!android/build",
|
|
15
|
+
"!ios/build",
|
|
16
|
+
"!**/__tests__",
|
|
17
|
+
"!**/__fixtures__",
|
|
18
|
+
"!**/__mocks__",
|
|
19
|
+
"!**/.*"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"typescript": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"react-native",
|
|
26
|
+
"vstarcam",
|
|
27
|
+
"p2p",
|
|
28
|
+
"camera",
|
|
29
|
+
"surveillance",
|
|
30
|
+
"security"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/Roviagate-Technology/my-watchman-cam.git"
|
|
35
|
+
},
|
|
36
|
+
"author": "Roviagate Technology <henrique.crusoe@bits-innovate.com>",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/Roviagate-Technology/my-watchman-cam/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/Roviagate-Technology/my-watchman-cam#readme",
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"react": "*",
|
|
44
|
+
"react-native": "*"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "react-native-vstarcam"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => "12.0" }
|
|
14
|
+
s.source = { :git => "https://github.com/Roviagate-Technology/my-watchman-cam.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
17
|
+
s.vendored_frameworks = "ios/Frameworks/*.xcframework"
|
|
18
|
+
|
|
19
|
+
s.dependency "React-Core"
|
|
20
|
+
end
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VStarCam React Native SDK
|
|
3
|
+
*
|
|
4
|
+
* This module provides a React Native bridge to the VStarCam P2P SDK,
|
|
5
|
+
* enabling direct communication with VStarCam cameras for:
|
|
6
|
+
* - P2P connection establishment
|
|
7
|
+
* - WiFi configuration
|
|
8
|
+
* - Video streaming
|
|
9
|
+
* - PTZ control
|
|
10
|
+
* - Camera settings
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { NativeModules, NativeEventEmitter, Platform } from "react-native";
|
|
14
|
+
|
|
15
|
+
const LINKING_ERROR =
|
|
16
|
+
`The package 'react-native-vstarcam' doesn't seem to be linked. Make sure: \n\n` +
|
|
17
|
+
Platform.select({ ios: "- You have run 'pod install'\n", default: "" }) +
|
|
18
|
+
"- You rebuilt the app after installing the package\n" +
|
|
19
|
+
"- You are not using Expo Go (requires development build)";
|
|
20
|
+
|
|
21
|
+
const VStarCamModule = NativeModules.VStarCam
|
|
22
|
+
? NativeModules.VStarCam
|
|
23
|
+
: new Proxy(
|
|
24
|
+
{},
|
|
25
|
+
{
|
|
26
|
+
get() {
|
|
27
|
+
throw new Error(LINKING_ERROR);
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Connection states
|
|
33
|
+
export enum ConnectionState {
|
|
34
|
+
INVALID_CLIENT = 0,
|
|
35
|
+
CONNECTING = 1,
|
|
36
|
+
INITIALIZING = 2,
|
|
37
|
+
ONLINE = 3,
|
|
38
|
+
CONNECT_FAILED = 4,
|
|
39
|
+
DISCONNECTED = 5,
|
|
40
|
+
INVALID_ID = 6,
|
|
41
|
+
OFFLINE = 7,
|
|
42
|
+
TIMEOUT = 8,
|
|
43
|
+
MAX_SESSION = 9,
|
|
44
|
+
MAX = 10,
|
|
45
|
+
REMOVE_CLOSE = 11,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Connection modes
|
|
49
|
+
export enum ConnectionMode {
|
|
50
|
+
NONE = 0,
|
|
51
|
+
P2P = 1, // Direct P2P connection
|
|
52
|
+
RELAY = 2, // Relay through cloud
|
|
53
|
+
SOCKET = 3, // Socket connection
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Channel types for P2P communication
|
|
57
|
+
export enum ChannelType {
|
|
58
|
+
CMD = 0, // Command channel
|
|
59
|
+
VIDEO = 1, // Video stream channel
|
|
60
|
+
AUDIO = 2, // Audio receive channel
|
|
61
|
+
TALK = 3, // Audio send (two-way audio)
|
|
62
|
+
PLAYBACK = 4, // Playback channel
|
|
63
|
+
ALARM = 5, // Alarm notification channel
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// WiFi network info
|
|
67
|
+
export interface WiFiNetwork {
|
|
68
|
+
ssid: string;
|
|
69
|
+
mac: string;
|
|
70
|
+
security: string;
|
|
71
|
+
signalStrength: number;
|
|
72
|
+
channel: number;
|
|
73
|
+
mode: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Device info
|
|
77
|
+
export interface DeviceInfo {
|
|
78
|
+
serialNumber: string;
|
|
79
|
+
model: string;
|
|
80
|
+
firmware: string;
|
|
81
|
+
manufacturer: string;
|
|
82
|
+
features: {
|
|
83
|
+
ptz: boolean;
|
|
84
|
+
audio: boolean;
|
|
85
|
+
twoWayAudio: boolean;
|
|
86
|
+
nightVision: boolean;
|
|
87
|
+
motionDetection: boolean;
|
|
88
|
+
wifi: boolean;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Connection result
|
|
93
|
+
export interface ConnectionResult {
|
|
94
|
+
success: boolean;
|
|
95
|
+
state: ConnectionState;
|
|
96
|
+
mode?: ConnectionMode;
|
|
97
|
+
error?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Command result
|
|
101
|
+
export interface CommandResult {
|
|
102
|
+
success: boolean;
|
|
103
|
+
data?: Record<string, string>;
|
|
104
|
+
error?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Event types
|
|
108
|
+
export type ConnectionEventListener = (
|
|
109
|
+
clientId: number,
|
|
110
|
+
state: ConnectionState
|
|
111
|
+
) => void;
|
|
112
|
+
|
|
113
|
+
export type CommandEventListener = (
|
|
114
|
+
clientId: number,
|
|
115
|
+
command: number,
|
|
116
|
+
data: Uint8Array
|
|
117
|
+
) => void;
|
|
118
|
+
|
|
119
|
+
export type VideoFrameListener = (
|
|
120
|
+
clientId: number,
|
|
121
|
+
frame: Uint8Array,
|
|
122
|
+
width: number,
|
|
123
|
+
height: number,
|
|
124
|
+
timestamp: number
|
|
125
|
+
) => void;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* VStarCam P2P Client
|
|
129
|
+
*
|
|
130
|
+
* Main class for interacting with VStarCam cameras
|
|
131
|
+
*/
|
|
132
|
+
class VStarCamClient {
|
|
133
|
+
private clientPtr: number = 0;
|
|
134
|
+
private eventEmitter: NativeEventEmitter;
|
|
135
|
+
private connectionListeners: Set<ConnectionEventListener> = new Set();
|
|
136
|
+
private commandListeners: Set<CommandEventListener> = new Set();
|
|
137
|
+
private videoListeners: Set<VideoFrameListener> = new Set();
|
|
138
|
+
|
|
139
|
+
constructor() {
|
|
140
|
+
this.eventEmitter = new NativeEventEmitter(VStarCamModule);
|
|
141
|
+
this.setupEventListeners();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private setupEventListeners() {
|
|
145
|
+
this.eventEmitter.addListener("onConnectionStateChanged", (event) => {
|
|
146
|
+
if (event.clientId === this.clientPtr) {
|
|
147
|
+
this.connectionListeners.forEach((listener) => {
|
|
148
|
+
listener(event.clientId, event.state);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.eventEmitter.addListener("onCommandReceived", (event) => {
|
|
154
|
+
if (event.clientId === this.clientPtr) {
|
|
155
|
+
this.commandListeners.forEach((listener) => {
|
|
156
|
+
listener(event.clientId, event.command, event.data);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
this.eventEmitter.addListener("onVideoFrame", (event) => {
|
|
162
|
+
if (event.clientId === this.clientPtr) {
|
|
163
|
+
this.videoListeners.forEach((listener) => {
|
|
164
|
+
listener(
|
|
165
|
+
event.clientId,
|
|
166
|
+
event.frame,
|
|
167
|
+
event.width,
|
|
168
|
+
event.height,
|
|
169
|
+
event.timestamp
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Create a P2P client for a device
|
|
178
|
+
* @param deviceId The device ID (DID) from VStarCam
|
|
179
|
+
* @returns Client pointer (used internally)
|
|
180
|
+
*/
|
|
181
|
+
async create(deviceId: string): Promise<number> {
|
|
182
|
+
this.clientPtr = await VStarCamModule.clientCreate(deviceId);
|
|
183
|
+
return this.clientPtr;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Connect to the camera via P2P
|
|
188
|
+
* @param lanScan Whether to scan LAN for the device
|
|
189
|
+
* @param serverParam Server connection parameters
|
|
190
|
+
* @param connectType Connection type (0 = P2P, 1 = Relay)
|
|
191
|
+
*/
|
|
192
|
+
async connect(
|
|
193
|
+
lanScan: boolean = true,
|
|
194
|
+
serverParam: string = "",
|
|
195
|
+
connectType: number = 0
|
|
196
|
+
): Promise<ConnectionResult> {
|
|
197
|
+
if (!this.clientPtr) {
|
|
198
|
+
return {
|
|
199
|
+
success: false,
|
|
200
|
+
state: ConnectionState.INVALID_CLIENT,
|
|
201
|
+
error: "Client not created",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const state = await VStarCamModule.clientConnect(
|
|
206
|
+
this.clientPtr,
|
|
207
|
+
lanScan,
|
|
208
|
+
serverParam,
|
|
209
|
+
connectType
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
success: state === ConnectionState.ONLINE,
|
|
214
|
+
state,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Login to the camera
|
|
220
|
+
* @param username Camera username (default: admin)
|
|
221
|
+
* @param password Camera password (default: 888888)
|
|
222
|
+
*/
|
|
223
|
+
async login(
|
|
224
|
+
username: string = "admin",
|
|
225
|
+
password: string = "888888"
|
|
226
|
+
): Promise<boolean> {
|
|
227
|
+
if (!this.clientPtr) return false;
|
|
228
|
+
return VStarCamModule.clientLogin(this.clientPtr, username, password);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Send a CGI command to the camera
|
|
233
|
+
* @param cgi The CGI command (e.g., "get_status.cgi?")
|
|
234
|
+
* @param timeout Timeout in seconds
|
|
235
|
+
*/
|
|
236
|
+
async sendCommand(cgi: string, timeout: number = 5): Promise<boolean> {
|
|
237
|
+
if (!this.clientPtr) return false;
|
|
238
|
+
return VStarCamModule.clientWriteCgi(this.clientPtr, cgi, timeout);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Scan for available WiFi networks
|
|
243
|
+
*/
|
|
244
|
+
async scanWiFi(): Promise<WiFiNetwork[]> {
|
|
245
|
+
if (!this.clientPtr) return [];
|
|
246
|
+
|
|
247
|
+
const success = await this.sendCommand("wifi_scan.cgi?");
|
|
248
|
+
if (!success) return [];
|
|
249
|
+
|
|
250
|
+
// Wait for response
|
|
251
|
+
return new Promise((resolve) => {
|
|
252
|
+
const timeout = setTimeout(() => resolve([]), 10000);
|
|
253
|
+
|
|
254
|
+
const handler: CommandEventListener = (_, cmd, data) => {
|
|
255
|
+
if (cmd === 24618 || cmd === 24584) {
|
|
256
|
+
clearTimeout(timeout);
|
|
257
|
+
this.removeCommandListener(handler);
|
|
258
|
+
// Parse WiFi list from data
|
|
259
|
+
const networks = this.parseWiFiList(data);
|
|
260
|
+
resolve(networks);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
this.addCommandListener(handler);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private parseWiFiList(data: Uint8Array): WiFiNetwork[] {
|
|
269
|
+
// Implementation to parse CGI response
|
|
270
|
+
const str = new TextDecoder().decode(data);
|
|
271
|
+
const networks: WiFiNetwork[] = [];
|
|
272
|
+
|
|
273
|
+
// Parse key=value pairs
|
|
274
|
+
const params = this.parseResponse(str);
|
|
275
|
+
const apNumber = parseInt(params["ap_number"] || "0", 10);
|
|
276
|
+
|
|
277
|
+
for (let i = 0; i < apNumber; i++) {
|
|
278
|
+
networks.push({
|
|
279
|
+
ssid: params[`ap_ssid[${i}]`] || "",
|
|
280
|
+
mac: params[`ap_mac[${i}]`] || "",
|
|
281
|
+
security: params[`ap_security[${i}]`] || "",
|
|
282
|
+
signalStrength: parseInt(params[`ap_dbm0[${i}]`] || "0", 10),
|
|
283
|
+
channel: parseInt(params[`ap_channel[${i}]`] || "0", 10),
|
|
284
|
+
mode: params[`ap_mode[${i}]`] || "",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return networks;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private parseResponse(str: string): Record<string, string> {
|
|
292
|
+
const result: Record<string, string> = {};
|
|
293
|
+
const pairs = str.split(";");
|
|
294
|
+
for (const pair of pairs) {
|
|
295
|
+
const [key, value] = pair.split("=");
|
|
296
|
+
if (key && value) {
|
|
297
|
+
result[key.trim()] = value.trim();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Configure camera WiFi
|
|
305
|
+
* @param ssid WiFi network name
|
|
306
|
+
* @param password WiFi password
|
|
307
|
+
* @param security Security type (e.g., "WPA2")
|
|
308
|
+
* @param channel WiFi channel
|
|
309
|
+
*/
|
|
310
|
+
async configureWiFi(
|
|
311
|
+
ssid: string,
|
|
312
|
+
password: string,
|
|
313
|
+
security: string = "WPA2",
|
|
314
|
+
channel: number = 0
|
|
315
|
+
): Promise<boolean> {
|
|
316
|
+
const encodedSsid = encodeURIComponent(ssid);
|
|
317
|
+
const encodedPassword = encodeURIComponent(password);
|
|
318
|
+
|
|
319
|
+
const cgi = `set_wifi.cgi?ssid=${encodedSsid}&channel=${channel}&authtype=${security}&wpa_psk=${encodedPassword}&enable=1&`;
|
|
320
|
+
|
|
321
|
+
const success = await this.sendCommand(cgi);
|
|
322
|
+
if (!success) return false;
|
|
323
|
+
|
|
324
|
+
// Wait for confirmation
|
|
325
|
+
return new Promise((resolve) => {
|
|
326
|
+
const timeout = setTimeout(() => resolve(false), 10000);
|
|
327
|
+
|
|
328
|
+
const handler: CommandEventListener = (_, cmd, data) => {
|
|
329
|
+
if (cmd === 24593) {
|
|
330
|
+
clearTimeout(timeout);
|
|
331
|
+
this.removeCommandListener(handler);
|
|
332
|
+
const params = this.parseResponse(new TextDecoder().decode(data));
|
|
333
|
+
resolve(params["result"] === "0");
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
this.addCommandListener(handler);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Get device information
|
|
343
|
+
*/
|
|
344
|
+
async getDeviceInfo(): Promise<DeviceInfo | null> {
|
|
345
|
+
const success = await this.sendCommand("get_status.cgi?");
|
|
346
|
+
if (!success) return null;
|
|
347
|
+
|
|
348
|
+
return new Promise((resolve) => {
|
|
349
|
+
const timeout = setTimeout(() => resolve(null), 5000);
|
|
350
|
+
|
|
351
|
+
const handler: CommandEventListener = (_, cmd, data) => {
|
|
352
|
+
clearTimeout(timeout);
|
|
353
|
+
this.removeCommandListener(handler);
|
|
354
|
+
const params = this.parseResponse(new TextDecoder().decode(data));
|
|
355
|
+
|
|
356
|
+
resolve({
|
|
357
|
+
serialNumber: params["id"] || "",
|
|
358
|
+
model: params["model"] || "",
|
|
359
|
+
firmware: params["sys_ver"] || "",
|
|
360
|
+
manufacturer: "VStarCam",
|
|
361
|
+
features: {
|
|
362
|
+
ptz: params["ptz"] === "1",
|
|
363
|
+
audio: params["haveAudio"] === "1",
|
|
364
|
+
twoWayAudio: params["support_talk"] === "1",
|
|
365
|
+
nightVision: params["haveIR"] === "1",
|
|
366
|
+
motionDetection: params["support_alarm"] === "1",
|
|
367
|
+
wifi: params["haveWifi"] === "1",
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
this.addCommandListener(handler);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* PTZ Control - Move camera
|
|
378
|
+
*/
|
|
379
|
+
async ptzUp(continuous: boolean = false): Promise<boolean> {
|
|
380
|
+
const cmd = continuous ? "command=0&onestep=0" : "command=0&onestep=1";
|
|
381
|
+
return this.sendCommand(`decoder_control.cgi?${cmd}&`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async ptzDown(continuous: boolean = false): Promise<boolean> {
|
|
385
|
+
const cmd = continuous ? "command=2&onestep=0" : "command=2&onestep=1";
|
|
386
|
+
return this.sendCommand(`decoder_control.cgi?${cmd}&`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async ptzLeft(continuous: boolean = false): Promise<boolean> {
|
|
390
|
+
const cmd = continuous ? "command=4&onestep=0" : "command=5&onestep=1";
|
|
391
|
+
return this.sendCommand(`decoder_control.cgi?${cmd}&`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async ptzRight(continuous: boolean = false): Promise<boolean> {
|
|
395
|
+
const cmd = continuous ? "command=6&onestep=0" : "command=7&onestep=1";
|
|
396
|
+
return this.sendCommand(`decoder_control.cgi?${cmd}&`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async ptzStop(): Promise<boolean> {
|
|
400
|
+
return this.sendCommand("decoder_control.cgi?command=1&onestep=0&");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Start video stream
|
|
405
|
+
* @param streamType 0 = main stream, 1 = sub stream
|
|
406
|
+
*/
|
|
407
|
+
async startVideo(streamType: number = 0): Promise<boolean> {
|
|
408
|
+
if (!this.clientPtr) return false;
|
|
409
|
+
return VStarCamModule.startVideoStream(this.clientPtr, streamType);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Stop video stream
|
|
414
|
+
*/
|
|
415
|
+
async stopVideo(): Promise<boolean> {
|
|
416
|
+
if (!this.clientPtr) return false;
|
|
417
|
+
return VStarCamModule.stopVideoStream(this.clientPtr);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Take a snapshot
|
|
422
|
+
*/
|
|
423
|
+
async takeSnapshot(): Promise<Uint8Array | null> {
|
|
424
|
+
if (!this.clientPtr) return null;
|
|
425
|
+
return VStarCamModule.takeSnapshot(this.clientPtr);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Disconnect from camera
|
|
430
|
+
*/
|
|
431
|
+
async disconnect(): Promise<boolean> {
|
|
432
|
+
if (!this.clientPtr) return false;
|
|
433
|
+
const result = await VStarCamModule.clientDisconnect(this.clientPtr);
|
|
434
|
+
return result;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Destroy client and release resources
|
|
439
|
+
*/
|
|
440
|
+
async destroy(): Promise<void> {
|
|
441
|
+
if (this.clientPtr) {
|
|
442
|
+
await VStarCamModule.clientDestroy(this.clientPtr);
|
|
443
|
+
this.clientPtr = 0;
|
|
444
|
+
}
|
|
445
|
+
this.connectionListeners.clear();
|
|
446
|
+
this.commandListeners.clear();
|
|
447
|
+
this.videoListeners.clear();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Event listener management
|
|
451
|
+
addConnectionListener(listener: ConnectionEventListener) {
|
|
452
|
+
this.connectionListeners.add(listener);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
removeConnectionListener(listener: ConnectionEventListener) {
|
|
456
|
+
this.connectionListeners.delete(listener);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
addCommandListener(listener: CommandEventListener) {
|
|
460
|
+
this.commandListeners.add(listener);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
removeCommandListener(listener: CommandEventListener) {
|
|
464
|
+
this.commandListeners.delete(listener);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
addVideoListener(listener: VideoFrameListener) {
|
|
468
|
+
this.videoListeners.add(listener);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
removeVideoListener(listener: VideoFrameListener) {
|
|
472
|
+
this.videoListeners.delete(listener);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
get isConnected(): boolean {
|
|
476
|
+
return this.clientPtr > 0;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Export singleton factory
|
|
481
|
+
export function createVStarCamClient(): VStarCamClient {
|
|
482
|
+
return new VStarCamClient();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export { VStarCamClient };
|
|
486
|
+
export default VStarCamModule;
|