10 Commits

Author SHA1 Message Date
d775a33107 some more performance update:
Changed:
+ FFmpeg with JavaCV
+ Exporting to a USB

Removed:
- JCodec!
2026-01-29 16:40:36 +01:00
c0aa3421a4 some UI Updates for Picking a custom Background color, and having Multiple Monitor Fullscreen! 2026-01-25 19:39:03 +01:00
98ff3b9b76 zooming in CameraPanel.java with new methods and more efficient rendering! 2026-01-23 19:18:37 +01:00
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
41fbf62757 just some theme changes, i like this one more 2026-01-18 22:10:37 +01:00
27 changed files with 1576 additions and 660 deletions

5
.gitignore vendored
View File

@@ -45,4 +45,7 @@ dependency-reduced-pom.xml
.idea .idea
## This is for our app, cause it likes to store stuff ## ## This is for our app, cause it likes to store stuff ##
network_cameras.json network_cameras.json
## exec launch4j config ##
execfg.xml

128
pom.xml
View File

@@ -7,7 +7,7 @@
<groupId>io.swtc</groupId> <groupId>io.swtc</groupId>
<artifactId>swtc</artifactId> <artifactId>swtc</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
<!--
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
@@ -42,6 +42,43 @@
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
-->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>io.swtc.Main</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties> <properties>
<maven.compiler.source>17</maven.compiler.source> <maven.compiler.source>17</maven.compiler.source>
@@ -63,30 +100,30 @@
<version>0.3.12</version> <version>0.3.12</version>
</dependency> </dependency>
<!-- for gl we use lwjgl --> <!-- &lt;!&ndash; for gl we use lwjgl &ndash;&gt;-->
<dependency> <!-- <dependency>-->
<groupId>org.lwjgl</groupId> <!-- <groupId>org.lwjgl</groupId>-->
<artifactId>lwjgl</artifactId> <!-- <artifactId>lwjgl</artifactId>-->
<version>3.3.3</version> <!-- <version>3.3.3</version>-->
</dependency> <!-- </dependency>-->
<dependency> <!-- <dependency>-->
<groupId>org.lwjgl</groupId> <!-- <groupId>org.lwjgl</groupId>-->
<artifactId>lwjgl-opengl</artifactId> <!-- <artifactId>lwjgl-opengl</artifactId>-->
<version>3.3.3</version> <!-- <version>3.3.3</version>-->
</dependency> <!-- </dependency>-->
<dependency> <!-- <dependency>-->
<groupId>org.lwjgl</groupId> <!-- <groupId>org.lwjgl</groupId>-->
<artifactId>lwjgl</artifactId> <!-- <artifactId>lwjgl</artifactId>-->
<version>3.3.3</version> <!-- <version>3.3.3</version>-->
<classifier>natives-windows</classifier> <!-- <classifier>natives-windows</classifier>-->
</dependency> <!-- </dependency>-->
<dependency> <!-- <dependency>-->
<groupId>org.lwjgl</groupId> <!-- <groupId>org.lwjgl</groupId>-->
<artifactId>lwjgl-opengl</artifactId> <!-- <artifactId>lwjgl-opengl</artifactId>-->
<version>3.3.3</version> <!-- <version>3.3.3</version>-->
<classifier>natives-windows</classifier> <!-- <classifier>natives-windows</classifier>-->
</dependency> <!-- </dependency>-->
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api --> <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency> <dependency>
@@ -106,22 +143,35 @@
<version>2.20.1</version> <version>2.20.1</version>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.jcodec/jcodec --> <!-- &lt;!&ndash; https://mvnrepository.com/artifact/org.jcodec/jcodec &ndash;&gt;-->
<!-- <!-- &lt;!&ndash;-->
Saving into Files <!-- Saving into Files-->
--> <!-- &ndash;&gt;-->
<dependency> <!-- <dependency>-->
<groupId>org.jcodec</groupId> <!-- <groupId>org.jcodec</groupId>-->
<artifactId>jcodec</artifactId> <!-- <artifactId>jcodec</artifactId>-->
<version>0.2.5</version> <!-- <version>0.2.5</version>-->
</dependency> <!-- </dependency>-->
<!-- https://mvnrepository.com/artifact/org.jcodec/jcodec-javase --> <!-- &lt;!&ndash; https://mvnrepository.com/artifact/org.jcodec/jcodec-javase &ndash;&gt;-->
<dependency> <!-- <dependency>-->
<groupId>org.jcodec</groupId> <!-- <groupId>org.jcodec</groupId>-->
<artifactId>jcodec-javase</artifactId> <!-- <artifactId>jcodec-javase</artifactId>-->
<version>0.2.5</version> <!-- <version>0.2.5</version>-->
</dependency> <!-- </dependency>-->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>1.5.10</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>6.1.1-1.5.10</version>
<classifier>windows-x86_64</classifier>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -1,26 +1,21 @@
package io.swtc; package io.swtc;
import javax.swing.*; import javax.swing.*;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.ShowError;
public class Main { public class Main {
public static void main(String[] args) { public static void main(String[] args) {
// for (int i = 0; i < args.length; i++) {
// System.out.println("Arg " + i + ": " + args[i]);
// }
try { try {
UIManager.setLookAndFeel( UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
"com.sun.java.swing.plaf.windows.WindowsLookAndFeel" } catch (Exception e) { /* Do nothing */ }
);
} catch (Exception e) {
JOptionPane.showMessageDialog(
null,
"LaF not available",
"LaF-Warning",
JOptionPane.WARNING_MESSAGE
);
}
// For some reason we need to invoke Later for LaF to work! SwingCCTVManager.main(null);
SwingUtilities.invokeLater(() -> SwingCCTVManager.main(null));
} }
} }

View File

