diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 6c37cd7..dd1d447 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -14,22 +14,20 @@ permissions: read-all
jobs:
build:
-
-
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
- name: Buildah Action
uses: redhat-actions/buildah-build@v2
with:
- image: phantom
+ image: ghcr.io/${{ github.repository }}
tags: ${{ github.sha }}
containerfiles: |
./Containerfile
extra-args: |
--target=build
- name: Run go vet
- run: podman run phantom:${{ github.sha }} go vet -v ./...
+ run: podman run ghcr.io/${{ github.repository }}:${{ github.sha }} go vet -v ./...
- name: Run go test
- run: podman run phantom:${{ github.sha }} go test -v ./...
+ run: podman run ghcr.io/${{ github.repository }}:${{ github.sha }} go test -v ./...
diff --git a/Containerfile b/Containerfile
index 435a847..29500db 100644
--- a/Containerfile
+++ b/Containerfile
@@ -2,7 +2,7 @@ FROM docker.io/golang:1.24.2-alpine3.21 AS build
WORKDIR /app
COPY . .
RUN apk add build-base musl-dev opencv-dev icu-libs --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community
-RUN go build main.go
+RUN go build cmd/main.go
FROM docker.io/alpine:3.21
WORKDIR /app
diff --git a/README.md b/README.md
index d2d62e2..83d6ec9 100644
--- a/README.md
+++ b/README.md
@@ -10,9 +10,20 @@ stream video from usb device over the web
In order to install opencv4 you can either rely on your system package manager or compile it directly using:
```bash
-pushd $GOPATH/gocv.io/x/gocv
+cd $GOPATH/gocv.io/x/gocv
make install
-popd
```
+## Getting started
+Build the container
+
+```bash
+podman build -t phantom:$(git describe) .
+```
+
+Run the container
+
+```bash
+podman run --device /dev/video0 -p 8080:8080 localhost/phantom:$(git describe) /app/main
+```
diff --git a/api/middlewares/recovery.go b/api/middlewares/recovery.go
deleted file mode 100644
index 95d61c5..0000000
--- a/api/middlewares/recovery.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package middlewares
-
-import (
- "log"
- "net/http"
-)
-
-func Recovery(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- defer func() {
- err := recover()
- if err != nil {
- log.Println(err)
-
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusInternalServerError)
- w.Write([]byte("{\"error\": \"Internal Server Error\"}"))
- }
- }()
-
- next.ServeHTTP(w, r)
- })
-}
diff --git a/api/server.go b/api/server.go
deleted file mode 100644
index 04ac067..0000000
--- a/api/server.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package api
-
-import (
- "log/slog"
- "net/http"
- "os"
-
- "github.com/bugbundle/phantom/api/middlewares"
- "github.com/bugbundle/phantom/api/routes"
-)
-
-func Server(addr string) {
- // Configure default logging to JSON
- logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
- slog.SetDefault(logger)
-
- router := http.NewServeMux()
-
- router.HandleFunc("/", routes.Homepage)
- router.HandleFunc("POST /cameras", routes.CreateCamera)
- router.HandleFunc("GET /cameras/status", routes.StreamStatus)
- router.HandleFunc("DELETE /cameras", routes.DeleteCamera)
- router.HandleFunc("GET /cameras", routes.StreamVideo)
-
- server_config := &http.Server{
- Addr: addr,
- Handler: middlewares.LoggingHandler(
- middlewares.Recovery(router),
- ),
- }
-
- slog.Info("Starting server...", "interface", addr)
- if err := server_config.ListenAndServe(); err != nil {
- slog.Error("An error occured !", "interface", addr, "error", err)
- }
-}
diff --git a/api/templates/index.html.tpl b/api/templates/index.html.tpl
deleted file mode 100644
index 37270d4..0000000
--- a/api/templates/index.html.tpl
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
- Phantom dashboard
-
-
-
-
-
-
-

