diff --git a/.gitignore b/.gitignore index 7568db7..d1c36b7 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,7 @@ dependency-reduced-pom.xml .idea ## This is for our app, cause it likes to store stuff ## -network_cameras.json \ No newline at end of file +network_cameras.json + +## exec launch4j config ## +execfg.xml \ No newline at end of file diff --git a/pom.xml b/pom.xml index 5e4fbc7..4bccba9 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ io.swtc swtc 1.0-SNAPSHOT - + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.1 + + + package + + shade + + + + + + io.swtc.Main + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + 17 @@ -63,30 +100,30 @@ 0.3.12 - - - org.lwjgl - lwjgl - 3.3.3 - - - org.lwjgl - lwjgl-opengl - 3.3.3 - + + + + + + + + + + + - - org.lwjgl - lwjgl - 3.3.3 - natives-windows - - - org.lwjgl - lwjgl-opengl - 3.3.3 - natives-windows - + + + + + + + + + + + + @@ -106,22 +143,35 @@ 2.20.1 - - - - org.jcodec - jcodec - 0.2.5 - + + + + + + + + + - - - org.jcodec - jcodec-javase - 0.2.5 - + + + + + + + + + org.bytedeco + javacv + 1.5.10 + + + + org.bytedeco + ffmpeg + 6.1.1-1.5.10 + windows-x86_64 + \ No newline at end of file diff --git a/readme.md b/readme.md index b8e459f..19d6bf7 100644 --- a/readme.md +++ b/readme.md @@ -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): diff --git a/src/main/java/io/swtc/Main.java b/src/main/java/io/swtc/Main.java index 1f9211f..4eb4f76 100644 --- a/src/main/java/io/swtc/Main.java +++ b/src/main/java/io/swtc/Main.java @@ -2,20 +2,12 @@ package io.swtc; import javax.swing.*; -import io.swtc.proccessing.ui.ShowError; - public class Main { public static void main(String[] args) { - for (int i = 0; i < args.length; i++) { - System.out.println("Arg " + i + ": " + args[i]); - } - try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - } catch (Exception e) { - ShowError.warning(null,"LaF Warn","LaF"); - } + } catch (Exception e) { /* Do nothing */ } SwingCCTVManager.main(null); } diff --git a/src/main/java/io/swtc/SwingCCTVManager.java b/src/main/java/io/swtc/SwingCCTVManager.java index 89cd90a..9327aa5 100644 --- a/src/main/java/io/swtc/SwingCCTVManager.java +++ b/src/main/java/io/swtc/SwingCCTVManager.java @@ -2,25 +2,37 @@ package io.swtc; import com.github.sarxos.webcam.Webcam; import com.github.sarxos.webcam.WebcamCompositeDriver; +import com.github.sarxos.webcam.WebcamDevice; import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDriver; -import com.github.sarxos.webcam.ds.ipcam.IpCamDeviceRegistry; -import com.github.sarxos.webcam.ds.ipcam.IpCamDriver; -import com.github.sarxos.webcam.ds.ipcam.IpCamMode; +import com.github.sarxos.webcam.ds.ipcam.*; import io.swtc.networking.CameraConfig; import io.swtc.networking.CameraSettings; +import io.swtc.proccessing.ui.IconSetter; +import io.swtc.proccessing.ui.ShowError; import javax.swing.*; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.DefaultTableModel; import java.awt.*; +import java.awt.datatransfer.DataFlavor; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.io.File; +import java.net.InetSocketAddress; import java.net.MalformedURLException; +import java.net.Socket; +import java.net.URL; import java.util.List; public class SwingCCTVManager { + private static final String TIMEOUT_MS = "1500"; + private static final int REFRESH_INTERVAL = 30000; + private static final String STATUS_ONLINE = "ONLINE"; + private static final String STATUS_OFFLINE = "OFFLINE"; + static { + System.setProperty("ipcam.connection.timeout", TIMEOUT_MS); Webcam.setDriver(new WebcamCompositeDriver() {{ add(new WebcamDefaultDriver()); add(new IpCamDriver()); @@ -32,206 +44,237 @@ public class SwingCCTVManager { private final JTable deviceTable; private final DefaultTableModel tableModel; private final SwingIFrame IFrame; - + private boolean isRefreshing = false; public SwingCCTVManager() { - frame = new JFrame("dashboard"); + frame = new JFrame("Dashboard"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - frame.setSize(1000, 600); + frame.setSize(1100, 600); + frame.setIconImage(IconSetter.getIcon()); this.IFrame = new SwingIFrame(); this.IFrame.show(); - String[] columns = {"Status", "Device Name", "Type", "Resolution", "Address"}; - tableModel = new DefaultTableModel(columns, 0) { + tableModel = new DefaultTableModel(new String[]{"Status", "Device Name", "Type", "Resolution", "Address"}, 0) { @Override - public boolean isCellEditable(int r, int c) { - return false; - } + public boolean isCellEditable(int r, int c) { return false; } }; deviceTable = new JTable(tableModel); - - deviceTable.setRowSelectionAllowed(true); - deviceTable.setFocusable(true); - deviceTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); - setupTableAppearance(); - - deviceTable.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - deviceTable.requestFocusInWindow(); - - if (e.getClickCount() == 2 && deviceTable.getSelectedRow() != -1) { - launchSelected(); - } - if (SwingUtilities.isRightMouseButton(e)) { - showContextMenu(e); - } - } - }); + setupDragAndDrop(); // Initialize DND + setupListeners(); JToolBar toolBar = new JToolBar(); - JButton btnAdd = new JButton("Add IP Cam"); - JButton btnLaunch = new JButton("Launch Stream"); - toolBar.add(btnAdd); - toolBar.addSeparator(); - toolBar.add(btnLaunch); + setupBrandedToolbar(toolBar); - frame.add(toolBar, BorderLayout.NORTH); + frame.add(toolBar, BorderLayout.SOUTH); frame.add(new JScrollPane(deviceTable), BorderLayout.CENTER); - btnAdd.addActionListener(e -> showAddCameraDialog()); - btnLaunch.addActionListener(e -> launchSelected()); - startAutoRefresh(); refreshTable(); } - private void setupTableAppearance() { - deviceTable.getColumnModel().getColumn(0).setMaxWidth(80); - deviceTable.setRowHeight(30); - - deviceTable.getColumnModel().getColumn(0) - .setCellRenderer(new DefaultTableCellRenderer() { - @Override - public Component getTableCellRendererComponent( - JTable t, Object v, boolean s, boolean f, int r, int c) { - - Component comp = super.getTableCellRendererComponent(t, v, s, f, r, c); - setHorizontalAlignment(JLabel.CENTER); - comp.setForeground("ONLINE".equals(v) - ? new Color(0, 150, 0) - : Color.RED); - return comp; - } - }); - } - - private void startAutoRefresh() { - new Timer(5000, e -> refreshTable()).start(); - } - - private void refreshTable() { - int[] selectedRows = deviceTable.getSelectedRows(); - - tableModel.setRowCount(0); - List webcams = Webcam.getWebcams(); - - for (Webcam w : webcams) { - boolean isIp = w.getDevice().getClass().getSimpleName().contains("IpCam"); - String status = w.getDevice() != null ? "ONLINE" : "OFFLINE"; - - tableModel.addRow(new Object[]{ - status, - w.getName(), - isIp ? "IP Stream" : "USB Hardware", - w.getViewSize().width + "x" + w.getViewSize().height, - isIp ? "Network" : "Local" - }); + private void setupBrandedToolbar(JToolBar toolBar) { + Image ogIcon = IconSetter.getIcon(); + if (ogIcon != null) { + Image scaledIcon = ogIcon.getScaledInstance(32, 32, Image.SCALE_SMOOTH); + toolBar.add(new JLabel(new ImageIcon(scaledIcon))); } - for (int r : selectedRows) { - if (r < tableModel.getRowCount()) { - deviceTable.addRowSelectionInterval(r, r); + toolBar.add(new JLabel("SWT-CCTV")); + toolBar.add(Box.createRigidArea(new Dimension(10, 0))); + toolBar.addSeparator(new Dimension(10, 32)); + + JButton btnAdd = new JButton("Add IP Cam"); + JButton btnImport = new JButton("Import Config"); // New Import Button + JButton btnLaunch = new JButton("Launch Stream"); + + btnAdd.setPreferredSize(new Dimension(100, 32)); + btnImport.setPreferredSize(new Dimension(110, 32)); + btnLaunch.setPreferredSize(new Dimension(120, 32)); + + toolBar.add(Box.createRigidArea(new Dimension(5, 0))); + toolBar.add(btnAdd); + toolBar.add(btnImport); + toolBar.addSeparator(); + toolBar.add(btnLaunch); + + btnAdd.addActionListener(e -> showAddCameraDialog()); + btnImport.addActionListener(e -> showImportDialog()); + btnLaunch.addActionListener(e -> launchSelected()); + } + + private void setupDragAndDrop() { + deviceTable.setDragEnabled(true); + deviceTable.setTransferHandler(new TransferHandler() { + @Override + public boolean canImport(TransferSupport support) { + return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor); } + + @Override + public boolean importData(TransferSupport support) { + if (!canImport(support)) return false; + try { + @SuppressWarnings("unchecked") + List files = (List) support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor); + for (File file : files) { + if (file.getName().endsWith(".json")) { + List configs = CameraSettings.loadFromFile(file); + importCameras(configs); + } + } + return true; + } catch (Exception e) { + return false; + } + } + }); + } + + private void showImportDialog() { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle("Select Camera Configuration JSON"); + if (chooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) { + File file = chooser.getSelectedFile(); + List configs = CameraSettings.loadFromFile(file); + importCameras(configs); } } - private void showContextMenu(MouseEvent e) { - int row = deviceTable.rowAtPoint(e.getPoint()); - if (row >= 0) { - deviceTable.setRowSelectionInterval(row, row); + private void importCameras(List configs) { + if (configs.isEmpty()) { + ShowError.warning(frame, "Import Empty", "No valid camera configurations found in file."); + return; } + for (CameraConfig config : configs) { + try { + if (!IpCamDeviceRegistry.isRegistered(config.getName())) { + IpCamDeviceRegistry.register(config.getName(), config.getUrl(), IpCamMode.PUSH); + CameraSettings.save(config); + } + } catch (Exception ignored) {} + } + refreshTable(); + } - JPopupMenu menu = new JPopupMenu(); - JMenuItem launch = new JMenuItem("Launch Live Stream"); - JMenuItem delete = new JMenuItem("Remove Device"); + private void setupTableAppearance() { + deviceTable.setRowHeight(30); + deviceTable.getColumnModel().getColumn(0).setMaxWidth(80); + deviceTable.getColumnModel().getColumn(0).setCellRenderer(new DefaultTableCellRenderer() { + @Override + public Component getTableCellRendererComponent(JTable t, Object v, boolean s, boolean f, int r, int c) { + Component comp = super.getTableCellRendererComponent(t, v, s, f, r, c); + setHorizontalAlignment(JLabel.CENTER); + comp.setForeground(STATUS_ONLINE.equals(v) ? new Color(0, 150, 0) : Color.RED); + return comp; + } + }); + } - launch.addActionListener(al -> launchSelected()); - delete.addActionListener(al -> deleteSelected()); + private synchronized void refreshTable() { + if (isRefreshing) return; + isRefreshing = true; + final int selectedRow = deviceTable.getSelectedRow(); + new SwingWorker() { + @Override + protected Void doInBackground() { + for (WebcamDevice device : new WebcamDefaultDriver().getDevices()) { + Dimension res = (device.getResolutions().length > 0) ? device.getResolutions()[0] : new Dimension(0,0); + publish(new Object[]{STATUS_ONLINE, device.getName(), "USB Hardware", res.width + "x" + res.height, "Local"}); + } + for (IpCamDevice ipDevice : IpCamDeviceRegistry.getIpCameras()) { + publish(probeIpCamera(ipDevice)); + } + return null; + } + @Override + protected void process(List chunks) { + if (!Boolean.TRUE.equals(frame.getRootPane().getClientProperty("cleared"))) { + tableModel.setRowCount(0); + frame.getRootPane().putClientProperty("cleared", true); + } + for (Object[] row : chunks) tableModel.addRow(row); + } + @Override + protected void done() { + frame.getRootPane().putClientProperty("cleared", null); + if (selectedRow != -1 && selectedRow < tableModel.getRowCount()) { + deviceTable.setRowSelectionInterval(selectedRow, selectedRow); + } + isRefreshing = false; + } + }.execute(); + } - menu.add(launch); - menu.addSeparator(); - menu.add(delete); - menu.show(deviceTable, e.getX(), e.getY()); + private Object[] probeIpCamera(IpCamDevice ipDevice) { + String status = STATUS_OFFLINE; String resText = "N/A"; + try { + URL url = ipDevice.getURL(); + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress(url.getHost(), url.getPort() != -1 ? url.getPort() : 80), 800); + Dimension d = ipDevice.getResolution(); + if (d != null) { status = STATUS_ONLINE; resText = d.width + "x" + d.height; } + } + } catch (Exception e) { status = STATUS_OFFLINE; } + return new Object[]{status, ipDevice.getName(), "IP Stream", resText, "Network"}; } private void launchSelected() { int row = deviceTable.getSelectedRow(); if (row == -1) return; - String name = (String) tableModel.getValueAt(row, 1); - Webcam.getWebcams().stream() - .filter(w -> w.getName().equals(name)) - .findFirst() - .ifPresent(cam -> - new Thread(() -> IFrame.addCameraInternalFrame(cam)).start() - ); - } - - private void deleteSelected() { - int row = deviceTable.getSelectedRow(); - if (row == -1) return; - - String name = (String) tableModel.getValueAt(row, 1); - if (name.toLowerCase().contains("usb")) return; - - CameraSettings.delete(name); - IpCamDeviceRegistry.unregister(name); - refreshTable(); - } - - private static void loadSavedCameras() { - for (CameraConfig config : CameraSettings.load()) { - try { - IpCamDeviceRegistry.register( - config.getName(), - config.getUrl(), - IpCamMode.PUSH - ); - } catch (MalformedURLException e) { - JOptionPane.showMessageDialog( - null, - "Malformed URL\n" + e.getMessage(), - "Malformed URL", - JOptionPane.ERROR_MESSAGE - ); - } + if (STATUS_OFFLINE.equals(tableModel.getValueAt(row, 0))) { + ShowError.warning(frame, "Device Offline", "Cannot connect to '" + name + "'."); + return; } + new Thread(() -> { + Webcam selected = Webcam.getWebcams().stream().filter(w -> w.getName().equals(name)).findFirst().orElse(null); + if (selected != null) IFrame.addCameraInternalFrame(selected); + }).start(); } private void showAddCameraDialog() { JPanel p = new JPanel(new GridLayout(2, 2, 5, 5)); - JTextField n = new JTextField(); - JTextField u = new JTextField(); - - p.add(new JLabel("Name:")); - p.add(n); - p.add(new JLabel("URL:")); - p.add(u); - - int result = JOptionPane.showConfirmDialog( - frame, p, "Register IP Camera", JOptionPane.OK_CANCEL_OPTION - ); - - if (result == JOptionPane.OK_OPTION) { - try { - IpCamDeviceRegistry.register(n.getText(), u.getText(), IpCamMode.PUSH); - CameraSettings.save(new CameraConfig(n.getText(), u.getText())); - refreshTable(); - } catch (Exception ex) { - JOptionPane.showMessageDialog(frame, "Error: " + ex.getMessage()); - } + JTextField n = new JTextField(); JTextField u = new JTextField(); + p.add(new JLabel("Name:")); p.add(n); p.add(new JLabel("URL:")); p.add(u); + if (JOptionPane.showConfirmDialog(frame, p, "Register IP Camera", JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION) { + importCameras(List.of(new CameraConfig(n.getText(), u.getText()))); } } - public void open() { - frame.setLocationRelativeTo(null); - frame.setVisible(true); + private void setupListeners() { + deviceTable.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + deviceTable.requestFocusInWindow(); + if (e.getClickCount() == 2 && deviceTable.getSelectedRow() != -1) launchSelected(); + if (SwingUtilities.isRightMouseButton(e)) showContextMenu(e); + } + }); } + private void showContextMenu(MouseEvent e) { + int row = deviceTable.rowAtPoint(e.getPoint()); + if (row >= 0) deviceTable.setRowSelectionInterval(row, row); + JPopupMenu menu = new JPopupMenu(); + JMenuItem launch = new JMenuItem("Launch Live Stream"); + launch.addActionListener(al -> launchSelected()); + menu.add(launch); + menu.show(deviceTable, e.getX(), e.getY()); + } + + private static void loadSavedCameras() { + for (CameraConfig config : CameraSettings.load()) { + try { IpCamDeviceRegistry.register(config.getName(), config.getUrl(), IpCamMode.PUSH); } catch (Exception ignored) {} + } + } + + private void startAutoRefresh() { new Timer(REFRESH_INTERVAL, e -> refreshTable()).start(); } + + public void open() { frame.setLocationRelativeTo(null); frame.setVisible(true); } + public static void main(String[] args) { SwingUtilities.invokeLater(() -> new SwingCCTVManager().open()); } -} +} \ No newline at end of file diff --git a/src/main/java/io/swtc/SwingIFrame.java b/src/main/java/io/swtc/SwingIFrame.java index ff1cc03..b3f1065 100644 --- a/src/main/java/io/swtc/SwingIFrame.java +++ b/src/main/java/io/swtc/SwingIFrame.java @@ -1,18 +1,30 @@ package io.swtc; import com.github.sarxos.webcam.Webcam; -import io.swtc.proccessing.ui.iframe.*; // Your custom frames +import io.swtc.proccessing.ui.IconSetter; +import io.swtc.proccessing.ui.ShowError; +import io.swtc.proccessing.ui.desktop.DIM; +import io.swtc.proccessing.ui.desktop.debug.Profiler; +import io.swtc.proccessing.ui.desktop.evidence.EvidenceExportFrame; +import io.swtc.proccessing.ui.desktop.recording.MultiRecordingFrame; +import io.swtc.proccessing.ui.iframe.*; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.io.File; +import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.Objects; + +import static java.awt.SystemColor.desktop; public class SwingIFrame { private final JFrame mainFrame; private final DesktopPane desktopPane; + private final DIM desktopIconManager; private final Map cameraToEffects = new HashMap<>(); private boolean fullscreen = false; @@ -20,18 +32,27 @@ public class SwingIFrame { private boolean blackbg = false; private final Color bgcolor = Color.decode("#336B6A"); private final Color defDesktopBg = Color.WHITE; - private final JPopupMenu popupMenu = new JPopupMenu(); + public SwingIFrame() { mainFrame = new JFrame("Viewer"); mainFrame.setSize(1280, 720); mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + mainFrame.setIconImage(IconSetter.getIcon()); + desktopPane = new DesktopPane(cameraToEffects); desktopPane.setBackground(defDesktopBg); mainFrame.add(desktopPane, BorderLayout.CENTER); + desktopIconManager = new DIM(desktopPane); + + setupDesktopExportFrame(); + setupRecordingFrame(); + setupFileEx(); + setupProfiler(); + setupFullscreenToggle(); setupBlackBg(); initPopupMenu(); @@ -39,6 +60,66 @@ public class SwingIFrame { desktopPane.addMouseListener(popupListener()); } + private void setupDesktopExportFrame() { + desktopIconManager.addIcon( + "Export Evidence", + IconSetter.getSaveIconAsImageIcon(), + /* e1003c20ff00c637d963ce21fd685fed6460602a: Fix to icon, need to pass parent! Or Override method which is dumb */ + () -> EvidenceExportFrame.showExport(mainFrame) + ); + } + + private void setupRecordingFrame() { + desktopIconManager.addIcon( + "Record Batch", + IconSetter.getCamerarec_ImageIcon(), + () -> { + MultiRecordingFrame mrf = new MultiRecordingFrame(); + desktopPane.add(mrf); + mrf.show(); + try { + mrf.setSelected(true); + } catch (java.beans.PropertyVetoException e) { + ShowError.error(null,"Exception", "" + e.getStackTrace()); + } + } + ); + } + + private void setupFileEx() { + desktopIconManager.addIcon( + "Open Recordings", + IconSetter.getExplorerIcon(), + () -> { + String userHome = System.getProperty("user.home"); + File folder = new File(userHome,"Videos/swtcctv-rec"); + if (Desktop.isDesktopSupported() && folder.exists()) { + try { + Desktop.getDesktop().open(folder); + } catch (IOException e) { + ShowError.error(null, + "Failed to open Folder", + "Failed" + e.getMessage() + ); + } + } + } + + ); + } + + private void setupProfiler() { + desktopIconManager.addIcon( + "Profiler", + IconSetter.getDbg_icon(), + () -> { + SwingUtilities.invokeLater(() -> { + Profiler.showFrame(new Profiler(mainFrame)); + }); + }); + } + + public void addCameraInternalFrame(Webcam webcam) { CameraInternalFrame cameraFrame = new CameraInternalFrame(webcam, this::handleEffectsRequest); @@ -106,7 +187,14 @@ public class SwingIFrame { popupMenu.removeAll(); // clean slate JCheckBoxMenuItem fullscreenItem = new JCheckBoxMenuItem("Fullscreen"); + JCheckBoxMenuItem mmfullscreenItem = new JCheckBoxMenuItem("Multi Monitor Fullscreen"); + JCheckBoxMenuItem backgroundcolor = new JCheckBoxMenuItem("Calmer Background"); + JMenuItem colorpicker = new JMenuItem("Set background color"); + fullscreenItem.addActionListener(e -> toggleFullscreen()); + mmfullscreenItem.addActionListener(e -> toggleMMFullscreen()); + backgroundcolor.addActionListener(e -> toggleBackground()); + colorpicker.addActionListener(e -> chooseBgColor()); popupMenu.addPopupMenuListener(new javax.swing.event.PopupMenuListener() { @Override @@ -118,6 +206,22 @@ public class SwingIFrame { }); popupMenu.add(fullscreenItem); + popupMenu.add(mmfullscreenItem); + popupMenu.add(backgroundcolor); + popupMenu.add(colorpicker); + } + + private void chooseBgColor() { + Color selected = JColorChooser.showDialog( + mainFrame, + "Select Background Color", + desktopPane.getBackground() + ); + + if (!Objects.isNull(selected)) { + desktopPane.setBackground(selected); + desktopPane.repaint(); + } } private void setupBlackBg() { @@ -134,6 +238,7 @@ public class SwingIFrame { private void setupFullscreenToggle() { JRootPane root = mainFrame.getRootPane(); + // One Monitor FS root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) .put(KeyStroke.getKeyStroke("F11"), "toggleFullscreen"); root.getActionMap().put("toggleFullscreen", new AbstractAction() { @@ -142,6 +247,16 @@ public class SwingIFrame { toggleFullscreen(); } }); + + // Multi Monitor FS + root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) + .put(KeyStroke.getKeyStroke("F12"), "toggleMMFullscreen"); + root.getActionMap().put("toggleMMFullscreen", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + toggleMMFullscreen(); + } + }); } private void toggleBackground() { @@ -150,6 +265,33 @@ public class SwingIFrame { desktopPane.repaint(); } + private void toggleMMFullscreen() { + if (!fullscreen) { + windowedBounds = mainFrame.getBounds(); + + Rectangle virtualBounds = new Rectangle(); + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice[] gs = ge.getScreenDevices(); + + for (GraphicsDevice gd : gs) { + GraphicsConfiguration[] gc = gd.getConfigurations(); + for (GraphicsConfiguration configuration : gc) { + virtualBounds = virtualBounds.union(configuration.getBounds()); + } + } + + mainFrame.dispose(); + mainFrame.setUndecorated(true); + + mainFrame.setBounds(virtualBounds); + mainFrame.setVisible(true); + + fullscreen = true; + } else { + toggleFullscreen(); + } + } + /** Toggle fullscreen mode */ private void toggleFullscreen() { if (!fullscreen) { diff --git a/src/main/java/io/swtc/networking/CameraConfig.java b/src/main/java/io/swtc/networking/CameraConfig.java index d3814a0..1716f54 100644 --- a/src/main/java/io/swtc/networking/CameraConfig.java +++ b/src/main/java/io/swtc/networking/CameraConfig.java @@ -5,6 +5,7 @@ public class CameraConfig { public String url; // Default constructor for Jackson + public CameraConfig() {} public CameraConfig(String name, String url) { this.name = name; diff --git a/src/main/java/io/swtc/networking/CameraSettings.java b/src/main/java/io/swtc/networking/CameraSettings.java index 15e6268..e8541fa 100644 --- a/src/main/java/io/swtc/networking/CameraSettings.java +++ b/src/main/java/io/swtc/networking/CameraSettings.java @@ -7,24 +7,32 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; -/* -* Some JSON stuff for camera config saving -* */ + public class CameraSettings { - private static final File storage_file = new File("network_cameras.json"); - private static final ObjectMapper mapper = new ObjectMapper(); + private static final File STORAGE_FILE = new File("network_cameras.json"); + private static final ObjectMapper MAPPER = new ObjectMapper(); public static List load() { - if (!storage_file.exists() || storage_file.length() == 0) return new ArrayList<>(); + return loadFromFile(STORAGE_FILE); + } + + public static List loadFromFile(File file) { + if (!file.exists() || file.length() == 0) return new ArrayList<>(); try { - return mapper.readValue(storage_file, new TypeReference>() {}); + return MAPPER.readValue(file, new TypeReference>() {}); } catch (IOException e) { + System.err.println("Error reading camera config: " + e.getMessage()); return new ArrayList<>(); } } + /** + * Saves a single camera. Prevents duplicates by name. + */ public static void save(CameraConfig newCam) { List current = load(); + // Prevent duplicate names in the local storage + current.removeIf(cam -> cam.getName().equalsIgnoreCase(newCam.getName())); current.add(newCam); write(current); } @@ -37,7 +45,7 @@ public class CameraSettings { private static void write(List list) { try { - mapper.writerWithDefaultPrettyPrinter().writeValue(storage_file, list); + MAPPER.writerWithDefaultPrettyPrinter().writeValue(STORAGE_FILE, list); } catch (IOException e) { e.printStackTrace(); } diff --git a/src/main/java/io/swtc/proccessing/CameraPanel.java b/src/main/java/io/swtc/proccessing/CameraPanel.java index e27851b..2259704 100644 --- a/src/main/java/io/swtc/proccessing/CameraPanel.java +++ b/src/main/java/io/swtc/proccessing/CameraPanel.java @@ -1,30 +1,213 @@ package io.swtc.proccessing; +import io.swtc.proccessing.ui.IconSetter; +import io.swtc.proccessing.ui.ShowError; +import io.swtc.recording.cv.AVRecorder; + import javax.swing.*; import java.awt.*; +import java.awt.event.*; +import java.awt.font.GlyphVector; import java.awt.image.BufferedImage; +import java.io.InputStream; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; -/** - * Enhanced CameraPanel with support for custom image processors - */ public class CameraPanel extends JPanel { private volatile BufferedImage sourceImage; private volatile BufferedImage processedImage; - private Function imageProcessor; + + private Font overlayFont; + private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy"); + private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss"); + + // Cached glyph vectors for performance + private GlyphVector dateGlyphs; + private GlyphVector timeGlyphs; + private String lastDate = ""; + private String lastTime = ""; + + // Pre-calculated positions to avoid repeated calculations + private static final int OVERLAY_X = 8; + private static final int OVERLAY_Y = 20; + private static final int TIME_Y_OFFSET = 19; + + private volatile AVRecorder recorder; // this is now javacv, jcodec is stoopid private final AtomicBoolean repaintScheduled = new AtomicBoolean(false); + private double zoomLevel = 1.0; + private double xOffset = 0; + private double yOffset = 0; + private Point dragStartPoint; + + private static final double MIN_ZOOM = 1.0; + private static final double MAX_ZOOM = 10.0; + private static final double ZOOM_MULTIPLIER = 1.1; + + private final GraphicsConfiguration graphicsConfig; + public CameraPanel() { setBackground(Color.BLACK); setPreferredSize(new Dimension(640, 480)); + + loadFont(); + + graphicsConfig = GraphicsEnvironment.getLocalGraphicsEnvironment() + .getDefaultScreenDevice().getDefaultConfiguration(); + + initInteractionListeners(); + } + + private void loadFont() { + try (InputStream is = getClass().getResourceAsStream("/font/OverlayFont.ttf")) { + if (is != null) { + overlayFont = Font.createFont(Font.TRUETYPE_FONT, is).deriveFont(15f); + } else { + overlayFont = new Font(Font.MONOSPACED, Font.PLAIN, 15); + } + } catch (Exception e) { + overlayFont = new Font(Font.MONOSPACED, Font.PLAIN, 15); + } + } + + private void initInteractionListeners() { + MouseAdapter mouseHandler = new MouseAdapter() { + @Override + public void mouseWheelMoved(MouseWheelEvent e) { handleZoom(e); } + + @Override + public void mousePressed(MouseEvent e) { + dragStartPoint = e.getPoint(); + setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); + } + + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) resetView(); + } + + @Override + public void mouseReleased(MouseEvent e) { setCursor(Cursor.getDefaultCursor()); } + + @Override + public void mouseDragged(MouseEvent e) { handlePan(e); } + + @Override + public void mouseExited(MouseEvent e) { setCursor(Cursor.getDefaultCursor()); } + }; + + addMouseWheelListener(mouseHandler); + addMouseListener(mouseHandler); + addMouseMotionListener(mouseHandler); + } + + public void setExternalRecorder(AVRecorder recorder) { + this.recorder = recorder; } public void setImage(BufferedImage img) { - sourceImage = img; + this.sourceImage = img; + updateProcessedImage(); + // Feed the AVRecorder using its 'accept' method if active + AVRecorder currentRecorder = this.recorder; + if (currentRecorder != null && currentRecorder.isRecording() && processedImage != null) { + currentRecorder.accept(processedImage); + } + + scheduleRepaint(); + } + + public void setImageProcessor(Function processor) { + this.imageProcessor = processor; + if (sourceImage != null) { + updateProcessedImage(); + scheduleRepaint(); + } + } + + public BufferedImage getCurrentProcessedImage() { + return processedImage; + } + + public void resetView() { + zoomLevel = 1.0; + xOffset = 0; + yOffset = 0; + repaint(); + } + + private void updateProcessedImage() { + if (sourceImage == null) return; + + BufferedImage temp; + if (imageProcessor != null) { + try { + temp = imageProcessor.apply(sourceImage); + } catch (Exception e) { + ShowError.error(null, "Error in image processing: \n" + Arrays.toString(e.getStackTrace()), "Processing Error"); + temp = sourceImage; + } + } else { + temp = sourceImage; + } + + if (processedImage == null || + processedImage.getWidth() != temp.getWidth() || + processedImage.getHeight() != temp.getHeight()) { + + processedImage = graphicsConfig.createCompatibleImage( + temp.getWidth(), + temp.getHeight(), + temp.getTransparency() + ); + } + + Graphics2D g2d = processedImage.createGraphics(); + try { + g2d.drawImage(temp, 0, 0, null); // this is fucking expensive + drawTimeOverlay(g2d); + } finally { + g2d.dispose(); + } + } + + + private void drawTimeOverlay(Graphics2D g) { + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); + g.setFont(overlayFont); + + LocalDateTime now = LocalDateTime.now(); + String dateStr = now.format(dateFormatter); + String timeStr = now.format(timeFormatter); + + if (!dateStr.equals(lastDate)) { + dateGlyphs = overlayFont.createGlyphVector(g.getFontRenderContext(), dateStr); + lastDate = dateStr; + } + + if (!timeStr.equals(lastTime)) { + timeGlyphs = overlayFont.createGlyphVector(g.getFontRenderContext(), timeStr); + lastTime = timeStr; + } + + g.setColor(Color.BLACK); + g.drawGlyphVector(dateGlyphs, OVERLAY_X + 2, OVERLAY_Y + 2); + g.drawGlyphVector(timeGlyphs, OVERLAY_X + 2, OVERLAY_Y + TIME_Y_OFFSET + 2); + + g.setColor(Color.WHITE); + g.drawGlyphVector(dateGlyphs, OVERLAY_X, OVERLAY_Y); + g.drawGlyphVector(timeGlyphs, OVERLAY_X, OVERLAY_Y + TIME_Y_OFFSET); + } + + private void scheduleRepaint() { if (repaintScheduled.compareAndSet(false, true)) { SwingUtilities.invokeLater(() -> { repaintScheduled.set(false); @@ -33,37 +216,79 @@ public class CameraPanel extends JPanel { } } - public void setImageProcessor(Function processor) { - this.imageProcessor = processor; - } - @Override protected void paintComponent(Graphics g) { super.paintComponent(g); + if (processedImage == null) return; - BufferedImage src = sourceImage; - if (src == null) return; + Graphics2D g2d = (Graphics2D) g.create(); + try { + g2d.setRenderingHint( + RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR + ); - BufferedImage img = src; + g2d.translate((int)xOffset, (int)yOffset); - 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); + double scaleX = (double) getWidth() / processedImage.getWidth(); + double scaleY = (double) getHeight() / processedImage.getHeight(); + + g2d.scale(scaleX * zoomLevel, scaleY * zoomLevel); + g2d.drawImage(processedImage, 0, 0, null); + + } finally { + g2d.dispose(); + } + } + + private void handleZoom(MouseWheelEvent e) { + if (processedImage == null) return; + + double oldZoom = zoomLevel; + if (e.getWheelRotation() < 0) { + zoomLevel *= ZOOM_MULTIPLIER; + } else { + zoomLevel /= ZOOM_MULTIPLIER; } - processedImage = img; + zoomLevel = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoomLevel)); - Graphics2D g2d = (Graphics2D) g; - g2d.setRenderingHint( - RenderingHints.KEY_INTERPOLATION, - RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR - ); + if (oldZoom != zoomLevel) { + double xRel = e.getX() - xOffset; + double yRel = e.getY() - yOffset; + double zoomFactor = zoomLevel / oldZoom; - g2d.drawImage(img, 0, 0, getWidth(), getHeight(), null); + xOffset = e.getX() - (xRel * zoomFactor); + yOffset = e.getY() - (yRel * zoomFactor); + + checkBounds(); + repaint(); + } } - public BufferedImage getCurrentProcessedImage() { - return processedImage; + private void handlePan(MouseEvent e) { + if (processedImage == null || dragStartPoint == null) return; + + xOffset += e.getX() - dragStartPoint.x; + yOffset += e.getY() - dragStartPoint.y; + + dragStartPoint = e.getPoint(); + checkBounds(); + repaint(); } -} + + private void checkBounds() { + double viewedWidth = getWidth() * zoomLevel; + double viewedHeight = getHeight() * zoomLevel; + + if (xOffset > 0) xOffset = 0; + if (yOffset > 0) yOffset = 0; + + if (xOffset + viewedWidth < getWidth()) { + xOffset = getWidth() - viewedWidth; + } + if (yOffset + viewedHeight < getHeight()) { + yOffset = getHeight() - viewedHeight; + } + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/IconSetter.java b/src/main/java/io/swtc/proccessing/ui/IconSetter.java new file mode 100644 index 0000000..c985e26 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/IconSetter.java @@ -0,0 +1,115 @@ +package io.swtc.proccessing.ui; + +import javax.swing.*; +import java.awt.*; +import java.net.URL; +import java.util.Objects; + +/* vital boilerplate class, shouldve made it better but idk. */ +public class IconSetter { + + private static Image ICON_IMAGE; + private static ImageIcon ICON_ICON; + private static Image effects_icon; + private static ImageIcon dbg_icon; + private static Image camerarec; + private static ImageIcon camerarec_imgico; + private static ImageIcon expIcon; + + /* this is used for the app icon itself (the one in tb) */ + public static Image getIcon() { + if (ICON_IMAGE == null) { + URL url = IconSetter.class.getResource("/icons/artwork.png"); + if (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; + } + + public static Image getEffectIcon() { + if (Objects.isNull(effects_icon)) { + URL url = IconSetter.class.getResource("/icons/effectsframe.png"); + 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; + } + + public static ImageIcon getIconAsImageIcon() { + if (ICON_ICON == null) { + URL url = IconSetter.class.getResource("/icons/artwork.png"); + if (Objects.isNull(url)) { + ShowError.error(null,"Icon","Icon not found!, NULL! (Type ImageIcon)"); + throw new RuntimeException("NULL!"); + } + ICON_ICON = new ImageIcon(url); // separate variable for ImageIcon + } + return ICON_ICON; + } + + public static ImageIcon getSaveIconAsImageIcon() { + if (Objects.isNull(ICON_ICON)) { + URL url = IconSetter.class.getResource("/icons/save.png"); + if (Objects.isNull(url)) { + ShowError.error(null,"Icon","getSaveIconAsImageIcon failed, NULL! (Type ImageIcon)"); + throw new RuntimeException("NULL!"); + } + ICON_ICON = new ImageIcon(url); + } + return ICON_ICON; + } + + public static ImageIcon getDbg_icon() { + if (Objects.isNull(dbg_icon)) { + URL url = IconSetter.class.getResource("/icons/icondbg-7.png"); + if (Objects.isNull(url)) { + ShowError.error(null, "Icon", "getDbg_icon, object url was null (Type ImageIcon)"); + throw new RuntimeException("NULL!"); + } + dbg_icon = new ImageIcon(url); + } + return dbg_icon; + } + + public static Image getCamerarec_img() { + if (Objects.isNull(camerarec)) { + URL url = IconSetter.class.getResource("/icons/rec.png"); + if (Objects.isNull(url)) { + ShowError.error(null,"icon","recicon was null Type Image"); + throw new RuntimeException("NULL!"); + } + camerarec = Toolkit.getDefaultToolkit().getImage(url); + } + return camerarec; + } + + public static ImageIcon getCamerarec_ImageIcon() { + if (Objects.isNull(camerarec_imgico)) { + URL url = IconSetter.class.getResource("/icons/rec.png"); + if (Objects.isNull(url)) { + ShowError.error(null,"icon","getCamerarec_ImageIcon failed, Type Image"); + throw new RuntimeException("NULL!"); + } + camerarec_imgico = new ImageIcon(url); + } + return camerarec_imgico; + } + + public static ImageIcon getExplorerIcon() { + if (Objects.isNull(expIcon)) { + URL url = IconSetter.class.getResource("/icons/explorer.png"); + if (Objects.isNull(url)) { + ShowError.error(null,"icon","getExplorerIcon failed, Type Image"); + throw new RuntimeException("NULL!"); + } + expIcon = new ImageIcon(url); + } + return expIcon; + } +} diff --git a/src/main/java/io/swtc/proccessing/ui/ShowError.java b/src/main/java/io/swtc/proccessing/ui/ShowError.java index 5efca5a..b93c7a7 100644 --- a/src/main/java/io/swtc/proccessing/ui/ShowError.java +++ b/src/main/java/io/swtc/proccessing/ui/ShowError.java @@ -12,8 +12,8 @@ public final class ShowError { public static void error(Component parent, String title, String message) { JOptionPane.showMessageDialog( parent, - message, title, + message, JOptionPane.ERROR_MESSAGE ); } @@ -21,8 +21,8 @@ public final class ShowError { public static void warning(Component parent, String title, String message) { JOptionPane.showMessageDialog( parent, - message, title, + message, JOptionPane.WARNING_MESSAGE ); } diff --git a/src/main/java/io/swtc/proccessing/ui/desktop/DIM.java b/src/main/java/io/swtc/proccessing/ui/desktop/DIM.java new file mode 100644 index 0000000..c7435d1 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/desktop/DIM.java @@ -0,0 +1,50 @@ +package io.swtc.proccessing.ui.desktop; + +import javax.swing.*; +import java.awt.*; + +/* DesktopIconManager */ +public class DIM { + + private final JDesktopPane desktop; + + private int cX; + private int cY; + + private static final int PAD = 6; + + public DIM(JDesktopPane desktop) { + this.desktop = desktop; + + Insets insets = desktop.getInsets(); + this.cX = insets.left + PAD; + this.cY = insets.top + PAD; + } + + public void addIcon(String label, Icon icon, Runnable runaction) { + DesktopIcon desktopIcon = new DesktopIcon(label, icon, runaction); + + Dimension pref = desktopIcon.getPreferredSize(); + int w = pref.width; + int h = pref.height; + + Insets insets = desktop.getInsets(); + int usableHeight = desktop.getHeight() - insets.top - insets.bottom; + + if (usableHeight <= 0) { + usableHeight = Integer.MAX_VALUE; + } + + if (cY + h > usableHeight) { + cY = insets.top + PAD; + cX += w + PAD; + } + + desktopIcon.setBounds(cX, cY, w, h); + + desktop.add(desktopIcon, JLayeredPane.DEFAULT_LAYER); + + cY += h + PAD; + desktop.repaint(); + } +} diff --git a/src/main/java/io/swtc/proccessing/ui/desktop/DesktopIcon.java b/src/main/java/io/swtc/proccessing/ui/desktop/DesktopIcon.java new file mode 100644 index 0000000..5a0265c --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/desktop/DesktopIcon.java @@ -0,0 +1,102 @@ +package io.swtc.proccessing.ui.desktop; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.geom.RoundRectangle2D; + +public class DesktopIcon extends JPanel { + + private static final int ICON_SIZE = 64; + private static final int TOTAL_WIDTH = 100; + private static final int TOTAL_HEIGHT = 85; + + private boolean hovered = false; + + public DesktopIcon(String label, Icon icon, Runnable action) { + setLayout(new BorderLayout(0, 2)); + setOpaque(false); + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + + + Dimension fixedSize = new Dimension(TOTAL_WIDTH, TOTAL_HEIGHT); + setPreferredSize(fixedSize); + setMinimumSize(fixedSize); + setMaximumSize(fixedSize); + + setBorder(new EmptyBorder(4, 4, 4, 4)); + + Icon scaledIcon = (icon instanceof ImageIcon) + ? new SmoothIcon(((ImageIcon) icon).getImage(), ICON_SIZE, ICON_SIZE) + : icon; + + JLabel iconLabel = new JLabel(scaledIcon, SwingConstants.CENTER); + iconLabel.setVerticalAlignment(SwingConstants.BOTTOM); + + JLabel textLabel = new ShadowLabel(label); + textLabel.setHorizontalAlignment(SwingConstants.CENTER); + textLabel.setVerticalAlignment(SwingConstants.TOP); + + add(iconLabel, BorderLayout.CENTER); + add(textLabel, BorderLayout.SOUTH); + + setupMouseListeners(action); + } + + private void setupMouseListeners(Runnable action) { + addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) { + if (action != null) action.run(); + } + } + + @Override + public void mouseEntered(MouseEvent e) { + hovered = true; + repaint(); + } + + @Override + public void mouseExited(MouseEvent e) { + hovered = false; + repaint(); + } + }); + } + + @Override + protected void paintComponent(Graphics g) { + if (hovered) { + paintHoverEffect(g); + } + super.paintComponent(g); + } + + private void paintHoverEffect(Graphics g) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + boolean lightBg = isBackgroundLight(); + Color fill = lightBg ? new Color(0, 0, 0, 20) : new Color(255, 255, 255, 30); + Color border = lightBg ? new Color(0, 0, 0, 50) : new Color(255, 255, 255, 70); + + g2.setColor(fill); + g2.fill(new RoundRectangle2D.Float(2, 2, getWidth() - 4, getHeight() - 4, 10, 10)); + + g2.setColor(border); + g2.draw(new RoundRectangle2D.Float(2, 2, getWidth() - 4, getHeight() - 4, 10, 10)); + + g2.dispose(); + } + + public boolean isBackgroundLight() { + Container p = getParent(); + Color bg = (p != null) ? p.getBackground() : Color.WHITE; + double luminance = (0.299 * bg.getRed() + 0.587 * bg.getGreen() + 0.114 * bg.getBlue()) / 255; + return luminance > 0.6; + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/desktop/ShadowLabel.java b/src/main/java/io/swtc/proccessing/ui/desktop/ShadowLabel.java new file mode 100644 index 0000000..ca9f5e0 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/desktop/ShadowLabel.java @@ -0,0 +1,48 @@ +package io.swtc.proccessing.ui.desktop; + +import javax.swing.*; +import java.awt.*; + +public class ShadowLabel extends JLabel { + public ShadowLabel(String text) { + super(text); + setForeground(Color.WHITE); + } + + @Override + protected void paintComponent(Graphics g) { + String text = getText(); + if (text == null || text.isEmpty()) return; + + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + + FontMetrics fm = g2.getFontMetrics(); + int availableWidth = getWidth(); + + String drawText = text; + if (fm.stringWidth(text) > availableWidth) { + for (int i = text.length(); i > 0; i--) { + String temp = text.substring(0, i) + "..."; + if (fm.stringWidth(temp) <= availableWidth) { + drawText = temp; + break; + } + } + } + DesktopIcon parent = (DesktopIcon) getParent(); + boolean isLightBg = (parent != null) && parent.isBackgroundLight(); + + Color textColor = isLightBg ? Color.BLACK : Color.WHITE; + Color shadowColor = isLightBg ? new Color(255, 255, 255, 200) : new Color(0, 0, 0, 180); + + int x = (availableWidth - fm.stringWidth(drawText)) / 2; + int y = fm.getAscent(); + + g2.setColor(shadowColor); + g2.drawString(drawText, x + 1, y + 1); + + g2.setColor(textColor); + g2.drawString(drawText, x, y); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/desktop/SmoothIcon.java b/src/main/java/io/swtc/proccessing/ui/desktop/SmoothIcon.java new file mode 100644 index 0000000..8ba9188 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/desktop/SmoothIcon.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/desktop/debug/Profiler.java b/src/main/java/io/swtc/proccessing/ui/desktop/debug/Profiler.java new file mode 100644 index 0000000..3f6ec7b --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/desktop/debug/Profiler.java @@ -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 pools = + ManagementFactory.getMemoryPoolMXBeans(); + private final List gcs = + ManagementFactory.getGarbageCollectorMXBeans(); + private final List 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) + ); + } +} diff --git a/src/main/java/io/swtc/proccessing/ui/desktop/evidence/EvidenceExportFrame.java b/src/main/java/io/swtc/proccessing/ui/desktop/evidence/EvidenceExportFrame.java new file mode 100644 index 0000000..1564e1c --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/desktop/evidence/EvidenceExportFrame.java @@ -0,0 +1,119 @@ +package io.swtc.proccessing.ui.desktop.evidence; + +import io.swtc.proccessing.ui.IconSetter; +import io.swtc.proccessing.ui.ShowError; +import io.swtc.recording.evidence.USBExportManager; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.nio.file.Path; +import java.io.File; + +public class EvidenceExportFrame extends JFrame { + + private final JProgressBar progressBar; + private final JLabel statusLabel; + private final JLabel detailLabel; + private final JButton actionBtn; + private final JFrame parent; /* neccessary to get icon working, inheritance is a bitch */ + + private EvidenceExportFrame(Path sourceDir, Path usbTargetDir, JFrame parent) { + + this.parent = parent; + + setTitle("Export"); + setSize(400, 220); + setLocationRelativeTo(this.parent); + setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + + JPanel contentPane = new JPanel(); + + contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.Y_AXIS)); + contentPane.setBorder(new EmptyBorder(25, 25, 25, 25)); + + ImageIcon ico = IconSetter.getSaveIconAsImageIcon(); + this.setIconImage(ico.getImage()); + + statusLabel = new JLabel("Starting export"); + statusLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + statusLabel.setFont(new Font(statusLabel.getFont().getName(), Font.BOLD, 14)); + detailLabel = new JLabel("Initializing"); + detailLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + detailLabel.setForeground(Color.GRAY); + + progressBar = new JProgressBar(0, 100); + progressBar.setAlignmentX(Component.LEFT_ALIGNMENT); + progressBar.setMaximumSize(new Dimension(Integer.MAX_VALUE, 20)); + + actionBtn = new JButton("Cancel"); + actionBtn.setAlignmentX(Component.LEFT_ALIGNMENT); + contentPane.add(statusLabel); + contentPane.add(Box.createRigidArea(new Dimension(0, 10))); + contentPane.add(detailLabel); + contentPane.add(Box.createRigidArea(new Dimension(0, 20))); + contentPane.add(progressBar); + contentPane.add(Box.createVerticalGlue()); + contentPane.add(actionBtn); + + add(contentPane); + + actionBtn.addActionListener(e -> handleAction()); + + setVisible(true); + startExport(sourceDir, usbTargetDir); + } + + private void handleAction() { + if (actionBtn.getText().equals("Close")) { + dispose(); + return; + } + + int confirm = JOptionPane.showConfirmDialog(this.parent, + "Stop export?", "Confirm", JOptionPane.YES_NO_OPTION); + if (confirm == JOptionPane.YES_OPTION) dispose(); + } + + private void startExport(Path sourceDir, Path usbTargetDir) { + USBExportManager.exportAsync( + sourceDir, + usbTargetDir, + stats -> SwingUtilities.invokeLater(() -> { + progressBar.setValue(stats.percent()); + statusLabel.setText("Exporting " + stats.percent() + "%"); + detailLabel.setText(String.format("%s | %s remaining", + stats.getSpeedMBps(), stats.timeLeft())); + }), + () -> SwingUtilities.invokeLater(() -> { + progressBar.setValue(100); + statusLabel.setText("Export Complete"); + detailLabel.setText("Files saved"); + actionBtn.setText("Close"); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + }), + ex -> SwingUtilities.invokeLater(() -> { + statusLabel.setText("Export Failed"); + detailLabel.setText(ex.getMessage()); + ShowError.error(this, ex.getMessage(), "Error"); + }) + ); + } + + public static void showExport(JFrame parent) { + SwingUtilities.invokeLater(() -> { + File videoDir = new File(System.getProperty("user.home"), "Videos/swtcctv-rec"); + if (!videoDir.exists()) { + ShowError.warning(null, "No recordings found.", "Not Found"); + return; + } + + JFileChooser chooser = new JFileChooser(); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + if (chooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) { + Path target = chooser.getSelectedFile().toPath().resolve("swtcctv-rec_" + System.currentTimeMillis() / 1000); + new EvidenceExportFrame(videoDir.toPath(), target, parent); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/desktop/recording/CameraCheckItem.java b/src/main/java/io/swtc/proccessing/ui/desktop/recording/CameraCheckItem.java new file mode 100644 index 0000000..647522c --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/desktop/recording/CameraCheckItem.java @@ -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(); } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/desktop/recording/CheckBoxListRenderer.java b/src/main/java/io/swtc/proccessing/ui/desktop/recording/CheckBoxListRenderer.java new file mode 100644 index 0000000..f661fdf --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/desktop/recording/CheckBoxListRenderer.java @@ -0,0 +1,17 @@ +package io.swtc.proccessing.ui.desktop.recording; + +import javax.swing.*; +import java.awt.*; + +public class CheckBoxListRenderer extends JCheckBox implements ListCellRenderer { + public CheckBoxListRenderer() { setOpaque(true); } + @Override + public Component getListCellRendererComponent(JList 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; + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/desktop/recording/MultiRecordingFrame.java b/src/main/java/io/swtc/proccessing/ui/desktop/recording/MultiRecordingFrame.java new file mode 100644 index 0000000..8ce808d --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/desktop/recording/MultiRecordingFrame.java @@ -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 listModel = new DefaultListModel<>(); + private final JList cameraList = new JList<>(listModel); + + private final List activeRecorders = new ArrayList<>(); + private boolean isRecording = false; + + private JButton toggleBtn; + private JComboBox 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 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(); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/iframe/CameraInternalFrame.java b/src/main/java/io/swtc/proccessing/ui/iframe/CameraInternalFrame.java index 127789a..16a0349 100644 --- a/src/main/java/io/swtc/proccessing/ui/iframe/CameraInternalFrame.java +++ b/src/main/java/io/swtc/proccessing/ui/iframe/CameraInternalFrame.java @@ -3,31 +3,25 @@ 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.recording.VideoRecorder; -import javax.imageio.ImageIO; +import io.swtc.proccessing.ui.IconSetter; +import io.swtc.proccessing.ui.ShowError; import javax.swing.*; -import javax.swing.border.EmptyBorder; import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; import java.util.function.Consumer; public class CameraInternalFrame extends JInternalFrame { private final WebcamCaptureLoop captureLoop; private final CameraPanel cameraPanel; - private final VideoRecorder videoRecorder; // Instance of the recorder - private JButton recordBtn; public CameraInternalFrame(Webcam webcam, Consumer onOpenEffects) { super(webcam.getName(), true, true, true, true); - this.cameraPanel = new CameraPanel(); - this.videoRecorder = new VideoRecorder(); // Initialize recorder - // Initialize capture loop - this.captureLoop = new WebcamCaptureLoop(webcam, img -> - SwingUtilities.invokeLater(() -> cameraPanel.setImage(img)) - ); + Image ico = IconSetter.getIcon(); + setFrameIcon(new ImageIcon(ico)); + + this.cameraPanel = new CameraPanel(); + + this.captureLoop = new WebcamCaptureLoop(webcam, cameraPanel::setImage); setupUI(onOpenEffects); captureLoop.start(); @@ -36,11 +30,15 @@ public class CameraInternalFrame extends JInternalFrame { private void setupUI(Consumer 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) { + tabbedPane.setSelectedIndex(0); + openRecordingFrame(); + } else if (index == 2) { tabbedPane.setSelectedIndex(0); onOpenEffects.accept(this); } @@ -50,82 +48,24 @@ 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 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); - } - } - - 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) { + private void openRecordingFrame() { + RecordingFrame rf = new RecordingFrame(this.getTitle(), cameraPanel); + JDesktopPane desktopPane = getDesktopPane(); + if (desktopPane != null) { + desktopPane.add(rf); + rf.setVisible(true); 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); + rf.setSelected(true); + } catch (java.beans.PropertyVetoException veto) { + ShowError.error(null, "Focus Error: " + veto.getMessage(), "Error"); } } } - 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) {} - } captureLoop.stop(); super.dispose(); } diff --git a/src/main/java/io/swtc/proccessing/ui/iframe/DesktopPane.java b/src/main/java/io/swtc/proccessing/ui/iframe/DesktopPane.java index 9fc933d..e2b5267 100644 --- a/src/main/java/io/swtc/proccessing/ui/iframe/DesktopPane.java +++ b/src/main/java/io/swtc/proccessing/ui/iframe/DesktopPane.java @@ -2,7 +2,6 @@ package io.swtc.proccessing.ui.iframe; import javax.swing.*; import java.awt.*; -import java.awt.geom.CubicCurve2D; import java.util.HashMap; import java.util.Map; import java.util.Random; @@ -38,29 +37,8 @@ public class DesktopPane extends JDesktopPane { if (camera.isVisible() && effects.isVisible() && !camera.isIcon() && !effects.isIcon()) { g2d.setColor(getConnectionColor(camera)); - drawBezierConnection(g2d, camera, effects); } } g2d.dispose(); } - - private void drawBezierConnection(Graphics2D g2d, JInternalFrame from, JInternalFrame to) { - Rectangle f = from.getBounds(); - Rectangle t = to.getBounds(); - - int x1 = f.x + f.width; - int y1 = f.y + (f.height / 2); - int x2 = t.x; - int y2 = t.y + (t.height / 2); - - int ctrlOffset = Math.min(Math.abs(x2 - x1) / 2, 150); - CubicCurve2D curve = new CubicCurve2D.Double(x1, y1, x1 + ctrlOffset, y1, x2 - ctrlOffset, y2, x2, y2); - - //g2d.setStroke(new BasicStroke(3f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); - //g2d.draw(curve); - - // Terminals - //g2d.fillOval(x1 - 5, y1 - 5, 10, 10); - //g2d.fillOval(x2 - 5, y2 - 5, 10, 10); - } } \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/iframe/EffectsPanelFrame.java b/src/main/java/io/swtc/proccessing/ui/iframe/EffectsPanelFrame.java index a6533c1..62ab516 100644 --- a/src/main/java/io/swtc/proccessing/ui/iframe/EffectsPanelFrame.java +++ b/src/main/java/io/swtc/proccessing/ui/iframe/EffectsPanelFrame.java @@ -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); diff --git a/src/main/java/io/swtc/proccessing/ui/iframe/RecordingFrame.java b/src/main/java/io/swtc/proccessing/ui/iframe/RecordingFrame.java new file mode 100644 index 0000000..6e822be --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/iframe/RecordingFrame.java @@ -0,0 +1,223 @@ +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.proccessing.ui.sections.recording.ExportSection; +import io.swtc.recording.cv.AVRecorder; +import io.swtc.recording.cv.Quality; +import io.swtc.recording.cv.RecorderConfig; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import javax.imageio.ImageIO; + +public class RecordingFrame extends JInternalFrame { + private AVRecorder avRecorder; + private ExportSection exportSection; + private final CameraPanel cameraPanel; + + private JButton recordBtn; + private JLabel statusLabel; + private JLabel statsLabel; + private JComboBox presetCombo; + + private File outputDirectory; + private File currentFile; + private final Timer statsTimer; + private long startTime; + private String camName; + + private final StringBuilder sb = new StringBuilder(32); + + public RecordingFrame(String cameraName, CameraPanel cameraPanel) { + super(cameraName + " Capture", true, true, false, true); + + setupDirectory(); + + Image ico = IconSetter.getIcon(); + setFrameIcon(new ImageIcon(ico)); + + this.cameraPanel = cameraPanel; + this.camName = cameraName; + + initializeUI(); + + this.statsTimer = new Timer(1000, e -> updateStats()); + this.statsTimer.setCoalesce(true); + + pack(); + } + + private void setupDirectory() { + File videoDir = new File(System.getProperty("user.home"), "Videos"); + outputDirectory = new File(videoDir, "swtcctv-rec"); + + if (!outputDirectory.exists()) { + outputDirectory.mkdirs(); + } + } + + private void initializeUI() { + JPanel mainContent = new JPanel(); + mainContent.setLayout(new BoxLayout(mainContent, BoxLayout.Y_AXIS)); + mainContent.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + exportSection = new ExportSection(this, outputDirectory, statusLabel); + JPanel settingsPanel = createSettingsPanel(); + JPanel statsPanel = createStatsPanel(); + JPanel actionPanel = createActionPanel(); + + mainContent.add(exportSection); + mainContent.add(Box.createVerticalStrut(10)); + + mainContent.add(settingsPanel); + mainContent.add(Box.createVerticalStrut(10)); + + mainContent.add(statsPanel); + mainContent.add(Box.createVerticalStrut(10)); + + mainContent.add(actionPanel); + + getContentPane().add(mainContent); + } + + private JPanel createSettingsPanel() { + JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + panel.setBorder(BorderFactory.createTitledBorder("Encoding Settings")); + + presetCombo = new JComboBox<>(Quality.values()); + presetCombo.setSelectedItem(Quality.SUPERFAST); // Default + + panel.add(new JLabel("CPU Preset:")); + panel.add(presetCombo); + + // Disable combo box during recording to prevent mid-stream config changes + return panel; + } + + private JPanel createStatsPanel() { + JPanel panel = new JPanel(new GridLayout(2, 1)); + panel.setBorder(BorderFactory.createTitledBorder("Session Info")); + + statusLabel = new JLabel("Status: Idle"); + statsLabel = new JLabel("Length: 00:00 | Size: 0.00 MB"); + statsLabel.setFont(new Font("Monospaced", Font.PLAIN, 12)); + + panel.add(statusLabel); + panel.add(statsLabel); + return panel; + } + + private JPanel createActionPanel() { + JPanel panel = new JPanel(new GridLayout(1, 2, 5, 5)); + recordBtn = new JButton("Start Recording"); + recordBtn.addActionListener(e -> toggleRecording()); + + JButton snapBtn = new JButton("Snapshot"); + snapBtn.addActionListener(e -> takeSnapshot()); + + panel.add(recordBtn); + panel.add(snapBtn); + return panel; + } + + private void startRec() { + BufferedImage sample = cameraPanel.getCurrentProcessedImage(); + if (sample == null) { + ShowError.warning(this, "No camera feed detected.", "Warning"); + return; + } + + try { + currentFile = new File(outputDirectory, "(" + this.camName + ") " + "vid_" + System.currentTimeMillis() + ".mp4"); + + Quality selected = (Quality) presetCombo.getSelectedItem(); + String preset = (selected != null) ? selected.getFFmpegValue() : "superfast"; + + RecorderConfig config = new RecorderConfig( + currentFile, + sample.getWidth(), + sample.getHeight(), + 20, + 18, + preset + ); + + avRecorder = new AVRecorder(config); + avRecorder.start(); + + cameraPanel.setExternalRecorder(avRecorder); + + startTime = System.currentTimeMillis(); + statsTimer.start(); + + // UI Feedback + presetCombo.setEnabled(false); // Lock settings during recording + recordBtn.setText("Stop Recording"); + recordBtn.setForeground(Color.RED); + statusLabel.setText("Recording..."); + } catch (Exception ex) { + ShowError.error(this, "Failed to start: " + ex.getMessage(), "Error"); + } + } + + private void stopRec() { + if (avRecorder != null) { + statusLabel.setText("Finalizing file..."); + avRecorder.stop(); + cameraPanel.setExternalRecorder(null); + } + + statsTimer.stop(); + presetCombo.setEnabled(true); // Unlock settings + recordBtn.setText("Start Recording"); + recordBtn.setForeground(null); + + String fileName = (currentFile != null) ? currentFile.getName() : "N/A"; + statusLabel.setText("File saved: " + fileName); + } + + private void toggleRecording() { + if (avRecorder == null || !avRecorder.isRecording()) { + startRec(); + } else { + stopRec(); + } + } + + private void updateStats() { + if (avRecorder == null || !avRecorder.isRecording() || currentFile == null) return; + + long elapsedSecs = (System.currentTimeMillis() - startTime) / 1000; + long minutes = elapsedSecs / 60; + long seconds = elapsedSecs % 60; + double sizeInMb = currentFile.length() / 1048576.0; + + sb.setLength(0); + sb.append("Length: ") + .append(minutes < 10 ? "0" : "").append(minutes).append(":") + .append(seconds < 10 ? "0" : "").append(seconds) + .append(" | Size: ") + .append(String.format("%.2f", sizeInMb)) + .append(" MB"); + + statsLabel.setText(sb.toString()); + } + + private void takeSnapshot() { + BufferedImage img = cameraPanel.getCurrentProcessedImage(); + if (img != null) { + try { + File file = new File(outputDirectory, "snap_" + System.currentTimeMillis() + ".png"); + ImageIO.write(img, "PNG", file); + statusLabel.setText("Snapshot: " + file.getName()); + } catch (IOException ex) { + ShowError.error(this, "Snapshot failed: " + ex.getMessage(), "Error"); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/iframe/RecordingPane.java b/src/main/java/io/swtc/proccessing/ui/iframe/RecordingPane.java deleted file mode 100644 index 22b7d93..0000000 --- a/src/main/java/io/swtc/proccessing/ui/iframe/RecordingPane.java +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/sections/recording/ExportSection.java b/src/main/java/io/swtc/proccessing/ui/sections/recording/ExportSection.java new file mode 100644 index 0000000..2656fb7 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/sections/recording/ExportSection.java @@ -0,0 +1,132 @@ +package io.swtc.proccessing.ui.sections.recording; + +import io.swtc.proccessing.ui.ShowError; +import io.swtc.recording.evidence.USBExportManager; + +import javax.swing.*; +import java.awt.*; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +public class ExportSection extends JPanel { + private final Component parent; + private File sourceDirectory; + private final JLabel statusLabel; + private final JTextField pathField; + + public ExportSection(Component parent, File initialSource, JLabel statusLabel) { + this.parent = parent; + this.sourceDirectory = initialSource; + this.statusLabel = new JLabel("", SwingConstants.CENTER); + + + setLayout(new BorderLayout()); + + this.pathField = new JTextField(sourceDirectory.getAbsolutePath()); + this.pathField.setEditable(false); + + JTabbedPane tabbedPane = new JTabbedPane(); + tabbedPane.addTab("Storage Settings", createSettingsTab()); + tabbedPane.addTab("Evidence-Export", createTransferTab()); + + + add(tabbedPane, BorderLayout.CENTER); + } + + private JPanel createTransferTab() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + + JButton exportBtn = new JButton("Export all Evidence"); + exportBtn.addActionListener(e -> startUsbExport()); + + JLabel infoLabel = new JLabel("" + + "Export Evidence (Can also be done via Desktop)" + + ""); + + 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"); + } + } +} diff --git a/src/main/java/io/swtc/recording/VideoRecorder.java b/src/main/java/io/swtc/recording/VideoRecorder.java deleted file mode 100644 index de09f07..0000000 --- a/src/main/java/io/swtc/recording/VideoRecorder.java +++ /dev/null @@ -1,114 +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 javax.swing.*; -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. -* -* */ - -public class VideoRecorder { - private boolean recording = false; - private Timer captureTimer; - private File outputFile; - private AWTSequenceEncoder encoder; - private SeekableByteChannel channel; - private static final int FPS = 30; - - public void startRecording(CameraPanel panel, File output) throws IOException { - this.outputFile = output; - this.recording = true; - - channel = NIOUtils.writableFileChannel(String.valueOf(outputFile)); - encoder = new AWTSequenceEncoder(channel, Rational.R(FPS, 1)); - - 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 - ); - try { - stopRecording(); - } catch (IOException stopEx) { - JOptionPane.showMessageDialog( - null, - "IOException@stopEx\n" + stopEx, - "IOException in Recorder@stopEx", - JOptionPane.ERROR_MESSAGE - ); - - } - } - }); - captureTimer.start(); - } - - public File stopRecording() throws IOException { - recording = false; - - /* some helper methods, i swear its always the same? */ - if (captureTimer != null) { - captureTimer.stop(); - } - - 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; - } -} \ No newline at end of file diff --git a/src/main/java/io/swtc/recording/cv/AVRecorder.java b/src/main/java/io/swtc/recording/cv/AVRecorder.java new file mode 100644 index 0000000..f19d45c --- /dev/null +++ b/src/main/java/io/swtc/recording/cv/AVRecorder.java @@ -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 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; + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/recording/cv/FrameProccessor.java b/src/main/java/io/swtc/recording/cv/FrameProccessor.java new file mode 100644 index 0000000..6bf0f5e --- /dev/null +++ b/src/main/java/io/swtc/recording/cv/FrameProccessor.java @@ -0,0 +1,33 @@ +package io.swtc.recording.cv; + +import org.bytedeco.javacv.Frame; +import org.bytedeco.javacv.Java2DFrameConverter; + +import java.awt.image.BufferedImage; +import java.util.Objects; + +public class FrameProccessor { + private final Java2DFrameConverter converter = new Java2DFrameConverter(); + private BufferedImage reuseImg; + + public Frame convert(BufferedImage rawImg) { + if (Objects.isNull(reuseImg)) { + reuseImg = new BufferedImage( + rawImg.getWidth(), + rawImg.getHeight(), + BufferedImage.TYPE_3BYTE_BGR + ); + } + + var g = reuseImg.createGraphics(); + g.drawImage(rawImg, 0, 0, null); + g.dispose(); + + return converter.getFrame(reuseImg); + } + + public void close() { + converter.close(); + if (!Objects.isNull(reuseImg)) { reuseImg.flush(); } + } +} diff --git a/src/main/java/io/swtc/recording/cv/MediaSink.java b/src/main/java/io/swtc/recording/cv/MediaSink.java new file mode 100644 index 0000000..5f4f4d7 --- /dev/null +++ b/src/main/java/io/swtc/recording/cv/MediaSink.java @@ -0,0 +1,48 @@ +package io.swtc.recording.cv; + +import org.bytedeco.ffmpeg.global.avcodec; +import org.bytedeco.javacv.FFmpegFrameRecorder; + +import org.bytedeco.ffmpeg.global.avutil; +import org.bytedeco.javacv.Frame; + +public class MediaSink { + private final FFmpegFrameRecorder recorder; + private final long startNanos; + + public MediaSink(RecorderConfig config) throws Exception { + this.recorder = new FFmpegFrameRecorder(config.outputFile(), config.width(), config.height()); + this.startNanos = System.nanoTime(); + + avutil.av_log_set_level(avutil.AV_LOG_ERROR); + + recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264); + recorder.setFormat("mp4"); + recorder.setFrameRate(config.fps()); + /* this is essentially just building FFmpeg? Would've used ProccessBuilder for this lol */ + recorder.setVideoOption("pixel_format", "yuv420p"); + recorder.setVideoOption("preset", config.preset()); + recorder.setVideoOption("crf", String.valueOf(config.crf())); + recorder.setVideoOption("tune", "zerolatency"); + recorder.setVideoOption("x264opts", "keyint=40:min-keyint=20"); + recorder.setVideoBitrate(0); // javacv respects bitrate already ; this is for my own safety + recorder.setGopSize(config.fps() * 2); + + recorder.start(); + } + + public void write(Frame frame) throws Exception { + long pts = (System.nanoTime() - startNanos) / 1000; + if (pts > recorder.getTimestamp()) { + recorder.setTimestamp(pts); + recorder.record(frame); + } + } + + public void stop() { + try { + recorder.stop(); + recorder.release(); + } catch (Exception ignored) { /* Do absolutley nothing ;) */ } + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/recording/cv/Quality.java b/src/main/java/io/swtc/recording/cv/Quality.java new file mode 100644 index 0000000..b102727 --- /dev/null +++ b/src/main/java/io/swtc/recording/cv/Quality.java @@ -0,0 +1,25 @@ +package io.swtc.recording.cv; + +public enum Quality { + ULTRAFAST("Ultrafast (Lowest CPU)"), + SUPERFAST("Superfast"), + VERYFAST("Very Fast"), + FASTER("Faster"), + FAST("Fast"), + MEDIUM("Medium (Best Quality/Size)"); + + private final String label; + + Quality(String label) { + this.label = label; + } + + public String getFFmpegValue() { + return this.name().toLowerCase(); + } + + @Override + public String toString() { + return label; + } +} diff --git a/src/main/java/io/swtc/recording/cv/RecorderConfig.java b/src/main/java/io/swtc/recording/cv/RecorderConfig.java new file mode 100644 index 0000000..e41619c --- /dev/null +++ b/src/main/java/io/swtc/recording/cv/RecorderConfig.java @@ -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 */ } diff --git a/src/main/java/io/swtc/recording/evidence/ExportStats.java b/src/main/java/io/swtc/recording/evidence/ExportStats.java new file mode 100644 index 0000000..9804f0d --- /dev/null +++ b/src/main/java/io/swtc/recording/evidence/ExportStats.java @@ -0,0 +1,54 @@ +package io.swtc.recording.evidence; + +import java.util.concurrent.TimeUnit; + +public record ExportStats( + long totalBytes, + long copiedBytes, + long startTimeNanos, + String currentFileName +) { + + public int percent() { + if (totalBytes <= 0) return 0; + return (int) Math.min(100, (copiedBytes * 100) / totalBytes); + } + + public String timeLeft() { + if (copiedBytes <= 0) return "Calculating..."; + + long elapsedNanos = System.nanoTime() - startTimeNanos; + if (elapsedNanos <= 0) return "Calculating..."; + + // Bytes per nanosecond + double bps = (double) copiedBytes / elapsedNanos; + long remainingBytes = totalBytes - copiedBytes; + + if (remainingBytes <= 0) return "Done"; + + long remainingNanos = (long) (remainingBytes / bps); + return formatDuration(remainingNanos); + } + + public String getSpeedMBps() { + long elapsedNanos = System.nanoTime() - startTimeNanos; + if (elapsedNanos <= 0 || copiedBytes <= 0) return "0 MB/s"; + + double seconds = elapsedNanos / 1_000_000_000.0; + double megabytes = copiedBytes / (1024.0 * 1024.0); + return String.format("%.1f MB/s", megabytes / seconds); + } + + private static String formatDuration(long nanos) { + long totalSeconds = TimeUnit.NANOSECONDS.toSeconds(nanos); + if (totalSeconds < 1) return "less than a sec"; + + long mins = totalSeconds / 60; + long secs = totalSeconds % 60; + + if (mins > 0) { + return String.format("%dm %ds", mins, secs); + } + return secs + "s"; + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/recording/evidence/USBExportManager.java b/src/main/java/io/swtc/recording/evidence/USBExportManager.java new file mode 100644 index 0000000..3c2c45d --- /dev/null +++ b/src/main/java/io/swtc/recording/evidence/USBExportManager.java @@ -0,0 +1,87 @@ +package io.swtc.recording.evidence; + +import java.io.IOException; +import java.nio.file.*; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; +import java.util.function.Consumer; + +public class USBExportManager { + + public static void exportAsync(Path source, Path target, Consumer progress, Runnable onDone, Consumer onError) { + CompletableFuture.runAsync(() -> { + try { + performExport(source, target, progress); + onDone.run(); + } catch (Exception e) { + onError.accept(e); + } + }); + } + + private static void performExport(Path source, Path target, Consumer progress) throws IOException { + List allFiles; + try (Stream stream = Files.walk(source)) { + allFiles = stream.filter(Files::isRegularFile).toList(); + } + + long totalBytes = allFiles.stream().mapToLong(p -> p.toFile().length()).sum(); + + if (!hasEnoughSpace(target, totalBytes)) { + throw new IOException("Not enough space on target drive. Required: " + (totalBytes / 1024 / 1024) + " MB"); + } + + long totalCopied = 0; + long startTimeNanos = System.nanoTime(); + + for (Path file : allFiles) { + String fileName = file.getFileName().toString(); + + if (progress != null) { + progress.accept(new ExportStats(totalBytes, totalCopied, startTimeNanos, fileName)); + } + + Path relativePath = source.relativize(file); + Path destination = target.resolve(relativePath); + + Files.createDirectories(destination.getParent()); + + Files.copy(file, destination, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); + + totalCopied += Files.size(file); + + if (progress != null) { + progress.accept(new ExportStats(totalBytes, totalCopied, startTimeNanos, fileName)); + } + } + } + + public static boolean hasEnoughSpace(Path target, long requiredBytes) throws IOException { + Path root = target; + while (root != null && !Files.exists(root)) { + root = root.getParent(); + } + if (root == null) return true; + + FileStore store = Files.getFileStore(root); + return store.getUsableSpace() >= requiredBytes; + } + + + public static long calculateDirectorySize(Path dir) throws IOException { + if (dir == null || !Files.exists(dir)) return 0L; + + try (Stream walk = Files.walk(dir)) { + return walk.filter(Files::isRegularFile) + .mapToLong(p -> { + try { + return Files.size(p); + } catch (IOException e) { + return 0L; // Skip files that can't be read + } + }) + .sum(); + } + } +} \ No newline at end of file diff --git a/src/main/resources/font/OverlayFont.ttf b/src/main/resources/font/OverlayFont.ttf new file mode 100644 index 0000000..2442aff Binary files /dev/null and b/src/main/resources/font/OverlayFont.ttf differ diff --git a/src/main/resources/icons/artwork.ico b/src/main/resources/icons/artwork.ico new file mode 100644 index 0000000..2bb9a78 Binary files /dev/null and b/src/main/resources/icons/artwork.ico differ diff --git a/src/main/resources/icons/artwork.png b/src/main/resources/icons/artwork.png new file mode 100644 index 0000000..0bbf48e Binary files /dev/null and b/src/main/resources/icons/artwork.png differ diff --git a/src/main/resources/icons/effectsframe.png b/src/main/resources/icons/effectsframe.png new file mode 100644 index 0000000..20daf03 Binary files /dev/null and b/src/main/resources/icons/effectsframe.png differ diff --git a/src/main/resources/icons/explorer.png b/src/main/resources/icons/explorer.png new file mode 100644 index 0000000..c16f50b Binary files /dev/null and b/src/main/resources/icons/explorer.png differ diff --git a/src/main/resources/icons/icondbg-7.png b/src/main/resources/icons/icondbg-7.png new file mode 100644 index 0000000..72f975b Binary files /dev/null and b/src/main/resources/icons/icondbg-7.png differ diff --git a/src/main/resources/icons/rec.png b/src/main/resources/icons/rec.png new file mode 100644 index 0000000..4bafd23 Binary files /dev/null and b/src/main/resources/icons/rec.png differ diff --git a/src/main/resources/icons/save.png b/src/main/resources/icons/save.png new file mode 100644 index 0000000..61cf68e Binary files /dev/null and b/src/main/resources/icons/save.png differ