diff --git a/.gitignore b/.gitignore
index 8fb9740..7568db7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,6 +41,7 @@ build/
### Other stuff ###
.mvn
+dependency-reduced-pom.xml
.idea
## This is for our app, cause it likes to store stuff ##
diff --git a/pom.xml b/pom.xml
index d03adf0..fca7502 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,6 +8,41 @@
swtc
1.0-SNAPSHOT
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.1
+
+
+ package
+
+ shade
+
+
+
+
+ io.swtc.Main
+
+
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+
+
+
+
17
17
diff --git a/src/main/java/io/swtc/Main.java b/src/main/java/io/swtc/Main.java
index 3071a17..0bfd6a2 100644
--- a/src/main/java/io/swtc/Main.java
+++ b/src/main/java/io/swtc/Main.java
@@ -1,7 +1,16 @@
package io.swtc;
+
public class Main {
+
public static void main(String[] args) {
- // very simple main, so that we can start the manager!
- CCTVManager.main(args);
+ for (int i = 0; i < args.length; i++) {
+ System.out.println("Arg " + i + ": " + args[i]);
+ }
+
+ if (args.length > 0 && "swing".equalsIgnoreCase(args[0])) {
+ SwingCCTVManager.main(new String[0]);
+ } else {
+ CCTVManager.main(new String[0]);
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/io/swtc/SwingCCTVManager.java b/src/main/java/io/swtc/SwingCCTVManager.java
new file mode 100644
index 0000000..95b8ee3
--- /dev/null
+++ b/src/main/java/io/swtc/SwingCCTVManager.java
@@ -0,0 +1,199 @@
+package io.swtc;
+
+import com.github.sarxos.webcam.Webcam;
+import com.github.sarxos.webcam.WebcamCompositeDriver;
+import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDriver;
+import com.github.sarxos.webcam.ds.ipcam.IpCamDeviceRegistry;
+import com.github.sarxos.webcam.ds.ipcam.IpCamDriver;
+import com.github.sarxos.webcam.ds.ipcam.IpCamMode;
+import io.swtc.networking.CameraConfig;
+import io.swtc.networking.CameraSettings;
+
+import javax.swing.*;
+import javax.swing.table.DefaultTableCellRenderer;
+import javax.swing.table.DefaultTableModel;
+import java.awt.*;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.net.MalformedURLException;
+import java.util.List;
+
+public class SwingCCTVManager {
+
+ static {
+ Webcam.setDriver(new WebcamCompositeDriver() {{
+ add(new WebcamDefaultDriver());
+ add(new IpCamDriver());
+ }});
+ loadSavedCameras();
+ }
+
+ private final JFrame frame;
+ private final JTable deviceTable;
+ private final DefaultTableModel tableModel;
+ private Timer autoRefreshTimer;
+
+ public SwingCCTVManager() {
+ frame = new JFrame("dashboard");
+ frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ frame.setSize(1000, 600);
+
+ String[] columns = {"Status", "Device Name", "Type", "Resolution", "Address"};
+ tableModel = new DefaultTableModel(columns, 0) {
+ @Override public boolean isCellEditable(int r, int c) { return false; }
+ };
+
+ deviceTable = new JTable(tableModel);
+ setupTableAppearance();
+
+ deviceTable.addMouseListener(new MouseAdapter() {
+ public void mousePressed(MouseEvent e) {
+ if (e.getClickCount() == 2 && deviceTable.getSelectedRow() != -1) {
+ launchSelected();
+ }
+ if (SwingUtilities.isRightMouseButton(e)) {
+ showContextMenu(e);
+ }
+ }
+ });
+
+ JToolBar toolBar = new JToolBar();
+ JButton btnAdd = new JButton("Add IP Cam");
+ JButton btnLaunch = new JButton("Launch Stream");
+ toolBar.add(btnAdd);
+ toolBar.addSeparator();
+ toolBar.add(btnLaunch);
+
+ frame.add(toolBar, BorderLayout.NORTH);
+ frame.add(new JScrollPane(deviceTable), BorderLayout.CENTER);
+
+ btnAdd.addActionListener(e -> showAddCameraDialog());
+ btnLaunch.addActionListener(e -> launchSelected());
+
+ startAutoRefresh();
+ refreshTable();
+ }
+
+ private void setupTableAppearance() {
+ deviceTable.getColumnModel().getColumn(0).setMaxWidth(80); // Status column
+ deviceTable.setRowHeight(30);
+
+ // Custom Renderer for Status Colors
+ deviceTable.getColumnModel().getColumn(0).setCellRenderer(new DefaultTableCellRenderer() {
+ @Override
+ public Component getTableCellRendererComponent(JTable t, Object v, boolean s, boolean f, int r, int c) {
+ Component comp = super.getTableCellRendererComponent(t, v, s, f, r, c);
+ if ("ONLINE".equals(v)) comp.setForeground(new Color(0, 150, 0));
+ else comp.setForeground(Color.RED);
+ setHorizontalAlignment(JLabel.CENTER);
+ return comp;
+ }
+ });
+ }
+
+ private void startAutoRefresh() {
+ autoRefreshTimer = new Timer(5000, e -> refreshTable());
+ autoRefreshTimer.start();
+ }
+
+ private void refreshTable() {
+ int selectedRow = deviceTable.getSelectedRow();
+
+ tableModel.setRowCount(0);
+ List webcams = Webcam.getWebcams();
+
+ for (Webcam w : webcams) {
+ boolean isIp = w.getDevice().getClass().getSimpleName().contains("IpCam");
+ String status = w.getDevice() != null ? "ONLINE" : "OFFLINE";
+
+ tableModel.addRow(new Object[]{
+ status,
+ w.getName(),
+ isIp ? "IP Stream" : "USB Hardware",
+ w.getViewSize().width + "x" + w.getViewSize().height,
+ isIp ? "Network" : "Local"
+ });
+ }
+
+ if (selectedRow != -1 && selectedRow < tableModel.getRowCount()) {
+ deviceTable.setRowSelectionInterval(selectedRow, selectedRow);
+ }
+ }
+
+ private void showContextMenu(MouseEvent e) {
+ int row = deviceTable.rowAtPoint(e.getPoint());
+ deviceTable.setRowSelectionInterval(row, row);
+
+ JPopupMenu menu = new JPopupMenu();
+ JMenuItem launch = new JMenuItem("Launch Live Stream");
+ JMenuItem delete = new JMenuItem("Remove Device");
+
+ launch.addActionListener(al -> launchSelected());
+ delete.addActionListener(al -> deleteSelected());
+
+ menu.add(launch);
+ menu.addSeparator();
+ menu.add(delete);
+ menu.show(deviceTable, e.getX(), e.getY());
+ }
+
+ private void launchSelected() {
+ int row = deviceTable.getSelectedRow();
+ if (row == -1) return;
+
+ String name = (String) tableModel.getValueAt(row, 1);
+ Webcam selected = Webcam.getWebcams().stream()
+ .filter(w -> w.getName().equals(name))
+ .findFirst().orElse(null);
+
+ if (selected != null) {
+ new Thread(() -> new SwingCameraWindow(selected).open()).start();
+ }
+ }
+
+ private void deleteSelected() {
+ int row = deviceTable.getSelectedRow();
+ String name = (String) tableModel.getValueAt(row, 1);
+ if (name.toLowerCase().contains("usb")) return;
+
+ CameraSettings.delete(name);
+ IpCamDeviceRegistry.unregister(name);
+ refreshTable();
+ }
+
+ private static void loadSavedCameras() {
+ for (CameraConfig config : CameraSettings.load()) {
+ try {
+ IpCamDeviceRegistry.register(config.getName(), config.getUrl(), IpCamMode.PUSH);
+ } catch (MalformedURLException e) { e.printStackTrace(); }
+ }
+ }
+
+ private void showAddCameraDialog() {
+ JPanel p = new JPanel(new GridLayout(2, 2, 5, 5));
+ JTextField n = new JTextField();
+ JTextField u = new JTextField();
+ p.add(new JLabel("Name:")); p.add(n);
+ p.add(new JLabel("URL:")); p.add(u);
+
+ int result = JOptionPane.showConfirmDialog(frame, p, "Register IP Camera", JOptionPane.OK_CANCEL_OPTION);
+ if (result == JOptionPane.OK_OPTION) {
+ try {
+ IpCamDeviceRegistry.register(n.getText(), u.getText(), IpCamMode.PUSH);
+ CameraSettings.save(new CameraConfig(n.getText(), u.getText()));
+ refreshTable();
+ } catch (Exception ex) {
+ JOptionPane.showMessageDialog(frame, "Error: " + ex.getMessage());
+ }
+ }
+ }
+
+ public void open() {
+ frame.setLocationRelativeTo(null);
+ frame.setVisible(true);
+ }
+
+ public static void main(String[] args) {
+ SwingUtilities.invokeLater(() -> new SwingCCTVManager().open());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/swtc/SwingCameraWindow.java b/src/main/java/io/swtc/SwingCameraWindow.java
new file mode 100644
index 0000000..30cf65e
--- /dev/null
+++ b/src/main/java/io/swtc/SwingCameraWindow.java
@@ -0,0 +1,82 @@
+package io.swtc;
+
+import com.github.sarxos.webcam.Webcam;
+import io.swtc.proccessing.WebcamCaptureLoop;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.awt.image.BufferedImage;
+
+public class SwingCameraWindow {
+ private final JFrame frame;
+ private final CameraPanel cameraPanel;
+ private final WebcamCaptureLoop captureLoop;
+
+ public SwingCameraWindow(Webcam webcam) {
+ this.frame = new JFrame("scctv@" + webcam.getName());
+ this.frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+ this.frame.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent e) {
+ // clean shit up
+ frame.dispose();
+ }
+ });
+
+ this.cameraPanel = new CameraPanel();
+ this.frame.add(cameraPanel);
+ this.frame.pack();
+ this.frame.setSize(640, 480);
+
+ this.captureLoop = new WebcamCaptureLoop(webcam, (BufferedImage img) -> {
+ SwingUtilities.invokeLater(() -> {
+ cameraPanel.setImage(img);
+ });
+ });
+
+ this.frame.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent e) {
+ captureLoop.stop();
+ }
+ });
+ }
+
+ public void open() {
+ frame.setVisible(true);
+ captureLoop.start();
+ }
+
+ private static class CameraPanel extends JPanel {
+ private BufferedImage currentImage;
+
+ public void setImage(BufferedImage img) {
+ this.currentImage = img;
+ this.repaint(); // Triggers paintComponent
+ }
+
+ @Override
+ protected void paintComponent(Graphics g) {
+ super.paintComponent(g);
+ if (currentImage != null) {
+ // Draw the image scaled to the panel size
+ g.drawImage(currentImage, 0, 0, getWidth(), getHeight(), null);
+ }
+ }
+ }
+
+ public static void main(String[] args) {
+ SwingUtilities.invokeLater(() -> {
+ // init in edt
+ Webcam webcam = Webcam.getDefault();
+ if (webcam != null) {
+ SwingCameraWindow window = new SwingCameraWindow(webcam);
+ window.open();
+ } else {
+ System.err.println("No webcam found!");
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/swtc/proccessing/WebcamCaptureLoop.java b/src/main/java/io/swtc/proccessing/WebcamCaptureLoop.java
index 1f70711..ebcdef5 100644
--- a/src/main/java/io/swtc/proccessing/WebcamCaptureLoop.java
+++ b/src/main/java/io/swtc/proccessing/WebcamCaptureLoop.java
@@ -44,7 +44,11 @@ public class WebcamCaptureLoop {
break;
}
}
- webcam.close();
+ try {
+ webcam.close();
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ }
});
captureThread.setName("cam_cap_thread");
captureThread.start();
diff --git a/src/test/java/CameraSettingsTest.java b/src/test/java/CameraSettingsTest.java
new file mode 100644
index 0000000..0abe0ec
--- /dev/null
+++ b/src/test/java/CameraSettingsTest.java
@@ -0,0 +1,98 @@
+import io.swtc.networking.CameraConfig;
+import io.swtc.networking.CameraSettings;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class CameraSettingsTest {
+
+ // Must match the filename used in CameraSettings.java
+ private final File TEST_FILE = new File("network_cameras.json");
+
+ @BeforeEach
+ @AfterEach
+ void cleanUp() {
+ // Ensure we start and end with a clean slate to avoid side effects
+ if (TEST_FILE.exists()) {
+ TEST_FILE.delete();
+ }
+ }
+
+ @Test
+ void testLoadReturnsEmptyListWhenNoFile() {
+ // If the file doesn't exist, it should return an empty list (not null)
+ List result = CameraSettings.load();
+
+ assertNotNull(result, "Load should never return null");
+ assertTrue(result.isEmpty(), "Should return empty list if file doesn't exist");
+ }
+
+ @Test
+ void testSaveAndLoad() {
+ // 1. Create a config (Using your actual constructor)
+ CameraConfig config = new CameraConfig("FrontDoor", "http://192.168.1.100/mjpeg");
+
+ // 2. Save it
+ CameraSettings.save(config);
+
+ // 3. Verify file creation
+ assertTrue(TEST_FILE.exists(), "File should be created after save");
+
+ // 4. Load it back
+ List loaded = CameraSettings.load();
+
+ // 5. Verify contents
+ assertEquals(1, loaded.size());
+ assertEquals("FrontDoor", loaded.get(0).getName());
+ assertEquals("http://192.168.1.100/mjpeg", loaded.get(0).getUrl());
+ }
+
+ @Test
+ void testSaveMultiple() {
+ // Save two distinct cameras
+ CameraSettings.save(new CameraConfig("Cam1", "rtsp://10.0.0.1/stream"));
+ CameraSettings.save(new CameraConfig("Cam2", "rtsp://10.0.0.2/stream"));
+
+ List loaded = CameraSettings.load();
+
+ assertEquals(2, loaded.size());
+ assertEquals("Cam1", loaded.get(0).getName());
+ assertEquals("Cam2", loaded.get(1).getName());
+ }
+
+ @Test
+ void testDelete() {
+ // Setup: Save two cameras
+ CameraSettings.save(new CameraConfig("Garage", "http://1.1.1.1"));
+ CameraSettings.save(new CameraConfig("Garden", "http://2.2.2.2"));
+
+ // Action: Delete "Garage"
+ CameraSettings.delete("Garage");
+
+ // Verify: Only "Garden" remains
+ List result = CameraSettings.load();
+ assertEquals(1, result.size());
+ assertEquals("Garden", result.get(0).getName());
+ }
+
+ @Test
+ void testLoadCorruptFile() throws IOException {
+ // Manually write broken JSON to the file
+ try (FileWriter writer = new FileWriter(TEST_FILE)) {
+ writer.write("{ \"this is broken json\": ... ");
+ }
+
+ // The code catches IOException and returns empty list
+ List result = CameraSettings.load();
+
+ assertNotNull(result);
+ assertTrue(result.isEmpty(), "Should handle corrupt JSON gracefully by returning empty list");
+ }
+}
\ No newline at end of file