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