11 Commits

Author SHA1 Message Date
40a6183529 some fixes to config import 2026-01-22 15:33:59 +01:00
c32b5d7278 refactors ; some new additions to recording which are critical for cctv softwares 2026-01-21 18:09:30 +01:00
3eaf6f0303 fix
Signed-off-by: rattatwinko <seppmutterman@gmail.com>
2026-01-20 08:37:48 +01:00
11c5aa9115 Merge branch 'experimenting'
Signed-off-by: rattatwinko <seppmutterman@gmail.com>

# Conflicts:
#	src/main/java/io/swtc/Main.java
2026-01-20 08:28:10 +01:00
55474092e3 some more optimizing!
now runs much better with lower power systems as we refactored code to firstly rule out double invokelaters, and we optimized the capture loop iteslf!

and we introduced a ShowError class which will be refactored and used quite thoroughly!

Signed-off-by: rattatwinko <seppmutterman@gmail.com>
2026-01-20 08:25:26 +01:00
b49cc8b2f0 refactors 2026-01-19 18:33:42 +01:00
565a4f3cf3 fixed some functionallity added back some which was removed cause of refactoring ...
some new refactoring to make the UI code cleaner

Signed-off-by: rattatwinko <seppmutterman@gmail.com>
2026-01-19 13:04:40 +01:00
f6ee3e915e refactored shitty unusable code
Signed-off-by: rattatwinko <seppmutterman@gmail.com>
2026-01-19 12:00:50 +01:00
41fbf62757 just some theme changes, i like this one more 2026-01-18 22:10:37 +01:00
c393e05bb1 testing some new stuff! 2026-01-18 21:00:24 +01:00
e7a3d98dd0 Quality of Life changes to the viewer
changed:
/ SwingIFrame ; Some new methodes for setting bg color, and fullscreen, keybinds are "F11" and "B"
/ WebcamCaptureLoop.java ; Safety is key!

---
rattatwinko
2026-01-14 20:55:45 +01:00
38 changed files with 1830 additions and 923 deletions

View File

@@ -122,5 +122,6 @@
<artifactId>jcodec-javase</artifactId>
<version>0.2.5</version>
</dependency>
</dependencies>
</project>

View File

@@ -1,11 +1,23 @@
package io.swtc;
import javax.swing.*;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.ShowError;
public class Main {
public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println("Arg " + i + ": " + args[i]);
}
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
ShowError.warning(null,"LaF Warn","LaF");
}
SwingCCTVManager.main(null);
}
}

View File

@@ -2,25 +2,37 @@ package io.swtc;
import com.github.sarxos.webcam.Webcam;
import com.github.sarxos.webcam.WebcamCompositeDriver;
import com.github.sarxos.webcam.WebcamDevice;
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 com.github.sarxos.webcam.ds.ipcam.*;
import io.swtc.networking.CameraConfig;
import io.swtc.networking.CameraSettings;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.ShowError;
import javax.swing.*;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URL;
import java.util.List;
public class SwingCCTVManager {
private static final String TIMEOUT_MS = "1500";
private static final int REFRESH_INTERVAL = 30000;
private static final String STATUS_ONLINE = "ONLINE";
private static final String STATUS_OFFLINE = "OFFLINE";
static {
System.setProperty("ipcam.connection.timeout", TIMEOUT_MS);
Webcam.setDriver(new WebcamCompositeDriver() {{
add(new WebcamDefaultDriver());
add(new IpCamDriver());
@@ -32,206 +44,238 @@ public class SwingCCTVManager {
private final JTable deviceTable;
private final DefaultTableModel tableModel;
private final SwingIFrame IFrame;
private boolean isRefreshing = false;
public SwingCCTVManager() {
frame = new JFrame("dashboard");
frame = new JFrame("Dashboard");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(1000, 600);
frame.setSize(1100, 600);
frame.setIconImage(IconSetter.getIcon());
this.IFrame = new SwingIFrame();
this.IFrame.show();
String[] columns = {"Status", "Device Name", "Type", "Resolution", "Address"};
tableModel = new DefaultTableModel(columns, 0) {
tableModel = new DefaultTableModel(new String[]{"Status", "Device Name", "Type", "Resolution", "Address"}, 0) {
@Override
public boolean isCellEditable(int r, int c) {
return false;
}
public boolean isCellEditable(int r, int c) { return false; }
};
deviceTable = new JTable(tableModel);
deviceTable.setRowSelectionAllowed(true);
deviceTable.setFocusable(true);
deviceTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
setupTableAppearance();
deviceTable.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
deviceTable.requestFocusInWindow();
if (e.getClickCount() == 2 && deviceTable.getSelectedRow() != -1) {
launchSelected();
}
if (SwingUtilities.isRightMouseButton(e)) {
showContextMenu(e);
}
}
});
setupDragAndDrop(); // Initialize DND
setupListeners();
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);
setupBrandedToolbar(toolBar);
frame.add(toolBar, BorderLayout.NORTH);
frame.add(toolBar, BorderLayout.SOUTH);
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);
deviceTable.setRowHeight(30);
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);
setHorizontalAlignment(JLabel.CENTER);
comp.setForeground("ONLINE".equals(v)
? new Color(0, 150, 0)
: Color.RED);
return comp;
}
});
}
private void startAutoRefresh() {
new Timer(5000, e -> refreshTable()).start();
}
private void refreshTable() {
int[] selectedRows = deviceTable.getSelectedRows();
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"
});
private void setupBrandedToolbar(JToolBar toolBar) {
Image ogIcon = IconSetter.getIcon();
if (ogIcon != null) {
Image scaledIcon = ogIcon.getScaledInstance(32, 32, Image.SCALE_SMOOTH);
toolBar.add(new JLabel(new ImageIcon(scaledIcon)));
}
for (int r : selectedRows) {
if (r < tableModel.getRowCount()) {
deviceTable.addRowSelectionInterval(r, r);
toolBar.add(new JLabel("SWT-CCTV"));
toolBar.add(Box.createRigidArea(new Dimension(10, 0)));
toolBar.addSeparator(new Dimension(10, 32));
JButton btnAdd = new JButton("Add IP Cam");
JButton btnImport = new JButton("Import Config"); // New Import Button
JButton btnLaunch = new JButton("Launch Stream");
btnAdd.setPreferredSize(new Dimension(100, 32));
btnImport.setPreferredSize(new Dimension(110, 32));
btnLaunch.setPreferredSize(new Dimension(120, 32));
toolBar.add(Box.createRigidArea(new Dimension(5, 0)));
toolBar.add(btnAdd);
toolBar.add(btnImport);
toolBar.addSeparator();
toolBar.add(btnLaunch);
btnAdd.addActionListener(e -> showAddCameraDialog());
btnImport.addActionListener(e -> showImportDialog());
btnLaunch.addActionListener(e -> launchSelected());
}
private void setupDragAndDrop() {
deviceTable.setDragEnabled(true);
deviceTable.setTransferHandler(new TransferHandler() {
@Override
public boolean canImport(TransferSupport support) {
return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor);
}
@Override
public boolean importData(TransferSupport support) {
if (!canImport(support)) return false;
try {
@SuppressWarnings("unchecked")
List<File> files = (List<File>) support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
for (File file : files) {
if (file.getName().endsWith(".json")) {
List<CameraConfig> configs = CameraSettings.loadFromFile(file);
importCameras(configs);
}
}
return true;
} catch (Exception e) {
return false;
}
}
});
}
private void showImportDialog() {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("Select Camera Configuration JSON");
if (chooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
List<CameraConfig> configs = CameraSettings.loadFromFile(file);
importCameras(configs);
}
}
private void showContextMenu(MouseEvent e) {
int row = deviceTable.rowAtPoint(e.getPoint());
if (row >= 0) {
deviceTable.setRowSelectionInterval(row, row);
private void importCameras(List<CameraConfig> configs) {
if (configs.isEmpty()) {
ShowError.warning(frame, "Import Empty", "No valid camera configurations found in file.");
return;
}
for (CameraConfig config : configs) {
try {
if (!IpCamDeviceRegistry.isRegistered(config.getName())) {
IpCamDeviceRegistry.register(config.getName(), config.getUrl(), IpCamMode.PUSH);
CameraSettings.save(config);
}
} catch (Exception ignored) {}
}
refreshTable();
}
JPopupMenu menu = new JPopupMenu();
JMenuItem launch = new JMenuItem("Launch Live Stream");
JMenuItem delete = new JMenuItem("Remove Device");
private void setupTableAppearance() {
deviceTable.setRowHeight(30);
deviceTable.getColumnModel().getColumn(0).setMaxWidth(80);
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);
setHorizontalAlignment(JLabel.CENTER);
comp.setForeground(STATUS_ONLINE.equals(v) ? new Color(0, 150, 0) : Color.RED);
return comp;
}
});
}
launch.addActionListener(al -> launchSelected());
delete.addActionListener(al -> deleteSelected());
private synchronized void refreshTable() {
if (isRefreshing) return;
isRefreshing = true;
final int selectedRow = deviceTable.getSelectedRow();
new SwingWorker<Void, Object[]>() {
@Override
protected Void doInBackground() {
for (WebcamDevice device : new WebcamDefaultDriver().getDevices()) {
Dimension res = (device.getResolutions().length > 0) ? device.getResolutions()[0] : new Dimension(0,0);
publish(new Object[]{STATUS_ONLINE, device.getName(), "USB Hardware", res.width + "x" + res.height, "Local"});
}
for (IpCamDevice ipDevice : IpCamDeviceRegistry.getIpCameras()) {
publish(probeIpCamera(ipDevice));
}
return null;
}
@Override
protected void process(List<Object[]> chunks) {
if (!Boolean.TRUE.equals(frame.getRootPane().getClientProperty("cleared"))) {
tableModel.setRowCount(0);
frame.getRootPane().putClientProperty("cleared", true);
}
for (Object[] row : chunks) tableModel.addRow(row);
}
@Override
protected void done() {
frame.getRootPane().putClientProperty("cleared", null);
if (selectedRow != -1 && selectedRow < tableModel.getRowCount()) {
deviceTable.setRowSelectionInterval(selectedRow, selectedRow);
}
isRefreshing = false;
}
}.execute();
}
menu.add(launch);
menu.addSeparator();
menu.add(delete);
menu.show(deviceTable, e.getX(), e.getY());
private Object[] probeIpCamera(IpCamDevice ipDevice) {
String status = STATUS_OFFLINE; String resText = "N/A";
try {
URL url = ipDevice.getURL();
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(url.getHost(), url.getPort() != -1 ? url.getPort() : 80), 800);
Dimension d = ipDevice.getResolution();
if (d != null) { status = STATUS_ONLINE; resText = d.width + "x" + d.height; }
}
} catch (Exception e) { status = STATUS_OFFLINE; }
return new Object[]{status, ipDevice.getName(), "IP Stream", resText, "Network"};
}
private void launchSelected() {
int row = deviceTable.getSelectedRow();
if (row == -1) return;
String name = (String) tableModel.getValueAt(row, 1);
Webcam.getWebcams().stream()
.filter(w -> w.getName().equals(name))
.findFirst()
.ifPresent(cam ->
new Thread(() -> IFrame.addCameraInternalFrame(cam)).start()
);
}
private void deleteSelected() {
int row = deviceTable.getSelectedRow();
if (row == -1) return;
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) {
JOptionPane.showMessageDialog(
null,
"Malformed URL\n" + e.getMessage(),
"Malformed URL",
JOptionPane.ERROR_MESSAGE
);
}
if (STATUS_OFFLINE.equals(tableModel.getValueAt(row, 0))) {
ShowError.warning(frame, "Device Offline", "Cannot connect to '" + name + "'.");
return;
}
new Thread(() -> {
Webcam selected = Webcam.getWebcams().stream().filter(w -> w.getName().equals(name)).findFirst().orElse(null);
if (selected != null) IFrame.addCameraInternalFrame(selected);
}).start();
}
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());
}
JTextField n = new JTextField(); JTextField u = new JTextField();
p.add(new JLabel("Name:")); p.add(n); p.add(new JLabel("URL:")); p.add(u);
if (JOptionPane.showConfirmDialog(frame, p, "Register IP Camera", JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION) {
importCameras(List.of(new CameraConfig(n.getText(), u.getText())));
}
}
public void open() {
frame.setLocationRelativeTo(null);
frame.setVisible(true);
private void setupListeners() {
deviceTable.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
deviceTable.requestFocusInWindow();
if (e.getClickCount() == 2 && deviceTable.getSelectedRow() != -1) launchSelected();
if (SwingUtilities.isRightMouseButton(e)) showContextMenu(e);
}
});
}
private void showContextMenu(MouseEvent e) {
int row = deviceTable.rowAtPoint(e.getPoint());
if (row >= 0) deviceTable.setRowSelectionInterval(row, row);
JPopupMenu menu = new JPopupMenu();
JMenuItem launch = new JMenuItem("Launch Live Stream");
launch.addActionListener(al -> launchSelected());
menu.add(launch);
menu.show(deviceTable, e.getX(), e.getY());
}
private static void loadSavedCameras() {
for (CameraConfig config : CameraSettings.load()) {
try { IpCamDeviceRegistry.register(config.getName(), config.getUrl(), IpCamMode.PUSH); } catch (Exception ignored) {}
}
}
private void startAutoRefresh() { new Timer(REFRESH_INTERVAL, e -> refreshTable()).start(); }
public void open() { frame.setLocationRelativeTo(null); frame.setVisible(true); }
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new SwingCCTVManager().open());
}
}
}

