diff --git a/src/main/java/io/swtc/Main.java b/src/main/java/io/swtc/Main.java index 1f9211f..253b598 100644 --- a/src/main/java/io/swtc/Main.java +++ b/src/main/java/io/swtc/Main.java @@ -2,6 +2,7 @@ package io.swtc; import javax.swing.*; +import io.swtc.proccessing.ui.IconSetter; import io.swtc.proccessing.ui.ShowError; public class Main { diff --git a/src/main/java/io/swtc/SwingCCTVManager.java b/src/main/java/io/swtc/SwingCCTVManager.java index 89cd90a..64091d8 100644 --- a/src/main/java/io/swtc/SwingCCTVManager.java +++ b/src/main/java/io/swtc/SwingCCTVManager.java @@ -8,6 +8,7 @@ import com.github.sarxos.webcam.ds.ipcam.IpCamDriver; import com.github.sarxos.webcam.ds.ipcam.IpCamMode; import io.swtc.networking.CameraConfig; import io.swtc.networking.CameraSettings; +import io.swtc.proccessing.ui.IconSetter; import javax.swing.*; import javax.swing.table.DefaultTableCellRenderer; @@ -37,6 +38,7 @@ public class SwingCCTVManager { frame = new JFrame("dashboard"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(1000, 600); + frame.setIconImage(IconSetter.getIcon()); this.IFrame = new SwingIFrame(); this.IFrame.show(); @@ -71,14 +73,33 @@ public class SwingCCTVManager { } }); + JToolBar toolBar = new JToolBar(); + + Image ogIcon = IconSetter.getIcon(); + Image scaledIcon = ogIcon.getScaledInstance(32, 32, Image.SCALE_SMOOTH); + JLabel iconLabel = new JLabel(new ImageIcon(scaledIcon)); + toolBar.add(iconLabel); + + JLabel textLabel = new JLabel("SWT-CCTV"); + toolBar.add(textLabel); + + toolBar.add(Box.createRigidArea(new Dimension(10, 0))); // 20px horizontal padding + + toolBar.addSeparator(new Dimension(10, 32)); // 10px width, 32px height + JButton btnAdd = new JButton("Add IP Cam"); JButton btnLaunch = new JButton("Launch Stream"); + + btnAdd.setPreferredSize(new Dimension(100, 32)); + btnLaunch.setPreferredSize(new Dimension(120, 32)); + + toolBar.add(Box.createRigidArea(new Dimension(5,0))); toolBar.add(btnAdd); toolBar.addSeparator(); toolBar.add(btnLaunch); - frame.add(toolBar, BorderLayout.NORTH); + frame.add(toolBar, BorderLayout.SOUTH); frame.add(new JScrollPane(deviceTable), BorderLayout.CENTER); btnAdd.addActionListener(e -> showAddCameraDialog()); @@ -145,12 +166,14 @@ public class SwingCCTVManager { } JPopupMenu menu = new JPopupMenu(); + JMenuItem ico = new JMenuItem("test"); JMenuItem launch = new JMenuItem("Launch Live Stream"); JMenuItem delete = new JMenuItem("Remove Device"); launch.addActionListener(al -> launchSelected()); delete.addActionListener(al -> deleteSelected()); + menu.add(ico); menu.add(launch); menu.addSeparator(); menu.add(delete); diff --git a/src/main/java/io/swtc/SwingIFrame.java b/src/main/java/io/swtc/SwingIFrame.java index ff1cc03..f830cd1 100644 --- a/src/main/java/io/swtc/SwingIFrame.java +++ b/src/main/java/io/swtc/SwingIFrame.java @@ -1,6 +1,7 @@ package io.swtc; import com.github.sarxos.webcam.Webcam; +import io.swtc.proccessing.ui.IconSetter; import io.swtc.proccessing.ui.iframe.*; // Your custom frames import javax.swing.*; import java.awt.*; @@ -28,6 +29,8 @@ public class SwingIFrame { mainFrame.setSize(1280, 720); mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + mainFrame.setIconImage(IconSetter.getIcon()); + desktopPane = new DesktopPane(cameraToEffects); desktopPane.setBackground(defDesktopBg); mainFrame.add(desktopPane, BorderLayout.CENTER); 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..afbc5ba --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/IconSetter.java @@ -0,0 +1,40 @@ +package io.swtc.proccessing.ui; + +import javax.swing.*; +import java.awt.*; +import java.net.URL; +import java.util.Objects; + +public class IconSetter { + + private static Image ICON_IMAGE; + private static ImageIcon ICON_ICON; + private static Image effects_icon; + + public static Image getIcon() { + if (ICON_IMAGE == null) { + URL url = IconSetter.class.getResource("/icons/artwork.png"); + if (url == null) throw new RuntimeException("Icon not found: /icons/artwork.png"); + ICON_IMAGE = Toolkit.getDefaultToolkit().getImage(url); + } + return ICON_IMAGE; + } + + public static Image getEffectIcon() { + if (Objects.isNull(effects_icon)) { + URL url = IconSetter.class.getResource("/icons/effectsframe.png"); + if (Objects.isNull(url)) ShowError.error(null,"Error","Icon not found"); + effects_icon = Toolkit.getDefaultToolkit().getImage(url); + } + return effects_icon; + } + + public static ImageIcon getIconAsImageIcon() { + if (ICON_ICON == null) { + URL url = IconSetter.class.getResource("/icons/artwork.png"); + if (url == null) throw new RuntimeException("Icon not found: /icons/artwork.png"); + ICON_ICON = new ImageIcon(url); // separate variable for ImageIcon + } + return ICON_ICON; + } +} 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/iframe/CameraInternalFrame.java b/src/main/java/io/swtc/proccessing/ui/iframe/CameraInternalFrame.java index 127789a..4eb03ea 100644 --- a/src/main/java/io/swtc/proccessing/ui/iframe/CameraInternalFrame.java +++ b/src/main/java/io/swtc/proccessing/ui/iframe/CameraInternalFrame.java @@ -3,12 +3,16 @@ package io.swtc.proccessing.ui.iframe; import com.github.sarxos.webcam.Webcam; import io.swtc.proccessing.WebcamCaptureLoop; import io.swtc.proccessing.CameraPanel; +import io.swtc.proccessing.ui.IconSetter; +import io.swtc.proccessing.ui.ShowError; +import io.swtc.recording.RecordingManager; import io.swtc.recording.VideoRecorder; import javax.imageio.ImageIO; import javax.swing.*; import javax.swing.border.EmptyBorder; import java.awt.*; import java.awt.image.BufferedImage; +import java.awt.image.RescaleOp; import java.io.File; import java.io.IOException; import java.util.function.Consumer; @@ -16,15 +20,19 @@ import java.util.function.Consumer; public class CameraInternalFrame extends JInternalFrame { private final WebcamCaptureLoop captureLoop; private final CameraPanel cameraPanel; - private final VideoRecorder videoRecorder; // Instance of the recorder - private JButton recordBtn; + private final VideoRecorder videoRecorder; + private final RecordingManager recordingManager; public CameraInternalFrame(Webcam webcam, Consumer onOpenEffects) { super(webcam.getName(), true, true, true, true); - this.cameraPanel = new CameraPanel(); - this.videoRecorder = new VideoRecorder(); // Initialize recorder - // Initialize capture loop + Image ico = IconSetter.getIcon(); + setFrameIcon(new ImageIcon(ico)); + + this.cameraPanel = new CameraPanel(); + this.videoRecorder = new VideoRecorder(); + this.recordingManager = new RecordingManager(cameraPanel); // Initialize recorder + this.captureLoop = new WebcamCaptureLoop(webcam, img -> SwingUtilities.invokeLater(() -> cameraPanel.setImage(img)) ); @@ -36,11 +44,15 @@ public class CameraInternalFrame extends JInternalFrame { private void setupUI(Consumer onOpenEffects) { JTabbedPane tabbedPane = new JTabbedPane(); tabbedPane.addTab("View", cameraPanel); - tabbedPane.addTab("Capture", new RecordingPane(cameraPanel, videoRecorder)); + tabbedPane.addTab("Capture", new JPanel()); tabbedPane.addTab("Effects", new JPanel()); tabbedPane.addChangeListener(e -> { - if (tabbedPane.getSelectedIndex() == 2) { + int index = tabbedPane.getSelectedIndex(); + if (index == 1) { // the tab index for capture is 1 + tabbedPane.setSelectedIndex(0); + openRecordingFrame(); + } else if (index == 2) { tabbedPane.setSelectedIndex(0); onOpenEffects.accept(this); } @@ -50,79 +62,25 @@ public class CameraInternalFrame extends JInternalFrame { setSize(600, 500); } - private JPanel createCapturePanel() { - JPanel panel = new JPanel(new GridLayout(2, 1, 10, 10)); // Changed to Grid for better button layout - panel.setBorder(new EmptyBorder(15, 15, 15, 15)); - - JButton screenshotBtn = new JButton("Take Screenshot"); - screenshotBtn.addActionListener(e -> saveSnapshot()); - - recordBtn = new JButton("Start Recording"); - recordBtn.addActionListener(e -> toggleRecording()); - - panel.add(screenshotBtn); - panel.add(recordBtn); - - // Wrap in a wrapper to prevent buttons from stretching too much - JPanel wrapper = new JPanel(new BorderLayout()); - wrapper.add(panel, BorderLayout.NORTH); - return wrapper; - } - - private void toggleRecording() { - if (!videoRecorder.isRecording()) { - startVideo(); - } else { - stopVideo(); + private void openRecordingFrame() { + RecordingFrame rf = new RecordingFrame(this.getTitle(),cameraPanel, videoRecorder); + JDesktopPane desktopPane = getDesktopPane(); + if (desktopPane != null) { + desktopPane.add(rf); + rf.setVisible(true); } - } - private void startVideo() { try { - File file = new File(System.getProperty("user.home"), "vid_" + System.currentTimeMillis() + ".mp4"); - videoRecorder.startRecording(cameraPanel, file); - - recordBtn.setText("Stop Recording"); - recordBtn.setForeground(Color.RED); - } catch (IOException ex) { - showError("Failed to start recording", ex); + rf.setSelected(true); + } catch (java.beans.PropertyVetoException veto) { + ShowError.error(null,"VetoException"+veto.getMessage(),"veto"); } } - private void stopVideo() { - try { - File savedFile = videoRecorder.stopRecording(); - - recordBtn.setText("Start Recording"); - recordBtn.setForeground(Color.BLACK); - - JOptionPane.showMessageDialog(this, "Video saved to: " + savedFile.getAbsolutePath()); - } catch (IOException ex) { - showError("Failed to stop recording safely", ex); - } - } - - private void saveSnapshot() { - BufferedImage img = cameraPanel.getCurrentProcessedImage(); - if (img != null) { - try { - File file = new File(System.getProperty("user.home"), "snap_" + System.currentTimeMillis() + ".png"); - ImageIO.write(img, "PNG", file); - } catch (Exception ex) { - showError("Snapshot failed", ex); - } - } - } - - private void showError(String title, Exception ex) { - JOptionPane.showMessageDialog(this, title + "\n" + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); - } - public CameraPanel getCameraPanel() { return cameraPanel; } @Override public void dispose() { - // Safety check: stop recording if the window is closed if (videoRecorder.isRecording()) { try { videoRecorder.stopRecording(); } catch (IOException ignored) {} } 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..5fd4947 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/iframe/RecordingFrame.java @@ -0,0 +1,178 @@ +package io.swtc.proccessing.ui.iframe; + +import io.swtc.proccessing.CameraPanel; +import io.swtc.proccessing.ui.IconSetter; +import io.swtc.proccessing.ui.ShowError; +import io.swtc.recording.VideoRecorder; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import javax.imageio.ImageIO; + +public class RecordingFrame extends JInternalFrame { + private final VideoRecorder videoRecorder; + private final CameraPanel cameraPanel; + + private JButton recordBtn; + private JLabel statusLabel; + private JLabel statsLabel; + + private File outputDirectory = new File(System.getProperty("user.home")); + private File currentFile; + private final Timer statsTimer; + private long startTime; + + + // Cache for string building to avoid object allocation every second + private final StringBuilder sb = new StringBuilder(32); + + public RecordingFrame(String cameraName, CameraPanel cameraPanel, VideoRecorder videoRecorder) { + super(cameraName + " Capture", true, true, false, true); + + Image ico = IconSetter.getIcon(); + setFrameIcon(new ImageIcon(ico)); + + this.cameraPanel = cameraPanel; + this.videoRecorder = videoRecorder; + + initializeUI(); + + this.statsTimer = new Timer(1000, e -> updateStats()); + this.statsTimer.setCoalesce(true); + + pack(); + } + + private void initializeUI() { + JPanel mainContent = new JPanel(); + mainContent.setLayout(new BoxLayout(mainContent, BoxLayout.Y_AXIS)); + mainContent.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + mainContent.add(createStoragePanel()); + mainContent.add(Box.createVerticalStrut(10)); + mainContent.add(createActionPanel()); + mainContent.add(Box.createVerticalStrut(10)); + mainContent.add(createStatsPanel()); + + getContentPane().add(mainContent); + } + + private JPanel createStatsPanel() { + JPanel panel = new JPanel(new GridLayout(2, 1)); + panel.setBorder(BorderFactory.createTitledBorder("Session Info")); + + statusLabel = new JLabel("Status: Idle"); + + statsLabel = new JLabel("Length: 00:00 | Size: 0.00 MB"); + statsLabel.setFont(new Font("Monospaced", Font.PLAIN, 12)); + + statsLabel.setPreferredSize(new Dimension(220, 20)); + + panel.add(statusLabel); + panel.add(statsLabel); + return panel; + } + + private void updateStats() { + if (!videoRecorder.isRecording() || currentFile == null) return; + + long elapsedSecs = (System.currentTimeMillis() - startTime) / 1000; + long minutes = elapsedSecs / 60; + long seconds = elapsedSecs % 60; + + double sizeInMb = currentFile.length() / 1048576.0; + + sb.setLength(0); + sb.append("Length: ") + .append(minutes < 10 ? "0" : "").append(minutes).append(":") + .append(seconds < 10 ? "0" : "").append(seconds) + .append(" | Size: ") + .append(Math.round(sizeInMb * 100.0) / 100.0) + .append(" MB"); + + statsLabel.setText(sb.toString()); + } + + private void toggleRecording() { + if (!videoRecorder.isRecording()) { + startRec(); + } else { + stopRec(); + } + } + + private void startRec() { + try { + currentFile = new File(outputDirectory, "vid_" + System.currentTimeMillis() + ".mp4"); + videoRecorder.startRecording(cameraPanel, currentFile); + + startTime = System.currentTimeMillis(); + statsTimer.start(); // Only run timer when needed + + recordBtn.setText("Stop Recording"); + statusLabel.setText("Currently Recording"); + } catch (IOException ex) { + ShowError.error(null,"Error starting Recording" + ex.getMessage(), "Error"); + } + } + + private void stopRec() { + try { + videoRecorder.stopRecording(); + statsTimer.stop(); + + recordBtn.setText("Started Recording"); + recordBtn.setForeground(null); + statusLabel.setText("Finished recording"); + } catch (IOException ex) { + ShowError.error(null,"RecordStop Error"+ex.getMessage(), "Error"); + } + } + + private JPanel createStoragePanel() { + JPanel panel = new JPanel(new BorderLayout(5, 5)); + panel.setBorder(BorderFactory.createTitledBorder("Storage")); + JTextField pathField = new JTextField(outputDirectory.getAbsolutePath()); + pathField.setEditable(false); + JButton browseBtn = new JButton("..."); + browseBtn.addActionListener(e -> { + JFileChooser chooser = new JFileChooser(outputDirectory); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { + outputDirectory = chooser.getSelectedFile(); + pathField.setText(outputDirectory.getAbsolutePath()); + } + }); + panel.add(pathField, BorderLayout.CENTER); + panel.add(browseBtn, BorderLayout.EAST); + return panel; + } + + private JPanel createActionPanel() { + JPanel panel = new JPanel(new GridLayout(1, 2, 5, 5)); + recordBtn = new JButton("Start Recording"); + recordBtn.addActionListener(e -> toggleRecording()); + JButton snapBtn = new JButton("Snapshot"); + snapBtn.addActionListener(e -> takeSnapshot()); + panel.add(recordBtn); + panel.add(snapBtn); + return panel; + } + + private void takeSnapshot() { + BufferedImage img = cameraPanel.getCurrentProcessedImage(); + if (img != null) { + try { + File file = new File(outputDirectory, "snap_" + System.currentTimeMillis() + ".png"); + ImageIO.write(img, "PNG", file); + statusLabel.setText("Saved a Snapshot"); + } catch (IOException ex) { + ShowError.error(null,"Snapshot failed"+ex.getMessage(),"Snapshot Error"); + } + } + } + +} \ 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/recording/RecordingManager.java b/src/main/java/io/swtc/recording/RecordingManager.java new file mode 100644 index 0000000..906e78a --- /dev/null +++ b/src/main/java/io/swtc/recording/RecordingManager.java @@ -0,0 +1,70 @@ +package io.swtc.recording; + +import io.swtc.proccessing.CameraPanel; +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.util.function.Consumer; + +public class RecordingManager { + private final VideoRecorder recorder; + private final CameraPanel cameraPanel; + + // Reuse a single buffer to avoid constant memory allocation (GC pressure) + private BufferedImage reuseBuffer; + + public RecordingManager(CameraPanel cameraPanel) { + this.cameraPanel = cameraPanel; + this.recorder = new VideoRecorder(); + } + + public BufferedImage fastConvertToRGB(BufferedImage source) { + if (source.getType() == BufferedImage.TYPE_INT_RGB) { + return source; + } + + // Initialize or resize reuseBuffer only when necessary + if (reuseBuffer == null || + reuseBuffer.getWidth() != source.getWidth() || + reuseBuffer.getHeight() != source.getHeight()) { + reuseBuffer = new BufferedImage(source.getWidth(), source.getHeight(), BufferedImage.TYPE_INT_RGB); + } + + // Graphics2D.drawImage is much faster than manual setRGB loops + Graphics2D g = reuseBuffer.createGraphics(); + g.drawImage(source, 0, 0, null); + g.dispose(); + + return reuseBuffer; + } + + public void toggleRecording(File outputFile, Runnable onStart, Consumer onStop, Consumer onError) { + if (!recorder.isRecording()) { + new SwingWorker() { + @Override + protected Void doInBackground() throws Exception { + recorder.startRecording(cameraPanel, outputFile); + return null; + } + @Override + protected void done() { + try { get(); if (onStart != null) onStart.run(); } + catch (Exception e) { if (onError != null) onError.accept(e); } + } + }.execute(); + } else { + new SwingWorker() { + @Override + protected File doInBackground() throws Exception { + return recorder.stopRecording(); + } + @Override + protected void done() { + try { File f = get(); if (onStop != null) onStop.accept(f); } + catch (Exception e) { if (onError != null) onError.accept(e); } + } + }.execute(); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/recording/VideoRecorder.java b/src/main/java/io/swtc/recording/VideoRecorder.java index de09f07..352bfec 100644 --- a/src/main/java/io/swtc/recording/VideoRecorder.java +++ b/src/main/java/io/swtc/recording/VideoRecorder.java @@ -6,109 +6,82 @@ import org.jcodec.common.io.NIOUtils; import org.jcodec.common.io.SeekableByteChannel; import org.jcodec.common.model.Rational; -import javax.swing.*; +import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; - -/* -* -* This is not that interesting but surely improtant for any security based people, -* here we record using jcodec, which is more efficient than me writing my own recorder, -* which i am certainly not doing. -* -* Anyways we dont do shit properly anyway. -* -* */ +import java.util.concurrent.*; public class VideoRecorder { - private boolean recording = false; - private Timer captureTimer; + private volatile boolean recording = false; private File outputFile; private AWTSequenceEncoder encoder; private SeekableByteChannel channel; private static final int FPS = 30; + private LinkedBlockingQueue frameQueue; + private ExecutorService workerThread; + public void startRecording(CameraPanel panel, File output) throws IOException { this.outputFile = output; + this.channel = NIOUtils.writableFileChannel(output.getAbsolutePath()); + this.encoder = new AWTSequenceEncoder(channel, Rational.R(FPS, 1)); + + this.frameQueue = new LinkedBlockingQueue<>(60); this.recording = true; - channel = NIOUtils.writableFileChannel(String.valueOf(outputFile)); - encoder = new AWTSequenceEncoder(channel, Rational.R(FPS, 1)); + workerThread = Executors.newSingleThreadExecutor(); + workerThread.submit(this::processQueue); - captureTimer = new Timer(1000 / FPS, e -> { - try { - BufferedImage img = panel.getCurrentProcessedImage(); - if (img != null) { - BufferedImage rgbImage = convertToRGB(img); - encoder.encodeImage(rgbImage); - } - } catch (IOException ex) { - JOptionPane.showMessageDialog( - null, - "IOException\n" + ex, - "IOException in Recorder", - JOptionPane.ERROR_MESSAGE - ); + new Thread(() -> { + while (recording) { try { - stopRecording(); - } catch (IOException stopEx) { - JOptionPane.showMessageDialog( - null, - "IOException@stopEx\n" + stopEx, - "IOException in Recorder@stopEx", - JOptionPane.ERROR_MESSAGE - ); - + BufferedImage img = panel.getCurrentProcessedImage(); + if (img != null) { + BufferedImage copy = snapshotToRGB(img); + frameQueue.offer(copy); + } + Thread.sleep(1000 / FPS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } } - }); - captureTimer.start(); + }).start(); + } + + private void processQueue() { + while (recording || !frameQueue.isEmpty()) { + try { + BufferedImage img = frameQueue.poll(500, TimeUnit.MILLISECONDS); + if (img != null) { + encoder.encodeImage(img); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private BufferedImage snapshotToRGB(BufferedImage source) { + BufferedImage rgb = new BufferedImage(source.getWidth(), source.getHeight(), BufferedImage.TYPE_INT_RGB); + Graphics2D g = rgb.createGraphics(); + g.drawImage(source, 0, 0, null); + g.dispose(); + return rgb; } public File stopRecording() throws IOException { recording = false; - - /* some helper methods, i swear its always the same? */ - if (captureTimer != null) { - captureTimer.stop(); + workerThread.shutdown(); + try { + workerThread.awaitTermination(5, TimeUnit.SECONDS); + if (encoder != null) encoder.finish(); + if (channel != null) channel.close(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } - - if (encoder != null) { - encoder.finish(); - } - - if (channel != null) { - channel.close(); - } - return outputFile; } - public boolean isRecording() { - return recording; - } - - // maybe i should move this to a component, some useless conversion right here, - // (performance wise) - // yes, capitain? - private BufferedImage convertToRGB(BufferedImage source) { - if (source.getType() == BufferedImage.TYPE_INT_RGB) { - return source; - } - - BufferedImage rgb = new BufferedImage( - source.getWidth(), - source.getHeight(), - BufferedImage.TYPE_INT_RGB - ); - - for (int y = 0; y < source.getHeight(); y++) { - for (int x = 0; x < source.getWidth(); x++) { - rgb.setRGB(x, y, source.getRGB(x, y) & 0xFFFFFF); - } - } - - return rgb; - } + public boolean isRecording() { return recording; } } \ No newline at end of file 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