@@ -2,25 +2,37 @@ package io.swtc;
import com.github.sarxos.webcam.Webcam; import com.github.sarxos.webcam.Webcam;
import com.github.sarxos.webcam.WebcamCompositeDriver; 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.buildin.WebcamDefaultDriver;
import com.github.sarxos.webcam.ds.ipcam.IpCamDeviceRegistry; import com.github.sarxos.webcam.ds.ipcam.*;
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.CameraConfig;
import io.swtc.networking.CameraSettings; import io.swtc.networking.CameraSettings;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.ShowError;
import javax.swing.*; import javax.swing.*;
import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel; import javax.swing.table.DefaultTableModel;
import java.awt.*; import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.io.File;
import java.net.InetSocketAddress;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URL;
import java.util.List; import java.util.List;
public class SwingCCTVManager { 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 { static {
System.setProperty("ipcam.connection.timeout", TIMEOUT_MS);
Webcam.setDriver(new WebcamCompositeDriver() {{ Webcam.setDriver(new WebcamCompositeDriver() {{
add(new WebcamDefaultDriver()); add(new WebcamDefaultDriver());
add(new IpCamDriver()); add(new IpCamDriver());
@@ -32,206 +44,238 @@ public class SwingCCTVManager {
private final JTable deviceTable; private final JTable deviceTable;
private final DefaultTableModel tableModel; private final DefaultTableModel tableModel;
private final SwingIFrame IFrame; private final SwingIFrame IFrame;
private boolean isRefreshing = false;
public SwingCCTVManager() { public SwingCCTVManager() {
frame = new JFrame("dashboard"); frame = new JFrame("Dashboard");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(1000, 600); frame.setSize(1100, 600);
frame.setIconImage(IconSetter.getIcon());
this.IFrame = new SwingIFrame(); this.IFrame = new SwingIFrame();
this.IFrame.show(); this.IFrame.show();
String[] columns = {"Status", "Device Name", "Type", "Resolution", "Address"}; tableModel = new DefaultTableModel(new String[]{"Status", "Device Name", "Type", "Resolution", "Address"}, 0) {
tableModel = new DefaultTableModel(columns, 0) {
@Override @Override
public boolean isCellEditable(int r, int c) { public boolean isCellEditable(int r, int c) { return false; }
return false;
}
}; };
deviceTable = new JTable(tableModel); deviceTable = new JTable(tableModel);
deviceTable.setRowSelectionAllowed(true);
deviceTable.setFocusable(true);
deviceTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
setupTableAppearance(); setupTableAppearance();
setupDragAndDrop(); // Initialize DND
deviceTable.addMouseListener(new MouseAdapter() { setupListeners();
@Override
public void mousePressed(MouseEvent e) {
deviceTable.requestFocusInWindow();
if (e.getClickCount() == 2 && deviceTable.getSelectedRow() != -1) {
launchSelected();
}
if (SwingUtilities.isRightMouseButton(e)) {
showContextMenu(e);
}
}
});
JToolBar toolBar = new JToolBar(); JToolBar toolBar = new JToolBar();
JButton btnAdd = new JButton("Add IP Cam"); setupBrandedToolbar(toolBar);
JButton btnLaunch = new JButton("Launch Stream");
toolBar.add(btnAdd);
toolBar.addSeparator();
toolBar.add(btnLaunch);
frame.add(toolBar, BorderLayout.NORTH); frame.add(toolBar, BorderLayout.SOUTH);
frame.add(new JScrollPane(deviceTable), BorderLayout.CENTER); frame.add(new JScrollPane(deviceTable), BorderLayout.CENTER);
btnAdd.addActionListener(e -> showAddCameraDialog());
btnLaunch.addActionListener(e -> launchSelected());
startAutoRefresh(); startAutoRefresh();
refreshTable(); refreshTable();
} }
private void setupTableAppearance() { private void setupBrandedToolbar(JToolBar toolBar) {
deviceTable.getColumnModel().getColumn(0).setMaxWidth(80); Image ogIcon = IconSetter.getIcon();
deviceTable.setRowHeight(30); if (ogIcon != null) {
Image scaledIcon = ogIcon.getScaledInstance(32, 32, Image.SCALE_SMOOTH);
deviceTable.getColumnModel().getColumn(0) toolBar.add(new JLabel(new ImageIcon(scaledIcon)));
.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"
});
} }
for (int r : selectedRows) { toolBar.add(new JLabel("SWT-CCTV"));
if (r < tableModel.getRowCount()) { toolBar.add(Box.createRigidArea(new Dimension(10, 0)));
deviceTable.addRowSelectionInterval(r, r); 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) { private void importCameras(List<CameraConfig> configs) {
int row = deviceTable.rowAtPoint(e.getPoint()); if (configs.isEmpty()) {
if (row >= 0) { ShowError.warning(frame, "Import Empty", "No valid camera configurations found in file.");
deviceTable.setRowSelectionInterval(row, row); 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(); private void setupTableAppearance() {
JMenuItem launch = new JMenuItem("Launch Live Stream"); deviceTable.setRowHeight(30);
JMenuItem delete = new JMenuItem("Remove Device"); 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()); private synchronized void refreshTable() {
delete.addActionListener(al -> deleteSelected()); 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); private Object[] probeIpCamera(IpCamDevice ipDevice) {
menu.addSeparator(); String status = STATUS_OFFLINE; String resText = "N/A";
menu.add(delete); try {
menu.show(deviceTable, e.getX(), e.getY()); 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() { private void launchSelected() {
int row = deviceTable.getSelectedRow(); int row = deviceTable.getSelectedRow();
if (row == -1) return; if (row == -1) return;
String name = (String) tableModel.getValueAt(row, 1); String name = (String) tableModel.getValueAt(row, 1);
Webcam.getWebcams().stream() if (STATUS_OFFLINE.equals(tableModel.getValueAt(row, 0))) {
.filter(w -> w.getName().equals(name)) ShowError.warning(frame, "Device Offline", "Cannot connect to '" + name + "'.");
.findFirst() return;
.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
);
}
} }
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() { private void showAddCameraDialog() {
JPanel p = new JPanel(new GridLayout(2, 2, 5, 5)); JPanel p = new JPanel(new GridLayout(2, 2, 5, 5));
JTextField n = new JTextField(); JTextField n = new JTextField(); JTextField u = 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) {
p.add(new JLabel("Name:")); importCameras(List.of(new CameraConfig(n.getText(), u.getText())));
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() { private void setupListeners() {
frame.setLocationRelativeTo(null); deviceTable.addMouseListener(new MouseAdapter() {
frame.setVisible(true); @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) { public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new SwingCCTVManager().open()); SwingUtilities.invokeLater(() -> new SwingCCTVManager().open());
} }
} }

View File

@@ -1,25 +1,46 @@
package io.swtc; package io.swtc;
import com.github.sarxos.webcam.Webcam; import com.github.sarxos.webcam.Webcam;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.iframe.*; import io.swtc.proccessing.ui.iframe.*;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects;
public class SwingIFrame { public class SwingIFrame {
private final JFrame mainFrame; private final JFrame mainFrame;
private final DesktopPane desktopPane; private final DesktopPane desktopPane;
private final Map<JInternalFrame, EffectsPanelFrame> cameraToEffects = new HashMap<>(); 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() { public SwingIFrame() {
mainFrame = new JFrame("Viewer"); mainFrame = new JFrame("Viewer");
mainFrame.setSize(1280, 720); mainFrame.setSize(1280, 720);
mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
mainFrame.setIconImage(IconSetter.getIcon());
desktopPane = new DesktopPane(cameraToEffects); desktopPane = new DesktopPane(cameraToEffects);
desktopPane.setBackground(Color.WHITE); desktopPane.setBackground(defDesktopBg);
mainFrame.add(desktopPane, BorderLayout.CENTER); mainFrame.add(desktopPane, BorderLayout.CENTER);
setupFullscreenToggle();
setupBlackBg();
initPopupMenu();
desktopPane.addMouseListener(popupListener());
} }
public void addCameraInternalFrame(Webcam webcam) { public void addCameraInternalFrame(Webcam webcam) {
@@ -43,11 +64,20 @@ public class SwingIFrame {
EffectsPanelFrame ef = cameraToEffects.remove(cameraFrame); EffectsPanelFrame ef = cameraToEffects.remove(cameraFrame);
if (ef != null) ef.dispose(); if (ef != null) ef.dispose();
desktopPane.forgetFrame(cameraFrame); desktopPane.forgetFrame(cameraFrame);
cameraFrame.dispose();
} }
}); });
desktopPane.add(cameraFrame); desktopPane.add(cameraFrame);
desktopPane.add(effectsFrame); desktopPane.add(effectsFrame);
// Attach popup menu to frames and content
MouseAdapter popup = popupListener();
cameraFrame.addMouseListener(popup);
cameraFrame.getContentPane().addMouseListener(popup);
effectsFrame.addMouseListener(popup);
effectsFrame.getContentPane().addMouseListener(popup);
cameraFrame.setVisible(true); cameraFrame.setVisible(true);
} }
@@ -58,12 +88,153 @@ public class SwingIFrame {
try { try {
effectsFrame.setSelected(true); effectsFrame.setSelected(true);
effectsFrame.toFront(); effectsFrame.toFront();
} catch (java.beans.PropertyVetoException ex) { JOptionPane.showMessageDialog(null,"Exception" + ex.getMessage() , "Exception", JOptionPane.ERROR_MESSAGE); } } 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");
JCheckBoxMenuItem mmfullscreenItem = new JCheckBoxMenuItem("Multi Monitor Fullscreen");
JCheckBoxMenuItem backgroundcolor = new JCheckBoxMenuItem("Calmer Background");
JMenuItem colorpicker = new JMenuItem("Set background color");
fullscreenItem.addActionListener(e -> toggleFullscreen());
mmfullscreenItem.addActionListener(e -> toggleMMFullscreen());
backgroundcolor.addActionListener(e -> toggleBackground());
colorpicker.addActionListener(e -> chooseBgColor());
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);
popupMenu.add(mmfullscreenItem);
popupMenu.add(backgroundcolor);
popupMenu.add(colorpicker);
}
private void chooseBgColor() {
Color selected = JColorChooser.showDialog(
mainFrame,
"Select Background Color",
desktopPane.getBackground()
);
if (!Objects.isNull(selected)) {
desktopPane.setBackground(selected);
desktopPane.repaint();
}
}
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();
// One Monitor FS
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();
}
});
// Multi Monitor FS
root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke("F12"), "toggleMMFullscreen");
root.getActionMap().put("toggleMMFullscreen", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleMMFullscreen();
}
});
}
private void toggleBackground() {
desktopPane.setBackground(blackbg ? defDesktopBg : bgcolor);
blackbg = !blackbg;
desktopPane.repaint();
}
private void toggleMMFullscreen() {
if (!fullscreen) {
windowedBounds = mainFrame.getBounds();
Rectangle virtualBounds = new Rectangle();
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice[] gs = ge.getScreenDevices();
for (GraphicsDevice gd : gs) {
GraphicsConfiguration[] gc = gd.getConfigurations();
for (GraphicsConfiguration configuration : gc) {
virtualBounds = virtualBounds.union(configuration.getBounds());
}
}
mainFrame.dispose();
mainFrame.setUndecorated(true);
mainFrame.setBounds(virtualBounds);
mainFrame.setVisible(true);
fullscreen = true;
} else {
toggleFullscreen();
}
}
/** 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() { public void show() {
mainFrame.setLocationRelativeTo(null); mainFrame.setLocationRelativeTo(null);
mainFrame.setVisible(true); mainFrame.setVisible(true);
} }
} }

View File

@@ -5,6 +5,7 @@ public class CameraConfig {
public String url; public String url;
// Default constructor for Jackson // Default constructor for Jackson
public CameraConfig() {}
public CameraConfig(String name, String url) { public CameraConfig(String name, String url) {
this.name = name; this.name = name;

View File

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

View File

@@ -1,93 +1,99 @@
package io.swtc.proccessing; package io.swtc.proccessing;
import io.swtc.proccessing.ui.ShowError;
import io.swtc.recording.cv.AVRecorder;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function; import java.util.function.Function;
/**
* Enhanced CameraPanel with support for custom image processors
*/
public class CameraPanel extends JPanel { public class CameraPanel extends JPanel {
private BufferedImage currentImage;
private BufferedImage processedImage;
// Custom processor for advanced effects private volatile BufferedImage sourceImage;
private volatile BufferedImage processedImage;
private Function<BufferedImage, BufferedImage> imageProcessor; private Function<BufferedImage, BufferedImage> imageProcessor;
private volatile AVRecorder recorder; // this is now javacv, jcodec is stoopid
private final AtomicBoolean repaintScheduled = new AtomicBoolean(false);
private double zoomLevel = 1.0;
private double xOffset = 0;
private double yOffset = 0;
private Point dragStartPoint;
private static final double MIN_ZOOM = 1.0;
private static final double MAX_ZOOM = 10.0;
private static final double ZOOM_MULTIPLIER = 1.1;
private final GraphicsConfiguration graphicsConfig;
public CameraPanel() { public CameraPanel() {
setBackground(Color.BLACK); setBackground(Color.BLACK);
setPreferredSize(new Dimension(640, 480)); setPreferredSize(new Dimension(640, 480));
graphicsConfig = GraphicsEnvironment.getLocalGraphicsEnvironment()
.getDefaultScreenDevice().getDefaultConfiguration();
initInteractionListeners();
}
private void initInteractionListeners() {
MouseAdapter mouseHandler = new MouseAdapter() {
@Override
public void mouseWheelMoved(MouseWheelEvent e) { handleZoom(e); }
@Override
public void mousePressed(MouseEvent e) {
dragStartPoint = e.getPoint();
setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
}
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) resetView();
}
@Override
public void mouseReleased(MouseEvent e) { setCursor(Cursor.getDefaultCursor()); }
@Override
public void mouseDragged(MouseEvent e) { handlePan(e); }
@Override
public void mouseExited(MouseEvent e) { setCursor(Cursor.getDefaultCursor()); }
};
addMouseWheelListener(mouseHandler);
addMouseListener(mouseHandler);
addMouseMotionListener(mouseHandler);
}
public void setExternalRecorder(AVRecorder recorder) {
this.recorder = recorder;
} }
public void setImage(BufferedImage img) { public void setImage(BufferedImage img) {
this.currentImage = img; this.sourceImage = img;
processImage(); updateProcessedImage();
repaint();
// Feed the AVRecorder using its 'accept' method if active
AVRecorder currentRecorder = this.recorder;
if (currentRecorder != null && currentRecorder.isRecording() && processedImage != null) {
currentRecorder.accept(processedImage);
}
scheduleRepaint();
} }
public void setImageProcessor(Function<BufferedImage, BufferedImage> processor) { public void setImageProcessor(Function<BufferedImage, BufferedImage> processor) {
this.imageProcessor = processor; this.imageProcessor = processor;
processImage(); if (sourceImage != null) {
repaint(); updateProcessedImage();
} scheduleRepaint();
private void processImage() {
if (currentImage == null) {
processedImage = null;
return;
}
BufferedImage result = currentImage;
// Apply basic transforms first
result = applyBasicEffects(result);
// Apply custom processor if set
if (imageProcessor != null) {
result = imageProcessor.apply(result);
}
processedImage = result;
}
private BufferedImage applyBasicEffects(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++) {
// Handle mirror/flip/rotate
int srcX = x;
int srcY = y;
// Ensure coordinates are in bounds
srcX = Math.max(0, Math.min(width - 1, srcX));
srcY = Math.max(0, Math.min(height - 1, srcY));
int rgb = img.getRGB(srcX, srcY);
int r = (rgb >> 16) & 0xFF;
int g = (rgb >> 8) & 0xFF;
int b = rgb & 0xFF;
result.setRGB(x, y, (r << 16) | (g << 8) | b);
}
}
return result;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (processedImage != null) {
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.drawImage(processedImage, 0, 0, getWidth(), getHeight(), null);
} }
} }
@@ -95,4 +101,126 @@ public class CameraPanel extends JPanel {
return processedImage; return processedImage;
} }
public void resetView() {
zoomLevel = 1.0;
xOffset = 0;
yOffset = 0;
repaint();
}
private void updateProcessedImage() {
if (sourceImage == null) return;
BufferedImage temp;
if (imageProcessor != null) {
try {
temp = imageProcessor.apply(sourceImage);
} catch (Exception e) {
ShowError.error(null, "Error in image processing: \n" + Arrays.toString(e.getStackTrace()), "Processing Error");
temp = sourceImage;
}
} else {
temp = sourceImage;
}
if (processedImage == null ||
processedImage.getWidth() != temp.getWidth() ||
processedImage.getHeight() != temp.getHeight()) {
processedImage = graphicsConfig.createCompatibleImage(
temp.getWidth(),
temp.getHeight(),
temp.getTransparency()
);
}
Graphics2D g2d = processedImage.createGraphics();
g2d.drawImage(temp, 0, 0, null);
g2d.dispose();
}
private void scheduleRepaint() {
if (repaintScheduled.compareAndSet(false, true)) {
SwingUtilities.invokeLater(() -> {
repaintScheduled.set(false);
repaint();
});
}
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (processedImage == null) return;
Graphics2D g2d = (Graphics2D) g.create();
try {
g2d.setRenderingHint(
RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR
);
g2d.translate((int)xOffset, (int)yOffset);
double scaleX = (double) getWidth() / processedImage.getWidth();
double scaleY = (double) getHeight() / processedImage.getHeight();
g2d.scale(scaleX * zoomLevel, scaleY * zoomLevel);
g2d.drawImage(processedImage, 0, 0, null);
} finally {
g2d.dispose();
}
}
private void handleZoom(MouseWheelEvent e) {
if (processedImage == null) return;
double oldZoom = zoomLevel;
if (e.getWheelRotation() < 0) {
zoomLevel *= ZOOM_MULTIPLIER;
} else {
zoomLevel /= ZOOM_MULTIPLIER;
}
zoomLevel = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoomLevel));
if (oldZoom != zoomLevel) {
double xRel = e.getX() - xOffset;
double yRel = e.getY() - yOffset;
double zoomFactor = zoomLevel / oldZoom;
xOffset = e.getX() - (xRel * zoomFactor);
yOffset = e.getY() - (yRel * zoomFactor);
checkBounds();
repaint();
}
}
private void handlePan(MouseEvent e) {
if (processedImage == null || dragStartPoint == null) return;
xOffset += e.getX() - dragStartPoint.x;
yOffset += e.getY() - dragStartPoint.y;
dragStartPoint = e.getPoint();
checkBounds();
repaint();
}
private void checkBounds() {
double viewedWidth = getWidth() * zoomLevel;
double viewedHeight = getHeight() * zoomLevel;
if (xOffset > 0) xOffset = 0;
if (yOffset > 0) yOffset = 0;
if (xOffset + viewedWidth < getWidth()) {
xOffset = getWidth() - viewedWidth;
}
if (yOffset + viewedHeight < getHeight()) {
yOffset = getHeight() - viewedHeight;
}
}
} }

