22 Commits

Author SHA1 Message Date
e4d91806d9 Merge pull request 'stabilisation' (#2) from stabilisation into main
Reviewed-on: #2

this is fine ....

<img src="https://c.tenor.com/MYZgsN2TDJAAAAAd/tenor.gif"></img>
2026-02-19 14:14:22 +00:00
57ee4d9a92 New Desktop Icons
Some new Functionality to record more than 1 camera using the desktop icon.

open local files using desktop,
open profiler (nothing changed)

Signed-off-by: rattatwinko <seppmutterman@gmail.com>
2026-02-09 12:23:37 +01:00
e225d8f0bc Added Profiler
+ Some Fixes
+ Profiler.java

new readme

Signed-off-by: rattatwinko <seppmutterman@gmail.com>
2026-02-08 17:00:05 +01:00
701d95ab2d fix for icon change. e1003c20ff
Signed-off-by: rattatwinko <seppmutterman@gmail.com>
2026-02-02 12:31:02 +01:00
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
b49cc8b2f0 refactors 2026-01-19 18:33:42 +01:00
565a4f3cf3 fixed some functionallity added back some which was removed cause of refactoring ...
some new refactoring to make the UI code cleaner

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

---
rattatwinko
2026-01-14 20:55:45 +01:00
ccc3d264f7 a new windowing system!
add:
+ CameraPanel ; To make life easier for coders
+ SwingIFrame ; Which is now our main UI component
+ VideoRecorder ; A helper Class for SwingIFrame to record cameras!

changed:
/ SwingCCTVManager ; changes for the new UI Component

deprecation:
/-/ AutoGainProcessor ; cause it isnt needed anymore, back then this was needed cause we opened the webcams manually (color wise)

---
rattatwinko
2026-01-13 20:52:32 +01:00
b767ba27b3 JOptionPane for Errors, general refactoring, deprecated class removed (CameraRenderer) moved into Swing now
modified:
/ SwingCCTVManager ; Error Pane, refactoring
/ WebcamCaptureLoop ; cleanup method for closing cameras reliably, and some MessageDialogs for error handling (just fails lol)
/ SwingCameraWindow ; refactor some legacy code into a "modern" lambda function , Message Dialog for error handling , and some g2d stuff (paintComponent)

removed:
- CameraRenderer.java ; Deprecated Component, was used for SWT GL Surfaces. We dont do that now!

---

rattatwinko
2026-01-13 17:33:47 +01:00
61 changed files with 3525 additions and 485 deletions

5
.gitignore vendored
View File

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

129
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,35 @@
<version>2.20.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.jcodec/jcodec -->
<!--
Saving into Files
-->
<dependency>
<groupId>org.jcodec</groupId>
<artifactId>jcodec</artifactId>
<version>0.2.5</version>
</dependency>
<!-- &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.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>
<!-- https://mvnrepository.com/artifact/org.jcodec/jcodec-javase -->
<dependency>
<groupId>org.jcodec</groupId>
<artifactId>jcodec-javase</artifactId>
<version>0.2.5</version>
</dependency>
</dependencies>
</project>

View File

@@ -1,26 +1,47 @@
# SWT-CCTV
# SWT-CCTV (Simple Watch Tool)
A rather simple CCTV software which operates with Java.
If you want to build this project on yourself, you will need IntelliJ (or any other IDE) and Maven!
If you are looking for a desktop like experience this is the software, it has its own windowing system!
## Downloads:
If you are looking for downloads then you are in luck! Currently there are Windows Executables and portable Jar Files in place!
Take a look at the [releases](https://rattatwinko.servecounterstrike.com/gitea/rattatwinko/swt-cctv/releases) page for the newest software releases!
[Releases Page](https://rattatwinko.servecounterstrike.com/gitea/rattatwinko/swt-cctv/releases)
## Dependencies:
- Webcam by Sarxos
- Swing (AWT)
- _lwjgl (with opengl)_ → This is important for our goals of rendering on the GPU.
- junit for testing stuff
- jcodec, in the future we will be recording using this
- Jackson (fasterxml) → serializing the config for network cams
- JavaCV
- FFmpeg
### Future Plans:
They arent too big, i want one thing more and that is some more utilities in the camera window.
Implement some more JavaCV cause of performance.
Protable Jar which can be run with JRE 17 (already done but not too good!)
## Requirements:
- JRE/JDK 1.8.00 - 25 ([Reccomended](https://adoptium.net/de/download?link=https%3A%2F%2Fgithub.com%2Fadoptium%2Ftemurin17-binaries%2Freleases%2Fdownload%2Fjdk-17.0.17%252B10%2FOpenJDK17U-jre_x64_windows_hotspot_17.0.17_10.msi&vendor=Adoptium))
| System Requirements | Minimum Requirements | Reccomended Requirements |
|--------------------- |---------------------------------------------------------------------------------------------------------------------------------------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **_CPU_** | [Intel(R) Celeron(R) CPU G550 @ 2.60GHz](https://www.techpowerup.com/cpu-specs/celeron-g550.c1339) **_Or any Dual Core CPU_** | [Intel® Core™ i5-3470](https://www.intel.de/content/www/de/de/products/sku/68316/intel-core-i53470-processor-6m-cache-up-to-3-60-ghz/specifications.html) Or any **_Quad (or more) Core CPU_** |
| **_RAM_** | **2GB DDR3** | **4/8GB DDR3/4/5** (You can have **_more_** than this, _Java likes it_) |
| **_JRE_** | Java Runtime Enviroment: [1.8.000](https://javadl.oracle.com/webapps/download/AutoDL?BundleId=252905_0d06828d282343ea81775b28020a7cd3) | Java Runtime Enviroment _(or JDK)_: [17](https://adoptium.net/download?link=https%3A%2F%2Fgithub.com%2Fadoptium%2Ftemurin17-binaries%2Freleases%2Fdownload%2Fjdk-17.0.17%252B10%2FOpenJDK17U-jre_x64_windows_hotspot_17.0.17_10.msi&vendor=Adoptium) |
| **_Disk Space_** | **_100Mb of HDD/SSD_** Space for the Program (currently **40.3Mb**) ; _For Recording more is needed_ | _100Mb SSD Space_ ; For Recording more than 100Mb , this depends on how many cameras you have |
**Note: This was tested on these CPU'S!**
### Future Plans:
- [x] basic network cam interfacing
- [ ] better multiplexer (or whatever the viewport is called in cctv)
- [x] better multiplexer (or whatever the viewport is called in cctv)
- [x] Protable .jar which can be run with **JRE 17**
- [ / ] Performance stabilisation (currently it is in place, and this app can be run on shitty hardware, but it can be better (current low is a celeron g550))
- [ ] JavaCV integration for cameras
### Author(s):

View File

@@ -1,11 +1,14 @@
package io.swtc;
import javax.swing.*;
public class Main {
public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println("Arg " + i + ": " + args[i]);
}
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) { /* Do nothing */ }
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());
@@ -31,168 +43,237 @@ public class SwingCCTVManager {
private final JFrame frame;
private final JTable deviceTable;
private final DefaultTableModel tableModel;
private Timer autoRefreshTimer;
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());
String[] columns = {"Status", "Device Name", "Type", "Resolution", "Address"};
tableModel = new DefaultTableModel(columns, 0) {
@Override public boolean isCellEditable(int r, int c) { return false; }
this.IFrame = new SwingIFrame();
this.IFrame.show();
tableModel = new DefaultTableModel(new String[]{"Status", "Device Name", "Type", "Resolution", "Address"}, 0) {
@Override
public boolean isCellEditable(int r, int c) { return false; }
};
deviceTable = new JTable(tableModel);
setupTableAppearance();
deviceTable.addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
if (e.getClickCount() == 2 && deviceTable.getSelectedRow() != -1) {
launchSelected();
}
if (SwingUtilities.isRightMouseButton(e)) {
showContextMenu(e);
}
}
});
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); // Status column
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)));
}
// Custom Renderer for Status Colors
toolBar.add(new JLabel("SWT-CCTV"));
toolBar.add(Box.createRigidArea(new Dimension(10, 0)));
toolBar.addSeparator(new Dimension(10, 32));
JButton btnAdd = new JButton("Add IP Cam");
JButton btnImport = new JButton("Import Config"); // New Import Button
JButton btnLaunch = new JButton("Launch Stream");
btnAdd.setPreferredSize(new Dimension(100, 32));
btnImport.setPreferredSize(new Dimension(110, 32));
btnLaunch.setPreferredSize(new Dimension(120, 32));
toolBar.add(Box.createRigidArea(new Dimension(5, 0)));
toolBar.add(btnAdd);
toolBar.add(btnImport);
toolBar.addSeparator();
toolBar.add(btnLaunch);
btnAdd.addActionListener(e -> showAddCameraDialog());
btnImport.addActionListener(e -> showImportDialog());
btnLaunch.addActionListener(e -> launchSelected());
}
private void setupDragAndDrop() {
deviceTable.setDragEnabled(true);
deviceTable.setTransferHandler(new TransferHandler() {
@Override
public boolean canImport(TransferSupport support) {
return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor);
}
@Override
public boolean importData(TransferSupport support) {
if (!canImport(support)) return false;
try {
@SuppressWarnings("unchecked")
List<File> files = (List<File>) support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
for (File file : files) {
if (file.getName().endsWith(".json")) {
List<CameraConfig> configs = CameraSettings.loadFromFile(file);
importCameras(configs);
}
}
return true;
} catch (Exception e) {
return false;
}
}
});
}
private void showImportDialog() {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("Select Camera Configuration JSON");
if (chooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
List<CameraConfig> configs = CameraSettings.loadFromFile(file);
importCameras(configs);
}
}
private void 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);
if ("ONLINE".equals(v)) comp.setForeground(new Color(0, 150, 0));
else comp.setForeground(Color.RED);
setHorizontalAlignment(JLabel.CENTER);
comp.setForeground(STATUS_ONLINE.equals(v) ? new Color(0, 150, 0) : Color.RED);
return comp;
}
});
}
private void startAutoRefresh() {
autoRefreshTimer = new Timer(5000, e -> refreshTable());
autoRefreshTimer.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"});
}
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();
}
private void refreshTable() {
int selectedRow = deviceTable.getSelectedRow();
tableModel.setRowCount(0);
List<Webcam> webcams = Webcam.getWebcams();
for (Webcam w : webcams) {
boolean isIp = w.getDevice().getClass().getSimpleName().contains("IpCam");
String status = w.getDevice() != null ? "ONLINE" : "OFFLINE";
tableModel.addRow(new Object[]{
status,
w.getName(),
isIp ? "IP Stream" : "USB Hardware",
w.getViewSize().width + "x" + w.getViewSize().height,
isIp ? "Network" : "Local"
});
}
if (selectedRow != -1 && selectedRow < tableModel.getRowCount()) {
deviceTable.setRowSelectionInterval(selectedRow, selectedRow);
}
}
private void showContextMenu(MouseEvent e) {
int row = deviceTable.rowAtPoint(e.getPoint());
deviceTable.setRowSelectionInterval(row, row);
JPopupMenu menu = new JPopupMenu();
JMenuItem launch = new JMenuItem("Launch Live Stream");
JMenuItem delete = new JMenuItem("Remove Device");
launch.addActionListener(al -> launchSelected());
delete.addActionListener(al -> deleteSelected());
menu.add(launch);
menu.addSeparator();
menu.add(delete);
menu.show(deviceTable, e.getX(), e.getY());
private Object[] probeIpCamera(IpCamDevice ipDevice) {
String status = STATUS_OFFLINE; String resText = "N/A";
try {
URL url = ipDevice.getURL();
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(url.getHost(), url.getPort() != -1 ? url.getPort() : 80), 800);
Dimension d = ipDevice.getResolution();
if (d != null) { status = STATUS_ONLINE; resText = d.width + "x" + d.height; }
}
} catch (Exception e) { status = STATUS_OFFLINE; }
return new Object[]{status, ipDevice.getName(), "IP Stream", resText, "Network"};
}
private void launchSelected() {
int row = deviceTable.getSelectedRow();
if (row == -1) return;
String name = (String) tableModel.getValueAt(row, 1);
Webcam selected = Webcam.getWebcams().stream()
.filter(w -> w.getName().equals(name))
.findFirst().orElse(null);
if (selected != null) {
new Thread(() -> new SwingCameraWindow(selected).open()).start();
}
}
private void deleteSelected() {
int row = deviceTable.getSelectedRow();
String name = (String) tableModel.getValueAt(row, 1);
if (name.toLowerCase().contains("usb")) return;
CameraSettings.delete(name);
IpCamDeviceRegistry.unregister(name);
refreshTable();
}
private static void loadSavedCameras() {
for (CameraConfig config : CameraSettings.load()) {
try {
IpCamDeviceRegistry.register(config.getName(), config.getUrl(), IpCamMode.PUSH);
} catch (MalformedURLException e) { e.printStackTrace(); }
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,82 +0,0 @@
package io.swtc;
import com.github.sarxos.webcam.Webcam;
import io.swtc.proccessing.WebcamCaptureLoop;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
public class SwingCameraWindow {
private final JFrame frame;
private final CameraPanel cameraPanel;
private final WebcamCaptureLoop captureLoop;
public SwingCameraWindow(Webcam webcam) {
this.frame = new JFrame("scctv@" + webcam.getName());
this.frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
this.frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
// clean shit up
frame.dispose();
}
});
this.cameraPanel = new CameraPanel();
this.frame.add(cameraPanel);
this.frame.pack();
this.frame.setSize(640, 480);
this.captureLoop = new WebcamCaptureLoop(webcam, (BufferedImage img) -> {
SwingUtilities.invokeLater(() -> {
cameraPanel.setImage(img);
});
});
this.frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
captureLoop.stop();
}
});
}
public void open() {
frame.setVisible(true);
captureLoop.start();
}
private static class CameraPanel extends JPanel {
private BufferedImage currentImage;
public void setImage(BufferedImage img) {
this.currentImage = img;
this.repaint(); // Triggers paintComponent
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (currentImage != null) {
// Draw the image scaled to the panel size
g.drawImage(currentImage, 0, 0, getWidth(), getHeight(), null);
}
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
// init in edt
Webcam webcam = Webcam.getDefault();
if (webcam != null) {
SwingCameraWindow window = new SwingCameraWindow(webcam);
window.open();
} else {
System.err.println("No webcam found!");
}
});
}
}

