1 Commits

Author SHA1 Message Date
d775a33107 some more performance update:
Changed:
+ FFmpeg with JavaCV
+ Exporting to a USB

Removed:
- JCodec!
2026-01-29 16:40:36 +01:00
14 changed files with 766 additions and 327 deletions

120
pom.xml
View File

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

View File

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

View File

@@ -1,11 +1,11 @@
package io.swtc.proccessing; package io.swtc.proccessing;
import io.swtc.proccessing.ui.ShowError; import io.swtc.proccessing.ui.ShowError;
import io.swtc.recording.cv.AVRecorder;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.awt.event.*; import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.Arrays; import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -16,6 +16,8 @@ public class CameraPanel extends JPanel {
private volatile BufferedImage sourceImage; private volatile BufferedImage sourceImage;
private volatile BufferedImage processedImage; private volatile BufferedImage processedImage;
private Function<BufferedImage, BufferedImage> imageProcessor; private Function<BufferedImage, BufferedImage> imageProcessor;
private volatile AVRecorder recorder; // this is now javacv, jcodec is stoopid
private final AtomicBoolean repaintScheduled = new AtomicBoolean(false); private final AtomicBoolean repaintScheduled = new AtomicBoolean(false);
private double zoomLevel = 1.0; private double zoomLevel = 1.0;
@@ -24,22 +26,25 @@ public class CameraPanel extends JPanel {
private Point dragStartPoint; private Point dragStartPoint;
private static final double MIN_ZOOM = 1.0; private static final double MIN_ZOOM = 1.0;
private static final double MAX_ZOOM = 10.0; // ten is enough if ur using 640x480 private static final double MAX_ZOOM = 10.0;
private static final double ZOOM_MULTIPLIER = 1.1; private static final double ZOOM_MULTIPLIER = 1.1;
private final GraphicsConfiguration graphicsConfig;
public CameraPanel() { public CameraPanel() {
setBackground(Color.BLACK); setBackground(Color.BLACK);
setPreferredSize(new Dimension(640, 480)); setPreferredSize(new Dimension(640, 480));
graphicsConfig = GraphicsEnvironment.getLocalGraphicsEnvironment()
.getDefaultScreenDevice().getDefaultConfiguration();
initInteractionListeners(); initInteractionListeners();
} }
private void initInteractionListeners() { private void initInteractionListeners() {
MouseAdapter mouseHandler = new MouseAdapter() { MouseAdapter mouseHandler = new MouseAdapter() {
@Override @Override
public void mouseWheelMoved(MouseWheelEvent e) { public void mouseWheelMoved(MouseWheelEvent e) { handleZoom(e); }
handleZoom(e);
}
@Override @Override
public void mousePressed(MouseEvent e) { public void mousePressed(MouseEvent e) {
@@ -49,17 +54,14 @@ public class CameraPanel extends JPanel {
@Override @Override
public void mouseClicked(MouseEvent e) { public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) if (e.getClickCount() == 2) resetView();
resetView();
} }
@Override @Override
public void mouseReleased(MouseEvent e) { setCursor(Cursor.getDefaultCursor()); } public void mouseReleased(MouseEvent e) { setCursor(Cursor.getDefaultCursor()); }
@Override @Override
public void mouseDragged(MouseEvent e) { public void mouseDragged(MouseEvent e) { handlePan(e); }
handlePan(e);
}
@Override @Override
public void mouseExited(MouseEvent e) { setCursor(Cursor.getDefaultCursor()); } public void mouseExited(MouseEvent e) { setCursor(Cursor.getDefaultCursor()); }
@@ -70,10 +72,20 @@ public class CameraPanel extends JPanel {
addMouseMotionListener(mouseHandler); addMouseMotionListener(mouseHandler);
} }
public void setExternalRecorder(AVRecorder recorder) {
this.recorder = recorder;
}
public void setImage(BufferedImage img) { public void setImage(BufferedImage img) {
sourceImage = img; this.sourceImage = img;
updateProcessedImage(); updateProcessedImage();
// Feed the AVRecorder using its 'accept' method if active
AVRecorder currentRecorder = this.recorder;
if (currentRecorder != null && currentRecorder.isRecording() && processedImage != null) {
currentRecorder.accept(processedImage);
}
scheduleRepaint(); scheduleRepaint();
} }
@@ -85,19 +97,46 @@ public class CameraPanel extends JPanel {
} }
} }
public BufferedImage getCurrentProcessedImage() {
return processedImage;
}
public void resetView() {
zoomLevel = 1.0;
xOffset = 0;
yOffset = 0;
repaint();
}
private void updateProcessedImage() { private void updateProcessedImage() {
if (sourceImage == null) return; if (sourceImage == null) return;
BufferedImage temp;
if (imageProcessor != null) { if (imageProcessor != null) {
try { try {
processedImage = imageProcessor.apply(sourceImage); temp = imageProcessor.apply(sourceImage);
} catch (Exception e) { } catch (Exception e) {
ShowError.error(null,"Fucked up in rendering \n" + Arrays.toString(e.getStackTrace()),"Problem"); ShowError.error(null, "Error in image processing: \n" + Arrays.toString(e.getStackTrace()), "Processing Error");
processedImage = sourceImage; // Fallback temp = sourceImage;
} }
} else { } else {
processedImage = sourceImage; temp = sourceImage;
} }
if (processedImage == null ||
processedImage.getWidth() != temp.getWidth() ||
processedImage.getHeight() != temp.getHeight()) {
processedImage = graphicsConfig.createCompatibleImage(
temp.getWidth(),
temp.getHeight(),
temp.getTransparency()
);
}
Graphics2D g2d = processedImage.createGraphics();
g2d.drawImage(temp, 0, 0, null);
g2d.dispose();
} }
private void scheduleRepaint() { private void scheduleRepaint() {
@@ -109,24 +148,46 @@ public class CameraPanel extends JPanel {
} }
} }
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (processedImage == null) return;
Graphics2D g2d = (Graphics2D) g.create();
try {
g2d.setRenderingHint(
RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR
);
g2d.translate((int)xOffset, (int)yOffset);
double scaleX = (double) getWidth() / processedImage.getWidth();
double scaleY = (double) getHeight() / processedImage.getHeight();
g2d.scale(scaleX * zoomLevel, scaleY * zoomLevel);
g2d.drawImage(processedImage, 0, 0, null);
} finally {
g2d.dispose();
}
}
private void handleZoom(MouseWheelEvent e) { private void handleZoom(MouseWheelEvent e) {
if (processedImage == null) return; if (processedImage == null) return;
double oldZoom = zoomLevel; double oldZoom = zoomLevel;
if (e.getWheelRotation() < 0) { if (e.getWheelRotation() < 0) {
zoomLevel *= ZOOM_MULTIPLIER; // Zoom In zoomLevel *= ZOOM_MULTIPLIER;
} else { } else {
zoomLevel /= ZOOM_MULTIPLIER; // Zoom Out zoomLevel /= ZOOM_MULTIPLIER;
} }
// clamp shit
zoomLevel = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoomLevel)); zoomLevel = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoomLevel));
if (oldZoom != zoomLevel) { if (oldZoom != zoomLevel) {
double xRel = e.getX() - xOffset; double xRel = e.getX() - xOffset;
double yRel = e.getY() - yOffset; double yRel = e.getY() - yOffset;
double zoomFactor = zoomLevel / oldZoom; double zoomFactor = zoomLevel / oldZoom;
xOffset = e.getX() - (xRel * zoomFactor); xOffset = e.getX() - (xRel * zoomFactor);
@@ -140,15 +201,10 @@ public class CameraPanel extends JPanel {
private void handlePan(MouseEvent e) { private void handlePan(MouseEvent e) {
if (processedImage == null || dragStartPoint == null) return; if (processedImage == null || dragStartPoint == null) return;
// Calculate delta xOffset += e.getX() - dragStartPoint.x;
int dx = e.getX() - dragStartPoint.x; yOffset += e.getY() - dragStartPoint.y;
int dy = e.getY() - dragStartPoint.y;
xOffset += dx;
yOffset += dy;
dragStartPoint = e.getPoint(); dragStartPoint = e.getPoint();
checkBounds(); checkBounds();
repaint(); repaint();
} }
@@ -167,42 +223,4 @@ public class CameraPanel extends JPanel {
yOffset = getHeight() - viewedHeight; yOffset = getHeight() - viewedHeight;
} }
} }
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (processedImage == null) return;
Graphics2D g2d = (Graphics2D) g;
AffineTransform originalTransform = g2d.getTransform();
g2d.setRenderingHint(
RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR
);
g2d.translate(xOffset, yOffset);
double scaleX = (double) getWidth() / processedImage.getWidth();
double scaleY = (double) getHeight() / processedImage.getHeight();
g2d.scale(scaleX * zoomLevel, scaleY * zoomLevel);
g2d.drawImage(processedImage, 0, 0, null);
g2d.setTransform(originalTransform);
}
public BufferedImage getCurrentProcessedImage() {
return processedImage;
}
public void resetView() {
zoomLevel = 1.0;
xOffset = 0;
yOffset = 0;
repaint();
}
} }

