11 Commits

Author SHA1 Message Date
e1003c20ff A LOT of changes!
Firstly a desktop icon system, secondly in the cameras ui you can now see time, thirdly you can now set the recording quality, lastly you have a desktop icon specifically for exporting!
2026-01-31 21:40:34 +01:00
8239b910fe recording was fucked up. FIX!
mainly in RecordingFrame
2026-01-29 17:02:04 +01:00
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
41fbf62757 just some theme changes, i like this one more 2026-01-18 22:10:37 +01:00
34 changed files with 1803 additions and 673 deletions

3
.gitignore vendored
View File

@@ -46,3 +46,6 @@ dependency-reduced-pom.xml
## This is for our app, cause it likes to store stuff ##
network_cameras.json
## exec launch4j config ##
execfg.xml

120
pom.xml
View File

@@ -7,7 +7,7 @@
<groupId>io.swtc</groupId>
<artifactId>swtc</artifactId>
<version>1.0-SNAPSHOT</version>
<!--
<build>
<plugins>
<plugin>
@@ -42,6 +42,43 @@
</plugin>
</plugins>
</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>
<maven.compiler.source>17</maven.compiler.source>
@@ -63,30 +100,30 @@
<version>0.3.12</version>
</dependency>
<!-- for gl we use lwjgl -->
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl</artifactId>
<version>3.3.3</version>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-opengl</artifactId>
<version>3.3.3</version>
</dependency>
<!-- &lt;!&ndash; for gl we use lwjgl &ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>org.lwjgl</groupId>-->
<!-- <artifactId>lwjgl</artifactId>-->
<!-- <version>3.3.3</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.lwjgl</groupId>-->
<!-- <artifactId>lwjgl-opengl</artifactId>-->
<!-- <version>3.3.3</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl</artifactId>
<version>3.3.3</version>
<classifier>natives-windows</classifier>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-opengl</artifactId>
<version>3.3.3</version>
<classifier>natives-windows</classifier>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.lwjgl</groupId>-->
<!-- <artifactId>lwjgl</artifactId>-->
<!-- <version>3.3.3</version>-->
<!-- <classifier>natives-windows</classifier>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.lwjgl</groupId>-->
<!-- <artifactId>lwjgl-opengl</artifactId>-->
<!-- <version>3.3.3</version>-->
<!-- <classifier>natives-windows</classifier>-->
<!-- </dependency>-->
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
@@ -106,21 +143,34 @@
<version>2.20.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.jcodec/jcodec -->
<!--
Saving into Files
-->
<!-- &lt;!&ndash; https://mvnrepository.com/artifact/org.jcodec/jcodec &ndash;&gt;-->
<!-- &lt;!&ndash;-->
<!-- Saving into Files-->
<!-- &ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>org.jcodec</groupId>-->
<!-- <artifactId>jcodec</artifactId>-->
<!-- <version>0.2.5</version>-->
<!-- </dependency>-->
<!-- &lt;!&ndash; https://mvnrepository.com/artifact/org.jcodec/jcodec-javase &ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>org.jcodec</groupId>-->
<!-- <artifactId>jcodec-javase</artifactId>-->
<!-- <version>0.2.5</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.jcodec</groupId>
<artifactId>jcodec</artifactId>
<version>0.2.5</version>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>1.5.10</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.jcodec/jcodec-javase -->
<dependency>
<groupId>org.jcodec</groupId>
<artifactId>jcodec-javase</artifactId>
<version>0.2.5</version>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>6.1.1-1.5.10</version>
<classifier>windows-x86_64</classifier>
</dependency>
</dependencies>

View File

