diff --git a/README.md b/README.md index ace35b4..1b40d32 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,14 @@ or ### Requirements -* [OpenCV](http://opencv.org/) (default is version 2.x via [go-opencv](https://github.com/lazywei/go-opencv), use `-tags cv3` for [gocv](https://github.com/hybridgroup/gocv)) -* [libjpeg-turbo](https://www.libjpeg-turbo.org/) (use `-tags jpeg` for native image/jpeg, but note that CPU usage will be much higher) +* [OpenCV](http://opencv.org/) +* [libjpeg-turbo](https://www.libjpeg-turbo.org/) +### Build tags + +* `cv3` - build with OpenCV 3.x [gocv](https://github.com/hybridgroup/gocv), default is version 2.x via [go-opencv](https://github.com/lazywei/go-opencv) +* `native` - build with native Go [V4L](https://github.com/korandiz/v4l) implementation on Linux/RPi instead of `OpenCV` +* `jpeg` - build with native Go `image/jpeg` instead of `libjpeg-turbo` ### Download diff --git a/cam2ip.go b/cam2ip.go index 3653c8a..8ec2a96 100644 --- a/cam2ip.go +++ b/cam2ip.go @@ -1,3 +1,5 @@ +// +build !native + package main import ( @@ -57,15 +59,12 @@ func main() { srv.Reader = vid } else { - cam, err := camera.New(camera.Options{srv.Index, srv.Rotate}) + cam, err := camera.New(camera.Options{srv.Index, srv.Rotate, srv.FrameWidth, srv.FrameHeight}) if err != nil { fmt.Fprintf(os.Stderr, "%s\n", err.Error()) os.Exit(1) } - cam.SetProperty(camera.PropFrameWidth, srv.FrameWidth) - cam.SetProperty(camera.PropFrameHeight, srv.FrameHeight) - srv.Reader = cam } diff --git a/cam2ip_native.go b/cam2ip_native.go new file mode 100644 index 0000000..33e8ee8 --- /dev/null +++ b/cam2ip_native.go @@ -0,0 +1,68 @@ +// +build native + +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/gen2brain/cam2ip/camera" + "github.com/gen2brain/cam2ip/server" +) + +const ( + name = "cam2ip" + version = "1.4" +) + +func main() { + srv := server.NewServer() + + flag.IntVar(&srv.Index, "index", 0, "Camera index") + flag.IntVar(&srv.Delay, "delay", 10, "Delay between frames, in milliseconds") + flag.Float64Var(&srv.FrameWidth, "width", 640, "Frame width") + flag.Float64Var(&srv.FrameHeight, "height", 480, "Frame height") + flag.IntVar(&srv.Rotate, "rotate", 0, "Rotate image, valid values are 90, 180, 270") + flag.BoolVar(&srv.NoWebGL, "nowebgl", false, "Disable WebGL drawing of images (html handler)") + flag.StringVar(&srv.Bind, "bind-addr", ":56000", "Bind address") + flag.StringVar(&srv.Htpasswd, "htpasswd-file", "", "Path to htpasswd file, if empty auth is disabled") + flag.Parse() + + srv.Name = name + srv.Version = version + + var err error + + if srv.Htpasswd != "" { + if _, err = os.Stat(srv.Htpasswd); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + os.Exit(1) + } + } + + if srv.FileName != "" { + if _, err = os.Stat(srv.FileName); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + os.Exit(1) + } + } + + cam, err := camera.New(camera.Options{srv.Index, srv.Rotate, srv.FrameWidth, srv.FrameHeight}) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + os.Exit(1) + } + + srv.Reader = cam + + defer srv.Reader.Close() + + fmt.Fprintf(os.Stderr, "Listening on %s\n", srv.Bind) + + err = srv.ListenAndServe() + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + os.Exit(1) + } +} diff --git a/camera/camera.go b/camera/camera.go index 532da20..808e5ef 100644 --- a/camera/camera.go +++ b/camera/camera.go @@ -1,4 +1,4 @@ -// +build !cv3 +// +build !cv3,!native // Package camera. package camera @@ -15,6 +15,8 @@ import ( type Options struct { Index int Rotate int + Width float64 + Height float64 } // Camera represents camera. @@ -34,6 +36,9 @@ func New(opts Options) (camera *Camera, err error) { err = fmt.Errorf("camera: can not open camera %d", opts.Index) } + camera.SetProperty(PropFrameWidth, opts.Width) + camera.SetProperty(PropFrameHeight, opts.Height) + return } @@ -43,7 +48,7 @@ func (c *Camera) Read() (img image.Image, err error) { c.frame = c.camera.RetrieveFrame(1) if c.frame == nil { - err = fmt.Errorf("camera: can not grab frame") + err = fmt.Errorf("camera: can not retrieve frame") return } diff --git a/camera/camera_const.go b/camera/camera_const.go index 036cd84..5d5a756 100644 --- a/camera/camera_const.go +++ b/camera/camera_const.go @@ -1,3 +1,5 @@ +// +build !native + package camera // Property identifiers. diff --git a/camera/camera_const_native.go b/camera/camera_const_native.go new file mode 100644 index 0000000..1eb1e83 --- /dev/null +++ b/camera/camera_const_native.go @@ -0,0 +1,21 @@ +// +build native + +package camera + +import ( + "github.com/korandiz/v4l" +) + +// Property identifiers. +const ( + PropBrightness = v4l.CtrlBrightness + PropContrast = v4l.CtrlContrast + PropSaturation = v4l.CtrlSaturation + PropHue = v4l.CtrlHue + PropGain = v4l.CtrlGain + PropExposure = v4l.CtrlExposure + PropWhiteBalanceU = v4l.CtrlWhiteBalance + PropSharpness = v4l.CtrlSharpness + PropWhiteBalanceV = v4l.CtrlDoWhiteBalance + PropBacklight = v4l.CtrlBacklightCompensation +) diff --git a/camera/camera_cv3.go b/camera/camera_cv3.go index 238ab1f..546cca1 100644 --- a/camera/camera_cv3.go +++ b/camera/camera_cv3.go @@ -1,4 +1,4 @@ -// +build cv3 +// +build cv3,!native // Package camera. package camera @@ -15,6 +15,8 @@ import ( type Options struct { Index int Rotate int + Width float64 + Height float64 } // Camera represents camera. @@ -37,6 +39,9 @@ func New(opts Options) (camera *Camera, err error) { err = fmt.Errorf("camera: can not open camera %d: %s", opts.Index, err.Error()) } + camera.SetProperty(PropFrameWidth, opts.Width) + camera.SetProperty(PropFrameHeight, opts.Height) + return } @@ -55,7 +60,7 @@ func (c *Camera) Read() (img image.Image, err error) { } if c.frame == nil { - err = fmt.Errorf("camera: can not grab frame") + err = fmt.Errorf("camera: can not retrieve frame") return } diff --git a/camera/camera_native_linux.go b/camera/camera_native_linux.go new file mode 100644 index 0000000..d26aeb8 --- /dev/null +++ b/camera/camera_native_linux.go @@ -0,0 +1,132 @@ +// +build native + +// Package camera. +package camera + +import ( + "fmt" + "image" + + "github.com/disintegration/imaging" + "github.com/korandiz/v4l" + "github.com/korandiz/v4l/fmt/mjpeg" + + im "github.com/gen2brain/cam2ip/image" +) + +// Options. +type Options struct { + Index int + Rotate int + Width float64 + Height float64 +} + +// Camera represents camera. +type Camera struct { + opts Options + camera *v4l.Device +} + +// New returns new Camera for given camera index. +func New(opts Options) (camera *Camera, err error) { + camera = &Camera{} + camera.opts = opts + + devices := v4l.FindDevices() + if len(devices) < opts.Index+1 { + err = fmt.Errorf("camera: no camera at index %d", opts.Index) + return + } + + camera.camera, err = v4l.Open(devices[opts.Index].Path) + if err != nil { + err = fmt.Errorf("camera: %s", err.Error()) + return + } + + if camera.camera == nil { + err = fmt.Errorf("camera: can not open camera %d", opts.Index) + return + } + + config, err := camera.camera.GetConfig() + if err != nil { + err = fmt.Errorf("camera: %s", err.Error()) + return + } + + config.Format = mjpeg.FourCC + config.Width = int(opts.Width) + config.Height = int(opts.Height) + + err = camera.camera.SetConfig(config) + if err != nil { + err = fmt.Errorf("camera: %s", err.Error()) + return + } + + err = camera.camera.TurnOn() + if err != nil { + err = fmt.Errorf("camera: %s", err.Error()) + return + } + + return +} + +// Read reads next frame from camera and returns image. +func (c *Camera) Read() (img image.Image, err error) { + + buffer, err := c.camera.Capture() + if err != nil { + err = fmt.Errorf("camera: can not grab frame: %s", err.Error()) + return + } + + img, err = im.NewDecoder(buffer).Decode() + if err != nil { + err = fmt.Errorf("camera: %s", err.Error()) + return + } + + if c.opts.Rotate == 0 { + return + } + + switch c.opts.Rotate { + case 90: + img = imaging.Rotate90(img) + case 180: + img = imaging.Rotate180(img) + case 270: + img = imaging.Rotate270(img) + } + + return +} + +// GetProperty returns the specified camera property. +func (c *Camera) GetProperty(id int) float64 { + ret, _ := c.camera.GetControl(uint32(id)) + return float64(ret) +} + +// SetProperty sets a camera property. +func (c *Camera) SetProperty(id int, value float64) { + c.camera.SetControl(uint32(id), int32(value)) +} + +// Close closes camera. +func (c *Camera) Close() (err error) { + if c.camera == nil { + err = fmt.Errorf("camera: camera is not opened") + return + } + + c.camera.TurnOff() + + c.camera.Close() + c.camera = nil + return +} diff --git a/camera/camera_test.go b/camera/camera_test.go index b301d4a..62a4ea0 100644 --- a/camera/camera_test.go +++ b/camera/camera_test.go @@ -11,7 +11,7 @@ import ( ) func TestCamera(t *testing.T) { - camera, err := New(1) + camera, err := New(Options{0, 0, 640, 480}) if err != nil { t.Fatal(err) } @@ -25,18 +25,6 @@ func TestCamera(t *testing.T) { defer os.RemoveAll(tmpdir) - var width, height float64 = 640, 480 - camera.SetProperty(PropFrameWidth, width) - camera.SetProperty(PropFrameHeight, height) - - if camera.GetProperty(PropFrameWidth) != width { - t.Error("FrameWidth not correct") - } - - if camera.GetProperty(PropFrameHeight) != height { - t.Error("FrameHeight not correct") - } - var i int var n int = 10 diff --git a/handlers/jpeg.go b/handlers/jpeg.go index 713aa24..69bafd3 100644 --- a/handlers/jpeg.go +++ b/handlers/jpeg.go @@ -4,7 +4,7 @@ import ( "log" "net/http" - "github.com/gen2brain/cam2ip/encoder" + "github.com/gen2brain/cam2ip/image" "github.com/gen2brain/cam2ip/reader" ) @@ -35,7 +35,7 @@ func (j *JPEG) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - err = encoder.New(w).Encode(img) + err = image.NewEncoder(w).Encode(img) if err != nil { log.Printf("jpeg: encode: %v", err) return diff --git a/handlers/mjpeg.go b/handlers/mjpeg.go index 559d36e..6893c1d 100644 --- a/handlers/mjpeg.go +++ b/handlers/mjpeg.go @@ -9,7 +9,7 @@ import ( "net/textproto" "time" - "github.com/gen2brain/cam2ip/encoder" + "github.com/gen2brain/cam2ip/image" "github.com/gen2brain/cam2ip/reader" ) @@ -62,7 +62,7 @@ loop: continue } - err = encoder.New(partWriter).Encode(img) + err = image.NewEncoder(partWriter).Encode(img) if err != nil { log.Printf("mjpeg: encode: %v", err) continue diff --git a/handlers/socket.go b/handlers/socket.go index 15439bc..f7948de 100644 --- a/handlers/socket.go +++ b/handlers/socket.go @@ -10,7 +10,7 @@ import ( "golang.org/x/net/websocket" - "github.com/gen2brain/cam2ip/encoder" + "github.com/gen2brain/cam2ip/image" "github.com/gen2brain/cam2ip/reader" ) @@ -37,7 +37,7 @@ func (s *Socket) write(ws *websocket.Conn) { w := new(bytes.Buffer) - err = encoder.New(w).Encode(img) + err = image.NewEncoder(w).Encode(img) if err != nil { log.Printf("socket: encode: %v", err) continue diff --git a/handlers/socket_amd64.go b/handlers/socket_amd64.go index 7568c18..b1e4376 100644 --- a/handlers/socket_amd64.go +++ b/handlers/socket_amd64.go @@ -8,7 +8,7 @@ import ( "golang.org/x/net/websocket" "goost.org/encoding/base64" - "github.com/gen2brain/cam2ip/encoder" + "github.com/gen2brain/cam2ip/image" "github.com/gen2brain/cam2ip/reader" ) @@ -35,7 +35,7 @@ func (s *Socket) write(ws *websocket.Conn) { w := new(bytes.Buffer) - err = encoder.New(w).Encode(img) + err = image.NewEncoder(w).Encode(img) if err != nil { log.Printf("socket: encode: %v", err) continue diff --git a/image/decode.go b/image/decode.go new file mode 100644 index 0000000..d68d475 --- /dev/null +++ b/image/decode.go @@ -0,0 +1,26 @@ +// +build !jpeg + +// Package image. +package image + +import ( + "image" + "io" + + jpeg "github.com/antonini/golibjpegturbo" +) + +// NewDecoder returns a new Decoder. +func NewDecoder(r io.Reader) *Decoder { + return &Decoder{r} +} + +// Decoder struct. +type Decoder struct { + r io.Reader +} + +// Decode decodes image from JPEG. +func (d Decoder) Decode() (image.Image, error) { + return jpeg.Decode(d.r) +} diff --git a/image/decode_jpeg.go b/image/decode_jpeg.go new file mode 100644 index 0000000..95320d3 --- /dev/null +++ b/image/decode_jpeg.go @@ -0,0 +1,25 @@ +// +build jpeg + +// Package image. +package image + +import ( + "image" + "image/jpeg" + "io" +) + +// NewDecoder returns a new Decoder. +func NewDecoder(r io.Reader) *Decoder { + return &Decoder{r} +} + +// Decoder struct. +type Decoder struct { + r io.Reader +} + +// Decode decodes image from JPEG. +func (d Decoder) Decode() (image.Image, error) { + return jpeg.Decode(d.r) +} diff --git a/encoder/encode.go b/image/encode.go similarity index 76% rename from encoder/encode.go rename to image/encode.go index bd22186..b00b98e 100644 --- a/encoder/encode.go +++ b/image/encode.go @@ -1,7 +1,7 @@ // +build !jpeg -// Package encoder. -package encoder +// Package image. +package image import ( "image" @@ -10,8 +10,8 @@ import ( jpeg "github.com/antonini/golibjpegturbo" ) -// New returns a new Encoder. -func New(w io.Writer) *Encoder { +// NewEncoder returns a new Encoder. +func NewEncoder(w io.Writer) *Encoder { return &Encoder{w} } diff --git a/encoder/encode_jpeg.go b/image/encode_jpeg.go similarity index 74% rename from encoder/encode_jpeg.go rename to image/encode_jpeg.go index c7487e6..52e597a 100644 --- a/encoder/encode_jpeg.go +++ b/image/encode_jpeg.go @@ -1,7 +1,7 @@ // +build jpeg -// Package encoder. -package encoder +// Package image. +package image import ( "image" @@ -9,8 +9,8 @@ import ( "io" ) -// New returns a new Encoder. -func New(w io.Writer) *Encoder { +// NewEncoder returns a new Encoder. +func NewEncoder(w io.Writer) *Encoder { return &Encoder{w} } diff --git a/make.bash b/make.bash index ab9e0bf..fa11a7c 100755 --- a/make.bash +++ b/make.bash @@ -13,7 +13,14 @@ PKG_CONFIG_PATH="$CHROOT/usr/lib/pkgconfig" \ PKG_CONFIG_LIBDIR="$CHROOT/usr/lib/pkgconfig" \ CGO_LDFLAGS="-L$CHROOT/usr/lib -L$CHROOT/lib" \ CGO_CFLAGS="-I$CHROOT/usr/include" \ -CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -v -x -o build/cam2ip.linux.amd64 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip +CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o build/cam2ip.linux.amd64 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip + +LIBRARY_PATH="$CHROOT/usr/lib:$CHROOT/lib" \ +PKG_CONFIG_PATH="$CHROOT/usr/lib/pkgconfig" \ +PKG_CONFIG_LIBDIR="$CHROOT/usr/lib/pkgconfig" \ +CGO_LDFLAGS="-L$CHROOT/usr/lib -L$CHROOT/lib" \ +CGO_CFLAGS="-I$CHROOT/usr/include" \ +CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -tags native -o build/cam2ip.linux.native.amd64 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip PKG_CONFIG="/usr/bin/i686-w64-mingw32-pkg-config" \ PKG_CONFIG_PATH="$MINGW/usr/lib/pkgconfig" \ @@ -21,7 +28,7 @@ PKG_CONFIG_LIBDIR="$MINGW/usr/lib/pkgconfig" \ CGO_LDFLAGS="-L$MINGW/usr/lib" \ CGO_CFLAGS="-I$MINGW/usr/include" \ CC="i686-w64-mingw32-gcc" CXX="i686-w64-mingw32-g++" \ -CGO_ENABLED=1 GOOS=windows GOARCH=386 go build -v -x -o build/cam2ip.exe -ldflags "-linkmode external -s -w '-extldflags=-static'" github.com/gen2brain/cam2ip +CGO_ENABLED=1 GOOS=windows GOARCH=386 go build -o build/cam2ip.exe -ldflags "-linkmode external -s -w '-extldflags=-static'" github.com/gen2brain/cam2ip PKG_CONFIG="/usr/bin/x86_64-w64-mingw32-pkg-config" \ PKG_CONFIG_PATH="$MINGW64/usr/lib/pkgconfig" \ @@ -29,7 +36,7 @@ PKG_CONFIG_LIBDIR="$MINGW64/usr/lib/pkgconfig" \ CGO_LDFLAGS="-L$MINGW64/usr/lib" \ CGO_CFLAGS="-I$MINGW64/usr/include" \ CC="x86_64-w64-mingw32-gcc" CXX="x86_64-w64-mingw32-g++" \ -CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -v -x -o build/cam2ip.exe.amd64 -ldflags "-linkmode external -s -w '-extldflags=-static'" github.com/gen2brain/cam2ip +CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -o build/cam2ip.exe.amd64 -ldflags "-linkmode external -s -w '-extldflags=-static'" github.com/gen2brain/cam2ip PKG_CONFIG="/usr/bin/armv6j-hardfloat-linux-gnueabi-pkg-config" \ PKG_CONFIG_PATH="$RPI/usr/lib/pkgconfig" \ @@ -37,7 +44,15 @@ PKG_CONFIG_LIBDIR="$RPI/usr/lib/pkgconfig" \ CGO_LDFLAGS="-L$RPI/usr/lib" \ CGO_CFLAGS="-I$RPI/usr/include" \ CC="armv6j-hardfloat-linux-gnueabi-gcc" CXX="armv6j-hardfloat-linux-gnueabi-g++" \ -CGO_ENABLED=1 GOOS=linux GOARCH=arm go build -v -x -o build/cam2ip.linux.arm -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip +CGO_ENABLED=1 GOOS=linux GOARCH=arm go build -o build/cam2ip.linux.arm -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip + +PKG_CONFIG="/usr/bin/armv6j-hardfloat-linux-gnueabi-pkg-config" \ +PKG_CONFIG_PATH="$RPI/usr/lib/pkgconfig" \ +PKG_CONFIG_LIBDIR="$RPI/usr/lib/pkgconfig" \ +CGO_LDFLAGS="-L$RPI/usr/lib" \ +CGO_CFLAGS="-I$RPI/usr/include" \ +CC="armv6j-hardfloat-linux-gnueabi-gcc" CXX="armv6j-hardfloat-linux-gnueabi-g++" \ +CGO_ENABLED=1 GOOS=linux GOARCH=arm go build -tags native -o build/cam2ip.linux.native.arm -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip PKG_CONFIG="/usr/bin/armv7a-hardfloat-linux-gnueabi-pkg-config" \ PKG_CONFIG_PATH="$RPI3/usr/lib/pkgconfig" \ @@ -45,4 +60,12 @@ PKG_CONFIG_LIBDIR="$RPI3/usr/lib/pkgconfig" \ CGO_LDFLAGS="-L$RPI3/usr/lib" \ CGO_CFLAGS="-I$RPI3/usr/include" \ CC="armv7a-hardfloat-linux-gnueabi-gcc" CXX="armv7a-hardfloat-linux-gnueabi-g++" \ -CGO_ENABLED=1 GOOS=linux GOARCH=arm go build -v -x -o build/cam2ip.linux.arm7 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip +CGO_ENABLED=1 GOOS=linux GOARCH=arm go build -o build/cam2ip.linux.arm7 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip + +PKG_CONFIG="/usr/bin/armv7a-hardfloat-linux-gnueabi-pkg-config" \ +PKG_CONFIG_PATH="$RPI3/usr/lib/pkgconfig" \ +PKG_CONFIG_LIBDIR="$RPI3/usr/lib/pkgconfig" \ +CGO_LDFLAGS="-L$RPI3/usr/lib" \ +CGO_CFLAGS="-I$RPI3/usr/include" \ +CC="armv7a-hardfloat-linux-gnueabi-gcc" CXX="armv7a-hardfloat-linux-gnueabi-g++" \ +CGO_ENABLED=1 GOOS=linux GOARCH=arm go build -tags native -o build/cam2ip.linux.native.arm7 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip diff --git a/video/video.go b/video/video.go index 5ee31ce..55338f3 100644 --- a/video/video.go +++ b/video/video.go @@ -1,4 +1,4 @@ -// +build !cv3 +// +build !cv3,!native // Package video. package video @@ -42,7 +42,7 @@ func (v *Video) Read() (img image.Image, err error) { if v.video.GrabFrame() { v.frame = v.video.RetrieveFrame(1) if v.frame == nil { - err = fmt.Errorf("video: can not grab frame") + err = fmt.Errorf("video: can not retrieve frame") return } diff --git a/video/video_cv3.go b/video/video_cv3.go index 30e2027..ae1bb26 100644 --- a/video/video_cv3.go +++ b/video/video_cv3.go @@ -1,4 +1,4 @@ -// +build cv3 +// +build cv3,!native // Package video. package video @@ -49,7 +49,7 @@ func (v *Video) Read() (img image.Image, err error) { } if v.frame == nil { - err = fmt.Errorf("video: can not grab frame") + err = fmt.Errorf("video: can not retrieve frame") return } diff --git a/video/video_test.go b/video/video_test.go index cde1467..503b635 100644 --- a/video/video_test.go +++ b/video/video_test.go @@ -11,7 +11,7 @@ import ( ) func TestVideo(t *testing.T) { - video, err := New("test.mp4") + video, err := New(video.Options{"test.mp4", 0}) if err != nil { t.Fatal(err) }