4 Commits

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

this is fine ....

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

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

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

new readme

Signed-off-by: rattatwinko <seppmutterman@gmail.com>
2026-02-08 17:00:05 +01:00
701d95ab2d fix for icon change. e1003c20ff
Signed-off-by: rattatwinko <seppmutterman@gmail.com>
2026-02-02 12:31:02 +01:00
18 changed files with 669 additions and 99 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,18 +2,25 @@ 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;
@@ -25,7 +32,6 @@ 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();
@@ -43,6 +49,9 @@ public class SwingIFrame {
desktopIconManager = new DIM(desktopPane);
setupDesktopExportFrame();
setupRecordingFrame();
setupFileEx();
setupProfiler();
setupFullscreenToggle();
setupBlackBg();
@@ -55,10 +64,62 @@ public class SwingIFrame {
desktopIconManager.addIcon(
"Export Evidence",
IconSetter.getSaveIconAsImageIcon(),
EvidenceExportFrame::showExport
/* 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

@@ -2,20 +2,28 @@ package io.swtc.proccessing.ui;
import javax.swing.*;
import java.awt.*;
import java.io.InputStream;
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;
@@ -24,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;
@@ -33,7 +44,10 @@ 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;
@@ -42,9 +56,60 @@ public class IconSetter {
public static ImageIcon getSaveIconAsImageIcon() {
if (Objects.isNull(ICON_ICON)) {
URL url = IconSetter.class.getResource("/icons/save.png");
if (Objects.isNull(url)) throw new RuntimeException("Icon not found: /icons/save.ico");
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

@@ -1,35 +1,51 @@
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;
private final JLabel iconLabel;
private final JLabel textLabel;
public DesktopIcon(String label, Icon icon, Runnable action) {
setLayout(new BorderLayout(4, 4));
setLayout(new BorderLayout(0, 2));
setOpaque(false);
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
if (icon instanceof ImageIcon) {
Image img = ((ImageIcon) icon).getImage();
Image scaled = img.getScaledInstance(64, 64, Image.SCALE_SMOOTH);
icon = new ImageIcon(scaled);
}
iconLabel = new JLabel(icon, SwingConstants.CENTER);
textLabel = new ShadowLabel(label);
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) {
@@ -52,56 +68,35 @@ public class DesktopIcon extends JPanel {
});
}
@Override
public Dimension getPreferredSize() {
Dimension icon = iconLabel.getPreferredSize();
Dimension text = textLabel.getPreferredSize();
int w = Math.max(icon.width, text.width) + 12;
int h = icon.height + text.height + 12;
return new Dimension(w, h);
}
@Override
protected void paintComponent(Graphics g) {
if (hovered) {
paintHoverEffect(g);
}
super.paintComponent(g);
}
private void paintHoverEffect(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
boolean lightBg = isBackgroundLight();
Color fill = lightBg
? new Color(0, 0, 0, 30)
: new Color(255, 255, 255, 40);
Color border = lightBg
? new Color(0, 0, 0, 80)
: new Color(255, 255, 255, 100);
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.fillRoundRect(2, 2, getWidth() - 4, getHeight() - 4, 10, 10);
g2.fill(new RoundRectangle2D.Float(2, 2, getWidth() - 4, getHeight() - 4, 10, 10));
g2.setColor(border);
g2.drawRoundRect(2, 2, getWidth() - 5, getHeight() - 5, 10, 10);
g2.draw(new RoundRectangle2D.Float(2, 2, getWidth() - 4, getHeight() - 4, 10, 10));
g2.dispose();
}
super.paintComponent(g);
}
private boolean isBackgroundLight() {
public boolean isBackgroundLight() {
Container p = getParent();
if (p == null) return true;
Color bg = p.getBackground();
int luminance = (int) (
bg.getRed() * 0.299 +
bg.getGreen() * 0.587 +
bg.getBlue() * 0.114
);
return luminance > 180;
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

@@ -5,28 +5,44 @@ import java.awt.*;
public class ShadowLabel extends JLabel {
public ShadowLabel(String text) {
super(text, SwingConstants.CENTER);
super(text);
setForeground(Color.WHITE);
setPreferredSize(new Dimension(15, 20));
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
FontMetrics fm = g2d.getFontMetrics();
String text = getText();
if (text == null || text.isEmpty()) return;
int x = (getWidth() - fm.stringWidth(text)) / 2;
int y = ((getHeight() - fm.getHeight()) / 2) + fm.getAscent();
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setColor(new Color(0, 0, 0, 200));
g2d.drawString(text, x + 1, y + 1);
FontMetrics fm = g2.getFontMetrics();
int availableWidth = getWidth();
g2d.setColor(getForeground());
g2d.drawString(text, x, y);
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();
g2d.dispose();
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

@@ -1,5 +1,6 @@
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;
@@ -15,17 +16,25 @@ public class EvidenceExportFrame extends JFrame {
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;
private EvidenceExportFrame(Path sourceDir, Path usbTargetDir) {
setTitle("Export");
setSize(400, 220);
setLocationRelativeTo(null);
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));
@@ -61,7 +70,7 @@ public class EvidenceExportFrame extends JFrame {
return;
}
int confirm = JOptionPane.showConfirmDialog(this,
int confirm = JOptionPane.showConfirmDialog(this.parent,
"Stop export?", "Confirm", JOptionPane.YES_NO_OPTION);
if (confirm == JOptionPane.YES_OPTION) dispose();
}
@@ -91,7 +100,7 @@ public class EvidenceExportFrame extends JFrame {
);
}
public static void showExport() {
public static void showExport(JFrame parent) {
SwingUtilities.invokeLater(() -> {
File videoDir = new File(System.getProperty("user.home"), "Videos/swtcctv-rec");
if (!videoDir.exists()) {
@@ -101,9 +110,9 @@ public class EvidenceExportFrame extends JFrame {
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
if (chooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) {
Path target = chooser.getSelectedFile().toPath().resolve("swtcctv-rec_" + System.currentTimeMillis() / 1000);
new EvidenceExportFrame(videoDir.toPath(), target);
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

@@ -29,6 +29,7 @@ public class RecordingFrame extends JInternalFrame {
private File currentFile;
private final Timer statsTimer;
private long startTime;
private String camName;
private final StringBuilder sb = new StringBuilder(32);
@@ -41,6 +42,7 @@ public class RecordingFrame extends JInternalFrame {
setFrameIcon(new ImageIcon(ico));
this.cameraPanel = cameraPanel;
this.camName = cameraName;
initializeUI();
@@ -131,7 +133,7 @@ public class RecordingFrame extends JInternalFrame {
}
try {
currentFile = new File(outputDirectory, "vid_" + System.currentTimeMillis() + ".mp4");
currentFile = new File(outputDirectory, "(" + this.camName + ") " + "vid_" + System.currentTimeMillis() + ".mp4");
Quality selected = (Quality) presetCombo.getSelectedItem();
String preset = (selected != null) ? selected.getFFmpegValue() : "superfast";

View File

@@ -18,7 +18,6 @@ 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");
@@ -26,7 +25,7 @@ public class MediaSink {
recorder.setVideoOption("crf", String.valueOf(config.crf()));
recorder.setVideoOption("tune", "zerolatency");
recorder.setVideoOption("x264opts", "keyint=40:min-keyint=20");
recorder.setVideoBitrate(0); // 0 tells the recorder to respect CRF strictly
recorder.setVideoBitrate(0); // javacv respects bitrate already ; this is for my own safety
recorder.setGopSize(config.fps() * 2);
recorder.start();

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