Skip to content

Instantly share code, notes, and snippets.

@ugjka
Last active February 10, 2025 11:12
Show Gist options
  • Save ugjka/2c7fb0a359645b1f4ca0a06d79d47459 to your computer and use it in GitHub Desktop.
Save ugjka/2c7fb0a359645b1f4ca0a06d79d47459 to your computer and use it in GitHub Desktop.
package main
import (
"bytes"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"net/textproto"
"os"
"os/exec"
"os/signal"
"sync"
"syscall"
"time"
_ "time/tzdata"
"github.com/blackjack/webcam"
)
func main() {
logf, err := os.OpenFile(
"cam.log",
os.O_CREATE|os.O_RDWR|os.O_APPEND|os.O_SYNC,
0644,
)
if err != nil {
log.Fatal(err)
}
log.SetOutput(logf)
loc, err := time.LoadLocation("Europe/Riga")
if err != nil {
fmt.Fprintln(os.Stderr, err)
log.Fatal(err)
}
time.Local = loc
exes := []string{
"convert",
"pidof",
}
for _, exe := range exes {
_, err := exec.LookPath(exe)
if err != nil {
fmt.Fprintln(os.Stderr, "dependency:", err)
log.Println("dependency:", err)
log.Fatal("cam: exited")
}
}
out, err := exec.Command("pidof", os.Args[0]).CombinedOutput()
if err != nil {
fmt.Fprintln(os.Stderr, err)
log.Fatalf("pidof: %v, %s\n", err, out)
}
arr := bytes.Split(out, []byte(" "))
if len(arr) > 1 {
log.Fatal("cam: already running")
}
log.Println("cam: launched")
sig := make(chan os.Signal, 1)
signal.Notify(
sig, syscall.SIGINT, syscall.SIGTERM, //syscall.SIGHUP,
)
go func() {
s := <-sig
log.Println("cam:", s)
os.Exit(0)
}()
var c cam
go func() {
w, err := webcam.Open("/dev/video0")
if err != nil {
log.Fatal(err)
}
fmt.Println(w.GetSupportedFormats())
_, _, _, err = w.SetImageFormat(1196444237, 1280, 720)
if err != nil {
log.Fatal(err)
}
err = w.StartStreaming()
if err != nil {
log.Fatal(err)
}
ticker := time.NewTicker(time.Second * 5)
for {
err = w.WaitForFrame(1000)
if err != nil {
log.Fatal(err)
}
select {
case <-ticker.C:
buffer, id, err := w.GetFrame()
if err != nil {
log.Fatal(err)
}
now := time.Now()
cmd := exec.Command(
"magick",
"-",
"-quality", "90",
"-pointsize", "30",
"-font", "Ubuntu-Mono",
"-undercolor", "white",
"-annotate", "+10+30", now.Format(time.DateTime+" MST"),
"jpeg:-")
cmd.Stdin = bytes.NewBuffer(buffer)
stderr := bytes.NewBuffer(nil)
cmd.Stderr = stderr
out, err := cmd.Output()
if err != nil {
log.Fatal(err, stderr)
}
c.mu.Lock()
c.currentimg = out
c.timestamp = time.Now().UnixNano()
c.mu.Unlock()
w.ReleaseFrame(id)
default:
_, id, err := w.GetFrame()
if err != nil {
log.Fatal(err)
}
w.ReleaseFrame(id)
}
}
}()
mux := http.NewServeMux()
mux.Handle("/live", &c)
err = http.ListenAndServe(":9999", mux)
log.Println(err)
log.Fatal("cam: exited")
}
type cam struct {
currentimg []byte
timestamp int64
mu sync.RWMutex
}
func (c *cam) ServeHTTP(w http.ResponseWriter, r *http.Request) {
mw := multipart.NewWriter(w)
ct := fmt.Sprintf(
"multipart/x-mixed-replace;boundary=%s", mw.Boundary(),
)
w.Header().Add("Content-Type", ct)
w.Header().Set("Cache-Control", "no-cache")
if r.Method == http.MethodHead {
w.WriteHeader(http.StatusOK)
return
}
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
ip := r.Header.Get("X-Real-IP")
log.Printf("cam: accessd (%s)", ip)
now := time.Now()
defer func() {
log.Printf("cam: watched [%s] (%s)", time.Since(now), ip)
}()
var img []byte
var err error
var writer io.Writer
header := make(textproto.MIMEHeader)
header.Add("Content-Type", "image/jpeg")
for {
c.mu.RLock()
if c.currentimg != nil {
c.mu.RUnlock()
break
}
c.mu.RUnlock()
time.Sleep(time.Millisecond * 100)
}
var previous int64
for {
c.mu.RLock()
if previous == c.timestamp {
c.mu.RUnlock()
time.Sleep(time.Millisecond * 100)
continue
}
previous = c.timestamp
img = c.currentimg
c.mu.RUnlock()
for range 3 {
writer, err = mw.CreatePart(header)
if err != nil {
return
}
_, err = writer.Write(img)
if err != nil {
return
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment