5 Commits

Author SHA1 Message Date
c32b5d7278 refactors ; some new additions to recording which are critical for cctv softwares 2026-01-21 18:09:30 +01:00
3eaf6f0303 fix
Signed-off-by: rattatwinko <seppmutterman@gmail.com>
2026-01-20 08:37:48 +01:00
11c5aa9115 Merge branch 'experimenting'
Signed-off-by: rattatwinko <seppmutterman@gmail.com>

# Conflicts:
#	src/main/java/io/swtc/Main.java
2026-01-20 08:28:10 +01:00
55474092e3 some more optimizing!
now runs much better with lower power systems as we refactored code to firstly rule out double invokelaters, and we optimized the capture loop iteslf!

and we introduced a ShowError class which will be refactored and used quite thoroughly!

Signed-off-by: rattatwinko <seppmutterman@gmail.com>
2026-01-20 08:25:26 +01:00
41fbf62757 just some theme changes, i like this one more 2026-01-18 22:10:37 +01:00
16 changed files with 530 additions and 391 deletions

View File

@@ -1,26 +1,23 @@
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) {
try {
UIManager.setLookAndFeel(
"com.sun.java.swing.plaf.windows.WindowsLookAndFeel"
);
} catch (Exception e) {
JOptionPane.showMessageDialog(
null,
"LaF not available",
"LaF-Warning",
JOptionPane.WARNING_MESSAGE
);
for (int i = 0; i < args.length; i++) {
System.out.println("Arg " + i + ": " + args[i]);
}
// For some reason we need to invoke Later for LaF to work!
SwingUtilities.invokeLater(() -> SwingCCTVManager.main(null));
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
ShowError.warning(null,"LaF Warn","LaF");
}
SwingCCTVManager.main(null);
}
}

View File

@@ -8,6 +8,7 @@ import com.github.sarxos.webcam.ds.ipcam.IpCamDriver;
import com.github.sarxos.webcam.ds.ipcam.IpCamMode;
import io.swtc.networking.CameraConfig;
import io.swtc.networking.CameraSettings;
import io.swtc.proccessing.ui.IconSetter;
import javax.swing.*;
import javax.swing.table.DefaultTableCellRenderer;
@@ -37,6 +38,7 @@ public class SwingCCTVManager {
frame = new JFrame("dashboard");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(1000, 600);
frame.setIconImage(IconSetter.getIcon());
this.IFrame = new SwingIFrame();
this.IFrame.show();
@@ -71,14 +73,33 @@ public class SwingCCTVManager {
}
});
JToolBar toolBar = new JToolBar();
Image ogIcon = IconSetter.getIcon();
Image scaledIcon = ogIcon.getScaledInstance(32, 32, Image.SCALE_SMOOTH);
JLabel iconLabel = new JLabel(new ImageIcon(scaledIcon));
toolBar.add(iconLabel);
JLabel textLabel = new JLabel("SWT-CCTV");
toolBar.add(textLabel);
toolBar.add(Box.createRigidArea(new Dimension(10, 0))); // 20px horizontal padding
toolBar.addSeparator(new Dimension(10, 32)); // 10px width, 32px height
JButton btnAdd = new JButton("Add IP Cam");
JButton btnLaunch = new JButton("Launch Stream");
btnAdd.setPreferredSize(new Dimension(100, 32));
btnLaunch.setPreferredSize(new Dimension(120, 32));
toolBar.add(Box.createRigidArea(new Dimension(5,0)));
toolBar.add(btnAdd);
toolBar.addSeparator();
toolBar.add(btnLaunch);
frame.add(toolBar, BorderLayout.NORTH);
frame.add(toolBar, BorderLayout.SOUTH);
frame.add(new JScrollPane(deviceTable), BorderLayout.CENTER);
btnAdd.addActionListener(e -> showAddCameraDialog());
@@ -145,12 +166,14 @@ public class SwingCCTVManager {
}
JPopupMenu menu = new JPopupMenu();
JMenuItem ico = new JMenuItem("test");
JMenuItem launch = new JMenuItem("Launch Live Stream");
JMenuItem delete = new JMenuItem("Remove Device");
launch.addActionListener(al -> launchSelected());
delete.addActionListener(al -> deleteSelected());
menu.add(ico);
menu.add(launch);
menu.addSeparator();
menu.add(delete);