@@ -1,26 +1,21 @@
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(
"com.sun.java.swing.plaf.windows.WindowsLookAndFeel"
);
} catch (Exception e) {
JOptionPane.showMessageDialog(
null,
"LaF not available",
"LaF-Warning",
JOptionPane.WARNING_MESSAGE
);
}
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) { /* Do nothing */ }
// For some reason we need to invoke Later for LaF to work!
SwingUtilities.invokeLater(() -> SwingCCTVManager.main(null));
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,204 +44,236 @@ 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);
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)));
}
deviceTable.getColumnModel().getColumn(0)
.setCellRenderer(new DefaultTableCellRenderer() {
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 Component getTableCellRendererComponent(
JTable t, Object v, boolean s, boolean f, int r, int c) {
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 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();
}
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("ONLINE".equals(v)
? new Color(0, 150, 0)
: Color.RED);
comp.setForeground(STATUS_ONLINE.equals(v) ? new Color(0, 150, 0) : Color.RED);
return comp;
}
});
}
private void startAutoRefresh() {
new Timer(5000, e -> refreshTable()).start();
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"});
}
private void refreshTable() {
int[] selectedRows = deviceTable.getSelectedRows();
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);
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"
});
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();
}
for (int r : selectedRows) {
if (r < tableModel.getRowCount()) {
deviceTable.addRowSelectionInterval(r, r);
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; }
}
}
}
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");
JMenuItem delete = new JMenuItem("Remove Device");
launch.addActionListener(al -> launchSelected());
delete.addActionListener(al -> deleteSelected());
menu.add(launch);
menu.addSeparator();
menu.add(delete);
menu.show(deviceTable, e.getX(), e.getY());
} 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,7 +1,10 @@
package io.swtc;
import com.github.sarxos.webcam.Webcam;
import io.swtc.proccessing.ui.iframe.*; // Your custom frames
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.desktop.DIM;
import io.swtc.proccessing.ui.desktop.evidence.EvidenceExportFrame;
import io.swtc.proccessing.ui.iframe.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
@@ -9,10 +12,12 @@ import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class SwingIFrame {
private final JFrame mainFrame;
private final DesktopPane desktopPane;
private final DIM desktopIconManager;
private final Map<JInternalFrame, EffectsPanelFrame> cameraToEffects = new HashMap<>();
private boolean fullscreen = false;
@@ -23,15 +28,22 @@ public class SwingIFrame {
private final JPopupMenu popupMenu = new JPopupMenu();
public SwingIFrame() {
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);
desktopIconManager = new DIM(desktopPane);
setupDesktopExportFrame();
setupFullscreenToggle();
setupBlackBg();
initPopupMenu();
@@ -39,6 +51,14 @@ public class SwingIFrame {
desktopPane.addMouseListener(popupListener());
}
private void setupDesktopExportFrame() {
desktopIconManager.addIcon(
"Export Evidence",
IconSetter.getSaveIconAsImageIcon(),
EvidenceExportFrame::showExport
);
}
public void addCameraInternalFrame(Webcam webcam) {
CameraInternalFrame cameraFrame = new CameraInternalFrame(webcam, this::handleEffectsRequest);
@@ -106,7 +126,14 @@ public class SwingIFrame {
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
@@ -118,6 +145,22 @@ public class SwingIFrame {
});
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() {
@@ -134,6 +177,7 @@ public class SwingIFrame {
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() {
@@ -142,6 +186,16 @@ public class SwingIFrame {
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() {
@@ -150,6 +204,33 @@ public class SwingIFrame {
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) {

View File

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

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,93 +1,135 @@
package io.swtc.proccessing;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.ShowError;
import io.swtc.recording.cv.AVRecorder;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.font.GlyphVector;
import java.awt.image.BufferedImage;
import java.io.InputStream;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
/**
* Enhanced CameraPanel with support for custom image processors
*/
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 Font overlayFont;
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
// Cached glyph vectors for performance
private GlyphVector dateGlyphs;
private GlyphVector timeGlyphs;
private String lastDate = "";
private String lastTime = "";
// Pre-calculated positions to avoid repeated calculations
private static final int OVERLAY_X = 8;
private static final int OVERLAY_Y = 20;
private static final int TIME_Y_OFFSET = 19;
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() {
setBackground(Color.BLACK);
setPreferredSize(new Dimension(640, 480));
loadFont();
graphicsConfig = GraphicsEnvironment.getLocalGraphicsEnvironment()
.getDefaultScreenDevice().getDefaultConfiguration();
initInteractionListeners();
}
private void loadFont() {
try (InputStream is = getClass().getResourceAsStream("/font/OverlayFont.ttf")) {
if (is != null) {
overlayFont = Font.createFont(Font.TRUETYPE_FONT, is).deriveFont(15f);
} else {
overlayFont = new Font(Font.MONOSPACED, Font.PLAIN, 15);
}
} catch (Exception e) {
overlayFont = new Font(Font.MONOSPACED, Font.PLAIN, 15);
}
}
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) {
this.currentImage = img;
processImage();
repaint();
this.sourceImage = img;
updateProcessedImage();
// 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) {
this.imageProcessor = processor;
processImage();
repaint();
}
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);
if (sourceImage != null) {
updateProcessedImage();
scheduleRepaint();
}
}
@@ -95,4 +137,158 @@ public class CameraPanel extends JPanel {
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();
try {
g2d.drawImage(temp, 0, 0, null); // this is fucking expensive
drawTimeOverlay(g2d);
} finally {
g2d.dispose();
}
}
private void drawTimeOverlay(Graphics2D g) {
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
g.setFont(overlayFont);
LocalDateTime now = LocalDateTime.now();
String dateStr = now.format(dateFormatter);
String timeStr = now.format(timeFormatter);
if (!dateStr.equals(lastDate)) {
dateGlyphs = overlayFont.createGlyphVector(g.getFontRenderContext(), dateStr);
lastDate = dateStr;
}
if (!timeStr.equals(lastTime)) {
timeGlyphs = overlayFont.createGlyphVector(g.getFontRenderContext(), timeStr);
lastTime = timeStr;
}
g.setColor(Color.BLACK);
g.drawGlyphVector(dateGlyphs, OVERLAY_X + 2, OVERLAY_Y + 2);
g.drawGlyphVector(timeGlyphs, OVERLAY_X + 2, OVERLAY_Y + TIME_Y_OFFSET + 2);
g.setColor(Color.WHITE);
g.drawGlyphVector(dateGlyphs, OVERLAY_X, OVERLAY_Y);
g.drawGlyphVector(timeGlyphs, OVERLAY_X, OVERLAY_Y + TIME_Y_OFFSET);
}
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,76 +2,84 @@ 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.*;
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.
try {
webcam.open();
} catch (WebcamException e) {
JOptionPane.showMessageDialog(
null,
"WebcamException" + e.getMessage(),
"WebcamException",
JOptionPane.ERROR_MESSAGE
);
}
if (!webcam.isOpen()) webcam.open(); // open if not
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) {
//System.err.println(img);
if (img != null)
// there is no need for a invoke later swing wise!
onFrameCaptured.accept(img);
}
try {
Thread.sleep(15);
} catch (InterruptedException e) {
break;
}
}
try {
webcam.close();
long timespent = System.nanoTime() - startTime;
long sleeptime = frameDurationLimitns - timespent;
} catch (IllegalStateException e) {
// show a error message from javax.swing.awt.JOptionPane
JOptionPane.showMessageDialog(
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();
}
@@ -111,6 +119,5 @@ public class WebcamCaptureLoop {
*/
public void stop() {
running = false;
cleanup();
}
}

View File

@@ -0,0 +1,50 @@
package io.swtc.proccessing.ui;
import javax.swing.*;
import java.awt.*;
import java.io.InputStream;
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;
}
public static ImageIcon getSaveIconAsImageIcon() {
if (Objects.isNull(ICON_ICON)) {
URL url = IconSetter.class.getResource("/icons/save.png");
if (Objects.isNull(url)) throw new RuntimeException("Icon not found: /icons/save.ico");
ICON_ICON = new ImageIcon(url);
}
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

@@ -0,0 +1,50 @@
package io.swtc.proccessing.ui.desktop;
import javax.swing.*;
import java.awt.*;
/* DesktopIconManager */
public class DIM {
private final JDesktopPane desktop;
private int cX;
private int cY;
private static final int PAD = 6;
public DIM(JDesktopPane desktop) {
this.desktop = desktop;
Insets insets = desktop.getInsets();
this.cX = insets.left + PAD;
this.cY = insets.top + PAD;
}
public void addIcon(String label, Icon icon, Runnable runaction) {
DesktopIcon desktopIcon = new DesktopIcon(label, icon, runaction);
Dimension pref = desktopIcon.getPreferredSize();
int w = pref.width;
int h = pref.height;
Insets insets = desktop.getInsets();
int usableHeight = desktop.getHeight() - insets.top - insets.bottom;
if (usableHeight <= 0) {
usableHeight = Integer.MAX_VALUE;
}
if (cY + h > usableHeight) {
cY = insets.top + PAD;
cX += w + PAD;
}
desktopIcon.setBounds(cX, cY, w, h);
desktop.add(desktopIcon, JLayeredPane.DEFAULT_LAYER);
cY += h + PAD;
desktop.repaint();
}
}

View File

@@ -0,0 +1,107 @@
package io.swtc.proccessing.ui.desktop;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class DesktopIcon extends JPanel {
private boolean hovered = false;
private final JLabel iconLabel;
private final JLabel textLabel;
public DesktopIcon(String label, Icon icon, Runnable action) {
setLayout(new BorderLayout(4, 4));
setOpaque(false);
if (icon instanceof ImageIcon) {
Image img = ((ImageIcon) icon).getImage();
Image scaled = img.getScaledInstance(64, 64, Image.SCALE_SMOOTH);
icon = new ImageIcon(scaled);
}
iconLabel = new JLabel(icon, SwingConstants.CENTER);
textLabel = new ShadowLabel(label);
textLabel.setHorizontalAlignment(SwingConstants.CENTER);
add(iconLabel, BorderLayout.CENTER);
add(textLabel, BorderLayout.SOUTH);
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) {
if (action != null) action.run();
}
}
@Override
public void mouseEntered(MouseEvent e) {
hovered = true;
repaint();
}
@Override
public void mouseExited(MouseEvent e) {
hovered = false;
repaint();
}
});
}
@Override
public Dimension getPreferredSize() {
Dimension icon = iconLabel.getPreferredSize();
Dimension text = textLabel.getPreferredSize();
int w = Math.max(icon.width, text.width) + 12;
int h = icon.height + text.height + 12;
return new Dimension(w, h);
}
@Override
protected void paintComponent(Graphics g) {
if (hovered) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
boolean lightBg = isBackgroundLight();
Color fill = lightBg
? new Color(0, 0, 0, 30)
: new Color(255, 255, 255, 40);
Color border = lightBg
? new Color(0, 0, 0, 80)
: new Color(255, 255, 255, 100);
g2.setColor(fill);
g2.fillRoundRect(2, 2, getWidth() - 4, getHeight() - 4, 10, 10);
g2.setColor(border);
g2.drawRoundRect(2, 2, getWidth() - 5, getHeight() - 5, 10, 10);
g2.dispose();
}
super.paintComponent(g);
}
private boolean isBackgroundLight() {
Container p = getParent();
if (p == null) return true;
Color bg = p.getBackground();
int luminance = (int) (
bg.getRed() * 0.299 +
bg.getGreen() * 0.587 +
bg.getBlue() * 0.114
);
return luminance > 180;
}
}

