6 Commits

Author SHA1 Message Date
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
34 changed files with 1748 additions and 421 deletions

120
pom.xml
View File

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

View File

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

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

View File

@@ -45,7 +45,6 @@ public class SwingCCTVManager {
private final DefaultTableModel tableModel;
private final SwingIFrame IFrame;
private boolean isRefreshing = false;
public SwingCCTVManager() {
frame = new JFrame("Dashboard");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

View File

@@ -2,19 +2,29 @@ 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;
@@ -22,9 +32,9 @@ public class SwingIFrame {
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);
@@ -36,6 +46,13 @@ public class SwingIFrame {
desktopPane.setBackground(defDesktopBg);
mainFrame.add(desktopPane, BorderLayout.CENTER);
desktopIconManager = new DIM(desktopPane);
setupDesktopExportFrame();
setupRecordingFrame();
setupFileEx();
setupProfiler();
setupFullscreenToggle();
setupBlackBg();
initPopupMenu();
@@ -43,6 +60,66 @@ public class SwingIFrame {
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);

View File

@@ -1,12 +1,19 @@
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.geom.AffineTransform;
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;
@@ -16,6 +23,23 @@ 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;
@@ -24,22 +48,39 @@ public class CameraPanel extends JPanel {
private Point dragStartPoint;
private static final double MIN_ZOOM = 1.0;
private static final double MAX_ZOOM = 10.0; // ten is enough if ur using 640x480
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);
}
public void mouseWheelMoved(MouseWheelEvent e) { handleZoom(e); }
@Override
public void mousePressed(MouseEvent e) {
@@ -49,17 +90,14 @@ public class CameraPanel extends JPanel {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2)
resetView();
if (e.getClickCount() == 2) resetView();
}
@Override
public void mouseReleased(MouseEvent e) { setCursor(Cursor.getDefaultCursor()); }
@Override
public void mouseDragged(MouseEvent e) {
handlePan(e);
}
public void mouseDragged(MouseEvent e) { handlePan(e); }
@Override
public void mouseExited(MouseEvent e) { setCursor(Cursor.getDefaultCursor()); }
@@ -70,10 +108,20 @@ public class CameraPanel extends JPanel {
addMouseMotionListener(mouseHandler);
}
public void setExternalRecorder(AVRecorder recorder) {
this.recorder = recorder;
}
public void setImage(BufferedImage img) {
sourceImage = 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();
}
@@ -85,19 +133,78 @@ public class CameraPanel extends JPanel {
}
}
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 {
processedImage = imageProcessor.apply(sourceImage);
temp = imageProcessor.apply(sourceImage);
} catch (Exception e) {
ShowError.error(null,"Fucked up in rendering \n" + Arrays.toString(e.getStackTrace()),"Problem");
processedImage = sourceImage; // Fallback
ShowError.error(null, "Error in image processing: \n" + Arrays.toString(e.getStackTrace()), "Processing Error");
temp = sourceImage;
}
} else {
processedImage = sourceImage;
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() {
@@ -109,24 +216,46 @@ public class CameraPanel extends JPanel {
}
}
@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; // Zoom In
zoomLevel *= ZOOM_MULTIPLIER;
} else {
zoomLevel /= ZOOM_MULTIPLIER; // Zoom Out
zoomLevel /= ZOOM_MULTIPLIER;
}
// clamp shit
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);
@@ -140,15 +269,10 @@ public class CameraPanel extends JPanel {
private void handlePan(MouseEvent e) {
if (processedImage == null || dragStartPoint == null) return;
// Calculate delta
int dx = e.getX() - dragStartPoint.x;
int dy = e.getY() - dragStartPoint.y;
xOffset += dx;
yOffset += dy;
xOffset += e.getX() - dragStartPoint.x;
yOffset += e.getY() - dragStartPoint.y;
dragStartPoint = e.getPoint();
checkBounds();
repaint();
}
@@ -167,42 +291,4 @@ public class CameraPanel extends JPanel {
yOffset = getHeight() - viewedHeight;
}
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (processedImage == null) return;
Graphics2D g2d = (Graphics2D) g;
AffineTransform originalTransform = g2d.getTransform();
g2d.setRenderingHint(
RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR
);
g2d.translate(xOffset, 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);
g2d.setTransform(originalTransform);
}
public BufferedImage getCurrentProcessedImage() {
return processedImage;
}
public void resetView() {
zoomLevel = 1.0;
xOffset = 0;
yOffset = 0;
repaint();
}
}

View File