View File

@@ -1,79 +0,0 @@
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();
captureLoop.stop(); // be sure to call this! otherwise the camera will stay open!
}
});
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)));
}
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) {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2.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 {
JOptionPane.showMessageDialog(
null,
"No Webcam found!",
"Error",
JOptionPane.WARNING_MESSAGE
);
}
});
}
}

View File

@@ -1,311 +1,178 @@
package io.swtc;
import com.github.sarxos.webcam.Webcam;
import io.swtc.proccessing.WebcamCaptureLoop;
import io.swtc.proccessing.CameraPanel;
import io.swtc.recording.VideoRecorder;
import javax.imageio.ImageIO;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.iframe.*; // Your custom frames
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
/*
*
* This file is basically just UI, its boring the interesting stuff is in the utilities section!
*
* */
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.HashMap;
import java.util.Map;
public class SwingIFrame {
private final JFrame mainFrame;
private final JDesktopPane desktopPane;
private final DesktopPane desktopPane;
private final Map<JInternalFrame, EffectsPanelFrame> cameraToEffects = new HashMap<>();
private boolean fullscreen = false;
private Rectangle windowedBounds;
private boolean blackbg = false;
private final Color bgcolor = Color.decode("#336B6A");
private final Color defDesktopBg = Color.WHITE;
private final JPopupMenu popupMenu = new JPopupMenu();
public SwingIFrame() {
mainFrame = new JFrame("viewer");
mainFrame.setSize(1280,720);
mainFrame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
desktopPane = new JDesktopPane();
desktopPane.setBackground(Color.WHITE);
mainFrame = new JFrame("Viewer");
mainFrame.setSize(1280, 720);
mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
mainFrame.setIconImage(IconSetter.getIcon());
desktopPane = new DesktopPane(cameraToEffects);
desktopPane.setBackground(defDesktopBg);
mainFrame.add(desktopPane, BorderLayout.CENTER);
setupFullscreenToggle();
setupBlackBg();
initPopupMenu();
desktopPane.addMouseListener(popupListener());
}
public void addCameraInternalFrame(Webcam webcam) {
JInternalFrame iframe = new JInternalFrame(
webcam.getName(),
true, true, true, true
CameraInternalFrame cameraFrame = new CameraInternalFrame(webcam, this::handleEffectsRequest);
EffectsPanelFrame effectsFrame = new EffectsPanelFrame(
"Effects - " + webcam.getName(),
cameraFrame.getCameraPanel()
);
CameraPanel cameraPanel = new CameraPanel();
WebcamCaptureLoop captureLoop = new WebcamCaptureLoop(webcam, (BufferedImage img) ->
SwingUtilities.invokeLater(() -> cameraPanel.setImage(img))
);
cameraToEffects.put(cameraFrame, effectsFrame);
JPanel contentPanel = new JPanel(new BorderLayout());
int offset = desktopPane.getAllFrames().length * 15;
cameraFrame.setLocation(50 + offset, 50 + offset);
effectsFrame.setLocation(700 + offset, 50 + offset);
effectsFrame.setVisible(false);
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab("View", cameraPanel);
tabbedPane.addTab("Capture", createCapturePanel(cameraPanel, webcam));
tabbedPane.addTab("Effects", createEffectsPanel(cameraPanel));
contentPanel.add(tabbedPane, BorderLayout.CENTER);
iframe.addInternalFrameListener(new javax.swing.event.InternalFrameAdapter() {
cameraFrame.addInternalFrameListener(new javax.swing.event.InternalFrameAdapter() {
@Override
public void internalFrameClosing(javax.swing.event.InternalFrameEvent e) {
// if we dont call this the camera stays open until the procces dies.
captureLoop.stop();
EffectsPanelFrame ef = cameraToEffects.remove(cameraFrame);
if (ef != null) ef.dispose();
desktopPane.forgetFrame(cameraFrame);
cameraFrame.dispose();
}
});
iframe.add(contentPanel);
iframe.setSize(600, 500);
desktopPane.add(cameraFrame);
desktopPane.add(effectsFrame);
int offset = desktopPane.getAllFrames().length * 30;
iframe.setLocation(offset, offset);
// Attach popup menu to frames and content
MouseAdapter popup = popupListener();
cameraFrame.addMouseListener(popup);
cameraFrame.getContentPane().addMouseListener(popup);
effectsFrame.addMouseListener(popup);
effectsFrame.getContentPane().addMouseListener(popup);
desktopPane.add(iframe);
iframe.setVisible(true);
captureLoop.start();
cameraFrame.setVisible(true);
}
private JPanel createCapturePanel(CameraPanel cameraPanel, Webcam webcam) {
JPanel panel = new JPanel(new BorderLayout(10, 10));
panel.setBorder(new EmptyBorder(15, 15, 15, 15));
JPanel mainPanel = new JPanel();
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
// Screenshot section
JPanel screenshotPanel = new JPanel();
screenshotPanel.setLayout(new BoxLayout(screenshotPanel, BoxLayout.Y_AXIS));
screenshotPanel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(), "Screenshot",
TitledBorder.LEFT, TitledBorder.TOP));
JPanel screenshotPathPanel = new JPanel(new BorderLayout(5, 5));
JTextField screenshotPath = new JTextField(System.getProperty("user.home") + File.separator + "screenshots");
JButton browseSS = new JButton("...");
browseSS.setPreferredSize(new Dimension(40, 25));
browseSS.addActionListener(e -> browseDirectory(screenshotPath, panel));
screenshotPathPanel.add(screenshotPath, BorderLayout.CENTER);
screenshotPathPanel.add(browseSS, BorderLayout.EAST);
JButton takeScreenshot = new JButton("Take Screenshot (S)");
takeScreenshot.setAlignmentX(Component.CENTER_ALIGNMENT);
takeScreenshot.addActionListener(e -> saveSnapshot(cameraPanel, webcam, screenshotPath.getText(), panel));
screenshotPanel.add(screenshotPathPanel);
screenshotPanel.add(Box.createRigidArea(new Dimension(0, 10)));
screenshotPanel.add(takeScreenshot);
// Recording section
JPanel recordPanel = new JPanel();
recordPanel.setLayout(new BoxLayout(recordPanel, BoxLayout.Y_AXIS));
recordPanel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(), "Recording",
TitledBorder.LEFT, TitledBorder.TOP));
JPanel recordPathPanel = new JPanel(new BorderLayout(5, 5));
JTextField recordPath = new JTextField(System.getProperty("user.home") + File.separator + "recordings");
JButton browseRec = new JButton("...");
browseRec.setPreferredSize(new Dimension(40, 25));
browseRec.addActionListener(e -> browseDirectory(recordPath, panel));
recordPathPanel.add(recordPath, BorderLayout.CENTER);
recordPathPanel.add(browseRec, BorderLayout.EAST);
VideoRecorder recorder = new VideoRecorder();
JButton recordButton = new JButton("Start Recording (R)");
JLabel recordingStatus = new JLabel("Ready");
recordingStatus.setAlignmentX(Component.CENTER_ALIGNMENT);
recordButton.setAlignmentX(Component.CENTER_ALIGNMENT);
recordButton.addActionListener(e -> {
if (!recorder.isRecording()) {
String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String filename = "recording_" + webcam.getName().replaceAll("[^a-zA-Z0-9]", "_")
+ "_" + timestamp + ".mp4";
File dir = new File(recordPath.getText());
dir.mkdirs();
try {
recorder.startRecording(cameraPanel, new File(dir, filename));
recordButton.setText("Stop Recording");
recordingStatus.setText("Recording...");
recordingStatus.setForeground(Color.RED);
} catch (Exception ex) {
JOptionPane.showMessageDialog(panel,
"Error starting recording: " + ex.getMessage(),
"Error", JOptionPane.ERROR_MESSAGE);
}
} else {
try {
File saved = recorder.stopRecording();
recordButton.setText("Start Recording (R)");
recordingStatus.setText("Saved: " + saved.getName());
recordingStatus.setForeground(Color.BLACK);
} catch (Exception ex) {
recordButton.setText("Start Recording (R)");
recordingStatus.setText("Error saving");
recordingStatus.setForeground(Color.RED);
JOptionPane.showMessageDialog(panel,
"Error saving recording: " + ex.getMessage(),
"Error", JOptionPane.ERROR_MESSAGE);
}
}
});
recordPanel.add(recordPathPanel);
recordPanel.add(Box.createRigidArea(new Dimension(0, 10)));
recordPanel.add(recordButton);
recordPanel.add(Box.createRigidArea(new Dimension(0, 5)));
recordPanel.add(recordingStatus);
mainPanel.add(screenshotPanel);
mainPanel.add(Box.createRigidArea(new Dimension(0, 15)));
mainPanel.add(recordPanel);
mainPanel.add(Box.createVerticalGlue());
panel.add(mainPanel, BorderLayout.NORTH);
return panel;
}
private JPanel createEffectsPanel(CameraPanel cameraPanel) {
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panel.setBorder(new EmptyBorder(15, 15, 15, 15));
JPanel transformPanel = new JPanel(new GridLayout(0, 1, 5, 5));
transformPanel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(), "Transform",
TitledBorder.LEFT, TitledBorder.TOP));
JCheckBox mirrorCheck = new JCheckBox("Mirror Horizontal");
JCheckBox flipCheck = new JCheckBox("Flip Vertical");
JCheckBox rotateCheck = new JCheckBox("Rotate 180°");
mirrorCheck.addActionListener(e -> cameraPanel.setMirror(mirrorCheck.isSelected()));
flipCheck.addActionListener(e -> cameraPanel.setFlip(flipCheck.isSelected()));
rotateCheck.addActionListener(e -> cameraPanel.setRotate(rotateCheck.isSelected()));
transformPanel.add(mirrorCheck);
transformPanel.add(flipCheck);
transformPanel.add(rotateCheck);
JPanel colorPanel = new JPanel();
colorPanel.setLayout(new BoxLayout(colorPanel, BoxLayout.Y_AXIS));
colorPanel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(), "Color",
TitledBorder.LEFT, TitledBorder.TOP));
JCheckBox grayscaleCheck = new JCheckBox("Grayscale");
JCheckBox invertCheck = new JCheckBox("Invert Colors");
grayscaleCheck.addActionListener(e -> cameraPanel.setGrayscale(grayscaleCheck.isSelected()));
invertCheck.addActionListener(e -> cameraPanel.setInvert(invertCheck.isSelected()));
colorPanel.add(grayscaleCheck);
colorPanel.add(Box.createRigidArea(new Dimension(0, 5)));
colorPanel.add(invertCheck);
JPanel brightnessPanel = new JPanel();
brightnessPanel.setLayout(new BoxLayout(brightnessPanel, BoxLayout.Y_AXIS));
brightnessPanel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(), "Adjustments",
TitledBorder.LEFT, TitledBorder.TOP));
JPanel brightnessSliderPanel = new JPanel(new BorderLayout());
JLabel brightnessLabel = new JLabel("Brightness: 0");
JSlider brightnessSlider = new JSlider(-100, 100, 0);
brightnessSlider.addChangeListener(e -> {
int value = brightnessSlider.getValue();
brightnessLabel.setText("Brightness: " + value);
cameraPanel.setBrightness(value);
});
brightnessSliderPanel.add(brightnessLabel, BorderLayout.NORTH);
brightnessSliderPanel.add(brightnessSlider, BorderLayout.CENTER);
JPanel contrastSliderPanel = new JPanel(new BorderLayout());
JLabel contrastLabel = new JLabel("Contrast: 1.0");
JSlider contrastSlider = new JSlider(0, 200, 100);
contrastSlider.addChangeListener(e -> {
float value = contrastSlider.getValue() / 100f;
contrastLabel.setText("Contrast: " + String.format("%.1f", value));
cameraPanel.setContrast(value);
});
contrastSliderPanel.add(contrastLabel, BorderLayout.NORTH);
contrastSliderPanel.add(contrastSlider, BorderLayout.CENTER);
brightnessPanel.add(brightnessSliderPanel);
brightnessPanel.add(Box.createRigidArea(new Dimension(0, 10)));
brightnessPanel.add(contrastSliderPanel);
JButton resetButton = new JButton("Reset All Effects");
resetButton.setAlignmentX(Component.CENTER_ALIGNMENT);
resetButton.addActionListener(e -> {
mirrorCheck.setSelected(false);
flipCheck.setSelected(false);
rotateCheck.setSelected(false);
grayscaleCheck.setSelected(false);
invertCheck.setSelected(false);
brightnessSlider.setValue(0);
contrastSlider.setValue(100);
cameraPanel.resetEffects();
});
panel.add(transformPanel);
panel.add(Box.createRigidArea(new Dimension(0, 10)));
panel.add(colorPanel);
panel.add(Box.createRigidArea(new Dimension(0, 10)));
panel.add(brightnessPanel);
panel.add(Box.createRigidArea(new Dimension(0, 15)));
panel.add(resetButton);
panel.add(Box.createVerticalGlue());
return panel;
}
private void browseDirectory(JTextField field, Component parent) {
JFileChooser chooser = new JFileChooser(field.getText());
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
if (chooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) {
field.setText(chooser.getSelectedFile().getAbsolutePath());
}
}
private void saveSnapshot(CameraPanel cameraPanel, Webcam webcam, String directory, Component parent) {
BufferedImage img = cameraPanel.getCurrentProcessedImage();
if (img != null) {
private void handleEffectsRequest(CameraInternalFrame source) {
EffectsPanelFrame effectsFrame = cameraToEffects.get(source);
if (effectsFrame != null) {
effectsFrame.setVisible(true);
try {
File dir = new File(directory);
dir.mkdirs();
String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String filename = "screenshot_" + webcam.getName().replaceAll("[^a-zA-Z0-9]", "_")
+ "_" + timestamp + ".png";
File file = new File(dir, filename);
ImageIO.write(img, "PNG", file);
} catch (Exception ex) {
JOptionPane.showMessageDialog(parent,
"Error: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
effectsFrame.setSelected(true);
effectsFrame.toFront();
} catch (java.beans.PropertyVetoException ex) {
JOptionPane.showMessageDialog(null, "Focus Error: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
}
private MouseAdapter popupListener() {
return new MouseAdapter() {
private void showPopup(MouseEvent e) {
if (e.isPopupTrigger()) { // cross-platform trigger
popupMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
@Override public void mousePressed(MouseEvent e) { showPopup(e); }
@Override public void mouseReleased(MouseEvent e) { showPopup(e); }
};
}
private void initPopupMenu() {
popupMenu.removeAll(); // clean slate
JCheckBoxMenuItem fullscreenItem = new JCheckBoxMenuItem("Fullscreen");
fullscreenItem.addActionListener(e -> toggleFullscreen());
popupMenu.addPopupMenuListener(new javax.swing.event.PopupMenuListener() {
@Override
public void popupMenuWillBecomeVisible(javax.swing.event.PopupMenuEvent e) {
fullscreenItem.setState(fullscreen);
}
@Override public void popupMenuWillBecomeInvisible(javax.swing.event.PopupMenuEvent e) {}
@Override public void popupMenuCanceled(javax.swing.event.PopupMenuEvent e) {}
});
popupMenu.add(fullscreenItem);
}
private void setupBlackBg() {
JRootPane root = mainFrame.getRootPane();
root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke("B"), "toggleBlackBg");
root.getActionMap().put("toggleBlackBg", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleBackground();
}
});
}
private void setupFullscreenToggle() {
JRootPane root = mainFrame.getRootPane();
root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke("F11"), "toggleFullscreen");
root.getActionMap().put("toggleFullscreen", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleFullscreen();
}
});
}
private void toggleBackground() {
desktopPane.setBackground(blackbg ? defDesktopBg : bgcolor);
blackbg = !blackbg;
desktopPane.repaint();
}
/** Toggle fullscreen mode */
private void toggleFullscreen() {
if (!fullscreen) {
windowedBounds = mainFrame.getBounds();
mainFrame.dispose();
mainFrame.setUndecorated(true);
mainFrame.setExtendedState(JFrame.MAXIMIZED_BOTH);
mainFrame.setVisible(true);
} else {
mainFrame.dispose();
mainFrame.setUndecorated(false);
mainFrame.setExtendedState(JFrame.NORMAL);
mainFrame.setBounds(windowedBounds);
mainFrame.setVisible(true);
}
fullscreen = !fullscreen;
}
public void show() {
mainFrame.setLocationRelativeTo(null);
mainFrame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
SwingIFrame dashboard = new SwingIFrame();
dashboard.show();
});
}
}
}

