mirror of
https://github.com/gen2brain/cam2ip.git
synced 2025-12-15 11:58:33 +00:00
Compare commits
91 Commits
1.2
...
5fcc525cec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fcc525cec | ||
|
|
1e6eae1e14 | ||
|
|
3fe0d88418 | ||
|
|
6826193e2c | ||
|
|
d84884f26b | ||
|
|
bf647116a3 | ||
|
|
8667fe4b48 | ||
|
|
7e4e58029a | ||
|
|
fd5cb861cd | ||
|
|
26b04f44ad | ||
|
|
f556285ad5 | ||
|
|
933b5eef22 | ||
|
|
e5ee1a2049 | ||
|
|
7ed9c4c442 | ||
|
|
711ad2f102 | ||
|
|
84135f3304 | ||
|
|
e01c80ca67 | ||
|
|
4a09c9b803 | ||
|
|
948fe29079 | ||
|
|
074d14ad01 | ||
|
|
b302c77f20 | ||
|
|
a1b32804da | ||
|
|
880b41dea2 | ||
|
|
244b4f51fc | ||
|
|
c177a0bb77 | ||
|
|
071a6c4f3c | ||
|
|
30e30117a1 | ||
|
|
fd8152f7a4 | ||
|
|
7dc02de8f4 | ||
|
|
6e046d47d0 | ||
|
|
df2f672da6 | ||
|
|
1ea7956db5 | ||
|
|
fa99c12ec1 | ||
|
|
f58b475549 | ||
|
|
d4ea63f95d | ||
|
|
7b60039e66 | ||
|
|
89298de3e9 | ||
|
|
fc22ce5871 | ||
|
|
19ea541157 | ||
|
|
be5863de1d | ||
|
|
7c3fe4be3c | ||
|
|
cc5d3ad202 | ||
|
|
cb2f566154 | ||
|
|
3b86bc0d69 | ||
|
|
8bfe9c787e | ||
|
|
12b5f29452 | ||
|
|
adf2a742e6 | ||
|
|
1c342a67df | ||
|
|
9e6d20863a | ||
|
|
3844a46486 | ||
|
|
2628bbefbf | ||
|
|
3b0b066cc1 | ||
|
|
277d6c5b48 | ||
|
|
1baf0deb39 | ||
|
|
50f28f57db | ||
|
|
b4e1af7729 | ||
|
|
1c8347fee8 | ||
|
|
63dd32115c | ||
|
|
773b8920b0 | ||
|
|
01b9c564ac | ||
|
|
f1df08acc7 | ||
|
|
5b7d51a9ac | ||
|
|
6b28dfec25 | ||
|
|
ff09bbeff4 | ||
|
|
4d73010019 | ||
|
|
13385ba650 | ||
|
|
09932a978c | ||
|
|
fb1388a10a | ||
|
|
8ebd729dcd | ||
|
|
309f6d339f | ||
|
|
3d617687fa | ||
|
|
d5870110d9 | ||
|
|
72f12931da | ||
|
|
bb676e4e44 | ||
|
|
ec60387edf | ||
|
|
0503355005 | ||
|
|
c9d77f03e7 | ||
|
|
37b19fcfe8 | ||
|
|
5006c48690 | ||
|
|
d340fa2dc3 | ||
|
|
fa5233255e | ||
|
|
ac555cbf7b | ||
|
|
e1f03b55a1 | ||
|
|
3c0c949f31 | ||
|
|
8a740337ab | ||
|
|
cbc7d21b23 | ||
|
|
45ca5bdedb | ||
|
|
173deebc88 | ||
|
|
7392d8d9b8 | ||
|
|
1c9dfcb84c | ||
|
|
f65a7cf1aa |
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM golang:alpine as build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 go build -o cam2ip -trimpath -ldflags "-s -w" github.com/gen2brain/cam2ip/cmd/cam2ip
|
||||
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=build /build/cam2ip /cam2ip
|
||||
|
||||
EXPOSE 56000
|
||||
|
||||
ENTRYPOINT ["/cam2ip"]
|
||||
73
README.md
73
README.md
@@ -4,50 +4,69 @@ Turn any webcam into an IP camera.
|
||||
|
||||
Example (in web browser):
|
||||
|
||||
http://localhost:56000/mjpeg
|
||||
http://localhost:56000/html
|
||||
|
||||
or
|
||||
|
||||
http://localhost:56000/html
|
||||
http://localhost:56000/mjpeg
|
||||
|
||||
You can also use apps like `ffplay` or `vlc`:
|
||||
|
||||
ffplay -i http://localhost:56000/mjpeg
|
||||
|
||||
### Requirements
|
||||
|
||||
* [OpenCV 2.x](http://opencv.org/)
|
||||
* On Linux/RPi native Go [V4L](https://github.com/korandiz/v4l) implementation is used to capture images.
|
||||
* On Windows [Video for Windows (VfW)](https://en.wikipedia.org/wiki/Video_for_Windows) framework is used over win32 API.
|
||||
|
||||
### Build tags
|
||||
|
||||
* `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 library:
|
||||
|
||||
- [Android 32bit](https://github.com/gen2brain/cam2ip/releases/download/1.2/cam2ip-1.2-android.tar.gz)
|
||||
- [Linux 64bit](https://github.com/gen2brain/cam2ip/releases/download/1.2/cam2ip-1.2-64bit.tar.gz)
|
||||
- [RPi 32bit](https://github.com/gen2brain/cam2ip/releases/download/1.2/cam2ip-1.2-RPi.tar.gz)
|
||||
- [Windows 32bit](https://github.com/gen2brain/cam2ip/releases/download/1.2/cam2ip-1.2.zip)
|
||||
|
||||
Download the latest binaries from the [releases](https://github.com/gen2brain/cam2ip/releases).
|
||||
|
||||
### Installation
|
||||
|
||||
go get -v github.com/gen2brain/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
|
||||
|
||||
docker run --device=/dev/video0:/dev/video0 -p56000:56000 -it gen2brain/cam2ip # on RPi use gen2brain/cam2ip:arm
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
Usage of ./cam2ip:
|
||||
-bind-addr string
|
||||
Bind address (default ":56000")
|
||||
-delay int
|
||||
Delay between frames, in milliseconds (default 10)
|
||||
-frame-height float
|
||||
Frame height (default 480)
|
||||
-frame-width float
|
||||
Frame width (default 640)
|
||||
-htpasswd-file string
|
||||
Path to htpasswd file, if empty auth is disabled
|
||||
-index int
|
||||
Camera index
|
||||
-webgl
|
||||
Use WebGL to draw images
|
||||
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
|
||||
|
||||
59
cam2ip.go
59
cam2ip.go
@@ -1,59 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/gen2brain/cam2ip/camera"
|
||||
"github.com/gen2brain/cam2ip/server"
|
||||
)
|
||||
|
||||
const (
|
||||
name = "cam2ip"
|
||||
version = "1.0"
|
||||
)
|
||||
|
||||
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, "frame-width", 640, "Frame width")
|
||||
flag.Float64Var(&srv.FrameHeight, "frame-height", 480, "Frame height")
|
||||
flag.BoolVar(&srv.WebGL, "webgl", false, "Use WebGL to draw 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.Parse()
|
||||
|
||||
srv.Name = name
|
||||
srv.Version = version
|
||||
|
||||
var err error
|
||||
|
||||
if srv.Htpasswd != "" {
|
||||
if _, err = os.Stat(srv.Htpasswd); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
srv.Camera, err = camera.NewCamera(srv.Index)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
srv.Camera.SetProperty(camera.PropFrameWidth, srv.FrameWidth)
|
||||
srv.Camera.SetProperty(camera.PropFrameHeight, srv.FrameHeight)
|
||||
|
||||
defer srv.Camera.Close()
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Listening on %s\n", srv.Bind)
|
||||
|
||||
err = srv.ListenAndServe()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
160
camera/camera.go
160
camera/camera.go
@@ -1,105 +1,99 @@
|
||||
// Package camera.
|
||||
package camera
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"github.com/lazywei/go-opencv/opencv"
|
||||
)
|
||||
|
||||
// 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
|
||||
// Options .
|
||||
type Options struct {
|
||||
Index int
|
||||
Rotate int
|
||||
Flip string
|
||||
Width float64
|
||||
Height float64
|
||||
Timestamp bool
|
||||
TimeFormat string
|
||||
}
|
||||
|
||||
var (
|
||||
yuy2FourCC = fourcc("YUY2")
|
||||
yuyvFourCC = fourcc("YUYV")
|
||||
mjpgFourCC = fourcc("MJPG")
|
||||
)
|
||||
|
||||
// Camera represents camera.
|
||||
type Camera struct {
|
||||
Index int
|
||||
camera *opencv.Capture
|
||||
func fourcc(b string) uint32 {
|
||||
return uint32(b[0]) | (uint32(b[1]) << 8) | (uint32(b[2]) << 16) | (uint32(b[3]) << 24)
|
||||
}
|
||||
|
||||
// NewCamera returns new Camera for given camera index.
|
||||
func NewCamera(index int) (camera *Camera, err error) {
|
||||
camera = &Camera{}
|
||||
camera.Index = index
|
||||
func bmp24ToRgba(data []byte, dst *image.RGBA) error {
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
camera.camera = opencv.NewCameraCapture(index)
|
||||
if camera.camera == nil {
|
||||
err = fmt.Errorf("camera: can not open camera %d", index)
|
||||
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
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read reads next frame from camera and returns image.
|
||||
func (c *Camera) Read() (img image.Image, err error) {
|
||||
if c.camera.GrabFrame() {
|
||||
frame := c.camera.RetrieveFrame(1)
|
||||
img = frame.ToImage()
|
||||
} else {
|
||||
err = fmt.Errorf("camera: can not grab frame")
|
||||
// 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())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
width := dst.Bounds().Dx()
|
||||
height := dst.Bounds().Dy()
|
||||
|
||||
// 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) int {
|
||||
return 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
|
||||
if width%2 != 0 {
|
||||
return fmt.Errorf("width must be even for YUY2")
|
||||
}
|
||||
|
||||
c.camera.Release()
|
||||
c.camera = nil
|
||||
return
|
||||
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
|
||||
}
|
||||
|
||||
298
camera/camera_android.go
Normal file
298
camera/camera_android.go
Normal file
@@ -0,0 +1,298 @@
|
||||
//go:build android
|
||||
|
||||
// Package camera.
|
||||
package camera
|
||||
|
||||
/*
|
||||
#include <android/log.h>
|
||||
|
||||
#include <media/NdkImageReader.h>
|
||||
|
||||
#include <camera/NdkCameraDevice.h>
|
||||
#include <camera/NdkCameraManager.h>
|
||||
|
||||
#define TAG "camera"
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
|
||||
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
|
||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
|
||||
|
||||
AImage *image;
|
||||
AImageReader *imageReader;
|
||||
|
||||
ANativeWindow *nativeWindow;
|
||||
|
||||
ACameraDevice *cameraDevice;
|
||||
ACameraManager *cameraManager;
|
||||
ACameraOutputTarget *cameraOutputTarget;
|
||||
ACameraCaptureSession *cameraCaptureSession;
|
||||
|
||||
ACaptureRequest *captureRequest;
|
||||
ACaptureSessionOutput *captureSessionOutput;
|
||||
ACaptureSessionOutputContainer *captureSessionOutputContainer;
|
||||
|
||||
void device_on_disconnected(void *context, ACameraDevice *device) {
|
||||
LOGI("camera %s is disconnected.\n", ACameraDevice_getId(device));
|
||||
}
|
||||
|
||||
void device_on_error(void *context, ACameraDevice *device, int error) {
|
||||
LOGE("error %d on camera %s.\n", error, ACameraDevice_getId(device));
|
||||
}
|
||||
|
||||
ACameraDevice_stateCallbacks deviceStateCallbacks = {
|
||||
.context = NULL,
|
||||
.onDisconnected = device_on_disconnected,
|
||||
.onError = device_on_error,
|
||||
};
|
||||
|
||||
void session_on_ready(void *context, ACameraCaptureSession *session) {
|
||||
LOGI("session is ready. %p\n", session);
|
||||
}
|
||||
|
||||
void session_on_active(void *context, ACameraCaptureSession *session) {
|
||||
LOGI("session is activated. %p\n", session);
|
||||
}
|
||||
|
||||
void session_on_closed(void *context, ACameraCaptureSession *session) {
|
||||
LOGI("session is closed. %p\n", session);
|
||||
}
|
||||
|
||||
ACameraCaptureSession_stateCallbacks captureSessionStateCallbacks = {
|
||||
.context = NULL,
|
||||
.onActive = session_on_active,
|
||||
.onReady = session_on_ready,
|
||||
.onClosed = session_on_closed,
|
||||
};
|
||||
|
||||
void image_callback(void *context, AImageReader *reader) {
|
||||
LOGD("image_callback");
|
||||
|
||||
media_status_t status = AImageReader_acquireLatestImage(reader, &image);
|
||||
if(status != AMEDIA_OK) {
|
||||
LOGE("failed to acquire next image (reason: %d).\n", status);
|
||||
}
|
||||
}
|
||||
|
||||
AImageReader_ImageListener imageListener = {
|
||||
.context = NULL,
|
||||
.onImageAvailable = image_callback,
|
||||
};
|
||||
|
||||
int openCamera(int index, int width, int height) {
|
||||
ACameraIdList *cameraIdList;
|
||||
const char *selectedCameraId;
|
||||
|
||||
camera_status_t status = ACAMERA_OK;
|
||||
|
||||
cameraManager = ACameraManager_create();
|
||||
|
||||
status = ACameraManager_getCameraIdList(cameraManager, &cameraIdList);
|
||||
if(status != ACAMERA_OK) {
|
||||
LOGE("failed to get camera id list (reason: %d).\n", status);
|
||||
return status;
|
||||
}
|
||||
|
||||
if(cameraIdList->numCameras < 1) {
|
||||
LOGE("no camera device detected.\n");
|
||||
}
|
||||
|
||||
if(cameraIdList->numCameras < index+1) {
|
||||
LOGE("no camera at index %d.\n", index);
|
||||
}
|
||||
|
||||
selectedCameraId = cameraIdList->cameraIds[index];
|
||||
LOGI("open camera (id: %s, num of cameras: %d).\n", selectedCameraId, cameraIdList->numCameras);
|
||||
|
||||
status = ACameraManager_openCamera(cameraManager, selectedCameraId, &deviceStateCallbacks, &cameraDevice);
|
||||
if(status != ACAMERA_OK) {
|
||||
LOGE("failed to open camera device (id: %s)\n", selectedCameraId);
|
||||
return status;
|
||||
}
|
||||
|
||||
status = ACameraDevice_createCaptureRequest(cameraDevice, TEMPLATE_STILL_CAPTURE, &captureRequest);
|
||||
if(status != ACAMERA_OK) {
|
||||
LOGE("failed to create snapshot capture request (id: %s)\n", selectedCameraId);
|
||||
return status;
|
||||
}
|
||||
|
||||
status = ACaptureSessionOutputContainer_create(&captureSessionOutputContainer);
|
||||
if(status != ACAMERA_OK) {
|
||||
LOGE("failed to create session output container (id: %s)\n", selectedCameraId);
|
||||
return status;
|
||||
}
|
||||
|
||||
media_status_t mstatus = AImageReader_new(width, height, AIMAGE_FORMAT_YUV_420_888, 2, &imageReader);
|
||||
if(mstatus != AMEDIA_OK) {
|
||||
LOGE("failed to create image reader (reason: %d).\n", mstatus);
|
||||
return mstatus;
|
||||
}
|
||||
|
||||
mstatus = AImageReader_setImageListener(imageReader, &imageListener);
|
||||
if(mstatus != AMEDIA_OK) {
|
||||
LOGE("failed to set image listener (reason: %d).\n", mstatus);
|
||||
return mstatus;
|
||||
}
|
||||
|
||||
AImageReader_getWindow(imageReader, &nativeWindow);
|
||||
ANativeWindow_acquire(nativeWindow);
|
||||
|
||||
ACameraOutputTarget_create(nativeWindow, &cameraOutputTarget);
|
||||
ACaptureRequest_addTarget(captureRequest, cameraOutputTarget);
|
||||
|
||||
ACaptureSessionOutput_create(nativeWindow, &captureSessionOutput);
|
||||
ACaptureSessionOutputContainer_add(captureSessionOutputContainer, captureSessionOutput);
|
||||
|
||||
status = ACameraDevice_createCaptureSession(cameraDevice, captureSessionOutputContainer, &captureSessionStateCallbacks, &cameraCaptureSession);
|
||||
if(status != ACAMERA_OK) {
|
||||
LOGE("failed to create capture session (reason: %d).\n", status);
|
||||
return status;
|
||||
}
|
||||
|
||||
ACameraManager_deleteCameraIdList(cameraIdList);
|
||||
ACameraManager_delete(cameraManager);
|
||||
|
||||
return ACAMERA_OK;
|
||||
}
|
||||
|
||||
int captureCamera() {
|
||||
camera_status_t status = ACameraCaptureSession_capture(cameraCaptureSession, NULL, 1, &captureRequest, NULL);
|
||||
if(status != ACAMERA_OK) {
|
||||
LOGE("failed to capture image (reason: %d).\n", status);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
int closeCamera() {
|
||||
camera_status_t status = ACAMERA_OK;
|
||||
|
||||
if(captureRequest != NULL) {
|
||||
ACaptureRequest_free(captureRequest);
|
||||
captureRequest = NULL;
|
||||
}
|
||||
|
||||
if(cameraOutputTarget != NULL) {
|
||||
ACameraOutputTarget_free(cameraOutputTarget);
|
||||
cameraOutputTarget = NULL;
|
||||
}
|
||||
|
||||
if(cameraDevice != NULL) {
|
||||
status = ACameraDevice_close(cameraDevice);
|
||||
|
||||
if(status != ACAMERA_OK) {
|
||||
LOGE("failed to close camera device.\n");
|
||||
return status;
|
||||
}
|
||||
|
||||
cameraDevice = NULL;
|
||||
}
|
||||
|
||||
if(captureSessionOutput != NULL) {
|
||||
ACaptureSessionOutput_free(captureSessionOutput);
|
||||
captureSessionOutput = NULL;
|
||||
}
|
||||
|
||||
if(captureSessionOutputContainer != NULL) {
|
||||
ACaptureSessionOutputContainer_free(captureSessionOutputContainer);
|
||||
captureSessionOutputContainer = NULL;
|
||||
}
|
||||
|
||||
if(imageReader != NULL) {
|
||||
AImageReader_delete(imageReader);
|
||||
imageReader = NULL;
|
||||
}
|
||||
|
||||
if(image != NULL) {
|
||||
AImage_delete(image);
|
||||
image = NULL;
|
||||
}
|
||||
|
||||
LOGI("camera closed.\n");
|
||||
return ACAMERA_OK;
|
||||
}
|
||||
|
||||
int openCamera(int index, int width, int height);
|
||||
int captureCamera();
|
||||
int closeCamera();
|
||||
|
||||
#cgo android CFLAGS: -D__ANDROID_API__=24
|
||||
#cgo android LDFLAGS: -lcamera2ndk -lmediandk -llog -landroid
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Camera represents camera.
|
||||
type Camera struct {
|
||||
opts Options
|
||||
img *image.YCbCr
|
||||
}
|
||||
|
||||
// New returns new Camera for given camera index.
|
||||
func New(opts Options) (camera *Camera, err error) {
|
||||
camera = &Camera{}
|
||||
camera.opts = opts
|
||||
|
||||
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 int(ret) != 0 {
|
||||
err = fmt.Errorf("camera: can not open camera %d: error %d", opts.Index, int(ret))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Read reads next frame from camera and returns image.
|
||||
func (c *Camera) Read() (img image.Image, err error) {
|
||||
ret := C.captureCamera()
|
||||
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
|
||||
}
|
||||
|
||||
var yStride C.int
|
||||
var yLen, cbLen, crLen C.int
|
||||
var yPtr, cbPtr, crPtr *C.uint8_t
|
||||
|
||||
C.AImage_getPlaneRowStride(C.image, 0, &yStride)
|
||||
C.AImage_getPlaneData(C.image, 0, &yPtr, &yLen)
|
||||
C.AImage_getPlaneData(C.image, 1, &cbPtr, &cbLen)
|
||||
C.AImage_getPlaneData(C.image, 2, &crPtr, &crLen)
|
||||
|
||||
c.img.YStride = int(yStride)
|
||||
c.img.CStride = int(yStride) / 2
|
||||
|
||||
c.img.Y = C.GoBytes(unsafe.Pointer(yPtr), yLen)
|
||||
c.img.Cb = C.GoBytes(unsafe.Pointer(cbPtr), cbLen)
|
||||
c.img.Cr = C.GoBytes(unsafe.Pointer(crPtr), crLen)
|
||||
|
||||
img = c.img
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Close closes camera.
|
||||
func (c *Camera) Close() (err error) {
|
||||
ret := C.closeCamera()
|
||||
if int(ret) != 0 {
|
||||
err = fmt.Errorf("camera: can not close camera %d: error %d", c.opts.Index, int(ret))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
166
camera/camera_linux.go
Normal file
166
camera/camera_linux.go
Normal file
@@ -0,0 +1,166 @@
|
||||
//go:build !opencv && !android
|
||||
|
||||
// Package camera.
|
||||
package camera
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"slices"
|
||||
|
||||
"github.com/korandiz/v4l"
|
||||
|
||||
im "github.com/gen2brain/cam2ip/image"
|
||||
)
|
||||
|
||||
// Camera represents camera.
|
||||
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) (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
|
||||
}
|
||||
|
||||
c.camera, err = v4l.Open(devices[opts.Index].Path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("camera: %w", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if c.camera == nil {
|
||||
err = fmt.Errorf("camera: can not open camera %d", opts.Index)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
configs, e := c.camera.ListConfigs()
|
||||
if e != nil {
|
||||
err = fmt.Errorf("camera: can not list configs: %w", e)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
formats := make([]uint32, 0)
|
||||
for _, config := range configs {
|
||||
formats = append(formats, config.Format)
|
||||
}
|
||||
|
||||
c.config, err = c.camera.GetConfig()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("camera: can not get config: %w", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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: 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
|
||||
}
|
||||
|
||||
// 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: format %d: can not grab frame: %w", c.config.Format, err)
|
||||
|
||||
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)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Close closes camera.
|
||||
func (c *Camera) Close() (err error) {
|
||||
if c.camera == nil {
|
||||
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
102
camera/camera_opencv.go
Normal 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
|
||||
}
|
||||
@@ -2,50 +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 := NewCamera(1)
|
||||
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)
|
||||
|
||||
var width, height float64 = 640, 480
|
||||
camera.SetProperty(PropFrameWidth, width)
|
||||
camera.SetProperty(PropFrameHeight, height)
|
||||
|
||||
if camera.GetProperty(PropFrameWidth) != width {
|
||||
t.Error("FrameWidth not correct")
|
||||
}
|
||||
|
||||
if camera.GetProperty(PropFrameHeight) != height {
|
||||
t.Error("FrameHeight not correct")
|
||||
}
|
||||
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
|
||||
@@ -55,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)
|
||||
}
|
||||
|
||||
427
camera/camera_windows.go
Normal file
427
camera/camera_windows.go
Normal file
@@ -0,0 +1,427 @@
|
||||
//go:build !opencv
|
||||
|
||||
// Package camera.
|
||||
package camera
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
im "github.com/gen2brain/cam2ip/image"
|
||||
)
|
||||
|
||||
func init() {
|
||||
runtime.LockOSThread()
|
||||
}
|
||||
|
||||
// Camera represents camera.
|
||||
type Camera struct {
|
||||
opts Options
|
||||
camera syscall.Handle
|
||||
rgba *image.RGBA
|
||||
ycbcr *image.YCbCr
|
||||
hdr *videoHdr
|
||||
instance syscall.Handle
|
||||
className string
|
||||
format uint32
|
||||
}
|
||||
|
||||
// New returns new Camera for given camera index.
|
||||
func New(opts Options) (camera *Camera, err error) {
|
||||
camera = &Camera{}
|
||||
camera.opts = opts
|
||||
camera.className = "capWindowClass"
|
||||
|
||||
go func(c *Camera) {
|
||||
fn := func(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
|
||||
switch msg {
|
||||
case wmClose:
|
||||
_ = 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, 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
|
||||
}
|
||||
|
||||
c.camera, err = capCreateCaptureWindow("", wsChild, 0, 0, int64(c.opts.Width), int64(c.opts.Height), hwnd, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ret := sendMessage(c.camera, wmCapDriverConnect, uintptr(c.opts.Index), 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)))
|
||||
|
||||
bi.BmiHeader.BiWidth = int32(c.opts.Width)
|
||||
bi.BmiHeader.BiHeight = int32(c.opts.Height)
|
||||
|
||||
ret = sendMessage(c.camera, wmCapSetVideoformat, size, uintptr(unsafe.Pointer(&bi)))
|
||||
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))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Read reads next frame from camera and returns image.
|
||||
func (c *Camera) Read() (img image.Image, err error) {
|
||||
ret := sendMessage(c.camera, wmCapGrabFrame, 0, 0)
|
||||
if int(ret) == 0 {
|
||||
err = fmt.Errorf("camera: can not grab frame")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
data := unsafe.Slice((*byte)(unsafe.Pointer(c.hdr.LpData)), c.hdr.DwBufferLength)
|
||||
|
||||
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)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
sendMessage(c.camera, wmCapSetCallbackFrame, 0, 0)
|
||||
unregisterClass(c.className, c.instance)
|
||||
sendMessage(c.camera, wmCapDriverDisconnect, 0, 0)
|
||||
|
||||
return destroyWindow(c.camera)
|
||||
}
|
||||
|
||||
// callback function.
|
||||
func (c *Camera) callback(hwnd syscall.Handle, hdr *videoHdr) uintptr {
|
||||
if hdr != nil {
|
||||
c.hdr = hdr
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
var (
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
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")
|
||||
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")
|
||||
)
|
||||
|
||||
const (
|
||||
wmDestroy = 0x0002
|
||||
wmClose = 0x0010
|
||||
wmUser = 0x0400
|
||||
|
||||
wmCapStart = wmUser
|
||||
wmCapSetCallbackFrame = wmCapStart + 5
|
||||
wmCapDriverConnect = wmCapStart + 10
|
||||
wmCapDriverDisconnect = wmCapStart + 11
|
||||
wmCapGetVideoformat = wmCapStart + 44
|
||||
wmCapSetVideoformat = wmCapStart + 45
|
||||
wmCapSetPreview = wmCapStart + 50
|
||||
wmCapSetOverlay = wmCapStart + 51
|
||||
wmCapGrabFrame = wmCapStart + 60
|
||||
wmCapGrabFrameNoStop = wmCapStart + 61
|
||||
wmCapStop = wmCapStart + 68
|
||||
wmCapAbort = wmCapStart + 69
|
||||
|
||||
wsChild = 0x40000000
|
||||
wsOverlappedWindow = 0x00CF0000
|
||||
|
||||
cwUseDefault = 0x7fffffff
|
||||
)
|
||||
|
||||
// wndClassExW https://msdn.microsoft.com/en-us/library/windows/desktop/ms633577.aspx
|
||||
type wndClassExW struct {
|
||||
size uint32
|
||||
style uint32
|
||||
wndProc uintptr
|
||||
clsExtra int32
|
||||
wndExtra int32
|
||||
instance syscall.Handle
|
||||
icon syscall.Handle
|
||||
cursor syscall.Handle
|
||||
background syscall.Handle
|
||||
menuName *uint16
|
||||
className *uint16
|
||||
iconSm syscall.Handle
|
||||
}
|
||||
|
||||
// msgW https://msdn.microsoft.com/en-us/library/windows/desktop/ms644958.aspx
|
||||
type msgW struct {
|
||||
hwnd syscall.Handle
|
||||
message uint32
|
||||
wParam uintptr
|
||||
lParam uintptr
|
||||
time uint32
|
||||
pt pointW
|
||||
}
|
||||
|
||||
// https://msdn.microsoft.com/en-us/ecb0f0e1-90c2-48ab-a069-552262b49c7c
|
||||
type pointW struct {
|
||||
x, y int32
|
||||
}
|
||||
|
||||
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183376.aspx
|
||||
type bitmapInfoHeader struct {
|
||||
BiSize uint32
|
||||
BiWidth int32
|
||||
BiHeight int32
|
||||
BiPlanes uint16
|
||||
BiBitCount uint16
|
||||
BiCompression uint32
|
||||
BiSizeImage uint32
|
||||
BiXPelsPerMeter int32
|
||||
BiYPelsPerMeter int32
|
||||
BiClrUsed uint32
|
||||
BiClrImportant uint32
|
||||
}
|
||||
|
||||
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183375.aspx
|
||||
type bitmapInfo struct {
|
||||
BmiHeader bitmapInfoHeader
|
||||
BmiColors *rgbQuad
|
||||
}
|
||||
|
||||
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd162938.aspx
|
||||
type rgbQuad struct {
|
||||
RgbBlue byte
|
||||
RgbGreen byte
|
||||
RgbRed byte
|
||||
RgbReserved byte
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/vfw/ns-vfw-videohdr_tag
|
||||
type videoHdr struct {
|
||||
LpData *uint8
|
||||
DwBufferLength uint32
|
||||
DwBytesUsed uint32
|
||||
DwTimeCaptured uint32
|
||||
DwUser uint64
|
||||
DwFlags uint32
|
||||
DwReserved [4]uint64
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/libloaderapi/nf-libloaderapi-getmodulehandlea
|
||||
func getModuleHandle() (syscall.Handle, error) {
|
||||
ret, _, err := getModuleHandleW.Call(uintptr(0))
|
||||
if ret == 0 {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return syscall.Handle(ret), nil
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-createwindowexw
|
||||
func createWindow(exStyle uint64, className, windowName string, style uint64, x, y, width, height int64,
|
||||
parent, menu, instance syscall.Handle) (syscall.Handle, error) {
|
||||
ret, _, err := createWindowExW.Call(uintptr(exStyle), uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(className))),
|
||||
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(windowName))), uintptr(style), uintptr(x), uintptr(y),
|
||||
uintptr(width), uintptr(height), uintptr(parent), uintptr(menu), uintptr(instance), uintptr(0))
|
||||
|
||||
if ret == 0 {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return syscall.Handle(ret), nil
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-destroywindow
|
||||
func destroyWindow(hwnd syscall.Handle) error {
|
||||
ret, _, err := destroyWindowW.Call(uintptr(hwnd))
|
||||
if ret == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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), wparam, lparam)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-dispatchmessagew
|
||||
func dispatchMessage(msg *msgW) {
|
||||
dispatchMessageW.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))
|
||||
if int32(ret) == -1 {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return int32(ret) != 0, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-postquitmessage
|
||||
func postQuitMessage(exitCode int32) {
|
||||
postQuitMessageW.Call(uintptr(exitCode))
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-registerclassexw
|
||||
func registerClass(className string, instance syscall.Handle, fn interface{}) error {
|
||||
var wcx wndClassExW
|
||||
wcx.size = uint32(unsafe.Sizeof(wcx))
|
||||
wcx.wndProc = syscall.NewCallback(fn)
|
||||
wcx.instance = instance
|
||||
wcx.className = syscall.StringToUTF16Ptr(className)
|
||||
|
||||
ret, _, err := registerClassExW.Call(uintptr(unsafe.Pointer(&wcx)))
|
||||
if ret == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/vfw/nf-vfw-capcreatecapturewindoww
|
||||
func capCreateCaptureWindow(lpszWindowName string, dwStyle, x, y, width, height int64, parent syscall.Handle, id int64) (syscall.Handle, error) {
|
||||
ret, _, err := capCreateCaptureWindowW.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(lpszWindowName))),
|
||||
uintptr(dwStyle), uintptr(x), uintptr(y), uintptr(width), uintptr(height), uintptr(parent), uintptr(id))
|
||||
if ret == 0 {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return syscall.Handle(ret), nil
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package camera
|
||||
|
||||
import (
|
||||
"image"
|
||||
//"image/jpeg"
|
||||
"io"
|
||||
|
||||
jpeg "github.com/kjk/golibjpegturbo"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
90
cmd/cam2ip/main.go
Normal file
90
cmd/cam2ip/main.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"go.senan.xyz/flagconf"
|
||||
|
||||
"github.com/gen2brain/cam2ip/camera"
|
||||
"github.com/gen2brain/cam2ip/server"
|
||||
)
|
||||
|
||||
const (
|
||||
name = "cam2ip"
|
||||
version = "1.6"
|
||||
)
|
||||
|
||||
func main() {
|
||||
srv := server.NewServer()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
_ = flagconf.ParseEnv()
|
||||
|
||||
srv.Name = name
|
||||
srv.Version = version
|
||||
|
||||
if srv.Htpasswd != "" {
|
||||
if _, err := os.Stat(srv.Htpasswd); err != nil {
|
||||
stderr("%s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
stderr("%s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
srv.Reader = cam
|
||||
|
||||
defer srv.Reader.Close()
|
||||
|
||||
stderr("Listening on %s\n", srv.Bind)
|
||||
|
||||
err = srv.ListenAndServe()
|
||||
if err != nil {
|
||||
stderr("%s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func stderr(format string, a ...any) {
|
||||
_, _ = fmt.Fprintf(os.Stderr, format, a...)
|
||||
}
|
||||
25
go.mod
Normal file
25
go.mod
Normal file
@@ -0,0 +1,25 @@
|
||||
module github.com/gen2brain/cam2ip
|
||||
|
||||
require (
|
||||
github.com/abbot/go-http-auth v0.4.0
|
||||
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/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
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/tetratelabs/wazero v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
)
|
||||
|
||||
go 1.23
|
||||
|
||||
toolchain go1.24.3
|
||||
34
go.sum
Normal file
34
go.sum
Normal file
@@ -0,0 +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/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/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/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/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/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
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=
|
||||
@@ -1,4 +1,3 @@
|
||||
// Package handlers.
|
||||
package handlers
|
||||
|
||||
import (
|
||||
@@ -13,20 +12,13 @@ type HTML struct {
|
||||
}
|
||||
|
||||
// NewHTML returns new HTML handler.
|
||||
func NewHTML(bind string, width, height float64, gl bool) *HTML {
|
||||
func NewHTML(width, height float64, noWebGL bool) *HTML {
|
||||
h := &HTML{}
|
||||
|
||||
b := strings.Split(bind, ":")
|
||||
if b[0] == "" {
|
||||
bind = "127.0.0.1" + bind
|
||||
tpl := htmlWebGL
|
||||
if noWebGL {
|
||||
tpl = html
|
||||
}
|
||||
|
||||
tpl := html
|
||||
if gl {
|
||||
tpl = htmlWebGL
|
||||
}
|
||||
|
||||
tpl = strings.Replace(tpl, "{BIND}", bind, -1)
|
||||
tpl = strings.Replace(tpl, "{WIDTH}", fmt.Sprintf("%.0f", width), -1)
|
||||
tpl = strings.Replace(tpl, "{HEIGHT}", fmt.Sprintf("%.0f", height), -1)
|
||||
|
||||
@@ -39,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>
|
||||
@@ -52,7 +45,11 @@ var html = `<html>
|
||||
<meta charset="utf-8"/>
|
||||
<title>cam2ip</title>
|
||||
<script>
|
||||
ws = new WebSocket("ws://{BIND}/socket");
|
||||
if (location.protocol === 'https:') {
|
||||
ws = new WebSocket("wss://" + window.location.host + "/socket");
|
||||
} else {
|
||||
ws = new WebSocket("ws://" + window.location.host + "/socket");
|
||||
}
|
||||
var image = new Image();
|
||||
|
||||
ws.onopen = function() {
|
||||
@@ -85,11 +82,15 @@ var htmlWebGL = `<html>
|
||||
<script>
|
||||
var texture, vloc, tloc, vertexBuff, textureBuff;
|
||||
|
||||
ws = new WebSocket("ws://{BIND}/socket");
|
||||
if (location.protocol === 'https:') {
|
||||
ws = new WebSocket("wss://" + window.location.host + "/socket");
|
||||
} else {
|
||||
ws = new WebSocket("ws://" + window.location.host + "/socket");
|
||||
}
|
||||
var image = new Image();
|
||||
|
||||
ws.onopen = function() {
|
||||
var gl = document.getElementById('canvas').getContext('webgl');
|
||||
var gl = document.getElementById('canvas').getContext('webgl',{antialias:false}) || canvas.getContext('experimental-webgl');
|
||||
|
||||
var vertexShaderSrc =
|
||||
"attribute vec2 aVertex;" +
|
||||
|
||||
34
handlers/index.go
Normal file
34
handlers/index.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Package handlers provides HTTP handlers for the cam2ip application.
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Index handler.
|
||||
type Index struct {
|
||||
}
|
||||
|
||||
// NewIndex returns new Index handler.
|
||||
func NewIndex() *Index {
|
||||
return &Index{}
|
||||
}
|
||||
|
||||
// ServeHTTP handles requests on incoming connections.
|
||||
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>
|
||||
<head><title>cam2ip</title></head>
|
||||
<body>
|
||||
<h1>cam2ip</h1>
|
||||
<p><a href='/html'>html</a></p>
|
||||
<p><a href='/jpeg'>jpeg</a></p>
|
||||
<p><a href='/mjpeg'>mjpeg</a></p>
|
||||
</body>
|
||||
</html>`))
|
||||
}
|
||||
@@ -4,23 +4,25 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gen2brain/cam2ip/camera"
|
||||
"github.com/gen2brain/cam2ip/image"
|
||||
)
|
||||
|
||||
// JPEG handler.
|
||||
type JPEG struct {
|
||||
camera *camera.Camera
|
||||
reader ImageReader
|
||||
quality int
|
||||
}
|
||||
|
||||
// NewJPEG returns new JPEG handler.
|
||||
func NewJPEG(camera *camera.Camera) *JPEG {
|
||||
return &JPEG{camera}
|
||||
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" {
|
||||
if r.Method != "GET" && r.Method != "HEAD" {
|
||||
http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -28,17 +30,19 @@ func (j *JPEG) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Cache-Control", "no-store, no-cache")
|
||||
w.Header().Add("Content-Type", "image/jpeg")
|
||||
|
||||
img, err := j.camera.Read()
|
||||
img, err := j.reader.Read()
|
||||
if err != nil {
|
||||
log.Printf("jpeg: read: %v", err)
|
||||
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
enc := camera.NewEncoder(w)
|
||||
|
||||
err = enc.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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Package handlers.
|
||||
package handlers
|
||||
|
||||
import (
|
||||
@@ -9,40 +8,42 @@ import (
|
||||
"net/textproto"
|
||||
"time"
|
||||
|
||||
"github.com/gen2brain/cam2ip/camera"
|
||||
"github.com/gen2brain/cam2ip/image"
|
||||
)
|
||||
|
||||
// MJPEG handler.
|
||||
type MJPEG struct {
|
||||
camera *camera.Camera
|
||||
delay int
|
||||
reader ImageReader
|
||||
delay int
|
||||
quality int
|
||||
}
|
||||
|
||||
// NewMJPEG returns new MJPEG handler.
|
||||
func NewMJPEG(camera *camera.Camera, delay int) *MJPEG {
|
||||
return &MJPEG{camera, 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" {
|
||||
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:
|
||||
@@ -55,23 +56,23 @@ loop:
|
||||
continue
|
||||
}
|
||||
|
||||
img, err := m.camera.Read()
|
||||
img, err := m.reader.Read()
|
||||
if err != nil {
|
||||
log.Printf("mjpeg: read: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
enc := camera.NewEncoder(partWriter)
|
||||
|
||||
err = enc.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()
|
||||
}
|
||||
|
||||
14
handlers/reader.go
Normal file
14
handlers/reader.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"image"
|
||||
)
|
||||
|
||||
// ImageReader interface
|
||||
type ImageReader interface {
|
||||
// Read reads next frame from camera/video and returns image.
|
||||
Read() (img image.Image, err error)
|
||||
|
||||
// Close closes camera/video.
|
||||
Close() error
|
||||
}
|
||||
@@ -2,52 +2,65 @@ package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/websocket"
|
||||
"github.com/coder/websocket"
|
||||
|
||||
"github.com/gen2brain/cam2ip/camera"
|
||||
"github.com/gen2brain/cam2ip/image"
|
||||
)
|
||||
|
||||
// Socket handler.
|
||||
type Socket struct {
|
||||
camera *camera.Camera
|
||||
delay int
|
||||
reader ImageReader
|
||||
delay int
|
||||
quality int
|
||||
}
|
||||
|
||||
// NewSocket returns new socket handler.
|
||||
func NewSocket(camera *camera.Camera, delay int) websocket.Handler {
|
||||
s := &Socket{camera, delay}
|
||||
return websocket.Handler(s.write)
|
||||
func NewSocket(reader ImageReader, delay, quality int) *Socket {
|
||||
return &Socket{reader, delay, quality}
|
||||
}
|
||||
|
||||
// write writes images to socket
|
||||
func (s *Socket) write(ws *websocket.Conn) {
|
||||
// ServeHTTP handles requests on incoming connections.
|
||||
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
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
for {
|
||||
img, err := s.camera.Read()
|
||||
img, err := s.reader.Read()
|
||||
if err != nil {
|
||||
log.Printf("socket: read: %v", err)
|
||||
continue
|
||||
break
|
||||
}
|
||||
|
||||
w := new(bytes.Buffer)
|
||||
enc := camera.NewEncoder(w)
|
||||
|
||||
err = enc.Encode(img)
|
||||
err = image.NewEncoder(w, s.quality).Encode(img)
|
||||
if err != nil {
|
||||
log.Printf("socket: encode: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
b64 := base64.StdEncoding.EncodeToString(w.Bytes())
|
||||
b64 := image.EncodeToString(w.Bytes())
|
||||
|
||||
_, err = ws.Write([]byte(b64))
|
||||
err = conn.Write(ctx, websocket.MessageText, []byte(b64))
|
||||
if err != nil {
|
||||
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, "")
|
||||
}
|
||||
|
||||
11
image/base64.go
Normal file
11
image/base64.go
Normal file
@@ -0,0 +1,11 @@
|
||||
//go:build !amd64
|
||||
|
||||
package image
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
func EncodeToString(src []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(src)
|
||||
}
|
||||
11
image/base64_amd64.go
Normal file
11
image/base64_amd64.go
Normal file
@@ -0,0 +1,11 @@
|
||||
//go:build amd64
|
||||
|
||||
package image
|
||||
|
||||
import (
|
||||
"github.com/gen2brain/base64"
|
||||
)
|
||||
|
||||
func EncodeToString(src []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(src)
|
||||
}
|
||||
25
image/decode.go
Normal file
25
image/decode.go
Normal file
@@ -0,0 +1,25 @@
|
||||
//go:build !libjpeg && !jpegli
|
||||
|
||||
// Package image.
|
||||
package image
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
)
|
||||
|
||||
// NewDecoder returns a new Decoder.
|
||||
func NewDecoder(r io.Reader) *Decoder {
|
||||
return &Decoder{r}
|
||||
}
|
||||
|
||||
// Decoder struct.
|
||||
type Decoder struct {
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
// Decode decodes image from JPEG.
|
||||
func (d Decoder) Decode() (image.Image, error) {
|
||||
return jpeg.Decode(d.r)
|
||||
}
|
||||
31
image/decode_jpegli.go
Normal file
31
image/decode_jpegli.go
Normal 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,
|
||||
})
|
||||
}
|
||||
30
image/decode_libjpeg.go
Normal file
30
image/decode_libjpeg.go
Normal file
@@ -0,0 +1,30 @@
|
||||
//go:build libjpeg
|
||||
|
||||
// Package image.
|
||||
package image
|
||||
|
||||
import (
|
||||
"image"
|
||||
"io"
|
||||
|
||||
"github.com/pixiv/go-libjpeg/jpeg"
|
||||
)
|
||||
|
||||
// NewDecoder returns a new Decoder.
|
||||
func NewDecoder(r io.Reader) *Decoder {
|
||||
return &Decoder{r}
|
||||
}
|
||||
|
||||
// Decoder struct.
|
||||
type Decoder struct {
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
// Decode decodes image from JPEG.
|
||||
func (d Decoder) Decode() (image.Image, error) {
|
||||
return jpeg.Decode(d.r, &jpeg.DecoderOptions{
|
||||
DCTMethod: jpeg.DCTIFast,
|
||||
DisableFancyUpsampling: true,
|
||||
DisableBlockSmoothing: true,
|
||||
})
|
||||
}
|
||||
31
image/encode.go
Normal file
31
image/encode.go
Normal file
@@ -0,0 +1,31 @@
|
||||
//go:build !libjpeg && !jpegli
|
||||
|
||||
// Package image.
|
||||
package image
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
err := jpeg.Encode(e.w, img, &jpeg.Options{Quality: e.quality})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
36
image/encode_jpegli.go
Normal file
36
image/encode_jpegli.go
Normal 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
32
image/encode_libjpeg.go
Normal 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
48
image/image.go
Normal 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
43
image/image_test.go
Normal 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
BIN
image/testdata/test.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
83
make.bash
83
make.bash
@@ -2,39 +2,70 @@
|
||||
|
||||
CHROOT="/usr/x86_64-pc-linux-gnu-static"
|
||||
MINGW="/usr/i686-w64-mingw32"
|
||||
MINGW64="/usr/x86_64-w64-mingw32"
|
||||
RPI="/usr/armv6j-hardfloat-linux-gnueabi"
|
||||
ANDROID="/opt/android-toolchain-arm7"
|
||||
RPI3="/usr/armv7a-hardfloat-linux-gnueabi"
|
||||
APPLE="/usr/x86_64-apple-darwin14"
|
||||
ANDROID="/usr/arm-linux-androideabi"
|
||||
|
||||
mkdir -p build
|
||||
|
||||
LIBRARY_PATH="$CHROOT/usr/lib:$CHROOT/lib" \
|
||||
PKG_CONFIG_PATH="$CHROOT/usr/lib/pkgconfig" \
|
||||
PKG_CONFIG_LIBDIR="$CHROOT/usr/lib/pkgconfig" \
|
||||
CGO_LDFLAGS="-L$CHROOT/usr/lib -L$CHROOT/lib" \
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -v -x -o build/cam2ip.linux.amd64 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip
|
||||
LIBRARY_PATH="$CHROOT/usr/lib64:$CHROOT/lib64" \
|
||||
PKG_CONFIG_PATH="$CHROOT/usr/lib64/pkgconfig" \
|
||||
PKG_CONFIG_LIBDIR="$CHROOT/usr/lib64/pkgconfig" \
|
||||
CGO_LDFLAGS="-L$CHROOT/usr/lib64 -L$CHROOT/lib" \
|
||||
CGO_CFLAGS="-I$CHROOT/usr/include" \
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -tags cv2,turbo -o build/cam2ip.linux.amd64.cv2 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip/cmd/cam2ip
|
||||
|
||||
LIBRARY_PATH="$CHROOT/usr/lib64:$CHROOT/lib64" \
|
||||
PKG_CONFIG_PATH="$CHROOT/usr/lib64/pkgconfig" \
|
||||
PKG_CONFIG_LIBDIR="$CHROOT/usr/lib64/pkgconfig" \
|
||||
CGO_LDFLAGS="-L$CHROOT/usr/lib64 -L$CHROOT/lib64" \
|
||||
CGO_CFLAGS="-I$CHROOT/usr/include" \
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o build/cam2ip.linux.amd64 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip/cmd/cam2ip
|
||||
|
||||
PKG_CONFIG="/usr/bin/i686-w64-mingw32-pkg-config" \
|
||||
PKG_CONFIG_PATH="$MINGW/usr/lib/pkgconfig" \
|
||||
PKG_CONFIG_LIBDIR="$MINGW/usr/lib/pkgconfig" \
|
||||
CGO_LDFLAGS="-L$MINGW/usr/lib" \
|
||||
CGO_CFLAGS="-I$MINGW/usr/include" \
|
||||
CC="i686-w64-mingw32-gcc" CXX="i686-w64-mingw32-g++" \
|
||||
CGO_ENABLED=1 GOOS=windows GOARCH=386 go build -v -x -o build/cam2ip.exe -ldflags "-linkmode external -s -w '-extldflags=-static'" github.com/gen2brain/cam2ip
|
||||
PKG_CONFIG="/usr/bin/x86_64-w64-mingw32-pkg-config" \
|
||||
PKG_CONFIG_PATH="$MINGW64/usr/lib/pkgconfig" \
|
||||
PKG_CONFIG_LIBDIR="$MINGW64/usr/lib/pkgconfig" \
|
||||
CGO_LDFLAGS="-L$MINGW64/usr/lib" \
|
||||
CGO_CFLAGS="-I$MINGW64/usr/include" \
|
||||
CC="x86_64-w64-mingw32-gcc" CXX="x86_64-w64-mingw32-g++" \
|
||||
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -tags cv2,turbo,pkgconfig -o build/cam2ip.amd64.cv2.exe -ldflags "-linkmode external -s -w '-extldflags=-static'" github.com/gen2brain/cam2ip/cmd/cam2ip
|
||||
|
||||
PKG_CONFIG="/usr/bin/armv6j-hardfloat-linux-gnueabi-pkg-config" \
|
||||
PKG_CONFIG_PATH="$RPI/usr/lib/pkgconfig" \
|
||||
PKG_CONFIG_LIBDIR="$RPI/usr/lib/pkgconfig" \
|
||||
CGO_LDFLAGS="-L$RPI/usr/lib" \
|
||||
CGO_CFLAGS="-I$RPI/usr/include" \
|
||||
CC="armv6j-hardfloat-linux-gnueabi-gcc" CXX="armv6j-hardfloat-linux-gnueabi-g++" \
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=arm go build -v -x -o build/cam2ip.linux.arm -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip
|
||||
PKG_CONFIG="/usr/bin/x86_64-w64-mingw32-pkg-config" \
|
||||
PKG_CONFIG_PATH="$MINGW64/usr/lib/pkgconfig" \
|
||||
PKG_CONFIG_LIBDIR="$MINGW64/usr/lib/pkgconfig" \
|
||||
CGO_LDFLAGS="-L$MINGW64/usr/lib" \
|
||||
CGO_CFLAGS="-I$MINGW64/usr/include" \
|
||||
CC="x86_64-w64-mingw32-gcc" CXX="x86_64-w64-mingw32-g++" \
|
||||
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -o build/cam2ip.amd64.exe -ldflags "-linkmode external -s -w '-extldflags=-static'" github.com/gen2brain/cam2ip/cmd/cam2ip
|
||||
|
||||
PKG_CONFIG="/usr/bin/armv7a-hardfloat-linux-gnueabi-pkg-config" \
|
||||
PKG_CONFIG_PATH="$RPI3/usr/lib/pkgconfig" \
|
||||
PKG_CONFIG_LIBDIR="$RPI3/usr/lib/pkgconfig" \
|
||||
CGO_LDFLAGS="-L$RPI3/usr/lib" \
|
||||
CGO_CFLAGS="-I$RPI3/usr/include" \
|
||||
CC="armv7a-hardfloat-linux-gnueabi-gcc" CXX="armv7a-hardfloat-linux-gnueabi-g++" \
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=arm go build -tags cv2,turbo -o build/cam2ip.linux.arm7.cv2 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip/cmd/cam2ip
|
||||
|
||||
PKG_CONFIG="/usr/bin/armv7a-hardfloat-linux-gnueabi-pkg-config" \
|
||||
PKG_CONFIG_PATH="$RPI3/usr/lib/pkgconfig" \
|
||||
PKG_CONFIG_LIBDIR="$RPI3/usr/lib/pkgconfig" \
|
||||
CGO_LDFLAGS="-L$RPI3/usr/lib" \
|
||||
CGO_CFLAGS="-I$RPI3/usr/include" \
|
||||
CC="armv7a-hardfloat-linux-gnueabi-gcc" CXX="armv7a-hardfloat-linux-gnueabi-g++" \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -o build/cam2ip.linux.arm7 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip/cmd/cam2ip
|
||||
|
||||
PKG_CONFIG_PATH="$APPLE/SDK/MacOSX10.10.sdk/usr/lib/pkgconfig" \
|
||||
PKG_CONFIG_LIBDIR="$APPLE/SDK/MacOSX10.10.sdk/usr/lib/pkgconfig" \
|
||||
CGO_LDFLAGS="-L$APPLE/SDK/MacOSX10.10.sdk/usr/lib -mmacosx-version-min=10.10" \
|
||||
CGO_CFLAGS="-I$APPLE/SDK/MacOSX10.10.sdk/usr/include -mmacosx-version-min=10.10" \
|
||||
CC="$APPLE/bin/x86_64-apple-darwin14-clang" CXX="$APPLE/bin/x86_64-apple-darwin14-clang++" \
|
||||
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -tags cv2,turbo -o build/cam2ip.darwin.amd64 -ldflags "-linkmode external -s -w '-extldflags=-mmacosx-version-min=10.10'" github.com/gen2brain/cam2ip/cmd/cam2ip
|
||||
|
||||
PATH="$PATH:$ANDROID/bin" \
|
||||
PKG_CONFIG="$ANDROID/bin/arm-linux-androideabi-pkg-config" \
|
||||
PKG_CONFIG_PATH="$ANDROID/lib/pkgconfig" \
|
||||
PKG_CONFIG_LIBDIR="$ANDROID/lib/pkgconfig" \
|
||||
CGO_LDFLAGS="-L$ANDROID/lib" \
|
||||
CGO_CFLAGS="-I$ANDROID/include" \
|
||||
CC="arm-linux-androideabi-gcc" CXX="arm-linux-androideabi-g++" \
|
||||
CGO_ENABLED=1 GOOS=android GOARCH=arm go build -v -x -o build/cam2ip.android.arm -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip
|
||||
CGO_LDFLAGS="-L$ANDROID/sysroot/usr/lib" \
|
||||
CGO_CFLAGS="-I$ANDROID/sysroot/usr/include --sysroot=$ANDROID/sysroot" \
|
||||
CC="$ANDROID/bin/arm-linux-androideabi-clang" CXX="$ANDROID/bin/arm-linux-androideabi-clang++" \
|
||||
CGO_ENABLED=1 GOOS=android GOARCH=arm go build -o build/cam2ip.android.arm7 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip/cmd/cam2ip
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/abbot/go-http-auth"
|
||||
|
||||
"github.com/gen2brain/cam2ip/camera"
|
||||
"github.com/gen2brain/cam2ip/handlers"
|
||||
)
|
||||
|
||||
@@ -17,21 +17,31 @@ type Server struct {
|
||||
Name string
|
||||
Version string
|
||||
|
||||
Index int
|
||||
Delay int
|
||||
|
||||
Width float64
|
||||
Height float64
|
||||
|
||||
Quality int
|
||||
Rotate int
|
||||
Flip string
|
||||
|
||||
NoWebGL bool
|
||||
|
||||
Timestamp bool
|
||||
TimeFormat string
|
||||
|
||||
Bind string
|
||||
Htpasswd string
|
||||
|
||||
Index int
|
||||
Delay int
|
||||
FrameWidth float64
|
||||
FrameHeight float64
|
||||
WebGL bool
|
||||
|
||||
Camera *camera.Camera
|
||||
Reader handlers.ImageReader
|
||||
}
|
||||
|
||||
// NewServer returns new Server.
|
||||
func NewServer() *Server {
|
||||
s := &Server{}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -43,23 +53,23 @@ func (s *Server) ListenAndServe() error {
|
||||
basic = auth.NewBasicAuthenticator(realm, auth.HtpasswdFileProvider(s.Htpasswd))
|
||||
}
|
||||
|
||||
http.Handle("/html", newAuthHandler(handlers.NewHTML(s.Bind, s.FrameWidth, s.FrameHeight, s.WebGL), basic))
|
||||
http.Handle("/jpeg", newAuthHandler(handlers.NewJPEG(s.Camera), basic))
|
||||
http.Handle("/mjpeg", newAuthHandler(handlers.NewMJPEG(s.Camera, s.Delay), basic))
|
||||
|
||||
http.Handle("/socket", handlers.NewSocket(s.Camera, s.Delay))
|
||||
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)
|
||||
})
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
http.Handle("/", newAuthHandler(handlers.NewIndex(), basic))
|
||||
|
||||
srv := &http.Server{}
|
||||
srv := &http.Server{
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp4", s.Bind)
|
||||
listener, err := net.Listen("tcp", s.Bind)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -74,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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user