View File

@@ -0,0 +1,32 @@
package io.swtc.proccessing.ui.desktop;
import javax.swing.*;
import java.awt.*;
public class ShadowLabel extends JLabel {
public ShadowLabel(String text) {
super(text, SwingConstants.CENTER);
setForeground(Color.WHITE);
setPreferredSize(new Dimension(15, 20));
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
FontMetrics fm = g2d.getFontMetrics();
String text = getText();
int x = (getWidth() - fm.stringWidth(text)) / 2;
int y = ((getHeight() - fm.getHeight()) / 2) + fm.getAscent();
g2d.setColor(new Color(0, 0, 0, 200));
g2d.drawString(text, x + 1, y + 1);
g2d.setColor(getForeground());
g2d.drawString(text, x, y);
g2d.dispose();
}
}

View File

@@ -0,0 +1,110 @@
package io.swtc.proccessing.ui.desktop.evidence;
import io.swtc.proccessing.ui.ShowError;
import io.swtc.recording.evidence.USBExportManager;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.nio.file.Path;
import java.io.File;
public class EvidenceExportFrame extends JFrame {
private final JProgressBar progressBar;
private final JLabel statusLabel;
private final JLabel detailLabel;
private final JButton actionBtn;
private EvidenceExportFrame(Path sourceDir, Path usbTargetDir) {
setTitle("Export");
setSize(400, 220);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
JPanel contentPane = new JPanel();
contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.Y_AXIS));
contentPane.setBorder(new EmptyBorder(25, 25, 25, 25));
statusLabel = new JLabel("Starting export");
statusLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
statusLabel.setFont(new Font(statusLabel.getFont().getName(), Font.BOLD, 14));
detailLabel = new JLabel("Initializing");
detailLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
detailLabel.setForeground(Color.GRAY);
progressBar = new JProgressBar(0, 100);
progressBar.setAlignmentX(Component.LEFT_ALIGNMENT);
progressBar.setMaximumSize(new Dimension(Integer.MAX_VALUE, 20));
actionBtn = new JButton("Cancel");
actionBtn.setAlignmentX(Component.LEFT_ALIGNMENT);
contentPane.add(statusLabel);
contentPane.add(Box.createRigidArea(new Dimension(0, 10)));
contentPane.add(detailLabel);
contentPane.add(Box.createRigidArea(new Dimension(0, 20)));
contentPane.add(progressBar);
contentPane.add(Box.createVerticalGlue());
contentPane.add(actionBtn);
add(contentPane);
actionBtn.addActionListener(e -> handleAction());
setVisible(true);
startExport(sourceDir, usbTargetDir);
}
private void handleAction() {
if (actionBtn.getText().equals("Close")) {
dispose();
return;
}
int confirm = JOptionPane.showConfirmDialog(this,
"Stop export?", "Confirm", JOptionPane.YES_NO_OPTION);
if (confirm == JOptionPane.YES_OPTION) dispose();
}
private void startExport(Path sourceDir, Path usbTargetDir) {
USBExportManager.exportAsync(
sourceDir,
usbTargetDir,
stats -> SwingUtilities.invokeLater(() -> {
progressBar.setValue(stats.percent());
statusLabel.setText("Exporting " + stats.percent() + "%");
detailLabel.setText(String.format("%s | %s remaining",
stats.getSpeedMBps(), stats.timeLeft()));
}),
() -> SwingUtilities.invokeLater(() -> {
progressBar.setValue(100);
statusLabel.setText("Export Complete");
detailLabel.setText("Files saved");
actionBtn.setText("Close");
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
}),
ex -> SwingUtilities.invokeLater(() -> {
statusLabel.setText("Export Failed");
detailLabel.setText(ex.getMessage());
ShowError.error(this, ex.getMessage(), "Error");
})
);
}
public static void showExport() {
SwingUtilities.invokeLater(() -> {
File videoDir = new File(System.getProperty("user.home"), "Videos/swtcctv-rec");
if (!videoDir.exists()) {
ShowError.warning(null, "No recordings found.", "Not Found");
return;
}
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
Path target = chooser.getSelectedFile().toPath().resolve("swtcctv-rec_" + System.currentTimeMillis() / 1000);
new EvidenceExportFrame(videoDir.toPath(), target);
}
});
}
}