View File

@@ -0,0 +1,317 @@
package io.swtc;
import com.github.sarxos.webcam.Webcam;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.ShowError;
import io.swtc.proccessing.ui.desktop.DIM;
import io.swtc.proccessing.ui.desktop.debug.Profiler;
import io.swtc.proccessing.ui.desktop.evidence.EvidenceExportFrame;
import io.swtc.proccessing.ui.desktop.recording.MultiRecordingFrame;
import io.swtc.proccessing.ui.iframe.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import static java.awt.SystemColor.desktop;
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;
private Rectangle windowedBounds;
private boolean blackbg = false;
private final Color bgcolor = Color.decode("#336B6A");
private final Color defDesktopBg = Color.WHITE;
private final JPopupMenu popupMenu = new JPopupMenu();
public SwingIFrame() {
mainFrame = new JFrame("Viewer");
mainFrame.setSize(1280, 720);
mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
mainFrame.setIconImage(IconSetter.getIcon());
desktopPane = new DesktopPane(cameraToEffects);
desktopPane.setBackground(defDesktopBg);
mainFrame.add(desktopPane, BorderLayout.CENTER);
desktopIconManager = new DIM(desktopPane);
setupDesktopExportFrame();
setupRecordingFrame();
setupFileEx();
setupProfiler();
setupFullscreenToggle();
setupBlackBg();
initPopupMenu();
desktopPane.addMouseListener(popupListener());
}
private void setupDesktopExportFrame() {
desktopIconManager.addIcon(
"Export Evidence",
IconSetter.getSaveIconAsImageIcon(),
/* e1003c20ff00c637d963ce21fd685fed6460602a: Fix to icon, need to pass parent! Or Override method which is dumb */
() -> EvidenceExportFrame.showExport(mainFrame)
);
}
private void setupRecordingFrame() {
desktopIconManager.addIcon(
"Record Batch",
IconSetter.getCamerarec_ImageIcon(),
() -> {
MultiRecordingFrame mrf = new MultiRecordingFrame();
desktopPane.add(mrf);
mrf.show();
try {
mrf.setSelected(true);
} catch (java.beans.PropertyVetoException e) {
ShowError.error(null,"Exception", "" + e.getStackTrace());
}
}
);
}
private void setupFileEx() {
desktopIconManager.addIcon(
"Open Recordings",
IconSetter.getExplorerIcon(),
() -> {
String userHome = System.getProperty("user.home");
File folder = new File(userHome,"Videos/swtcctv-rec");
if (Desktop.isDesktopSupported() && folder.exists()) {
try {
Desktop.getDesktop().open(folder);
} catch (IOException e) {
ShowError.error(null,
"Failed to open Folder",
"Failed" + e.getMessage()
);
}
}
}
);
}
private void setupProfiler() {
desktopIconManager.addIcon(
"Profiler",
IconSetter.getDbg_icon(),
() -> {
SwingUtilities.invokeLater(() -> {
Profiler.showFrame(new Profiler(mainFrame));
});
});
}
public void addCameraInternalFrame(Webcam webcam) {
CameraInternalFrame cameraFrame = new CameraInternalFrame(webcam, this::handleEffectsRequest);
EffectsPanelFrame effectsFrame = new EffectsPanelFrame(
"Effects - " + webcam.getName(),
cameraFrame.getCameraPanel()
);
cameraToEffects.put(cameraFrame, effectsFrame);
int offset = desktopPane.getAllFrames().length * 15;
cameraFrame.setLocation(50 + offset, 50 + offset);
effectsFrame.setLocation(700 + offset, 50 + offset);
effectsFrame.setVisible(false);
cameraFrame.addInternalFrameListener(new javax.swing.event.InternalFrameAdapter() {
@Override
public void internalFrameClosing(javax.swing.event.InternalFrameEvent e) {
EffectsPanelFrame ef = cameraToEffects.remove(cameraFrame);
if (ef != null) ef.dispose();
desktopPane.forgetFrame(cameraFrame);
cameraFrame.dispose();
}
});
desktopPane.add(cameraFrame);
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);
}
private void handleEffectsRequest(CameraInternalFrame source) {
EffectsPanelFrame effectsFrame = cameraToEffects.get(source);
if (effectsFrame != null) {
effectsFrame.setVisible(true);
try {
effectsFrame.setSelected(true);
effectsFrame.toFront();
} catch (java.beans.PropertyVetoException ex) {
JOptionPane.showMessageDialog(null, "Focus Error: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
}
private MouseAdapter popupListener() {
return new MouseAdapter() {
private void showPopup(MouseEvent e) {
if (e.isPopupTrigger()) { // cross-platform trigger
popupMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
@Override public void mousePressed(MouseEvent e) { showPopup(e); }
@Override public void mouseReleased(MouseEvent e) { showPopup(e); }
};
}
private void initPopupMenu() {
popupMenu.removeAll(); // clean slate
JCheckBoxMenuItem fullscreenItem = new JCheckBoxMenuItem("Fullscreen");
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() {
mainFrame.setLocationRelativeTo(null);
mainFrame.setVisible(true);
}
}

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

View File

@@ -0,0 +1,294 @@
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;
public class CameraPanel extends JPanel {
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.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;
if (sourceImage != null) {
updateProcessedImage();
scheduleRepaint();
}
}
public BufferedImage getCurrentProcessedImage() {
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

@@ -1,96 +0,0 @@
package io.swtc.proccessing;
import org.eclipse.swt.opengl.GLCanvas;
import org.eclipse.swt.widgets.Composite;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL11;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class CameraRenderer {
private final GLCanvas canvas;
private int textureId = -1;
private ByteBuffer pixelBuffer;
private final AutoGainProcessor gainProcessor;
public CameraRenderer(Composite parent, org.eclipse.swt.opengl.GLData data) {
this.canvas = new GLCanvas(parent, 0, data);
this.gainProcessor = new AutoGainProcessor();
// Initialize OpenGL context immediately
this.canvas.setCurrent();
GL.createCapabilities();
initGL();
}
public GLCanvas getCanvas() {
return canvas;
}
private void initGL() {
GL11.glEnable(GL11.GL_TEXTURE_2D);
textureId = GL11.glGenTextures();
}
public void render(BufferedImage img) {
if (canvas.isDisposed()) return;
canvas.setCurrent();
int width = img.getWidth();
int height = img.getHeight();
int[] rgbArray = new int[width * height];
// this is hellishly unefficcient but who cares.
img.getRGB(0, 0, width, height, rgbArray, 0, width);
float[] gains = gainProcessor.calculateAutoGains(rgbArray);
int bufferSize = width * height * 3;
if (pixelBuffer == null || pixelBuffer.capacity() < bufferSize) {
pixelBuffer = ByteBuffer.allocateDirect(bufferSize);
pixelBuffer.order(ByteOrder.nativeOrder());
}
pixelBuffer.clear();
for (int pixel : rgbArray) {
int r = (pixel >> 16) & 0xFF;
int g = (pixel >> 8) & 0xFF;
int b = pixel & 0xFF;
r = Math.min(255, (int)(r * gains[0]));
g = Math.min(255, (int)(g * gains[1]));
b = Math.min(255, (int)(b * gains[2]));
pixelBuffer.put((byte) r);
pixelBuffer.put((byte) g);
pixelBuffer.put((byte) b);
}
pixelBuffer.flip();
// this is just gl stuff
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
GL11.glViewport(0, 0, canvas.getClientArea().width, canvas.getClientArea().height);
GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId);
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, width, height,
0, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, pixelBuffer);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
GL11.glColor3f(1.0f, 1.0f, 1.0f);
GL11.glBegin(GL11.GL_QUADS);
GL11.glTexCoord2f(0, 0); GL11.glVertex2f(-1, 1);
GL11.glTexCoord2f(1, 0); GL11.glVertex2f(1, 1);
GL11.glTexCoord2f(1, 1); GL11.glVertex2f(1, -1);
GL11.glTexCoord2f(0, 1); GL11.glVertex2f(-1, -1);
GL11.glEnd();
canvas.swapBuffers();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,115 @@
package io.swtc.proccessing.ui;
import javax.swing.*;
import java.awt.*;
import java.net.URL;
import java.util.Objects;
/* vital boilerplate class, shouldve made it better but idk. */
public class IconSetter {
private static Image ICON_IMAGE;
private static ImageIcon ICON_ICON;
private static Image effects_icon;
private static ImageIcon dbg_icon;
private static Image camerarec;
private static ImageIcon camerarec_imgico;
private static ImageIcon expIcon;
/* this is used for the app icon itself (the one in tb) */
public static Image getIcon() {
if (ICON_IMAGE == null) {
URL url = IconSetter.class.getResource("/icons/artwork.png");
if (Objects.isNull(url)) {
ShowError.error(null,"Icon","Icon (Type: Image) failed, NULL!");
throw new RuntimeException("NULL!");
}
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,"Icon","Effects icon was Null! (Type Image)");
throw new RuntimeException("NULL!");
}
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 (Objects.isNull(url)) {
ShowError.error(null,"Icon","Icon not found!, NULL! (Type ImageIcon)");
throw new RuntimeException("NULL!");
}
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)) {
ShowError.error(null,"Icon","getSaveIconAsImageIcon failed, NULL! (Type ImageIcon)");
throw new RuntimeException("NULL!");
}
ICON_ICON = new ImageIcon(url);
}
return ICON_ICON;
}
public static ImageIcon getDbg_icon() {
if (Objects.isNull(dbg_icon)) {
URL url = IconSetter.class.getResource("/icons/icondbg-7.png");
if (Objects.isNull(url)) {
ShowError.error(null, "Icon", "getDbg_icon, object url was null (Type ImageIcon)");
throw new RuntimeException("NULL!");
}
dbg_icon = new ImageIcon(url);
}
return dbg_icon;
}
public static Image getCamerarec_img() {
if (Objects.isNull(camerarec)) {
URL url = IconSetter.class.getResource("/icons/rec.png");
if (Objects.isNull(url)) {
ShowError.error(null,"icon","recicon was null Type Image");
throw new RuntimeException("NULL!");
}
camerarec = Toolkit.getDefaultToolkit().getImage(url);
}
return camerarec;
}
public static ImageIcon getCamerarec_ImageIcon() {
if (Objects.isNull(camerarec_imgico)) {
URL url = IconSetter.class.getResource("/icons/rec.png");
if (Objects.isNull(url)) {
ShowError.error(null,"icon","getCamerarec_ImageIcon failed, Type Image");
throw new RuntimeException("NULL!");
}
camerarec_imgico = new ImageIcon(url);
}
return camerarec_imgico;
}
public static ImageIcon getExplorerIcon() {
if (Objects.isNull(expIcon)) {
URL url = IconSetter.class.getResource("/icons/explorer.png");
if (Objects.isNull(url)) {
ShowError.error(null,"icon","getExplorerIcon failed, Type Image");
throw new RuntimeException("NULL!");
}
expIcon = new ImageIcon(url);
}
return expIcon;
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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,102 @@
package io.swtc.proccessing.ui.desktop;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.RoundRectangle2D;
public class DesktopIcon extends JPanel {
private static final int ICON_SIZE = 64;
private static final int TOTAL_WIDTH = 100;
private static final int TOTAL_HEIGHT = 85;
private boolean hovered = false;
public DesktopIcon(String label, Icon icon, Runnable action) {
setLayout(new BorderLayout(0, 2));
setOpaque(false);
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
Dimension fixedSize = new Dimension(TOTAL_WIDTH, TOTAL_HEIGHT);
setPreferredSize(fixedSize);
setMinimumSize(fixedSize);
setMaximumSize(fixedSize);
setBorder(new EmptyBorder(4, 4, 4, 4));
Icon scaledIcon = (icon instanceof ImageIcon)
? new SmoothIcon(((ImageIcon) icon).getImage(), ICON_SIZE, ICON_SIZE)
: icon;
JLabel iconLabel = new JLabel(scaledIcon, SwingConstants.CENTER);
iconLabel.setVerticalAlignment(SwingConstants.BOTTOM);
JLabel textLabel = new ShadowLabel(label);
textLabel.setHorizontalAlignment(SwingConstants.CENTER);
textLabel.setVerticalAlignment(SwingConstants.TOP);
add(iconLabel, BorderLayout.CENTER);
add(textLabel, BorderLayout.SOUTH);
setupMouseListeners(action);
}
private void setupMouseListeners(Runnable action) {
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
protected void paintComponent(Graphics g) {
if (hovered) {
paintHoverEffect(g);
}
super.paintComponent(g);
}
private void paintHoverEffect(Graphics g) {
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, 20) : new Color(255, 255, 255, 30);
Color border = lightBg ? new Color(0, 0, 0, 50) : new Color(255, 255, 255, 70);
g2.setColor(fill);
g2.fill(new RoundRectangle2D.Float(2, 2, getWidth() - 4, getHeight() - 4, 10, 10));
g2.setColor(border);
g2.draw(new RoundRectangle2D.Float(2, 2, getWidth() - 4, getHeight() - 4, 10, 10));
g2.dispose();
}
public boolean isBackgroundLight() {
Container p = getParent();
Color bg = (p != null) ? p.getBackground() : Color.WHITE;
double luminance = (0.299 * bg.getRed() + 0.587 * bg.getGreen() + 0.114 * bg.getBlue()) / 255;
return luminance > 0.6;
}
}

View File

@@ -0,0 +1,48 @@
package io.swtc.proccessing.ui.desktop;
import javax.swing.*;
import java.awt.*;
public class ShadowLabel extends JLabel {
public ShadowLabel(String text) {
super(text);
setForeground(Color.WHITE);
}
@Override
protected void paintComponent(Graphics g) {
String text = getText();
if (text == null || text.isEmpty()) return;
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
FontMetrics fm = g2.getFontMetrics();
int availableWidth = getWidth();
String drawText = text;
if (fm.stringWidth(text) > availableWidth) {
for (int i = text.length(); i > 0; i--) {
String temp = text.substring(0, i) + "...";
if (fm.stringWidth(temp) <= availableWidth) {
drawText = temp;
break;
}
}
}
DesktopIcon parent = (DesktopIcon) getParent();
boolean isLightBg = (parent != null) && parent.isBackgroundLight();
Color textColor = isLightBg ? Color.BLACK : Color.WHITE;
Color shadowColor = isLightBg ? new Color(255, 255, 255, 200) : new Color(0, 0, 0, 180);
int x = (availableWidth - fm.stringWidth(drawText)) / 2;
int y = fm.getAscent();
g2.setColor(shadowColor);
g2.drawString(drawText, x + 1, y + 1);
g2.setColor(textColor);
g2.drawString(drawText, x, y);
}
}

View File

@@ -0,0 +1,28 @@
package io.swtc.proccessing.ui.desktop;
import javax.swing.*;
import java.awt.*;
public record SmoothIcon(Image image, int width, int height) implements Icon {
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.drawImage(image, x, y, width, height, null);
g2.dispose();
}
@Override
public int getIconWidth() {
return width;
}
@Override
public int getIconHeight() {
return height;
}
}

View File

@@ -0,0 +1,169 @@
package io.swtc.proccessing.ui.desktop.debug;
import io.swtc.proccessing.ui.IconSetter;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.lang.management.*;
import java.util.List;
/* simple profiler to see memory usage, this isnt too important but certainly useful */
public class Profiler extends JFrame {
private final MemoryMXBean memoryMXBean =
ManagementFactory.getMemoryMXBean();
private final ThreadMXBean threadMXBean =
ManagementFactory.getThreadMXBean();
private final List<MemoryPoolMXBean> pools =
ManagementFactory.getMemoryPoolMXBeans();
private final List<GarbageCollectorMXBean> gcs =
ManagementFactory.getGarbageCollectorMXBeans();
private final List<BufferPoolMXBean> buffers =
ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class);
private final JLabel heapLabel = new JLabel();
private final JLabel nonHeapLabel = new JLabel();
private final JLabel threadLabel = new JLabel();
private final DefaultTableModel poolModel =
new DefaultTableModel(
new String[]{"Pool", "Type", "Used (MB)", "Committed (MB)", "Max (MB)"},
0
);
private final DefaultTableModel gcModel =
new DefaultTableModel(
new String[]{"GC", "Collections", "Time (ms)"},
0
);
private final DefaultTableModel bufferModel =
new DefaultTableModel(
new String[]{"Buffer", "Used (MB)", "Count"},
0
);
public Profiler(JFrame parent) {
setTitle("Profiler");
setSize(750, 400);
setLocationRelativeTo(parent);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
ImageIcon ico = IconSetter.getDbg_icon();
setIconImage(ico.getImage());
JPanel root = new JPanel(new BorderLayout(10, 10));
root.setBorder(new EmptyBorder(10, 10, 10, 10));
setContentPane(root);
JPanel summary = new JPanel();
summary.setLayout(new BoxLayout(summary, BoxLayout.Y_AXIS));
summary.add(heapLabel);
summary.add(nonHeapLabel);
summary.add(threadLabel);
root.add(summary, BorderLayout.NORTH);
JTabbedPane tabs = new JTabbedPane();
tabs.add("Memory Pools", new JScrollPane(new JTable(poolModel)));
tabs.add("GC", new JScrollPane(new JTable(gcModel)));
tabs.add("Buffers", new JScrollPane(new JTable(bufferModel)));
root.add(tabs, BorderLayout.CENTER);
Timer timer = new Timer(1000, e -> update());
timer.start();
update();
}
private void update() {
updateSummary();
updatePools();
updateGC();
updateBuffers();
}
private void updateSummary() {
MemoryUsage heap = memoryMXBean.getHeapMemoryUsage();
MemoryUsage nonHeap = memoryMXBean.getNonHeapMemoryUsage();
heapLabel.setText(String.format(
"Heap: used %d MB / committed %d MB / max %d MB",
mb(heap.getUsed()),
mb(heap.getCommitted()),
mb(heap.getMax())
));
nonHeapLabel.setText(String.format(
"Non-Heap: used %d MB / committed %d MB",
mb(nonHeap.getUsed()),
mb(nonHeap.getCommitted())
));
int threads = threadMXBean.getThreadCount();
int peak = threadMXBean.getPeakThreadCount();
int daemons = threadMXBean.getDaemonThreadCount();
threadLabel.setText(String.format(
"Threads: %d live (%d daemon, peak %d)",
threads, daemons, peak
));
}
private void updatePools() {
poolModel.setRowCount(0);
for (MemoryPoolMXBean pool : pools) {
MemoryUsage u = pool.getUsage();
if (u == null) continue;
poolModel.addRow(new Object[]{
pool.getName(),
pool.getType(),
mb(u.getUsed()),
mb(u.getCommitted()),
mb(u.getMax())
});
}
}
private void updateGC() {
gcModel.setRowCount(0);
for (GarbageCollectorMXBean gc : gcs) {
gcModel.addRow(new Object[]{
gc.getName(),
gc.getCollectionCount(),
gc.getCollectionTime()
});
}
}
private void updateBuffers() {
bufferModel.setRowCount(0);
for (BufferPoolMXBean b : buffers) {
bufferModel.addRow(new Object[]{
b.getName(),
mb(b.getMemoryUsed()),
b.getCount()
});
}
}
/* Conversion logic for byte -> mb */
public long mb(long bytes) {
return bytes < 0 ? -1 : bytes / 1024 / 1024;
}
public static void showFrame(JFrame parent) {
SwingUtilities.invokeLater(() ->
new Profiler(parent).setVisible(true)
);
}
}

View File

@@ -0,0 +1,119 @@
package io.swtc.proccessing.ui.desktop.evidence;
import io.swtc.proccessing.ui.IconSetter;
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 final JFrame parent; /* neccessary to get icon working, inheritance is a bitch */
private EvidenceExportFrame(Path sourceDir, Path usbTargetDir, JFrame parent) {
this.parent = parent;
setTitle("Export");
setSize(400, 220);
setLocationRelativeTo(this.parent);
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));
ImageIcon ico = IconSetter.getSaveIconAsImageIcon();
this.setIconImage(ico.getImage());
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.parent,
"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(JFrame parent) {
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(parent) == JFileChooser.APPROVE_OPTION) {
Path target = chooser.getSelectedFile().toPath().resolve("swtcctv-rec_" + System.currentTimeMillis() / 1000);
new EvidenceExportFrame(videoDir.toPath(), target, parent);
}
});
}
}

