From ddba427205e4654099d009eb165f126964e5ca29 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Sun, 4 Jan 2026 18:13:21 +0100 Subject: [PATCH] some fixes --- .gitignore | 3 + pom.xml | 32 +++ readme.md | 8 +- src/main/java/io/swtc/CCTVManager.java | 184 ++++++++++++++---- src/main/java/io/swtc/CameraWindow.java | 9 +- .../java/io/swtc/networking/CameraConfig.java | 23 +++ .../io/swtc/networking/CameraSettings.java | 45 +++++ src/main/java/io/swtc/networking/bk.gif | Bin 0 -> 77463 bytes src/main/java/io/swtc/networking/server.py | 51 +++++ .../swtc/proccessing/WebcamCaptureLoop.java | 12 +- 10 files changed, 328 insertions(+), 39 deletions(-) create mode 100644 src/main/java/io/swtc/networking/CameraConfig.java create mode 100644 src/main/java/io/swtc/networking/CameraSettings.java create mode 100644 src/main/java/io/swtc/networking/bk.gif create mode 100644 src/main/java/io/swtc/networking/server.py diff --git a/.gitignore b/.gitignore index 10a4441..8fb9740 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ build/ .mvn .idea + +## This is for our app, cause it likes to store stuff ## +network_cameras.json \ No newline at end of file diff --git a/pom.xml b/pom.xml index ce6f11b..d03adf0 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,11 @@ 0.3.12 + + com.github.sarxos + webcam-capture-driver-ipcam + 0.3.12 + @@ -62,5 +67,32 @@ 6.1.0-M1 test + + + + + + com.fasterxml.jackson.core + jackson-databind + 2.20.1 + + + + + + org.jcodec + jcodec + 0.2.5 + + + + + org.jcodec + jcodec-javase + 0.2.5 + \ No newline at end of file diff --git a/readme.md b/readme.md index ba21665..ea2e593 100644 --- a/readme.md +++ b/readme.md @@ -8,10 +8,16 @@ If you want to build this project on yourself, you will need IntelliJ (or any ot - SWT - _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 ### Future Plans: They arent too big, i want one thing more and that is some more utilities in the camera window. -Also some Network streaming but i am too lazy to do that + +### Future Plans: + +- [x] basic network cam interfacing +- [ ] better multiplexer (or whatever the viewport is called in cctv) ### Author(s): - rattatwinko \ No newline at end of file diff --git a/src/main/java/io/swtc/CCTVManager.java b/src/main/java/io/swtc/CCTVManager.java index 37529af..78c696c 100644 --- a/src/main/java/io/swtc/CCTVManager.java +++ b/src/main/java/io/swtc/CCTVManager.java @@ -1,74 +1,192 @@ package io.swtc; import com.github.sarxos.webcam.Webcam; +import com.github.sarxos.webcam.WebcamCompositeDriver; + +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 io.swtc.networking.CameraConfig; +import io.swtc.networking.CameraSettings; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.*; +import java.net.MalformedURLException; import java.util.List; public class CCTVManager { + + static { + Webcam.setDriver(new WebcamCompositeDriver() {{ + add(new WebcamDefaultDriver()); + add(new IpCamDriver()); + }}); + + for (CameraConfig config : CameraSettings.load()) { + try { + IpCamDeviceRegistry.register(config.getName(), config.getUrl(), IpCamMode.PUSH); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + } + } + public static void main(String[] args) { Display display = new Display(); - Shell shell = new Shell(display, SWT.SHELL_TRIM | SWT.ON_TOP); + Shell shell = new Shell(display); shell.setText("Dashboard"); + shell.setSize(900, 600); + + renderUI(shell, display); + + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) display.sleep(); + } + display.dispose(); + } + + private static void renderUI(Shell shell, Display display) { + // Clear existing children if refreshing + for (Control child : shell.getChildren()) child.dispose(); List webcams = Webcam.getWebcams(); - int columnCount = webcams.size() > 3 ? 3 : Math.max(1, webcams.size()); + int columnCount = Math.min(3, Math.max(1, webcams.size())); GridLayout mainLayout = new GridLayout(columnCount, true); mainLayout.marginWidth = 20; mainLayout.marginHeight = 20; - mainLayout.horizontalSpacing = 15; - mainLayout.verticalSpacing = 15; shell.setLayout(mainLayout); - // Header (Spans across all columns) - Label title = new Label(shell, SWT.NONE); - title.setText("Connected Devices"); - title.setFont(new Font(display, "Segoe UI", 14, SWT.BOLD)); - GridData titleData = new GridData(SWT.FILL, SWT.CENTER, true, false, columnCount, 1); - title.setLayoutData(titleData); + // Header Section + Composite header = new Composite(shell, SWT.NONE); + header.setLayout(new GridLayout(2, false)); + header.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, columnCount, 1)); + + Label title = new Label(header, SWT.NONE); + title.setText("Connected Devices (" + webcams.size() + ")"); + title.setFont(new Font(display, "Segoe UI", 16, SWT.BOLD)); + title.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + Button addBtn = new Button(header, SWT.PUSH); + addBtn.setText("+ Add IP Camera"); + addBtn.addListener(SWT.Selection, e -> showAddCameraDialog(shell, display)); if (webcams.isEmpty()) { - Label error = new Label(shell, SWT.NONE); - error.setText("no available cameras!"); + Label note = new Label(shell, SWT.NONE); + note.setText("No cameras detected. Add an IP camera to begin."); } else { for (Webcam webcam : webcams) { createCameraCard(shell, display, webcam); } } - shell.pack(); - shell.open(); - - while (!shell.isDisposed()) { - if (!display.readAndDispatch()) { - display.sleep(); - } - } - display.dispose(); + shell.layout(true, true); } private static void createCameraCard(Composite parent, Display display, Webcam webcam) { + // We check if its a IP Cam by getting the class, if it is then we show the delete button + boolean isIpCam = webcam.getDevice().getClass().getSimpleName().contains("IpCam"); + Group card = new Group(parent, SWT.NONE); card.setText(webcam.getName()); - card.setLayout(new GridLayout(1, false)); - card.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + card.setLayout(new GridLayout(2, false)); + card.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); - Button btn = new Button(card, SWT.PUSH); - btn.setText("View"); - btn.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + Label info = new Label(card, SWT.WRAP); + info.setText("Type: " + (isIpCam ? "Network IP" : "Local USB")); + info.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); - btn.addListener(SWT.Selection, e -> { - /** - * This is where magic happens! We start the Window Here! As arguments, we give our display and webcam index! - * */ - System.out.println("Starting: " + webcam.getName()); - CameraWindow window = new CameraWindow(display,webcam); - window.open(); + Button viewBtn = new Button(card, SWT.PUSH); + viewBtn.setText("Launch"); + viewBtn.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + viewBtn.addListener(SWT.Selection, e -> new CameraWindow(display, webcam).open()); + + Button deleteBtn = new Button(card, SWT.PUSH); + deleteBtn.setText("Delete"); + deleteBtn.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + // this is where we do that, this is where the ipcam class is used + deleteBtn.setEnabled(isIpCam); + + deleteBtn.addListener(SWT.Selection, e -> { + MessageBox mb = new MessageBox(parent.getShell(), SWT.ICON_QUESTION | SWT.YES | SWT.NO); + mb.setText("Confirm"); + mb.setMessage("Remove " + webcam.getName() + "?"); + if (mb.open() == SWT.YES) { + CameraSettings.delete(webcam.getName()); + IpCamDeviceRegistry.unregister(webcam.getName()); + renderUI(parent.getShell(), display); + } }); } + + private static void showAddCameraDialog(Shell parent, Display display) { + Shell dialog = new Shell(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL); + dialog.setText("Register New IP Camera"); + dialog.setLayout(new GridLayout(2, false)); + + new Label(dialog, SWT.NONE).setText("Name:"); + Text nameIn = new Text(dialog, SWT.BORDER); + nameIn.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + new Label(dialog, SWT.NONE).setText("MJPEG URL:"); + Text urlIn = new Text(dialog, SWT.BORDER); + urlIn.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + urlIn.setMessage("http://10.0.0.x/mjpeg"); + + // Error message label + Label errorLabel = new Label(dialog, SWT.NONE); + errorLabel.setForeground(display.getSystemColor(SWT.COLOR_RED)); + errorLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); + errorLabel.setVisible(false); + + Button save = new Button(dialog, SWT.PUSH); + save.setText("Add Camera"); + save.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); + + // error handling for stoopid people + save.addListener(SWT.Selection, e -> { + String name = nameIn.getText().trim(); + String urlString = urlIn.getText().trim(); + + if (name.isEmpty() || urlString.isEmpty()) { + errorLabel.setText("Error: All fields are required."); + errorLabel.setVisible(true); + dialog.pack(); + return; + } + + try { + if (!urlString.toLowerCase().startsWith("http://") && !urlString.toLowerCase().startsWith("https://")) { + throw new MalformedURLException("URL must start with http:// or https://"); + } + + java.net.URL validatedUrl = new java.net.URL(urlString); + + IpCamDeviceRegistry.register(name, validatedUrl.toExternalForm(), IpCamMode.PUSH); + + CameraSettings.save(new CameraConfig(name, validatedUrl.toExternalForm())); + + dialog.close(); + renderUI(parent, display); + + } catch (MalformedURLException ex) { + errorLabel.setText("Invalid URL: " + ex.getMessage()); + errorLabel.setVisible(true); + dialog.pack(); + } catch (Exception ex) { + errorLabel.setText("Registration failed: " + ex.getMessage()); + errorLabel.setVisible(true); + dialog.pack(); + } + }); + + dialog.pack(); + dialog.open(); + } } \ No newline at end of file diff --git a/src/main/java/io/swtc/CameraWindow.java b/src/main/java/io/swtc/CameraWindow.java index ec5b6e4..0a99cc7 100644 --- a/src/main/java/io/swtc/CameraWindow.java +++ b/src/main/java/io/swtc/CameraWindow.java @@ -27,10 +27,15 @@ public class CameraWindow { data.doubleBuffer = true; this.renderer = new CameraRenderer(shell, data); + // this was inefficient before this.captureLoop = new WebcamCaptureLoop(webcam, (BufferedImage img) -> { if (!display.isDisposed() && !shell.isDisposed()) { - // terribly uneficcient. very stupid tbh - display.syncExec(() -> renderer.render(img)); + display.asyncExec(() -> { + if (!shell.isDisposed()) { + // render the image to the shell using gl + renderer.render(img); + } + }); } }); diff --git a/src/main/java/io/swtc/networking/CameraConfig.java b/src/main/java/io/swtc/networking/CameraConfig.java new file mode 100644 index 0000000..1716f54 --- /dev/null +++ b/src/main/java/io/swtc/networking/CameraConfig.java @@ -0,0 +1,23 @@ +package io.swtc.networking; + +public class CameraConfig { + public String name; + public String url; + + // Default constructor for Jackson + public CameraConfig() {} + + public CameraConfig(String name, String url) { + this.name = name; + this.url = url; + } + + + public String getName() { + return name; + } + + public String getUrl() { + return url; + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/networking/CameraSettings.java b/src/main/java/io/swtc/networking/CameraSettings.java new file mode 100644 index 0000000..15e6268 --- /dev/null +++ b/src/main/java/io/swtc/networking/CameraSettings.java @@ -0,0 +1,45 @@ +package io.swtc.networking; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +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(); + + public static List load() { + if (!storage_file.exists() || storage_file.length() == 0) return new ArrayList<>(); + try { + return mapper.readValue(storage_file, new TypeReference>() {}); + } catch (IOException e) { + return new ArrayList<>(); + } + } + + public static void save(CameraConfig newCam) { + List current = load(); + current.add(newCam); + write(current); + } + + public static void delete(String name) { + List current = load(); + current.removeIf(cam -> cam.getName().equals(name)); + write(current); + } + + private static void write(List list) { + try { + mapper.writerWithDefaultPrettyPrinter().writeValue(storage_file, list); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/networking/bk.gif b/src/main/java/io/swtc/networking/bk.gif new file mode 100644 index 0000000000000000000000000000000000000000..b4cf384e23b4a6be3011beb0de96e6e117077694 GIT binary patch literal 77463 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 literal 0 HcmV?d00001 diff --git a/src/main/java/io/swtc/networking/server.py b/src/main/java/io/swtc/networking/server.py new file mode 100644 index 0000000..7484523 --- /dev/null +++ b/src/main/java/io/swtc/networking/server.py @@ -0,0 +1,51 @@ +""" + +A short test script which tests network cams + +""" +import time +import io +from http.server import BaseHTTPRequestHandler, HTTPServer +from PIL import Image, ImageSequence + +GIF_PATH = "bk.gif" +FPS = 15 + +class MJPEGHandler(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header( + "Content-Type", + "multipart/x-mixed-replace; boundary=frame" + ) + self.end_headers() + + gif = Image.open(GIF_PATH) + frames = [ + frame.convert("RGB") + for frame in ImageSequence.Iterator(gif) + ] + + try: + while True: + for frame_img in frames: + buf = io.BytesIO() + frame_img.save(buf, format="JPEG", quality=80) + frame = buf.getvalue() + + self.wfile.write(b"--frame\r\n") + self.wfile.write(b"Content-Type: image/jpeg\r\n") + self.wfile.write( + f"Content-Length: {len(frame)}\r\n\r\n".encode() + ) + self.wfile.write(frame) + self.wfile.write(b"\r\n") + + time.sleep(1 / FPS) + + except BrokenPipeError: + pass # client disconnected + +if __name__ == "__main__": + print("MJPEG GIF stream running at http://localhost:8080/") + HTTPServer(("", 8080), MJPEGHandler).serve_forever() diff --git a/src/main/java/io/swtc/proccessing/WebcamCaptureLoop.java b/src/main/java/io/swtc/proccessing/WebcamCaptureLoop.java index ad30edc..1f70711 100644 --- a/src/main/java/io/swtc/proccessing/WebcamCaptureLoop.java +++ b/src/main/java/io/swtc/proccessing/WebcamCaptureLoop.java @@ -14,9 +14,13 @@ public class WebcamCaptureLoop { this.webcam = webcam; this.onFrameCaptured = onFrameCaptured; - // Configure webcam - Dimension[] sizes = webcam.getViewSizes(); - webcam.setViewSize(sizes[sizes.length - 1]); + // this is some of the most stupid shit ive seen in years, this is needed for opening the stream. + // the webcam package may not know the res before its opened and well this is before we open it. that is below in the thread. + // so this freaks out and doesnt want to cooperate. + if (!webcam.getName().toLowerCase().contains("ipcam")) { + Dimension[] sizes = webcam.getViewSizes(); + webcam.setViewSize(sizes[sizes.length - 1]); + } } public void start() { @@ -24,11 +28,13 @@ public class WebcamCaptureLoop { running = true; Thread captureThread = new Thread(() -> { + // this is where we open it. if the res isnt known then it fucks up. webcam.open(); while (running) { BufferedImage img = webcam.getImage(); if (img != null) { + //System.err.println(img); onFrameCaptured.accept(img); }