Compare commits

18 Commits

Author SHA1 Message Date
Milan Nikolic
3fe0d88418 Update README.md 2025-06-15 08:48:39 +02:00
Milan Nikolic
6826193e2c Update timestamp 2025-06-15 08:47:13 +02:00
Milan Nikolic
d84884f26b Update server 2025-06-15 08:34:19 +02:00
Milan Nikolic
bf647116a3 Update Dockerfile 2025-06-15 08:32:50 +02:00
Milan Nikolic
8667fe4b48 Add support for YUYV/YUY2 format 2025-06-15 08:32:19 +02:00
Milan Nikolic
7e4e58029a Add some options 2025-06-14 01:45:32 +02:00
Milan Nikolic
fd5cb861cd Update flags 2025-06-14 00:06:04 +02:00
Milan Nikolic
26b04f44ad Remove properties 2025-06-13 21:35:27 +02:00
Milan Nikolic
f556285ad5 Move reader 2025-06-13 21:20:11 +02:00
Milan Nikolic
933b5eef22 Update handlers 2025-06-13 20:36:55 +02:00
Milan Nikolic
e5ee1a2049 Update url for websocket library 2025-06-13 20:10:12 +02:00
Milan Nikolic
7ed9c4c442 Add functions for rotate and timestamp 2025-06-13 19:51:23 +02:00
Milan Nikolic
711ad2f102 Add jpegli 2025-06-13 18:42:00 +02:00
Milan Nikolic
84135f3304 Add benchmark 2025-06-13 17:58:10 +02:00
Milan Nikolic
e01c80ca67 Update modules 2025-06-13 17:03:18 +02:00
Milan Nikolic
4a09c9b803 Change libjpeg library, rename tag 2025-06-13 17:01:56 +02:00
Milan Nikolic
948fe29079 Change image library 2025-06-12 21:58:20 +02:00
Milan Nikolic
074d14ad01 Drop support for cv2, rename tag 2025-06-12 21:19:52 +02:00
34 changed files with 802 additions and 690 deletions

View File

@@ -10,7 +10,7 @@ RUN go mod download
COPY . . COPY . .
RUN CGO_ENABLED=0 go build -tags jpeg -o cam2ip -ldflags "-s -w" github.com/gen2brain/cam2ip/cmd/cam2ip RUN CGO_ENABLED=0 go build -o cam2ip -trimpath -ldflags "-s -w" github.com/gen2brain/cam2ip/cmd/cam2ip
FROM scratch FROM scratch

View File