View File

@@ -0,0 +1,13 @@
package io.swtc.proccessing.ui.desktop.recording;
import io.swtc.proccessing.ui.iframe.CameraInternalFrame;
public class CameraCheckItem {
private final CameraInternalFrame frame;
private boolean selected = true;
public CameraCheckItem(CameraInternalFrame frame) { this.frame = frame; }
public CameraInternalFrame getFrame() { return frame; }
public boolean isSelected() { return selected; }
public void setSelected(boolean s) { this.selected = s; }
@Override public String toString() { return frame.getTitle(); }
}

View File

@@ -0,0 +1,17 @@
package io.swtc.proccessing.ui.desktop.recording;
import javax.swing.*;
import java.awt.*;
public class CheckBoxListRenderer extends JCheckBox implements ListCellRenderer<CameraCheckItem> {
public CheckBoxListRenderer() { setOpaque(true); }
@Override
public Component getListCellRendererComponent(JList<? extends CameraCheckItem> list, CameraCheckItem value, int index, boolean isSel, boolean cellHasFocus) {
setSelected(value.isSelected());
setText(value.toString());
setBackground(isSel ? list.getSelectionBackground() : list.getBackground());
setForeground(isSel ? list.getSelectionForeground() : list.getForeground());
setEnabled(list.isEnabled());
return this;
}
}

