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 . .
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

View File

@@ -21,33 +21,19 @@ You can also use apps like `ffplay` or `vlc`:
### Build tags
* `cv2` - build with `OpenCV` 2.x ([go-opencv](https://github.com/lazywei/go-opencv))
* `cv4` - build with `OpenCV` 4.x ([gocv](https://github.com/hybridgroup/gocv))
* `turbo` - build with `libjpeg-turbo` ([libjpeg-turbo](https://www.libjpeg-turbo.org/)) instead of native Go `image/jpeg`
* `opencv` - use `OpenCV` library to access camera ([gocv](https://github.com/hybridgroup/gocv))
* `libjpeg` - build with `libjpeg` ([go-libjpeg](https://github.com/pixiv/go-libjpeg)) instead of native `image/jpeg`
* `jpegli` - build with `jpegli` ([jpegli](https://github.com/gen2brain/jpegli)) instead of native `image/jpeg`
### Download
Binaries are compiled with static OpenCV/libjpeg-turbo libraries, they should just work:
- [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)
Download the latest binaries from the [releases](https://github.com/gen2brain/cam2ip/releases).
### 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
@@ -56,25 +42,31 @@ This will install app in `$GOPATH/bin/cam2ip`.
### Usage
```
Usage of cam2ip:
-bind-addr string
Bind address [CAM2IP_BIND_ADDR] (default ":56000")
-delay int
Delay between frames, in milliseconds [CAM2IP_DELAY] (default 10)
-height float
Frame height [CAM2IP_HEIGHT] (default 480)
-htpasswd-file string
Path to htpasswd file, if empty auth is disabled [CAM2IP_HTPASSWD_FILE]
-index int
Camera index [CAM2IP_INDEX]
-nowebgl
Disable WebGL drawing of images (html handler) [CAM2IP_NOWEBGL]
-rotate int
Rotate image, valid values are 90, 180, 270 [CAM2IP_ROTATE]
-timestamp
Draws timestamp on images [CAM2IP_TIMESTAMP]
-width float
Frame width [CAM2IP_WIDTH] (default 640)
Usage: cam2ip [<flags>]
--index
Camera index [CAM2IP_INDEX] (default "0")
--delay
Delay between frames, in milliseconds [CAM2IP_DELAY] (default "10")
--width
Frame width [CAM2IP_WIDTH] (default "640")
--height
Frame height [CAM2IP_HEIGHT] (default "480")
--quality
Image quality [CAM2IP_QUALITY] (default "75")
--rotate
Rotate image, valid values are 90, 180, 270 [CAM2IP_ROTATE] (default "0")
--flip
Flip image, valid values are horizontal and vertical [CAM2IP_FLIP] (default "")
--no-webgl
Disable WebGL drawing of image (html handler) [CAM2IP_NO_WEBGL] (default "false")
--timestamp
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

View File

@@ -1,10 +1,99 @@
package camera
// Options.
import (
"bytes"
"fmt"
"image"
)
// Options .
type Options struct {
Index int
Rotate int
Width float64
Height float64
Timestamp bool
Index int
Rotate int
Flip string
Width float64
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
// +build android
// Package camera.
package camera
@@ -33,7 +32,7 @@ ACaptureSessionOutput *captureSessionOutput;
ACaptureSessionOutputContainer *captureSessionOutputContainer;
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) {
@@ -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)
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))
return
}
@@ -252,13 +252,15 @@ func New(opts Options) (camera *Camera, err error) {
// Read reads next frame from camera and returns image.
func (c *Camera) Read() (img image.Image, err error) {
ret := C.captureCamera()
if bool(int(ret) != 0) {
if int(ret) != 0 {
err = fmt.Errorf("camera: can not grab frame: error %d", int(ret))
return
}
if C.image == nil {
err = fmt.Errorf("camera: can not retrieve frame")
return
}
@@ -283,20 +285,12 @@ func (c *Camera) Read() (img image.Image, err error) {
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.
func (c *Camera) Close() (err error) {
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))
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
// +build !cv2,!cv4,!android
//go:build !opencv && !android
// Package camera.
package camera
@@ -7,14 +6,10 @@ package camera
import (
"fmt"
"image"
"image/color"
"image/draw"
"time"
"io"
"slices"
"github.com/disintegration/imaging"
"github.com/korandiz/v4l"
"github.com/korandiz/v4l/fmt/mjpeg"
"github.com/pbnjay/pixfont"
im "github.com/gen2brain/cam2ip/image"
)
@@ -23,49 +18,82 @@ import (
type Camera struct {
opts Options
camera *v4l.Device
config v4l.DeviceConfig
ycbcr *image.YCbCr
}
// New returns new Camera for given camera index.
func New(opts Options) (camera *Camera, err error) {
camera = &Camera{}
camera.opts = opts
func New(opts Options) (c *Camera, err error) {
c = &Camera{}
c.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)
c.camera, err = v4l.Open(devices[opts.Index].Path)
if err != nil {
err = fmt.Errorf("camera: %s", err.Error())
err = fmt.Errorf("camera: %w", err)
return
}
if camera.camera == nil {
if c.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())
configs, e := c.camera.ListConfigs()
if e != nil {
err = fmt.Errorf("camera: can not list configs: %w", e)
return
}
config.Format = mjpeg.FourCC
config.Width = int(opts.Width)
config.Height = int(opts.Height)
formats := make([]uint32, 0)
for _, config := range configs {
formats = append(formats, config.Format)
}
err = camera.camera.SetConfig(config)
c.config, err = c.camera.GetConfig()
if err != nil {
err = fmt.Errorf("camera: %s", err.Error())
err = fmt.Errorf("camera: can not get config: %w", err)
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 {
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
}
@@ -74,63 +102,65 @@ func New(opts Options) (camera *Camera, err error) {
// 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())
err = fmt.Errorf("camera: format %d: can not grab frame: %w", c.config.Format, err)
return
}
img, err = im.NewDecoder(buffer).Decode()
if err != nil {
err = fmt.Errorf("camera: %s", err.Error())
return
}
switch c.config.Format {
case yuy2FourCC, yuyvFourCC:
data, e := io.ReadAll(buffer)
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
}
pixfont.DrawString(dimg, 10, 10, time.Now().Format("2006-01-02 15:04:05"), color.White)
img = dimg
e = yuy2ToYCbCr422(data, c.ycbcr)
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
}
// 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")
err = fmt.Errorf("camera: close: camera is not opened")
return
}
c.camera.TurnOff()
c.camera.Close()
c.camera = nil
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 (
"fmt"
"image/jpeg"
"io/ioutil"
"os"
"path/filepath"
"io"
"testing"
"time"
"github.com/gen2brain/cam2ip/image"
)
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 {
t.Fatal(err)
}
defer camera.Close()
tmpdir, err := ioutil.TempDir(os.TempDir(), "cam2ip")
if err != nil {
t.Error(err)
}
defer os.RemoveAll(tmpdir)
defer func(camera *Camera) {
err := camera.Close()
if err != nil {
t.Error(err)
}
}(camera)
var i int
var n int = 10
var n = 10
timeout := time.After(time.Duration(n) * time.Second)
for {
select {
case <-timeout:
//fmt.Printf("Fps: %d\n", i/n)
fmt.Printf("FPS: %.2f\n", float64(i)/float64(n))
return
default:
i += 1
@@ -43,17 +40,7 @@ func TestCamera(t *testing.T) {
t.Error(err)
}
file, err := os.Create(filepath.Join(tmpdir, fmt.Sprintf("%03d.jpg", i)))
if err != nil {
t.Error(err)
}
err = jpeg.Encode(file, img, &jpeg.Options{Quality: 75})
if err != nil {
t.Error(err)
}
err = file.Close()
err = image.NewEncoder(io.Discard, 75).Encode(img)
if err != nil {
t.Error(err)
}

View File

@@ -1,5 +1,4 @@
//go:build !cv2 && !cv4
// +build !cv2,!cv4
//go:build !opencv
// Package camera.
package camera
@@ -8,24 +7,27 @@ import (
"bytes"
"fmt"
"image"
"image/color"
"image/draw"
"runtime"
"syscall"
"time"
"unsafe"
"github.com/disintegration/imaging"
"github.com/pbnjay/pixfont"
im "github.com/gen2brain/cam2ip/image"
)
func init() {
runtime.LockOSThread()
}
// Camera represents camera.
type Camera struct {
opts Options
camera syscall.Handle
frame *image.RGBA
rgba *image.RGBA
ycbcr *image.YCbCr
hdr *videoHdr
instance syscall.Handle
className string
format uint32
}
// New returns new Camera for given camera index.
@@ -34,35 +36,37 @@ func New(opts Options) (camera *Camera, err error) {
camera.opts = opts
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) {
fn := func(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
case wmClose:
destroyWindow(hwnd)
_ = destroyWindow(hwnd)
case wmDestroy:
postQuitMessage(0)
default:
ret := defWindowProc(hwnd, msg, wparam, lparam)
return ret
}
return 0
}
c.instance, err = getModuleHandle()
if err != nil {
return
}
err = registerClass(c.className, c.instance, fn)
if err != nil {
return
}
hwnd, err := createWindow(0, c.className, "", wsOverlappedWindow, cwUseDefault, cwUseDefault, int64(c.opts.Width)+100, int64(c.opts.Height)+100, 0, 0, c.instance)
if err != nil {
hwnd, e := createWindow(0, c.className, "", wsOverlappedWindow, cwUseDefault, cwUseDefault,
int64(c.opts.Width)+100, int64(c.opts.Height)+100, 0, 0, c.instance)
if e != nil {
err = e
return
}
@@ -72,11 +76,15 @@ func New(opts Options) (camera *Camera, err error) {
}
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)
return
}
sendMessage(c.camera, wmCapSetPreview, 0, 0)
sendMessage(c.camera, wmCapSetOverlay, 0, 0)
var bi bitmapInfo
size := sendMessage(c.camera, wmCapGetVideoformat, 0, 0)
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)
ret = sendMessage(c.camera, wmCapSetVideoformat, size, uintptr(unsafe.Pointer(&bi)))
if bool(int(ret) == 0) {
err = fmt.Errorf("camera: can not set video format")
if int(ret) == 0 {
err = fmt.Errorf("camera: can not set video format: %dx%d, %d", int(c.opts.Width), int(c.opts.Height), c.format)
return
}
c.format = bi.BmiHeader.BiCompression
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)
return
@@ -100,86 +138,75 @@ func New(opts Options) (camera *Camera, err error) {
// Read reads next frame from camera and returns image.
func (c *Camera) Read() (img image.Image, err error) {
ret := sendMessage(c.camera, wmCapGrabFrameNoStop, 0, 0)
if bool(int(ret) == 0) {
ret := sendMessage(c.camera, wmCapGrabFrame, 0, 0)
if int(ret) == 0 {
err = fmt.Errorf("camera: can not grab frame")
return
}
data := (*[1 << 24]uint8)(unsafe.Pointer(c.hdr.LpData))[0:c.hdr.DwBytesUsed]
r := bytes.NewReader(data)
data := unsafe.Slice((*byte)(unsafe.Pointer(c.hdr.LpData)), c.hdr.DwBufferLength)
width := int(c.opts.Width)
height := int(c.opts.Height)
switch c.format {
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
}
p := c.frame.Pix[y*c.frame.Stride : y*c.frame.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
img = c.rgba
case yuy2FourCC, yuyvFourCC:
e := yuy2ToYCbCr422(data, c.ycbcr)
if e != nil {
err = fmt.Errorf("camera: format %d: can not retrieve frame: %w", c.format, e)
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 {
case 90:
img = imaging.Rotate90(img)
case 180:
img = imaging.Rotate180(img)
case 270:
img = imaging.Rotate270(img)
if c.opts.Flip != "" {
img = im.Flip(img, c.opts.Flip)
}
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
img = im.Timestamp(img, c.opts.TimeFormat)
}
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.
func (c *Camera) Close() (err error) {
sendMessage(c.camera, wmCapSetCallbackFrame, 0, 0)
unregisterClass(c.className, c.instance)
sendMessage(c.camera, wmCapDriverDisconnect, 0, 0)
destroyWindow(c.camera)
return
return destroyWindow(c.camera)
}
// callback function.
func (c *Camera) callback(hwvd syscall.Handle, hdr *videoHdr) uintptr {
c.hdr = hdr
func (c *Camera) callback(hwnd syscall.Handle, hdr *videoHdr) uintptr {
if hdr != nil {
c.hdr = hdr
}
return 0
}
@@ -188,16 +215,15 @@ var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
avicap32 = syscall.NewLazyDLL("avicap32.dll")
createWindowExW = user32.NewProc("CreateWindowExW")
destroyWindowW = user32.NewProc("DestroyWindow")
defWindowProcW = user32.NewProc("DefWindowProcW")
dispatchMessageW = user32.NewProc("DispatchMessageW")
translateMessageW = user32.NewProc("TranslateMessage")
getMessageW = user32.NewProc("GetMessageW")
sendMessageW = user32.NewProc("SendMessageW")
postQuitMessageW = user32.NewProc("PostQuitMessage")
registerClassExW = user32.NewProc("RegisterClassExW")
unregisterClassW = user32.NewProc("UnregisterClassW")
createWindowExW = user32.NewProc("CreateWindowExW")
destroyWindowW = user32.NewProc("DestroyWindow")
defWindowProcW = user32.NewProc("DefWindowProcW")
dispatchMessageW = user32.NewProc("DispatchMessageW")
getMessageW = user32.NewProc("GetMessageW")
sendMessageW = user32.NewProc("SendMessageW")
postQuitMessageW = user32.NewProc("PostQuitMessage")
registerClassExW = user32.NewProc("RegisterClassExW")
unregisterClassW = user32.NewProc("UnregisterClassW")
getModuleHandleW = kernel32.NewProc("GetModuleHandleW")
capCreateCaptureWindowW = avicap32.NewProc("capCreateCaptureWindowW")
@@ -214,6 +240,8 @@ const (
wmCapDriverDisconnect = wmCapStart + 11
wmCapGetVideoformat = wmCapStart + 44
wmCapSetVideoformat = wmCapStart + 45
wmCapSetPreview = wmCapStart + 50
wmCapSetOverlay = wmCapStart + 51
wmCapGrabFrame = wmCapStart + 60
wmCapGrabFrameNoStop = wmCapStart + 61
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
func defWindowProc(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
ret, _, _ := defWindowProcW.Call(uintptr(hwnd), uintptr(msg), uintptr(wparam), uintptr(lparam))
return uintptr(ret)
ret, _, _ := defWindowProcW.Call(uintptr(hwnd), uintptr(msg), wparam, lparam)
return ret
}
// 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)))
}
// 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
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))
@@ -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
func sendMessage(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
ret, _, _ := sendMessageW.Call(uintptr(hwnd), uintptr(msg), wparam, lparam, 0, 0)
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
func unregisterClass(className string, instance syscall.Handle) bool {
ret, _, _ := unregisterClassW.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(className))), uintptr(instance))
return ret != 0
}
@@ -399,19 +425,3 @@ func capCreateCaptureWindow(lpszWindowName string, dwStyle, x, y, width, height
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"
"os"
"github.com/jamiealquiza/envy"
"go.senan.xyz/flagconf"
"github.com/gen2brain/cam2ip/camera"
"github.com/gen2brain/cam2ip/server"
@@ -19,41 +19,56 @@ const (
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.BoolVar(&srv.Timestamp, "timestamp", false, "Draws timestamp on images")
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.IntVar(&srv.Index, "index", 0, "Camera index [CAM2IP_INDEX]")
flag.IntVar(&srv.Delay, "delay", 10, "Delay between frames, in milliseconds [CAM2IP_DELAY]")
flag.Float64Var(&srv.Width, "width", 640, "Frame width [CAM2IP_WIDTH]")
flag.Float64Var(&srv.Height, "height", 480, "Frame height [CAM2IP_HEIGHT]")
flag.IntVar(&srv.Quality, "quality", 75, "Image quality [CAM2IP_QUALITY]")
flag.IntVar(&srv.Rotate, "rotate", 0, "Rotate image, valid values are 90, 180, 270 [CAM2IP_ROTATE]")
flag.StringVar(&srv.Flip, "flip", "", "Flip image, valid values are horizontal and vertical [CAM2IP_FLIP]")
flag.BoolVar(&srv.NoWebGL, "no-webgl", false, "Disable WebGL drawing of image (html handler) [CAM2IP_NO_WEBGL]")
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()
_ = flagconf.ParseEnv()
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())
if _, err := os.Stat(srv.Htpasswd); err != nil {
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, srv.Timestamp})
cam, err := camera.New(camera.Options{
Index: srv.Index,
Rotate: srv.Rotate,
Flip: srv.Flip,
Width: srv.Width,
Height: srv.Height,
Timestamp: srv.Timestamp,
TimeFormat: srv.TimeFormat,
})
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
stderr("%s\n", err.Error())
os.Exit(1)
}
@@ -61,11 +76,15 @@ func main() {
defer srv.Reader.Close()
fmt.Fprintf(os.Stderr, "Listening on %s\n", srv.Bind)
stderr("Listening on %s\n", srv.Bind)
err = srv.ListenAndServe()
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
stderr("%s\n", err.Error())
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 (
github.com/abbot/go-http-auth v0.4.0
github.com/antonini/golibjpegturbo v0.0.0-20141208033414-c03a2fa1e89a
github.com/disintegration/imaging v1.6.2
github.com/anthonynsimon/bild v0.14.0
github.com/coder/websocket v1.8.13
github.com/gen2brain/base64 v0.0.0-20221015184129-317a5c93030c
github.com/gen2brain/go-opencv v0.0.0-20191005190506-bf186fc94f7a
github.com/jamiealquiza/envy v1.1.0
github.com/gen2brain/jpegli v0.3.4
github.com/korandiz/v4l v1.1.0
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
nhooyr.io/websocket v1.8.10
)
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tetratelabs/wazero v1.9.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/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/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM=
github.com/antonini/golibjpegturbo v0.0.0-20141208033414-c03a2fa1e89a h1:+Q4qlzO9KeavJSvWgRKckT3ViTSQ8rR6GroSiXPRXhs=
github.com/antonini/golibjpegturbo v0.0.0-20141208033414-c03a2fa1e89a/go.mod h1:UOX4aiVZ5WVUBY3D/31H4m6Z8UHgBj5Qr/oldRBBUMY=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync=
github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs=
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
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/go-opencv v0.0.0-20191005190506-bf186fc94f7a h1:0arrt5Ke40opD5glNdh9ltrkZ0jaqPWsquGPSE3ukug=
github.com/gen2brain/go-opencv v0.0.0-20191005190506-bf186fc94f7a/go.mod h1:pOLh42huXUuMoJWvD2K+EeXzvQ9GZ5HN6gdFk5ZwIuU=
github.com/gen2brain/jpegli v0.3.4 h1:wFoUHIjfPJGGeuW3r9dqy0MTT1TtvJuWf6EqfHPPGFM=
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/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/go.mod h1:pftxPG7hkuUgepioAY6PAE81mShaVjzd95X/WF4Izus=
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/go.mod h1:ytYavTmrpWG4s7UOfDhP6m4ASL5XA66nrOcUn1e2M78=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d h1:ls+7AYarUlUSetfnN/DKVNcK6W8mQWc6VblmOm4XwX0=
github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d/go.mod h1:DO7ixpslN6XfbWzeNH9vkS5CF2FQUX81B85rYe9zDxU=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
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/go.mod h1:oc6FvfYqfBp99p+yOEzs9tbYF9gOrAQSeL/dyIPefJU=
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/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/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,21 +7,21 @@ import (
"net/http"
"time"
"nhooyr.io/websocket"
"github.com/coder/websocket"
"github.com/gen2brain/cam2ip/image"
"github.com/gen2brain/cam2ip/reader"
)
// Socket handler.
type Socket struct {
reader reader.ImageReader
delay int
reader ImageReader
delay int
quality int
}
// NewSocket returns new socket handler.
func NewSocket(reader reader.ImageReader, delay int) *Socket {
return &Socket{reader, delay}
func NewSocket(reader ImageReader, delay, quality int) *Socket {
return &Socket{reader, delay, quality}
}
// 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)
if err != nil {
log.Printf("socket: accept: %v", err)
return
}
@@ -43,7 +44,7 @@ func (s *Socket) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w := new(bytes.Buffer)
err = image.NewEncoder(w).Encode(img)
err = image.NewEncoder(w, s.quality).Encode(img)
if err != nil {
log.Printf("socket: encode: %v", err)
continue
@@ -56,8 +57,10 @@ func (s *Socket) ServeHTTP(w http.ResponseWriter, r *http.Request) {
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
// +build !amd64
package image

View File

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

View File

@@ -1,14 +1,12 @@
//go:build turbo
// +build turbo
//go:build !libjpeg && !jpegli
// Package image.
package image
import (
"image"
"image/jpeg"
"io"
jpeg "github.com/antonini/golibjpegturbo"
)
// 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
// +build !turbo
//go:build libjpeg
// Package image.
package image
import (
"image"
"image/jpeg"
"io"
"github.com/pixiv/go-libjpeg/jpeg"
)
// NewDecoder returns a new Decoder.
@@ -22,5 +22,9 @@ type Decoder struct {
// Decode decodes image from JPEG.
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
// +build turbo
//go:build !libjpeg && !jpegli
// Package image.
package image
import (
"image"
"image/jpeg"
"io"
jpeg "github.com/antonini/golibjpegturbo"
)
// NewEncoder returns a new Encoder.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{w}
func NewEncoder(w io.Writer, quality int) *Encoder {
return &Encoder{w, quality}
}
// Encoder struct.
type Encoder struct {
w io.Writer
w io.Writer
quality int
}
// Encode encodes image to JPEG.
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 {
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"
"net"
"net/http"
"time"
"github.com/abbot/go-http-auth"
"github.com/gen2brain/cam2ip/handlers"
"github.com/gen2brain/cam2ip/reader"
)
// Server struct.
@@ -17,28 +17,31 @@ type Server struct {
Name string
Version string
Bind string
Htpasswd string
Index int
Delay int
FrameWidth float64
FrameHeight float64
Width float64
Height float64
Rotate int
Quality int
Rotate int
Flip string
NoWebGL bool
Timestamp bool
NoWebGL bool
FileName string
Timestamp bool
TimeFormat string
Reader reader.ImageReader
Bind string
Htpasswd string
Reader handlers.ImageReader
}
// NewServer returns new Server.
func NewServer() *Server {
s := &Server{}
return s
}
@@ -50,10 +53,10 @@ func (s *Server) ListenAndServe() error {
basic = auth.NewBasicAuthenticator(realm, auth.HtpasswdFileProvider(s.Htpasswd))
}
http.Handle("/html", newAuthHandler(handlers.NewHTML(s.FrameWidth, s.FrameHeight, s.NoWebGL), basic))
http.Handle("/jpeg", newAuthHandler(handlers.NewJPEG(s.Reader), basic))
http.Handle("/mjpeg", newAuthHandler(handlers.NewMJPEG(s.Reader, s.Delay), basic))
http.Handle("/socket", newAuthHandler(handlers.NewSocket(s.Reader, s.Delay), basic))
http.Handle("/html", newAuthHandler(handlers.NewHTML(s.Width, s.Height, s.NoWebGL), basic))
http.Handle("/jpeg", newAuthHandler(handlers.NewJPEG(s.Reader, s.Quality), basic))
http.Handle("/mjpeg", newAuthHandler(handlers.NewMJPEG(s.Reader, s.Delay, s.Quality), 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) {
w.WriteHeader(http.StatusOK)
@@ -61,7 +64,10 @@ func (s *Server) ListenAndServe() error {
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)
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))
if authenticator.CheckAuth(r) == "" {
http.Error(w, "401 Unauthorized", http.StatusUnauthorized)
return
}
}