diff --git a/webui/web/index.html b/webui/web/index.html
index 021137a..2e915be 100644
--- a/webui/web/index.html
+++ b/webui/web/index.html
@@ -207,21 +207,6 @@
Starting scan...
-
-
- 0
- Tested
-
-
- 0
- Found
-
-
- 0
- Remaining
-
-
-
Found Connections
diff --git a/webui/web/js/api/camera-search.js b/webui/web/js/api/camera-search.js
index 703728c..3e93181 100644
--- a/webui/web/js/api/camera-search.js
+++ b/webui/web/js/api/camera-search.js
@@ -1,14 +1,23 @@
+import { MockCameraSearch } from '../mock/mock-data.js';
+
export class CameraSearchAPI {
- constructor(baseURL = null) {
+ constructor(baseURL = null, useMock = false) {
// Use relative URLs since API and UI are on the same port
if (!baseURL) {
this.baseURL = '';
} else {
this.baseURL = baseURL;
}
+ this.useMock = useMock;
+ this.mockAPI = useMock ? new MockCameraSearch() : null;
}
async search(query, limit = 10) {
+ // Use mock API if enabled
+ if (this.useMock) {
+ return await this.mockAPI.search(query, limit);
+ }
+
const response = await fetch(`${this.baseURL}api/v1/cameras/search`, {
method: 'POST',
headers: {
diff --git a/webui/web/js/api/stream-discovery.js b/webui/web/js/api/stream-discovery.js
index 87bacd3..ed8f8bf 100644
--- a/webui/web/js/api/stream-discovery.js
+++ b/webui/web/js/api/stream-discovery.js
@@ -1,5 +1,7 @@
+import { MockStreamDiscovery } from '../mock/mock-data.js';
+
export class StreamDiscoveryAPI {
- constructor(baseURL = null) {
+ constructor(baseURL = null, useMock = false) {
// Use relative URLs since API and UI are on the same port
if (!baseURL) {
this.baseURL = '';
@@ -7,11 +9,19 @@ export class StreamDiscoveryAPI {
this.baseURL = baseURL;
}
this.eventSource = null;
+ this.useMock = useMock;
+ this.mockAPI = useMock ? new MockStreamDiscovery() : null;
}
discover(request, callbacks) {
this.close();
+ // Use mock API if enabled
+ if (this.useMock) {
+ this.mockAPI.discover(request, callbacks);
+ return;
+ }
+
fetch(`${this.baseURL}api/v1/streams/discover`, {
method: 'POST',
headers: {
@@ -91,6 +101,9 @@ export class StreamDiscoveryAPI {
}
close() {
+ if (this.useMock && this.mockAPI) {
+ this.mockAPI.close();
+ }
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
diff --git a/webui/web/js/main.js b/webui/web/js/main.js
index a891e4e..689bb8b 100644
--- a/webui/web/js/main.js
+++ b/webui/web/js/main.js
@@ -8,8 +8,12 @@ import { showToast } from './utils/toast.js';
class StrixApp {
constructor() {
- this.cameraAPI = new CameraSearchAPI();
- this.streamAPI = new StreamDiscoveryAPI();
+ // Check if mock mode is enabled via URL parameter (?mock=true)
+ const urlParams = new URLSearchParams(window.location.search);
+ const useMock = urlParams.get('mock') === 'true';
+
+ this.cameraAPI = new CameraSearchAPI(null, useMock);
+ this.streamAPI = new StreamDiscoveryAPI(null, useMock);
this.searchForm = new SearchForm();
this.carousel = new StreamCarousel();
@@ -20,6 +24,7 @@ class StrixApp {
this.selectedMainStream = null;
this.selectedSubStream = null;
this.isSelectingSubStream = false;
+ this.useMock = useMock;
this.init();
}
@@ -27,6 +32,31 @@ class StrixApp {
init() {
this.setupEventListeners();
this.showScreen('address');
+
+ // Show mock mode indicator if enabled
+ if (this.useMock) {
+ this.showMockIndicator();
+ }
+ }
+
+ showMockIndicator() {
+ const indicator = document.createElement('div');
+ indicator.id = 'mock-indicator';
+ indicator.style.cssText = `
+ position: fixed;
+ top: 10px;
+ right: 10px;
+ background: #7E57C2;
+ color: white;
+ padding: 8px 16px;
+ border-radius: 8px;
+ font-size: 14px;
+ font-weight: 600;
+ z-index: 9999;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3);
+ `;
+ indicator.textContent = 'MOCK MODE';
+ document.body.appendChild(indicator);
}
setupEventListeners() {
diff --git a/webui/web/js/mock/mock-data.js b/webui/web/js/mock/mock-data.js
new file mode 100644
index 0000000..2c0b328
--- /dev/null
+++ b/webui/web/js/mock/mock-data.js
@@ -0,0 +1,209 @@
+// Mock data for development and testing
+export const MOCK_CAMERAS = [
+ { brand: "Hikvision", model: "DS-2CD2143G0-I" },
+ { brand: "Hikvision", model: "DS-2CD2385G1-I" },
+ { brand: "Hikvision", model: "DS-2CD2T85G1-I8" },
+ { brand: "Dahua", model: "IPC-HFW5831E-Z5E" },
+ { brand: "Dahua", model: "IPC-HDW5831R-ZE" },
+ { brand: "Axis", model: "M3046-V" },
+ { brand: "Axis", model: "P3245-LVE" },
+ { brand: "Uniview", model: "IPC2324LB-ADZK-G" },
+ { brand: "Reolink", model: "RLC-810A" },
+ { brand: "TP-Link", model: "VIGI C540V" }
+];
+
+export const MOCK_STREAMS = [
+ {
+ url: "rtsp://admin:password@192.168.1.100:554/stream1",
+ type: "FFMPEG",
+ resolution: "1920x1080",
+ codec: "h264",
+ fps: 25,
+ bitrate: 4096,
+ audio: true
+ },
+ {
+ url: "rtsp://admin:password@192.168.1.100:554/stream2",
+ type: "FFMPEG",
+ resolution: "640x360",
+ codec: "h264",
+ fps: 15,
+ bitrate: 512,
+ audio: true
+ },
+ {
+ url: "http://admin:password@192.168.1.100:80/onvif/device_service",
+ type: "ONVIF",
+ resolution: "1920x1080",
+ codec: "h264",
+ fps: 25,
+ bitrate: 4096,
+ audio: false
+ },
+ {
+ url: "rtsp://admin:password@192.168.1.100/live/main",
+ type: "FFMPEG",
+ resolution: "2560x1440",
+ codec: "h265",
+ fps: 30,
+ bitrate: 6144,
+ audio: true
+ },
+ {
+ url: "rtsp://admin:password@192.168.1.100/live/sub",
+ type: "FFMPEG",
+ resolution: "704x576",
+ codec: "h264",
+ fps: 15,
+ bitrate: 768,
+ audio: false
+ },
+ {
+ url: "rtsp://admin:password@192.168.1.100:554/ch01/0",
+ type: "FFMPEG",
+ resolution: "3840x2160",
+ codec: "h265",
+ fps: 25,
+ bitrate: 8192,
+ audio: true
+ },
+ {
+ url: "rtsp://admin:password@192.168.1.100:554/ch01/1",
+ type: "FFMPEG",
+ resolution: "1280x720",
+ codec: "h264",
+ fps: 20,
+ bitrate: 2048,
+ audio: false
+ },
+ {
+ url: "http://admin:password@192.168.1.100:8080/video.mjpeg",
+ type: "MJPEG",
+ resolution: "1920x1080",
+ codec: "mjpeg",
+ fps: 10,
+ bitrate: 3072,
+ audio: false
+ },
+ {
+ url: "rtsp://admin:password@192.168.1.100/h264_stream",
+ type: "FFMPEG",
+ resolution: "1920x1080",
+ codec: "h264",
+ fps: 30,
+ bitrate: 4096,
+ audio: true
+ },
+ {
+ url: "http://admin:password@192.168.1.100:8081/stream.m3u8",
+ type: "HLS",
+ resolution: "1920x1080",
+ codec: "h264",
+ fps: 25,
+ bitrate: 4096,
+ audio: true
+ }
+];
+
+// Mock Camera Search API
+export class MockCameraSearch {
+ async search(query, limit = 10) {
+ // Simulate network delay
+ await this.delay(100);
+
+ const results = MOCK_CAMERAS.filter(camera => {
+ const searchStr = `${camera.brand} ${camera.model}`.toLowerCase();
+ return searchStr.includes(query.toLowerCase());
+ });
+
+ return {
+ cameras: results.slice(0, limit),
+ total: results.length
+ };
+ }
+
+ delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+}
+
+// Mock Stream Discovery API
+export class MockStreamDiscovery {
+ constructor() {
+ this.isRunning = false;
+ this.timeoutId = null;
+ }
+
+ discover(request, callbacks) {
+ this.isRunning = true;
+ let tested = 0;
+ const totalToTest = 516;
+ const foundStreams = [...MOCK_STREAMS];
+
+ // Initial progress
+ callbacks.onProgress({
+ tested: 0,
+ found: 0,
+ remaining: totalToTest
+ });
+
+ // Simulate progressive testing
+ const progressInterval = setInterval(() => {
+ if (!this.isRunning) {
+ clearInterval(progressInterval);
+ return;
+ }
+
+ tested += Math.floor(Math.random() * 30) + 20;
+ if (tested > totalToTest) tested = totalToTest;
+
+ callbacks.onProgress({
+ tested: tested,
+ found: foundStreams.length,
+ remaining: totalToTest - tested
+ });
+
+ if (tested >= totalToTest) {
+ clearInterval(progressInterval);
+ }
+ }, 200);
+
+ // Send found streams progressively
+ let streamIndex = 0;
+ const streamInterval = setInterval(() => {
+ if (!this.isRunning) {
+ clearInterval(streamInterval);
+ return;
+ }
+
+ if (streamIndex < foundStreams.length) {
+ callbacks.onStreamFound({
+ stream: foundStreams[streamIndex]
+ });
+ streamIndex++;
+ } else {
+ clearInterval(streamInterval);
+ }
+ }, 800);
+
+ // Complete after ~7.7 seconds
+ this.timeoutId = setTimeout(() => {
+ if (!this.isRunning) return;
+
+ callbacks.onComplete({
+ total_found: foundStreams.length,
+ duration: 7.7
+ });
+
+ this.isRunning = false;
+ }, 7700);
+ }
+
+ close() {
+ this.isRunning = false;
+ if (this.timeoutId) {
+ clearTimeout(this.timeoutId);
+ this.timeoutId = null;
+ }
+ }
+}