View File

@@ -2,78 +2,84 @@ package io.swtc.proccessing;
import com.github.sarxos.webcam.Webcam; import com.github.sarxos.webcam.Webcam;
import com.github.sarxos.webcam.WebcamException; import com.github.sarxos.webcam.WebcamException;
import com.github.sarxos.webcam.WebcamResolution;
import java.awt.Dimension; import java.awt.Dimension;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
import java.util.function.Consumer; import java.util.function.Consumer;
import javax.swing.*; import javax.swing.*;
import io.swtc.proccessing.ui.ShowError;
public class WebcamCaptureLoop { public class WebcamCaptureLoop {
private final Webcam webcam; private final Webcam webcam;
private final Consumer<BufferedImage> onFrameCaptured; private final Consumer<BufferedImage> onFrameCaptured;
private volatile boolean running = false; private volatile boolean running = false;
private final AtomicBoolean cleanedUp = new AtomicBoolean(false); private final AtomicBoolean cleanedUp = new AtomicBoolean(false);
private final int targetframes = 20;
public WebcamCaptureLoop(Webcam webcam, Consumer<BufferedImage> onFrameCaptured) { public WebcamCaptureLoop(Webcam webcam, Consumer<BufferedImage> onFrameCaptured) {
this.webcam = webcam; this.webcam = webcam;
this.onFrameCaptured = onFrameCaptured; this.onFrameCaptured = onFrameCaptured;
// this is some of the most stupid shit ive seen in years, this is needed for opening the stream. Dimension vga = WebcamResolution.VGA.getSize();
// 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. if (isSizeSupported(webcam, vga)) { // we dont do stupid shit anymore! cause we are smarter than before...
// so this freaks out and doesnt want to cooperate. webcam.setViewSize(vga);
if (!webcam.getName().toLowerCase().contains("ipcam")) { } else {
Dimension[] sizes = webcam.getViewSizes(); Dimension[] dimensions = webcam.getViewSizes();
webcam.setViewSize(sizes[sizes.length - 1]); 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() { public void start() {
if (running) return; if (running) return;
running = true; running = true;
Thread captureThread = new Thread(() -> { Thread captureThread = new Thread(() -> {
// this is where we open it. if the res isnt known then it fucks up.
try { try {
webcam.open(); if (!webcam.isOpen()) webcam.open(); // open if not
} catch (WebcamException e) {
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, null,
"WebcamException" + e.getMessage(), "Exception" + e,
"WebcamException", "Exception"
JOptionPane.ERROR_MESSAGE
); );
} finally { } finally {
webcam.open(); cleanup();
} }
}, "cam_cap_thread");
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();
} catch (IllegalStateException e) {
// show a error message from javax.swing.awt.JOptionPane
JOptionPane.showMessageDialog(
null,
"IllegalStateException@"+e,
"IllegalStateException",
JOptionPane.ERROR_MESSAGE
);
}
});
captureThread.setName("cam_cap_thread");
captureThread.start(); captureThread.start();
} }
@@ -113,6 +119,5 @@ public class WebcamCaptureLoop {
*/ */
public void stop() { public void stop() {
running = false; running = false;
cleanup();
} }
} }

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,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

@@ -3,31 +3,25 @@ package io.swtc.proccessing.ui.iframe;
import com.github.sarxos.webcam.Webcam; import com.github.sarxos.webcam.Webcam;
import io.swtc.proccessing.WebcamCaptureLoop; import io.swtc.proccessing.WebcamCaptureLoop;
import io.swtc.proccessing.CameraPanel; import io.swtc.proccessing.CameraPanel;
import io.swtc.recording.VideoRecorder; import io.swtc.proccessing.ui.IconSetter;
import javax.imageio.ImageIO; import io.swtc.proccessing.ui.ShowError;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*; import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.function.Consumer; import java.util.function.Consumer;
public class CameraInternalFrame extends JInternalFrame { public class CameraInternalFrame extends JInternalFrame {
private final WebcamCaptureLoop captureLoop; private final WebcamCaptureLoop captureLoop;
private final CameraPanel cameraPanel; private final CameraPanel cameraPanel;
private final VideoRecorder videoRecorder; // Instance of the recorder
private JButton recordBtn;
public CameraInternalFrame(Webcam webcam, Consumer<CameraInternalFrame> onOpenEffects) { public CameraInternalFrame(Webcam webcam, Consumer<CameraInternalFrame> onOpenEffects) {
super(webcam.getName(), true, true, true, true); super(webcam.getName(), true, true, true, true);
this.cameraPanel = new CameraPanel();
this.videoRecorder = new VideoRecorder(); // Initialize recorder
// Initialize capture loop Image ico = IconSetter.getIcon();
this.captureLoop = new WebcamCaptureLoop(webcam, img -> setFrameIcon(new ImageIcon(ico));
SwingUtilities.invokeLater(() -> cameraPanel.setImage(img))
); this.cameraPanel = new CameraPanel();
this.captureLoop = new WebcamCaptureLoop(webcam, cameraPanel::setImage);
setupUI(onOpenEffects); setupUI(onOpenEffects);
captureLoop.start(); captureLoop.start();
@@ -36,11 +30,15 @@ public class CameraInternalFrame extends JInternalFrame {
private void setupUI(Consumer<CameraInternalFrame> onOpenEffects) { private void setupUI(Consumer<CameraInternalFrame> onOpenEffects) {
JTabbedPane tabbedPane = new JTabbedPane(); JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab("View", cameraPanel); tabbedPane.addTab("View", cameraPanel);
tabbedPane.addTab("Capture", new RecordingPane(cameraPanel, videoRecorder)); tabbedPane.addTab("Capture", new JPanel());
tabbedPane.addTab("Effects", new JPanel()); tabbedPane.addTab("Effects", new JPanel());
tabbedPane.addChangeListener(e -> { tabbedPane.addChangeListener(e -> {
if (tabbedPane.getSelectedIndex() == 2) { int index = tabbedPane.getSelectedIndex();
if (index == 1) {
tabbedPane.setSelectedIndex(0);
openRecordingFrame();
} else if (index == 2) {
tabbedPane.setSelectedIndex(0); tabbedPane.setSelectedIndex(0);
onOpenEffects.accept(this); onOpenEffects.accept(this);
} }
@@ -50,82 +48,24 @@ public class CameraInternalFrame extends JInternalFrame {
setSize(600, 500); setSize(600, 500);
} }
private JPanel createCapturePanel() { private void openRecordingFrame() {
JPanel panel = new JPanel(new GridLayout(2, 1, 10, 10)); // Changed to Grid for better button layout RecordingFrame rf = new RecordingFrame(this.getTitle(), cameraPanel);
panel.setBorder(new EmptyBorder(15, 15, 15, 15)); JDesktopPane desktopPane = getDesktopPane();
if (desktopPane != null) {
JButton screenshotBtn = new JButton("Take Screenshot"); desktopPane.add(rf);
screenshotBtn.addActionListener(e -> saveSnapshot()); rf.setVisible(true);
recordBtn = new JButton("Start Recording");
recordBtn.addActionListener(e -> toggleRecording());
panel.add(screenshotBtn);
panel.add(recordBtn);
// Wrap in a wrapper to prevent buttons from stretching too much
JPanel wrapper = new JPanel(new BorderLayout());
wrapper.add(panel, BorderLayout.NORTH);
return wrapper;
}
private void toggleRecording() {
if (!videoRecorder.isRecording()) {
startVideo();
} else {
stopVideo();
}
}
private void startVideo() {
try {
File file = new File(System.getProperty("user.home"), "vid_" + System.currentTimeMillis() + ".mp4");
videoRecorder.startRecording(cameraPanel, file);
recordBtn.setText("Stop Recording");
recordBtn.setForeground(Color.RED);
} catch (IOException ex) {
showError("Failed to start recording", ex);
}
}
private void stopVideo() {
try {
File savedFile = videoRecorder.stopRecording();
recordBtn.setText("Start Recording");
recordBtn.setForeground(Color.BLACK);
JOptionPane.showMessageDialog(this, "Video saved to: " + savedFile.getAbsolutePath());
} catch (IOException ex) {
showError("Failed to stop recording safely", ex);
}
}
private void saveSnapshot() {
BufferedImage img = cameraPanel.getCurrentProcessedImage();
if (img != null) {
try { try {
File file = new File(System.getProperty("user.home"), "snap_" + System.currentTimeMillis() + ".png"); rf.setSelected(true);
ImageIO.write(img, "PNG", file); } catch (java.beans.PropertyVetoException veto) {
} catch (Exception ex) { ShowError.error(null, "Focus Error: " + veto.getMessage(), "Error");
showError("Snapshot failed", ex);
} }
} }
} }
private void showError(String title, Exception ex) {
JOptionPane.showMessageDialog(this, title + "\n" + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
public CameraPanel getCameraPanel() { return cameraPanel; } public CameraPanel getCameraPanel() { return cameraPanel; }
@Override @Override
public void dispose() { public void dispose() {
// Safety check: stop recording if the window is closed
if (videoRecorder.isRecording()) {
try { videoRecorder.stopRecording(); } catch (IOException ignored) {}
}
captureLoop.stop(); captureLoop.stop();
super.dispose(); super.dispose();
} }

View File

@@ -56,11 +56,11 @@ public class DesktopPane extends JDesktopPane {
int ctrlOffset = Math.min(Math.abs(x2 - x1) / 2, 150); 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); 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.setStroke(new BasicStroke(3f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g2d.draw(curve); //g2d.draw(curve);
// Terminals // Terminals
g2d.fillOval(x1 - 5, y1 - 5, 10, 10); //g2d.fillOval(x1 - 5, y1 - 5, 10, 10);
g2d.fillOval(x2 - 5, y2 - 5, 10, 10); //g2d.fillOval(x2 - 5, y2 - 5, 10, 10);
} }
} }

View File

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

View File

@@ -0,0 +1,286 @@
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.proccessing.ui.sections.recording.ExportSection;
import io.swtc.recording.cv.AVRecorder;
import io.swtc.recording.cv.RecorderConfig;
import io.swtc.recording.evidence.USBExportManager;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import java.nio.file.Path;
public class RecordingFrame extends JInternalFrame {
private AVRecorder avRecorder;
private ExportSection exportSection;
private final CameraPanel cameraPanel;
private JButton recordBtn;
private JLabel statusLabel;
private JLabel statsLabel;
private File outputDirectory;
private File currentFile;
private final Timer statsTimer;
private long startTime;
private final StringBuilder sb = new StringBuilder(32);
public RecordingFrame(String cameraName, CameraPanel cameraPanel) {
super(cameraName + " Capture", true, true, false, true);
setupDirectory();
Image ico = IconSetter.getIcon();
setFrameIcon(new ImageIcon(ico));
this.cameraPanel = cameraPanel;
initializeUI();
// Timer for UI updates only (1 FPS is enough for the clock)
this.statsTimer = new Timer(1000, e -> updateStats());
this.statsTimer.setCoalesce(true);
pack();
}
private void setupDirectory() {
File videoDir = new File(System.getProperty("user.home"), "Videos");
outputDirectory = new File(videoDir, "swtcctv-rec");
if (!outputDirectory.exists()) {
boolean created = outputDirectory.mkdirs();
if (!created) {
System.err.println("Could not create recording directory: " + outputDirectory.getAbsolutePath());
}
}
}
private void initializeUI() {
JPanel mainContent = new JPanel();
mainContent.setLayout(new BoxLayout(mainContent, BoxLayout.Y_AXIS));
mainContent.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
exportSection = new ExportSection(this, outputDirectory, statusLabel);
JPanel actionPanel = createActionPanel();
JPanel statsPanel = createStatsPanel();
mainContent.add(exportSection);
mainContent.add(Box.createVerticalStrut(10));
mainContent.add(statsPanel);
mainContent.add(Box.createVerticalStrut(10));
mainContent.add(actionPanel);
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(240, 20));
panel.add(statusLabel);
panel.add(statsLabel);
return panel;
}
private void updateStats() {
if (avRecorder == null || !avRecorder.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(String.format("%.2f", sizeInMb))
.append(" MB");
statsLabel.setText(sb.toString());
}
private void toggleRecording() {
// Updated check for the new recorder state
if (avRecorder == null || !avRecorder.isRecording()) {
startRec();
} else {
stopRec();
}
}
private void startRec() {
BufferedImage sample = cameraPanel.getCurrentProcessedImage();
if (sample == null) {
ShowError.warning(this, "No camera feed detected. Start camera first.", "Warning");
return;
}
try {
if (!outputDirectory.exists() && !outputDirectory.mkdirs()) {
throw new IOException("Failed to create directory: " + outputDirectory);
}
currentFile = new File(outputDirectory, "vid_" + System.currentTimeMillis() + ".mp4");
// 1. Define the production config
RecorderConfig config = new RecorderConfig(
currentFile,
sample.getWidth(),
sample.getHeight(),
20, // Frame Rate
25, // CRF (Quality)
"ultrafast"
);
// 2. Initialize the HA Recorder
avRecorder = new AVRecorder(config);
avRecorder.start();
// 3. Link to CameraPanel (Ensure CameraPanel calls .accept(img))
cameraPanel.setExternalRecorder(avRecorder);
startTime = System.currentTimeMillis();
statsTimer.start();
recordBtn.setText("Stop Recording");
recordBtn.setForeground(Color.RED);
statusLabel.setText("Recording...");
} catch (Exception ex) {
ShowError.error(this, "Failed to start Recorder: " + ex.getMessage(), "Error");
ex.printStackTrace();
}
}
private void stopRec() {
if (avRecorder != null) {
statusLabel.setText("Finalizing file...");
avRecorder.stop();
cameraPanel.setExternalRecorder(null);
}
statsTimer.stop();
recordBtn.setText("Start Recording");
recordBtn.setForeground(null);
String fileName = (currentFile != null) ? currentFile.getName() : "N/A";
statusLabel.setText("File saved: " + fileName);
}
private JPanel createStoragePanel() {
JPanel panel = new JPanel(new BorderLayout(5, 5));
panel.setBorder(BorderFactory.createTitledBorder("Storage Folder"));
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 void exportToUSB() {
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
chooser.setDialogTitle("Select USB destination");
if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) return;
Path usbRoot = chooser.getSelectedFile().toPath();
Path exportTarget = usbRoot.resolve("CCTV_EXPORT");
try {
long size = USBExportManager.calculateDirectorySize(outputDirectory.toPath());
if (!USBExportManager.hasEnoughSpace(usbRoot, size)) {
ShowError.error(this, "Not enough space on USB device.", "Export failed");
return;
}
JProgressBar bar = new JProgressBar(0, 100);
JOptionPane pane = new JOptionPane(bar,
JOptionPane.INFORMATION_MESSAGE,
JOptionPane.DEFAULT_OPTION,
null,
new Object[]{});
JDialog dialog = pane.createDialog(this, "Exporting...");
dialog.setModal(false);
dialog.setVisible(true);
USBExportManager.exportAsync(
outputDirectory.toPath(),
exportTarget,
stats -> bar.setValue(stats.percent()),
() -> {
dialog.dispose();
statusLabel.setText("Export completed");
},
ex -> {
dialog.dispose();
ShowError.error(this, ex.getMessage(), "Export error");
}
);
} catch (IOException ex) {
ShowError.error(this, ex.getMessage(), "Export error");
}
}
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());
// Export is now handled by the ExportSection component
panel.add(recordBtn);
panel.add(snapBtn);
return panel;
}
private void takeSnapshot() {
BufferedImage img = cameraPanel.getCurrentProcessedImage();
if (img != null) {
try {
if (!outputDirectory.exists()) outputDirectory.mkdirs();
File file = new File(outputDirectory, "snap_" + System.currentTimeMillis() + ".png");
ImageIO.write(img, "PNG", file);
statusLabel.setText("Snapshot: " + file.getName());
} catch (IOException ex) {
ShowError.error(this, "Snapshot failed: " + ex.getMessage(), "Error");
}
}
}
}

View File

@@ -1,126 +0,0 @@
package io.swtc.proccessing.ui.iframe;
import io.swtc.proccessing.CameraPanel;
import io.swtc.recording.VideoRecorder;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
public class RecordingPane extends JPanel {
private final VideoRecorder videoRecorder;
private final CameraPanel cameraPanel;
private JTextField pathField;
private JButton recordBtn;
private JLabel statusLabel;
private File outputDirectory;
public RecordingPane(CameraPanel cameraPanel, VideoRecorder videoRecorder) {
this.cameraPanel = cameraPanel;
this.videoRecorder = videoRecorder;
this.outputDirectory = new File(System.getProperty("user.home"));
setLayout(new GridBagLayout());
JPanel contentPanel = new JPanel();
contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
contentPanel.setPreferredSize(new Dimension(400, 250));
// Add the functional sections
contentPanel.add(createStoragePanel());
contentPanel.add(Box.createVerticalStrut(15));
contentPanel.add(createActionPanel());
contentPanel.add(Box.createVerticalStrut(15));
contentPanel.add(createStatusPanel());
add(contentPanel);
}
private JPanel createStoragePanel() {
JPanel panel = new JPanel(new BorderLayout(5, 5));
panel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(), "Storage Settings", TitledBorder.LEFT, TitledBorder.TOP));
pathField = new JTextField(outputDirectory.getAbsolutePath());
pathField.setEditable(false);
JButton browseBtn = new JButton("Browse...");
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, 10, 10));
recordBtn = new JButton("start recording");
recordBtn.addActionListener(e -> toggleRecording());
JButton snapBtn = new JButton("take snapshot");
snapBtn.addActionListener(e -> takeSnapshot());
panel.add(recordBtn);
panel.add(snapBtn);
return panel;
}
private JPanel createStatusPanel() {
JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER));
statusLabel = new JLabel("");
statusLabel.setForeground(Color.DARK_GRAY);
panel.add(statusLabel);
return panel;
}
private void toggleRecording() {
if (!videoRecorder.isRecording()) {
try {
File file = new File(outputDirectory, "vid_" + System.currentTimeMillis() + ".mp4");
videoRecorder.startRecording(cameraPanel, file);
recordBtn.setText("stop recording");
statusLabel.setText("recording");
} catch (IOException ex) {
showError("Start Error", ex);
}
} else {
try {
File saved = videoRecorder.stopRecording();
recordBtn.setText("Start Recording");
statusLabel.setText("Status: Saved " + saved.getName());
} catch (IOException ex) {
showError("Stop Error", ex);
}
}
}
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("captured");
} catch (IOException ex) {
showError("Snapshot Error", ex);
}
}
}
private void showError(String title, Exception ex) {
JOptionPane.showMessageDialog(this, ex.getMessage(), title, JOptionPane.ERROR_MESSAGE);
}
}