View File

@@ -0,0 +1,183 @@
package io.swtc.proccessing.ui.desktop.recording;
import io.swtc.proccessing.CameraPanel;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.ShowError;
import io.swtc.proccessing.ui.iframe.CameraInternalFrame;
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.util.ArrayList;
import java.util.List;
public class MultiRecordingFrame extends JInternalFrame {
private final DefaultListModel<CameraCheckItem> listModel = new DefaultListModel<>();
private final JList<CameraCheckItem> cameraList = new JList<>(listModel);
private final List<AVRecorder> activeRecorders = new ArrayList<>();
private boolean isRecording = false;
private JButton toggleBtn;
private JComboBox<Quality> globalQualityCombo;
private JLabel statusSummaryLabel;
public MultiRecordingFrame() {
super("Record Batch", true, true, false, true);
Image ico = IconSetter.getCamerarec_img();
setFrameIcon(new ImageIcon(ico));
setupUI();
setSize(350, 400);
}
private void setupUI() {
setLayout(new BorderLayout(10, 10));
((JPanel)getContentPane()).setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
statusSummaryLabel = new JLabel("Ready", SwingConstants.CENTER);
JPanel settingsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
settingsPanel.setBorder(BorderFactory.createTitledBorder("Global Encoding"));
globalQualityCombo = new JComboBox<>(Quality.values());
globalQualityCombo.setSelectedItem(Quality.VERYFAST);
settingsPanel.add(new JLabel("CPU Preset:"));
settingsPanel.add(globalQualityCombo);
cameraList.setCellRenderer(new CheckBoxListRenderer());
cameraList.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseClicked(java.awt.event.MouseEvent e) {
if (isRecording) return;
int index = cameraList.locationToIndex(e.getPoint());
if (index != -1) {
CameraCheckItem item = listModel.getElementAt(index);
item.setSelected(!item.isSelected());
cameraList.repaint(cameraList.getCellBounds(index, index));
}
}
});
toggleBtn = new JButton("Start Batch Recording");
toggleBtn.addActionListener(e -> handleToggleAction());
JButton refreshBtn = new JButton("Refresh List");
refreshBtn.addActionListener(e -> refreshCameraList());
JPanel actionPanel = new JPanel(new GridLayout(2, 1, 5, 5));
actionPanel.add(refreshBtn);
actionPanel.add(toggleBtn);
JPanel southPanel = new JPanel(new BorderLayout(5, 5));
southPanel.add(statusSummaryLabel, BorderLayout.NORTH);
southPanel.add(actionPanel, BorderLayout.SOUTH);
// Add components to frame
add(settingsPanel, BorderLayout.NORTH);
add(new JScrollPane(cameraList), BorderLayout.CENTER);
add(southPanel, BorderLayout.SOUTH);
}
private void startRecordingProcess() {
List<CameraInternalFrame> selectedFrames = new ArrayList<>();
for (int i = 0; i < listModel.size(); i++) {
CameraCheckItem item = listModel.get(i);
if (item.isSelected()) selectedFrames.add(item.getFrame());
}
if (selectedFrames.isEmpty()) {
ShowError.warning(this,"Select 1 Camera at minimum","Selection");
return;
}
Quality quality = (Quality) globalQualityCombo.getSelectedItem();
String preset = (quality != null) ? quality.getFFmpegValue() : "superfast";
File videoDir = new File(System.getProperty("user.home"), "Videos/swtcctv-rec");
if (!videoDir.exists()) videoDir.mkdirs();
for (CameraInternalFrame frame : selectedFrames) {
try {
CameraPanel panel = frame.getCameraPanel();
BufferedImage sample = panel.getCurrentProcessedImage();
if (sample == null) continue;
File outputFile = new File(videoDir, "(" + frame.getTitle() + ") batch " + System.currentTimeMillis() + ".mp4");
RecorderConfig config = new RecorderConfig(outputFile, sample.getWidth(), sample.getHeight(), 20, 18, preset);
AVRecorder recorder = new AVRecorder(config);
recorder.start();
panel.setExternalRecorder(recorder);
activeRecorders.add(recorder);
} catch (Exception e) {
System.err.println("Failed to start recorder for: " + frame.getTitle());
}
}
isRecording = true;
updateUIState(true);
}
private void stopRecordingProcess() {
for (AVRecorder recorder : activeRecorders) {
recorder.stop();
}
activeRecorders.clear();
// Clear references from panels
for (int i = 0; i < listModel.size(); i++) {
listModel.get(i).getFrame().getCameraPanel().setExternalRecorder(null);
}
isRecording = false;
updateUIState(false);
}
private void handleToggleAction() {
if (!isRecording) startRecordingProcess();
else stopRecordingProcess();
}
private void refreshCameraList() {
if (isRecording) return;
listModel.clear();
JDesktopPane desktop = getDesktopPane();
if (desktop == null) return;
for (JInternalFrame f : desktop.getAllFrames()) {
if (f instanceof CameraInternalFrame camFrame) {
listModel.addElement(new CameraCheckItem(camFrame));
}
}
if (statusSummaryLabel != null) {
statusSummaryLabel.setText("Total" + listModel.size() + " Cameras");
}
}
private void updateUIState(boolean recordingActive) {
globalQualityCombo.setEnabled(!recordingActive);
cameraList.setEnabled(!recordingActive);
if (recordingActive) {
toggleBtn.setText("Stop all");
statusSummaryLabel.setText("Active");
} else {
toggleBtn.setText("Start Batch Recording");
toggleBtn.setBackground(null);
toggleBtn.setForeground(null);
statusSummaryLabel.setText("Status: Ready");
statusSummaryLabel.setForeground(null);
}
}
@Override
public void addNotify() {
super.addNotify();
refreshCameraList();
}
}