-
-
-
-
-
-
-
diff --git a/cmd/main.go b/cmd/main.go
index 490aee2..e95e196 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -1,23 +1,9 @@
-package cmd
+package main
import (
- "fmt"
- "os"
-
- "github.com/spf13/cobra"
+ "github.com/bugbundle/phantom/internal/adapter/http"
)
-var rootCmd = &cobra.Command{
- Use: "phantom",
- Short: "phantom is used to stream video device over internet using multipart",
- Run: func(cmd *cobra.Command, args []string) {
- cmd.Usage()
- },
-}
-
-func Execute() {
- if err := rootCmd.Execute(); err != nil {
- fmt.Println(err)
- os.Exit(1)
- }
+func main() {
+ http.Server("0.0.0.0:8080")
}
diff --git a/cmd/server.go b/cmd/server.go
deleted file mode 100644
index 1147220..0000000
--- a/cmd/server.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package cmd
-
-import (
- "log"
-
- "github.com/bugbundle/phantom/api"
- "github.com/spf13/cobra"
-)
-
-func init() {
- rootCmd.AddCommand(serverCmd)
- serverCmd.Flags().String("addr", "127.0.0.1:8080", "Listening interface")
-}
-
-var serverCmd = &cobra.Command{
- Use: "server",
- Short: "Run phantom in server mode",
- Run: func(cmd *cobra.Command, args []string) {
- addr, err := cmd.Flags().GetString("addr")
- if err != nil {
- log.Printf("Can't retrieve addr flag.")
- }
- api.Server(addr)
- },
-}
diff --git a/go.mod b/go.mod
index bcbcac0..b1f7012 100644
--- a/go.mod
+++ b/go.mod
@@ -2,12 +2,4 @@ module github.com/bugbundle/phantom
go 1.22.2
-require (
- github.com/spf13/cobra v1.9.1
- gocv.io/x/gocv v0.41.0
-)
-
-require (
- github.com/inconshreveable/mousetrap v1.1.0 // indirect
- github.com/spf13/pflag v1.0.6 // indirect
-)
+require gocv.io/x/gocv v0.41.0
diff --git a/go.sum b/go.sum
index 3abaa46..ffdc39e 100644
--- a/go.sum
+++ b/go.sum
@@ -1,12 +1,2 @@
-github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
-github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
-github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
-github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
-github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
-github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
gocv.io/x/gocv v0.41.0 h1:KM+zRXUP28b6dHfhy+4JxDODbCNQNtLg8kio+YE7TqA=
gocv.io/x/gocv v0.41.0/go.mod h1:zYdWMj29WAEznM3Y8NsU3A0TRq/wR/cy75jeUypThqU=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/adapter/camera/camera.go b/internal/adapter/camera/camera.go
new file mode 100644
index 0000000..522ea51
--- /dev/null
+++ b/internal/adapter/camera/camera.go
@@ -0,0 +1 @@
+package camera
diff --git a/internal/adapter/http/http.go b/internal/adapter/http/http.go
new file mode 100644
index 0000000..08dff3c
--- /dev/null
+++ b/internal/adapter/http/http.go
@@ -0,0 +1,51 @@
+package http
+
+import (
+ "fmt"
+ "log/slog"
+ "net/http"
+
+ "github.com/bugbundle/phantom/internal/adapter/logger"
+ httpRoutes "github.com/bugbundle/phantom/internal/app/http"
+)
+
+func Recovery(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ defer func() {
+ err := recover()
+ if err != nil {
+ slog.Error(fmt.Sprint(err))
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte("{\"error\": \"Internal Server Error\"}"))
+ }
+ }()
+
+ next.ServeHTTP(w, r)
+ })
+}
+
+func Server(addr string) {
+ fmt.Println("here")
+ router := http.NewServeMux()
+
+ router.HandleFunc("/", httpRoutes.Homepage)
+ router.HandleFunc("POST /cameras", httpRoutes.CreateCamera)
+ router.HandleFunc("GET /cameras/status", httpRoutes.StreamStatus)
+ router.HandleFunc("DELETE /cameras", httpRoutes.DeleteCamera)
+ router.HandleFunc("GET /cameras", httpRoutes.StreamVideo)
+
+ fmt.Println("hore")
+ server_config := &http.Server{
+ Addr: addr,
+ Handler: logger.LoggingHandler(
+ Recovery(router),
+ ),
+ }
+
+ slog.Info("Starting server...", "interface", addr)
+ if err := server_config.ListenAndServe(); err != nil {
+ slog.Error("An error occured !", "interface", addr, "error", err)
+ }
+}
diff --git a/api/middlewares/logging.go b/internal/adapter/logger/logger.go
similarity index 83%
rename from api/middlewares/logging.go
rename to internal/adapter/logger/logger.go
index ed97ee3..33e0c8d 100644
--- a/api/middlewares/logging.go
+++ b/internal/adapter/logger/logger.go
@@ -1,9 +1,10 @@
-package middlewares
+package logger
import (
"context"
"log/slog"
"net/http"
+ "os"
)
type statusResponseWriter struct {
@@ -11,6 +12,11 @@ type statusResponseWriter struct {
statusCode int
}
+func init() {
+ logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
+ slog.SetDefault(logger)
+}
+
func LoggingHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
statusResponseWriter := &statusResponseWriter{
diff --git a/api/routes/stream.go b/internal/app/http/http.go
similarity index 92%
rename from api/routes/stream.go
rename to internal/app/http/http.go
index 54e67aa..78eb1c8 100644
--- a/api/routes/stream.go
+++ b/internal/app/http/http.go
@@ -1,4 +1,4 @@
-package routes
+package http
import (
"fmt"
@@ -10,14 +10,14 @@ import (
"net/textproto"
"time"
- "github.com/bugbundle/phantom/api/utils"
+ "github.com/bugbundle/phantom/internal/port/camera"
"gocv.io/x/gocv"
)
// Return default Homepage, a simple alpineJS application to users
func Homepage(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html")
- template, err := template.ParseFiles("api/templates/index.html.tpl")
+ template, err := template.ParseFiles("templates/index.html.tpl")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -30,20 +30,19 @@ func Homepage(w http.ResponseWriter, r *http.Request) {
func StreamStatus(w http.ResponseWriter, r *http.Request) {
// If the camera is unavailable return 428
- _, err := utils.GetCamera()
+ _, err := camera.GetCamera()
if err != nil {
w.Write([]byte("false"))
return
}
w.Write([]byte("true"))
- return
}
// This function retrieve camera device and start streaming using multipart/x-mixed-replace
// TODO: Add device number option
func StreamVideo(w http.ResponseWriter, r *http.Request) {
// If the camera is unavailable return 428
- webcam, err := utils.GetCamera()
+ webcam, err := camera.GetCamera()
if err != nil {
w.Header().Set("Content-Type", "application/json")
http.Error(w, "{\"reason\": \"webcam is not started\"}", http.StatusPreconditionRequired)
@@ -100,7 +99,7 @@ func StreamVideo(w http.ResponseWriter, r *http.Request) {
// TODO: Add device number option
func CreateCamera(w http.ResponseWriter, r *http.Request) {
// Trigger singleton to instanciate camera
- utils.CreateOrGetCamera()
+ camera.CreateOrGetCamera()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
}
@@ -108,7 +107,7 @@ func CreateCamera(w http.ResponseWriter, r *http.Request) {
// Delete Camera using DELETE request
// TODO: Add device number option
func DeleteCamera(w http.ResponseWriter, r *http.Request) {
- utils.DeleteCamera()
+ camera.DeleteCamera()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
diff --git a/internal/domain/camera/camera.go b/internal/domain/camera/camera.go
new file mode 100644
index 0000000..522ea51
--- /dev/null
+++ b/internal/domain/camera/camera.go
@@ -0,0 +1 @@
+package camera
diff --git a/api/utils/camera.go b/internal/port/camera/camera.go
similarity index 90%
rename from api/utils/camera.go
rename to internal/port/camera/camera.go
index 517d237..8f9df94 100644
--- a/api/utils/camera.go
+++ b/internal/port/camera/camera.go
@@ -1,4 +1,4 @@
-package utils
+package camera
import (
"errors"
@@ -7,13 +7,26 @@ import (
"gocv.io/x/gocv"
)
-var instance *WebCamSingleton
-
type WebCamSingleton struct {
- capture *gocv.VideoCapture
+ Capture *gocv.VideoCapture
isOpen bool
}
+var instance *WebCamSingleton
+
+// CaptureImage captures an image from the webcam and returns it.
+func (wc *WebCamSingleton) CaptureImage() (*gocv.Mat, error) {
+ if !wc.IsOpen() {
+ return nil, fmt.Errorf("webcam is not open")
+ }
+
+ img := gocv.NewMat()
+ if ok := wc.Capture.Read(&img); !ok {
+ return nil, fmt.Errorf("failed to read image from webcam")
+ }
+ return &img, nil
+}
+
func GetCamera() (*WebCamSingleton, error) {
if instance != nil {
return instance, nil
@@ -54,7 +67,7 @@ func (wc *WebCamSingleton) Open() error {
return fmt.Errorf("error opening video capture device: %v", err)
}
- wc.capture = capture
+ wc.Capture = capture
wc.isOpen = true
return nil
}
@@ -66,19 +79,6 @@ func (wc *WebCamSingleton) Stop() {
return
}
- wc.capture.Close()
+ wc.Capture.Close()
wc.isOpen = false
}
-
-// CaptureImage captures an image from the webcam and returns it.
-func (wc *WebCamSingleton) CaptureImage() (*gocv.Mat, error) {
- if !wc.isOpen {
- return nil, fmt.Errorf("webcam is not open")
- }
-
- img := gocv.NewMat()
- if ok := wc.capture.Read(&img); !ok {
- return nil, fmt.Errorf("failed to read image from webcam")
- }
- return &img, nil
-}
diff --git a/main.go b/main.go
deleted file mode 100644
index a41c339..0000000
--- a/main.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package main
-
-import (
- "github.com/bugbundle/phantom/cmd"
-)
-
-func main() {
- cmd.Execute()
-}
diff --git a/templates/index.html.tpl b/templates/index.html.tpl
new file mode 100644
index 0000000..c50d8b8
--- /dev/null
+++ b/templates/index.html.tpl
@@ -0,0 +1,30 @@
+
+
+
+
+
+ Phantom dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+