View File

@@ -0,0 +1,135 @@
package io.swtc.proccessing.ui.sections.recording;
import io.swtc.proccessing.ui.ShowError;
import io.swtc.recording.evidence.USBExportManager;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
public class ExportSection extends JPanel {
private final Component parent;
private File sourceDirectory;
private final JLabel statusLabel;
private final JTextField pathField;
public ExportSection(Component parent, File initialSource, JLabel statusLabel) {
this.parent = parent;
this.sourceDirectory = initialSource;
this.statusLabel = new JLabel("", SwingConstants.CENTER);
setLayout(new BorderLayout());
this.pathField = new JTextField(sourceDirectory.getAbsolutePath());
this.pathField.setEditable(false);
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab("Evidence-Export", createTransferTab());
tabbedPane.addTab("Storage Settings", createSettingsTab());
add(tabbedPane, BorderLayout.CENTER);
}
private static JLabel getStatusLabel(JLabel statusLabel) {
return statusLabel;
}
private JPanel createTransferTab() {
JPanel panel = new JPanel(new GridBagLayout());
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
GridBagConstraints gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1.0;
JButton exportBtn = new JButton("Export all Evidence");
exportBtn.addActionListener(e -> startUsbExport());
JLabel infoLabel = new JLabel("<html><small>" +
"Export Evidence to a USB Drive!" +
"</small></html>");
gbc.gridy = 0;
panel.add(exportBtn, gbc);
gbc.gridy = 1;
gbc.insets = new Insets(10, 0, 0, 0);
panel.add(infoLabel, gbc);
return panel;
}
private JPanel createSettingsTab() {
JPanel panel = new JPanel(new BorderLayout(5, 5));
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
JButton browseBtn = new JButton("Change Folder");
browseBtn.addActionListener(e -> changeSourceDirectory());
panel.add(new JLabel("Recording Path:"), BorderLayout.NORTH);
panel.add(pathField, BorderLayout.CENTER);
panel.add(browseBtn, BorderLayout.SOUTH);
return panel;
}
private void changeSourceDirectory() {
JFileChooser chooser = new JFileChooser(sourceDirectory);
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
if (chooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) {
this.sourceDirectory = chooser.getSelectedFile();
this.pathField.setText(sourceDirectory.getAbsolutePath());
}
}
private void startUsbExport() {
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
chooser.setDialogTitle("Select USB Destination");
if (chooser.showSaveDialog(parent) != JFileChooser.APPROVE_OPTION) return;
Path usbRoot = chooser.getSelectedFile().toPath();
Path exportTarget = usbRoot.resolve("");
try {
long size = USBExportManager.calculateDirectorySize(sourceDirectory.toPath());
if (!USBExportManager.hasEnoughSpace(usbRoot, size)) {
ShowError.error(parent, "Not enough space on USB device.", "Export failed");
return;
}
JProgressBar bar = new JProgressBar(0, 100);
bar.setStringPainted(true);
JDialog dialog = new JDialog(SwingUtilities.getWindowAncestor(parent), "Exporting Data...");
dialog.setLayout(new BorderLayout());
dialog.add(new JLabel("Copying files ...", JLabel.CENTER), BorderLayout.NORTH);
dialog.add(bar, BorderLayout.CENTER);
dialog.setSize(300, 100);
dialog.setLocationRelativeTo(parent);
dialog.setModal(true);
USBExportManager.exportAsync(
sourceDirectory.toPath(),
exportTarget,
stats -> SwingUtilities.invokeLater(() -> bar.setValue(stats.percent())),
() -> {
dialog.dispose();
statusLabel.setText("Export completed successfully.");
},
ex -> {
dialog.dispose();
ShowError.error(parent, "Export error: " + ex.getMessage(), "Error");
}
);
dialog.setVisible(true);
} catch (IOException ex) {
ShowError.error(parent, "IO Error: " + ex.getMessage(), "Export error");
}
}
}