View File

@@ -0,0 +1,72 @@
package io.swtc.proccessing.ui.iframe;
import com.github.sarxos.webcam.Webcam;
import io.swtc.proccessing.WebcamCaptureLoop;
import io.swtc.proccessing.CameraPanel;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.ShowError;
import javax.swing.*;
import java.awt.*;
import java.util.function.Consumer;
public class CameraInternalFrame extends JInternalFrame {
private final WebcamCaptureLoop captureLoop;
private final CameraPanel cameraPanel;
public CameraInternalFrame(Webcam webcam, Consumer<CameraInternalFrame> onOpenEffects) {
super(webcam.getName(), true, true, true, true);
Image ico = IconSetter.getIcon();
setFrameIcon(new ImageIcon(ico));
this.cameraPanel = new CameraPanel();
this.captureLoop = new WebcamCaptureLoop(webcam, cameraPanel::setImage);
setupUI(onOpenEffects);
captureLoop.start();
}
private void setupUI(Consumer<CameraInternalFrame> onOpenEffects) {
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab("View", cameraPanel);
tabbedPane.addTab("Capture", new JPanel());
tabbedPane.addTab("Effects", new JPanel());
tabbedPane.addChangeListener(e -> {
int index = tabbedPane.getSelectedIndex();
if (index == 1) {
tabbedPane.setSelectedIndex(0);
openRecordingFrame();
} else if (index == 2) {
tabbedPane.setSelectedIndex(0);
onOpenEffects.accept(this);
}
});
add(tabbedPane);
setSize(600, 500);
}
private void openRecordingFrame() {
RecordingFrame rf = new RecordingFrame(this.getTitle(), cameraPanel);
JDesktopPane desktopPane = getDesktopPane();
if (desktopPane != null) {
desktopPane.add(rf);
rf.setVisible(true);
try {
rf.setSelected(true);
} catch (java.beans.PropertyVetoException veto) {
ShowError.error(null, "Focus Error: " + veto.getMessage(), "Error");
}
}
}
public CameraPanel getCameraPanel() { return cameraPanel; }
@Override
public void dispose() {
captureLoop.stop();
super.dispose();
}
}

