diff --git a/README.md b/README.md index 79338f9..9fe8e8a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ # go-bsb-cams -Simple program to take and output the Bigscreen Beyond 2e cameras to a webserver to be used with eyetracking software +Simple program to take and output the Bigscreen Beyond 2e cameras to a webserver to be used with eyetracking software, the stream.go is taken from https://github.com/garymcbay/mjpeg but has the content length removed as our stream is live. + +## Usage +Clone This repo and get the dependencies with: `go get .` + +To run, execute the following command within the root directory: `go run .` or build the package and run the resulting executable diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b0d31f9 --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module github.com/LilliaElaine/go-bsb-cams + +go 1.24.5 + +require ( + github.com/google/gousb v1.1.3 + github.com/kevmo314/go-uvc v0.0.0-20250915020343-0eb292711e9f + github.com/labstack/echo v3.3.10+incompatible +) + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.45.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a6aea00 --- /dev/null +++ b/go.sum @@ -0,0 +1,29 @@ +github.com/google/gousb v1.1.3 h1:xt6M5TDsGSZ+rlomz5Si5Hmd/Fvbmo2YCJHN+yGaK4o= +github.com/google/gousb v1.1.3/go.mod h1:GGWUkK0gAXDzxhwrzetW592aOmkkqSGcj5KLEgmCVUg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kevmo314/go-uvc v0.0.0-20250915020343-0eb292711e9f h1:4wmr7EOCjLjhSsdmMZz0gI+yko0Xs3liB6Dv6sroETM= +github.com/kevmo314/go-uvc v0.0.0-20250915020343-0eb292711e9f/go.mod h1:tTMA/Kw0QEfReK+VotFviO3Gntk3ito14rh9m0W7Flg= +github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= +github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..0601502 --- /dev/null +++ b/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "bytes" + "fmt" + "image/jpeg" + "log" + "net/http" + "syscall" + + // "github.com/garymcbay/mjpeg" + "github.com/google/gousb" + "github.com/kevmo314/go-uvc" + "github.com/kevmo314/go-uvc/pkg/descriptors" +) + +func getdevice() (device string) { + ctx := gousb.NewContext() + defer ctx.Close() + dev, err := ctx.OpenDeviceWithVIDPID(0x35bd, 0x0202) + if err != nil { + log.Fatalf("Could not open a device: %v", err) + } + defer dev.Close() + return fmt.Sprintf("/dev/bus/usb/%03v/%03v", dev.Desc.Bus, dev.Desc.Address) +} + +func main() { + stream := NewStream() + device := getdevice() + // Pass your jpegBuffer frames using stream.UpdateJPEG() + go imagestreamer(stream, device) + mux := http.NewServeMux() + mux.Handle("/stream", stream) + log.Fatal(http.ListenAndServe(":8080", mux)) + +} + +func imagestreamer(stream *Stream, device string) { + fd, err := syscall.Open(device, syscall.O_RDWR, 0) + if err != nil { + panic(err) + } + ctx, err := uvc.NewUVCDevice(uintptr(fd)) + if err != nil { + panic(err) + } + + info, err := ctx.DeviceInfo() + if err != nil { + panic(err) + } + for _, iface := range info.StreamingInterfaces { + + for i, desc := range iface.Descriptors { + fd, ok := desc.(*descriptors.MJPEGFormatDescriptor) + if !ok { + continue + } + frd := iface.Descriptors[i+1].(*descriptors.MJPEGFrameDescriptor) + + resp, err := iface.ClaimFrameReader(fd.Index(), frd.Index()) + if err != nil { + panic(err) + } + for i := 0; ; i++ { + fr, err := resp.ReadFrame() + if err != nil { + panic(err) + } + img, err := jpeg.Decode(fr) + if err != nil { + continue + } + jpegbuf := new(bytes.Buffer) + + if err = jpeg.Encode(jpegbuf, img, nil); err != nil { + log.Printf("failed to encode: %v", err) + } + // boundry := ("--frame-boundary\r\nContent-Type: image/jpeg\r\nContent-Length: " + strconv.Itoa(len(jpegbuf.Bytes())) + "\r\n\r\n") + // stream.UpdateJPEG(append([]byte(boundry), jpegbuf.Bytes()...)) + stream.UpdateJPEG(jpegbuf.Bytes()) + } + } + } +} diff --git a/stream.go b/stream.go new file mode 100644 index 0000000..3d65a6f --- /dev/null +++ b/stream.go @@ -0,0 +1,118 @@ +// Package mjpeg implements a simple MJPEG streamer. +// +// Stream objects implement the http.Handler interface, allowing to use them with the net/http package like so: +// +// stream = mjpeg.NewStream() +// http.Handle("/camera", stream) +// +// Then push new JPEG frames to the connected clients using stream.UpdateJPEG(). +package main + +import ( + "fmt" + "log" + "net/http" + "sync" + "time" + + "github.com/labstack/echo" +) + +// Stream represents a single video feed. +type Stream struct { + m map[chan []byte]bool + frame []byte + lock sync.Mutex + FrameInterval time.Duration +} + +const boundaryWord = "MJPEGBOUNDARY" +const headerf = "\r\n" + + "--" + boundaryWord + "\r\n" + + "Content-Type: image/jpeg\r\n" + + // "Content-Length: %d\r\n" + + "X-Timestamp: 0.000000\r\n" + + "\r\n" + +// ServeHTTP responds to HTTP requests with the MJPEG stream, implementing the http.Handler interface. +func (s *Stream) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Println("Stream:", r.RemoteAddr, "connected") + w.Header().Add("Content-Type", "multipart/x-mixed-replace") + + c := make(chan []byte) + s.lock.Lock() + s.m[c] = true + s.lock.Unlock() + + for { + time.Sleep(s.FrameInterval) + b := <-c + _, err := w.Write(b) + if err != nil { + break + } + } + + s.lock.Lock() + delete(s.m, c) + s.lock.Unlock() + log.Println("Stream:", r.RemoteAddr, "disconnected") +} + +// StreamToEcho implements Echo headers to respond to Echo HTTP requests with an MJPEG stream. +func (s *Stream) StreamToEcho(c echo.Context) error { + log.Println("Stream:", c.Request().RemoteAddr, "connected") + c.Response().Header().Set("Content-Type", "multipart/x-mixed-replace;boundary="+boundaryWord) + + ch := make(chan []byte) + s.lock.Lock() + s.m[ch] = true + s.lock.Unlock() + + for { + time.Sleep(s.FrameInterval) + b := <-ch + _, err := c.Response().Write(b) + if err != nil { + break + } + } + + s.lock.Lock() + delete(s.m, ch) + s.lock.Unlock() + log.Println("Stream:", c.Request().RemoteAddr, "disconnected") + return nil +} + +// UpdateJPEG pushes a new JPEG frame onto the clients. +func (s *Stream) UpdateJPEG(jpeg []byte) { + header := fmt.Sprintf(headerf, len(jpeg)) + if len(s.frame) < len(jpeg)+len(header) { + s.frame = make([]byte, (len(jpeg)+len(header))*2) + // s.frame = make([]byte, (len(jpeg) + len(header))) + } + + copy(s.frame, header) + copy(s.frame[len(header):], jpeg) + + s.lock.Lock() + for c := range s.m { + // Select to skip streams which are sleeping to drop frames. + // This might need more thought. + select { + case c <- s.frame: + default: + } + } + s.lock.Unlock() +} + +// NewStream initializes and returns a new Stream. +func NewStream() *Stream { + return &Stream{ + m: make(map[chan []byte]bool), + frame: make([]byte, len(headerf)), + FrameInterval: 50 * time.Millisecond, + } +}