View File

@@ -1,114 +0,0 @@
package io.swtc.recording;
import io.swtc.proccessing.CameraPanel;
import org.jcodec.api.awt.AWTSequenceEncoder;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.common.model.Rational;
import javax.swing.*;
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.
*
* */
public class VideoRecorder {
private boolean recording = false;
private Timer captureTimer;
private File outputFile;
private AWTSequenceEncoder encoder;
private SeekableByteChannel channel;
private static final int FPS = 30;
public void startRecording(CameraPanel panel, File output) throws IOException {
this.outputFile = output;
this.recording = true;
channel = NIOUtils.writableFileChannel(String.valueOf(outputFile));
encoder = new AWTSequenceEncoder(channel, Rational.R(FPS, 1));
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
);
try {
stopRecording();
} catch (IOException stopEx) {
JOptionPane.showMessageDialog(
null,
"IOException@stopEx\n" + stopEx,
"IOException in Recorder@stopEx",
JOptionPane.ERROR_MESSAGE
);
}
}
});
captureTimer.start();
}
public File stopRecording() throws IOException {
recording = false;
/* some helper methods, i swear its always the same? */
if (captureTimer != null) {
captureTimer.stop();
}
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;
}
}

View File

@@ -0,0 +1,60 @@
package io.swtc.recording.cv;
import io.swtc.proccessing.ui.ShowError;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class AVRecorder {
private final BlockingQueue<BufferedImage> queue = new LinkedBlockingQueue<>(120);
private final RecorderConfig config;
private volatile boolean running = false;
public AVRecorder(RecorderConfig config) {
this.config = config;
}
public void start() {
running = true;
Thread worker = new Thread(this::runLoop, "Recording-Worker");
worker.start();
}
private void runLoop() {
FrameProccessor processor = new FrameProccessor();
MediaSink sink = null;
try {
while (running || !queue.isEmpty()) {
BufferedImage img = queue.poll(500, TimeUnit.MILLISECONDS);
if (img == null) continue;
if (sink == null) {
sink = new MediaSink(config);
}
sink.write(processor.convert(img));
}
} catch (Exception e) {
ShowError.error(null,"Error in AVRecorder", "AVRecorder isn't responding!");
} finally {
if (sink != null) sink.stop();
processor.close();
}
}
public void accept(BufferedImage img) {
if (!queue.offer(img)) {
System.err.println("Recorder lag: Frame dropped");
}
}
public boolean isRecording() { return running; }
public void stop() {
running = false;
}
}

