5 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
28 changed files with 1216 additions and 328 deletions

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,16 +2,9 @@ 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) { /* Do nothing */ }

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.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;
@@ -17,6 +24,21 @@ public class CameraPanel extends JPanel {
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);
@@ -35,12 +57,26 @@ public class CameraPanel extends JPanel {
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
@@ -135,8 +171,40 @@ public class CameraPanel extends JPanel {
}
Graphics2D g2d = processedImage.createGraphics();
g2d.drawImage(temp, 0, 0, null);
g2d.dispose();
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() {

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

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

@@ -5,8 +5,8 @@ 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 io.swtc.recording.evidence.USBExportManager;
import javax.swing.*;
import java.awt.*;
@@ -14,7 +14,6 @@ import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import java.nio.file.Path;
public class RecordingFrame extends JInternalFrame {
private AVRecorder avRecorder;
@@ -24,11 +23,13 @@ public class RecordingFrame extends JInternalFrame {
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);
@@ -41,10 +42,10 @@ public class RecordingFrame extends JInternalFrame {
setFrameIcon(new ImageIcon(ico));
this.cameraPanel = cameraPanel;
this.camName = cameraName;
initializeUI();
// Timer for UI updates only (1 FPS is enough for the clock)
this.statsTimer = new Timer(1000, e -> updateStats());
this.statsTimer.setCoalesce(true);
@@ -56,10 +57,7 @@ public class RecordingFrame extends JInternalFrame {
outputDirectory = new File(videoDir, "swtcctv-rec");
if (!outputDirectory.exists()) {
boolean created = outputDirectory.mkdirs();
if (!created) {
System.err.println("Could not create recording directory: " + outputDirectory.getAbsolutePath());
}
outputDirectory.mkdirs();
}
}
@@ -69,21 +67,38 @@ public class RecordingFrame extends JInternalFrame {
mainContent.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
exportSection = new ExportSection(this, outputDirectory, statusLabel);
JPanel actionPanel = createActionPanel();
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"));
@@ -91,20 +106,95 @@ public class RecordingFrame extends JInternalFrame {
statusLabel = new JLabel("Status: Idle");
statsLabel = new JLabel("Length: 00:00 | Size: 0.00 MB");
statsLabel.setFont(new Font("Monospaced", Font.PLAIN, 12));
statsLabel.setPreferredSize(new Dimension(240, 20));
panel.add(statusLabel);
panel.add(statsLabel);
return panel;
}
private 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);
@@ -118,163 +208,10 @@ public class RecordingFrame extends JInternalFrame {
statsLabel.setText(sb.toString());
}
private void toggleRecording() {
// Updated check for the new recorder state
if (avRecorder == null || !avRecorder.isRecording()) {
startRec();
} else {
stopRec();
}
}
private void startRec() {
BufferedImage sample = cameraPanel.getCurrentProcessedImage();
if (sample == null) {
ShowError.warning(this, "No camera feed detected. Start camera first.", "Warning");
return;
}
try {
if (!outputDirectory.exists() && !outputDirectory.mkdirs()) {
throw new IOException("Failed to create directory: " + outputDirectory);
}
currentFile = new File(outputDirectory, "vid_" + System.currentTimeMillis() + ".mp4");
// 1. Define the production config
RecorderConfig config = new RecorderConfig(
currentFile,
sample.getWidth(),
sample.getHeight(),
20, // Frame Rate
25, // CRF (Quality)
"ultrafast"
);
// 2. Initialize the HA Recorder
avRecorder = new AVRecorder(config);
avRecorder.start();
// 3. Link to CameraPanel (Ensure CameraPanel calls .accept(img))
cameraPanel.setExternalRecorder(avRecorder);
startTime = System.currentTimeMillis();
statsTimer.start();
recordBtn.setText("Stop Recording");
recordBtn.setForeground(Color.RED);
statusLabel.setText("Recording...");
} catch (Exception ex) {
ShowError.error(this, "Failed to start Recorder: " + ex.getMessage(), "Error");
ex.printStackTrace();
}
}
private void stopRec() {
if (avRecorder != null) {
statusLabel.setText("Finalizing file...");
avRecorder.stop();
cameraPanel.setExternalRecorder(null);
}
statsTimer.stop();
recordBtn.setText("Start Recording");
recordBtn.setForeground(null);
String fileName = (currentFile != null) ? currentFile.getName() : "N/A";
statusLabel.setText("File saved: " + fileName);
}
private JPanel createStoragePanel() {
JPanel panel = new JPanel(new BorderLayout(5, 5));
panel.setBorder(BorderFactory.createTitledBorder("Storage Folder"));
JTextField pathField = new JTextField(outputDirectory.getAbsolutePath());
pathField.setEditable(false);
JButton browseBtn = new JButton("...");
browseBtn.addActionListener(e -> {
JFileChooser chooser = new JFileChooser(outputDirectory);
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
outputDirectory = chooser.getSelectedFile();
pathField.setText(outputDirectory.getAbsolutePath());
}
});
panel.add(pathField, BorderLayout.CENTER);
panel.add(browseBtn, BorderLayout.EAST);
return panel;
}
private void exportToUSB() {
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
chooser.setDialogTitle("Select USB destination");
if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) return;
Path usbRoot = chooser.getSelectedFile().toPath();
Path exportTarget = usbRoot.resolve("CCTV_EXPORT");
try {
long size = USBExportManager.calculateDirectorySize(outputDirectory.toPath());
if (!USBExportManager.hasEnoughSpace(usbRoot, size)) {
ShowError.error(this, "Not enough space on USB device.", "Export failed");
return;
}
JProgressBar bar = new JProgressBar(0, 100);
JOptionPane pane = new JOptionPane(bar,
JOptionPane.INFORMATION_MESSAGE,
JOptionPane.DEFAULT_OPTION,
null,
new Object[]{});
JDialog dialog = pane.createDialog(this, "Exporting...");
dialog.setModal(false);
dialog.setVisible(true);
USBExportManager.exportAsync(
outputDirectory.toPath(),
exportTarget,
stats -> bar.setValue(stats.percent()),
() -> {
dialog.dispose();
statusLabel.setText("Export completed");
},
ex -> {
dialog.dispose();
ShowError.error(this, ex.getMessage(), "Export error");
}
);
} catch (IOException ex) {
ShowError.error(this, ex.getMessage(), "Export error");
}
}
private JPanel createActionPanel() {
JPanel panel = new JPanel(new GridLayout(1, 2, 5, 5));
recordBtn = new JButton("Start Recording");
recordBtn.addActionListener(e -> toggleRecording());
JButton snapBtn = new JButton("Snapshot");
snapBtn.addActionListener(e -> takeSnapshot());
// Export is now handled by the ExportSection component
panel.add(recordBtn);
panel.add(snapBtn);
return panel;
}
private void takeSnapshot() {
BufferedImage img = cameraPanel.getCurrentProcessedImage();
if (img != null) {
try {
if (!outputDirectory.exists()) outputDirectory.mkdirs();
File file = new File(outputDirectory, "snap_" + System.currentTimeMillis() + ".png");
ImageIO.write(img, "PNG", file);
statusLabel.setText("Snapshot: " + file.getName());

View File

@@ -27,16 +27,13 @@ public class ExportSection extends JPanel {
this.pathField.setEditable(false);
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab("Evidence-Export", createTransferTab());
tabbedPane.addTab("Storage Settings", createSettingsTab());
tabbedPane.addTab("Evidence-Export", createTransferTab());
add(tabbedPane, BorderLayout.CENTER);
}
private static JLabel getStatusLabel(JLabel statusLabel) {
return statusLabel;
}
private JPanel createTransferTab() {
JPanel panel = new JPanel(new GridBagLayout());
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
@@ -48,7 +45,7 @@ public class ExportSection extends JPanel {
exportBtn.addActionListener(e -> startUsbExport());
JLabel infoLabel = new JLabel("<html><small>" +
"Export Evidence to a USB Drive!" +
"Export Evidence (Can also be done via Desktop)" +
"</small></html>");
gbc.gridy = 0;

View File

@@ -15,13 +15,14 @@ public class FrameProccessor {
reuseImg = new BufferedImage(
rawImg.getWidth(),
rawImg.getHeight(),
BufferedImage.TYPE_3BYTE_BGR // default java BufferedImage Type
BufferedImage.TYPE_3BYTE_BGR
);
var g = reuseImg.createGraphics();
g.drawImage(rawImg, 0, 0, null);
g.dispose();
}
var g = reuseImg.createGraphics();
g.drawImage(rawImg, 0, 0, null);
g.dispose();
return converter.getFrame(reuseImg);
}

View File

@@ -18,11 +18,14 @@ public class MediaSink {
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setFormat("mp4");
recorder.setPixelFormat(avutil.AV_PIX_FMT_BGR24);
recorder.setFrameRate(config.fps());
/* this is essentially just building FFmpeg? Would've used ProccessBuilder for this lol */
recorder.setVideoOption("pixel_format", "yuv420p");
recorder.setVideoOption("preset", config.preset());
recorder.setVideoOption("crf", String.valueOf(config.crf()));
recorder.setVideoOption("tune", "zerolatency");
recorder.setVideoOption("x264opts", "keyint=40:min-keyint=20");
recorder.setVideoBitrate(0); // javacv respects bitrate already ; this is for my own safety
recorder.setGopSize(config.fps() * 2);
recorder.start();

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

@@ -5,38 +5,50 @@ import java.util.concurrent.TimeUnit;
public record ExportStats(
long totalBytes,
long copiedBytes,
long startTimeNanos
long startTimeNanos,
String currentFileName
) {
public int percent() {
return totalBytes == 0 ? 0 :
(int) ((copiedBytes * 100) / totalBytes);
if (totalBytes <= 0) return 0;
return (int) Math.min(100, (copiedBytes * 100) / totalBytes);
}
/* timeLeft, ill implement this in the future (in the UI) i think
* this is the proper way to do this? idk
* */
public String timeLeft() {
if (copiedBytes == 0) return "0";
if (copiedBytes <= 0) return "Calculating...";
long elapsedNanos = System.nanoTime() - startTimeNanos;
if (elapsedNanos <= 0) return "Calculating...";
double bytesPerNano = (double) copiedBytes / elapsedNanos;
// Bytes per nanosecond
double bps = (double) copiedBytes / elapsedNanos;
long remainingBytes = totalBytes - copiedBytes;
long remainingNanos = (long) (remainingBytes / bytesPerNano);
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 seconds = TimeUnit.NANOSECONDS.toSeconds(nanos);
long mins = seconds / 60;
long secs = seconds % 60;
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 mins + "m " + secs + "s";
return String.format("%dm %ds", mins, secs);
}
return secs + "s";
}
}
}

View File

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

Binary file not shown.

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