View File

@@ -7,24 +7,32 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/*
* Some JSON stuff for camera config saving
* */
public class CameraSettings {
private static final File storage_file = new File("network_cameras.json");
private static final ObjectMapper mapper = new ObjectMapper();
private static final File STORAGE_FILE = new File("network_cameras.json");
private static final ObjectMapper MAPPER = new ObjectMapper();
public static List<CameraConfig> load() {
if (!storage_file.exists() || storage_file.length() == 0) return new ArrayList<>();
return loadFromFile(STORAGE_FILE);
}
public static List<CameraConfig> loadFromFile(File file) {
if (!file.exists() || file.length() == 0) return new ArrayList<>();
try {
return mapper.readValue(storage_file, new TypeReference<List<CameraConfig>>() {});
return MAPPER.readValue(file, new TypeReference<List<CameraConfig>>() {});
} catch (IOException e) {
System.err.println("Error reading camera config: " + e.getMessage());
return new ArrayList<>();
}
}
/**
* Saves a single camera. Prevents duplicates by name.
*/
public static void save(CameraConfig newCam) {
List<CameraConfig> current = load();
// Prevent duplicate names in the local storage
current.removeIf(cam -> cam.getName().equalsIgnoreCase(newCam.getName()));
current.add(newCam);
write(current);
}
@@ -37,7 +45,7 @@ public class CameraSettings {
private static void write(List<CameraConfig> list) {
try {
mapper.writerWithDefaultPrettyPrinter().writeValue(storage_file, list);
MAPPER.writerWithDefaultPrettyPrinter().writeValue(STORAGE_FILE, list);
} catch (IOException e) {
e.printStackTrace();
}

View File

@@ -1,13 +1,22 @@
package io.swtc.proccessing;
/*
*
* Soon to be deprecated!
*
* */
/**
* Gray World Algorithm.
*
* <p>
* This class is an implementation of the Gray World algorithm, an automatic
* white balancing method.
* </p>
*
* <p>
* See:
* <a href="https://acorn.stanford.edu/psych221/projects/2000/trek/GWimages.html">
* Stanford explanation
* </a>
* </p>
*/
@Deprecated
public class AutoGainProcessor {
public class AWBProccessor {
public float[] calculateAutoGains(int[] pixels) {
long rSum = 0, gSum = 0, bSum = 0;

View File

@@ -3,202 +3,67 @@ package io.swtc.proccessing;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
/*
*
* Now here its getting rather interesting! this class processes some
* important stuff!
*
* */
/**
* Enhanced CameraPanel with support for custom image processors
*/
public class CameraPanel extends JPanel {
private BufferedImage currentImage;
private boolean mirror = false;
private boolean flip = false;
private boolean rotate = false;
private boolean grayscale = false;
private boolean invert = false;
private int brightness = 0;
private float contrast = 1.0f;
private volatile BufferedImage sourceImage;
private volatile BufferedImage processedImage;
private Function<BufferedImage, BufferedImage> imageProcessor;
private final AtomicBoolean repaintScheduled = new AtomicBoolean(false);
public CameraPanel() {
setBackground(Color.BLACK);
setPreferredSize(new Dimension(640, 480));
}
public void setImage(BufferedImage img) {
this.currentImage = img;
this.repaint();
}
sourceImage = img;
public BufferedImage getCurrentImage() {
return currentImage;
}
public BufferedImage getCurrentProcessedImage() {
if (currentImage == null) {
return null;
if (repaintScheduled.compareAndSet(false, true)) {
SwingUtilities.invokeLater(() -> {
repaintScheduled.set(false);
repaint();
});
}
BufferedImage processed = currentImage;
// apply color effects
if (grayscale || invert || brightness != 0 || contrast != 1.0f) {
processed = applyColorEffects(processed);
}
// apply transform.
if (mirror || flip || rotate) {
processed = applyTransforms(processed);
}
return processed;
}
/* Helper Methods ... its the same boilerplate shit over and over again, im sick of this. */
public void setMirror(boolean mirror) {
this.mirror = mirror;
this.repaint();
}
public void setFlip(boolean flip) {
this.flip = flip;
this.repaint();
}
public void setRotate(boolean rotate) {
this.rotate = rotate;
this.repaint();
}
public void setGrayscale(boolean grayscale) {
this.grayscale = grayscale;
this.repaint();
}
public void setInvert(boolean invert) {
this.invert = invert;
this.repaint();
}
public void setBrightness(int brightness) {
this.brightness = brightness;
this.repaint();
}
public void setContrast(float contrast) {
this.contrast = contrast;
this.repaint();
}
public void resetEffects() {
mirror = flip = rotate = grayscale = invert = false;
brightness = 0;
contrast = 1.0f;
this.repaint();
public void setImageProcessor(Function<BufferedImage, BufferedImage> processor) {
this.imageProcessor = processor;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (currentImage != null) {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
BufferedImage processedImage = currentImage;
BufferedImage src = sourceImage;
if (src == null) return;
// effects
if (grayscale || invert || brightness != 0 || contrast != 1.0f) {
processedImage = applyColorEffects(processedImage);
}
BufferedImage img = src;
// transforms
int w = getWidth(), h = getHeight();
if (rotate) {
g2.translate(w / 2, h / 2);
g2.rotate(Math.PI);
g2.translate(-w / 2, -h / 2);
}
// here we have if, cause we need to do the stuff, yk?
if (mirror && flip) {
g2.drawImage(processedImage, w, h, -w, -h, null);
} else if (mirror) {
g2.drawImage(processedImage, w, 0, -w, h, null);
} else if (flip) {
g2.drawImage(processedImage, 0, h, w, -h, null);
} else {
g2.drawImage(processedImage, 0, 0, w, h, null);
}
}
}
private BufferedImage applyColorEffects(BufferedImage img) {
BufferedImage result = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB);
for (int y = 0; y < img.getHeight(); y++) {
for (int x = 0; x < img.getWidth(); x++) {
int rgb = img.getRGB(x, y);
int r = (rgb >> 16) & 0xFF;
int g = (rgb >> 8) & 0xFF;
int b = rgb & 0xFF;
if (grayscale) {
int gray = (r + g + b) / 3;
r = g = b = gray;
}
// this is fun, this regulates the brightness or the contrast!
// this is some real java, the other stuff is just UI design, which i am bad at,
// but this! This is some real shit
r = (int) ((r - 128) * contrast + 128 + brightness);
g = (int) ((g - 128) * contrast + 128 + brightness);
b = (int) ((b - 128) * contrast + 128 + brightness);
// invert the colors!
if (invert) {
r = 255 - r;
g = 255 - g;
b = 255 - b;
}
// clamp so we dont get into weird color grades, or any weird looking spaces
// if we dont do this we cant really do stuff which looks bearable
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
result.setRGB(x, y, (r << 16) | (g << 8) | b);
}
if (imageProcessor != null) {
// we only apply the proccessor if it is present
// that is due too CPU Usage ; This project was built for a DUAL Core CPU, not a quad or even 6 Core CPU
img = imageProcessor.apply(src);
}
return result;
processedImage = img;
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(
RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR
);
g2d.drawImage(img, 0, 0, getWidth(), getHeight(), null);
}
private BufferedImage applyTransforms(BufferedImage img) {
int width = img.getWidth();
int height = img.getHeight();
BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int sourceX = x;
int sourceY = y;
if (mirror) {
sourceX = width - 1 - x;
}
if (flip) {
sourceY = height - 1 - y;
}
if (rotate) {
int tempX = width - 1 - sourceX;
int tempY = height - 1 - sourceY;
sourceX = tempX;
sourceY = tempY;
}
result.setRGB(x, y, img.getRGB(sourceX, sourceY));
}
}
return result;
public BufferedImage getCurrentProcessedImage() {
return processedImage;
}
}
}

View File

@@ -0,0 +1,62 @@
package io.swtc.proccessing;
import java.util.stream.IntStream;
public class ColorProccessor {
public void process(int[] pixels, EffectState state, float[] awbGains) {
float[] effectiveGains = (state.awbEnabled() && awbGains != null) ? awbGains : new float[]{1f, 1f, 1f};
// Parallel processing for O(N) pixel operations
IntStream.range(0, pixels.length).parallel().forEach(i -> {
int rgb = pixels[i];
int r = (rgb >> 16) & 0xFF;
int g = (rgb >> 8) & 0xFF;
int b = rgb & 0xFF;
if (state.awbEnabled()) {
float s = state.awbStrength() / 100f;
r = Math.min(255, (int)(r * (1 + (effectiveGains[0] - 1) * s)));
g = Math.min(255, (int)(g * (1 + (effectiveGains[1] - 1) * s)));
b = Math.min(255, (int)(b * (1 + (effectiveGains[2] - 1) * s)));
}
// 2. Temperature & Tint
if (state.temperature() != 0) {
float factor = state.temperature() / 100f;
r = ImageUtils.clamp(r + (int)(factor * 30));
b = ImageUtils.clamp(b - (int)(factor * 30));
}
if (state.tint() != 0) {
g = ImageUtils.clamp(g + (int)((state.tint() / 100f) * 20));
}
// 3. Saturation
if (state.saturation() != 100) {
float factor = state.saturation() / 100f;
float gray = (r + g + b) / 3f;
r = ImageUtils.clamp((int)(gray + (r - gray) * factor));
g = ImageUtils.clamp((int)(gray + (g - gray) * factor));
b = ImageUtils.clamp((int)(gray + (b - gray) * factor));
}
// 4. Shadows & Highlights
if (state.shadows() != 0 || state.highlights() != 0) {
float lum = (r + g + b) / 765f; // 765 = 3 * 255
if (lum < 0.5f && state.shadows() != 0) {
int adj = (int)((state.shadows() / 100f) * (1 - lum * 2) * 50);
r = ImageUtils.clamp(r + adj);
g = ImageUtils.clamp(g + adj);
b = ImageUtils.clamp(b + adj);
} else if (lum > 0.5f && state.highlights() != 0) {
int adj = (int)((state.highlights() / 100f) * (lum * 2 - 1) * 50);
r = ImageUtils.clamp(r + adj);
g = ImageUtils.clamp(g + adj);
b = ImageUtils.clamp(b + adj);
}
}
pixels[i] = (r << 16) | (g << 8) | b;
});
}
}

View File

@@ -0,0 +1,51 @@
package io.swtc.proccessing;
import java.util.stream.IntStream;
public class DenoiseProccessor {
public int[] process(int[] srcPixels, int width, int height, float strength) {
if (strength <= 0) return srcPixels;
int[] dstPixels = new int[srcPixels.length];
int[] tempPixels = new int[srcPixels.length];
int radius = (int) (strength / 100f * 2) + 1;
// Pass 1: Horizontal
IntStream.range(0, height).parallel().forEach(y ->
blurLine(srcPixels, tempPixels, width, height, y, radius, true)
);
// Pass 2: Vertical
IntStream.range(0, width).parallel().forEach(x ->
blurLine(tempPixels, dstPixels, width, height, x, radius, false)
);
return dstPixels;
}
private void blurLine(int[] src, int[] dest, int w, int h, int lineIndex, int radius, boolean horizontal) {
int length = horizontal ? w : h;
int limit = length - 1;
for (int i = 0; i < length; i++) {
long rSum = 0, gSum = 0, bSum = 0;
int count = 0;
int start = Math.max(0, i - radius);
int end = Math.min(limit, i + radius);
for (int k = start; k <= end; k++) {
int idx = horizontal ? (lineIndex * w + k) : (k * w + lineIndex);
int rgb = src[idx];
rSum += (rgb >> 16) & 0xFF;
gSum += (rgb >> 8) & 0xFF;
bSum += rgb & 0xFF;
count++;
}
int targetIdx = horizontal ? (lineIndex * w + i) : (i * w + lineIndex);
dest[targetIdx] = ((int)(rSum/count) << 16) | ((int)(gSum/count) << 8) | (int)(bSum/count);
}
}
}

View File

@@ -0,0 +1,6 @@
package io.swtc.proccessing;
public record EffectState(boolean awbEnabled, int awbStrength, boolean dnrEnabled, int dnrSpatial, int dnrTemporal,
int temperature, int tint, int saturation, int shadows, int highlights, int sharpness,
boolean edgeEnhance) {
}

View File

@@ -0,0 +1,135 @@
package io.swtc.proccessing;
import io.swtc.proccessing.ui.UIFactory;
import io.swtc.proccessing.ui.sections.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
/**
* This is basically the UI Commander, this orchestrates the whole ui package into one useful
* feature
*
* Without this the code will look like ass
* */
public class FilterPanel extends JPanel {
private final CameraPanel cameraPanel;
private final AWBProccessor awbProcessor;
private final PresetLibrary presetLibrary;
private WhiteBalanceSection awbSection;
private DNRSection dnrSection;
private ColorSection colorSection;
private DetailSection detailSection;
private float[] currentGains = {1f, 1f, 1f};
public FilterPanel(CameraPanel cameraPanel) {
this.cameraPanel = cameraPanel;
this.awbProcessor = new AWBProccessor();
this.presetLibrary = new PresetLibrary();
initializePanel();
buildUI();
initGlobalListeners();
}
private void initializePanel() {
setLayout(new BorderLayout());
setBorder(new EmptyBorder(10, 10, 10, 10));
}
private void buildUI() {
PresetSection presetSection = new PresetSection(presetLibrary, this::applyPresetToUI, this::getCurrentState);
add(presetSection, BorderLayout.NORTH);
JPanel scrollContainer = new JPanel();
scrollContainer.setLayout(new BoxLayout(scrollContainer, BoxLayout.Y_AXIS));
awbSection = new WhiteBalanceSection(this::performOneTimeBalance, this::applyToCamera);
dnrSection = new DNRSection(this::applyToCamera);
colorSection = new ColorSection(this::applyToCamera);
detailSection = new DetailSection(this::applyToCamera);
scrollContainer.add(awbSection);
scrollContainer.add(Box.createRigidArea(new Dimension(0, 10)));
scrollContainer.add(dnrSection);
scrollContainer.add(Box.createRigidArea(new Dimension(0, 10)));
scrollContainer.add(colorSection);
scrollContainer.add(Box.createRigidArea(new Dimension(0, 10)));
scrollContainer.add(detailSection);
add(UIFactory.createTransparentScrollPane(scrollContainer), BorderLayout.CENTER);
JPanel footer = new JPanel(new GridLayout(1, 1, 5, 5));
footer.setBorder(new EmptyBorder(10, 0, 0, 0));
footer.add(UIFactory.createActionButton("Reset All Factory Settings", e -> resetUI()));
add(footer, BorderLayout.SOUTH);
}
public EffectState getCurrentState() {
return new EffectState(
awbSection.isEnabled(), awbSection.getStrength(),
dnrSection.isEnabled(), dnrSection.getSpatial(), dnrSection.getTemporal(),
colorSection.getTemp(), colorSection.getTint(), colorSection.getSaturation(),
colorSection.getShadows(), colorSection.getHighlights(),
detailSection.getSharpness(), detailSection.isEdgeEnhanceEnabled()
);
}
private void applyToCamera() {
if (cameraPanel == null) return;
EffectState state = getCurrentState();
cameraPanel.setImageProcessor(img ->
ImageEffectEngine.applyEffects(img, state, currentGains)
);
}
/**
* call awb stuff
*/
private void performOneTimeBalance() {
java.awt.image.BufferedImage img = cameraPanel.getCurrentProcessedImage();
if (img != null) {
int[] pixels = img.getRGB(0, 0, img.getWidth(), img.getHeight(), null, 0, img.getWidth());
currentGains = awbProcessor.calculateAutoGains(pixels);
applyToCamera();
}
}
/**
* Update for a specified state
* */
private void applyPresetToUI(EffectState s) {
awbSection.setState(s.awbEnabled(), s.awbStrength());
dnrSection.setState(s.dnrEnabled(), s.dnrSpatial(), s.dnrTemporal());
colorSection.setState(s.temperature(), s.tint(), s.saturation(), s.shadows(), s.highlights());
detailSection.setState(s.sharpness(), s.edgeEnhance());
// Reset gains if AWB is disabled in the preset
if (!s.awbEnabled()) currentGains = new float[]{1f, 1f, 1f};
applyToCamera();
}
private void resetUI() {
applyPresetToUI(new EffectState(false, 100, false, 30, 50, 0, 0, 100, 0, 0, 100, false));
}
private void initGlobalListeners() {
java.awt.event.ComponentAdapter repaintListener = new java.awt.event.ComponentAdapter() {
@Override public void componentMoved(java.awt.event.ComponentEvent e) { updateOverlay(); }
@Override public void componentResized(java.awt.event.ComponentEvent e) { updateOverlay(); }
};
this.addComponentListener(repaintListener);
if (cameraPanel != null) cameraPanel.addComponentListener(repaintListener);
}
private void updateOverlay() {
RootPaneContainer root = (RootPaneContainer) SwingUtilities.getWindowAncestor(this);
if (root != null) root.getGlassPane().repaint();
}
}

View File

@@ -0,0 +1,40 @@
package io.swtc.proccessing;
import java.awt.image.BufferedImage;
public class ImageEffectEngine {
private static final ColorProccessor colorProcessor = new ColorProccessor();
private static final DenoiseProccessor denoiseProcessor = new DenoiseProccessor();
private static final SharpnessProccessor sharpnessProcessor = new SharpnessProccessor();
public static BufferedImage applyEffects(BufferedImage img, EffectState state, float[] currentGains) {
if (img == null) return null;
// 1. Extract raw data (High Performance)
int width = img.getWidth();
int height = img.getHeight();
int[] pixels = ImageUtils.getPixels(img);
// NOTE: If we want to avoid modifying the original image's backing array,
// we should clone 'pixels' here. If in-place modification is okay, we proceed.
// int[] workingPixels = pixels.clone();
int[] workingPixels = pixels; // Assuming in-place is fine for performance
// 2. Apply Color Pipeline (In-Place)
colorProcessor.process(workingPixels, state, currentGains);
// 3. Apply Sharpness (Returns new array if applied)
if (state.sharpness() != 100 || state.edgeEnhance()) {
workingPixels = sharpnessProcessor.process(workingPixels, width, height, state.sharpness(), state.edgeEnhance());
}
// 4. Apply Denoise (Returns new array if applied)
if (state.dnrEnabled()) {
workingPixels = denoiseProcessor.process(workingPixels, width, height, state.dnrSpatial());
}
// 5. Reconstruct Image
return ImageUtils.createFromPixels(workingPixels, width, height);
}
}

View File

@@ -0,0 +1,36 @@
package io.swtc.proccessing;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
public class ImageUtils {
public static int[] getPixels(BufferedImage img) {
return ((DataBufferInt) ensureIntRGB(img).getRaster().getDataBuffer()).getData();
}
public static BufferedImage createFromPixels(int[] pixels, int width, int height) {
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
int[] dst = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
System.arraycopy(pixels, 0, dst, 0, pixels.length);
return img;
}
public static BufferedImage ensureIntRGB(BufferedImage img) {
if (img.getType() == BufferedImage.TYPE_INT_RGB) {
return img;
}
BufferedImage newImg = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics g = newImg.getGraphics();
g.drawImage(img, 0, 0, null);
g.dispose();
return newImg;
}
public static int clamp(int val) {
if (val < 0) return 0;
if (val > 255) return 255;
return val;
}
}

View File

@@ -0,0 +1,46 @@
package io.swtc.proccessing;
import java.util.HashMap;
import java.util.Map;
public class PresetLibrary {
private final Map<String, EffectState> presets = new HashMap<>();
public PresetLibrary() {
presets.put("Natural", new EffectState(
false, 100, false, 20, 40, 0, 0, 100, 0, 0, 100, false
));
presets.put("Vivid", new EffectState(
true, 100, false, 15, 30, 10, 5, 130, 0, 0, 120, true
));
presets.put("Portrait", new EffectState(
true, 80, true, 40, 50, -5, 10, 95, 10, -5, 90, false
));
presets.put("Low Light", new EffectState(
true, 100, true, 60, 70, 0, 0, 110, 20, -10, 80, false
));
presets.put("High Contrast", new EffectState(
false, 100, false, 25, 35, 0, 0, 120, -20, 20, 130, true
));
presets.put("Cinematic", new EffectState(
true, 70, true, 30, 45, -15, -5, 90, -10, 5, 110, false
));
}
public void savePreset(String name, EffectState state) {
presets.put(name, state);
}
public EffectState get(String name) {
return presets.get(name);
}
public String[] getPresetNames() {
return presets.keySet().toArray(new String[0]);
}
}

View File

@@ -0,0 +1,70 @@
package io.swtc.proccessing;
import java.util.stream.IntStream;
public class SharpnessProccessor {
public int[] process(int[] srcPixels, int width, int height, float amount, boolean edgeEnhance) {
if (amount == 0 && !edgeEnhance) return srcPixels;
int[] dstPixels = new int[srcPixels.length];
// Normalization setup
float centerWeight = edgeEnhance ? 5f : 9f;
float neighborWeight = -1f;
float strength = (amount / 100f);
// Adjust strength scaling to match your original "amount - 1 / 8f" logic if needed,
// but typically sharpness is 0.0 to 1.0.
// Adapting to your specific previous math:
float weightFactor = (amount / 100f - 1) / 8f;
// Parallel loop skipping borders
IntStream.range(1, height - 1).parallel().forEach(y -> {
int yOffset = y * width;
for (int x = 1; x < width - 1; x++) {
int i = yOffset + x;
float rAcc = 0, gAcc = 0, bAcc = 0;
// Center
int pC = srcPixels[i];
float wC = centerWeight * weightFactor + 1.0f;
rAcc += ((pC >> 16) & 0xFF) * wC;
gAcc += ((pC >> 8) & 0xFF) * wC;
bAcc += (pC & 0xFF) * wC;
// Neighbors (North, South, East, West)
int[] neighbors = {
srcPixels[i - width], srcPixels[i + width],
srcPixels[i - 1], srcPixels[i + 1]
};
float wN = neighborWeight * weightFactor;
for(int p : neighbors) {
rAcc += ((p >> 16) & 0xFF) * wN;
gAcc += ((p >> 8) & 0xFF) * wN;
bAcc += (p & 0xFF) * wN;
}
// Diagonals (only if not edge enhance mode, per your original code)
if (!edgeEnhance) {
int[] diags = {
srcPixels[i - width - 1], srcPixels[i - width + 1],
srcPixels[i + width - 1], srcPixels[i + width + 1]
};
for(int p : diags) {
rAcc += ((p >> 16) & 0xFF) * wN;
gAcc += ((p >> 8) & 0xFF) * wN;
bAcc += (p & 0xFF) * wN;
}
}
dstPixels[i] = (ImageUtils.clamp((int)rAcc) << 16) |
(ImageUtils.clamp((int)gAcc) << 8) |
ImageUtils.clamp((int)bAcc);
}
});
return dstPixels;
}
}

View File

@@ -2,86 +2,122 @@ package io.swtc.proccessing;
import com.github.sarxos.webcam.Webcam;
import com.github.sarxos.webcam.WebcamException;
import com.github.sarxos.webcam.WebcamResolution;
import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
import java.util.function.Consumer;
import javax.swing.JOptionPane;
import javax.swing.*;
import io.swtc.proccessing.ui.ShowError;
public class WebcamCaptureLoop {
private final Webcam webcam;
private final Consumer<BufferedImage> onFrameCaptured;
private volatile boolean running = false;
private final AtomicBoolean cleanedUp = new AtomicBoolean(false);
private final int targetframes = 20;
public WebcamCaptureLoop(Webcam webcam, Consumer<BufferedImage> onFrameCaptured) {
this.webcam = webcam;
this.onFrameCaptured = onFrameCaptured;
// this is some of the most stupid shit ive seen in years, this is needed for opening the stream.
// the webcam package may not know the res before its opened and well this is before we open it. that is below in the thread.
// so this freaks out and doesnt want to cooperate.
if (!webcam.getName().toLowerCase().contains("ipcam")) {
Dimension[] sizes = webcam.getViewSizes();
webcam.setViewSize(sizes[sizes.length - 1]);
Dimension vga = WebcamResolution.VGA.getSize();
if (isSizeSupported(webcam, vga)) { // we dont do stupid shit anymore! cause we are smarter than before...
webcam.setViewSize(vga);
} else {
Dimension[] dimensions = webcam.getViewSizes();
webcam.setViewSize(dimensions[dimensions.length - 1]);
}
}
private boolean isSizeSupported(Webcam webcam, Dimension vga) {
for (Dimension d: webcam.getViewSizes()) {
if (
d.width == vga.width && d.height == vga.height
) return true; // this should return 640x480 :)
}
return false;
}
public void start() {
if (running) return;
running = true;
Thread captureThread = new Thread(() -> {
// this is where we open it. if the res isnt known then it fucks up.
webcam.open();
while (running) {
BufferedImage img = webcam.getImage();
if (img != null) {
//System.err.println(img);
onFrameCaptured.accept(img);
}
try {
Thread.sleep(15);
} catch (InterruptedException e) {
break;
}
}
try {
webcam.close();
if (!webcam.isOpen()) webcam.open(); // open if not
} catch (IllegalStateException e) {
// show a error message from javax.swing.awt.JOptionPane
JOptionPane.showMessageDialog(
long frameDurationLimitns = 1_000_000_000L / targetframes; // we use NanoSeconds cause its more accurate
while (running) {
long startTime = System.nanoTime();
BufferedImage img = webcam.getImage();
if (img != null)
// there is no need for a invoke later swing wise!
onFrameCaptured.accept(img);
long timespent = System.nanoTime() - startTime;
long sleeptime = frameDurationLimitns - timespent;
if (sleeptime > 0) {
LockSupport.parkNanos(sleeptime);
} else {
// do a yield to prevent ui freezes
Thread.yield();
}
}
} catch (Exception e) {
ShowError.warning(
null,
"IllegalStateException@"+e,
"IllegalStateException",
JOptionPane.ERROR_MESSAGE
"Exception" + e,
"Exception"
);
} finally {
cleanup();
}
});
captureThread.setName("cam_cap_thread");
}, "cam_cap_thread");
captureThread.start();
}
// method for cleaning up
/**
* Safely release webcam
*/
private synchronized void cleanup() {
if (!cleanedUp.compareAndSet(false, true)) {
return;
}
boolean success = false;
try {
// if the camera is open try to cloes it!
webcam.close();
if (webcam.isOpen())
webcam.close();
success = true;
} catch (WebcamException exception) {
SwingUtilities.invokeLater(() ->
JOptionPane.showMessageDialog(
null,
"Cleanup failed \nWebCamException@"+exception.getMessage(),
"WebCamException",
JOptionPane.ERROR_MESSAGE
);
JOptionPane.WARNING_MESSAGE // changed to warning, its better tbh
));
} finally {
if (!success)
cleanedUp.set(false);
}
}
/**
* Arguments: null (or just none)
* What does this method do? : Sets CameraThreads running flag to false and calls cleanup();
*/
public void stop() {
running = false;
cleanup();
}
}

View File

@@ -0,0 +1,29 @@
package io.swtc.proccessing.ui;
import javax.swing.*;
import java.awt.*;
public abstract class FilterSection extends JPanel {
public FilterSection(String title) {
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(), title));
}
/**
* Helper to create and add a slider in one line
*/
protected LabeledSlider addSlider(String label, int min, int max, int val, String unit) {
LabeledSlider ls = new LabeledSlider(label, min, max, val, unit);
add(ls);
return ls;
}
/**
* Helper to add spacing between elements
*/
protected void addPadding(int height) {
add(Box.createRigidArea(new Dimension(0, height)));
}
}

View File

@@ -0,0 +1,40 @@
package io.swtc.proccessing.ui;
import javax.swing.*;
import java.awt.*;
import java.net.URL;
import java.util.Objects;
public class IconSetter {
private static Image ICON_IMAGE;
private static ImageIcon ICON_ICON;
private static Image effects_icon;
public static Image getIcon() {
if (ICON_IMAGE == null) {
URL url = IconSetter.class.getResource("/icons/artwork.png");
if (url == null) throw new RuntimeException("Icon not found: /icons/artwork.png");
ICON_IMAGE = Toolkit.getDefaultToolkit().getImage(url);
}
return ICON_IMAGE;
}
public static Image getEffectIcon() {
if (Objects.isNull(effects_icon)) {
URL url = IconSetter.class.getResource("/icons/effectsframe.png");
if (Objects.isNull(url)) ShowError.error(null,"Error","Icon not found");
effects_icon = Toolkit.getDefaultToolkit().getImage(url);
}
return effects_icon;
}
public static ImageIcon getIconAsImageIcon() {
if (ICON_ICON == null) {
URL url = IconSetter.class.getResource("/icons/artwork.png");
if (url == null) throw new RuntimeException("Icon not found: /icons/artwork.png");
ICON_ICON = new ImageIcon(url); // separate variable for ImageIcon
}
return ICON_ICON;
}
}

View File

@@ -0,0 +1,45 @@
package io.swtc.proccessing.ui;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
public class LabeledSlider extends JPanel {
private final JSlider slider;
private final JLabel label;
private final String title;
private final String unit;
public LabeledSlider(String title, int min, int max, int value, String unit) {
this.title = title;
this.unit = unit;
setLayout(new BorderLayout());
label = new JLabel(title + ": " + value + unit);
slider = new JSlider(min, max, value);
// Internal listener to update the text label as user drags
slider.addChangeListener(e -> updateLabel());
add(label, BorderLayout.NORTH);
add(slider, BorderLayout.CENTER);
setBorder(new EmptyBorder(5, 0, 5, 0));
}
private void updateLabel() {
label.setText(title + ": " + slider.getValue() + unit);
}
public int getValue() { return slider.getValue(); }
public void setValue(int val) {
slider.setValue(val);
updateLabel();
}
public JSlider getSlider() { return slider; }
public void addChangeListener(javax.swing.event.ChangeListener cl) {
slider.addChangeListener(cl);
}
}

View File

@@ -0,0 +1,38 @@
package io.swtc.proccessing.ui;
import javax.swing.*;
import java.awt.*;
public final class ShowError {
private ShowError() {
// we dont instantiate cause it causes some errors
}
public static void error(Component parent, String title, String message) {
JOptionPane.showMessageDialog(
parent,
title,
message,
JOptionPane.ERROR_MESSAGE
);
}
public static void warning(Component parent, String title, String message) {
JOptionPane.showMessageDialog(
parent,
title,
message,
JOptionPane.WARNING_MESSAGE
);
}
public static void info(Component parent, String title, String message) {
JOptionPane.showMessageDialog(
parent,
message,
title,
JOptionPane.INFORMATION_MESSAGE
);
}
}

View File

@@ -0,0 +1,21 @@
package io.swtc.proccessing.ui;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
public class UIFactory {
public static JButton createActionButton(String text, java.awt.event.ActionListener listener) {
JButton btn = new JButton(text);
btn.addActionListener(listener);
return btn;
}
public static JScrollPane createTransparentScrollPane(Component view) {
JScrollPane scroll = new JScrollPane(view);
scroll.setBorder(null);
scroll.getVerticalScrollBar().setUnitIncrement(16);
return scroll;
}
}

View File

@@ -0,0 +1,90 @@
package io.swtc.proccessing.ui.iframe;
import com.github.sarxos.webcam.Webcam;
import io.swtc.proccessing.WebcamCaptureLoop;
import io.swtc.proccessing.CameraPanel;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.ShowError;
import io.swtc.recording.RecordingManager;
import io.swtc.recording.VideoRecorder;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.RescaleOp;
import java.io.File;
import java.io.IOException;
import java.util.function.Consumer;
public class CameraInternalFrame extends JInternalFrame {
private final WebcamCaptureLoop captureLoop;
private final CameraPanel cameraPanel;
private final VideoRecorder videoRecorder;
private final RecordingManager recordingManager;
public CameraInternalFrame(Webcam webcam, Consumer<CameraInternalFrame> onOpenEffects) {
super(webcam.getName(), true, true, true, true);
Image ico = IconSetter.getIcon();
setFrameIcon(new ImageIcon(ico));
this.cameraPanel = new CameraPanel();
this.videoRecorder = new VideoRecorder();
this.recordingManager = new RecordingManager(cameraPanel); // Initialize recorder
this.captureLoop = new WebcamCaptureLoop(webcam, img ->
SwingUtilities.invokeLater(() -> cameraPanel.setImage(img))
);
setupUI(onOpenEffects);
captureLoop.start();
}
private void setupUI(Consumer<CameraInternalFrame> onOpenEffects) {
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab("View", cameraPanel);
tabbedPane.addTab("Capture", new JPanel());
tabbedPane.addTab("Effects", new JPanel());
tabbedPane.addChangeListener(e -> {
int index = tabbedPane.getSelectedIndex();
if (index == 1) { // the tab index for capture is 1
tabbedPane.setSelectedIndex(0);
openRecordingFrame();
} else if (index == 2) {
tabbedPane.setSelectedIndex(0);
onOpenEffects.accept(this);
}
});
add(tabbedPane);
setSize(600, 500);
}
private void openRecordingFrame() {
RecordingFrame rf = new RecordingFrame(this.getTitle(),cameraPanel, videoRecorder);
JDesktopPane desktopPane = getDesktopPane();
if (desktopPane != null) {
desktopPane.add(rf);
rf.setVisible(true);
}
try {
rf.setSelected(true);
} catch (java.beans.PropertyVetoException veto) {
ShowError.error(null,"VetoException"+veto.getMessage(),"veto");
}
}
public CameraPanel getCameraPanel() { return cameraPanel; }
@Override
public void dispose() {
if (videoRecorder.isRecording()) {
try { videoRecorder.stopRecording(); } catch (IOException ignored) {}
}
captureLoop.stop();
super.dispose();
}
}

View File

@@ -0,0 +1,66 @@
package io.swtc.proccessing.ui.iframe;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.CubicCurve2D;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class DesktopPane extends JDesktopPane {
private final Map<JInternalFrame, EffectsPanelFrame> connections;
private final Map<JInternalFrame, Color> connectionColors = new HashMap<>();
public DesktopPane(Map<JInternalFrame, EffectsPanelFrame> connections) {
this.connections = connections;
}
private Color getConnectionColor(JInternalFrame frame) {
return connectionColors.computeIfAbsent(frame, k -> {
Random rand = new Random();
return new Color(rand.nextInt(256), rand.nextInt(256), rand.nextInt(256), 200);
});
}
public void forgetFrame(JInternalFrame frame) {
connectionColors.remove(frame);
}
@Override
protected void paintChildren(Graphics g) {
super.paintChildren(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
for (Map.Entry<JInternalFrame, EffectsPanelFrame> entry : connections.entrySet()) {
JInternalFrame camera = entry.getKey();
EffectsPanelFrame effects = entry.getValue();
if (camera.isVisible() && effects.isVisible() && !camera.isIcon() && !effects.isIcon()) {
g2d.setColor(getConnectionColor(camera));
drawBezierConnection(g2d, camera, effects);
}
}
g2d.dispose();
}
private void drawBezierConnection(Graphics2D g2d, JInternalFrame from, JInternalFrame to) {
Rectangle f = from.getBounds();
Rectangle t = to.getBounds();
int x1 = f.x + f.width;
int y1 = f.y + (f.height / 2);
int x2 = t.x;
int y2 = t.y + (t.height / 2);
int ctrlOffset = Math.min(Math.abs(x2 - x1) / 2, 150);
CubicCurve2D curve = new CubicCurve2D.Double(x1, y1, x1 + ctrlOffset, y1, x2 - ctrlOffset, y2, x2, y2);
//g2d.setStroke(new BasicStroke(3f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
//g2d.draw(curve);
// Terminals
//g2d.fillOval(x1 - 5, y1 - 5, 10, 10);
//g2d.fillOval(x2 - 5, y2 - 5, 10, 10);
}
}

View File

@@ -0,0 +1,21 @@
package io.swtc.proccessing.ui.iframe;
import io.swtc.proccessing.CameraPanel;
import io.swtc.proccessing.FilterPanel;
import io.swtc.proccessing.ui.IconSetter;
import javax.swing.*;
import java.awt.*;
public class EffectsPanelFrame extends JInternalFrame {
public EffectsPanelFrame(String title, CameraPanel cameraPanel) {
super(title, true, true, true, true);
Image ico = IconSetter.getIcon();
setFrameIcon(new ImageIcon(IconSetter.getEffectIcon()));
setDefaultCloseOperation(HIDE_ON_CLOSE);
add(new FilterPanel(cameraPanel));
setSize(350, 600);
}
}

View File

@@ -0,0 +1,178 @@
package io.swtc.proccessing.ui.iframe;
import io.swtc.proccessing.CameraPanel;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.ShowError;
import io.swtc.recording.VideoRecorder;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
public class RecordingFrame extends JInternalFrame {
private final VideoRecorder videoRecorder;
private final CameraPanel cameraPanel;
private JButton recordBtn;
private JLabel statusLabel;
private JLabel statsLabel;
private File outputDirectory = new File(System.getProperty("user.home"));
private File currentFile;
private final Timer statsTimer;
private long startTime;
// Cache for string building to avoid object allocation every second
private final StringBuilder sb = new StringBuilder(32);
public RecordingFrame(String cameraName, CameraPanel cameraPanel, VideoRecorder videoRecorder) {
super(cameraName + " Capture", true, true, false, true);
Image ico = IconSetter.getIcon();
setFrameIcon(new ImageIcon(ico));
this.cameraPanel = cameraPanel;
this.videoRecorder = videoRecorder;
initializeUI();
this.statsTimer = new Timer(1000, e -> updateStats());
this.statsTimer.setCoalesce(true);
pack();
}
private void initializeUI() {
JPanel mainContent = new JPanel();
mainContent.setLayout(new BoxLayout(mainContent, BoxLayout.Y_AXIS));
mainContent.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
mainContent.add(createStoragePanel());
mainContent.add(Box.createVerticalStrut(10));
mainContent.add(createActionPanel());
mainContent.add(Box.createVerticalStrut(10));
mainContent.add(createStatsPanel());
getContentPane().add(mainContent);
}
private JPanel createStatsPanel() {
JPanel panel = new JPanel(new GridLayout(2, 1));
panel.setBorder(BorderFactory.createTitledBorder("Session Info"));
statusLabel = new JLabel("Status: Idle");
statsLabel = new JLabel("Length: 00:00 | Size: 0.00 MB");
statsLabel.setFont(new Font("Monospaced", Font.PLAIN, 12));
statsLabel.setPreferredSize(new Dimension(220, 20));
panel.add(statusLabel);
panel.add(statsLabel);
return panel;
}
private void updateStats() {
if (!videoRecorder.isRecording() || currentFile == null) return;
long elapsedSecs = (System.currentTimeMillis() - startTime) / 1000;
long minutes = elapsedSecs / 60;
long seconds = elapsedSecs % 60;
double sizeInMb = currentFile.length() / 1048576.0;
sb.setLength(0);
sb.append("Length: ")
.append(minutes < 10 ? "0" : "").append(minutes).append(":")
.append(seconds < 10 ? "0" : "").append(seconds)
.append(" | Size: ")
.append(Math.round(sizeInMb * 100.0) / 100.0)
.append(" MB");
statsLabel.setText(sb.toString());
}
private void toggleRecording() {
if (!videoRecorder.isRecording()) {
startRec();
} else {
stopRec();
}
}
private void startRec() {
try {
currentFile = new File(outputDirectory, "vid_" + System.currentTimeMillis() + ".mp4");
videoRecorder.startRecording(cameraPanel, currentFile);
startTime = System.currentTimeMillis();
statsTimer.start(); // Only run timer when needed
recordBtn.setText("Stop Recording");
statusLabel.setText("Currently Recording");
} catch (IOException ex) {
ShowError.error(null,"Error starting Recording" + ex.getMessage(), "Error");
}
}
private void stopRec() {
try {
videoRecorder.stopRecording();
statsTimer.stop();
recordBtn.setText("Started Recording");
recordBtn.setForeground(null);
statusLabel.setText("Finished recording");
} catch (IOException ex) {
ShowError.error(null,"RecordStop Error"+ex.getMessage(), "Error");
}
}
private JPanel createStoragePanel() {
JPanel panel = new JPanel(new BorderLayout(5, 5));
panel.setBorder(BorderFactory.createTitledBorder("Storage"));
JTextField pathField = new JTextField(outputDirectory.getAbsolutePath());
pathField.setEditable(false);
JButton browseBtn = new JButton("...");
browseBtn.addActionListener(e -> {
JFileChooser chooser = new JFileChooser(outputDirectory);
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
outputDirectory = chooser.getSelectedFile();
pathField.setText(outputDirectory.getAbsolutePath());
}
});
panel.add(pathField, BorderLayout.CENTER);
panel.add(browseBtn, BorderLayout.EAST);
return panel;
}
private JPanel createActionPanel() {
JPanel panel = new JPanel(new GridLayout(1, 2, 5, 5));
recordBtn = new JButton("Start Recording");
recordBtn.addActionListener(e -> toggleRecording());
JButton snapBtn = new JButton("Snapshot");
snapBtn.addActionListener(e -> takeSnapshot());
panel.add(recordBtn);
panel.add(snapBtn);
return panel;
}
private void takeSnapshot() {
BufferedImage img = cameraPanel.getCurrentProcessedImage();
if (img != null) {
try {
File file = new File(outputDirectory, "snap_" + System.currentTimeMillis() + ".png");
ImageIO.write(img, "PNG", file);
statusLabel.setText("Saved a Snapshot");
} catch (IOException ex) {
ShowError.error(null,"Snapshot failed"+ex.getMessage(),"Snapshot Error");
}
}
}
}

View File

@@ -0,0 +1,37 @@
package io.swtc.proccessing.ui.sections;
import io.swtc.proccessing.ui.FilterSection;
import io.swtc.proccessing.ui.LabeledSlider;
public class ColorSection extends FilterSection {
private final LabeledSlider temp, tint, sat, shadows, highlights;
public ColorSection(Runnable onUpdate) {
super("Color Grading");
temp = addSlider("Temperature", -100, 100, 0, "");
tint = addSlider("Tint", -100, 100, 0, "");
sat = addSlider("Saturation", 0, 200, 100, "%");
shadows = addSlider("Shadows", -100, 100, 0, "");
highlights = addSlider("Highlights", -100, 100, 0, "");
LabeledSlider[] sliders = {temp, tint, sat, shadows, highlights};
for (LabeledSlider s : sliders) {
s.addChangeListener(e -> { if(!s.getSlider().getValueIsAdjusting()) onUpdate.run(); });
}
}
public int getTemp() { return temp.getValue(); }
public int getTint() { return tint.getValue(); }
public int getSaturation() { return sat.getValue(); }
public int getShadows() { return shadows.getValue(); }
public int getHighlights() { return highlights.getValue(); }
public void setState(int t, int ti, int s, int sh, int hi) {
temp.setValue(t);
tint.setValue(ti);
sat.setValue(s);
shadows.setValue(sh);
highlights.setValue(hi);
}
}

View File

@@ -0,0 +1,56 @@
package io.swtc.proccessing.ui.sections;
import io.swtc.proccessing.ui.FilterSection;
import io.swtc.proccessing.ui.LabeledSlider;
import javax.swing.*;
public class DNRSection extends FilterSection {
private final JCheckBox enabled;
private final LabeledSlider spatial;
private final LabeledSlider temporal;
public DNRSection(Runnable onUpdate) {
super("3D Denoise (DNR)");
enabled = new JCheckBox("Enable Temporal Denoise");
spatial = addSlider("Spatial Strength", 0, 100, 30, "%");
temporal = addSlider("Temporal Strength", 0, 100, 50, "%");
enabled.addActionListener(e -> {
updateEnabledStates();
onUpdate.run();
});
spatial.addChangeListener(e -> { if(!spatial.getSlider().getValueIsAdjusting()) onUpdate.run(); });
temporal.addChangeListener(e -> { if(!temporal.getSlider().getValueIsAdjusting()) onUpdate.run(); });
add(enabled, 0);
updateEnabledStates();
}
private void updateEnabledStates() {
boolean active = enabled.isSelected();
spatial.getSlider().setEnabled(active);
temporal.getSlider().setEnabled(active);
}
@Override
public boolean isEnabled() {
return enabled.isSelected();
}
public int getSpatial() {
return enabled.isSelected() ? spatial.getValue() : 0;
}
public int getTemporal() {
return enabled.isSelected() ? temporal.getValue() : 0;
}
public void setState(boolean isEnabled, int s, int t) {
enabled.setSelected(isEnabled);
spatial.setValue(s);
temporal.setValue(t);
updateEnabledStates();
}
}

View File

@@ -0,0 +1,38 @@
package io.swtc.proccessing.ui.sections;
import io.swtc.proccessing.ui.FilterSection;
import io.swtc.proccessing.ui.LabeledSlider;
import javax.swing.*;
public class DetailSection extends FilterSection {
private final LabeledSlider sharpness;
private final JCheckBox edgeEnhance;
public DetailSection(Runnable onUpdate) {
super("Detail & Sharpness");
sharpness = addSlider("Sharpness", 0, 200, 100, "%");
edgeEnhance = new JCheckBox("Edge Enhancement");
sharpness.addChangeListener(e -> {
if (!sharpness.getSlider().getValueIsAdjusting()) onUpdate.run();
});
edgeEnhance.addActionListener(e -> onUpdate.run());
add(edgeEnhance);
}
public int getSharpness() {
return sharpness.getValue();
}
public boolean isEdgeEnhanceEnabled() {
return edgeEnhance != null && edgeEnhance.isSelected();
}
public void setState(int sharpVal, boolean edgeActive) {
sharpness.setValue(sharpVal);
edgeEnhance.setSelected(edgeActive);
}
}

View File

@@ -0,0 +1,39 @@
package io.swtc.proccessing.ui.sections;
import io.swtc.proccessing.EffectState;
import io.swtc.proccessing.PresetLibrary;
import javax.swing.*;
import java.awt.*;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class PresetSection extends JPanel {
private final JComboBox<String> presetCombo;
private final JButton saveBtn;
public PresetSection(PresetLibrary library, Consumer<EffectState> onPresetSelected, Supplier<EffectState> stateSupplier) {
setLayout(new BorderLayout(5, 5));
setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), "Presets"));
presetCombo = new JComboBox<>(new String[]{"Custom", "Natural", "Vivid", "Portrait", "Low Light", "Cinematic"});
saveBtn = new JButton("Save");
presetCombo.addActionListener(e -> {
String selected = (String) presetCombo.getSelectedItem();
EffectState state = library.get(selected);
if (state != null) onPresetSelected.accept(state);
});
saveBtn.addActionListener(e -> {
String name = JOptionPane.showInputDialog(this, "Preset Name:");
if (name != null && !name.trim().isEmpty()) {
library.savePreset(name, stateSupplier.get());
presetCombo.addItem(name);
presetCombo.setSelectedItem(name);
}
});
add(presetCombo, BorderLayout.CENTER);
add(saveBtn, BorderLayout.EAST);
}
}