View File

@@ -0,0 +1,32 @@
package io.swtc.recording.cv;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import java.awt.image.BufferedImage;
import java.util.Objects;
public class FrameProccessor {
private final Java2DFrameConverter converter = new Java2DFrameConverter();
private BufferedImage reuseImg;
public Frame convert(BufferedImage rawImg) {
if (Objects.isNull(reuseImg)) {
reuseImg = new BufferedImage(
rawImg.getWidth(),
rawImg.getHeight(),
BufferedImage.TYPE_3BYTE_BGR // default java BufferedImage Type
);
var g = reuseImg.createGraphics();
g.drawImage(rawImg, 0, 0, null);
g.dispose();
}
return converter.getFrame(reuseImg);
}
public void close() {
converter.close();
if (!Objects.isNull(reuseImg)) { reuseImg.flush(); }
}
}

View File

@@ -0,0 +1,45 @@
package io.swtc.recording.cv;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.Frame;
public class MediaSink {
private final FFmpegFrameRecorder recorder;
private final long startNanos;
public MediaSink(RecorderConfig config) throws Exception {
this.recorder = new FFmpegFrameRecorder(config.outputFile(), config.width(), config.height());
this.startNanos = System.nanoTime();
avutil.av_log_set_level(avutil.AV_LOG_ERROR);
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setFormat("mp4");
recorder.setPixelFormat(avutil.AV_PIX_FMT_BGR24);
recorder.setFrameRate(config.fps());
recorder.setVideoOption("pixel_format", "yuv420p");
recorder.setVideoOption("preset", config.preset());
recorder.setVideoOption("crf", String.valueOf(config.crf()));
recorder.setGopSize(config.fps() * 2);
recorder.start();
}
public void write(Frame frame) throws Exception {
long pts = (System.nanoTime() - startNanos) / 1000;
if (pts > recorder.getTimestamp()) {
recorder.setTimestamp(pts);
recorder.record(frame);
}
}
public void stop() {
try {
recorder.stop();
recorder.release();
} catch (Exception ignored) { /* Do absolutley nothing ;) */ }
}
}