View File

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

View File

@@ -3,7 +3,10 @@ package io.swtc.proccessing.ui.iframe;
import io.swtc.proccessing.CameraPanel; import io.swtc.proccessing.CameraPanel;
import io.swtc.proccessing.ui.IconSetter; import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.ShowError; import io.swtc.proccessing.ui.ShowError;
import io.swtc.recording.VideoRecorder; import io.swtc.proccessing.ui.sections.recording.ExportSection;
import io.swtc.recording.cv.AVRecorder;
import io.swtc.recording.cv.RecorderConfig;
import io.swtc.recording.evidence.USBExportManager;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
@@ -11,51 +14,72 @@ import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.nio.file.Path;
public class RecordingFrame extends JInternalFrame { public class RecordingFrame extends JInternalFrame {
private final VideoRecorder videoRecorder; private AVRecorder avRecorder;
private ExportSection exportSection;
private final CameraPanel cameraPanel; private final CameraPanel cameraPanel;
private JButton recordBtn; private JButton recordBtn;
private JLabel statusLabel; private JLabel statusLabel;
private JLabel statsLabel; private JLabel statsLabel;
private File outputDirectory = new File(System.getProperty("user.home")); private File outputDirectory;
private File currentFile; private File currentFile;
private final Timer statsTimer; private final Timer statsTimer;
private long startTime; private long startTime;
// Cache for string building to avoid object allocation every second
private final StringBuilder sb = new StringBuilder(32); private final StringBuilder sb = new StringBuilder(32);
public RecordingFrame(String cameraName, CameraPanel cameraPanel, VideoRecorder videoRecorder) { public RecordingFrame(String cameraName, CameraPanel cameraPanel) {
super(cameraName + " Capture", true, true, false, true); super(cameraName + " Capture", true, true, false, true);
setupDirectory();
Image ico = IconSetter.getIcon(); Image ico = IconSetter.getIcon();
setFrameIcon(new ImageIcon(ico)); setFrameIcon(new ImageIcon(ico));
this.cameraPanel = cameraPanel; this.cameraPanel = cameraPanel;
this.videoRecorder = videoRecorder;
initializeUI(); initializeUI();
// Timer for UI updates only (1 FPS is enough for the clock)
this.statsTimer = new Timer(1000, e -> updateStats()); this.statsTimer = new Timer(1000, e -> updateStats());
this.statsTimer.setCoalesce(true); this.statsTimer.setCoalesce(true);
pack(); pack();
} }
private void setupDirectory() {
File videoDir = new File(System.getProperty("user.home"), "Videos");
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());
}
}
}
private void initializeUI() { private void initializeUI() {
JPanel mainContent = new JPanel(); JPanel mainContent = new JPanel();
mainContent.setLayout(new BoxLayout(mainContent, BoxLayout.Y_AXIS)); mainContent.setLayout(new BoxLayout(mainContent, BoxLayout.Y_AXIS));
mainContent.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); mainContent.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
mainContent.add(createStoragePanel()); exportSection = new ExportSection(this, outputDirectory, statusLabel);
JPanel actionPanel = createActionPanel();
JPanel statsPanel = createStatsPanel();
mainContent.add(exportSection);
mainContent.add(Box.createVerticalStrut(10)); mainContent.add(Box.createVerticalStrut(10));
mainContent.add(createActionPanel()); mainContent.add(statsPanel);
mainContent.add(Box.createVerticalStrut(10)); mainContent.add(Box.createVerticalStrut(10));
mainContent.add(createStatsPanel()); mainContent.add(actionPanel);
getContentPane().add(mainContent); getContentPane().add(mainContent);
} }
@@ -65,11 +89,9 @@ public class RecordingFrame extends JInternalFrame {
panel.setBorder(BorderFactory.createTitledBorder("Session Info")); panel.setBorder(BorderFactory.createTitledBorder("Session Info"));
statusLabel = new JLabel("Status: Idle"); statusLabel = new JLabel("Status: Idle");
statsLabel = new JLabel("Length: 00:00 | Size: 0.00 MB"); statsLabel = new JLabel("Length: 00:00 | Size: 0.00 MB");
statsLabel.setFont(new Font("Monospaced", Font.PLAIN, 12)); statsLabel.setFont(new Font("Monospaced", Font.PLAIN, 12));
statsLabel.setPreferredSize(new Dimension(240, 20));
statsLabel.setPreferredSize(new Dimension(220, 20));
panel.add(statusLabel); panel.add(statusLabel);
panel.add(statsLabel); panel.add(statsLabel);
@@ -77,7 +99,7 @@ public class RecordingFrame extends JInternalFrame {
} }
private void updateStats() { private void updateStats() {
if (!videoRecorder.isRecording() || currentFile == null) return; if (avRecorder == null || !avRecorder.isRecording() || currentFile == null) return;
long elapsedSecs = (System.currentTimeMillis() - startTime) / 1000; long elapsedSecs = (System.currentTimeMillis() - startTime) / 1000;
long minutes = elapsedSecs / 60; long minutes = elapsedSecs / 60;
@@ -90,14 +112,15 @@ public class RecordingFrame extends JInternalFrame {
.append(minutes < 10 ? "0" : "").append(minutes).append(":") .append(minutes < 10 ? "0" : "").append(minutes).append(":")
.append(seconds < 10 ? "0" : "").append(seconds) .append(seconds < 10 ? "0" : "").append(seconds)
.append(" | Size: ") .append(" | Size: ")
.append(Math.round(sizeInMb * 100.0) / 100.0) .append(String.format("%.2f", sizeInMb))
.append(" MB"); .append(" MB");
statsLabel.setText(sb.toString()); statsLabel.setText(sb.toString());
} }
private void toggleRecording() { private void toggleRecording() {
if (!videoRecorder.isRecording()) { // Updated check for the new recorder state
if (avRecorder == null || !avRecorder.isRecording()) {
startRec(); startRec();
} else { } else {
stopRec(); stopRec();
@@ -105,38 +128,70 @@ public class RecordingFrame extends JInternalFrame {
} }
private void startRec() { private void startRec() {
BufferedImage sample = cameraPanel.getCurrentProcessedImage();
if (sample == null) {
ShowError.warning(this, "No camera feed detected. Start camera first.", "Warning");
return;
}
try { try {
if (!outputDirectory.exists() && !outputDirectory.mkdirs()) {
throw new IOException("Failed to create directory: " + outputDirectory);
}
currentFile = new File(outputDirectory, "vid_" + System.currentTimeMillis() + ".mp4"); currentFile = new File(outputDirectory, "vid_" + System.currentTimeMillis() + ".mp4");
videoRecorder.startRecording(cameraPanel, currentFile);
// 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(); startTime = System.currentTimeMillis();
statsTimer.start(); // Only run timer when needed statsTimer.start();
recordBtn.setText("Stop Recording"); recordBtn.setText("Stop Recording");
statusLabel.setText("Currently Recording"); recordBtn.setForeground(Color.RED);
} catch (IOException ex) { statusLabel.setText("Recording...");
ShowError.error(null,"Error starting Recording" + ex.getMessage(), "Error"); } catch (Exception ex) {
ShowError.error(this, "Failed to start Recorder: " + ex.getMessage(), "Error");
ex.printStackTrace();
} }
} }
private void stopRec() { private void stopRec() {
try { if (avRecorder != null) {
videoRecorder.stopRecording(); statusLabel.setText("Finalizing file...");
statsTimer.stop(); avRecorder.stop();
cameraPanel.setExternalRecorder(null);
recordBtn.setText("Started Recording");
recordBtn.setForeground(null);
statusLabel.setText("Finished recording");
} catch (IOException ex) {
ShowError.error(null,"RecordStop Error"+ex.getMessage(), "Error");
} }
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() { private JPanel createStoragePanel() {
JPanel panel = new JPanel(new BorderLayout(5, 5)); JPanel panel = new JPanel(new BorderLayout(5, 5));
panel.setBorder(BorderFactory.createTitledBorder("Storage")); panel.setBorder(BorderFactory.createTitledBorder("Storage Folder"));
JTextField pathField = new JTextField(outputDirectory.getAbsolutePath()); JTextField pathField = new JTextField(outputDirectory.getAbsolutePath());
pathField.setEditable(false); pathField.setEditable(false);
JButton browseBtn = new JButton("..."); JButton browseBtn = new JButton("...");
browseBtn.addActionListener(e -> { browseBtn.addActionListener(e -> {
JFileChooser chooser = new JFileChooser(outputDirectory); JFileChooser chooser = new JFileChooser(outputDirectory);
@@ -146,17 +201,70 @@ public class RecordingFrame extends JInternalFrame {
pathField.setText(outputDirectory.getAbsolutePath()); pathField.setText(outputDirectory.getAbsolutePath());
} }
}); });
panel.add(pathField, BorderLayout.CENTER); panel.add(pathField, BorderLayout.CENTER);
panel.add(browseBtn, BorderLayout.EAST); panel.add(browseBtn, BorderLayout.EAST);
return panel; 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() { private JPanel createActionPanel() {
JPanel panel = new JPanel(new GridLayout(1, 2, 5, 5)); JPanel panel = new JPanel(new GridLayout(1, 2, 5, 5));
recordBtn = new JButton("Start Recording"); recordBtn = new JButton("Start Recording");
recordBtn.addActionListener(e -> toggleRecording()); recordBtn.addActionListener(e -> toggleRecording());
JButton snapBtn = new JButton("Snapshot"); JButton snapBtn = new JButton("Snapshot");
snapBtn.addActionListener(e -> takeSnapshot()); snapBtn.addActionListener(e -> takeSnapshot());
// Export is now handled by the ExportSection component
panel.add(recordBtn); panel.add(recordBtn);
panel.add(snapBtn); panel.add(snapBtn);
return panel; return panel;
@@ -166,13 +274,13 @@ public class RecordingFrame extends JInternalFrame {
BufferedImage img = cameraPanel.getCurrentProcessedImage(); BufferedImage img = cameraPanel.getCurrentProcessedImage();
if (img != null) { if (img != null) {
try { try {
if (!outputDirectory.exists()) outputDirectory.mkdirs();
File file = new File(outputDirectory, "snap_" + System.currentTimeMillis() + ".png"); File file = new File(outputDirectory, "snap_" + System.currentTimeMillis() + ".png");
ImageIO.write(img, "PNG", file); ImageIO.write(img, "PNG", file);
statusLabel.setText("Saved a Snapshot"); statusLabel.setText("Snapshot: " + file.getName());
} catch (IOException ex) { } catch (IOException ex) {
ShowError.error(null,"Snapshot failed"+ex.getMessage(),"Snapshot Error"); ShowError.error(this, "Snapshot failed: " + ex.getMessage(), "Error");
} }
} }
} }
} }

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,60 @@
package io.swtc.recording.cv;
import io.swtc.proccessing.ui.ShowError;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class AVRecorder {
private final BlockingQueue<BufferedImage> queue = new LinkedBlockingQueue<>(120);
private final RecorderConfig config;
private volatile boolean running = false;
public AVRecorder(RecorderConfig config) {
this.config = config;
}
public void start() {
running = true;
Thread worker = new Thread(this::runLoop, "Recording-Worker");
worker.start();
}
private void runLoop() {
FrameProccessor processor = new FrameProccessor();
MediaSink sink = null;
try {
while (running || !queue.isEmpty()) {
BufferedImage img = queue.poll(500, TimeUnit.MILLISECONDS);
if (img == null) continue;
if (sink == null) {
sink = new MediaSink(config);
}
sink.write(processor.convert(img));
}
} catch (Exception e) {
ShowError.error(null,"Error in AVRecorder", "AVRecorder isn't responding!");
} finally {
if (sink != null) sink.stop();
processor.close();
}
}
public void accept(BufferedImage img) {
if (!queue.offer(img)) {
System.err.println("Recorder lag: Frame dropped");
}
}
public boolean isRecording() { return running; }
public void stop() {
running = false;
}
}

View File

@@ -0,0 +1,32 @@
package io.swtc.recording.cv;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import java.awt.image.BufferedImage;
import java.util.Objects;
public class FrameProccessor {
private final Java2DFrameConverter converter = new Java2DFrameConverter();
private BufferedImage reuseImg;
public Frame convert(BufferedImage rawImg) {
if (Objects.isNull(reuseImg)) {
reuseImg = new BufferedImage(
rawImg.getWidth(),
rawImg.getHeight(),
BufferedImage.TYPE_3BYTE_BGR // default java BufferedImage Type
);
var g = reuseImg.createGraphics();
g.drawImage(rawImg, 0, 0, null);
g.dispose();
}
return converter.getFrame(reuseImg);
}
public void close() {
converter.close();
if (!Objects.isNull(reuseImg)) { reuseImg.flush(); }
}
}

View File

@@ -0,0 +1,45 @@
package io.swtc.recording.cv;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.Frame;
public class MediaSink {
private final FFmpegFrameRecorder recorder;
private final long startNanos;
public MediaSink(RecorderConfig config) throws Exception {
this.recorder = new FFmpegFrameRecorder(config.outputFile(), config.width(), config.height());
this.startNanos = System.nanoTime();
avutil.av_log_set_level(avutil.AV_LOG_ERROR);
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setFormat("mp4");
recorder.setPixelFormat(avutil.AV_PIX_FMT_BGR24);
recorder.setFrameRate(config.fps());
recorder.setVideoOption("pixel_format", "yuv420p");
recorder.setVideoOption("preset", config.preset());
recorder.setVideoOption("crf", String.valueOf(config.crf()));
recorder.setGopSize(config.fps() * 2);
recorder.start();
}
public void write(Frame frame) throws Exception {
long pts = (System.nanoTime() - startNanos) / 1000;
if (pts > recorder.getTimestamp()) {
recorder.setTimestamp(pts);
recorder.record(frame);
}
}
public void stop() {
try {
recorder.stop();
recorder.release();
} catch (Exception ignored) { /* Do absolutley nothing ;) */ }
}
}

View File

@@ -0,0 +1,12 @@
package io.swtc.recording.cv;
import java.io.File;
public record RecorderConfig(
File outputFile,
int width,
int height,
int fps,
int crf, // Quality, 18 is high and 28 is low
String preset // ultrafast
) { /* Record */ }

View File

@@ -0,0 +1,42 @@
package io.swtc.recording.evidence;
import java.util.concurrent.TimeUnit;
public record ExportStats(
long totalBytes,
long copiedBytes,
long startTimeNanos
) {
public int percent() {
return totalBytes == 0 ? 0 :
(int) ((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";
long elapsedNanos = System.nanoTime() - startTimeNanos;
double bytesPerNano = (double) copiedBytes / elapsedNanos;
long remainingBytes = totalBytes - copiedBytes;
long remainingNanos = (long) (remainingBytes / bytesPerNano);
return formatDuration(remainingNanos);
}
private static String formatDuration(long nanos) {
long seconds = TimeUnit.NANOSECONDS.toSeconds(nanos);
long mins = seconds / 60;
long secs = seconds % 60;
if (mins > 0) {
return mins + "m " + secs + "s";
}
return secs + "s";
}
}

View File

@@ -0,0 +1,114 @@
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.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;
}
});
return size.get();
}
public static boolean hasEnoughSpace(Path target, long requiredBytes) throws IOException {
FileStore store = Files.getFileStore(target);
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);
}
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();
}
}