View File

@@ -3,31 +3,25 @@ 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.recording.VideoRecorder;
import javax.imageio.ImageIO;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.ShowError;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.image.BufferedImage;
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; // Instance of the recorder
private JButton recordBtn;
public CameraInternalFrame(Webcam webcam, Consumer<CameraInternalFrame> onOpenEffects) {
super(webcam.getName(), true, true, true, true);
this.cameraPanel = new CameraPanel();
this.videoRecorder = new VideoRecorder(); // Initialize recorder
// Initialize capture loop
this.captureLoop = new WebcamCaptureLoop(webcam, img ->
SwingUtilities.invokeLater(() -> cameraPanel.setImage(img))
);
Image ico = IconSetter.getIcon();
setFrameIcon(new ImageIcon(ico));
this.cameraPanel = new CameraPanel();
this.captureLoop = new WebcamCaptureLoop(webcam, cameraPanel::setImage);
setupUI(onOpenEffects);
captureLoop.start();
@@ -36,11 +30,15 @@ public class CameraInternalFrame extends JInternalFrame {
private void setupUI(Consumer<CameraInternalFrame> onOpenEffects) {
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab("View", cameraPanel);
tabbedPane.addTab("Capture", new RecordingPane(cameraPanel, videoRecorder));
tabbedPane.addTab("Capture", new JPanel());
tabbedPane.addTab("Effects", new JPanel());
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);
onOpenEffects.accept(this);
}
@@ -50,82 +48,24 @@ public class CameraInternalFrame extends JInternalFrame {
setSize(600, 500);
}
private JPanel createCapturePanel() {
JPanel panel = new JPanel(new GridLayout(2, 1, 10, 10)); // Changed to Grid for better button layout
panel.setBorder(new EmptyBorder(15, 15, 15, 15));
JButton screenshotBtn = new JButton("Take Screenshot");
screenshotBtn.addActionListener(e -> saveSnapshot());
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() {
private void openRecordingFrame() {
RecordingFrame rf = new RecordingFrame(this.getTitle(), cameraPanel);
JDesktopPane desktopPane = getDesktopPane();
if (desktopPane != null) {
desktopPane.add(rf);
rf.setVisible(true);
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);
rf.setSelected(true);
} catch (java.beans.PropertyVetoException veto) {
ShowError.error(null, "Focus Error: " + veto.getMessage(), "Error");
}
}
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 {
File file = new File(System.getProperty("user.home"), "snap_" + System.currentTimeMillis() + ".png");
ImageIO.write(img, "PNG", file);
} catch (Exception ex) {
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; }
@Override
public void dispose() {
// Safety check: stop recording if the window is closed
if (videoRecorder.isRecording()) {
try { videoRecorder.stopRecording(); } catch (IOException ignored) {}
}
captureLoop.stop();
super.dispose();
}

View File

@@ -2,7 +2,6 @@ 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;
@@ -38,29 +37,8 @@ public class DesktopPane extends JDesktopPane {
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

@@ -2,11 +2,18 @@ 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,221 @@
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.Quality;
import io.swtc.recording.cv.RecorderConfig;
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 AVRecorder avRecorder;
private ExportSection exportSection;
private final CameraPanel cameraPanel;
private JButton recordBtn;
private JLabel statusLabel;
private JLabel statsLabel;
private JComboBox<Quality> presetCombo;
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();
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()) {
outputDirectory.mkdirs();
}
}
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 settingsPanel = createSettingsPanel();
JPanel statsPanel = createStatsPanel();
JPanel actionPanel = createActionPanel();
mainContent.add(exportSection);
mainContent.add(Box.createVerticalStrut(10));
mainContent.add(settingsPanel);
mainContent.add(Box.createVerticalStrut(10));
mainContent.add(statsPanel);
mainContent.add(Box.createVerticalStrut(10));
mainContent.add(actionPanel);
getContentPane().add(mainContent);
}
private JPanel createSettingsPanel() {
JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
panel.setBorder(BorderFactory.createTitledBorder("Encoding Settings"));
presetCombo = new JComboBox<>(Quality.values());
presetCombo.setSelectedItem(Quality.SUPERFAST); // Default
panel.add(new JLabel("CPU Preset:"));
panel.add(presetCombo);
// Disable combo box during recording to prevent mid-stream config changes
return panel;
}
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));
panel.add(statusLabel);
panel.add(statsLabel);
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 startRec() {
BufferedImage sample = cameraPanel.getCurrentProcessedImage();
if (sample == null) {
ShowError.warning(this, "No camera feed detected.", "Warning");
return;
}
try {
currentFile = new File(outputDirectory, "vid_" + System.currentTimeMillis() + ".mp4");
Quality selected = (Quality) presetCombo.getSelectedItem();
String preset = (selected != null) ? selected.getFFmpegValue() : "superfast";
RecorderConfig config = new RecorderConfig(
currentFile,
sample.getWidth(),
sample.getHeight(),
20,
18,
preset
);
avRecorder = new AVRecorder(config);
avRecorder.start();
cameraPanel.setExternalRecorder(avRecorder);
startTime = System.currentTimeMillis();
statsTimer.start();
// UI Feedback
presetCombo.setEnabled(false); // Lock settings during recording
recordBtn.setText("Stop Recording");
recordBtn.setForeground(Color.RED);
statusLabel.setText("Recording...");
} catch (Exception ex) {
ShowError.error(this, "Failed to start: " + ex.getMessage(), "Error");
}
}
private void stopRec() {
if (avRecorder != null) {
statusLabel.setText("Finalizing file...");
avRecorder.stop();
cameraPanel.setExternalRecorder(null);
}
statsTimer.stop();
presetCombo.setEnabled(true); // Unlock settings
recordBtn.setText("Start Recording");
recordBtn.setForeground(null);
String fileName = (currentFile != null) ? currentFile.getName() : "N/A";
statusLabel.setText("File saved: " + fileName);
}
private void toggleRecording() {
if (avRecorder == null || !avRecorder.isRecording()) {
startRec();
} else {
stopRec();
}
}
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 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("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,132 @@
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("Storage Settings", createSettingsTab());
tabbedPane.addTab("Evidence-Export", createTransferTab());
add(tabbedPane, BorderLayout.CENTER);
}
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 (Can also be done via Desktop)" +
"</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,33 @@
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
);
}
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,49 @@
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());
/* this is essentially just building FFmpeg? Would've used ProccessBuilder for this lol */
recorder.setVideoOption("pixel_format", "yuv420p");
recorder.setVideoOption("preset", config.preset());
recorder.setVideoOption("crf", String.valueOf(config.crf()));
recorder.setVideoOption("tune", "zerolatency");
recorder.setVideoOption("x264opts", "keyint=40:min-keyint=20");
recorder.setVideoBitrate(0); // 0 tells the recorder to respect CRF strictly
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,25 @@
package io.swtc.recording.cv;
public enum Quality {
ULTRAFAST("Ultrafast (Lowest CPU)"),
SUPERFAST("Superfast"),
VERYFAST("Very Fast"),
FASTER("Faster"),
FAST("Fast"),
MEDIUM("Medium (Best Quality/Size)");
private final String label;
Quality(String label) {
this.label = label;
}
public String getFFmpegValue() {
return this.name().toLowerCase();
}
@Override
public String toString() {
return label;
}
}

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,54 @@
package io.swtc.recording.evidence;
import java.util.concurrent.TimeUnit;
public record ExportStats(
long totalBytes,
long copiedBytes,
long startTimeNanos,
String currentFileName
) {
public int percent() {
if (totalBytes <= 0) return 0;
return (int) Math.min(100, (copiedBytes * 100) / totalBytes);
}
public String timeLeft() {
if (copiedBytes <= 0) return "Calculating...";
long elapsedNanos = System.nanoTime() - startTimeNanos;
if (elapsedNanos <= 0) return "Calculating...";
// Bytes per nanosecond
double bps = (double) copiedBytes / elapsedNanos;
long remainingBytes = totalBytes - copiedBytes;
if (remainingBytes <= 0) return "Done";
long remainingNanos = (long) (remainingBytes / bps);
return formatDuration(remainingNanos);
}
public String getSpeedMBps() {
long elapsedNanos = System.nanoTime() - startTimeNanos;
if (elapsedNanos <= 0 || copiedBytes <= 0) return "0 MB/s";
double seconds = elapsedNanos / 1_000_000_000.0;
double megabytes = copiedBytes / (1024.0 * 1024.0);
return String.format("%.1f MB/s", megabytes / seconds);
}
private static String formatDuration(long nanos) {
long totalSeconds = TimeUnit.NANOSECONDS.toSeconds(nanos);
if (totalSeconds < 1) return "less than a sec";
long mins = totalSeconds / 60;
long secs = totalSeconds % 60;
if (mins > 0) {
return String.format("%dm %ds", mins, secs);
}
return secs + "s";
}
}