View File

@@ -0,0 +1,12 @@
package io.swtc.recording.cv;
import java.io.File;
public record RecorderConfig(
File outputFile,
int width,
int height,
int fps,
int crf, // Quality, 18 is high and 28 is low
String preset // ultrafast
) { /* Record */ }

View File

@@ -0,0 +1,42 @@
package io.swtc.recording.evidence;
import java.util.concurrent.TimeUnit;
public record ExportStats(
long totalBytes,
long copiedBytes,
long startTimeNanos
) {
public int percent() {
return totalBytes == 0 ? 0 :
(int) ((copiedBytes * 100) / totalBytes);
}
/* timeLeft, ill implement this in the future (in the UI) i think
* this is the proper way to do this? idk
* */
public String timeLeft() {
if (copiedBytes == 0) return "0";
long elapsedNanos = System.nanoTime() - startTimeNanos;
double bytesPerNano = (double) copiedBytes / elapsedNanos;
long remainingBytes = totalBytes - copiedBytes;
long remainingNanos = (long) (remainingBytes / bytesPerNano);
return formatDuration(remainingNanos);
}
private static String formatDuration(long nanos) {
long seconds = TimeUnit.NANOSECONDS.toSeconds(nanos);
long mins = seconds / 60;
long secs = seconds % 60;
if (mins > 0) {
return mins + "m " + secs + "s";
}
return secs + "s";
}
}

