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 0000000..b4cf384 Binary files /dev/null and b/src/main/java/io/swtc/networking/bk.gif differ 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); }