View File

@@ -1,6 +1,7 @@
package io.swtc;
import com.github.sarxos.webcam.Webcam;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.iframe.*; // Your custom frames
import javax.swing.*;
import java.awt.*;
@@ -28,6 +29,8 @@ public class SwingIFrame {
mainFrame.setSize(1280, 720);
mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
mainFrame.setIconImage(IconSetter.getIcon());
desktopPane = new DesktopPane(cameraToEffects);
desktopPane.setBackground(defDesktopBg);
mainFrame.add(desktopPane, BorderLayout.CENTER);

View File

@@ -3,17 +3,19 @@ package io.swtc.proccessing;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
/**
* Enhanced CameraPanel with support for custom image processors
*/
public class CameraPanel extends JPanel {
private BufferedImage currentImage;
private BufferedImage processedImage;
// Custom processor for advanced effects
private volatile BufferedImage sourceImage;
private volatile BufferedImage processedImage;
private Function<BufferedImage, BufferedImage> imageProcessor;
private final AtomicBoolean repaintScheduled = new AtomicBoolean(false);
public CameraPanel() {
setBackground(Color.BLACK);
@@ -21,78 +23,47 @@ public class CameraPanel extends JPanel {
}
public void setImage(BufferedImage img) {
this.currentImage = img;
processImage();
repaint();
sourceImage = img;
if (repaintScheduled.compareAndSet(false, true)) {
SwingUtilities.invokeLater(() -> {
repaintScheduled.set(false);
repaint();
});
}
}
public void setImageProcessor(Function<BufferedImage, BufferedImage> processor) {
this.imageProcessor = processor;
processImage();
repaint();
}
private void processImage() {
if (currentImage == null) {
processedImage = null;
return;
}
BufferedImage result = currentImage;
// Apply basic transforms first
result = applyBasicEffects(result);
// Apply custom processor if set
if (imageProcessor != null) {
result = imageProcessor.apply(result);
}
processedImage = result;
}
private BufferedImage applyBasicEffects(BufferedImage img) {
int width = img.getWidth();
int height = img.getHeight();
BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// Handle mirror/flip/rotate
int srcX = x;
int srcY = y;
// Ensure coordinates are in bounds
srcX = Math.max(0, Math.min(width - 1, srcX));
srcY = Math.max(0, Math.min(height - 1, srcY));
int rgb = img.getRGB(srcX, srcY);
int r = (rgb >> 16) & 0xFF;
int g = (rgb >> 8) & 0xFF;
int b = rgb & 0xFF;
result.setRGB(x, y, (r << 16) | (g << 8) | b);
}
}
return result;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (processedImage != null) {
Graphics2D g2d = (Graphics2D) g;
BufferedImage src = sourceImage;
if (src == null) return;
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
BufferedImage img = src;
g2d.drawImage(processedImage, 0, 0, getWidth(), getHeight(), null);
if (imageProcessor != null) {
// we only apply the proccessor if it is present
// that is due too CPU Usage ; This project was built for a DUAL Core CPU, not a quad or even 6 Core CPU
img = imageProcessor.apply(src);
}
processedImage = img;
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(
RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR
);
g2d.drawImage(img, 0, 0, getWidth(), getHeight(), null);
}
public BufferedImage getCurrentProcessedImage() {
return processedImage;
}
}
}

View File

