2 Commits

16 changed files with 598 additions and 441 deletions

View File

@@ -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 {

View File

@@ -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,205 +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<Webcam> 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<File> files = (List<File>) support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
for (File file : files) {
if (file.getName().endsWith(".json")) {
List<CameraConfig> 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<CameraConfig> 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<CameraConfig> 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<Void, Object[]>() {
@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<Object[]> 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());
}

View File

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

View File

@@ -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;

View File

@@ -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<CameraConfig> load() {
if (!storage_file.exists() || storage_file.length() == 0) return new ArrayList<>();
return loadFromFile(STORAGE_FILE);
}
public static List<CameraConfig> loadFromFile(File file) {
if (!file.exists() || file.length() == 0) return new ArrayList<>();
try {
return mapper.readValue(storage_file, new TypeReference<List<CameraConfig>>() {});
return MAPPER.readValue(file, new TypeReference<List<CameraConfig>>() {});
} 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<CameraConfig> 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<CameraConfig> list) {
try {
mapper.writerWithDefaultPrettyPrinter().writeValue(storage_file, list);
MAPPER.writerWithDefaultPrettyPrinter().writeValue(STORAGE_FILE, list);
} catch (IOException e) {
e.printStackTrace();
}

View File

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

View File

@@ -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
);
}

View File

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

View File

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

View File

@@ -0,0 +1,178 @@
package io.swtc.proccessing.ui.iframe;
import io.swtc.proccessing.CameraPanel;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.ShowError;
import io.swtc.recording.VideoRecorder;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
public class RecordingFrame extends JInternalFrame {
private final VideoRecorder videoRecorder;
private final CameraPanel cameraPanel;
private JButton recordBtn;
private JLabel statusLabel;
private JLabel statsLabel;
private File outputDirectory = new File(System.getProperty("user.home"));
private File currentFile;
private final Timer statsTimer;
private long startTime;
// Cache for string building to avoid object allocation every second
private final StringBuilder sb = new StringBuilder(32);
public RecordingFrame(String cameraName, CameraPanel cameraPanel, VideoRecorder videoRecorder) {
super(cameraName + " Capture", true, true, false, true);
Image ico = IconSetter.getIcon();
setFrameIcon(new ImageIcon(ico));
this.cameraPanel = cameraPanel;
this.videoRecorder = videoRecorder;
initializeUI();
this.statsTimer = new Timer(1000, e -> updateStats());
this.statsTimer.setCoalesce(true);
pack();
}
private void initializeUI() {
JPanel mainContent = new JPanel();
mainContent.setLayout(new BoxLayout(mainContent, BoxLayout.Y_AXIS));
mainContent.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
mainContent.add(createStoragePanel());
mainContent.add(Box.createVerticalStrut(10));
mainContent.add(createActionPanel());
mainContent.add(Box.createVerticalStrut(10));
mainContent.add(createStatsPanel());
getContentPane().add(mainContent);
}
private JPanel createStatsPanel() {
JPanel panel = new JPanel(new GridLayout(2, 1));
panel.setBorder(BorderFactory.createTitledBorder("Session Info"));
statusLabel = new JLabel("Status: Idle");
statsLabel = new JLabel("Length: 00:00 | Size: 0.00 MB");
statsLabel.setFont(new Font("Monospaced", Font.PLAIN, 12));
statsLabel.setPreferredSize(new Dimension(220, 20));
panel.add(statusLabel);
panel.add(statsLabel);
return panel;
}
private void updateStats() {
if (!videoRecorder.isRecording() || currentFile == null) return;
long elapsedSecs = (System.currentTimeMillis() - startTime) / 1000;
long minutes = elapsedSecs / 60;
long seconds = elapsedSecs % 60;
double sizeInMb = currentFile.length() / 1048576.0;
sb.setLength(0);
sb.append("Length: ")
.append(minutes < 10 ? "0" : "").append(minutes).append(":")
.append(seconds < 10 ? "0" : "").append(seconds)
.append(" | Size: ")
.append(Math.round(sizeInMb * 100.0) / 100.0)
.append(" MB");
statsLabel.setText(sb.toString());
}
private void toggleRecording() {
if (!videoRecorder.isRecording()) {
startRec();
} else {
stopRec();
}
}
private void startRec() {
try {
currentFile = new File(outputDirectory, "vid_" + System.currentTimeMillis() + ".mp4");
videoRecorder.startRecording(cameraPanel, currentFile);
startTime = System.currentTimeMillis();
statsTimer.start(); // Only run timer when needed
recordBtn.setText("Stop Recording");
statusLabel.setText("Currently Recording");
} catch (IOException ex) {
ShowError.error(null,"Error starting Recording" + ex.getMessage(), "Error");
}
}
private void stopRec() {
try {
videoRecorder.stopRecording();
statsTimer.stop();
recordBtn.setText("Started Recording");
recordBtn.setForeground(null);
statusLabel.setText("Finished recording");
} catch (IOException ex) {
ShowError.error(null,"RecordStop Error"+ex.getMessage(), "Error");
}
}
private JPanel createStoragePanel() {
JPanel panel = new JPanel(new BorderLayout(5, 5));
panel.setBorder(BorderFactory.createTitledBorder("Storage"));
JTextField pathField = new JTextField(outputDirectory.getAbsolutePath());
pathField.setEditable(false);
JButton browseBtn = new JButton("...");
browseBtn.addActionListener(e -> {
JFileChooser chooser = new JFileChooser(outputDirectory);
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
outputDirectory = chooser.getSelectedFile();
pathField.setText(outputDirectory.getAbsolutePath());
}
});
panel.add(pathField, BorderLayout.CENTER);
panel.add(browseBtn, BorderLayout.EAST);
return panel;
}
private JPanel createActionPanel() {
JPanel panel = new JPanel(new GridLayout(1, 2, 5, 5));
recordBtn = new JButton("Start Recording");
recordBtn.addActionListener(e -> toggleRecording());
JButton snapBtn = new JButton("Snapshot");
snapBtn.addActionListener(e -> takeSnapshot());
panel.add(recordBtn);
panel.add(snapBtn);
return panel;
}
private void takeSnapshot() {
BufferedImage img = cameraPanel.getCurrentProcessedImage();
if (img != null) {
try {
File file = new File(outputDirectory, "snap_" + System.currentTimeMillis() + ".png");
ImageIO.write(img, "PNG", file);
statusLabel.setText("Saved a Snapshot");
} catch (IOException ex) {
ShowError.error(null,"Snapshot failed"+ex.getMessage(),"Snapshot Error");
}
}
}
}

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB