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 ###
|
||||
|
||||
.mvn
|
||||
dependency-reduced-pom.xml
|
||||
.idea
|
||||
|
||||
## This is for our app, cause it likes to store stuff ##
|
||||
|
||||
35
pom.xml
35
pom.xml
@@ -8,6 +8,41 @@
|
||||
<artifactId>swtc</artifactId>
|
||||
<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>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
webcam.close();
|
||||
try {
|
||||
webcam.close();
|
||||
} catch (IllegalStateException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
captureThread.setName("cam_cap_thread");
|
||||
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