shit
This commit is contained in:
89
README.md
89
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.
|
||||
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.
|
||||
83
scripts/image_filters.js
Normal file
83
scripts/image_filters.js
Normal file
@@ -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;
|
||||
53
scripts/motion_detection.py
Executable file
53
scripts/motion_detection.py
Executable file
@@ -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 <image_path>")
|
||||
sys.exit(1)
|
||||
|
||||
detect_motion(sys.argv[1])
|
||||
11
src/main/java/com/jsca/CameraConfig.java
Normal file
11
src/main/java/com/jsca/CameraConfig.java
Normal file
@@ -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
|
||||
}
|
||||
@@ -1,134 +1,351 @@
|
||||
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<Integer, CameraPanel> 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<Integer, CameraPanel> cameraPanels;
|
||||
private final Map<Integer, Future<?>> 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();
|
||||
this.gridPanel = gridPanel;
|
||||
// Do not add any panels initially
|
||||
}
|
||||
|
||||
// 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);
|
||||
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);
|
||||
if (targetPanel == null) {
|
||||
if (cameraPanels.size() < 4) {
|
||||
position = getNextAvailablePosition();
|
||||
targetPanel = new CameraPanel(position);
|
||||
cameraPanels.put(position, targetPanel);
|
||||
gridPanel.add(targetPanel);
|
||||
gridPanel.revalidate();
|
||||
gridPanel.repaint();
|
||||
|
||||
return true;
|
||||
} 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 removeCamera(int position) {
|
||||
if (position >= 0 && position < MAX_CAMERAS) {
|
||||
CameraPanel panel = cameras.get(position);
|
||||
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) {
|
||||
panel.stopStream();
|
||||
CameraPanel emptyPanel = new CameraPanel("Empty " + (position + 1));
|
||||
cameras.put(position, emptyPanel);
|
||||
gridPanel.remove(position);
|
||||
gridPanel.add(emptyPanel, position);
|
||||
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 void stopAllCameras() {
|
||||
for (CameraPanel panel : cameras.values()) {
|
||||
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);
|
||||
}
|
||||
|
||||
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.stopStream();
|
||||
}
|
||||
panel.setActive(false);
|
||||
panel.setCameraName("Camera " + (position + 1));
|
||||
}
|
||||
}
|
||||
|
||||
public CameraPanel getCameraPanel(int position) {
|
||||
return cameras.get(position);
|
||||
}
|
||||
|
||||
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);
|
||||
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() {
|
||||
JFileChooser fileChooser = new JFileChooser();
|
||||
fileChooser.setDialogTitle("Save Camera Configuration");
|
||||
fileChooser.setSelectedFile(new File("camera_config.json"));
|
||||
|
||||
if (fileChooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
|
||||
try (FileWriter writer = new FileWriter(fileChooser.getSelectedFile())) {
|
||||
List<CameraConfig> configs = new ArrayList<>();
|
||||
for (Map.Entry<Integer, CameraPanel> entry : cameras.entrySet()) {
|
||||
for (Map.Entry<Integer, CameraPanel> entry : cameraPanels.entrySet()) {
|
||||
CameraPanel panel = entry.getValue();
|
||||
if (panel.isStreaming()) {
|
||||
if (panel.isActive()) {
|
||||
CameraConfig config = new CameraConfig();
|
||||
config.position = entry.getKey();
|
||||
config.name = panel.getCameraName();
|
||||
// Add more configuration details as needed
|
||||
configs.add(config);
|
||||
}
|
||||
}
|
||||
|
||||
try (Writer writer = new FileWriter(CONFIG_FILE)) {
|
||||
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(),
|
||||
"Save Error",
|
||||
"Error",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void loadConfiguration() {
|
||||
try (Reader reader = new FileReader(CONFIG_FILE)) {
|
||||
JFileChooser fileChooser = new JFileChooser();
|
||||
fileChooser.setDialogTitle("Load Camera Configuration");
|
||||
|
||||
if (fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
|
||||
try (FileReader reader = new FileReader(fileChooser.getSelectedFile())) {
|
||||
List<CameraConfig> configs = gson.fromJson(reader,
|
||||
new TypeToken<List<CameraConfig>>(){}.getType());
|
||||
|
||||
if (configs != null) {
|
||||
stopAllCameras();
|
||||
// Stop all current cameras
|
||||
for (int position : cameraPanels.keySet()) {
|
||||
stopCamera(position);
|
||||
}
|
||||
|
||||
// Load new configuration
|
||||
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
|
||||
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(),
|
||||
"Load Error",
|
||||
"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;
|
||||
|
||||
@@ -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,64 +10,72 @@ 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;
|
||||
public void updateFrame(BufferedImage frame) {
|
||||
this.currentFrame = frame;
|
||||
repaint();
|
||||
}
|
||||
|
||||
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 int getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public void takeSnapshot() {
|
||||
if (currentFrame != null) {
|
||||
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);
|
||||
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",
|
||||
@@ -78,22 +87,53 @@ public class CameraPanel extends JPanel {
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateFrame(Image frame) {
|
||||
if (frame != null) {
|
||||
videoLabel.setIcon(new ImageIcon(frame));
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
public String getCameraName() {
|
||||
return cameraName;
|
||||
}
|
||||
|
||||
public boolean isStreaming() {
|
||||
return isStreaming;
|
||||
}
|
||||
|
||||
public StreamReader getCurrentStream() {
|
||||
return currentStream;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<Webcam> 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<String> 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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
124
src/main/java/com/jsca/MainApp.java
Normal file
124
src/main/java/com/jsca/MainApp.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,34 +13,89 @@ 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() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void connectAndStream() throws IOException {
|
||||
URL url = new URL(streamUrl);
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(5000);
|
||||
|
||||
try (BufferedInputStream in = new BufferedInputStream(connection.getInputStream())) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
byte[] imageBuffer = new byte[1024 * 1024]; // 1MB buffer for JPEG
|
||||
// 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);
|
||||
}
|
||||
|
||||
// For DroidCam compatibility
|
||||
connection.setRequestProperty("User-Agent", "Mozilla/5.0");
|
||||
connection.setRequestProperty("Accept", "multipart/x-mixed-replace;boundary=BOUNDARY");
|
||||
|
||||
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) {
|
||||
bytesRead = in.read(buffer);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for JPEG header (0xFF, 0xD8)
|
||||
if (!foundHeader && i < bytesRead - 1) {
|
||||
if (buffer[i] == (byte) 0xFF && buffer[i + 1] == (byte) 0xD8) {
|
||||
@@ -50,6 +105,7 @@ public class NetworkStreamReader implements StreamReader {
|
||||
}
|
||||
|
||||
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);
|
||||
processImage(Arrays.copyOf(imageBuffer, imagePos));
|
||||
foundHeader = false;
|
||||
imagePos = 0;
|
||||
}
|
||||
|
||||
if (imagePos >= imageBuffer.length) {
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
112
src/main/java/com/jsca/ScriptRunner.java
Normal file
112
src/main/java/com/jsca/ScriptRunner.java
Normal file
@@ -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<String> 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<Object> 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<Object> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
BIN
target/classes/com/jsca/CameraConfig.class
Normal file
BIN
target/classes/com/jsca/CameraConfig.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/jsca/MainApp$1.class
Normal file
BIN
target/classes/com/jsca/MainApp$1.class
Normal file
Binary file not shown.
BIN
target/classes/com/jsca/MainApp.class
Normal file
BIN
target/classes/com/jsca/MainApp.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/jsca/ScriptRunner.class
Normal file
BIN
target/classes/com/jsca/ScriptRunner.class
Normal file
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user