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