@@ -2,76 +2,84 @@ package io.swtc.proccessing;
import com.github.sarxos.webcam.Webcam;
import com.github.sarxos.webcam.WebcamException;
import com.github.sarxos.webcam.WebcamResolution;
import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
import java.util.function.Consumer;
import javax.swing.*;
import io.swtc.proccessing.ui.ShowError;
public class WebcamCaptureLoop {
private final Webcam webcam;
private final Consumer<BufferedImage> onFrameCaptured;
private volatile boolean running = false;
private final AtomicBoolean cleanedUp = new AtomicBoolean(false);
private final int targetframes = 20;
public WebcamCaptureLoop(Webcam webcam, Consumer<BufferedImage> onFrameCaptured) {
this.webcam = webcam;
this.onFrameCaptured = onFrameCaptured;
// this is some of the most stupid shit ive seen in years, this is needed for opening the stream.
// the webcam package may not know the res before its opened and well this is before we open it. that is below in the thread.
// so this freaks out and doesnt want to cooperate.
if (!webcam.getName().toLowerCase().contains("ipcam")) {
Dimension[] sizes = webcam.getViewSizes();
webcam.setViewSize(sizes[sizes.length - 1]);
Dimension vga = WebcamResolution.VGA.getSize();
if (isSizeSupported(webcam, vga)) { // we dont do stupid shit anymore! cause we are smarter than before...
webcam.setViewSize(vga);
} else {
Dimension[] dimensions = webcam.getViewSizes();
webcam.setViewSize(dimensions[dimensions.length - 1]);
}
}
private boolean isSizeSupported(Webcam webcam, Dimension vga) {
for (Dimension d: webcam.getViewSizes()) {
if (
d.width == vga.width && d.height == vga.height
) return true; // this should return 640x480 :)
}
return false;
}
public void start() {
if (running) return;
running = true;
Thread captureThread = new Thread(() -> {
// this is where we open it. if the res isnt known then it fucks up.
try {
webcam.open();
} catch (WebcamException e) {
JOptionPane.showMessageDialog(
null,
"WebcamException" + e.getMessage(),
"WebcamException",
JOptionPane.ERROR_MESSAGE
);
}
if (!webcam.isOpen()) webcam.open(); // open if not
while (running) {
BufferedImage img = webcam.getImage();
if (img != null) {
//System.err.println(img);
onFrameCaptured.accept(img);
long frameDurationLimitns = 1_000_000_000L / targetframes; // we use NanoSeconds cause its more accurate
while (running) {
long startTime = System.nanoTime();
BufferedImage img = webcam.getImage();
if (img != null)
// there is no need for a invoke later swing wise!
onFrameCaptured.accept(img);
long timespent = System.nanoTime() - startTime;
long sleeptime = frameDurationLimitns - timespent;
if (sleeptime > 0) {
LockSupport.parkNanos(sleeptime);
} else {
// do a yield to prevent ui freezes
Thread.yield();
}
}
try {
Thread.sleep(15);
} catch (InterruptedException e) {
break;
}
}
try {
webcam.close();
} catch (IllegalStateException e) {
// show a error message from javax.swing.awt.JOptionPane
JOptionPane.showMessageDialog(
} catch (Exception e) {
ShowError.warning(
null,
"IllegalStateException@"+e,
"IllegalStateException",
JOptionPane.ERROR_MESSAGE
"Exception" + e,
"Exception"
);
} finally {
cleanup();
}
});
captureThread.setName("cam_cap_thread");
}, "cam_cap_thread");
captureThread.start();
}
@@ -111,6 +119,5 @@ public class WebcamCaptureLoop {
*/
public void stop() {
running = false;
cleanup();
}
}

View File

@@ -0,0 +1,40 @@
package io.swtc.proccessing.ui;
import javax.swing.*;
import java.awt.*;
import java.net.URL;
import java.util.Objects;
public class IconSetter {
private static Image ICON_IMAGE;
private static ImageIcon ICON_ICON;
private static Image effects_icon;
public static Image getIcon() {
if (ICON_IMAGE == null) {
URL url = IconSetter.class.getResource("/icons/artwork.png");
if (url == null) throw new RuntimeException("Icon not found: /icons/artwork.png");
ICON_IMAGE = Toolkit.getDefaultToolkit().getImage(url);
}
return ICON_IMAGE;
}
public static Image getEffectIcon() {
if (Objects.isNull(effects_icon)) {
URL url = IconSetter.class.getResource("/icons/effectsframe.png");
if (Objects.isNull(url)) ShowError.error(null,"Error","Icon not found");
effects_icon = Toolkit.getDefaultToolkit().getImage(url);
}
return effects_icon;
}
public static ImageIcon getIconAsImageIcon() {
if (ICON_ICON == null) {
URL url = IconSetter.class.getResource("/icons/artwork.png");
if (url == null) throw new RuntimeException("Icon not found: /icons/artwork.png");
ICON_ICON = new ImageIcon(url); // separate variable for ImageIcon
}
return ICON_ICON;
}
}

View File

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

View File

@@ -3,12 +3,16 @@ package io.swtc.proccessing.ui.iframe;
import com.github.sarxos.webcam.Webcam;
import io.swtc.proccessing.WebcamCaptureLoop;
import io.swtc.proccessing.CameraPanel;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.ShowError;
import 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;
@@ -16,15 +20,19 @@ import java.util.function.Consumer;
public class CameraInternalFrame extends JInternalFrame {
private final WebcamCaptureLoop captureLoop;
private final CameraPanel cameraPanel;
private final VideoRecorder videoRecorder; // Instance of the recorder
private JButton recordBtn;
private final VideoRecorder videoRecorder;
private final RecordingManager recordingManager;
public CameraInternalFrame(Webcam webcam, Consumer<CameraInternalFrame> onOpenEffects) {
super(webcam.getName(), true, true, true, true);
this.cameraPanel = new CameraPanel();
this.videoRecorder = new VideoRecorder(); // Initialize recorder
// Initialize capture loop
Image ico = IconSetter.getIcon();
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))
);
@@ -36,11 +44,15 @@ public class CameraInternalFrame extends JInternalFrame {
private void setupUI(Consumer<CameraInternalFrame> onOpenEffects) {
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab("View", cameraPanel);
tabbedPane.addTab("Capture", new RecordingPane(cameraPanel, videoRecorder));
tabbedPane.addTab("Capture", new JPanel());
tabbedPane.addTab("Effects", new JPanel());
tabbedPane.addChangeListener(e -> {
if (tabbedPane.getSelectedIndex() == 2) {
int index = tabbedPane.getSelectedIndex();
if (index == 1) { // the tab index for capture is 1
tabbedPane.setSelectedIndex(0);
openRecordingFrame();
} else if (index == 2) {
tabbedPane.setSelectedIndex(0);
onOpenEffects.accept(this);
}
@@ -50,79 +62,25 @@ public class CameraInternalFrame extends JInternalFrame {
setSize(600, 500);
}
private JPanel createCapturePanel() {
JPanel panel = new JPanel(new GridLayout(2, 1, 10, 10)); // Changed to Grid for better button layout
panel.setBorder(new EmptyBorder(15, 15, 15, 15));
JButton screenshotBtn = new JButton("Take Screenshot");
screenshotBtn.addActionListener(e -> saveSnapshot());
recordBtn = new JButton("Start Recording");
recordBtn.addActionListener(e -> toggleRecording());
panel.add(screenshotBtn);
panel.add(recordBtn);
// Wrap in a wrapper to prevent buttons from stretching too much
JPanel wrapper = new JPanel(new BorderLayout());
wrapper.add(panel, BorderLayout.NORTH);
return wrapper;
}
private void toggleRecording() {
if (!videoRecorder.isRecording()) {
startVideo();
} else {
stopVideo();
private void openRecordingFrame() {
RecordingFrame rf = new RecordingFrame(this.getTitle(),cameraPanel, videoRecorder);
JDesktopPane desktopPane = getDesktopPane();
if (desktopPane != null) {
desktopPane.add(rf);
rf.setVisible(true);
}
}
private void startVideo() {
try {
File file = new File(System.getProperty("user.home"), "vid_" + System.currentTimeMillis() + ".mp4");
videoRecorder.startRecording(cameraPanel, file);
recordBtn.setText("Stop Recording");
recordBtn.setForeground(Color.RED);
} catch (IOException ex) {
showError("Failed to start recording", ex);
rf.setSelected(true);
} catch (java.beans.PropertyVetoException veto) {
ShowError.error(null,"VetoException"+veto.getMessage(),"veto");
}
}
private void stopVideo() {
try {
File savedFile = videoRecorder.stopRecording();
recordBtn.setText("Start Recording");
recordBtn.setForeground(Color.BLACK);
JOptionPane.showMessageDialog(this, "Video saved to: " + savedFile.getAbsolutePath());
} catch (IOException ex) {
showError("Failed to stop recording safely", ex);
}
}
private void saveSnapshot() {
BufferedImage img = cameraPanel.getCurrentProcessedImage();
if (img != null) {
try {
File file = new File(System.getProperty("user.home"), "snap_" + System.currentTimeMillis() + ".png");
ImageIO.write(img, "PNG", file);
} catch (Exception ex) {
showError("Snapshot failed", ex);
}
}
}
private void showError(String title, Exception ex) {
JOptionPane.showMessageDialog(this, title + "\n" + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
public CameraPanel getCameraPanel() { return cameraPanel; }
@Override
public void dispose() {
// Safety check: stop recording if the window is closed
if (videoRecorder.isRecording()) {
try { videoRecorder.stopRecording(); } catch (IOException ignored) {}
}

View File

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

View File

@@ -0,0 +1,178 @@
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 javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
public class RecordingFrame extends JInternalFrame {
private final VideoRecorder videoRecorder;
private final CameraPanel cameraPanel;
private JButton recordBtn;
private JLabel statusLabel;
private JLabel statsLabel;
private File outputDirectory = new File(System.getProperty("user.home"));
private File currentFile;
private final Timer statsTimer;
private long startTime;
// 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) {
super(cameraName + " Capture", true, true, false, true);
Image ico = IconSetter.getIcon();
setFrameIcon(new ImageIcon(ico));
this.cameraPanel = cameraPanel;
this.videoRecorder = videoRecorder;
initializeUI();
this.statsTimer = new Timer(1000, e -> updateStats());
this.statsTimer.setCoalesce(true);
pack();
}
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());
mainContent.add(Box.createVerticalStrut(10));
mainContent.add(createActionPanel());
mainContent.add(Box.createVerticalStrut(10));
mainContent.add(createStatsPanel());
getContentPane().add(mainContent);
}
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 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");
} catch (IOException ex) {
ShowError.error(null,"Snapshot failed"+ex.getMessage(),"Snapshot Error");
}
}
}
}

View File

@@ -1,126 +0,0 @@
package io.swtc.proccessing.ui.iframe;
import io.swtc.proccessing.CameraPanel;
import io.swtc.recording.VideoRecorder;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
public class RecordingPane extends JPanel {
private final VideoRecorder videoRecorder;
private final CameraPanel cameraPanel;
private JTextField pathField;
private JButton recordBtn;
private JLabel statusLabel;
private File outputDirectory;
public RecordingPane(CameraPanel cameraPanel, VideoRecorder videoRecorder) {
this.cameraPanel = cameraPanel;
this.videoRecorder = videoRecorder;
this.outputDirectory = new File(System.getProperty("user.home"));
setLayout(new GridBagLayout());
JPanel contentPanel = new JPanel();
contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
contentPanel.setPreferredSize(new Dimension(400, 250));
// Add the functional sections
contentPanel.add(createStoragePanel());
contentPanel.add(Box.createVerticalStrut(15));
contentPanel.add(createActionPanel());
contentPanel.add(Box.createVerticalStrut(15));
contentPanel.add(createStatusPanel());
add(contentPanel);
}
private JPanel createStoragePanel() {
JPanel panel = new JPanel(new BorderLayout(5, 5));
panel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(), "Storage Settings", TitledBorder.LEFT, TitledBorder.TOP));
pathField = new JTextField(outputDirectory.getAbsolutePath());
pathField.setEditable(false);
JButton browseBtn = new JButton("Browse...");
browseBtn.addActionListener(e -> {
JFileChooser chooser = new JFileChooser(outputDirectory);
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
outputDirectory = chooser.getSelectedFile();
pathField.setText(outputDirectory.getAbsolutePath());
}
});
panel.add(pathField, BorderLayout.CENTER);
panel.add(browseBtn, BorderLayout.EAST);
return panel;
}
private JPanel createActionPanel() {
JPanel panel = new JPanel(new GridLayout(1, 2, 10, 10));
recordBtn = new JButton("start recording");
recordBtn.addActionListener(e -> toggleRecording());
JButton snapBtn = new JButton("take snapshot");
snapBtn.addActionListener(e -> takeSnapshot());
panel.add(recordBtn);
panel.add(snapBtn);
return panel;
}
private JPanel createStatusPanel() {
JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER));
statusLabel = new JLabel("");
statusLabel.setForeground(Color.DARK_GRAY);
panel.add(statusLabel);
return panel;
}
private void toggleRecording() {
if (!videoRecorder.isRecording()) {
try {
File file = new File(outputDirectory, "vid_" + System.currentTimeMillis() + ".mp4");
videoRecorder.startRecording(cameraPanel, file);
recordBtn.setText("stop recording");
statusLabel.setText("recording");
} catch (IOException ex) {
showError("Start Error", ex);
}
} else {
try {
File saved = videoRecorder.stopRecording();
recordBtn.setText("Start Recording");
statusLabel.setText("Status: Saved " + saved.getName());
} catch (IOException ex) {
showError("Stop Error", ex);
}
}
}
private void takeSnapshot() {
BufferedImage img = cameraPanel.getCurrentProcessedImage();
if (img != null) {
try {
File file = new File(outputDirectory, "snap_" + System.currentTimeMillis() + ".png");
ImageIO.write(img, "PNG", file);
statusLabel.setText("captured");
} catch (IOException ex) {
showError("Snapshot Error", ex);
}
}
}
private void showError(String title, Exception ex) {
JOptionPane.showMessageDialog(this, ex.getMessage(), title, JOptionPane.ERROR_MESSAGE);
}
}