@@ -5,16 +5,25 @@ 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 (url == null) throw new RuntimeException("Icon not found: /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;
@@ -23,7 +32,10 @@ public class IconSetter {
public static Image getEffectIcon() {
if (Objects.isNull(effects_icon)) {
URL url = IconSetter.class.getResource("/icons/effectsframe.png");
if (Objects.isNull(url)) ShowError.error(null,"Error","Icon not found");
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;
@@ -32,9 +44,72 @@ public class IconSetter {
public static ImageIcon getIconAsImageIcon() {
if (ICON_ICON == null) {
URL url = IconSetter.class.getResource("/icons/artwork.png");
if (url == null) throw new RuntimeException("Icon not found: /icons/artwork.png");
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,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

@@ -5,23 +5,13 @@ import io.swtc.proccessing.WebcamCaptureLoop;
import io.swtc.proccessing.CameraPanel;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.ShowError;
import io.swtc.recording.RecordingManager;
import io.swtc.recording.VideoRecorder;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.RescaleOp;
import java.io.File;
import java.io.IOException;
import java.util.function.Consumer;
public class CameraInternalFrame extends JInternalFrame {
private final WebcamCaptureLoop captureLoop;
private final CameraPanel cameraPanel;
private final VideoRecorder videoRecorder;
private final RecordingManager recordingManager;
public CameraInternalFrame(Webcam webcam, Consumer<CameraInternalFrame> onOpenEffects) {
super(webcam.getName(), true, true, true, true);
@@ -30,12 +20,8 @@ public class CameraInternalFrame extends JInternalFrame {
setFrameIcon(new ImageIcon(ico));
this.cameraPanel = new CameraPanel();
this.videoRecorder = new VideoRecorder();
this.recordingManager = new RecordingManager(cameraPanel); // Initialize recorder
this.captureLoop = new WebcamCaptureLoop(webcam, img ->
SwingUtilities.invokeLater(() -> cameraPanel.setImage(img))
);
this.captureLoop = new WebcamCaptureLoop(webcam, cameraPanel::setImage);
setupUI(onOpenEffects);
captureLoop.start();
@@ -49,7 +35,7 @@ public class CameraInternalFrame extends JInternalFrame {
tabbedPane.addChangeListener(e -> {
int index = tabbedPane.getSelectedIndex();
if (index == 1) { // the tab index for capture is 1
if (index == 1) {
tabbedPane.setSelectedIndex(0);
openRecordingFrame();
} else if (index == 2) {
@@ -63,17 +49,16 @@ public class CameraInternalFrame extends JInternalFrame {
}
private void openRecordingFrame() {
RecordingFrame rf = new RecordingFrame(this.getTitle(),cameraPanel, videoRecorder);
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,"VetoException"+veto.getMessage(),"veto");
ShowError.error(null, "Focus Error: " + veto.getMessage(), "Error");
}
}
}
@@ -81,9 +66,6 @@ public class CameraInternalFrame extends JInternalFrame {
@Override
public void dispose() {
if (videoRecorder.isRecording()) {
try { videoRecorder.stopRecording(); } catch (IOException ignored) {}
}
captureLoop.stop();
super.dispose();
}

View File

@@ -2,7 +2,6 @@ package io.swtc.proccessing.ui.iframe;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.CubicCurve2D;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
@@ -38,29 +37,8 @@ public class DesktopPane extends JDesktopPane {
if (camera.isVisible() && effects.isVisible() && !camera.isIcon() && !effects.isIcon()) {
g2d.setColor(getConnectionColor(camera));
drawBezierConnection(g2d, camera, effects);
}
}
g2d.dispose();
}
private void drawBezierConnection(Graphics2D g2d, JInternalFrame from, JInternalFrame to) {
Rectangle f = from.getBounds();
Rectangle t = to.getBounds();
int x1 = f.x + f.width;
int y1 = f.y + (f.height / 2);
int x2 = t.x;
int y2 = t.y + (t.height / 2);
int ctrlOffset = Math.min(Math.abs(x2 - x1) / 2, 150);
CubicCurve2D curve = new CubicCurve2D.Double(x1, y1, x1 + ctrlOffset, y1, x2 - ctrlOffset, y2, x2, y2);
//g2d.setStroke(new BasicStroke(3f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
//g2d.draw(curve);
// Terminals
//g2d.fillOval(x1 - 5, y1 - 5, 10, 10);
//g2d.fillOval(x2 - 5, y2 - 5, 10, 10);
}
}

View File

@@ -3,7 +3,10 @@ package io.swtc.proccessing.ui.iframe;
import io.swtc.proccessing.CameraPanel;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.ShowError;
import io.swtc.recording.VideoRecorder;
import 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.*;
@@ -13,30 +16,33 @@ import java.io.IOException;
import javax.imageio.ImageIO;
public class RecordingFrame extends JInternalFrame {
private final VideoRecorder videoRecorder;
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 = new File(System.getProperty("user.home"));
private File outputDirectory;
private File currentFile;
private final Timer statsTimer;
private long startTime;
private String camName;
// Cache for string building to avoid object allocation every second
private final StringBuilder sb = new StringBuilder(32);
public RecordingFrame(String cameraName, CameraPanel cameraPanel, VideoRecorder videoRecorder) {
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.videoRecorder = videoRecorder;
this.camName = cameraName;
initializeUI();
@@ -46,133 +52,172 @@ public class RecordingFrame extends JInternalFrame {
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));
mainContent.add(createStoragePanel());
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(createActionPanel());
mainContent.add(settingsPanel);
mainContent.add(Box.createVerticalStrut(10));
mainContent.add(createStatsPanel());
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));
statsLabel.setPreferredSize(new Dimension(220, 20));
panel.add(statusLabel);
panel.add(statsLabel);
return panel;
}
private void updateStats() {
if (!videoRecorder.isRecording() || currentFile == null) return;
long elapsedSecs = (System.currentTimeMillis() - startTime) / 1000;
long minutes = elapsedSecs / 60;
long seconds = elapsedSecs % 60;
double sizeInMb = currentFile.length() / 1048576.0;
sb.setLength(0);
sb.append("Length: ")
.append(minutes < 10 ? "0" : "").append(minutes).append(":")
.append(seconds < 10 ? "0" : "").append(seconds)
.append(" | Size: ")
.append(Math.round(sizeInMb * 100.0) / 100.0)
.append(" MB");
statsLabel.setText(sb.toString());
}
private void toggleRecording() {
if (!videoRecorder.isRecording()) {
startRec();
} else {
stopRec();
}
}
private void startRec() {
try {
currentFile = new File(outputDirectory, "vid_" + System.currentTimeMillis() + ".mp4");
videoRecorder.startRecording(cameraPanel, currentFile);
startTime = System.currentTimeMillis();
statsTimer.start(); // Only run timer when needed
recordBtn.setText("Stop Recording");
statusLabel.setText("Currently Recording");
} catch (IOException ex) {
ShowError.error(null,"Error starting Recording" + ex.getMessage(), "Error");
}
}
private void stopRec() {
try {
videoRecorder.stopRecording();
statsTimer.stop();
recordBtn.setText("Started Recording");
recordBtn.setForeground(null);
statusLabel.setText("Finished recording");
} catch (IOException ex) {
ShowError.error(null,"RecordStop Error"+ex.getMessage(), "Error");
}
}
private JPanel createStoragePanel() {
JPanel panel = new JPanel(new BorderLayout(5, 5));
panel.setBorder(BorderFactory.createTitledBorder("Storage"));
JTextField pathField = new JTextField(outputDirectory.getAbsolutePath());
pathField.setEditable(false);
JButton browseBtn = new JButton("...");
browseBtn.addActionListener(e -> {
JFileChooser chooser = new JFileChooser(outputDirectory);
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
outputDirectory = chooser.getSelectedFile();
pathField.setText(outputDirectory.getAbsolutePath());
}
});
panel.add(pathField, BorderLayout.CENTER);
panel.add(browseBtn, BorderLayout.EAST);
return panel;
}
private JPanel createActionPanel() {
JPanel panel = new JPanel(new GridLayout(1, 2, 5, 5));
recordBtn = new JButton("Start Recording");
recordBtn.addActionListener(e -> toggleRecording());
JButton snapBtn = new JButton("Snapshot");
snapBtn.addActionListener(e -> takeSnapshot());
panel.add(recordBtn);
panel.add(snapBtn);
return panel;
}
private void 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("Saved a Snapshot");
statusLabel.setText("Snapshot: " + file.getName());
} catch (IOException ex) {
ShowError.error(null,"Snapshot failed"+ex.getMessage(),"Snapshot Error");
ShowError.error(this, "Snapshot failed: " + ex.getMessage(), "Error");
}
}
}
}

View File

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

View File

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

View File

@@ -1,87 +0,0 @@
package io.swtc.recording;
import io.swtc.proccessing.CameraPanel;
import org.jcodec.api.awt.AWTSequenceEncoder;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.common.model.Rational;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.*;
public class VideoRecorder {
private volatile boolean recording = false;
private File outputFile;
private AWTSequenceEncoder encoder;
private SeekableByteChannel channel;
private static final int FPS = 30;
private LinkedBlockingQueue<BufferedImage> frameQueue;
private ExecutorService workerThread;
public void startRecording(CameraPanel panel, File output) throws IOException {
this.outputFile = output;
this.channel = NIOUtils.writableFileChannel(output.getAbsolutePath());
this.encoder = new AWTSequenceEncoder(channel, Rational.R(FPS, 1));
this.frameQueue = new LinkedBlockingQueue<>(60);
this.recording = true;
workerThread = Executors.newSingleThreadExecutor();
workerThread.submit(this::processQueue);
new Thread(() -> {
while (recording) {
try {
BufferedImage img = panel.getCurrentProcessedImage();
if (img != null) {
BufferedImage copy = snapshotToRGB(img);
frameQueue.offer(copy);
}
Thread.sleep(1000 / FPS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
}
private void processQueue() {
while (recording || !frameQueue.isEmpty()) {
try {
BufferedImage img = frameQueue.poll(500, TimeUnit.MILLISECONDS);
if (img != null) {
encoder.encodeImage(img);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private BufferedImage snapshotToRGB(BufferedImage source) {
BufferedImage rgb = new BufferedImage(source.getWidth(), source.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D g = rgb.createGraphics();
g.drawImage(source, 0, 0, null);
g.dispose();
return rgb;
}
public File stopRecording() throws IOException {
recording = false;
workerThread.shutdown();
try {
workerThread.awaitTermination(5, TimeUnit.SECONDS);
if (encoder != null) encoder.finish();
if (channel != null) channel.close();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return outputFile;
}
public boolean isRecording() { return recording; }
}

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