View File

@@ -0,0 +1,44 @@
package io.swtc.proccessing.ui.iframe;
import javax.swing.*;
import java.awt.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class DesktopPane extends JDesktopPane {
private final Map<JInternalFrame, EffectsPanelFrame> connections;
private final Map<JInternalFrame, Color> connectionColors = new HashMap<>();
public DesktopPane(Map<JInternalFrame, EffectsPanelFrame> connections) {
this.connections = connections;
}
private Color getConnectionColor(JInternalFrame frame) {
return connectionColors.computeIfAbsent(frame, k -> {
Random rand = new Random();
return new Color(rand.nextInt(256), rand.nextInt(256), rand.nextInt(256), 200);
});
}
public void forgetFrame(JInternalFrame frame) {
connectionColors.remove(frame);
}
@Override
protected void paintChildren(Graphics g) {
super.paintChildren(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
for (Map.Entry<JInternalFrame, EffectsPanelFrame> entry : connections.entrySet()) {
JInternalFrame camera = entry.getKey();
EffectsPanelFrame effects = entry.getValue();
if (camera.isVisible() && effects.isVisible() && !camera.isIcon() && !effects.isIcon()) {
g2d.setColor(getConnectionColor(camera));
}
}
g2d.dispose();
}
}

View File

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

View File

@@ -0,0 +1,223 @@
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 String camName;
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;
this.camName = cameraName;
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, "(" + this.camName + ") " + "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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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

@@ -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,48 @@
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.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); // javacv respects bitrate already ; this is for my own safety
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: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

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

