From a95b78bd8f5600146f51f9b66135d571c3638968 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Tue, 27 May 2025 17:20:56 +0200 Subject: [PATCH] shit --- README.md | 89 ++-- scripts/image_filters.js | 83 ++++ scripts/motion_detection.py | 53 +++ src/main/java/com/jsca/CameraConfig.java | 11 + src/main/java/com/jsca/CameraManager.java | 395 ++++++++++++++---- src/main/java/com/jsca/CameraPanel.java | 178 +++++--- src/main/java/com/jsca/CameraViewer.java | 126 +----- src/main/java/com/jsca/Main.java | 12 +- src/main/java/com/jsca/MainApp.java | 124 ++++++ .../java/com/jsca/NetworkStreamReader.java | 135 ++++-- src/main/java/com/jsca/ScriptRunner.java | 112 +++++ .../java/com/jsca/WebcamStreamReader.java | 4 +- target/classes/com/jsca/CameraConfig.class | Bin 0 -> 427 bytes target/classes/com/jsca/CameraManager$1.class | Bin 702 -> 702 bytes .../com/jsca/CameraManager$CameraConfig.class | Bin 549 -> 549 bytes target/classes/com/jsca/CameraManager.class | Bin 6131 -> 14775 bytes target/classes/com/jsca/CameraPanel.class | Bin 5242 -> 6678 bytes target/classes/com/jsca/CameraViewer$1.class | Bin 761 -> 755 bytes target/classes/com/jsca/CameraViewer.class | Bin 10580 -> 5903 bytes target/classes/com/jsca/Main.class | Bin 1145 -> 1429 bytes target/classes/com/jsca/MainApp$1.class | Bin 0 -> 725 bytes target/classes/com/jsca/MainApp.class | Bin 0 -> 6357 bytes .../com/jsca/NetworkStreamReader.class | Bin 4443 -> 5906 bytes target/classes/com/jsca/ScriptRunner.class | Bin 0 -> 6276 bytes .../classes/com/jsca/WebcamStreamReader.class | Bin 3152 -> 3208 bytes 25 files changed, 965 insertions(+), 357 deletions(-) create mode 100644 scripts/image_filters.js create mode 100755 scripts/motion_detection.py create mode 100644 src/main/java/com/jsca/CameraConfig.java create mode 100644 src/main/java/com/jsca/MainApp.java create mode 100644 src/main/java/com/jsca/ScriptRunner.java create mode 100644 target/classes/com/jsca/CameraConfig.class create mode 100644 target/classes/com/jsca/MainApp$1.class create mode 100644 target/classes/com/jsca/MainApp.class create mode 100644 target/classes/com/jsca/ScriptRunner.class diff --git a/README.md b/README.md index 9a812a8..16a8495 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,88 @@ -# Java Security Camera App +# Java Security Camera Application (JSCA) -A Java desktop application for viewing live streams from both USB webcams and network MJPEG cameras. +A Java-based desktop application for managing and viewing multiple camera feeds simultaneously. Supports both local webcams and network cameras. ## Features -- View live streams from USB webcams -- Connect to network MJPEG cameras (e.g., DroidCamX) -- Take snapshots of the current view -- Simple and intuitive user interface -- Support for multiple camera sources +- 🎥 Support for local webcams and network cameras (MJPEG streams) +- 📺 2x2 grid layout for viewing up to 4 camera feeds +- 💾 Save and load camera configurations +- 🔐 Basic authentication for network cameras +- 🖱️ Click-to-select camera panels +- 🎛️ Simple and intuitive menu interface ## Requirements - Java 11 or higher -- Maven -- USB webcam (for local camera support) -- Network camera with MJPEG stream support (for network camera support) +- Maven 3.6 or higher +- Connected webcam(s) for local camera support +- Network camera URLs (if using IP cameras) ## Building the Application -1. Clone the repository -2. Navigate to the project directory -3. Build with Maven: +1. Clone the repository: + ```bash + git clone https://github.com/yourusername/jsca.git + cd jsca + ``` + +2. Build with Maven: ```bash mvn clean package ``` + This will create an executable JAR file in the `target` directory. + ## Running the Application -After building, run the application using: +Run the application using: ```bash java -jar target/security-camera-app-1.0-SNAPSHOT-jar-with-dependencies.jar ``` -## Usage +## Usage Instructions -1. Launch the application -2. Select the camera source from the dropdown: - - USB Camera: Uses your computer's webcam - - Network Camera: Connects to an MJPEG stream URL -3. Click "Start" to begin streaming -4. Use the "Snapshot" button to capture the current frame -5. Click "Stop" to end the stream +1. **Adding Cameras**: + - Select an empty camera panel by clicking on it + - Go to Camera -> Add Local Camera to add a webcam + - Go to Camera -> Add Network Camera to add an IP camera -### Using with Network Cameras +2. **Managing Cameras**: + - Click on a camera panel to select it + - Use Camera -> Remove Camera to stop and remove the selected camera + - Use Camera -> Restart Camera to restart the selected camera feed -For network cameras, you'll need to provide the MJPEG stream URL. Common formats include: -- DroidCamX: `http://[IP_ADDRESS]:4747/video` -- Generic IP Camera: `http://[IP_ADDRESS]/video` or `http://[IP_ADDRESS]/mjpeg` +3. **Saving/Loading Configurations**: + - File -> Save Setup to save your current camera configuration + - File -> Load Setup to restore a previously saved configuration + +## Network Camera URLs + +For network cameras, you'll need the MJPEG stream URL. Common formats include: + +- Generic IP Camera: `http://camera-ip:port/video` +- DroidCam: `http://phone-ip:4747/video` ## Troubleshooting -1. No webcam detected: - - Ensure your webcam is properly connected - - Check if other applications are using the webcam +1. **No webcams detected**: + - Check if your webcam is properly connected + - Ensure no other application is using the webcam -2. Network camera not connecting: +2. **Network camera not connecting**: - Verify the camera URL is correct - - Ensure the camera is on the same network - - Check if the camera supports MJPEG streaming + - Check if the camera is accessible from your network + - Ensure proper credentials are provided if required + +3. **Performance issues**: + - Try reducing the number of active cameras + - Check your network bandwidth for IP cameras + - Ensure your computer meets the minimum requirements ## License -This project is open source and available under the MIT License. \ No newline at end of file +This project is licensed under the MIT License - see the LICENSE file for details. + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. \ No newline at end of file diff --git a/scripts/image_filters.js b/scripts/image_filters.js new file mode 100644 index 0000000..d0ca32d --- /dev/null +++ b/scripts/image_filters.js @@ -0,0 +1,83 @@ +// Image filter script for JSCA +// This script demonstrates how to apply basic image filters to the camera feed + +function applyGrayscale(image) { + var width = image.getWidth(); + var height = image.getHeight(); + var result = new java.awt.image.BufferedImage( + width, height, + java.awt.image.BufferedImage.TYPE_BYTE_GRAY + ); + + var g = result.getGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + + return result; +} + +function applySepia(image) { + var width = image.getWidth(); + var height = image.getHeight(); + var result = new java.awt.image.BufferedImage( + width, height, + java.awt.image.BufferedImage.TYPE_INT_RGB + ); + + for (var y = 0; y < height; y++) { + for (var x = 0; x < width; x++) { + var rgb = image.getRGB(x, y); + var r = (rgb >> 16) & 0xFF; + var g = (rgb >> 8) & 0xFF; + var b = rgb & 0xFF; + + var tr = Math.min(255, (r * 0.393 + g * 0.769 + b * 0.189)); + var tg = Math.min(255, (r * 0.349 + g * 0.686 + b * 0.168)); + var tb = Math.min(255, (r * 0.272 + g * 0.534 + b * 0.131)); + + result.setRGB(x, y, (tr << 16) | (tg << 8) | tb); + } + } + + return result; +} + +function applyInvert(image) { + var width = image.getWidth(); + var height = image.getHeight(); + var result = new java.awt.image.BufferedImage( + width, height, + java.awt.image.BufferedImage.TYPE_INT_RGB + ); + + for (var y = 0; y < height; y++) { + for (var x = 0; x < width; x++) { + var rgb = image.getRGB(x, y); + result.setRGB(x, y, ~rgb); + } + } + + return result; +} + +// Apply the selected filter to the input image +var filterName = "grayscale"; // Change this to "sepia" or "invert" for different effects +var result; + +switch (filterName) { + case "grayscale": + result = applyGrayscale(inputImage); + break; + case "sepia": + result = applySepia(inputImage); + break; + case "invert": + result = applyInvert(inputImage); + break; + default: + print("Unknown filter: " + filterName); + result = inputImage; +} + +// Return the filtered image +result; \ No newline at end of file diff --git a/scripts/motion_detection.py b/scripts/motion_detection.py new file mode 100755 index 0000000..4c030a1 --- /dev/null +++ b/scripts/motion_detection.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +import sys +import cv2 +import numpy as np + +def detect_motion(image_path): + # Read the input image + frame = cv2.imread(image_path) + if frame is None: + print("Error: Could not read image file") + sys.exit(1) + + # Convert to grayscale + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + + # Apply Gaussian blur to reduce noise + blur = cv2.GaussianBlur(gray, (21, 21), 0) + + # Threshold the image to detect significant changes + _, thresh = cv2.threshold(blur, 20, 255, cv2.THRESH_BINARY) + + # Find contours in the thresholded image + contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + # Filter contours based on area to remove noise + min_area = 500 + motion_detected = False + + for contour in contours: + if cv2.contourArea(contour) > min_area: + motion_detected = True + # Draw rectangle around motion area + (x, y, w, h) = cv2.boundingRect(contour) + cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) + + # Save the output image with motion detection boxes + output_path = image_path.replace('.jpg', '_motion.jpg') + cv2.imwrite(output_path, frame) + + # Return result + if motion_detected: + print("Motion detected!") + print(f"Output saved to: {output_path}") + else: + print("No motion detected") + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python motion_detection.py ") + sys.exit(1) + + detect_motion(sys.argv[1]) \ No newline at end of file diff --git a/src/main/java/com/jsca/CameraConfig.java b/src/main/java/com/jsca/CameraConfig.java new file mode 100644 index 0000000..7fa228e --- /dev/null +++ b/src/main/java/com/jsca/CameraConfig.java @@ -0,0 +1,11 @@ +package com.jsca; + +public class CameraConfig { + public int position; + public String name; + public String type; // "local" or "network" + public String url; // for network cameras + public String username; + public String password; + public int deviceIndex; // for local cameras +} \ No newline at end of file diff --git a/src/main/java/com/jsca/CameraManager.java b/src/main/java/com/jsca/CameraManager.java index 946e704..5a10415 100644 --- a/src/main/java/com/jsca/CameraManager.java +++ b/src/main/java/com/jsca/CameraManager.java @@ -1,135 +1,352 @@ package com.jsca; +import com.github.sarxos.webcam.Webcam; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; import java.io.*; -import java.util.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.*; public class CameraManager { - private final Map cameras = new HashMap<>(); - private final JPanel gridPanel; - private static final int MAX_CAMERAS = 4; - private static final String CONFIG_FILE = "camera_config.json"; + private final Map cameraPanels; + private final Map> cameraThreads; + private final ExecutorService executorService; private final Gson gson; + private final JPanel gridPanel; public CameraManager(JPanel gridPanel) { - this.gridPanel = gridPanel; + this.cameraPanels = new HashMap<>(); + this.cameraThreads = new HashMap<>(); + this.executorService = Executors.newCachedThreadPool(); this.gson = new GsonBuilder().setPrettyPrinting().create(); - - // Initialize empty panels - for (int i = 0; i < MAX_CAMERAS; i++) { - CameraPanel emptyPanel = new CameraPanel("Empty " + (i + 1)); - cameras.put(i, emptyPanel); - gridPanel.add(emptyPanel); + this.gridPanel = gridPanel; + // Do not add any panels initially + } + + public void registerPanel(int position, CameraPanel panel) { + if (position >= 0 && position < 4) { + cameraPanels.put(position, panel); } } - public boolean addCamera(String name, StreamReader streamReader, int position) { - if (position < 0 || position >= MAX_CAMERAS) { - return false; + public void addLocalCamera() { + // Find the first inactive panel, or add a new one if less than 4 exist + CameraPanel targetPanel = null; + int position = -1; + for (int i = 0; i < 4; i++) { + CameraPanel panel = cameraPanels.get(i); + if (panel != null && !panel.isActive()) { + targetPanel = panel; + position = i; + break; + } } - - CameraPanel oldPanel = cameras.get(position); - if (oldPanel != null) { - oldPanel.stopStream(); - } - - CameraPanel newPanel = new CameraPanel(name); - newPanel.startStream(streamReader); - - cameras.put(position, newPanel); - gridPanel.remove(position); - gridPanel.add(newPanel, position); - gridPanel.revalidate(); - gridPanel.repaint(); - - return true; - } - - public void removeCamera(int position) { - if (position >= 0 && position < MAX_CAMERAS) { - CameraPanel panel = cameras.get(position); - if (panel != null) { - panel.stopStream(); - CameraPanel emptyPanel = new CameraPanel("Empty " + (position + 1)); - cameras.put(position, emptyPanel); - gridPanel.remove(position); - gridPanel.add(emptyPanel, position); + if (targetPanel == null) { + if (cameraPanels.size() < 4) { + position = getNextAvailablePosition(); + targetPanel = new CameraPanel(position); + cameraPanels.put(position, targetPanel); + gridPanel.add(targetPanel); gridPanel.revalidate(); gridPanel.repaint(); + } else { + JOptionPane.showMessageDialog(null, "All camera panels are active", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + } + Webcam[] webcams = Webcam.getWebcams().toArray(new Webcam[0]); + if (webcams.length == 0) { + JOptionPane.showMessageDialog(null, "No webcams found", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + String[] options = new String[webcams.length]; + for (int i = 0; i < webcams.length; i++) { + options[i] = webcams[i].getName(); + } + String selected = (String) JOptionPane.showInputDialog(null, + "Select webcam:", "Add Local Camera", + JOptionPane.QUESTION_MESSAGE, null, + options, options[0]); + if (selected != null) { + int index = -1; + for (int i = 0; i < options.length; i++) { + if (options[i].equals(selected)) { + index = i; + break; + } + } + if (index >= 0) { + startLocalCamera(position, webcams[index]); } } } - public void stopAllCameras() { - for (CameraPanel panel : cameras.values()) { - if (panel != null) { - panel.stopStream(); + public void addNetworkCamera() { + // Find the first inactive panel, or add a new one if less than 4 exist + CameraPanel targetPanel = null; + int position = -1; + for (int i = 0; i < 4; i++) { + CameraPanel panel = cameraPanels.get(i); + if (panel != null && !panel.isActive()) { + targetPanel = panel; + position = i; + break; + } + } + if (targetPanel == null) { + if (cameraPanels.size() < 4) { + position = getNextAvailablePosition(); + targetPanel = new CameraPanel(position); + cameraPanels.put(position, targetPanel); + gridPanel.add(targetPanel); + gridPanel.revalidate(); + gridPanel.repaint(); + } else { + JOptionPane.showMessageDialog(null, "All camera panels are active", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + } + JPanel form = new JPanel(new GridLayout(0, 2, 5, 5)); + JTextField urlField = new JTextField(20); + JTextField userField = new JTextField(20); + JPasswordField passField = new JPasswordField(20); + JCheckBox isDroidcamBox = new JCheckBox(); + form.add(new JLabel("Camera URL:")); + form.add(urlField); + form.add(new JLabel("Is DroidCam:")); + form.add(isDroidcamBox); + form.add(new JLabel("Username:")); + form.add(userField); + form.add(new JLabel("Password:")); + form.add(passField); + isDroidcamBox.addActionListener(e -> { + if (isDroidcamBox.isSelected() && urlField.getText().isEmpty()) { + urlField.setText("http://"); + } + }); + int result = JOptionPane.showConfirmDialog(null, form, + "Add Network Camera", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); + if (result == JOptionPane.OK_OPTION) { + String url = urlField.getText().trim(); + String username = userField.getText().trim(); + String password = new String(passField.getPassword()).trim(); + if (url.isEmpty()) { + JOptionPane.showMessageDialog(null, + "Please enter a camera URL", + "Error", + JOptionPane.ERROR_MESSAGE); + return; + } + if (isDroidcamBox.isSelected()) { + url = url.replaceAll("/$", ""); + if (!url.endsWith("/video")) { + url += "/video"; + } + } + startNetworkCamera(position, url, + username.isEmpty() ? null : username, + password.isEmpty() ? null : password); + } + } + + public void removeSelectedCamera() { + // Remove the first camera (if any) + if (cameraPanels.isEmpty()) { + JOptionPane.showMessageDialog(null, "No cameras to remove", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + int firstKey = cameraPanels.keySet().iterator().next(); + stopCamera(firstKey); + CameraPanel panel = cameraPanels.remove(firstKey); + if (panel != null) { + gridPanel.remove(panel); + gridPanel.revalidate(); + gridPanel.repaint(); + } + } + + public void restartSelectedCamera() { + // Restart the first camera (if any) + if (cameraPanels.isEmpty()) { + JOptionPane.showMessageDialog(null, "No cameras to restart", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + int firstKey = cameraPanels.keySet().iterator().next(); + CameraPanel panel = cameraPanels.get(firstKey); + if (panel != null && panel.isActive()) { + stopCamera(firstKey); + // If the camera was local or network, restart accordingly + String name = panel.getCameraName(); + if (name.startsWith("Local: ")) { + // Try to find the webcam by name + String webcamName = name.substring(7); + for (com.github.sarxos.webcam.Webcam webcam : com.github.sarxos.webcam.Webcam.getWebcams()) { + if (webcam.getName().equals(webcamName)) { + startLocalCamera(firstKey, webcam); + return; + } + } + } else if (name.startsWith("Network: ")) { + // Try to restart the network camera (URL only, no auth) + String url = name.substring(9); + startNetworkCamera(firstKey, url, null, null); } } } - public CameraPanel getCameraPanel(int position) { - return cameras.get(position); + private void startLocalCamera(int position, Webcam webcam) { + stopCamera(position); + CameraPanel panel = cameraPanels.get(position); + panel.setActive(true); + panel.setCameraName("Local: " + webcam.getName()); + + Future future = executorService.submit(() -> { + try { + webcam.open(); + while (!Thread.interrupted()) { + BufferedImage image = webcam.getImage(); + if (image != null) { + SwingUtilities.invokeLater(() -> panel.updateFrame(image)); + } + Thread.sleep(33); // ~30 FPS + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + webcam.close(); + SwingUtilities.invokeLater(() -> panel.setActive(false)); + } + }); + + cameraThreads.put(position, future); } - public void restartCamera(int position) { - CameraPanel panel = cameras.get(position); - if (panel != null && panel.isStreaming()) { - StreamReader currentStream = panel.getCurrentStream(); - if (currentStream != null) { - panel.stopStream(); - panel.startStream(currentStream); - } + private void startNetworkCamera(int position, String urlString, String username, String password) { + stopCamera(position); + CameraPanel panel = cameraPanels.get(position); + panel.setActive(true); + panel.setCameraName("Network: " + urlString); + + // For DroidCam, append video feed path if not present + if (urlString.contains("droidcam") && !urlString.endsWith("/video")) { + urlString = urlString.replaceAll("/$", "") + "/video"; + } + + // Create and start the network stream reader + NetworkStreamReader streamReader = new NetworkStreamReader(panel, urlString, username, password); + Future future = executorService.submit(streamReader); + cameraThreads.put(position, future); + } + + private void stopCamera(int position) { + Future future = cameraThreads.get(position); + if (future != null) { + future.cancel(true); + cameraThreads.remove(position); + } + + CameraPanel panel = cameraPanels.get(position); + if (panel != null) { + panel.setActive(false); + panel.setCameraName("Camera " + (position + 1)); + } + } + + public void shutdown() { + for (Future future : cameraThreads.values()) { + future.cancel(true); + } + cameraThreads.clear(); + executorService.shutdownNow(); + try { + executorService.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } } public void saveConfiguration() { - List configs = new ArrayList<>(); - for (Map.Entry entry : cameras.entrySet()) { - CameraPanel panel = entry.getValue(); - if (panel.isStreaming()) { - CameraConfig config = new CameraConfig(); - config.position = entry.getKey(); - config.name = panel.getCameraName(); - // Add more configuration details as needed - configs.add(config); - } - } + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setDialogTitle("Save Camera Configuration"); + fileChooser.setSelectedFile(new File("camera_config.json")); - try (Writer writer = new FileWriter(CONFIG_FILE)) { - gson.toJson(configs, writer); - } catch (IOException e) { - JOptionPane.showMessageDialog(null, - "Error saving configuration: " + e.getMessage(), - "Save Error", - JOptionPane.ERROR_MESSAGE); + if (fileChooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) { + try (FileWriter writer = new FileWriter(fileChooser.getSelectedFile())) { + List configs = new ArrayList<>(); + for (Map.Entry entry : cameraPanels.entrySet()) { + CameraPanel panel = entry.getValue(); + if (panel.isActive()) { + CameraConfig config = new CameraConfig(); + config.position = entry.getKey(); + config.name = panel.getCameraName(); + configs.add(config); + } + } + gson.toJson(configs, writer); + JOptionPane.showMessageDialog(null, + "Configuration saved successfully", + "Success", + JOptionPane.INFORMATION_MESSAGE); + } catch (IOException e) { + JOptionPane.showMessageDialog(null, + "Error saving configuration: " + e.getMessage(), + "Error", + JOptionPane.ERROR_MESSAGE); + } } } public void loadConfiguration() { - try (Reader reader = new FileReader(CONFIG_FILE)) { - List configs = gson.fromJson(reader, - new TypeToken>(){}.getType()); - - if (configs != null) { - stopAllCameras(); - for (CameraConfig config : configs) { - // Implement camera restoration based on config - // This is a placeholder for the actual implementation - // You'll need to create appropriate StreamReader instances + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setDialogTitle("Load Camera Configuration"); + + if (fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) { + try (FileReader reader = new FileReader(fileChooser.getSelectedFile())) { + List configs = gson.fromJson(reader, + new TypeToken>(){}.getType()); + + if (configs != null) { + // Stop all current cameras + for (int position : cameraPanels.keySet()) { + stopCamera(position); + } + + // Load new configuration + for (CameraConfig config : configs) { + if ("local".equals(config.type)) { + // TODO: Implement local camera restoration + } else if ("network".equals(config.type)) { + startNetworkCamera(config.position, config.url, config.username, config.password); + } + } } + } catch (IOException e) { + JOptionPane.showMessageDialog(null, + "Error loading configuration: " + e.getMessage(), + "Error", + JOptionPane.ERROR_MESSAGE); } - } catch (IOException e) { - JOptionPane.showMessageDialog(null, - "Error loading configuration: " + e.getMessage(), - "Load Error", - JOptionPane.ERROR_MESSAGE); } } + private int getNextAvailablePosition() { + for (int i = 0; i < 4; i++) { + if (!cameraPanels.containsKey(i)) return i; + } + return -1; + } + private static class CameraConfig { int position; String name; diff --git a/src/main/java/com/jsca/CameraPanel.java b/src/main/java/com/jsca/CameraPanel.java index bde55a9..d91c7a2 100644 --- a/src/main/java/com/jsca/CameraPanel.java +++ b/src/main/java/com/jsca/CameraPanel.java @@ -1,6 +1,7 @@ package com.jsca; import javax.swing.*; +import javax.swing.border.*; import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; @@ -9,91 +10,130 @@ import java.time.format.DateTimeFormatter; import javax.imageio.ImageIO; public class CameraPanel extends JPanel { - private final JLabel videoLabel; - private final String cameraName; - private volatile boolean isStreaming = false; - private StreamReader currentStream; + private final int position; + private BufferedImage currentFrame; + private String cameraName; + private boolean isActive; + private boolean isSelected; + private static final Color SELECTED_BORDER_COLOR = new Color(0, 120, 215); + private static final Color BACKGROUND_COLOR = Color.WHITE; + private static final Color PLACEHOLDER_COLOR = new Color(245, 245, 245); + private static final Color TEXT_COLOR = new Color(100, 100, 100); + private static final int BORDER_THICKNESS = 2; - public CameraPanel(String cameraName) { - this.cameraName = cameraName; - setLayout(new BorderLayout()); - - // Create video display panel - videoLabel = new JLabel(); - videoLabel.setPreferredSize(new Dimension(640, 480)); - videoLabel.setBorder(BorderFactory.createLineBorder(Color.BLACK)); - add(videoLabel, BorderLayout.CENTER); - - // Create control panel - JPanel controlPanel = new JPanel(); - controlPanel.setLayout(new FlowLayout()); - - JButton snapshotButton = new JButton("Snapshot"); - snapshotButton.addActionListener(e -> takeSnapshot()); - controlPanel.add(snapshotButton); - - add(controlPanel, BorderLayout.SOUTH); + public CameraPanel(int position) { + this.position = position; + this.isActive = false; + this.isSelected = false; + this.cameraName = String.format("Camera %d", position + 1); + setPreferredSize(new Dimension(320, 240)); + setBorder(BorderFactory.createLineBorder(new Color(220, 220, 220), 1)); + setBackground(BACKGROUND_COLOR); } - public void startStream(StreamReader streamReader) { - stopStream(); // Stop any existing stream - currentStream = streamReader; - new Thread(currentStream).start(); - isStreaming = true; + public void setSelected(boolean selected) { + this.isSelected = selected; + setBorder(createPanelBorder(selected)); + repaint(); } - public void stopStream() { - if (currentStream != null) { - currentStream.stop(); - currentStream = null; - } - isStreaming = false; - videoLabel.setIcon(null); + public boolean isSelected() { + return isSelected; } - private void takeSnapshot() { - if (videoLabel.getIcon() == null) return; - - try { - BufferedImage image = new BufferedImage( - videoLabel.getWidth(), - videoLabel.getHeight(), - BufferedImage.TYPE_INT_RGB - ); - Graphics g = image.getGraphics(); - videoLabel.paint(g); - g.dispose(); - - String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")); - File outputFile = new File(String.format("snapshot_%s_%s.jpg", cameraName.replaceAll("[^a-zA-Z0-9]", "_"), timestamp)); - ImageIO.write(image, "jpg", outputFile); - JOptionPane.showMessageDialog(this, - "Snapshot saved: " + outputFile.getName(), - "Snapshot Saved", - JOptionPane.INFORMATION_MESSAGE); - } catch (Exception e) { - JOptionPane.showMessageDialog(this, - "Error saving snapshot: " + e.getMessage(), - "Error", - JOptionPane.ERROR_MESSAGE); - } + public void updateFrame(BufferedImage frame) { + this.currentFrame = frame; + repaint(); } - public void updateFrame(Image frame) { - if (frame != null) { - videoLabel.setIcon(new ImageIcon(frame)); - } + public void setActive(boolean active) { + this.isActive = active; + repaint(); + } + + public boolean isActive() { + return isActive; + } + + public void setCameraName(String name) { + this.cameraName = name; + setBorder(createPanelBorder(isSelected)); } public String getCameraName() { return cameraName; } - public boolean isStreaming() { - return isStreaming; + public int getPosition() { + return position; } - public StreamReader getCurrentStream() { - return currentStream; + public void takeSnapshot() { + if (currentFrame != null) { + try { + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")); + File outputFile = new File(String.format("snapshot_%s_%s.jpg", + cameraName.replaceAll("[^a-zA-Z0-9]", "_"), + timestamp)); + ImageIO.write(currentFrame, "jpg", outputFile); + JOptionPane.showMessageDialog(this, + "Snapshot saved: " + outputFile.getName(), + "Snapshot Saved", + JOptionPane.INFORMATION_MESSAGE); + } catch (Exception e) { + JOptionPane.showMessageDialog(this, + "Error saving snapshot: " + e.getMessage(), + "Error", + JOptionPane.ERROR_MESSAGE); + } + } + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2d = (Graphics2D) g; + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + if (currentFrame != null && isActive) { + double scale = Math.min( + (double) getWidth() / currentFrame.getWidth(), + (double) getHeight() / currentFrame.getHeight() + ); + scale = Math.min(scale, 1.0); // Don't scale up, only down + int scaledWidth = (int) (currentFrame.getWidth() * scale); + int scaledHeight = (int) (currentFrame.getHeight() * scale); + int x = (getWidth() - scaledWidth) / 2; + int y = (getHeight() - scaledHeight) / 2; + g2d.drawImage(currentFrame, x, y, scaledWidth, scaledHeight, null); + } else { + g2d.setColor(BACKGROUND_COLOR); + g2d.fillRect(0, 0, getWidth(), getHeight()); + g2d.setColor(Color.BLACK); + g2d.setFont(new Font("Arial", Font.BOLD, 32)); + String text = cameraName; + FontMetrics fm = g2d.getFontMetrics(); + int textX = (getWidth() - fm.stringWidth(text)) / 2; + int textY = (getHeight() + fm.getAscent()) / 2 - 10; + g2d.drawString(text, textX, textY); + } + } + + private Border createPanelBorder(boolean selected) { + Border lineBorder = BorderFactory.createLineBorder( + selected ? SELECTED_BORDER_COLOR : new Color(200, 200, 200), + selected ? BORDER_THICKNESS : 1 + ); + Border emptyBorder = BorderFactory.createEmptyBorder(8, 8, 8, 8); + TitledBorder titleBorder = BorderFactory.createTitledBorder( + lineBorder, + cameraName, + TitledBorder.LEFT, + TitledBorder.TOP, + new Font("Arial", Font.PLAIN, 12), + selected ? SELECTED_BORDER_COLOR : TEXT_COLOR + ); + titleBorder.setTitlePosition(TitledBorder.ABOVE_TOP); + return BorderFactory.createCompoundBorder(emptyBorder, titleBorder); } } \ No newline at end of file diff --git a/src/main/java/com/jsca/CameraViewer.java b/src/main/java/com/jsca/CameraViewer.java index 0be9ecd..e0d8f0c 100644 --- a/src/main/java/com/jsca/CameraViewer.java +++ b/src/main/java/com/jsca/CameraViewer.java @@ -38,7 +38,7 @@ public class CameraViewer extends JFrame { addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { - cameraManager.stopAllCameras(); + cameraManager.shutdown(); } }); @@ -58,7 +58,7 @@ public class CameraViewer extends JFrame { loadSetup.addActionListener(e -> cameraManager.loadConfiguration()); saveSetup.addActionListener(e -> cameraManager.saveConfiguration()); exit.addActionListener(e -> { - cameraManager.stopAllCameras(); + cameraManager.shutdown(); dispose(); }); @@ -74,10 +74,10 @@ public class CameraViewer extends JFrame { JMenuItem removeCamera = new JMenuItem("Remove Camera"); JMenuItem restartCamera = new JMenuItem("Restart Camera"); - addNetwork.addActionListener(e -> addNetworkCamera()); - addLocal.addActionListener(e -> addLocalCamera()); - removeCamera.addActionListener(e -> removeSelectedCamera()); - restartCamera.addActionListener(e -> restartSelectedCamera()); + addNetwork.addActionListener(e -> cameraManager.addNetworkCamera()); + addLocal.addActionListener(e -> cameraManager.addLocalCamera()); + removeCamera.addActionListener(e -> cameraManager.removeSelectedCamera()); + restartCamera.addActionListener(e -> cameraManager.restartSelectedCamera()); cameraMenu.add(addNetwork); cameraMenu.add(addLocal); @@ -103,115 +103,6 @@ public class CameraViewer extends JFrame { return menuBar; } - private void addNetworkCamera() { - JPanel panel = new JPanel(new GridLayout(0, 2, 5, 5)); - JTextField urlField = new JTextField(); - JTextField usernameField = new JTextField(); - JPasswordField passwordField = new JPasswordField(); - SpinnerNumberModel positionModel = new SpinnerNumberModel(0, 0, 3, 1); - JSpinner positionSpinner = new JSpinner(positionModel); - - panel.add(new JLabel("URL:")); - panel.add(urlField); - panel.add(new JLabel("Username:")); - panel.add(usernameField); - panel.add(new JLabel("Password:")); - panel.add(passwordField); - panel.add(new JLabel("Position (0-3):")); - panel.add(positionSpinner); - - int result = JOptionPane.showConfirmDialog(this, panel, - "Add Network Camera", JOptionPane.OK_CANCEL_OPTION); - - if (result == JOptionPane.OK_OPTION) { - String url = urlField.getText(); - int position = (Integer) positionSpinner.getValue(); - - if (!url.trim().isEmpty()) { - String name = "Network Camera " + (position + 1); - NetworkStreamReader reader = new NetworkStreamReader( - cameraManager.getCameraPanel(position), - url - ); - cameraManager.addCamera(name, reader, position); - updateStatus("Added network camera at position " + position); - } - } - } - - private void addLocalCamera() { - java.util.List webcams = Webcam.getWebcams(); - if (webcams.isEmpty()) { - JOptionPane.showMessageDialog(this, - "No webcams detected", - "Error", - JOptionPane.ERROR_MESSAGE); - return; - } - - JPanel panel = new JPanel(new GridLayout(0, 2, 5, 5)); - String[] options = webcams.stream() - .map(Webcam::getName) - .toArray(String[]::new); - JComboBox webcamBox = new JComboBox<>(options); - SpinnerNumberModel positionModel = new SpinnerNumberModel(0, 0, 3, 1); - JSpinner positionSpinner = new JSpinner(positionModel); - - panel.add(new JLabel("Select Webcam:")); - panel.add(webcamBox); - panel.add(new JLabel("Position (0-3):")); - panel.add(positionSpinner); - - int result = JOptionPane.showConfirmDialog(this, panel, - "Add Local Camera", JOptionPane.OK_CANCEL_OPTION); - - if (result == JOptionPane.OK_OPTION) { - String selected = (String) webcamBox.getSelectedItem(); - int position = (Integer) positionSpinner.getValue(); - - if (selected != null) { - String name = "Local Camera " + (position + 1) + " (" + selected + ")"; - WebcamStreamReader reader = new WebcamStreamReader( - cameraManager.getCameraPanel(position) - ); - cameraManager.addCamera(name, reader, position); - updateStatus("Added local camera at position " + position); - } - } - } - - private void removeSelectedCamera() { - SpinnerNumberModel positionModel = new SpinnerNumberModel(0, 0, 3, 1); - JSpinner positionSpinner = new JSpinner(positionModel); - - int result = JOptionPane.showConfirmDialog(this, - positionSpinner, - "Select Camera Position to Remove (0-3)", - JOptionPane.OK_CANCEL_OPTION); - - if (result == JOptionPane.OK_OPTION) { - int position = (Integer) positionSpinner.getValue(); - cameraManager.removeCamera(position); - updateStatus("Removed camera at position " + position); - } - } - - private void restartSelectedCamera() { - SpinnerNumberModel positionModel = new SpinnerNumberModel(0, 0, 3, 1); - JSpinner positionSpinner = new JSpinner(positionModel); - - int result = JOptionPane.showConfirmDialog(this, - positionSpinner, - "Select Camera Position to Restart (0-3)", - JOptionPane.OK_CANCEL_OPTION); - - if (result == JOptionPane.OK_OPTION) { - int position = (Integer) positionSpinner.getValue(); - cameraManager.restartCamera(position); - updateStatus("Restarted camera at position " + position); - } - } - private void showAboutDialog() { JOptionPane.showMessageDialog(this, "Security Camera Viewer\nVersion 1.0\n\n" + @@ -225,10 +116,9 @@ public class CameraViewer extends JFrame { JOptionPane.showMessageDialog(this, "Instructions:\n\n" + "1. Use the Camera menu to add network or local cameras\n" + - "2. Select a position (0-3) for each camera\n" + + "2. Select a camera panel by clicking on it\n" + "3. Use the File menu to save/load your camera setup\n" + - "4. Each camera panel has its own snapshot button\n\n" + - "Network Camera URLs should be in MJPEG format\n" + + "4. Network Camera URLs should be in MJPEG format\n" + "Example: http://camera-ip:port/video", "Instructions", JOptionPane.INFORMATION_MESSAGE); diff --git a/src/main/java/com/jsca/Main.java b/src/main/java/com/jsca/Main.java index 284aac2..ef58874 100644 --- a/src/main/java/com/jsca/Main.java +++ b/src/main/java/com/jsca/Main.java @@ -1,12 +1,20 @@ package com.jsca; import javax.swing.SwingUtilities; +import javax.swing.UIManager; public class Main { public static void main(String[] args) { + try { + // Set system look and feel + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) { + e.printStackTrace(); + } + SwingUtilities.invokeLater(() -> { - CameraViewer viewer = new CameraViewer(); - viewer.setVisible(true); + MainApp app = new MainApp(); + app.setVisible(true); }); } } \ No newline at end of file diff --git a/src/main/java/com/jsca/MainApp.java b/src/main/java/com/jsca/MainApp.java new file mode 100644 index 0000000..f22bda1 --- /dev/null +++ b/src/main/java/com/jsca/MainApp.java @@ -0,0 +1,124 @@ +package com.jsca; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +public class MainApp extends JFrame { + private final CameraManager cameraManager; + private final JPanel cameraGrid; + + public MainApp() { + setTitle("Security Camera"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setSize(1280, 800); + setLocationRelativeTo(null); + setBackground(Color.WHITE); + + // Initialize components with proper spacing and white theme + cameraGrid = new JPanel(new GridLayout(2, 2, 15, 15)); + cameraGrid.setBackground(Color.WHITE); + cameraGrid.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); + + // Create a container panel with white background + JPanel containerPanel = new JPanel(new BorderLayout()); + containerPanel.setBackground(Color.WHITE); + containerPanel.add(cameraGrid, BorderLayout.CENTER); + + cameraManager = new CameraManager(cameraGrid); + + // Setup menu bar with modern look + JMenuBar menuBar = createMenuBar(); + menuBar.setBackground(Color.WHITE); + setJMenuBar(menuBar); + + // Setup main content + setContentPane(containerPanel); + + // Initialize camera panels in order (left to right, top to bottom) + for (int i = 0; i < 4; i++) { + CameraPanel panel = new CameraPanel(i); + cameraGrid.add(panel); + cameraManager.registerPanel(i, panel); + } + + // Handle window closing + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + cameraManager.shutdown(); + } + }); + } + + private JMenuBar createMenuBar() { + JMenuBar menuBar = new JMenuBar(); + menuBar.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); + + // File Menu + JMenu fileMenu = createMenu("File"); + fileMenu.add(createMenuItem("Load Setup", e -> cameraManager.loadConfiguration())); + fileMenu.add(createMenuItem("Save Setup", e -> cameraManager.saveConfiguration())); + fileMenu.addSeparator(); + fileMenu.add(createMenuItem("Exit", e -> { + cameraManager.shutdown(); + dispose(); + System.exit(0); + })); + + // Camera Menu + JMenu cameraMenu = createMenu("Camera"); + cameraMenu.add(createMenuItem("Add Network Camera", e -> cameraManager.addNetworkCamera())); + cameraMenu.add(createMenuItem("Add Local Camera", e -> cameraManager.addLocalCamera())); + cameraMenu.addSeparator(); + cameraMenu.add(createMenuItem("Remove Camera", e -> cameraManager.removeSelectedCamera())); + cameraMenu.add(createMenuItem("Restart Camera", e -> cameraManager.restartSelectedCamera())); + + // Help Menu + JMenu helpMenu = createMenu("Help"); + helpMenu.add(createMenuItem("About", e -> showAboutDialog())); + helpMenu.add(createMenuItem("Instructions", e -> showInstructions())); + + menuBar.add(fileMenu); + menuBar.add(cameraMenu); + menuBar.add(helpMenu); + + return menuBar; + } + + private JMenu createMenu(String title) { + JMenu menu = new JMenu(title); + menu.setFont(new Font("Arial", Font.PLAIN, 12)); + return menu; + } + + private JMenuItem createMenuItem(String title, java.awt.event.ActionListener listener) { + JMenuItem item = new JMenuItem(title); + item.setFont(new Font("Arial", Font.PLAIN, 12)); + if (listener != null) { + item.addActionListener(listener); + } + return item; + } + + private void showAboutDialog() { + JOptionPane.showMessageDialog(this, + "Security Camera Application\nVersion 1.0\n\n" + + "A multi-camera security monitoring application\n" + + "Supports both network and local USB cameras", + "About", + JOptionPane.INFORMATION_MESSAGE); + } + + private void showInstructions() { + JOptionPane.showMessageDialog(this, + "Instructions:\n\n" + + "1. Use the Camera menu to add network or local cameras\n" + + "2. Use the File menu to save/load your camera setup\n" + + "3. Cameras are arranged left to right, top to bottom\n" + + "4. No selection is needed; cameras fill the next available slot.", + "Instructions", + JOptionPane.INFORMATION_MESSAGE); + } +} \ No newline at end of file diff --git a/src/main/java/com/jsca/NetworkStreamReader.java b/src/main/java/com/jsca/NetworkStreamReader.java index 6324b6a..8e4220b 100644 --- a/src/main/java/com/jsca/NetworkStreamReader.java +++ b/src/main/java/com/jsca/NetworkStreamReader.java @@ -2,7 +2,7 @@ package com.jsca; import javax.imageio.ImageIO; import javax.swing.SwingUtilities; -import java.awt.Image; +import java.awt.image.BufferedImage; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -13,43 +13,99 @@ import java.util.Arrays; public class NetworkStreamReader implements StreamReader { private final CameraPanel panel; private final String streamUrl; + private final String username; + private final String password; private volatile boolean running = true; private HttpURLConnection connection; + private static final String BOUNDARY_MARKER = "--"; + private static final int MAX_HEADER_SIZE = 8192; + private static final int BUFFER_SIZE = 8192; + private static final int MAX_IMAGE_SIZE = 1024 * 1024; // 1MB - public NetworkStreamReader(CameraPanel panel, String streamUrl) { + public NetworkStreamReader(CameraPanel panel, String streamUrl, String username, String password) { this.panel = panel; this.streamUrl = streamUrl; + this.username = username; + this.password = password; } @Override public void run() { - try { - URL url = new URL(streamUrl); - connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(5000); - connection.setReadTimeout(5000); + while (running) { + try { + connectAndStream(); + Thread.sleep(1000); // Wait before reconnecting + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } catch (Exception e) { + if (running) { + SwingUtilities.invokeLater(() -> { + panel.setActive(false); + panel.updateFrame(null); + }); + try { + Thread.sleep(5000); // Wait longer on error + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } + } + } + } + } - try (BufferedInputStream in = new BufferedInputStream(connection.getInputStream())) { - byte[] buffer = new byte[8192]; - int bytesRead; - byte[] imageBuffer = new byte[1024 * 1024]; // 1MB buffer for JPEG - int imagePos = 0; - boolean foundHeader = false; + private void connectAndStream() throws IOException { + URL url = new URL(streamUrl); + connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + + // Add authentication if provided + if (username != null && !username.isEmpty() && password != null) { + String auth = username + ":" + password; + String encodedAuth = java.util.Base64.getEncoder().encodeToString(auth.getBytes()); + connection.setRequestProperty("Authorization", "Basic " + encodedAuth); + } - while (running) { - bytesRead = in.read(buffer); - if (bytesRead == -1) break; + // For DroidCam compatibility + connection.setRequestProperty("User-Agent", "Mozilla/5.0"); + connection.setRequestProperty("Accept", "multipart/x-mixed-replace;boundary=BOUNDARY"); - for (int i = 0; i < bytesRead; i++) { - // Look for JPEG header (0xFF, 0xD8) - if (!foundHeader && i < bytesRead - 1) { - if (buffer[i] == (byte) 0xFF && buffer[i + 1] == (byte) 0xD8) { - imagePos = 0; - foundHeader = true; + try (BufferedInputStream in = new BufferedInputStream(connection.getInputStream(), BUFFER_SIZE)) { + byte[] buffer = new byte[BUFFER_SIZE]; + byte[] imageBuffer = new byte[MAX_IMAGE_SIZE]; + int imagePos = 0; + boolean foundHeader = false; + String boundary = null; + + while (running) { + int bytesRead = in.read(buffer); + if (bytesRead == -1) break; + + for (int i = 0; i < bytesRead; i++) { + // Look for boundary if not found yet + if (boundary == null && i < bytesRead - 1) { + String content = new String(buffer, 0, Math.min(bytesRead, MAX_HEADER_SIZE)); + int boundaryIndex = content.indexOf("boundary="); + if (boundaryIndex != -1) { + int endIndex = content.indexOf("\r\n", boundaryIndex); + if (endIndex != -1) { + boundary = content.substring(boundaryIndex + 9, endIndex); } } + } - if (foundHeader) { + // Look for JPEG header (0xFF, 0xD8) + if (!foundHeader && i < bytesRead - 1) { + if (buffer[i] == (byte) 0xFF && buffer[i + 1] == (byte) 0xD8) { + imagePos = 0; + foundHeader = true; + } + } + + if (foundHeader) { + if (imagePos < MAX_IMAGE_SIZE) { imageBuffer[imagePos++] = buffer[i]; // Look for JPEG footer (0xFF, 0xD9) @@ -57,29 +113,18 @@ public class NetworkStreamReader implements StreamReader { imageBuffer[imagePos - 2] == (byte) 0xFF && imageBuffer[imagePos - 1] == (byte) 0xD9) { - // We have a complete JPEG image - byte[] imageData = Arrays.copyOf(imageBuffer, imagePos); - processImage(imageData); - foundHeader = false; - } - - if (imagePos >= imageBuffer.length) { - // Buffer overflow, reset + processImage(Arrays.copyOf(imageBuffer, imagePos)); foundHeader = false; + imagePos = 0; } + } else { + // Buffer overflow, reset + foundHeader = false; + imagePos = 0; } } } } - } catch (Exception e) { - if (running) { - SwingUtilities.invokeLater(() -> { - javax.swing.JOptionPane.showMessageDialog(panel, - "Error reading from network camera: " + e.getMessage(), - "Network Camera Error", - javax.swing.JOptionPane.ERROR_MESSAGE); - }); - } } finally { if (connection != null) { connection.disconnect(); @@ -89,12 +134,14 @@ public class NetworkStreamReader implements StreamReader { private void processImage(byte[] imageData) { try { - Image image = ImageIO.read(new ByteArrayInputStream(imageData)); + BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageData)); if (image != null) { - SwingUtilities.invokeLater(() -> panel.updateFrame(image)); + SwingUtilities.invokeLater(() -> { + panel.setActive(true); + panel.updateFrame(image); + }); } - Thread.sleep(33); // ~30 FPS - } catch (IOException | InterruptedException e) { + } catch (IOException e) { // Ignore individual frame errors } } diff --git a/src/main/java/com/jsca/ScriptRunner.java b/src/main/java/com/jsca/ScriptRunner.java new file mode 100644 index 0000000..2257c11 --- /dev/null +++ b/src/main/java/com/jsca/ScriptRunner.java @@ -0,0 +1,112 @@ +package com.jsca; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import javax.imageio.ImageIO; +import java.util.concurrent.CompletableFuture; + +public class ScriptRunner { + private static final String TEMP_DIR = System.getProperty("java.io.tmpdir"); + private final ScriptEngine jsEngine; + + public ScriptRunner() { + ScriptEngineManager manager = new ScriptEngineManager(); + jsEngine = manager.getEngineByName("nashorn"); + if (jsEngine == null) { + System.out.println("Warning: JavaScript engine not available. JavaScript scripts will not work."); + } + } + + public CompletableFuture runPythonScript(String scriptPath, BufferedImage inputImage) { + return CompletableFuture.supplyAsync(() -> { + try { + // Save input image to temp file + File inputFile = new File(TEMP_DIR, "input.jpg"); + ImageIO.write(inputImage, "jpg", inputFile); + + // Create process to run Python script + ProcessBuilder pb = new ProcessBuilder( + "python3", + scriptPath, + inputFile.getAbsolutePath() + ); + + // Redirect error stream to output stream + pb.redirectErrorStream(true); + + // Start process and wait for completion + Process process = pb.start(); + String output = new String(process.getInputStream().readAllBytes()); + int exitCode = process.waitFor(); + + // Clean up temp file + inputFile.delete(); + + if (exitCode != 0) { + throw new RuntimeException("Python script failed with exit code " + exitCode + "\n" + output); + } + + return output; + } catch (Exception e) { + throw new RuntimeException("Error running Python script", e); + } + }); + } + + public CompletableFuture runJavaScript(String script, BufferedImage inputImage) { + return CompletableFuture.supplyAsync(() -> { + try { + if (jsEngine == null) { + throw new RuntimeException("JavaScript engine not available"); + } + + // Make the input image available to the script + jsEngine.put("inputImage", inputImage); + + // Run the script + return jsEngine.eval(script); + } catch (Exception e) { + throw new RuntimeException("Error running JavaScript", e); + } + }); + } + + public CompletableFuture runJavaScriptFile(String scriptPath) { + return CompletableFuture.supplyAsync(() -> { + try { + if (jsEngine == null) { + throw new RuntimeException("JavaScript engine not available"); + } + + // Read the script file + String script = Files.readString(Path.of(scriptPath)); + + // Run the script + return jsEngine.eval(script); + } catch (Exception e) { + throw new RuntimeException("Error running JavaScript file", e); + } + }); + } + + public void saveScript(String script, String filename) { + try { + File scriptsDir = new File("scripts"); + if (!scriptsDir.exists()) { + scriptsDir.mkdir(); + } + + File scriptFile = new File(scriptsDir, filename); + try (FileWriter writer = new FileWriter(scriptFile)) { + writer.write(script); + } + } catch (Exception e) { + throw new RuntimeException("Error saving script", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/jsca/WebcamStreamReader.java b/src/main/java/com/jsca/WebcamStreamReader.java index f840e2e..09422f0 100644 --- a/src/main/java/com/jsca/WebcamStreamReader.java +++ b/src/main/java/com/jsca/WebcamStreamReader.java @@ -2,7 +2,7 @@ package com.jsca; import com.github.sarxos.webcam.Webcam; import java.awt.Dimension; -import java.awt.Image; +import java.awt.image.BufferedImage; import javax.swing.SwingUtilities; public class WebcamStreamReader implements StreamReader { @@ -26,7 +26,7 @@ public class WebcamStreamReader implements StreamReader { webcam.open(); while (running && webcam.isOpen()) { - final Image image = webcam.getImage(); + final BufferedImage image = webcam.getImage(); if (image != null) { SwingUtilities.invokeLater(() -> panel.updateFrame(image)); } diff --git a/target/classes/com/jsca/CameraConfig.class b/target/classes/com/jsca/CameraConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..b9bfaa8bff2021214353d0f45cb544fa1181b610 GIT binary patch literal 427 zcmZutyH3ME5S)#jI5>ekAUsMspui;sh{h69qzHRD)Yz?Yx7Je`nSFhB i1&qYlym-gpjY$Lhe735lg9GN>tpFn&a>d~*)$tE9B~{}9 literal 0 HcmV?d00001 diff --git a/target/classes/com/jsca/CameraManager$1.class b/target/classes/com/jsca/CameraManager$1.class index 5d1025a498a0814b9111bbfd57cf4b30d6f7958e..b93c88a131cafe16b0f9aa0f27df465bf0e4361f 100644 GIT binary patch delta 14 VcmdnTx{q~39upIz?c@R`KL8`s1Udi! delta 14 VcmdnTx{q~39upHo@#F#~KL8}#1aANU diff --git a/target/classes/com/jsca/CameraManager$CameraConfig.class b/target/classes/com/jsca/CameraManager$CameraConfig.class index 93c269160e719d022329947834dfc08be80918e1..84343e67d4f2a35beaa6a3b74700fc894fe772ea 100644 GIT binary patch delta 14 VcmZ3=vXo`RdqyV4xXB+GO#mmp1q}cI delta 14 VcmZ3=vXo`RdqyUP*2y0kO#mo+1v3Bu diff --git a/target/classes/com/jsca/CameraManager.class b/target/classes/com/jsca/CameraManager.class index e1dcbd41ea740cc2031f42791ef201acc3fac018..0bd6d7e1d0ba6e73fc58f8a34438a5bcca201308 100644 GIT binary patch literal 14775 zcmd5@33ycHx&FRCGjk@B!xjiI5HTQNRw9U~2`&&8gGpGzVxZMdl9Oa$G81PBL9Mll zrE1;l0xqpu8*8oBg$aS8Xj|=KYi(_-s###cEHGxhm8mJG11MODSAQ#iFIje zQC5%Dl88s5jaIZf*a9kNW-OdBZE^NEP~>WlMZ%Dnrnx;5X%AVI(p0$=e}wh+Xs}f? zz%))T+EW?Z9E9|i*=#X+<_E*U_~}d&iiaCevKG74L|QE$!%{76R z4y)Cc_lig)#8g~d^66wF8_zH)k4%G(2Q{M9p#oZ%2!>j%sE=k+f&3{osf^0ORm_U7 zh+6UZmKD)pI1X_!9gB^Rutv4apJh@dodA)x03dPbapGYnRSR`rp1KEqL@AU&cn z9%$JF0vv&j479dtLE8RA$ZarbIjsO~?G|X8ISg&XP-4(3n9*?V!AC1;wMlCT*5VDu z7PQ2J-B2IwWWDTmj!8|l9)=VPUSK!Z`k?pEnDkj`o(Liute*IS?m#dkieBOHE6biI zHupKqh3uiNW@VJWaVURe|A?23fpcv9lcZj?nkObUu+ z`=S=K9c%@BrFoM{A!#XsR|0gHGvb>;GO)T23hl*CG^D4&5PNm~si|EtMbt<+ACx~K17L*gi6+WX&>^w+E~$|Xe8K*setZN$adDmtY{dR z$LkO9nxe5PAKB&8COtzx0))j+!3SBXv*? z{Cy-W{=f<&gA{guV$x6PS@CSe zTQJ(0GRM<0mL7Eb87Ol+Qbvm-RR9J-=^=W-q+h4}hp;TR^`c3?l{ObVY^RSNq2HPG zdwLma(>7ouK|)l?h8h|FN73G&5C@7k)Cdf>(W@rCCc)7N#uj&W#kY8AKa+n&$O^=) zB8bq676no<1sD(szG>2*Wr{Btv)z={>ZP}tRONIp{gsJi-FHm-JH4ypR#zy{Vj(mQ zNi`hy4rMqqz&nGy^d1~YWp}XEipc8!H0fXTZ&<7qZjG&jOE?T5*XP5ld|=Xt^dCgL z&cG%MYH10?WeT3)X2Nl}gRC?o1?EEu6BqT7NeAg;KtQ{wEEj-744jm90htc{)3oSh zUSJQF648*hT}dRfjCK&`Frfr2${Cj11(SE43$v%B3{Ce+{uzvE%WMgB!ulbS?BQtx zgRrAkED=HqmIHc(D#wEHDBFpLK>&+dosn)Ug#z1ENM7}-mq(x*!4W&*q!=rTM~dvh z=;l!-kLEGhWRta}QS!VosYs|#hiIzK$GD5KTkGzlH=McvwBS~sMCV?(h*Ve2Xxzgkl#QsOX+-*`Mv+(j97_<(bRYfoto@??+VlE&VJ7|NE=95i6g-->TVu|LM z&e6n-vQ83?F(A$20+Hx^lTXv`q9YKK%-G9kV7_ApRYf9Fo!a>9pljzj9{Yu9>QR#_ zVQR6-OL(b}WyQrqOTz9dUSF~{pA>x{BbJ$bwgjOO7%|9pKIL$|$ql?5sEsC&5Z4zU zb;H9PzcP$=RU#ag+#iTq$&CiDVw$dvTq6qEQL1<_qzYj2YF-08K)9VjpzK5^0%+fm zUNjTC8Ee^v2w1GAO_wBaShq>Qq{Eq={1M$R`(l;N{N%j;#{5 zVX}gbb4@;vfpag&isOs{Kbgu^Gz&Ey0Tf2gPgvm=tGWc9)=eK6j1y2{`XJ`u0wA&i zovTor!I_?$*+LXz9Eg_N>N(_~m?3-zf1jpB>?s${%ASHWB$lx)iW`HP9)_%HgExV{ zVfAW@IFC1Sr^#WCKq)N&Fb%gTumtBGJ|8hgOvxMTNW@zso5SMf;H(ndjp6#74*5yh zVQ`C@NJx@=Bq)GZk9a${fpNq2%VHOre36{^+%1UBQNiNHCVxTBNO`F}8zP(CNT}o= zcP?MXmm9nlz!_XI17NFzoz@!EmrVW=Z-cjJTwJrfVNoLvW*HO?GhSQ46((QFS4kGU zIS`DmwxXRu)bYRw({aVijdLLiS*BX2c7SOb5IN8M{ldt3JVc3pM*^NUzwB1@Q z%iL)4S4Aqh@X%H?n&^rfd~>RjrPr-Zby%&7ds-}g0>HrlKE*MN!;XUWjfn-iC1q#} zwkHs7z@fpnL61UqIxSri3|TcDkq9EBk9Sakm+wRh)rgr+ZdjDH0><#yO}h|odW;O_&6X(wa{2* z$>iTTt^pwQ@yq;2ga43{SjxI0n`IZ1|HQArz;OC$OmuZ2>4?cuYhosqurGVC#>{KH z-{99%t3jW&%hQHr@*9$VNR`GZ6XlaZAHXNI4H^Ybs2YjG8H_}@%^FV-XBJ*z~em$5bxmhV}rB)ksDFf|N_pfQ(vg68tKq51#xvWthsV za$&}8(MYFlI;m14lws3oAN=h~b=Cq-Sk)^#Hs)1%fJaEm^sr(z!c-#(-qjVyhF&!q zl!fj3jr8W5Y|uUOG|ptN{WRX>T)M}{2l%CY@(QSt+Di&E6sZnv@La0URL4lID-W>K zIg?K9y`ey7b8BGw2}}*Pouy47ed5v}NjDE3^=$CS$sw~Ku~@1bkhE7#1tT5tcvn?r zC5i;A?a($`POlo%N2;(51-)I^mr6p9Dlt{591UC%U`-mz zO;w?0*^ark6D4H$wiE`0p`f%?E=;twSy8K1k3cS(t4=@;uRzbTBc_LkTOkt)=`7o) zPE;ouYVMF?S)+V$TZ0dh_=sV}Of^rP4BZ98-H}aJ9V+cmbt)`sXri&zMu4m~}Y9^#R*~ z_Z)c3!w?|De2tMrw8gS>m;9^%F-tf^>{%Fz#Brhsbk$q&j!0|Fr_NIWL*allP0K(_ z$A(U2owj!j478!B8HsK&RkLaV{hfnNOwVS|=E%;WoR7e5lziaYRWUDjdOX{l6EUBkK(r@^oU$Q3SEctKc~d zw(FH%1uyJ%sxds8#)||tn`)*y9unwAgI8S$2o4reyz29qVWY5!y$WBqj?o|PBpHOq zDzcLr=R|_4i&uRSW^50URAIO=&c<6!)u>iuYlM-EcOTwy-vE8^d^A%3;=4Y6$6(GFw8D|%Z7Wt&_&uexkH(em zp$ScWVeb2_zapz$LlSM?8)gZ{Bowj(pZpcX&DGOThHf-XsiQ8 zS`O+qNYG1!6H%QyE$8%Js*qie!LI#OuV^2wtaABXPt#bxD@lzzD9`VzJLa@r%G;X5 z#~61FyioQGt&}PKv<^DCo2p8awBfWpw6dwNkIt==u0pI@63ylL%J__x+@;vOWUekIrH4Tl<#*HxGM_W zbCPsL<5oB03X{sPZ!WR@0I6nL=o_Cd)%h|4L2|0^no!YiPGSx|c=71If5 zosE_`c&dPcPr{v!d64|c)PT2jP;?tq9ieLKM$hNzG`fUNrz>!~<4P3KcTf#|jTX^; zxUKOmXzshvT@tgO!tb-t+|Qu0=c%54h1Fid`>VJ)@g}XHw{bb)U0Q|j6pc{zYR*B$ zbR;w}md@cq+Mtoq1%rTs9b_n;Om_ee$XwXQh=Tddp*yit4tOchXmNp}%RrHs$#Oaax^0GPt8dH4SxpzfjO zrajc#l;ie#`{-MJ^gzQNdZ=mixITKg$~E}t-b0V**2k(mduS(|Nne#wv4?gw6PtbZ?8M{<_8gPY^SOI-2Aul|B|G? zVSaA@Ka%wR4l@0@yXn7(s&Y#U{keXho@;O~x&1zWUd9`q4-8t-o3I9=^I%1v1F8bB zqGlkk1y?R1DZ3H4W0rw$TQ73xBu%Za8s0&tfKCCDTD~iF2x`FG> zu%I5`bPGLB7vNj-h4_wo5xtC&f5GjM_vi~WK$ozGzR2TnWn?m4#wEDKQASsAC4T49 zm3%5)#Wg5&)YH{`F79Wv(Y4%;I~hT`o;T4A9Kw5qZsabyiF@c~zKm|+t8osznQlWt z@OHkB?%*Vz`(Zue`md27#Jkhhw<~RZN$sJBx4t`Iub&`9s@ z9b^St_khEHvWwkFK<46#iU-!`1@+x>!lhjB5Rwxv`M}8rIpM++APTUe5r^330KFl0 z=~!d4K@aJFA2Dbr+P2yM63rMu=sNni9IPbM24KTSAMJ8R>k0a{;8DoPn(%3Ma1ZW) zO+Ezjn0+bVg74}s9X*>RTJ^fhPGqix4kq4i=D0)8&@$pS1*QPE{17k_>l&k$~_7h zJ_as#LWX^i;V#5eBrmiZaq)Zb8G9kWC*Vo;(WUexU5l7{GkqWTl=dN}JxveNkHlGG z#Ist)62Bm$w2ZHHWV{gm!_K4DQHNBCftVnj!VB=^!pa4_5SHi0TDg$Cw87Hg#oQAH z=v*>v{H=7%$Ux|&q5%-@AXz$y4j_3{axDrvl_d2U>>UEAQI!iVg@R4LYiJT_=dI6SgI_>+&jUiggadv7Ny=MD58l?o zm}_tE2m^#@mjxe0XKQv9W}c+Ql7o3;crC9(T5%5gO|W3MG(i+fE>hS`L7X1RIH`MB zrUV_M;_3!SY=b4mPL13 z7Tw$qh0AH8Tf1Ay;=8rGbz$Z6w0zuHZwhyC5Zqgiq>Qwr?3N}cb@CD)q*^gw#m6B; z+F*DEU~pmORVkM21f4f(cT?`_=a3@FtF^whFOoOgGo>N(G>xms5!g(2eQq~`n4m_S zk}Z>&2*m_VjY04!K&Zrb5-LNWna?gd6JcgCLd;6=D1oiirY0?qRgOHy&;pGEh0s0T z#zvY?yRQkd65|+2>L8UFyzwB-u$@&DGi+`@#Ofl)4Z=`AlKf)X9 z${eQAq8-Ou_TUR}^A;HBXB@3X1k=ayC6Dl>{rqJYt?MOU-7fx0l5cnzTb+pkWHG7-wNc?^LP|kAA{>2`N*%w;)+OtX5aS38yxlv5qAarOsnyQ_!hnuo;PI% zc3%_vgz00+HGos71V}_}xPdVPI77f?;52CPO*q-|_LLOVMi2&+a?T#!(bUJc*OiUs zJCgjhvORov(^&pSAAd8+y=9!_Z||U2xSkvK@Iy^yNq)GGcOnJlKFsQ`azhA+YrCBQ zR%P_kD!<`~ac`AJtmO%bX)CL|ey{fHbN294nEL}s_N4oVsgSpme}on<9R5!JiQnzd z?dNBKJ6TTa;C}2i8|^USpCg36FqVIVI1rOF)VF@MF2luoa zF>^1MqqJT^PjUsl&$EzaRU+p)fj;5c$i`+N`iPk^WWjW!vSo>c!?!mwl51U2FsDBWf7H@l;YCBcos5UOqs+1^CIu3b@2)kC2Yn zP>tY!rE#|!n@Uw+bO!GJEw}^HKG*>+HPQ|^NIR&*%xpjK_Hk2b9}gszN-EDjy4XH- zUuDBDl|KpmCZ^!0)2Ml#(gF_>sWgc8``vcpfT1<1q5M>pXRPx1J&!39nZph$8LLL6 z-nP@^Qal5k=pG|~#>$@pADfurg zu$!bd5dd5w=G2_|K!X58XV7Z0Lgm3d`GKvB8+~^TysaLpaTv#5U)fky zlvL9O({#JkwiOZaa+KP(Y59x9Fqv;rGq8|IEC+rEJD|*K?*K(Ap_Y0|ld2dorx3qo z5~xR%mG4%SM7z|Saq5&^s%n=yefz-vURqdhM6H5Nq*{_{#b%h< zOVb)sElBmyQt?FUyqT9rTCqVB!rM1FV(-cRM*4kiGBmszX+56 zEvharf!W^y-Y=tg`6A~25ohv0Veac-em`cvf?Cz9SmSlnr1oQ_*MODR;obIAJI;_n zJcseT8TBbi6C_TKpmC}}Er(3Xoam^qh6@5*;RtXNdA$cI-%u+Lkozpe!F7gOx(09> zJpyt?72DC=OpF)UcuLEXYK^GLF2YJdm_W7chT>lZ{j^P8 zsjkvbUr|@9YxL8#>N<73e!4;3sJ^O1SJNPukMWCtaAk$n&G>M5i`tGtt)KTokGFer O)g9>cAX0qHPyILIRohnp literal 6131 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 a)U!*3ITzqu4;~f0o@)LlPQMsH!~Xy_J%r%^ literal 5242 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 diff --git a/target/classes/com/jsca/CameraViewer$1.class b/target/classes/com/jsca/CameraViewer$1.class index f086b80f6631b45f23f9d264395bdc5f73c819af..4b31bb85b423b7b89acbbb976c74e277b4d842d2 100644 GIT binary patch delta 33 pcmey#`k8gZaz+7;;*8Rgl>G8MMh3=IMh2P5f{ZeoZ!#({0szA63Dy7r delta 28 jcmey&`jd6Taz=i>;*$IV$DADJ#N5=P#Ny2x7@Zjbqx%Z4 diff --git a/target/classes/com/jsca/CameraViewer.class b/target/classes/com/jsca/CameraViewer.class index b4ace426d2f7df59ae8e3fd05c25f755e189242d..f60d2afdd7a9962e377ef530f45295b3fb803107 100644 GIT binary patch delta 1454 zcmZ{iS#KLv6vzK3wlmK3CNbWcm=H{x*u@Zs(n1SGLa3E!5UNnZ3le>p*rU{nJ>z=D z1k@nY4}f^M54`YzfG-e%BG#6DX=z%Z>`N&WO4-W36FB!~8C02vnS1X4{LZ=O-gAzR z-%~UXEk1q#z!usxXWkc0^m=>Tnd>IqroW?NRo$s5o|1RR_lFD|lqb6eq6hGdz_WNx zZtI!oC&Uy!=6HUiS@J8c<3;fz;SZTlj1QKqn%%Inb<44<*~vSyrD~-#U2&$eu9L0! z^4`Q&xtJIqA&6$ZZ29(tZ~0Bn#5TMla0n@a;o1Jp_U%4fHQV#7DO+yr?vs0yBM}@E zcuhW-ygq`r1m2cslS3`MC-6Q5A?odE`ek>%mv`CzjN6#rt_u&i&5HX>;B&?GFn6O{va07&SF{v%M&L_@B^!3lz0IDm zt9Hq^%jd#ZD)t+JZxx$r*q(1S{QrZusB3&L@PvX_ir=clp9G#)qB-F<8}dx5XyAAJ zX>bDu)44GI!ryXd`a}!A#jwCUk-!lw^cs9!EG%oC0>`x$3A8l=?`X{ow3xsNt@Q?4 zO5jtiB?B!Za9V5qE7};*g#uq`ad1T>8WK3Gwe^8^vA_>n+rS!6#+c*SjqO#-^K8$c zq8!Z>!j};Sm2S5Gll6u8L+!HqOg1qb!ahzr%V~X1duQ9U zFUi^AzMJrJP?*UD0(T*b{W>D8_g%|{fHNJ&tDsni>fCZk*YK*MqJ>8=Hjkt7)Np=1 zUdJ1n<8HR+-ju~$UaWH5VQJ-3op`q}j}PQ*ZZycjoo=7wV|h3?AaBU6*}R&EC!jrh zD!64Q6TmKcZ7v-g!ei1t@C$iMULO*$JU#8%ujTzqY<9IEo~mW`Dqht?Mfir_$Yx=^|~dqhZQvD^CTwgf8V1;(XR_UQ_(h2#wN4^wZ_MQDeRPUxMO2 A#{d8T literal 10580 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 Y->S&bgMa)ff5!jBf%Gr@kgT)(Kk>cE@Bjb+ diff --git a/target/classes/com/jsca/Main.class b/target/classes/com/jsca/Main.class index c32667ea3e8f1687511e7d01a8c5f056006f57c0..d097f97244d6370eb7fd369205f2b20252e68c7a 100644 GIT binary patch delta 730 zcmZ8f%Tg0j5IuJi=Z0ZOVnPxg0-_*EPzT?Ss39U?B+*5{D&1YetvE25Ol77NSNIFm zl;5B(*~AoaFfKrQ?|$TgNn1 zhJ-`}8|tv_cp zd}d(uKUO7%kwjkpkusM3jPk8mNVh3Mhs>Q7qy-DO{k8L#c91Mw41FG=ZGJ zWs4YrL;MqrXCHBCqZsYs@)03Q=oTkeAc+{gk`bgr7o8X}n&LV=Pf-14*nJDP=^NB4 Ugx$v~*04@UaHxlPgc`)zYrqMlWc*1J`-DC#khR4RgzqzSX5&frQPNw3GSF*cGD|G-F0 z5E38Ifp72`Ot3Ez6WPhy=d8Wf+S!+l?>4W0JU#=+VC8~e^EP_6I%q??I%9)oCqfpw z)D?^B-3Y5XTd&zgQQAKcBhwZl44s8aDRt--L@FyvvMTbfJe5_Begu3NbTEWr2JXqD zyz9C9Maf{q_r~)K=E=8~A++^x$O_qZD#z7=%(=cTur1tjQr&1_Gl5A96Y52a=~GCm zH|;ex9)!ukFzcWXW{Zg#A{d;Fa#>chMd5kUvyf4rdcs&@h$x|l)S8iF4eH#8)-q&e zbbH_tLEr%$xAf7#2|0DZf{jKh!1)oP2faX$`|F6YXtGL5Kcx8y{k!*wZYGR74E!ZE vL#L@t6+jc^{-)VTH(&&#G}JLhs3C?pb$?ZgxM`#@hk0WB6Bn?EWrV)~cvee= diff --git a/target/classes/com/jsca/MainApp$1.class b/target/classes/com/jsca/MainApp$1.class new file mode 100644 index 0000000000000000000000000000000000000000..5e2c3d42f6e6d20d9d4c8418b086dfe4478d9046 GIT binary patch literal 725 zcmaJghTxX3UpgwfEyNJ8Pah0;f(QGMTq!%({r zm%+{&QTX|)-nzTB1t5Pls}V+If)X3*Cc$F{sC4lTlLAx&~lNdc!8|<iE7@narLkQ@!2y;7A@F^C*U28vI7Mud#VKcpu-_J~463)vq@0xi literal 0 HcmV?d00001 diff --git a/target/classes/com/jsca/MainApp.class b/target/classes/com/jsca/MainApp.class new file mode 100644 index 0000000000000000000000000000000000000000..4e53596538991a120232c727e1d711819f121eda GIT binary patch literal 6357 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 CfxeUg literal 4443 zcmcInYjhN68GgR(CbK)4KnPjr0;Nky+T37Ufz&1xN&=D4-Gt`WA}uo6og|y=?yR%3 zgoxLQT5qjdt%Z7lN}GCV6)k~6g;p)q%JG-ysJ}eN-~Q`A?J+*z%w%^{R_z~il9_Mb z@4dgz`+i^E{@3NJ0PevJ1yuqY5@sekoloe|0mGg(t(jrlGW5)lp(hPXfnT70T0f>o z(|UF?IygRUBy0sW0->T@xqv`*PR|-?fi05f0r6o0HDPA6G@3HAbk^n18g_KQZRbXZV!g#(O4jU5WmEPpf%cZl)^9e~ zIws)nHIs%4A69EviADimOY2w=j6p*!G`YG(gMxrutVcYsfb{+bgGF7Z>i+H<;hae#{HtLo3bfG?a)w%6)IY>RYMea39QN+wr5}@l`+hM z&Fr=Gw~k4s4`|pXnO2ZV!pGxKutQ+IV<}}udkPa1hGiuCv$=xpB1g*Jqv3{L3s$FlUfyoNq}hjF643+^%-<4Fpf;5~X3iKf-8L*)o-!*%eJ&n8qT#5_gPKg`y(U&LzAO(# zzt{i;IS^JX!z=ruD#f$#2UX-g+`|DgK4%+wi9k+}e(ABSkPAG4fb(_G zWK-5on1yU|zccB`nxd$)xmUM!ezT~dvgnGw(qP@NttV#Udd}%`1yc$NoK8iM(*Xxa zWr$T_V^#w80TpvNq2OawF9nWA9itMw($D@uiPet_?D!9;c3skEZC9?;QBu0Qfe8Ji z1jwfZ_P#HGNKKCA%GjtT;^faJ6`#fD6r5aYmnCd6eHuQGFA(tgv|;4rL^#korr?W9 zrIw)$7SpHT%bbU$Ug*yfNme0e8_6OMbva3)tx~wIlM0?BOdRo|=QNqOOR?y(rzXIUl>+V4@n3kSFP4h%XCD^E=o z-^CdXXYrhzz*FX|=knf^o;G<*?)0q7eDs=`oS8MUc6S+c-DA6);yJ|0T}s;BtXG*P z4-?GS3wTk%JUy>;qHFje&asyYxgJ5@$#e0R3d3ou43*XGF-<#f zTY4^T*i&XQui|C=OufCa>(Cu4F-YNV_;F`h7 z@@t%QAK#5_7tzq>zkpC%`vt6R>$rgMc}HqJpKGWYSWWHq)Y?crA%3mF22vxgJZ{BC zeo>QSUpOo2K@~0CMyg}a_ZjM_1H{x7L=LsRg78J$9&2k~K=bQpy?}O-+E=Z9d;wdE ztGh*93|zr>$xg=3i|Fd|uZrNcKv#9ceS9ju&Va8oIFHj|f4F)9yE<3JW4Jn`ZsUGPF8oKg)&Kbng4E&f42FWIa3HLN0w)8)VH*hr z{O8aiN!3Xyv`H@g$!+z2kqWrJgr2V2ws7qN_S1<_Ago+HiyAIZszRDPyLgh0hfXgV zVgGd`!~O-t=J7~eeg&Mbj%x!QZy~THK99PNEzcv+v1I{+9oJ$~?iA|U+Am`ixP*sg z2CiePn}{RxXzJ2jlIoGd+O_3`^UU{dyd%qjsHhXnJ+r@AZ02zTu|xFn(+64OAEAxU z^~A~stYtMfv7YU$?ryI7uo*ElVGzw2;;nuZEjYsa@gzAi*n$(-im&l5{uJ)Q)4Xdw ziye58SLc^;FJ46#-o$CFMr#M^5-Vqynxo;xv0 z&xXZ*t_QguO-pbDBF!2FfhTm`|uJyoFaCQ;0dHK zP3*@oj~UVw#&Qm6(gKX=ETfSBKuzN@WcgI-Z4f5;YU$T1=bz-jL zGCtv8t`VQUgvaLLkDsR~h=|R2+;H%Uta~p>5U93~^#3GAQ<%FJ>UhAwg;pL4G{%ayfiX2%!Ij)?RSi-*? zSuUI#2mm<6PtnXY$5q;KMRR36SEN}E&eio*@)+ZmOv(^NWu%SNy_OT_smu5_=jpS) zOZa|iCb}`l$veP_Hps*uWbAPodhljL)XyW))4z?{rT(C&|M`+qIh?@+%A~oT20c%G tY*0DGe~h2-y@6i;f`6~zBIkl!y^7cHOZ=Mhzrl5K$iw+9{0_fI_}??pL;?T+ diff --git a/target/classes/com/jsca/ScriptRunner.class b/target/classes/com/jsca/ScriptRunner.class new file mode 100644 index 0000000000000000000000000000000000000000..ede3742805ad7791f0f25e3a1d003173e465ab12 GIT binary patch literal 6276 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 E1^lJ3%K!iX literal 0 HcmV?d00001 diff --git a/target/classes/com/jsca/WebcamStreamReader.class b/target/classes/com/jsca/WebcamStreamReader.class index b577b495b6f0a8df1230550cb9ca9f8078787bf2..cea23efbde001f878ec638afe42be3c20cddb3b3 100644 GIT binary patch delta 57 zcmca0(IL6vCKGd@hR;L+X-4bG>zU*>b1>U5GRlFNlS5ey8D%DKV3L_Ul|`3HQ3EW0 Ig{6ia0OKnVVgLXD delta 80 zcmeB>ydbgRCKI!>hR;L+X>Mzuti-ZJ{lxMT{mlZ*PK?|F2!YAjEaqH%aQVr*Sd4gt MH4yTf*;t#{0qG|gYXATM