@@ -21,33 +21,19 @@ You can also use apps like `ffplay` or `vlc`:
### Build tags ### Build tags
* `cv2` - build with `OpenCV` 2.x ([go-opencv](https://github.com/lazywei/go-opencv)) * `opencv` - use `OpenCV` library to access camera ([gocv](https://github.com/hybridgroup/gocv))
* `cv4` - build with `OpenCV` 4.x ([gocv](https://github.com/hybridgroup/gocv)) * `libjpeg` - build with `libjpeg` ([go-libjpeg](https://github.com/pixiv/go-libjpeg)) instead of native `image/jpeg`
* `turbo` - build with `libjpeg-turbo` ([libjpeg-turbo](https://www.libjpeg-turbo.org/)) instead of native Go `image/jpeg` * `jpegli` - build with `jpegli` ([jpegli](https://github.com/gen2brain/jpegli)) instead of native `image/jpeg`
### Download ### Download
Binaries are compiled with static OpenCV/libjpeg-turbo libraries, they should just work: Download the latest binaries from the [releases](https://github.com/gen2brain/cam2ip/releases).
- [Linux 64bit](https://github.com/gen2brain/cam2ip/releases/download/1.6/cam2ip-1.6-64bit.tar.gz)
- [Linux 64bit OpenCV](https://github.com/gen2brain/cam2ip/releases/download/1.6/cam2ip-1.6-64bit-cv2.tar.gz)
- [macOS 64bit OpenCV](https://github.com/gen2brain/cam2ip/releases/download/1.6/cam2ip-1.6-darwin-cv2.zip)
- [RPi 32bit](https://github.com/gen2brain/cam2ip/releases/download/1.6/cam2ip-1.6-RPi.tar.gz)
- [RPi 32bit OpenCV](https://github.com/gen2brain/cam2ip/releases/download/1.6/cam2ip-1.6-RPi-cv2.tar.gz)
- [RPi 32bit Static](https://github.com/gen2brain/cam2ip/releases/download/1.6/cam2ip-1.6-RPi-nocgo.tar.gz)
- [RPi3 32bit](https://github.com/gen2brain/cam2ip/releases/download/1.6/cam2ip-1.6-RPi3.tar.gz)
- [RPi3 32bit OpenCV](https://github.com/gen2brain/cam2ip/releases/download/1.6/cam2ip-1.6-RPi3-cv2.tar.gz)
- [Windows 32bit](https://github.com/gen2brain/cam2ip/releases/download/1.6/cam2ip-1.6-32bit.zip)
- [Windows 32bit OpenCV](https://github.com/gen2brain/cam2ip/releases/download/1.6/cam2ip-1.6-32bit-cv2.zip)
- [Windows 64bit](https://github.com/gen2brain/cam2ip/releases/download/1.6/cam2ip-1.6-64bit.zip)
- [Windows 64bit OpenCV](https://github.com/gen2brain/cam2ip/releases/download/1.6/cam2ip-1.6-64bit-cv2.zip)
### Installation ### Installation
go get -v github.com/gen2brain/cam2ip/cmd/cam2ip go install github.com/gen2brain/cam2ip/cmd/cam2ip@latest
This will install app in `$GOPATH/bin/cam2ip`. This command will install `cam2ip` in `GOBIN`, you can point `GOBIN` to e.g. `/usr/local/bin` or `~/.local/bin`.
### Run in Docker container ### Run in Docker container
@@ -56,25 +42,31 @@ This will install app in `$GOPATH/bin/cam2ip`.
### Usage ### Usage
``` ```
Usage of cam2ip: Usage: cam2ip [<flags>]
-bind-addr string --index
Bind address [CAM2IP_BIND_ADDR] (default ":56000") Camera index [CAM2IP_INDEX] (default "0")
-delay int --delay
Delay between frames, in milliseconds [CAM2IP_DELAY] (default 10) Delay between frames, in milliseconds [CAM2IP_DELAY] (default "10")
-height float --width
Frame height [CAM2IP_HEIGHT] (default 480) Frame width [CAM2IP_WIDTH] (default "640")
-htpasswd-file string --height
Path to htpasswd file, if empty auth is disabled [CAM2IP_HTPASSWD_FILE] Frame height [CAM2IP_HEIGHT] (default "480")
-index int --quality
Camera index [CAM2IP_INDEX] Image quality [CAM2IP_QUALITY] (default "75")
-nowebgl --rotate
Disable WebGL drawing of images (html handler) [CAM2IP_NOWEBGL] Rotate image, valid values are 90, 180, 270 [CAM2IP_ROTATE] (default "0")
-rotate int --flip
Rotate image, valid values are 90, 180, 270 [CAM2IP_ROTATE] Flip image, valid values are horizontal and vertical [CAM2IP_FLIP] (default "")
-timestamp --no-webgl
Draws timestamp on images [CAM2IP_TIMESTAMP] Disable WebGL drawing of image (html handler) [CAM2IP_NO_WEBGL] (default "false")
-width float --timestamp
Frame width [CAM2IP_WIDTH] (default 640) Draws timestamp on image [CAM2IP_TIMESTAMP] (default "false")
--time-format
Time format [CAM2IP_TIME_FORMAT] (default "2006-01-02 15:04:05")
--bind-addr
Bind address [CAM2IP_BIND_ADDR] (default ":56000")
--htpasswd-file
Path to htpasswd file, if empty auth is disabled [CAM2IP_HTPASSWD_FILE] (default "")
``` ```
### Handlers ### Handlers

View File

@@ -1,10 +1,99 @@
package camera package camera
// Options. import (
"bytes"
"fmt"
"image"
)
// Options .
type Options struct { type Options struct {
Index int Index int
Rotate int Rotate int
Width float64 Flip string
Height float64 Width float64
Timestamp bool Height float64
Timestamp bool
TimeFormat string
}
var (
yuy2FourCC = fourcc("YUY2")
yuyvFourCC = fourcc("YUYV")
mjpgFourCC = fourcc("MJPG")
)
func fourcc(b string) uint32 {
return uint32(b[0]) | (uint32(b[1]) << 8) | (uint32(b[2]) << 16) | (uint32(b[3]) << 24)
}
func bmp24ToRgba(data []byte, dst *image.RGBA) error {
r := bytes.NewReader(data)
width := dst.Bounds().Dx()
height := dst.Bounds().Dy()
// There are 3 bytes per pixel, and each row is 4-byte aligned.
b := make([]byte, (3*width+3)&^3)
// BMP images are stored bottom-up rather than top-down.
for y := height - 1; y >= 0; y-- {
_, err := r.Read(b)
if err != nil {
return err
}
p := dst.Pix[y*dst.Stride : y*dst.Stride+width*4]
for i, j := 0, 0; i < len(p); i, j = i+4, j+3 {
// BMP images are stored in BGR order rather than RGB order.
p[i+0] = b[j+2]
p[i+1] = b[j+1]
p[i+2] = b[j+0]
p[i+3] = 0xFF
}
}
return nil
}
// yuy2ToYCbCr422 converts a YUY2 (YUYV) byte slice to an image.YCbCr with YCbCrSubsampleRatio422 (I422).
func yuy2ToYCbCr422(data []byte, dst *image.YCbCr) error {
if dst.SubsampleRatio != image.YCbCrSubsampleRatio422 {
return fmt.Errorf("subsample ratio must be 422, got %s", dst.SubsampleRatio.String())
}
width := dst.Bounds().Dx()
height := dst.Bounds().Dy()
if width%2 != 0 {
return fmt.Errorf("width must be even for YUY2")
}
if len(data) != width*height*2 {
return fmt.Errorf("invalid data length for YUY2")
}
stride := width * 2 // 2 bytes per pixel
for y := 0; y < height; y++ {
for x := 0; x < width; x += 2 {
idx := y*stride + x*2
y0 := data[idx+0]
cb := data[idx+1]
y1 := data[idx+2]
cr := data[idx+3]
// Y plane: every pixel
dst.Y[y*dst.YStride+x+0] = y0
dst.Y[y*dst.YStride+x+1] = y1
// Cb/Cr plane: every 2 pixels (422)
off := y*dst.CStride + x/2
dst.Cb[off] = cb
dst.Cr[off] = cr
}
}
return nil
} }

View File

@@ -1,5 +1,4 @@
//go:build android //go:build android
// +build android
// Package camera. // Package camera.
package camera package camera
@@ -33,7 +32,7 @@ ACaptureSessionOutput *captureSessionOutput;
ACaptureSessionOutputContainer *captureSessionOutputContainer; ACaptureSessionOutputContainer *captureSessionOutputContainer;
void device_on_disconnected(void *context, ACameraDevice *device) { void device_on_disconnected(void *context, ACameraDevice *device) {
LOGI("camera %s is diconnected.\n", ACameraDevice_getId(device)); LOGI("camera %s is disconnected.\n", ACameraDevice_getId(device));
} }
void device_on_error(void *context, ACameraDevice *device, int error) { void device_on_error(void *context, ACameraDevice *device, int error) {
@@ -241,8 +240,9 @@ func New(opts Options) (camera *Camera, err error) {
camera.img = image.NewYCbCr(image.Rect(0, 0, int(opts.Width), int(opts.Height)), image.YCbCrSubsampleRatio420) camera.img = image.NewYCbCr(image.Rect(0, 0, int(opts.Width), int(opts.Height)), image.YCbCrSubsampleRatio420)
ret := C.openCamera(C.int(opts.Index), C.int(opts.Width), C.int(opts.Height)) ret := C.openCamera(C.int(opts.Index), C.int(opts.Width), C.int(opts.Height))
if bool(int(ret) != 0) { if int(ret) != 0 {
err = fmt.Errorf("camera: can not open camera %d: error %d", opts.Index, int(ret)) err = fmt.Errorf("camera: can not open camera %d: error %d", opts.Index, int(ret))
return return
} }
@@ -252,13 +252,15 @@ func New(opts Options) (camera *Camera, err error) {
// Read reads next frame from camera and returns image. // Read reads next frame from camera and returns image.
func (c *Camera) Read() (img image.Image, err error) { func (c *Camera) Read() (img image.Image, err error) {
ret := C.captureCamera() ret := C.captureCamera()
if bool(int(ret) != 0) { if int(ret) != 0 {
err = fmt.Errorf("camera: can not grab frame: error %d", int(ret)) err = fmt.Errorf("camera: can not grab frame: error %d", int(ret))
return return
} }
if C.image == nil { if C.image == nil {
err = fmt.Errorf("camera: can not retrieve frame") err = fmt.Errorf("camera: can not retrieve frame")
return return
} }
@@ -283,20 +285,12 @@ func (c *Camera) Read() (img image.Image, err error) {
return return
} }
// GetProperty returns the specified camera property.
func (c *Camera) GetProperty(id int) float64 {
return 0
}
// SetProperty sets a camera property.
func (c *Camera) SetProperty(id int, value float64) {
}
// Close closes camera. // Close closes camera.
func (c *Camera) Close() (err error) { func (c *Camera) Close() (err error) {
ret := C.closeCamera() ret := C.closeCamera()
if bool(int(ret) != 0) { if int(ret) != 0 {
err = fmt.Errorf("camera: can not close camera %d: error %d", c.opts.Index, int(ret)) err = fmt.Errorf("camera: can not close camera %d: error %d", c.opts.Index, int(ret))
return return
} }

View File

@@ -1,47 +0,0 @@
//go:build cv2 || cv4
// +build cv2 cv4
package camera
// Property identifiers.
const (
PropPosMsec = iota
PropPosFrames
PropPosAviRatio
PropFrameWidth
PropFrameHeight
PropFps
PropFourcc
PropFrameCount
PropFormat
PropMode
PropBrightness
PropContrast
PropSaturation
PropHue
PropGain
PropExposure
PropConvertRgb
PropWhiteBalanceU
PropRectification
PropMonocrome
PropSharpness
PropAutoExposure
PropGamma
PropTemperature
PropTrigger
PropTriggerDelay
PropWhiteBalanceV
PropZoom
PropFocus
PropGuid
PropIsoSpeed
PropMaxDc1394
PropBacklight
PropPan
PropTilt
PropRoll
PropIris
PropSettings
PropBuffersize
)

View File

@@ -1,22 +0,0 @@
//go:build !cv2 && !cv4 && !android
// +build !cv2,!cv4,!android
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
)

View File

@@ -1,101 +0,0 @@
//go:build cv2 && !cv4
// +build cv2,!cv4
// Package camera.
package camera
import (
"fmt"
"image"
"image/color"
"image/draw"
"time"
"github.com/disintegration/imaging"
"github.com/gen2brain/go-opencv/opencv"
"github.com/pbnjay/pixfont"
)
// Camera represents camera.
type Camera struct {
opts Options
camera *opencv.Capture
frame *opencv.IplImage
}
// New returns new Camera for given camera index.
func New(opts Options) (camera *Camera, err error) {
camera = &Camera{}
camera.opts = opts
camera.camera = opencv.NewCameraCapture(opts.Index)
if camera.camera == nil {
err = fmt.Errorf("camera: can not open camera %d", opts.Index)
}
camera.SetProperty(PropFrameWidth, opts.Width)
camera.SetProperty(PropFrameHeight, opts.Height)
return
}
// Read reads next frame from camera and returns image.
func (c *Camera) Read() (img image.Image, err error) {
if !c.camera.GrabFrame() {
err = fmt.Errorf("camera: can not grab frame")
return
}
c.frame = c.camera.RetrieveFrame(1)
if c.frame == nil {
err = fmt.Errorf("camera: can not retrieve frame")
return
}
img = c.frame.ToImage()
switch c.opts.Rotate {
case 90:
img = imaging.Rotate90(img)
case 180:
img = imaging.Rotate180(img)
case 270:
img = imaging.Rotate270(img)
}
if c.opts.Timestamp {
dimg, ok := img.(draw.Image)
if !ok {
err = fmt.Errorf("camera: %T is not a drawable image type", img)
return
}
pixfont.DrawString(dimg, 10, 10, time.Now().Format("2006-01-02 15:04:05"), color.White)
img = dimg
}
return
}
// GetProperty returns the specified camera property.
func (c *Camera) GetProperty(id int) float64 {
return c.camera.GetProperty(id)
}
// SetProperty sets a camera property.
func (c *Camera) SetProperty(id int, value float64) {
c.camera.SetProperty(id, value)
}
// Close closes camera.
func (c *Camera) Close() (err error) {
if c.camera == nil {
err = fmt.Errorf("camera: camera is not opened")
return
}
c.frame.Release()
c.camera.Release()
c.camera = nil
return
}

View File

@@ -1,108 +0,0 @@
//go:build cv4 && !cv2
// +build cv4,!cv2
// Package camera.
package camera
import (
"fmt"
"image"
"image/color"
"image/draw"
"time"
"github.com/disintegration/imaging"
"github.com/pbnjay/pixfont"
"gocv.io/x/gocv"
)
// Camera represents camera.
type Camera struct {
opts Options
camera *gocv.VideoCapture
frame *gocv.Mat
}
// New returns new Camera for given camera index.
func New(opts Options) (camera *Camera, err error) {
camera = &Camera{}
camera.opts = opts
mat := gocv.NewMat()
camera.frame = &mat
camera.camera, err = gocv.VideoCaptureDevice(opts.Index)
if err != nil {
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
}
// Read reads next frame from camera and returns image.
func (c *Camera) Read() (img image.Image, err error) {
ok := c.camera.Read(c.frame)
if !ok {
err = fmt.Errorf("camera: can not grab frame")
return
}
img, e := c.frame.ToImage()
if e != nil {
err = fmt.Errorf("camera: %v", e)
return
}
if c.frame == nil {
err = fmt.Errorf("camera: can not retrieve frame")
return
}
switch c.opts.Rotate {
case 90:
img = imaging.Rotate90(img)
case 180:
img = imaging.Rotate180(img)
case 270:
img = imaging.Rotate270(img)
}
if c.opts.Timestamp {
dimg, ok := img.(draw.Image)
if !ok {
err = fmt.Errorf("camera: %T is not a drawable image type", img)
return
}
pixfont.DrawString(dimg, 10, 10, time.Now().Format("2006-01-02 15:04:05"), color.White)
img = dimg
}
return
}
// GetProperty returns the specified camera property.
func (c *Camera) GetProperty(id int) float64 {
return c.camera.Get(gocv.VideoCaptureProperties(id))
}
// SetProperty sets a camera property.
func (c *Camera) SetProperty(id int, value float64) {
c.camera.Set(gocv.VideoCaptureProperties(id), value)
}
// Close closes camera.
func (c *Camera) Close() (err error) {
if c.camera == nil {
err = fmt.Errorf("camera: camera is not opened")
return
}
c.frame.Close()
err = c.camera.Close()
c.camera = nil
return
}

View File

@@ -1,5 +1,4 @@
//go:build !cv2 && !cv4 && !android //go:build !opencv && !android
// +build !cv2,!cv4,!android
// Package camera. // Package camera.
package camera package camera
@@ -7,14 +6,10 @@ package camera
import ( import (
"fmt" "fmt"
"image" "image"
"image/color" "io"
"image/draw" "slices"
"time"
"github.com/disintegration/imaging"
"github.com/korandiz/v4l" "github.com/korandiz/v4l"
"github.com/korandiz/v4l/fmt/mjpeg"
"github.com/pbnjay/pixfont"
im "github.com/gen2brain/cam2ip/image" im "github.com/gen2brain/cam2ip/image"
) )
@@ -23,49 +18,82 @@ import (
type Camera struct { type Camera struct {
opts Options opts Options
camera *v4l.Device camera *v4l.Device
config v4l.DeviceConfig
ycbcr *image.YCbCr
} }
// New returns new Camera for given camera index. // New returns new Camera for given camera index.
func New(opts Options) (camera *Camera, err error) { func New(opts Options) (c *Camera, err error) {
camera = &Camera{} c = &Camera{}
camera.opts = opts c.opts = opts
devices := v4l.FindDevices() devices := v4l.FindDevices()
if len(devices) < opts.Index+1 { if len(devices) < opts.Index+1 {
err = fmt.Errorf("camera: no camera at index %d", opts.Index) err = fmt.Errorf("camera: no camera at index %d", opts.Index)
return return
} }
camera.camera, err = v4l.Open(devices[opts.Index].Path) c.camera, err = v4l.Open(devices[opts.Index].Path)
if err != nil { if err != nil {
err = fmt.Errorf("camera: %s", err.Error()) err = fmt.Errorf("camera: %w", err)
return return
} }
if camera.camera == nil { if c.camera == nil {
err = fmt.Errorf("camera: can not open camera %d", opts.Index) err = fmt.Errorf("camera: can not open camera %d", opts.Index)
return return
} }
config, err := camera.camera.GetConfig() configs, e := c.camera.ListConfigs()
if err != nil { if e != nil {
err = fmt.Errorf("camera: %s", err.Error()) err = fmt.Errorf("camera: can not list configs: %w", e)
return return
} }
config.Format = mjpeg.FourCC formats := make([]uint32, 0)
config.Width = int(opts.Width) for _, config := range configs {
config.Height = int(opts.Height) formats = append(formats, config.Format)
}
err = camera.camera.SetConfig(config) c.config, err = c.camera.GetConfig()
if err != nil { if err != nil {
err = fmt.Errorf("camera: %s", err.Error()) err = fmt.Errorf("camera: can not get config: %w", err)
return return
} }
err = camera.camera.TurnOn() if slices.Contains(formats, mjpgFourCC) {
c.config.Format = mjpgFourCC
} else if slices.Contains(formats, yuyvFourCC) {
c.config.Format = yuyvFourCC
} else {
err = fmt.Errorf("camera: unsupported format %d", c.config.Format)
return
}
c.config.Width = int(opts.Width)
c.config.Height = int(opts.Height)
err = c.camera.SetConfig(c.config)
if err != nil { if err != nil {
err = fmt.Errorf("camera: %s", err.Error()) err = fmt.Errorf("camera: format %d: can not set config: %w", c.config.Format, err)
return
}
if c.config.Format == yuyvFourCC {
c.ycbcr = image.NewYCbCr(image.Rect(0, 0, int(c.opts.Width), int(c.opts.Height)), image.YCbCrSubsampleRatio422)
}
err = c.camera.TurnOn()
if err != nil {
err = fmt.Errorf("camera: format %d: can not turn on: %w", c.config.Format, err)
return return
} }
@@ -74,63 +102,65 @@ func New(opts Options) (camera *Camera, err error) {
// Read reads next frame from camera and returns image. // Read reads next frame from camera and returns image.
func (c *Camera) Read() (img image.Image, err error) { func (c *Camera) Read() (img image.Image, err error) {
buffer, err := c.camera.Capture() buffer, err := c.camera.Capture()
if err != nil { if err != nil {
err = fmt.Errorf("camera: can not grab frame: %s", err.Error()) err = fmt.Errorf("camera: format %d: can not grab frame: %w", c.config.Format, err)
return return
} }
img, err = im.NewDecoder(buffer).Decode() switch c.config.Format {
if err != nil { case yuy2FourCC, yuyvFourCC:
err = fmt.Errorf("camera: %s", err.Error()) data, e := io.ReadAll(buffer)
return if e != nil {
} err = fmt.Errorf("camera: format %d: can not read buffer: %w", c.config.Format, e)
switch c.opts.Rotate {
case 90:
img = imaging.Rotate90(img)
case 180:
img = imaging.Rotate180(img)
case 270:
img = imaging.Rotate270(img)
}
if c.opts.Timestamp {
dimg, ok := img.(draw.Image)
if !ok {
err = fmt.Errorf("camera: %T is not a drawable image type", img)
return return
} }
pixfont.DrawString(dimg, 10, 10, time.Now().Format("2006-01-02 15:04:05"), color.White) e = yuy2ToYCbCr422(data, c.ycbcr)
img = dimg if e != nil {
err = fmt.Errorf("camera: format %d: can not retrieve frame: %w", c.config.Format, e)
return
}
img = c.ycbcr
case mjpgFourCC:
img, err = im.NewDecoder(buffer).Decode()
if err != nil {
err = fmt.Errorf("camera: format %d: can not decode frame: %w", c.config.Format, err)
return
}
}
if c.opts.Rotate != 0 {
img = im.Rotate(img, c.opts.Rotate)
}
if c.opts.Flip != "" {
img = im.Flip(img, c.opts.Flip)
}
if c.opts.Timestamp {
img = im.Timestamp(img, c.opts.TimeFormat)
} }
return 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. // Close closes camera.
func (c *Camera) Close() (err error) { func (c *Camera) Close() (err error) {
if c.camera == nil { if c.camera == nil {
err = fmt.Errorf("camera: camera is not opened") err = fmt.Errorf("camera: close: camera is not opened")
return return
} }
c.camera.TurnOff() c.camera.TurnOff()
c.camera.Close() c.camera.Close()
c.camera = nil c.camera = nil
return return
} }

102
camera/camera_opencv.go Normal file
View File

@@ -0,0 +1,102 @@
//go:build opencv && !android
// Package camera.
package camera
import (
"fmt"
"image"
"gocv.io/x/gocv"
im "github.com/gen2brain/cam2ip/image"
)
const (
propFrameWidth = 3
propFrameHeight = 4
)
// Camera represents camera.
type Camera struct {
opts Options
camera *gocv.VideoCapture
frame *gocv.Mat
}
// New returns new Camera for given camera index.
func New(opts Options) (camera *Camera, err error) {
camera = &Camera{}
camera.opts = opts
mat := gocv.NewMat()
camera.frame = &mat
camera.camera, err = gocv.VideoCaptureDevice(opts.Index)
if err != nil {
err = fmt.Errorf("camera: can not open camera %d: %w", opts.Index, err)
}
camera.camera.Set(gocv.VideoCaptureProperties(propFrameWidth), opts.Width)
camera.camera.Set(gocv.VideoCaptureProperties(propFrameHeight), opts.Height)
return
}
// Read reads next frame from camera and returns image.
func (c *Camera) Read() (img image.Image, err error) {
ok := c.camera.Read(c.frame)
if !ok {
err = fmt.Errorf("camera: can not grab frame")
return
}
img, err = c.frame.ToImage()
if err != nil {
err = fmt.Errorf("camera: %w", err)
return
}
if c.frame == nil {
err = fmt.Errorf("camera: can not retrieve frame")
return
}
if c.opts.Rotate != 0 {
img = im.Rotate(img, c.opts.Rotate)
}
if c.opts.Flip != "" {
img = im.Flip(img, c.opts.Flip)
}
if c.opts.Timestamp {
img = im.Timestamp(img, c.opts.TimeFormat)
}
return
}
// Close closes camera.
func (c *Camera) Close() (err error) {
if c.camera == nil {
err = fmt.Errorf("camera: camera is not opened")
return
}
err = c.frame.Close()
if err != nil {
err = fmt.Errorf("camera: %w", err)
return
}
err = c.camera.Close()
c.camera = nil
return
}

View File

@@ -2,38 +2,35 @@ package camera
import ( import (
"fmt" "fmt"
"image/jpeg" "io"
"io/ioutil"
"os"
"path/filepath"
"testing" "testing"
"time" "time"
"github.com/gen2brain/cam2ip/image"
) )
func TestCamera(t *testing.T) { func TestCamera(t *testing.T) {
camera, err := New(Options{0, 0, 640, 480}) camera, err := New(Options{0, 0, "", 640, 480, false, ""})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer camera.Close() defer func(camera *Camera) {
err := camera.Close()
tmpdir, err := ioutil.TempDir(os.TempDir(), "cam2ip") if err != nil {
if err != nil { t.Error(err)
t.Error(err) }
} }(camera)
defer os.RemoveAll(tmpdir)
var i int var i int
var n int = 10 var n = 10
timeout := time.After(time.Duration(n) * time.Second) timeout := time.After(time.Duration(n) * time.Second)
for { for {
select { select {
case <-timeout: case <-timeout:
//fmt.Printf("Fps: %d\n", i/n) fmt.Printf("FPS: %.2f\n", float64(i)/float64(n))
return return
default: default:
i += 1 i += 1
@@ -43,17 +40,7 @@ func TestCamera(t *testing.T) {
t.Error(err) t.Error(err)
} }
file, err := os.Create(filepath.Join(tmpdir, fmt.Sprintf("%03d.jpg", i))) err = image.NewEncoder(io.Discard, 75).Encode(img)
if err != nil {
t.Error(err)
}
err = jpeg.Encode(file, img, &jpeg.Options{Quality: 75})
if err != nil {
t.Error(err)
}
err = file.Close()
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }

View File

@@ -1,5 +1,4 @@
//go:build !cv2 && !cv4 //go:build !opencv
// +build !cv2,!cv4
// Package camera. // Package camera.
package camera package camera
@@ -8,24 +7,27 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"image" "image"
"image/color" "runtime"
"image/draw"
"syscall" "syscall"
"time"
"unsafe" "unsafe"
"github.com/disintegration/imaging" im "github.com/gen2brain/cam2ip/image"
"github.com/pbnjay/pixfont"
) )
func init() {
runtime.LockOSThread()
}
// Camera represents camera. // Camera represents camera.
type Camera struct { type Camera struct {
opts Options opts Options
camera syscall.Handle camera syscall.Handle
frame *image.RGBA rgba *image.RGBA
ycbcr *image.YCbCr
hdr *videoHdr hdr *videoHdr
instance syscall.Handle instance syscall.Handle
className string className string
format uint32
} }
// New returns new Camera for given camera index. // New returns new Camera for given camera index.
@@ -34,35 +36,37 @@ func New(opts Options) (camera *Camera, err error) {
camera.opts = opts camera.opts = opts
camera.className = "capWindowClass" camera.className = "capWindowClass"
camera.instance, err = getModuleHandle()
if err != nil {
return
}
camera.frame = image.NewRGBA(image.Rect(0, 0, int(camera.opts.Width), int(camera.opts.Height)))
go func(c *Camera) { go func(c *Camera) {
fn := func(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr { fn := func(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
switch msg { switch msg {
case wmClose: case wmClose:
destroyWindow(hwnd) _ = destroyWindow(hwnd)
case wmDestroy: case wmDestroy:
postQuitMessage(0) postQuitMessage(0)
default: default:
ret := defWindowProc(hwnd, msg, wparam, lparam) ret := defWindowProc(hwnd, msg, wparam, lparam)
return ret return ret
} }
return 0 return 0
} }
c.instance, err = getModuleHandle()
if err != nil {
return
}
err = registerClass(c.className, c.instance, fn) err = registerClass(c.className, c.instance, fn)
if err != nil { if err != nil {
return return
} }
hwnd, err := createWindow(0, c.className, "", wsOverlappedWindow, cwUseDefault, cwUseDefault, int64(c.opts.Width)+100, int64(c.opts.Height)+100, 0, 0, c.instance) hwnd, e := createWindow(0, c.className, "", wsOverlappedWindow, cwUseDefault, cwUseDefault,
if err != nil { int64(c.opts.Width)+100, int64(c.opts.Height)+100, 0, 0, c.instance)
if e != nil {
err = e
return return
} }
@@ -72,11 +76,15 @@ func New(opts Options) (camera *Camera, err error) {
} }
ret := sendMessage(c.camera, wmCapDriverConnect, uintptr(c.opts.Index), 0) ret := sendMessage(c.camera, wmCapDriverConnect, uintptr(c.opts.Index), 0)
if bool(int(ret) == 0) { if int(ret) == 0 {
err = fmt.Errorf("camera: can not open camera %d", c.opts.Index) err = fmt.Errorf("camera: can not open camera %d", c.opts.Index)
return return
} }
sendMessage(c.camera, wmCapSetPreview, 0, 0)
sendMessage(c.camera, wmCapSetOverlay, 0, 0)
var bi bitmapInfo var bi bitmapInfo
size := sendMessage(c.camera, wmCapGetVideoformat, 0, 0) size := sendMessage(c.camera, wmCapGetVideoformat, 0, 0)
sendMessage(c.camera, wmCapGetVideoformat, size, uintptr(unsafe.Pointer(&bi))) sendMessage(c.camera, wmCapGetVideoformat, size, uintptr(unsafe.Pointer(&bi)))
@@ -85,14 +93,44 @@ func New(opts Options) (camera *Camera, err error) {
bi.BmiHeader.BiHeight = int32(c.opts.Height) bi.BmiHeader.BiHeight = int32(c.opts.Height)
ret = sendMessage(c.camera, wmCapSetVideoformat, size, uintptr(unsafe.Pointer(&bi))) ret = sendMessage(c.camera, wmCapSetVideoformat, size, uintptr(unsafe.Pointer(&bi)))
if bool(int(ret) == 0) { if int(ret) == 0 {
err = fmt.Errorf("camera: can not set video format") err = fmt.Errorf("camera: can not set video format: %dx%d, %d", int(c.opts.Width), int(c.opts.Height), c.format)
return return
} }
c.format = bi.BmiHeader.BiCompression
sendMessage(c.camera, wmCapSetCallbackFrame, 0, syscall.NewCallback(c.callback)) sendMessage(c.camera, wmCapSetCallbackFrame, 0, syscall.NewCallback(c.callback))
messageLoop(c.camera) switch c.format {
case 0:
if bi.BmiHeader.BiBitCount != 24 {
err = fmt.Errorf("camera: unsupported format %d; bitcount: %d", c.format, bi.BmiHeader.BiBitCount)
return
}
c.rgba = image.NewRGBA(image.Rect(0, 0, int(c.opts.Width), int(c.opts.Height)))
case yuy2FourCC, yuyvFourCC:
c.ycbcr = image.NewYCbCr(image.Rect(0, 0, int(c.opts.Width), int(c.opts.Height)), image.YCbCrSubsampleRatio422)
case mjpgFourCC:
default:
err = fmt.Errorf("camera: unsupported format %d", c.format)
return
}
for {
var msg msgW
ok, _ := getMessage(&msg, 0, 0, 0)
if ok {
dispatchMessage(&msg)
} else {
break
}
}
return
}(camera) }(camera)
return return
@@ -100,86 +138,75 @@ func New(opts Options) (camera *Camera, err error) {
// Read reads next frame from camera and returns image. // Read reads next frame from camera and returns image.
func (c *Camera) Read() (img image.Image, err error) { func (c *Camera) Read() (img image.Image, err error) {
ret := sendMessage(c.camera, wmCapGrabFrameNoStop, 0, 0) ret := sendMessage(c.camera, wmCapGrabFrame, 0, 0)
if bool(int(ret) == 0) { if int(ret) == 0 {
err = fmt.Errorf("camera: can not grab frame") err = fmt.Errorf("camera: can not grab frame")
return return
} }
data := (*[1 << 24]uint8)(unsafe.Pointer(c.hdr.LpData))[0:c.hdr.DwBytesUsed] data := unsafe.Slice((*byte)(unsafe.Pointer(c.hdr.LpData)), c.hdr.DwBufferLength)
r := bytes.NewReader(data)
width := int(c.opts.Width) switch c.format {
height := int(c.opts.Height) case 0:
e := bmp24ToRgba(data, c.rgba)
if e != nil {
err = fmt.Errorf("camera: format %d: can not retrieve frame: %w", c.format, e)
// Taken from https://github.com/hotei/bmp/blob/master/bmpRGBA.go#L12
// There are 3 bytes per pixel, and each row is 4-byte aligned.
b := make([]byte, (3*width+3)&^3)
// BMP images are stored bottom-up rather than top-down.
for y := height - 1; y >= 0; y-- {
_, err = r.Read(b)
if err != nil {
err = fmt.Errorf("camera: can not retrieve frame: %v", err)
return return
} }
p := c.frame.Pix[y*c.frame.Stride : y*c.frame.Stride+width*4] img = c.rgba
for i, j := 0, 0; i < len(p); i, j = i+4, j+3 { case yuy2FourCC, yuyvFourCC:
// BMP images are stored in BGR order rather than RGB order. e := yuy2ToYCbCr422(data, c.ycbcr)
p[i+0] = b[j+2] if e != nil {
p[i+1] = b[j+1] err = fmt.Errorf("camera: format %d: can not retrieve frame: %w", c.format, e)
p[i+2] = b[j+0]
p[i+3] = 0xFF return
} }
img = c.ycbcr
case mjpgFourCC:
i, e := im.NewDecoder(bytes.NewReader(data)).Decode()
if e != nil {
err = fmt.Errorf("camera: format %d: can not retrieve frame: %w", c.format, e)
return
}
img = i
} }
img = c.frame if c.opts.Rotate != 0 {
img = im.Rotate(img, c.opts.Rotate)
}
switch c.opts.Rotate { if c.opts.Flip != "" {
case 90: img = im.Flip(img, c.opts.Flip)
img = imaging.Rotate90(img)
case 180:
img = imaging.Rotate180(img)
case 270:
img = imaging.Rotate270(img)
} }
if c.opts.Timestamp { if c.opts.Timestamp {
dimg, ok := img.(draw.Image) img = im.Timestamp(img, c.opts.TimeFormat)
if !ok {
err = fmt.Errorf("camera: %T is not a drawable image type", img)
return
}
pixfont.DrawString(dimg, 10, 10, time.Now().Format("2006-01-02 15:04:05"), color.White)
img = dimg
} }
return return
} }
// GetProperty returns the specified camera property.
func (c *Camera) GetProperty(id int) float64 {
return 0
}
// SetProperty sets a camera property.
func (c *Camera) SetProperty(id int, value float64) {
return
}
// Close closes camera. // Close closes camera.
func (c *Camera) Close() (err error) { func (c *Camera) Close() (err error) {
sendMessage(c.camera, wmCapSetCallbackFrame, 0, 0) sendMessage(c.camera, wmCapSetCallbackFrame, 0, 0)
unregisterClass(c.className, c.instance) unregisterClass(c.className, c.instance)
sendMessage(c.camera, wmCapDriverDisconnect, 0, 0) sendMessage(c.camera, wmCapDriverDisconnect, 0, 0)
destroyWindow(c.camera)
return return destroyWindow(c.camera)
} }
// callback function. // callback function.
func (c *Camera) callback(hwvd syscall.Handle, hdr *videoHdr) uintptr { func (c *Camera) callback(hwnd syscall.Handle, hdr *videoHdr) uintptr {
c.hdr = hdr if hdr != nil {
c.hdr = hdr
}
return 0 return 0
} }
@@ -188,16 +215,15 @@ var (
kernel32 = syscall.NewLazyDLL("kernel32.dll") kernel32 = syscall.NewLazyDLL("kernel32.dll")
avicap32 = syscall.NewLazyDLL("avicap32.dll") avicap32 = syscall.NewLazyDLL("avicap32.dll")
createWindowExW = user32.NewProc("CreateWindowExW") createWindowExW = user32.NewProc("CreateWindowExW")
destroyWindowW = user32.NewProc("DestroyWindow") destroyWindowW = user32.NewProc("DestroyWindow")
defWindowProcW = user32.NewProc("DefWindowProcW") defWindowProcW = user32.NewProc("DefWindowProcW")
dispatchMessageW = user32.NewProc("DispatchMessageW") dispatchMessageW = user32.NewProc("DispatchMessageW")
translateMessageW = user32.NewProc("TranslateMessage") getMessageW = user32.NewProc("GetMessageW")
getMessageW = user32.NewProc("GetMessageW") sendMessageW = user32.NewProc("SendMessageW")
sendMessageW = user32.NewProc("SendMessageW") postQuitMessageW = user32.NewProc("PostQuitMessage")
postQuitMessageW = user32.NewProc("PostQuitMessage") registerClassExW = user32.NewProc("RegisterClassExW")
registerClassExW = user32.NewProc("RegisterClassExW") unregisterClassW = user32.NewProc("UnregisterClassW")
unregisterClassW = user32.NewProc("UnregisterClassW")
getModuleHandleW = kernel32.NewProc("GetModuleHandleW") getModuleHandleW = kernel32.NewProc("GetModuleHandleW")
capCreateCaptureWindowW = avicap32.NewProc("capCreateCaptureWindowW") capCreateCaptureWindowW = avicap32.NewProc("capCreateCaptureWindowW")
@@ -214,6 +240,8 @@ const (
wmCapDriverDisconnect = wmCapStart + 11 wmCapDriverDisconnect = wmCapStart + 11
wmCapGetVideoformat = wmCapStart + 44 wmCapGetVideoformat = wmCapStart + 44
wmCapSetVideoformat = wmCapStart + 45 wmCapSetVideoformat = wmCapStart + 45
wmCapSetPreview = wmCapStart + 50
wmCapSetOverlay = wmCapStart + 51
wmCapGrabFrame = wmCapStart + 60 wmCapGrabFrame = wmCapStart + 60
wmCapGrabFrameNoStop = wmCapStart + 61 wmCapGrabFrameNoStop = wmCapStart + 61
wmCapStop = wmCapStart + 68 wmCapStop = wmCapStart + 68
@@ -332,8 +360,9 @@ func destroyWindow(hwnd syscall.Handle) error {
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-defwindowprocw // https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-defwindowprocw
func defWindowProc(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr { func defWindowProc(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
ret, _, _ := defWindowProcW.Call(uintptr(hwnd), uintptr(msg), uintptr(wparam), uintptr(lparam)) ret, _, _ := defWindowProcW.Call(uintptr(hwnd), uintptr(msg), wparam, lparam)
return uintptr(ret)
return ret
} }
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-dispatchmessagew // https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-dispatchmessagew
@@ -341,11 +370,6 @@ func dispatchMessage(msg *msgW) {
dispatchMessageW.Call(uintptr(unsafe.Pointer(msg))) dispatchMessageW.Call(uintptr(unsafe.Pointer(msg)))
} }
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-translatemessage
func translateMessage(msg *msgW) {
translateMessageW.Call(uintptr(unsafe.Pointer(msg)))
}
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getmessagew // https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getmessagew
func getMessage(msg *msgW, hwnd syscall.Handle, msgFilterMin, msgFilterMax uint32) (bool, error) { func getMessage(msg *msgW, hwnd syscall.Handle, msgFilterMin, msgFilterMax uint32) (bool, error) {
ret, _, err := getMessageW.Call(uintptr(unsafe.Pointer(msg)), uintptr(hwnd), uintptr(msgFilterMin), uintptr(msgFilterMax)) ret, _, err := getMessageW.Call(uintptr(unsafe.Pointer(msg)), uintptr(hwnd), uintptr(msgFilterMin), uintptr(msgFilterMax))
@@ -359,6 +383,7 @@ func getMessage(msg *msgW, hwnd syscall.Handle, msgFilterMin, msgFilterMax uint3
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-sendmessage // https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-sendmessage
func sendMessage(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr { func sendMessage(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
ret, _, _ := sendMessageW.Call(uintptr(hwnd), uintptr(msg), wparam, lparam, 0, 0) ret, _, _ := sendMessageW.Call(uintptr(hwnd), uintptr(msg), wparam, lparam, 0, 0)
return ret return ret
} }
@@ -386,6 +411,7 @@ func registerClass(className string, instance syscall.Handle, fn interface{}) er
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-unregisterclassw // https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-unregisterclassw
func unregisterClass(className string, instance syscall.Handle) bool { func unregisterClass(className string, instance syscall.Handle) bool {
ret, _, _ := unregisterClassW.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(className))), uintptr(instance)) ret, _, _ := unregisterClassW.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(className))), uintptr(instance))
return ret != 0 return ret != 0
} }
@@ -399,19 +425,3 @@ func capCreateCaptureWindow(lpszWindowName string, dwStyle, x, y, width, height
return syscall.Handle(ret), nil return syscall.Handle(ret), nil
} }
// messageLoop function
func messageLoop(hwnd syscall.Handle) {
for {
msg := &msgW{}
ok, _ := getMessage(msg, 0, 0, 0)
if ok {
translateMessage(msg)
dispatchMessage(msg)
} else {
break
}
}
return
}

View File

@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/jamiealquiza/envy" "go.senan.xyz/flagconf"
"github.com/gen2brain/cam2ip/camera" "github.com/gen2brain/cam2ip/camera"
"github.com/gen2brain/cam2ip/server" "github.com/gen2brain/cam2ip/server"
@@ -19,41 +19,56 @@ const (
func main() { func main() {
srv := server.NewServer() srv := server.NewServer()
flag.IntVar(&srv.Index, "index", 0, "Camera index") flag.IntVar(&srv.Index, "index", 0, "Camera index [CAM2IP_INDEX]")
flag.IntVar(&srv.Delay, "delay", 10, "Delay between frames, in milliseconds") flag.IntVar(&srv.Delay, "delay", 10, "Delay between frames, in milliseconds [CAM2IP_DELAY]")
flag.Float64Var(&srv.FrameWidth, "width", 640, "Frame width") flag.Float64Var(&srv.Width, "width", 640, "Frame width [CAM2IP_WIDTH]")
flag.Float64Var(&srv.FrameHeight, "height", 480, "Frame height") flag.Float64Var(&srv.Height, "height", 480, "Frame height [CAM2IP_HEIGHT]")
flag.IntVar(&srv.Rotate, "rotate", 0, "Rotate image, valid values are 90, 180, 270") flag.IntVar(&srv.Quality, "quality", 75, "Image quality [CAM2IP_QUALITY]")
flag.BoolVar(&srv.NoWebGL, "nowebgl", false, "Disable WebGL drawing of images (html handler)") flag.IntVar(&srv.Rotate, "rotate", 0, "Rotate image, valid values are 90, 180, 270 [CAM2IP_ROTATE]")
flag.BoolVar(&srv.Timestamp, "timestamp", false, "Draws timestamp on images") flag.StringVar(&srv.Flip, "flip", "", "Flip image, valid values are horizontal and vertical [CAM2IP_FLIP]")
flag.StringVar(&srv.Bind, "bind-addr", ":56000", "Bind address") flag.BoolVar(&srv.NoWebGL, "no-webgl", false, "Disable WebGL drawing of image (html handler) [CAM2IP_NO_WEBGL]")
flag.StringVar(&srv.Htpasswd, "htpasswd-file", "", "Path to htpasswd file, if empty auth is disabled") flag.BoolVar(&srv.Timestamp, "timestamp", false, "Draws timestamp on image [CAM2IP_TIMESTAMP]")
flag.StringVar(&srv.TimeFormat, "time-format", "2006-01-02 15:04:05", "Time format [CAM2IP_TIME_FORMAT]")
flag.StringVar(&srv.Bind, "bind-addr", ":56000", "Bind address [CAM2IP_BIND_ADDR]")
flag.StringVar(&srv.Htpasswd, "htpasswd-file", "", "Path to htpasswd file, if empty auth is disabled [CAM2IP_HTPASSWD_FILE]")
flag.Usage = func() {
stderr("Usage: %s [<flags>]\n", name)
order := []string{"index", "delay", "width", "height", "quality", "rotate", "flip", "no-webgl",
"timestamp", "time-format", "bind-addr", "htpasswd-file"}
for _, name := range order {
f := flag.Lookup(name)
if f != nil {
stderr(" --%s\n \t%v (default %q)\n", f.Name, f.Usage, f.DefValue)
}
}
}
envy.Parse("CAM2IP")
flag.Parse() flag.Parse()
_ = flagconf.ParseEnv()
srv.Name = name srv.Name = name
srv.Version = version srv.Version = version
var err error
if srv.Htpasswd != "" { if srv.Htpasswd != "" {
if _, err = os.Stat(srv.Htpasswd); err != nil { if _, err := os.Stat(srv.Htpasswd); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error()) stderr("%s\n", err.Error())
os.Exit(1) os.Exit(1)
} }
} }
if srv.FileName != "" { cam, err := camera.New(camera.Options{
if _, err = os.Stat(srv.FileName); err != nil { Index: srv.Index,
fmt.Fprintf(os.Stderr, "%s\n", err.Error()) Rotate: srv.Rotate,
os.Exit(1) Flip: srv.Flip,
} Width: srv.Width,
} Height: srv.Height,
Timestamp: srv.Timestamp,
cam, err := camera.New(camera.Options{srv.Index, srv.Rotate, srv.FrameWidth, srv.FrameHeight, srv.Timestamp}) TimeFormat: srv.TimeFormat,
})
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error()) stderr("%s\n", err.Error())
os.Exit(1) os.Exit(1)
} }
@@ -61,11 +76,15 @@ func main() {
defer srv.Reader.Close() defer srv.Reader.Close()
fmt.Fprintf(os.Stderr, "Listening on %s\n", srv.Bind) stderr("Listening on %s\n", srv.Bind)
err = srv.ListenAndServe() err = srv.ListenAndServe()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error()) stderr("%s\n", err.Error())
os.Exit(1) os.Exit(1)
} }
} }
func stderr(format string, a ...any) {
_, _ = fmt.Fprintf(os.Stderr, format, a...)
}

21
go.mod
View File

@@ -2,25 +2,24 @@ module github.com/gen2brain/cam2ip
require ( require (
github.com/abbot/go-http-auth v0.4.0 github.com/abbot/go-http-auth v0.4.0
github.com/antonini/golibjpegturbo v0.0.0-20141208033414-c03a2fa1e89a github.com/anthonynsimon/bild v0.14.0
github.com/disintegration/imaging v1.6.2 github.com/coder/websocket v1.8.13
github.com/gen2brain/base64 v0.0.0-20221015184129-317a5c93030c github.com/gen2brain/base64 v0.0.0-20221015184129-317a5c93030c
github.com/gen2brain/go-opencv v0.0.0-20191005190506-bf186fc94f7a github.com/gen2brain/jpegli v0.3.4
github.com/jamiealquiza/envy v1.1.0
github.com/korandiz/v4l v1.1.0 github.com/korandiz/v4l v1.1.0
github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567 github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567
github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d
go.senan.xyz/flagconf v0.1.9
gocv.io/x/gocv v0.35.0 gocv.io/x/gocv v0.35.0
nhooyr.io/websocket v1.8.10
) )
require ( require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/crypto v0.18.0 // indirect golang.org/x/crypto v0.18.0 // indirect
golang.org/x/image v0.15.0 // indirect
golang.org/x/net v0.20.0 // indirect golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect golang.org/x/sys v0.19.0 // indirect
) )
go 1.21 go 1.23
toolchain go1.24.3

44
go.sum
View File

@@ -1,42 +1,34 @@
github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0= github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0=
github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM= github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM=
github.com/antonini/golibjpegturbo v0.0.0-20141208033414-c03a2fa1e89a h1:+Q4qlzO9KeavJSvWgRKckT3ViTSQ8rR6GroSiXPRXhs= github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync=
github.com/antonini/golibjpegturbo v0.0.0-20141208033414-c03a2fa1e89a/go.mod h1:UOX4aiVZ5WVUBY3D/31H4m6Z8UHgBj5Qr/oldRBBUMY= github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/gen2brain/base64 v0.0.0-20221015184129-317a5c93030c h1:TUjjeJ2rV4KZxH6hIEi/boEQB3v6aKvwdakUJR3AwiE= github.com/gen2brain/base64 v0.0.0-20221015184129-317a5c93030c h1:TUjjeJ2rV4KZxH6hIEi/boEQB3v6aKvwdakUJR3AwiE=
github.com/gen2brain/base64 v0.0.0-20221015184129-317a5c93030c/go.mod h1:VG58IUyxPWojCtGwqwoZ/6LLXwClu1tssqa5ktOxI9o= github.com/gen2brain/base64 v0.0.0-20221015184129-317a5c93030c/go.mod h1:VG58IUyxPWojCtGwqwoZ/6LLXwClu1tssqa5ktOxI9o=
github.com/gen2brain/go-opencv v0.0.0-20191005190506-bf186fc94f7a h1:0arrt5Ke40opD5glNdh9ltrkZ0jaqPWsquGPSE3ukug= github.com/gen2brain/jpegli v0.3.4 h1:wFoUHIjfPJGGeuW3r9dqy0MTT1TtvJuWf6EqfHPPGFM=
github.com/gen2brain/go-opencv v0.0.0-20191005190506-bf186fc94f7a/go.mod h1:pOLh42huXUuMoJWvD2K+EeXzvQ9GZ5HN6gdFk5ZwIuU= github.com/gen2brain/jpegli v0.3.4/go.mod h1:tVnF7NPyufTo8noFlW5lurUUwZW8trwBENOItzuk2BM=
github.com/hybridgroup/mjpeg v0.0.0-20140228234708-4680f319790e/go.mod h1:eagM805MRKrioHYuU7iKLUyFPVKqVV6um5DAvCkUtXs= github.com/hybridgroup/mjpeg v0.0.0-20140228234708-4680f319790e/go.mod h1:eagM805MRKrioHYuU7iKLUyFPVKqVV6um5DAvCkUtXs=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jamiealquiza/envy v1.1.0 h1:Nwh4wqTZ28gDA8zB+wFkhnUpz3CEcO12zotjeqqRoKE=
github.com/jamiealquiza/envy v1.1.0/go.mod h1:MP36BriGCLwEHhi1OU8E9569JNZrjWfCvzG7RsPnHus=
github.com/korandiz/v4l v1.1.0 h1:VbzaWlhqNzVPfHEYEM+V8T7184ndiEzljJgDHSHc7pc= github.com/korandiz/v4l v1.1.0 h1:VbzaWlhqNzVPfHEYEM+V8T7184ndiEzljJgDHSHc7pc=
github.com/korandiz/v4l v1.1.0/go.mod h1:pftxPG7hkuUgepioAY6PAE81mShaVjzd95X/WF4Izus= github.com/korandiz/v4l v1.1.0/go.mod h1:pftxPG7hkuUgepioAY6PAE81mShaVjzd95X/WF4Izus=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567 h1:pKjmNHL7BCXhgsnSlN6Ov3WAN2jbJMCx6IvrMN9GNfc= github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567 h1:pKjmNHL7BCXhgsnSlN6Ov3WAN2jbJMCx6IvrMN9GNfc=
github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567/go.mod h1:ytYavTmrpWG4s7UOfDhP6m4ASL5XA66nrOcUn1e2M78= github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567/go.mod h1:ytYavTmrpWG4s7UOfDhP6m4ASL5XA66nrOcUn1e2M78=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d h1:ls+7AYarUlUSetfnN/DKVNcK6W8mQWc6VblmOm4XwX0=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d/go.mod h1:DO7ixpslN6XfbWzeNH9vkS5CF2FQUX81B85rYe9zDxU=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
go.senan.xyz/flagconf v0.1.9 h1:LBDmqiVFgijfqFXDzH97gPn0qDbg1Dq6/vxsxS/TzC4=
go.senan.xyz/flagconf v0.1.9/go.mod h1:NqOFfSwJvNWXOTUabcRZ8mPK9+sJmhStJhqtEt74wNQ=
gocv.io/x/gocv v0.35.0 h1:Qaxb5KdVyy8Spl4S4K0SMZ6CVmKtbfoSGQAxRD3FZlw= gocv.io/x/gocv v0.35.0 h1:Qaxb5KdVyy8Spl4S4K0SMZ6CVmKtbfoSGQAxRD3FZlw=
gocv.io/x/gocv v0.35.0/go.mod h1:oc6FvfYqfBp99p+yOEzs9tbYF9gOrAQSeL/dyIPefJU= gocv.io/x/gocv v0.35.0/go.mod h1:oc6FvfYqfBp99p+yOEzs9tbYF9gOrAQSeL/dyIPefJU=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=

View File

@@ -1,4 +1,3 @@
// Package handlers.
package handlers package handlers
import ( import (
@@ -13,11 +12,11 @@ type HTML struct {
} }
// NewHTML returns new HTML handler. // NewHTML returns new HTML handler.
func NewHTML(width, height float64, nogl bool) *HTML { func NewHTML(width, height float64, noWebGL bool) *HTML {
h := &HTML{} h := &HTML{}
tpl := htmlWebGL tpl := htmlWebGL
if nogl { if noWebGL {
tpl = html tpl = html
} }
tpl = strings.Replace(tpl, "{WIDTH}", fmt.Sprintf("%.0f", width), -1) tpl = strings.Replace(tpl, "{WIDTH}", fmt.Sprintf("%.0f", width), -1)
@@ -32,12 +31,13 @@ func (h *HTML) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" && r.Method != "HEAD" { if r.Method != "GET" && r.Method != "HEAD" {
msg := fmt.Sprintf("405 Method Not Allowed (%s)", r.Method) msg := fmt.Sprintf("405 Method Not Allowed (%s)", r.Method)
http.Error(w, msg, http.StatusMethodNotAllowed) http.Error(w, msg, http.StatusMethodNotAllowed)
return return
} }
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write(h.Template) _, _ = w.Write(h.Template)
} }
var html = `<html> var html = `<html>

View File

@@ -1,3 +1,4 @@
// Package handlers provides HTTP handlers for the cam2ip application.
package handlers package handlers
import ( import (
@@ -17,10 +18,11 @@ func NewIndex() *Index {
func (i *Index) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (i *Index) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" && r.Method != "HEAD" { if r.Method != "GET" && r.Method != "HEAD" {
http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed) http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed)
return return
} }
w.Write([]byte(`<html> _, _ = w.Write([]byte(`<html>
<head><title>cam2ip</title></head> <head><title>cam2ip</title></head>
<body> <body>
<h1>cam2ip</h1> <h1>cam2ip</h1>

View File

@@ -5,23 +5,24 @@ import (
"net/http" "net/http"
"github.com/gen2brain/cam2ip/image" "github.com/gen2brain/cam2ip/image"
"github.com/gen2brain/cam2ip/reader"
) )
// JPEG handler. // JPEG handler.
type JPEG struct { type JPEG struct {
reader reader.ImageReader reader ImageReader
quality int
} }
// NewJPEG returns new JPEG handler. // NewJPEG returns new JPEG handler.
func NewJPEG(reader reader.ImageReader) *JPEG { func NewJPEG(reader ImageReader, quality int) *JPEG {
return &JPEG{reader} return &JPEG{reader, quality}
} }
// ServeHTTP handles requests on incoming connections. // ServeHTTP handles requests on incoming connections.
func (j *JPEG) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (j *JPEG) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" && r.Method != "HEAD" { if r.Method != "GET" && r.Method != "HEAD" {
http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed) http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed)
return return
} }
@@ -32,12 +33,16 @@ func (j *JPEG) ServeHTTP(w http.ResponseWriter, r *http.Request) {
img, err := j.reader.Read() img, err := j.reader.Read()
if err != nil { if err != nil {
log.Printf("jpeg: read: %v", err) log.Printf("jpeg: read: %v", err)
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
return return
} }
err = image.NewEncoder(w).Encode(img) err = image.NewEncoder(w, j.quality).Encode(img)
if err != nil { if err != nil {
log.Printf("jpeg: encode: %v", err) log.Printf("jpeg: encode: %v", err)
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
return return
} }
} }

View File

@@ -1,4 +1,3 @@
// Package handlers.
package handlers package handlers
import ( import (
@@ -10,40 +9,41 @@ import (
"time" "time"
"github.com/gen2brain/cam2ip/image" "github.com/gen2brain/cam2ip/image"
"github.com/gen2brain/cam2ip/reader"
) )
// MJPEG handler. // MJPEG handler.
type MJPEG struct { type MJPEG struct {
reader reader.ImageReader reader ImageReader
delay int delay int
quality int
} }
// NewMJPEG returns new MJPEG handler. // NewMJPEG returns new MJPEG handler.
func NewMJPEG(reader reader.ImageReader, delay int) *MJPEG { func NewMJPEG(reader ImageReader, delay, quality int) *MJPEG {
return &MJPEG{reader, delay} return &MJPEG{reader, delay, quality}
} }
// ServeHTTP handles requests on incoming connections. // ServeHTTP handles requests on incoming connections.
func (m *MJPEG) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (m *MJPEG) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" && r.Method != "HEAD" { if r.Method != "GET" && r.Method != "HEAD" {
http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed) http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed)
return return
} }
mimeWriter := multipart.NewWriter(w) mimeWriter := multipart.NewWriter(w)
mimeWriter.SetBoundary("--boundary") _ = mimeWriter.SetBoundary("--boundary")
w.Header().Add("Connection", "close") w.Header().Add("Connection", "close")
w.Header().Add("Cache-Control", "no-store, no-cache") w.Header().Add("Cache-Control", "no-store, no-cache")
w.Header().Add("Content-Type", fmt.Sprintf("multipart/x-mixed-replace;boundary=%s", mimeWriter.Boundary())) w.Header().Add("Content-Type", fmt.Sprintf("multipart/x-mixed-replace;boundary=%s", mimeWriter.Boundary()))
cn := w.(http.CloseNotifier).CloseNotify() done := r.Context().Done()
loop: loop:
for { for {
select { select {
case <-cn: case <-done:
break loop break loop
default: default:
@@ -62,15 +62,17 @@ loop:
continue continue
} }
err = image.NewEncoder(partWriter).Encode(img) err = image.NewEncoder(partWriter, m.quality).Encode(img)
if err != nil { if err != nil {
log.Printf("mjpeg: encode: %v", err) log.Printf("mjpeg: encode: %v", err)
continue continue
} }
time.Sleep(time.Duration(m.delay) * time.Millisecond) if m.delay > 0 {
time.Sleep(time.Duration(m.delay) * time.Millisecond)
}
} }
} }
mimeWriter.Close() _ = mimeWriter.Close()
} }

View File

@@ -1,5 +1,4 @@
// Package reader. package handlers
package reader
import ( import (
"image" "image"

View File

@@ -7,21 +7,21 @@ import (
"net/http" "net/http"
"time" "time"
"nhooyr.io/websocket" "github.com/coder/websocket"
"github.com/gen2brain/cam2ip/image" "github.com/gen2brain/cam2ip/image"
"github.com/gen2brain/cam2ip/reader"
) )
// Socket handler. // Socket handler.
type Socket struct { type Socket struct {
reader reader.ImageReader reader ImageReader
delay int delay int
quality int
} }
// NewSocket returns new socket handler. // NewSocket returns new socket handler.
func NewSocket(reader reader.ImageReader, delay int) *Socket { func NewSocket(reader ImageReader, delay, quality int) *Socket {
return &Socket{reader, delay} return &Socket{reader, delay, quality}
} }
// ServeHTTP handles requests on incoming connections. // ServeHTTP handles requests on incoming connections.
@@ -29,6 +29,7 @@ func (s *Socket) ServeHTTP(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Accept(w, r, nil) conn, err := websocket.Accept(w, r, nil)
if err != nil { if err != nil {
log.Printf("socket: accept: %v", err) log.Printf("socket: accept: %v", err)
return return
} }
@@ -43,7 +44,7 @@ func (s *Socket) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w := new(bytes.Buffer) w := new(bytes.Buffer)
err = image.NewEncoder(w).Encode(img) err = image.NewEncoder(w, s.quality).Encode(img)
if err != nil { if err != nil {
log.Printf("socket: encode: %v", err) log.Printf("socket: encode: %v", err)
continue continue
@@ -56,8 +57,10 @@ func (s *Socket) ServeHTTP(w http.ResponseWriter, r *http.Request) {
break break
} }
time.Sleep(time.Duration(s.delay) * time.Millisecond) if s.delay > 0 {
time.Sleep(time.Duration(s.delay) * time.Millisecond)
}
} }
conn.Close(websocket.StatusNormalClosure, "") _ = conn.Close(websocket.StatusNormalClosure, "")
} }

View File

@@ -1,5 +1,4 @@
//go:build !amd64 //go:build !amd64
// +build !amd64
package image package image

View File

@@ -1,5 +1,4 @@
//go:build amd64 //go:build amd64
// +build amd64
package image package image

View File

@@ -1,14 +1,12 @@
//go:build turbo //go:build !libjpeg && !jpegli
// +build turbo
// Package image. // Package image.
package image package image
import ( import (
"image" "image"
"image/jpeg"
"io" "io"
jpeg "github.com/antonini/golibjpegturbo"
) )
// NewDecoder returns a new Decoder. // NewDecoder returns a new Decoder.

31
image/decode_jpegli.go Normal file
View File

@@ -0,0 +1,31 @@
//go:build jpegli
// Package image.
package image
import (
"image"
"io"
"github.com/gen2brain/jpegli"
)
// 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 jpegli.DecodeWithOptions(d.r, &jpegli.DecodingOptions{
DCTMethod: jpegli.DCTIFast,
FancyUpsampling: false,
BlockSmoothing: false,
ArithCode: true,
})
}

View File

@@ -1,13 +1,13 @@
//go:build !turbo //go:build libjpeg
// +build !turbo
// Package image. // Package image.
package image package image
import ( import (
"image" "image"
"image/jpeg"
"io" "io"
"github.com/pixiv/go-libjpeg/jpeg"
) )
// NewDecoder returns a new Decoder. // NewDecoder returns a new Decoder.
@@ -22,5 +22,9 @@ type Decoder struct {
// Decode decodes image from JPEG. // Decode decodes image from JPEG.
func (d Decoder) Decode() (image.Image, error) { func (d Decoder) Decode() (image.Image, error) {
return jpeg.Decode(d.r) return jpeg.Decode(d.r, &jpeg.DecoderOptions{
DCTMethod: jpeg.DCTIFast,
DisableFancyUpsampling: true,
DisableBlockSmoothing: true,
})
} }

View File

@@ -1,29 +1,28 @@
//go:build turbo //go:build !libjpeg && !jpegli
// +build turbo
// Package image. // Package image.
package image package image
import ( import (
"image" "image"
"image/jpeg"
"io" "io"
jpeg "github.com/antonini/golibjpegturbo"
) )
// NewEncoder returns a new Encoder. // NewEncoder returns a new Encoder.
func NewEncoder(w io.Writer) *Encoder { func NewEncoder(w io.Writer, quality int) *Encoder {
return &Encoder{w} return &Encoder{w, quality}
} }
// Encoder struct. // Encoder struct.
type Encoder struct { type Encoder struct {
w io.Writer w io.Writer
quality int
} }
// Encode encodes image to JPEG. // Encode encodes image to JPEG.
func (e Encoder) Encode(img image.Image) error { func (e Encoder) Encode(img image.Image) error {
err := jpeg.Encode(e.w, img, &jpeg.Options{Quality: 75}) err := jpeg.Encode(e.w, img, &jpeg.Options{Quality: e.quality})
if err != nil { if err != nil {
return err return err
} }

View File

@@ -1,31 +0,0 @@
//go:build !turbo
// +build !turbo
// Package image.
package image
import (
"image"
"image/jpeg"
"io"
)
// NewEncoder returns a new Encoder.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{w}
}
// Encoder struct.
type Encoder struct {
w io.Writer
}
// Encode encodes image to JPEG.
func (e Encoder) Encode(img image.Image) error {
err := jpeg.Encode(e.w, img, &jpeg.Options{Quality: 75})
if err != nil {
return err
}
return nil
}

36
image/encode_jpegli.go Normal file
View File

@@ -0,0 +1,36 @@
//go:build jpegli
// Package image.
package image
import (
"image"
"io"
"github.com/gen2brain/jpegli"
)
// NewEncoder returns a new Encoder.
func NewEncoder(w io.Writer, quality int) *Encoder {
return &Encoder{w, quality}
}
// Encoder struct.
type Encoder struct {
w io.Writer
quality int
}
// Encode encodes image to JPEG.
func (e Encoder) Encode(img image.Image) error {
return jpegli.Encode(e.w, img, &jpegli.EncodingOptions{
Quality: e.quality,
ProgressiveLevel: 0,
ChromaSubsampling: image.YCbCrSubsampleRatio420,
DCTMethod: jpegli.DCTIFast,
OptimizeCoding: false,
AdaptiveQuantization: false,
StandardQuantTables: false,
FancyDownsampling: false,
})
}

32
image/encode_libjpeg.go Normal file
View File

@@ -0,0 +1,32 @@
//go:build libjpeg
// Package image.
package image
import (
"image"
"io"
"github.com/pixiv/go-libjpeg/jpeg"
)
// NewEncoder returns a new Encoder.
func NewEncoder(w io.Writer, quality int) *Encoder {
return &Encoder{w, quality}
}
// Encoder struct.
type Encoder struct {
w io.Writer
quality int
}
// Encode encodes image to JPEG.
func (e Encoder) Encode(img image.Image) error {
return jpeg.Encode(e.w, img, &jpeg.EncoderOptions{
Quality: e.quality,
DCTMethod: jpeg.DCTIFast,
ProgressiveMode: false,
OptimizeCoding: false,
})
}

48
image/image.go Normal file
View File

@@ -0,0 +1,48 @@
package image
import (
"image"
"image/color"
"image/draw"
"time"
"github.com/anthonynsimon/bild/transform"
"github.com/pbnjay/pixfont"
)
func Rotate(img image.Image, angle int) image.Image {
switch angle {
case 90:
img = transform.Rotate(img, 90, &transform.RotationOptions{ResizeBounds: true})
case 180:
img = transform.Rotate(img, 180, &transform.RotationOptions{ResizeBounds: true})
case 270:
img = transform.Rotate(img, 270, &transform.RotationOptions{ResizeBounds: true})
}
return img
}
func Flip(img image.Image, dir string) image.Image {
switch dir {
case "horizontal":
img = transform.FlipH(img)
case "vertical":
img = transform.FlipV(img)
}
return img
}
func Timestamp(img image.Image, format string) image.Image {
dimg, ok := img.(draw.Image)
if !ok {
b := img.Bounds()
dimg = image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
draw.Draw(dimg, b, img, b.Min, draw.Src)
}
pixfont.DrawString(dimg, 10, 10, time.Now().Format(format), color.White)
return dimg
}

43
image/image_test.go Normal file
View File

@@ -0,0 +1,43 @@
package image_test
import (
"bytes"
_ "embed"
"image/jpeg"
"io"
"testing"
"github.com/gen2brain/cam2ip/image"
)
//go:embed testdata/test.jpg
var testJpg []byte
func BenchmarkDecode(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := image.NewDecoder(bytes.NewReader(testJpg)).Decode()
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkEncode(b *testing.B) {
img, err := jpeg.Decode(bytes.NewReader(testJpg))
if err != nil {
b.Fatal(err)
}
for i := 0; i < b.N; i++ {
err := image.NewEncoder(io.Discard).Encode(img)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkBase64(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = image.EncodeToString(testJpg)
}
}

BIN
image/testdata/test.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -5,11 +5,11 @@ import (
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"time"
"github.com/abbot/go-http-auth" "github.com/abbot/go-http-auth"
"github.com/gen2brain/cam2ip/handlers" "github.com/gen2brain/cam2ip/handlers"
"github.com/gen2brain/cam2ip/reader"
) )
// Server struct. // Server struct.
@@ -17,28 +17,31 @@ type Server struct {
Name string Name string
Version string Version string
Bind string
Htpasswd string
Index int Index int
Delay int Delay int
FrameWidth float64 Width float64
FrameHeight float64 Height float64
Rotate int Quality int
Rotate int
Flip string
NoWebGL bool NoWebGL bool
Timestamp bool
FileName string Timestamp bool
TimeFormat string
Reader reader.ImageReader Bind string
Htpasswd string
Reader handlers.ImageReader
} }
// NewServer returns new Server. // NewServer returns new Server.
func NewServer() *Server { func NewServer() *Server {
s := &Server{} s := &Server{}
return s return s
} }
@@ -50,10 +53,10 @@ func (s *Server) ListenAndServe() error {
basic = auth.NewBasicAuthenticator(realm, auth.HtpasswdFileProvider(s.Htpasswd)) basic = auth.NewBasicAuthenticator(realm, auth.HtpasswdFileProvider(s.Htpasswd))
} }
http.Handle("/html", newAuthHandler(handlers.NewHTML(s.FrameWidth, s.FrameHeight, s.NoWebGL), basic)) http.Handle("/html", newAuthHandler(handlers.NewHTML(s.Width, s.Height, s.NoWebGL), basic))
http.Handle("/jpeg", newAuthHandler(handlers.NewJPEG(s.Reader), basic)) http.Handle("/jpeg", newAuthHandler(handlers.NewJPEG(s.Reader, s.Quality), basic))
http.Handle("/mjpeg", newAuthHandler(handlers.NewMJPEG(s.Reader, s.Delay), basic)) http.Handle("/mjpeg", newAuthHandler(handlers.NewMJPEG(s.Reader, s.Delay, s.Quality), basic))
http.Handle("/socket", newAuthHandler(handlers.NewSocket(s.Reader, s.Delay), basic)) http.Handle("/socket", newAuthHandler(handlers.NewSocket(s.Reader, s.Delay, s.Quality), basic))
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
@@ -61,7 +64,10 @@ func (s *Server) ListenAndServe() error {
http.Handle("/", newAuthHandler(handlers.NewIndex(), basic)) http.Handle("/", newAuthHandler(handlers.NewIndex(), basic))
srv := &http.Server{} srv := &http.Server{
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
}
listener, err := net.Listen("tcp", s.Bind) listener, err := net.Listen("tcp", s.Bind)
if err != nil { if err != nil {
@@ -78,6 +84,7 @@ func newAuthHandler(handler http.Handler, authenticator *auth.BasicAuth) http.Ha
w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", authenticator.Realm)) w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", authenticator.Realm))
if authenticator.CheckAuth(r) == "" { if authenticator.CheckAuth(r) == "" {
http.Error(w, "401 Unauthorized", http.StatusUnauthorized) http.Error(w, "401 Unauthorized", http.StatusUnauthorized)
return return
} }
} }