View File

@@ -1,98 +0,0 @@
import io.swtc.networking.CameraConfig;
import io.swtc.networking.CameraSettings;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class CameraSettingsTest {
// Must match the filename used in CameraSettings.java
private final File TEST_FILE = new File("network_cameras.json");
@BeforeEach
@AfterEach
void cleanUp() {
// Ensure we start and end with a clean slate to avoid side effects
if (TEST_FILE.exists()) {
TEST_FILE.delete();
}
}
@Test
void testLoadReturnsEmptyListWhenNoFile() {
// If the file doesn't exist, it should return an empty list (not null)
List<CameraConfig> result = CameraSettings.load();
assertNotNull(result, "Load should never return null");
assertTrue(result.isEmpty(), "Should return empty list if file doesn't exist");
}
@Test
void testSaveAndLoad() {
// 1. Create a config (Using your actual constructor)
CameraConfig config = new CameraConfig("FrontDoor", "http://192.168.1.100/mjpeg");
// 2. Save it
CameraSettings.save(config);
// 3. Verify file creation
assertTrue(TEST_FILE.exists(), "File should be created after save");
// 4. Load it back
List<CameraConfig> loaded = CameraSettings.load();
// 5. Verify contents
assertEquals(1, loaded.size());
assertEquals("FrontDoor", loaded.get(0).getName());
assertEquals("http://192.168.1.100/mjpeg", loaded.get(0).getUrl());
}
@Test
void testSaveMultiple() {
// Save two distinct cameras
CameraSettings.save(new CameraConfig("Cam1", "rtsp://10.0.0.1/stream"));
CameraSettings.save(new CameraConfig("Cam2", "rtsp://10.0.0.2/stream"));
List<CameraConfig> loaded = CameraSettings.load();
assertEquals(2, loaded.size());
assertEquals("Cam1", loaded.get(0).getName());
assertEquals("Cam2", loaded.get(1).getName());
}
@Test
void testDelete() {
// Setup: Save two cameras
CameraSettings.save(new CameraConfig("Garage", "http://1.1.1.1"));
CameraSettings.save(new CameraConfig("Garden", "http://2.2.2.2"));
// Action: Delete "Garage"
CameraSettings.delete("Garage");
// Verify: Only "Garden" remains
List<CameraConfig> result = CameraSettings.load();
assertEquals(1, result.size());
assertEquals("Garden", result.get(0).getName());
}
@Test
void testLoadCorruptFile() throws IOException {
// Manually write broken JSON to the file
try (FileWriter writer = new FileWriter(TEST_FILE)) {
writer.write("{ \"this is broken json\": ... ");
}
// The code catches IOException and returns empty list
List<CameraConfig> result = CameraSettings.load();
assertNotNull(result);
assertTrue(result.isEmpty(), "Should handle corrupt JSON gracefully by returning empty list");
}
}