View File

@@ -0,0 +1,55 @@
package io.swtc.proccessing.ui.sections;
import io.swtc.proccessing.ui.FilterSection;
import io.swtc.proccessing.ui.LabeledSlider;
import javax.swing.*;
public class WhiteBalanceSection extends FilterSection {
private final JCheckBox enabled;
private final LabeledSlider strength;
private final JButton balanceBtn;
public WhiteBalanceSection(Runnable onBalanceNow, Runnable onUpdate) {
super("White Balance");
enabled = new JCheckBox("Enable AWB");
strength = addSlider("Strength", 0, 100, 100, "%");
balanceBtn = new JButton("Balance Now");
enabled.addActionListener(e -> {
updateEnabledStates();
onUpdate.run();
});
strength.addChangeListener(e -> {
if (!strength.getSlider().getValueIsAdjusting()) onUpdate.run();
});
balanceBtn.addActionListener(e -> onBalanceNow.run());
add(enabled, 0);
add(balanceBtn);
updateEnabledStates();
}
private void updateEnabledStates() {
boolean active = enabled.isSelected();
strength.getSlider().setEnabled(active);
balanceBtn.setEnabled(active);
}
@Override
public boolean isEnabled() {
return enabled != null && enabled.isSelected();
}
public int getStrength() {
return enabled.isSelected() ? strength.getValue() : 0;
}
public void setState(boolean isEnabled, int str) {
enabled.setSelected(isEnabled);
strength.setValue(str);
updateEnabledStates();
}
}