View File

@@ -0,0 +1,114 @@
package io.swtc.recording.evidence;
import javax.swing.*;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
public class USBExportManager {
public static long calculateDirectorySize(Path dir) throws IOException {
AtomicLong size = new AtomicLong();
Files.walkFileTree(dir, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
size.addAndGet(attrs.size());
return FileVisitResult.CONTINUE;
}
});
return size.get();
}
public static boolean hasEnoughSpace(Path target, long requiredBytes) throws IOException {
FileStore store = Files.getFileStore(target);
return store.getUsableSpace() >= requiredBytes;
}
public static void exportDirectory(
Path sourceDir,
Path usbTargetDir,
Consumer<ExportStats> progressCallback
) throws IOException {
if (!Files.isDirectory(sourceDir)) {
throw new IOException("Source is not a directory: " + sourceDir);
}
Files.createDirectories(usbTargetDir);
long totalBytes = calculateDirectorySize(sourceDir);
AtomicLong copiedBytes = new AtomicLong();
Files.walkFileTree(sourceDir, new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
Path targetDir = usbTargetDir.resolve(sourceDir.relativize(dir));
Files.createDirectories(targetDir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Path targetFile = usbTargetDir.resolve(sourceDir.relativize(file));
Files.copy(
file,
targetFile,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES
);
copiedBytes.addAndGet(attrs.size());
long startTime = System.nanoTime();
if (progressCallback != null) {
progressCallback.accept(
new ExportStats(totalBytes, copiedBytes.get(), startTime)
);
}
return FileVisitResult.CONTINUE;
}
});
}
public static void exportAsync(
Path sourceDir,
Path usbTargetDir,
Consumer<ExportStats> progressCallback,
Runnable onDone,
Consumer<Exception> onError
) {
new Thread(() -> {
try {
exportDirectory(sourceDir, usbTargetDir, stats ->
SwingUtilities.invokeLater(() ->
progressCallback.accept(stats))
);
if (onDone != null) {
SwingUtilities.invokeLater(onDone);
}
} catch (Exception ex) {
if (onError != null) {
SwingUtilities.invokeLater(() -> onError.accept(ex));
}
}
}, "USB-Export-Thread").start();
}
}

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