View File

@@ -0,0 +1,87 @@
package io.swtc.recording.evidence;
import java.io.IOException;
import java.nio.file.*;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import java.util.function.Consumer;
public class USBExportManager {
public static void exportAsync(Path source, Path target, Consumer<ExportStats> progress, Runnable onDone, Consumer<Throwable> onError) {
CompletableFuture.runAsync(() -> {
try {
performExport(source, target, progress);
onDone.run();
} catch (Exception e) {
onError.accept(e);
}
});
}
private static void performExport(Path source, Path target, Consumer<ExportStats> progress) throws IOException {
List<Path> allFiles;
try (Stream<Path> stream = Files.walk(source)) {
allFiles = stream.filter(Files::isRegularFile).toList();
}
long totalBytes = allFiles.stream().mapToLong(p -> p.toFile().length()).sum();
if (!hasEnoughSpace(target, totalBytes)) {
throw new IOException("Not enough space on target drive. Required: " + (totalBytes / 1024 / 1024) + " MB");
}
long totalCopied = 0;
long startTimeNanos = System.nanoTime();
for (Path file : allFiles) {
String fileName = file.getFileName().toString();
if (progress != null) {
progress.accept(new ExportStats(totalBytes, totalCopied, startTimeNanos, fileName));
}
Path relativePath = source.relativize(file);
Path destination = target.resolve(relativePath);
Files.createDirectories(destination.getParent());
Files.copy(file, destination, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
totalCopied += Files.size(file);
if (progress != null) {
progress.accept(new ExportStats(totalBytes, totalCopied, startTimeNanos, fileName));
}
}
}
public static boolean hasEnoughSpace(Path target, long requiredBytes) throws IOException {
Path root = target;
while (root != null && !Files.exists(root)) {
root = root.getParent();
}
if (root == null) return true;
FileStore store = Files.getFileStore(root);
return store.getUsableSpace() >= requiredBytes;
}
public static long calculateDirectorySize(Path dir) throws IOException {
if (dir == null || !Files.exists(dir)) return 0L;
try (Stream<Path> walk = Files.walk(dir)) {
return walk.filter(Files::isRegularFile)
.mapToLong(p -> {
try {
return Files.size(p);
} catch (IOException e) {
return 0L; // Skip files that can't be read
}
})
.sum();
}
}
}

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB