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 0000000..b9bfaa8 Binary files /dev/null and b/target/classes/com/jsca/CameraConfig.class differ diff --git a/target/classes/com/jsca/CameraManager$1.class b/target/classes/com/jsca/CameraManager$1.class index 5d1025a..b93c88a 100644 Binary files a/target/classes/com/jsca/CameraManager$1.class and b/target/classes/com/jsca/CameraManager$1.class differ diff --git a/target/classes/com/jsca/CameraManager$CameraConfig.class b/target/classes/com/jsca/CameraManager$CameraConfig.class index 93c2691..84343e6 100644 Binary files a/target/classes/com/jsca/CameraManager$CameraConfig.class and b/target/classes/com/jsca/CameraManager$CameraConfig.class differ diff --git a/target/classes/com/jsca/CameraManager.class b/target/classes/com/jsca/CameraManager.class index e1dcbd4..0bd6d7e 100644 Binary files a/target/classes/com/jsca/CameraManager.class and b/target/classes/com/jsca/CameraManager.class differ diff --git a/target/classes/com/jsca/CameraPanel.class b/target/classes/com/jsca/CameraPanel.class index 9d724ea..8bb8a62 100644 Binary files a/target/classes/com/jsca/CameraPanel.class and b/target/classes/com/jsca/CameraPanel.class differ diff --git a/target/classes/com/jsca/CameraViewer$1.class b/target/classes/com/jsca/CameraViewer$1.class index f086b80..4b31bb8 100644 Binary files a/target/classes/com/jsca/CameraViewer$1.class and b/target/classes/com/jsca/CameraViewer$1.class differ diff --git a/target/classes/com/jsca/CameraViewer.class b/target/classes/com/jsca/CameraViewer.class index b4ace42..f60d2af 100644 Binary files a/target/classes/com/jsca/CameraViewer.class and b/target/classes/com/jsca/CameraViewer.class differ diff --git a/target/classes/com/jsca/Main.class b/target/classes/com/jsca/Main.class index c32667e..d097f97 100644 Binary files a/target/classes/com/jsca/Main.class and b/target/classes/com/jsca/Main.class differ diff --git a/target/classes/com/jsca/MainApp$1.class b/target/classes/com/jsca/MainApp$1.class new file mode 100644 index 0000000..5e2c3d4 Binary files /dev/null and b/target/classes/com/jsca/MainApp$1.class differ diff --git a/target/classes/com/jsca/MainApp.class b/target/classes/com/jsca/MainApp.class new file mode 100644 index 0000000..4e53596 Binary files /dev/null and b/target/classes/com/jsca/MainApp.class differ diff --git a/target/classes/com/jsca/NetworkStreamReader.class b/target/classes/com/jsca/NetworkStreamReader.class index f9077e2..ed358cf 100644 Binary files a/target/classes/com/jsca/NetworkStreamReader.class and b/target/classes/com/jsca/NetworkStreamReader.class differ diff --git a/target/classes/com/jsca/ScriptRunner.class b/target/classes/com/jsca/ScriptRunner.class new file mode 100644 index 0000000..ede3742 Binary files /dev/null and b/target/classes/com/jsca/ScriptRunner.class differ diff --git a/target/classes/com/jsca/WebcamStreamReader.class b/target/classes/com/jsca/WebcamStreamReader.class index b577b49..cea23ef 100644 Binary files a/target/classes/com/jsca/WebcamStreamReader.class and b/target/classes/com/jsca/WebcamStreamReader.class differ