some swing stuff cause swt is heavy and swing isnt
+ SwingCameraWindow + SwingCCTVManager --- fixes in WebcamCaptureLoop.java : Include thread safety, and some more stuff which is needed for Swing components. Planning on deprecating SWT in general
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -41,6 +41,7 @@ build/
|
|||||||
### Other stuff ###
|
### Other stuff ###
|
||||||
|
|
||||||
.mvn
|
.mvn
|
||||||
|
dependency-reduced-pom.xml
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
## This is for our app, cause it likes to store stuff ##
|
## This is for our app, cause it likes to store stuff ##
|
||||||
|
|||||||
35
pom.xml
35
pom.xml
@@ -8,6 +8,41 @@
|
|||||||
<artifactId>swtc</artifactId>
|
<artifactId>swtc</artifactId>
|
||||||
<version>1.0-SNAPSHOT</version>
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.1</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<transformers>
|
||||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
|
<mainClass>io.swtc.Main</mainClass>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
<filters>
|
||||||
|
<filter>
|
||||||
|
<artifact>*:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>META-INF/*.SF</exclude>
|
||||||
|
<exclude>META-INF/*.DSA</exclude>
|
||||||
|
<exclude>META-INF/*.RSA</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
</filters>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>17</maven.compiler.source>
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
<maven.compiler.target>17</maven.compiler.target>
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
package io.swtc;
|
package io.swtc;
|
||||||
|
|
||||||
public class Main {
|
public class Main {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
// very simple main, so that we can start the manager!
|
for (int i = 0; i < args.length; i++) {
|
||||||
CCTVManager.main(args);
|
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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
199
src/main/java/io/swtc/SwingCCTVManager.java
Normal file
199
src/main/java/io/swtc/SwingCCTVManager.java
Normal file
@@ -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<Webcam> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/main/java/io/swtc/SwingCameraWindow.java
Normal file
82
src/main/java/io/swtc/SwingCameraWindow.java
Normal file
@@ -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!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,7 +44,11 @@ public class WebcamCaptureLoop {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
webcam.close();
|
webcam.close();
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
captureThread.setName("cam_cap_thread");
|
captureThread.setName("cam_cap_thread");
|
||||||
captureThread.start();
|
captureThread.start();
|
||||||
|
|||||||
98
src/test/java/CameraSettingsTest.java
Normal file
98
src/test/java/CameraSettingsTest.java
Normal file
@@ -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<CameraConfig> 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<CameraConfig> 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<CameraConfig> 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<CameraConfig> 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<CameraConfig> result = CameraSettings.load();
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertTrue(result.isEmpty(), "Should handle corrupt JSON gracefully by returning empty list");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user