View File

@@ -0,0 +1,70 @@
package io.swtc.recording;
import io.swtc.proccessing.CameraPanel;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.function.Consumer;
public class RecordingManager {
private final VideoRecorder recorder;
private final CameraPanel cameraPanel;
// Reuse a single buffer to avoid constant memory allocation (GC pressure)
private BufferedImage reuseBuffer;
public RecordingManager(CameraPanel cameraPanel) {
this.cameraPanel = cameraPanel;
this.recorder = new VideoRecorder();
}
public BufferedImage fastConvertToRGB(BufferedImage source) {
if (source.getType() == BufferedImage.TYPE_INT_RGB) {
return source;
}
// Initialize or resize reuseBuffer only when necessary
if (reuseBuffer == null ||
reuseBuffer.getWidth() != source.getWidth() ||
reuseBuffer.getHeight() != source.getHeight()) {
reuseBuffer = new BufferedImage(source.getWidth(), source.getHeight(), BufferedImage.TYPE_INT_RGB);
}
// Graphics2D.drawImage is much faster than manual setRGB loops
Graphics2D g = reuseBuffer.createGraphics();
g.drawImage(source, 0, 0, null);
g.dispose();
return reuseBuffer;
}
public void toggleRecording(File outputFile, Runnable onStart, Consumer<File> onStop, Consumer<Exception> onError) {
if (!recorder.isRecording()) {
new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
recorder.startRecording(cameraPanel, outputFile);
return null;
}
@Override
protected void done() {
try { get(); if (onStart != null) onStart.run(); }
catch (Exception e) { if (onError != null) onError.accept(e); }
}
}.execute();
} else {
new SwingWorker<File, Void>() {
@Override
protected File doInBackground() throws Exception {
return recorder.stopRecording();
}
@Override
protected void done() {
try { File f = get(); if (onStop != null) onStop.accept(f); }
catch (Exception e) { if (onError != null) onError.accept(e); }
}
}.execute();
}
}
}