View File

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

@@ -6,109 +6,82 @@ import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.common.model.Rational;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
/*
*
* This is not that interesting but surely improtant for any security based people,
* here we record using jcodec, which is more efficient than me writing my own recorder,
* which i am certainly not doing.
*
* Anyways we dont do shit properly anyway.
*
* */
import java.util.concurrent.*;
public class VideoRecorder {
private boolean recording = false;
private Timer captureTimer;
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;
channel = NIOUtils.writableFileChannel(String.valueOf(outputFile));
encoder = new AWTSequenceEncoder(channel, Rational.R(FPS, 1));
workerThread = Executors.newSingleThreadExecutor();
workerThread.submit(this::processQueue);
captureTimer = new Timer(1000 / FPS, e -> {
try {
BufferedImage img = panel.getCurrentProcessedImage();
if (img != null) {
BufferedImage rgbImage = convertToRGB(img);
encoder.encodeImage(rgbImage);
}
} catch (IOException ex) {
JOptionPane.showMessageDialog(
null,
"IOException\n" + ex,
"IOException in Recorder",
JOptionPane.ERROR_MESSAGE
);
new Thread(() -> {
while (recording) {
try {
stopRecording();
} catch (IOException stopEx) {
JOptionPane.showMessageDialog(
null,
"IOException@stopEx\n" + stopEx,
"IOException in Recorder@stopEx",
JOptionPane.ERROR_MESSAGE
);
BufferedImage img = panel.getCurrentProcessedImage();
if (img != null) {
BufferedImage copy = snapshotToRGB(img);
frameQueue.offer(copy);
}
Thread.sleep(1000 / FPS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
captureTimer.start();
}).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;
/* some helper methods, i swear its always the same? */
if (captureTimer != null) {
captureTimer.stop();
workerThread.shutdown();
try {
workerThread.awaitTermination(5, TimeUnit.SECONDS);
if (encoder != null) encoder.finish();
if (channel != null) channel.close();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
if (encoder != null) {
encoder.finish();
}
if (channel != null) {
channel.close();
}
return outputFile;
}
public boolean isRecording() {
return recording;
}
// maybe i should move this to a component, some useless conversion right here,
// (performance wise)
// yes, capitain?
private BufferedImage convertToRGB(BufferedImage source) {
if (source.getType() == BufferedImage.TYPE_INT_RGB) {
return source;
}
BufferedImage rgb = new BufferedImage(
source.getWidth(),
source.getHeight(),
BufferedImage.TYPE_INT_RGB
);
for (int y = 0; y < source.getHeight(); y++) {
for (int x = 0; x < source.getWidth(); x++) {
rgb.setRGB(x, y, source.getRGB(x, y) & 0xFFFFFF);
}
}
return rgb;
}
public boolean isRecording() { return recording; }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB