Skip to content

Instantly share code, notes, and snippets.

@ugjka
Last active February 24, 2024 17:44
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save ugjka/fd41efb07dcecf2c3d402f4232786eb9 to your computer and use it in GitHub Desktop.
package main
import (
"bytes"
_ "embed"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"net/textproto"
"os"
"os/exec"
"os/signal"
"sync"
"syscall"
"time"
_ "time/tzdata"
)
//go:embed loading.jpg
var loading []byte
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{
"autossh",
"mogrify",
"termux-camera-photo",
"pkill",
"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")
cleanup := func() {
out, err := exec.Command("pkill", "autossh").CombinedOutput()
if err != nil {
log.Printf("autossh: %v, %s", err, out)
} else {
log.Println("autossh: killed")
}
os.Remove("cam.jpg")
}
sig := make(chan os.Signal, 1)
signal.Notify(
sig, syscall.SIGINT, syscall.SIGTERM, //syscall.SIGHUP,
)
go func() {
s := <-sig
cleanup()
log.Println("cam:", s)
os.Exit(0)
}()
autossh := exec.Command(
// ssh config = >
// Host webcam
// HostName ugjka.net
// Port 22222
// User webcam
// IdentityFile ~/.ssh/webcam
// ServerAliveInterval 10
// ServerAliveCountMax 3
// StrictHostKeyChecking no
// UserKnownHostsFile=/dev/null
//
// forward local server to VPS
// Nginx config on VPS =>
// location /live {
// proxy_pass http://127.0.0.1:9999/live;
// chunked_transfer_encoding off;
// proxy_buffering off;
// proxy_cache off;
// limit_conn perip 3;
// }
"autossh", "-f",
"-M", "20000",
"-nNT", "webcam",
"-R", "9999:localhost:8080",
)
log.Println("autossh: starting")
out, err = autossh.CombinedOutput()
if err != nil {
log.Fatalf("autossh: %v, %s", err, out)
}
var c cam
go func() {
var err error
var now time.Time
for {
if !c.ready() {
time.Sleep(time.Millisecond * 100)
continue
}
now = time.Now()
err = c.get()
if err != nil {
log.Println("termux:", err)
}
time.Sleep(time.Second*5 - time.Since(now))
}
}()
mux := http.NewServeMux()
mux.Handle("/live", &c)
err = http.ListenAndServe(":8080", mux)
log.Println(err)
cleanup()
log.Fatal("cam: exited")
}
type cam struct {
currentimg []byte
timestamp int64
clients int
mu sync.RWMutex
}
func (c *cam) ready() bool {
c.mu.Lock()
if c.clients == 0 {
c.currentimg = nil
c.mu.Unlock()
return false
}
c.mu.Unlock()
return true
}
func (c *cam) get() error {
f := "cam.jpg"
out, err := exec.Command("termux-camera-photo", f).CombinedOutput()
if err != nil {
return fmt.Errorf("%v, %s", err, out)
}
now := time.Now()
cmd := exec.Command(
"mogrify",
"-strip",
"-quality", "70",
"-resize", "900x900",
"-rotate", "-90",
"-pointsize", "20",
"-font", "Droid-Sans-Mono",
"-undercolor", "White",
"-annotate", "+10+30", now.Format(time.TimeOnly),
f,
)
out, err = cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%v, %s", err, out)
}
data, err := os.ReadFile(f)
if err != nil {
return err
}
c.mu.Lock()
c.currentimg = data
c.timestamp = time.Now().UnixNano()
c.mu.Unlock()
os.Remove(f)
return nil
}
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
}
c.mu.Lock()
c.clients++
c.mu.Unlock()
defer func() {
c.mu.Lock()
c.clients--
c.mu.Unlock()
}()
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")
c.mu.RLock()
if c.currentimg == nil {
c.mu.RUnlock()
for range 3 {
writer, err = mw.CreatePart(header)
if err != nil {
return
}
_, err = writer.Write(loading)
if err != nil {
return
}
}
} else {
c.mu.RUnlock()
}
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