View File

@@ -6,109 +6,82 @@ import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.common.model.Rational;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
/*
*
* This is not that interesting but surely improtant for any security based people,
* here we record using jcodec, which is more efficient than me writing my own recorder,
* which i am certainly not doing.
*
* Anyways we dont do shit properly anyway.
*
* */
import java.util.concurrent.*;
public class VideoRecorder {
private boolean recording = false;
private Timer captureTimer;
private volatile boolean recording = false;
private File outputFile;
private AWTSequenceEncoder encoder;
private SeekableByteChannel channel;
private static final int FPS = 30;
private LinkedBlockingQueue<BufferedImage> frameQueue;
private ExecutorService workerThread;
public void startRecording(CameraPanel panel, File output) throws IOException {
this.outputFile = output;
this.channel = NIOUtils.writableFileChannel(output.getAbsolutePath());
this.encoder = new AWTSequenceEncoder(channel, Rational.R(FPS, 1));
this.frameQueue = new LinkedBlockingQueue<>(60);
this.recording = true;
channel = NIOUtils.writableFileChannel(String.valueOf(outputFile));
encoder = new AWTSequenceEncoder(channel, Rational.R(FPS, 1));
workerThread = Executors.newSingleThreadExecutor();
workerThread.submit(this::processQueue);
captureTimer = new Timer(1000 / FPS, e -> {
try {
BufferedImage img = panel.getCurrentProcessedImage();
if (img != null) {
BufferedImage rgbImage = convertToRGB(img);
encoder.encodeImage(rgbImage);
}
} catch (IOException ex) {
JOptionPane.showMessageDialog(
null,
"IOException\n" + ex,
"IOException in Recorder",
JOptionPane.ERROR_MESSAGE
);
new Thread(() -> {
while (recording) {
try {
stopRecording();
} catch (IOException stopEx) {
JOptionPane.showMessageDialog(
null,
"IOException@stopEx\n" + stopEx,
"IOException in Recorder@stopEx",
JOptionPane.ERROR_MESSAGE
);
BufferedImage img = panel.getCurrentProcessedImage();
if (img != null) {
BufferedImage copy = snapshotToRGB(img);
frameQueue.offer(copy);
}
Thread.sleep(1000 / FPS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
captureTimer.start();
}).start();
}
private void processQueue() {
while (recording || !frameQueue.isEmpty()) {
try {
BufferedImage img = frameQueue.poll(500, TimeUnit.MILLISECONDS);
if (img != null) {
encoder.encodeImage(img);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private BufferedImage snapshotToRGB(BufferedImage source) {
BufferedImage rgb = new BufferedImage(source.getWidth(), source.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D g = rgb.createGraphics();
g.drawImage(source, 0, 0, null);
g.dispose();
return rgb;
}
public File stopRecording() throws IOException {
recording = false;
/* some helper methods, i swear its always the same? */
if (captureTimer != null) {
captureTimer.stop();
workerThread.shutdown();
try {
workerThread.awaitTermination(5, TimeUnit.SECONDS);
if (encoder != null) encoder.finish();
if (channel != null) channel.close();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
if (encoder != null) {
encoder.finish();
}
if (channel != null) {
channel.close();
}
return outputFile;
}
public boolean isRecording() {
return recording;
}
// maybe i should move this to a component, some useless conversion right here,
// (performance wise)
// yes, capitain?
private BufferedImage convertToRGB(BufferedImage source) {
if (source.getType() == BufferedImage.TYPE_INT_RGB) {
return source;
}
BufferedImage rgb = new BufferedImage(
source.getWidth(),
source.getHeight(),
BufferedImage.TYPE_INT_RGB
);
for (int y = 0; y < source.getHeight(); y++) {
for (int x = 0; x < source.getWidth(); x++) {
rgb.setRGB(x, y, source.getRGB(x, y) & 0xFFFFFF);
}
}
return rgb;
}
public boolean isRecording() { return recording; }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -1,4 +1,4 @@
import io.swtc.proccessing.AutoGainProcessor;
import io.swtc.proccessing.AWBProccessor;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -13,13 +13,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
* of displaying stuff.
* */
class AutoGainProcessorTest {
class AWBProccessorTest {
private AutoGainProcessor processor;
private AWBProccessor processor;
@BeforeEach
void setUp() {
processor = new AutoGainProcessor();
processor = new AWBProccessor();
}
@Test

View File

@@ -1,98 +0,0 @@
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");
}
}