mirror of
https://github.com/gen2brain/cam2ip.git
synced 2025-12-15 20:08:30 +00:00
Compare commits
45 Commits
1.6
...
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 |
@@ -10,7 +10,7 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 go build -tags jpeg -o cam2ip -ldflags "-s -w"
|
RUN CGO_ENABLED=0 go build -o cam2ip -trimpath -ldflags "-s -w" github.com/gen2brain/cam2ip/cmd/cam2ip
|
||||||
|
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
|||||||
81
README.md
81
README.md
@@ -4,45 +4,36 @@ Turn any webcam into an IP camera.
|
|||||||
|
|
||||||
Example (in web browser):
|
Example (in web browser):
|
||||||
|
|
||||||
http://localhost:56000/mjpeg
|
http://localhost:56000/html
|
||||||
|
|
||||||
or
|
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
|
### Requirements
|
||||||
|
|
||||||
* [libjpeg-turbo](https://www.libjpeg-turbo.org/) (use `-tags jpeg` to build without `CGo`)
|
|
||||||
* On Linux/RPi native Go [V4L](https://github.com/korandiz/v4l) implementation is used to capture images.
|
* 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.
|
* On Windows [Video for Windows (VfW)](https://en.wikipedia.org/wiki/Video_for_Windows) framework is used over win32 API.
|
||||||
|
|
||||||
### Build tags
|
### Build tags
|
||||||
|
|
||||||
* `cv2` - build with `OpenCV` 2.x ([go-opencv](https://github.com/lazywei/go-opencv))
|
* `opencv` - use `OpenCV` library to access camera ([gocv](https://github.com/hybridgroup/gocv))
|
||||||
* `cv4` - build with `OpenCV` 4.x ([gocv](https://github.com/hybridgroup/gocv))
|
* `libjpeg` - build with `libjpeg` ([go-libjpeg](https://github.com/pixiv/go-libjpeg)) instead of native `image/jpeg`
|
||||||
* `jpeg` - build with native Go `image/jpeg` instead of `libjpeg-turbo`
|
* `jpegli` - build with `jpegli` ([jpegli](https://github.com/gen2brain/jpegli)) instead of native `image/jpeg`
|
||||||
|
|
||||||
### Download
|
### Download
|
||||||
|
|
||||||
Binaries are compiled with static OpenCV/libjpeg-turbo libraries, they should just work:
|
Download the latest binaries from the [releases](https://github.com/gen2brain/cam2ip/releases).
|
||||||
|
|
||||||
- [Linux 64bit](https://github.com/gen2brain/cam2ip/releases/download/1.5/cam2ip-1.5-64bit.tar.gz)
|
|
||||||
- [Linux 64bit OpenCV](https://github.com/gen2brain/cam2ip/releases/download/1.5/cam2ip-1.5-64bit-cv2.tar.gz)
|
|
||||||
- [RPi 32bit](https://github.com/gen2brain/cam2ip/releases/download/1.5/cam2ip-1.5-RPi.tar.gz)
|
|
||||||
- [RPi 32bit OpenCV](https://github.com/gen2brain/cam2ip/releases/download/1.5/cam2ip-1.5-RPi-cv2.tar.gz)
|
|
||||||
- [RPi 32bit Static](https://github.com/gen2brain/cam2ip/releases/download/1.5/cam2ip-1.5-RPi-nocgo.tar.gz)
|
|
||||||
- [RPi3 32bit](https://github.com/gen2brain/cam2ip/releases/download/1.5/cam2ip-1.5-RPi3.tar.gz)
|
|
||||||
- [RPi3 32bit OpenCV](https://github.com/gen2brain/cam2ip/releases/download/1.5/cam2ip-1.5-RPi3-cv2.tar.gz)
|
|
||||||
- [Windows 32bit](https://github.com/gen2brain/cam2ip/releases/download/1.5/cam2ip-1.5-32bit.zip)
|
|
||||||
- [Windows 32bit OpenCV](https://github.com/gen2brain/cam2ip/releases/download/1.5/cam2ip-1.5-32bit-cv2.zip)
|
|
||||||
- [Windows 64bit](https://github.com/gen2brain/cam2ip/releases/download/1.5/cam2ip-1.5-64bit.zip)
|
|
||||||
- [Windows 64bit OpenCV](https://github.com/gen2brain/cam2ip/releases/download/1.5/cam2ip-1.5-64bit-cv2.zip)
|
|
||||||
|
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
go get -v github.com/gen2brain/cam2ip/cmd/cam2ip
|
go install github.com/gen2brain/cam2ip/cmd/cam2ip@latest
|
||||||
|
|
||||||
This will install app in `$GOPATH/bin/cam2ip`.
|
This command will install `cam2ip` in `GOBIN`, you can point `GOBIN` to e.g. `/usr/local/bin` or `~/.local/bin`.
|
||||||
|
|
||||||
### Run in Docker container
|
### Run in Docker container
|
||||||
|
|
||||||
@@ -51,27 +42,31 @@ This will install app in `$GOPATH/bin/cam2ip`.
|
|||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
Usage of cam2ip:
|
Usage: cam2ip [<flags>]
|
||||||
-bind-addr string
|
--index
|
||||||
Bind address [CAM2IP_BIND_ADDR] (default ":56000")
|
Camera index [CAM2IP_INDEX] (default "0")
|
||||||
-delay int
|
--delay
|
||||||
Delay between frames, in milliseconds [CAM2IP_DELAY] (default 10)
|
Delay between frames, in milliseconds [CAM2IP_DELAY] (default "10")
|
||||||
-height float
|
--width
|
||||||
Frame height [CAM2IP_HEIGHT] (default 480)
|
Frame width [CAM2IP_WIDTH] (default "640")
|
||||||
-htpasswd-file string
|
--height
|
||||||
Path to htpasswd file, if empty auth is disabled [CAM2IP_HTPASSWD_FILE]
|
Frame height [CAM2IP_HEIGHT] (default "480")
|
||||||
-index int
|
--quality
|
||||||
Camera index [CAM2IP_INDEX]
|
Image quality [CAM2IP_QUALITY] (default "75")
|
||||||
-nowebgl
|
--rotate
|
||||||
Disable WebGL drawing of images (html handler) [CAM2IP_NOWEBGL]
|
Rotate image, valid values are 90, 180, 270 [CAM2IP_ROTATE] (default "0")
|
||||||
-rotate int
|
--flip
|
||||||
Rotate image, valid values are 90, 180, 270 [CAM2IP_ROTATE]
|
Flip image, valid values are horizontal and vertical [CAM2IP_FLIP] (default "")
|
||||||
-timestamp
|
--no-webgl
|
||||||
Draws timestamp on images [CAM2IP_TIMESTAMP]
|
Disable WebGL drawing of image (html handler) [CAM2IP_NO_WEBGL] (default "false")
|
||||||
-video-file string
|
--timestamp
|
||||||
Use video file instead of camera [CAM2IP_VIDEO_FILE]
|
Draws timestamp on image [CAM2IP_TIMESTAMP] (default "false")
|
||||||
-width float
|
--time-format
|
||||||
Frame width [CAM2IP_WIDTH] (default 640)
|
Time format [CAM2IP_TIME_FORMAT] (default "2006-01-02 15:04:05")
|
||||||
|
--bind-addr
|
||||||
|
Bind address [CAM2IP_BIND_ADDR] (default ":56000")
|
||||||
|
--htpasswd-file
|
||||||
|
Path to htpasswd file, if empty auth is disabled [CAM2IP_HTPASSWD_FILE] (default "")
|
||||||
```
|
```
|
||||||
|
|
||||||
### Handlers
|
### Handlers
|
||||||
|
|||||||
101
camera/camera.go
101
camera/camera.go
@@ -1,10 +1,99 @@
|
|||||||
package camera
|
package camera
|
||||||
|
|
||||||
// Options.
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options .
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Index int
|
Index int
|
||||||
Rotate int
|
Rotate int
|
||||||
Width float64
|
Flip string
|
||||||
Height float64
|
Width float64
|
||||||
Timestamp bool
|
Height float64
|
||||||
|
Timestamp bool
|
||||||
|
TimeFormat string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
yuy2FourCC = fourcc("YUY2")
|
||||||
|
yuyvFourCC = fourcc("YUYV")
|
||||||
|
mjpgFourCC = fourcc("MJPG")
|
||||||
|
)
|
||||||
|
|
||||||
|
func fourcc(b string) uint32 {
|
||||||
|
return uint32(b[0]) | (uint32(b[1]) << 8) | (uint32(b[2]) << 16) | (uint32(b[3]) << 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bmp24ToRgba(data []byte, dst *image.RGBA) error {
|
||||||
|
r := bytes.NewReader(data)
|
||||||
|
|
||||||
|
width := dst.Bounds().Dx()
|
||||||
|
height := dst.Bounds().Dy()
|
||||||
|
|
||||||
|
// There are 3 bytes per pixel, and each row is 4-byte aligned.
|
||||||
|
b := make([]byte, (3*width+3)&^3)
|
||||||
|
|
||||||
|
// BMP images are stored bottom-up rather than top-down.
|
||||||
|
for y := height - 1; y >= 0; y-- {
|
||||||
|
_, err := r.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p := dst.Pix[y*dst.Stride : y*dst.Stride+width*4]
|
||||||
|
for i, j := 0, 0; i < len(p); i, j = i+4, j+3 {
|
||||||
|
// BMP images are stored in BGR order rather than RGB order.
|
||||||
|
p[i+0] = b[j+2]
|
||||||
|
p[i+1] = b[j+1]
|
||||||
|
p[i+2] = b[j+0]
|
||||||
|
p[i+3] = 0xFF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// yuy2ToYCbCr422 converts a YUY2 (YUYV) byte slice to an image.YCbCr with YCbCrSubsampleRatio422 (I422).
|
||||||
|
func yuy2ToYCbCr422(data []byte, dst *image.YCbCr) error {
|
||||||
|
if dst.SubsampleRatio != image.YCbCrSubsampleRatio422 {
|
||||||
|
return fmt.Errorf("subsample ratio must be 422, got %s", dst.SubsampleRatio.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
width := dst.Bounds().Dx()
|
||||||
|
height := dst.Bounds().Dy()
|
||||||
|
|
||||||
|
if width%2 != 0 {
|
||||||
|
return fmt.Errorf("width must be even for YUY2")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) != width*height*2 {
|
||||||
|
return fmt.Errorf("invalid data length for YUY2")
|
||||||
|
}
|
||||||
|
|
||||||
|
stride := width * 2 // 2 bytes per pixel
|
||||||
|
|
||||||
|
for y := 0; y < height; y++ {
|
||||||
|
for x := 0; x < width; x += 2 {
|
||||||
|
idx := y*stride + x*2
|
||||||
|
|
||||||
|
y0 := data[idx+0]
|
||||||
|
cb := data[idx+1]
|
||||||
|
y1 := data[idx+2]
|
||||||
|
cr := data[idx+3]
|
||||||
|
|
||||||
|
// Y plane: every pixel
|
||||||
|
dst.Y[y*dst.YStride+x+0] = y0
|
||||||
|
dst.Y[y*dst.YStride+x+1] = y1
|
||||||
|
|
||||||
|
// Cb/Cr plane: every 2 pixels (422)
|
||||||
|
off := y*dst.CStride + x/2
|
||||||
|
dst.Cb[off] = cb
|
||||||
|
dst.Cr[off] = cr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
// +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
|
|
||||||
)
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// +build !cv2,!cv4
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
// +build cv2,!cv4
|
|
||||||
|
|
||||||
// Package camera.
|
|
||||||
package camera
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"image/color"
|
|
||||||
"image/draw"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pbnjay/pixfont"
|
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
|
||||||
"github.com/gen2brain/go-opencv/opencv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
// +build cv4,!cv2
|
|
||||||
|
|
||||||
// Package camera.
|
|
||||||
package camera
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"image/color"
|
|
||||||
"image/draw"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pbnjay/pixfont"
|
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// +build !cv2,!cv4
|
//go:build !opencv && !android
|
||||||
|
|
||||||
// Package camera.
|
// Package camera.
|
||||||
package camera
|
package camera
|
||||||
@@ -6,14 +6,10 @@ package camera
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"io"
|
||||||
"image/draw"
|
"slices"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
|
||||||
"github.com/korandiz/v4l"
|
"github.com/korandiz/v4l"
|
||||||
"github.com/korandiz/v4l/fmt/mjpeg"
|
|
||||||
"github.com/pbnjay/pixfont"
|
|
||||||
|
|
||||||
im "github.com/gen2brain/cam2ip/image"
|
im "github.com/gen2brain/cam2ip/image"
|
||||||
)
|
)
|
||||||
@@ -22,49 +18,82 @@ import (
|
|||||||
type Camera struct {
|
type Camera struct {
|
||||||
opts Options
|
opts Options
|
||||||
camera *v4l.Device
|
camera *v4l.Device
|
||||||
|
config v4l.DeviceConfig
|
||||||
|
ycbcr *image.YCbCr
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns new Camera for given camera index.
|
// New returns new Camera for given camera index.
|
||||||
func New(opts Options) (camera *Camera, err error) {
|
func New(opts Options) (c *Camera, err error) {
|
||||||
camera = &Camera{}
|
c = &Camera{}
|
||||||
camera.opts = opts
|
c.opts = opts
|
||||||
|
|
||||||
devices := v4l.FindDevices()
|
devices := v4l.FindDevices()
|
||||||
if len(devices) < opts.Index+1 {
|
if len(devices) < opts.Index+1 {
|
||||||
err = fmt.Errorf("camera: no camera at index %d", opts.Index)
|
err = fmt.Errorf("camera: no camera at index %d", opts.Index)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
camera.camera, err = v4l.Open(devices[opts.Index].Path)
|
c.camera, err = v4l.Open(devices[opts.Index].Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("camera: %s", err.Error())
|
err = fmt.Errorf("camera: %w", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if camera.camera == nil {
|
if c.camera == nil {
|
||||||
err = fmt.Errorf("camera: can not open camera %d", opts.Index)
|
err = fmt.Errorf("camera: can not open camera %d", opts.Index)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := camera.camera.GetConfig()
|
configs, e := c.camera.ListConfigs()
|
||||||
if err != nil {
|
if e != nil {
|
||||||
err = fmt.Errorf("camera: %s", err.Error())
|
err = fmt.Errorf("camera: can not list configs: %w", e)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Format = mjpeg.FourCC
|
formats := make([]uint32, 0)
|
||||||
config.Width = int(opts.Width)
|
for _, config := range configs {
|
||||||
config.Height = int(opts.Height)
|
formats = append(formats, config.Format)
|
||||||
|
}
|
||||||
|
|
||||||
err = camera.camera.SetConfig(config)
|
c.config, err = c.camera.GetConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("camera: %s", err.Error())
|
err = fmt.Errorf("camera: can not get config: %w", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = camera.camera.TurnOn()
|
if slices.Contains(formats, mjpgFourCC) {
|
||||||
|
c.config.Format = mjpgFourCC
|
||||||
|
} else if slices.Contains(formats, yuyvFourCC) {
|
||||||
|
c.config.Format = yuyvFourCC
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("camera: unsupported format %d", c.config.Format)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.config.Width = int(opts.Width)
|
||||||
|
c.config.Height = int(opts.Height)
|
||||||
|
|
||||||
|
err = c.camera.SetConfig(c.config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("camera: %s", err.Error())
|
err = fmt.Errorf("camera: format %d: can not set config: %w", c.config.Format, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.config.Format == yuyvFourCC {
|
||||||
|
c.ycbcr = image.NewYCbCr(image.Rect(0, 0, int(c.opts.Width), int(c.opts.Height)), image.YCbCrSubsampleRatio422)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.camera.TurnOn()
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("camera: format %d: can not turn on: %w", c.config.Format, err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,63 +102,65 @@ func New(opts Options) (camera *Camera, err error) {
|
|||||||
|
|
||||||
// Read reads next frame from camera and returns image.
|
// Read reads next frame from camera and returns image.
|
||||||
func (c *Camera) Read() (img image.Image, err error) {
|
func (c *Camera) Read() (img image.Image, err error) {
|
||||||
|
|
||||||
buffer, err := c.camera.Capture()
|
buffer, err := c.camera.Capture()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("camera: can not grab frame: %s", err.Error())
|
err = fmt.Errorf("camera: format %d: can not grab frame: %w", c.config.Format, err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
img, err = im.NewDecoder(buffer).Decode()
|
switch c.config.Format {
|
||||||
if err != nil {
|
case yuy2FourCC, yuyvFourCC:
|
||||||
err = fmt.Errorf("camera: %s", err.Error())
|
data, e := io.ReadAll(buffer)
|
||||||
return
|
if e != nil {
|
||||||
}
|
err = fmt.Errorf("camera: format %d: can not read buffer: %w", c.config.Format, e)
|
||||||
|
|
||||||
switch c.opts.Rotate {
|
|
||||||
case 90:
|
|
||||||
img = imaging.Rotate90(img)
|
|
||||||
case 180:
|
|
||||||
img = imaging.Rotate180(img)
|
|
||||||
case 270:
|
|
||||||
img = imaging.Rotate270(img)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.opts.Timestamp {
|
|
||||||
dimg, ok := img.(draw.Image)
|
|
||||||
if !ok {
|
|
||||||
err = fmt.Errorf("camera: %T is not a drawable image type", img)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pixfont.DrawString(dimg, 10, 10, time.Now().Format("2006-01-02 15:04:05"), color.White)
|
e = yuy2ToYCbCr422(data, c.ycbcr)
|
||||||
img = dimg
|
if e != nil {
|
||||||
|
err = fmt.Errorf("camera: format %d: can not retrieve frame: %w", c.config.Format, e)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
img = c.ycbcr
|
||||||
|
case mjpgFourCC:
|
||||||
|
img, err = im.NewDecoder(buffer).Decode()
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("camera: format %d: can not decode frame: %w", c.config.Format, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.opts.Rotate != 0 {
|
||||||
|
img = im.Rotate(img, c.opts.Rotate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.opts.Flip != "" {
|
||||||
|
img = im.Flip(img, c.opts.Flip)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.opts.Timestamp {
|
||||||
|
img = im.Timestamp(img, c.opts.TimeFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProperty returns the specified camera property.
|
|
||||||
func (c *Camera) GetProperty(id int) float64 {
|
|
||||||
ret, _ := c.camera.GetControl(uint32(id))
|
|
||||||
return float64(ret)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetProperty sets a camera property.
|
|
||||||
func (c *Camera) SetProperty(id int, value float64) {
|
|
||||||
c.camera.SetControl(uint32(id), int32(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes camera.
|
// Close closes camera.
|
||||||
func (c *Camera) Close() (err error) {
|
func (c *Camera) Close() (err error) {
|
||||||
if c.camera == nil {
|
if c.camera == nil {
|
||||||
err = fmt.Errorf("camera: camera is not opened")
|
err = fmt.Errorf("camera: close: camera is not opened")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.camera.TurnOff()
|
c.camera.TurnOff()
|
||||||
|
|
||||||
c.camera.Close()
|
c.camera.Close()
|
||||||
c.camera = nil
|
c.camera = nil
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
102
camera/camera_opencv.go
Normal file
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,38 +2,35 @@ package camera
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/jpeg"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gen2brain/cam2ip/image"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCamera(t *testing.T) {
|
func TestCamera(t *testing.T) {
|
||||||
camera, err := New(Options{0, 0, 640, 480})
|
camera, err := New(Options{0, 0, "", 640, 480, false, ""})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer camera.Close()
|
defer func(camera *Camera) {
|
||||||
|
err := camera.Close()
|
||||||
tmpdir, err := ioutil.TempDir(os.TempDir(), "cam2ip")
|
if err != nil {
|
||||||
if err != nil {
|
t.Error(err)
|
||||||
t.Error(err)
|
}
|
||||||
}
|
}(camera)
|
||||||
|
|
||||||
defer os.RemoveAll(tmpdir)
|
|
||||||
|
|
||||||
var i int
|
var i int
|
||||||
var n int = 10
|
var n = 10
|
||||||
|
|
||||||
timeout := time.After(time.Duration(n) * time.Second)
|
timeout := time.After(time.Duration(n) * time.Second)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-timeout:
|
case <-timeout:
|
||||||
//fmt.Printf("Fps: %d\n", i/n)
|
fmt.Printf("FPS: %.2f\n", float64(i)/float64(n))
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
i += 1
|
i += 1
|
||||||
@@ -43,17 +40,7 @@ func TestCamera(t *testing.T) {
|
|||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Create(filepath.Join(tmpdir, fmt.Sprintf("%03d.jpg", i)))
|
err = image.NewEncoder(io.Discard, 75).Encode(img)
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = jpeg.Encode(file, img, &jpeg.Options{Quality: 75})
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = file.Close()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// +build !cv2,!cv4
|
//go:build !opencv
|
||||||
|
|
||||||
// Package camera.
|
// Package camera.
|
||||||
package camera
|
package camera
|
||||||
@@ -7,25 +7,27 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"runtime"
|
||||||
"image/draw"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/pbnjay/pixfont"
|
im "github.com/gen2brain/cam2ip/image"
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
}
|
||||||
|
|
||||||
// Camera represents camera.
|
// Camera represents camera.
|
||||||
type Camera struct {
|
type Camera struct {
|
||||||
opts Options
|
opts Options
|
||||||
camera syscall.Handle
|
camera syscall.Handle
|
||||||
frame *image.RGBA
|
rgba *image.RGBA
|
||||||
|
ycbcr *image.YCbCr
|
||||||
hdr *videoHdr
|
hdr *videoHdr
|
||||||
instance syscall.Handle
|
instance syscall.Handle
|
||||||
className string
|
className string
|
||||||
|
format uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns new Camera for given camera index.
|
// New returns new Camera for given camera index.
|
||||||
@@ -34,35 +36,37 @@ func New(opts Options) (camera *Camera, err error) {
|
|||||||
camera.opts = opts
|
camera.opts = opts
|
||||||
camera.className = "capWindowClass"
|
camera.className = "capWindowClass"
|
||||||
|
|
||||||
camera.instance, err = getModuleHandle()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
camera.frame = image.NewRGBA(image.Rect(0, 0, int(camera.opts.Width), int(camera.opts.Height)))
|
|
||||||
|
|
||||||
go func(c *Camera) {
|
go func(c *Camera) {
|
||||||
fn := func(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
|
fn := func(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
|
||||||
switch msg {
|
switch msg {
|
||||||
case wmClose:
|
case wmClose:
|
||||||
destroyWindow(hwnd)
|
_ = destroyWindow(hwnd)
|
||||||
case wmDestroy:
|
case wmDestroy:
|
||||||
postQuitMessage(0)
|
postQuitMessage(0)
|
||||||
default:
|
default:
|
||||||
ret := defWindowProc(hwnd, msg, wparam, lparam)
|
ret := defWindowProc(hwnd, msg, wparam, lparam)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.instance, err = getModuleHandle()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = registerClass(c.className, c.instance, fn)
|
err = registerClass(c.className, c.instance, fn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hwnd, err := createWindow(0, c.className, "", wsOverlappedWindow, cwUseDefault, cwUseDefault, int64(c.opts.Width)+100, int64(c.opts.Height)+100, 0, 0, c.instance)
|
hwnd, e := createWindow(0, c.className, "", wsOverlappedWindow, cwUseDefault, cwUseDefault,
|
||||||
if err != nil {
|
int64(c.opts.Width)+100, int64(c.opts.Height)+100, 0, 0, c.instance)
|
||||||
|
if e != nil {
|
||||||
|
err = e
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,11 +76,15 @@ func New(opts Options) (camera *Camera, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ret := sendMessage(c.camera, wmCapDriverConnect, uintptr(c.opts.Index), 0)
|
ret := sendMessage(c.camera, wmCapDriverConnect, uintptr(c.opts.Index), 0)
|
||||||
if bool(int(ret) == 0) {
|
if int(ret) == 0 {
|
||||||
err = fmt.Errorf("camera: can not open camera %d", c.opts.Index)
|
err = fmt.Errorf("camera: can not open camera %d", c.opts.Index)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendMessage(c.camera, wmCapSetPreview, 0, 0)
|
||||||
|
sendMessage(c.camera, wmCapSetOverlay, 0, 0)
|
||||||
|
|
||||||
var bi bitmapInfo
|
var bi bitmapInfo
|
||||||
size := sendMessage(c.camera, wmCapGetVideoformat, 0, 0)
|
size := sendMessage(c.camera, wmCapGetVideoformat, 0, 0)
|
||||||
sendMessage(c.camera, wmCapGetVideoformat, size, uintptr(unsafe.Pointer(&bi)))
|
sendMessage(c.camera, wmCapGetVideoformat, size, uintptr(unsafe.Pointer(&bi)))
|
||||||
@@ -85,14 +93,44 @@ func New(opts Options) (camera *Camera, err error) {
|
|||||||
bi.BmiHeader.BiHeight = int32(c.opts.Height)
|
bi.BmiHeader.BiHeight = int32(c.opts.Height)
|
||||||
|
|
||||||
ret = sendMessage(c.camera, wmCapSetVideoformat, size, uintptr(unsafe.Pointer(&bi)))
|
ret = sendMessage(c.camera, wmCapSetVideoformat, size, uintptr(unsafe.Pointer(&bi)))
|
||||||
if bool(int(ret) == 0) {
|
if int(ret) == 0 {
|
||||||
err = fmt.Errorf("camera: can not set video format")
|
err = fmt.Errorf("camera: can not set video format: %dx%d, %d", int(c.opts.Width), int(c.opts.Height), c.format)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.format = bi.BmiHeader.BiCompression
|
||||||
sendMessage(c.camera, wmCapSetCallbackFrame, 0, syscall.NewCallback(c.callback))
|
sendMessage(c.camera, wmCapSetCallbackFrame, 0, syscall.NewCallback(c.callback))
|
||||||
|
|
||||||
messageLoop(c.camera)
|
switch c.format {
|
||||||
|
case 0:
|
||||||
|
if bi.BmiHeader.BiBitCount != 24 {
|
||||||
|
err = fmt.Errorf("camera: unsupported format %d; bitcount: %d", c.format, bi.BmiHeader.BiBitCount)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.rgba = image.NewRGBA(image.Rect(0, 0, int(c.opts.Width), int(c.opts.Height)))
|
||||||
|
case yuy2FourCC, yuyvFourCC:
|
||||||
|
c.ycbcr = image.NewYCbCr(image.Rect(0, 0, int(c.opts.Width), int(c.opts.Height)), image.YCbCrSubsampleRatio422)
|
||||||
|
case mjpgFourCC:
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("camera: unsupported format %d", c.format)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
var msg msgW
|
||||||
|
ok, _ := getMessage(&msg, 0, 0, 0)
|
||||||
|
if ok {
|
||||||
|
dispatchMessage(&msg)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}(camera)
|
}(camera)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -100,86 +138,75 @@ func New(opts Options) (camera *Camera, err error) {
|
|||||||
|
|
||||||
// Read reads next frame from camera and returns image.
|
// Read reads next frame from camera and returns image.
|
||||||
func (c *Camera) Read() (img image.Image, err error) {
|
func (c *Camera) Read() (img image.Image, err error) {
|
||||||
ret := sendMessage(c.camera, wmCapGrabFrameNoStop, 0, 0)
|
ret := sendMessage(c.camera, wmCapGrabFrame, 0, 0)
|
||||||
if bool(int(ret) == 0) {
|
if int(ret) == 0 {
|
||||||
err = fmt.Errorf("camera: can not grab frame")
|
err = fmt.Errorf("camera: can not grab frame")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data := (*[1 << 24]uint8)(unsafe.Pointer(c.hdr.LpData))[0:c.hdr.DwBytesUsed]
|
data := unsafe.Slice((*byte)(unsafe.Pointer(c.hdr.LpData)), c.hdr.DwBufferLength)
|
||||||
r := bytes.NewReader(data)
|
|
||||||
|
|
||||||
width := int(c.opts.Width)
|
switch c.format {
|
||||||
height := int(c.opts.Height)
|
case 0:
|
||||||
|
e := bmp24ToRgba(data, c.rgba)
|
||||||
|
if e != nil {
|
||||||
|
err = fmt.Errorf("camera: format %d: can not retrieve frame: %w", c.format, e)
|
||||||
|
|
||||||
// Taken from https://github.com/hotei/bmp/blob/master/bmpRGBA.go#L12
|
|
||||||
// There are 3 bytes per pixel, and each row is 4-byte aligned.
|
|
||||||
b := make([]byte, (3*width+3)&^3)
|
|
||||||
// BMP images are stored bottom-up rather than top-down.
|
|
||||||
for y := height - 1; y >= 0; y-- {
|
|
||||||
_, err = r.Read(b)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("camera: can not retrieve frame: %v", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p := c.frame.Pix[y*c.frame.Stride : y*c.frame.Stride+width*4]
|
img = c.rgba
|
||||||
for i, j := 0, 0; i < len(p); i, j = i+4, j+3 {
|
case yuy2FourCC, yuyvFourCC:
|
||||||
// BMP images are stored in BGR order rather than RGB order.
|
e := yuy2ToYCbCr422(data, c.ycbcr)
|
||||||
p[i+0] = b[j+2]
|
if e != nil {
|
||||||
p[i+1] = b[j+1]
|
err = fmt.Errorf("camera: format %d: can not retrieve frame: %w", c.format, e)
|
||||||
p[i+2] = b[j+0]
|
|
||||||
p[i+3] = 0xFF
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img = c.ycbcr
|
||||||
|
case mjpgFourCC:
|
||||||
|
i, e := im.NewDecoder(bytes.NewReader(data)).Decode()
|
||||||
|
if e != nil {
|
||||||
|
err = fmt.Errorf("camera: format %d: can not retrieve frame: %w", c.format, e)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
img = i
|
||||||
}
|
}
|
||||||
|
|
||||||
img = c.frame
|
if c.opts.Rotate != 0 {
|
||||||
|
img = im.Rotate(img, c.opts.Rotate)
|
||||||
|
}
|
||||||
|
|
||||||
switch c.opts.Rotate {
|
if c.opts.Flip != "" {
|
||||||
case 90:
|
img = im.Flip(img, c.opts.Flip)
|
||||||
img = imaging.Rotate90(img)
|
|
||||||
case 180:
|
|
||||||
img = imaging.Rotate180(img)
|
|
||||||
case 270:
|
|
||||||
img = imaging.Rotate270(img)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.opts.Timestamp {
|
if c.opts.Timestamp {
|
||||||
dimg, ok := img.(draw.Image)
|
img = im.Timestamp(img, c.opts.TimeFormat)
|
||||||
if !ok {
|
|
||||||
err = fmt.Errorf("camera: %T is not a drawable image type", img)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pixfont.DrawString(dimg, 10, 10, time.Now().Format("2006-01-02 15:04:05"), color.White)
|
|
||||||
img = dimg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProperty returns the specified camera property.
|
|
||||||
func (c *Camera) GetProperty(id int) float64 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetProperty sets a camera property.
|
|
||||||
func (c *Camera) SetProperty(id int, value float64) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes camera.
|
// Close closes camera.
|
||||||
func (c *Camera) Close() (err error) {
|
func (c *Camera) Close() (err error) {
|
||||||
sendMessage(c.camera, wmCapSetCallbackFrame, 0, 0)
|
sendMessage(c.camera, wmCapSetCallbackFrame, 0, 0)
|
||||||
unregisterClass(c.className, c.instance)
|
unregisterClass(c.className, c.instance)
|
||||||
sendMessage(c.camera, wmCapDriverDisconnect, 0, 0)
|
sendMessage(c.camera, wmCapDriverDisconnect, 0, 0)
|
||||||
destroyWindow(c.camera)
|
|
||||||
return
|
return destroyWindow(c.camera)
|
||||||
}
|
}
|
||||||
|
|
||||||
// callback function.
|
// callback function.
|
||||||
func (c *Camera) callback(hwvd syscall.Handle, hdr *videoHdr) uintptr {
|
func (c *Camera) callback(hwnd syscall.Handle, hdr *videoHdr) uintptr {
|
||||||
c.hdr = hdr
|
if hdr != nil {
|
||||||
|
c.hdr = hdr
|
||||||
|
}
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,16 +215,15 @@ var (
|
|||||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||||
avicap32 = syscall.NewLazyDLL("avicap32.dll")
|
avicap32 = syscall.NewLazyDLL("avicap32.dll")
|
||||||
|
|
||||||
createWindowExW = user32.NewProc("CreateWindowExW")
|
createWindowExW = user32.NewProc("CreateWindowExW")
|
||||||
destroyWindowW = user32.NewProc("DestroyWindow")
|
destroyWindowW = user32.NewProc("DestroyWindow")
|
||||||
defWindowProcW = user32.NewProc("DefWindowProcW")
|
defWindowProcW = user32.NewProc("DefWindowProcW")
|
||||||
dispatchMessageW = user32.NewProc("DispatchMessageW")
|
dispatchMessageW = user32.NewProc("DispatchMessageW")
|
||||||
translateMessageW = user32.NewProc("TranslateMessage")
|
getMessageW = user32.NewProc("GetMessageW")
|
||||||
getMessageW = user32.NewProc("GetMessageW")
|
sendMessageW = user32.NewProc("SendMessageW")
|
||||||
sendMessageW = user32.NewProc("SendMessageW")
|
postQuitMessageW = user32.NewProc("PostQuitMessage")
|
||||||
postQuitMessageW = user32.NewProc("PostQuitMessage")
|
registerClassExW = user32.NewProc("RegisterClassExW")
|
||||||
registerClassExW = user32.NewProc("RegisterClassExW")
|
unregisterClassW = user32.NewProc("UnregisterClassW")
|
||||||
unregisterClassW = user32.NewProc("UnregisterClassW")
|
|
||||||
|
|
||||||
getModuleHandleW = kernel32.NewProc("GetModuleHandleW")
|
getModuleHandleW = kernel32.NewProc("GetModuleHandleW")
|
||||||
capCreateCaptureWindowW = avicap32.NewProc("capCreateCaptureWindowW")
|
capCreateCaptureWindowW = avicap32.NewProc("capCreateCaptureWindowW")
|
||||||
@@ -214,6 +240,8 @@ const (
|
|||||||
wmCapDriverDisconnect = wmCapStart + 11
|
wmCapDriverDisconnect = wmCapStart + 11
|
||||||
wmCapGetVideoformat = wmCapStart + 44
|
wmCapGetVideoformat = wmCapStart + 44
|
||||||
wmCapSetVideoformat = wmCapStart + 45
|
wmCapSetVideoformat = wmCapStart + 45
|
||||||
|
wmCapSetPreview = wmCapStart + 50
|
||||||
|
wmCapSetOverlay = wmCapStart + 51
|
||||||
wmCapGrabFrame = wmCapStart + 60
|
wmCapGrabFrame = wmCapStart + 60
|
||||||
wmCapGrabFrameNoStop = wmCapStart + 61
|
wmCapGrabFrameNoStop = wmCapStart + 61
|
||||||
wmCapStop = wmCapStart + 68
|
wmCapStop = wmCapStart + 68
|
||||||
@@ -332,8 +360,9 @@ func destroyWindow(hwnd syscall.Handle) error {
|
|||||||
|
|
||||||
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-defwindowprocw
|
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-defwindowprocw
|
||||||
func defWindowProc(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
|
func defWindowProc(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
|
||||||
ret, _, _ := defWindowProcW.Call(uintptr(hwnd), uintptr(msg), uintptr(wparam), uintptr(lparam))
|
ret, _, _ := defWindowProcW.Call(uintptr(hwnd), uintptr(msg), wparam, lparam)
|
||||||
return uintptr(ret)
|
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-dispatchmessagew
|
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-dispatchmessagew
|
||||||
@@ -341,11 +370,6 @@ func dispatchMessage(msg *msgW) {
|
|||||||
dispatchMessageW.Call(uintptr(unsafe.Pointer(msg)))
|
dispatchMessageW.Call(uintptr(unsafe.Pointer(msg)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-translatemessage
|
|
||||||
func translateMessage(msg *msgW) {
|
|
||||||
translateMessageW.Call(uintptr(unsafe.Pointer(msg)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getmessagew
|
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getmessagew
|
||||||
func getMessage(msg *msgW, hwnd syscall.Handle, msgFilterMin, msgFilterMax uint32) (bool, error) {
|
func getMessage(msg *msgW, hwnd syscall.Handle, msgFilterMin, msgFilterMax uint32) (bool, error) {
|
||||||
ret, _, err := getMessageW.Call(uintptr(unsafe.Pointer(msg)), uintptr(hwnd), uintptr(msgFilterMin), uintptr(msgFilterMax))
|
ret, _, err := getMessageW.Call(uintptr(unsafe.Pointer(msg)), uintptr(hwnd), uintptr(msgFilterMin), uintptr(msgFilterMax))
|
||||||
@@ -359,6 +383,7 @@ func getMessage(msg *msgW, hwnd syscall.Handle, msgFilterMin, msgFilterMax uint3
|
|||||||
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-sendmessage
|
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-sendmessage
|
||||||
func sendMessage(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
|
func sendMessage(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
|
||||||
ret, _, _ := sendMessageW.Call(uintptr(hwnd), uintptr(msg), wparam, lparam, 0, 0)
|
ret, _, _ := sendMessageW.Call(uintptr(hwnd), uintptr(msg), wparam, lparam, 0, 0)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,6 +411,7 @@ func registerClass(className string, instance syscall.Handle, fn interface{}) er
|
|||||||
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-unregisterclassw
|
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-unregisterclassw
|
||||||
func unregisterClass(className string, instance syscall.Handle) bool {
|
func unregisterClass(className string, instance syscall.Handle) bool {
|
||||||
ret, _, _ := unregisterClassW.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(className))), uintptr(instance))
|
ret, _, _ := unregisterClassW.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(className))), uintptr(instance))
|
||||||
|
|
||||||
return ret != 0
|
return ret != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,19 +425,3 @@ func capCreateCaptureWindow(lpszWindowName string, dwStyle, x, y, width, height
|
|||||||
|
|
||||||
return syscall.Handle(ret), nil
|
return syscall.Handle(ret), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// messageLoop function
|
|
||||||
func messageLoop(hwnd syscall.Handle) {
|
|
||||||
for {
|
|
||||||
msg := &msgW{}
|
|
||||||
ok, _ := getMessage(msg, 0, 0, 0)
|
|
||||||
if ok {
|
|
||||||
translateMessage(msg)
|
|
||||||
dispatchMessage(msg)
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// +build !cv2,!cv4
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/jamiealquiza/envy"
|
"go.senan.xyz/flagconf"
|
||||||
|
|
||||||
"github.com/gen2brain/cam2ip/camera"
|
"github.com/gen2brain/cam2ip/camera"
|
||||||
"github.com/gen2brain/cam2ip/server"
|
"github.com/gen2brain/cam2ip/server"
|
||||||
@@ -21,41 +19,56 @@ const (
|
|||||||
func main() {
|
func main() {
|
||||||
srv := server.NewServer()
|
srv := server.NewServer()
|
||||||
|
|
||||||
flag.IntVar(&srv.Index, "index", 0, "Camera index")
|
flag.IntVar(&srv.Index, "index", 0, "Camera index [CAM2IP_INDEX]")
|
||||||
flag.IntVar(&srv.Delay, "delay", 10, "Delay between frames, in milliseconds")
|
flag.IntVar(&srv.Delay, "delay", 10, "Delay between frames, in milliseconds [CAM2IP_DELAY]")
|
||||||
flag.Float64Var(&srv.FrameWidth, "width", 640, "Frame width")
|
flag.Float64Var(&srv.Width, "width", 640, "Frame width [CAM2IP_WIDTH]")
|
||||||
flag.Float64Var(&srv.FrameHeight, "height", 480, "Frame height")
|
flag.Float64Var(&srv.Height, "height", 480, "Frame height [CAM2IP_HEIGHT]")
|
||||||
flag.IntVar(&srv.Rotate, "rotate", 0, "Rotate image, valid values are 90, 180, 270")
|
flag.IntVar(&srv.Quality, "quality", 75, "Image quality [CAM2IP_QUALITY]")
|
||||||
flag.BoolVar(&srv.NoWebGL, "nowebgl", false, "Disable WebGL drawing of images (html handler)")
|
flag.IntVar(&srv.Rotate, "rotate", 0, "Rotate image, valid values are 90, 180, 270 [CAM2IP_ROTATE]")
|
||||||
flag.BoolVar(&srv.Timestamp, "timestamp", false, "Draws timestamp on images")
|
flag.StringVar(&srv.Flip, "flip", "", "Flip image, valid values are horizontal and vertical [CAM2IP_FLIP]")
|
||||||
flag.StringVar(&srv.Bind, "bind-addr", ":56000", "Bind address")
|
flag.BoolVar(&srv.NoWebGL, "no-webgl", false, "Disable WebGL drawing of image (html handler) [CAM2IP_NO_WEBGL]")
|
||||||
flag.StringVar(&srv.Htpasswd, "htpasswd-file", "", "Path to htpasswd file, if empty auth is disabled")
|
flag.BoolVar(&srv.Timestamp, "timestamp", false, "Draws timestamp on image [CAM2IP_TIMESTAMP]")
|
||||||
|
flag.StringVar(&srv.TimeFormat, "time-format", "2006-01-02 15:04:05", "Time format [CAM2IP_TIME_FORMAT]")
|
||||||
|
flag.StringVar(&srv.Bind, "bind-addr", ":56000", "Bind address [CAM2IP_BIND_ADDR]")
|
||||||
|
flag.StringVar(&srv.Htpasswd, "htpasswd-file", "", "Path to htpasswd file, if empty auth is disabled [CAM2IP_HTPASSWD_FILE]")
|
||||||
|
|
||||||
|
flag.Usage = func() {
|
||||||
|
stderr("Usage: %s [<flags>]\n", name)
|
||||||
|
order := []string{"index", "delay", "width", "height", "quality", "rotate", "flip", "no-webgl",
|
||||||
|
"timestamp", "time-format", "bind-addr", "htpasswd-file"}
|
||||||
|
|
||||||
|
for _, name := range order {
|
||||||
|
f := flag.Lookup(name)
|
||||||
|
if f != nil {
|
||||||
|
stderr(" --%s\n \t%v (default %q)\n", f.Name, f.Usage, f.DefValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
envy.Parse("CAM2IP")
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
_ = flagconf.ParseEnv()
|
||||||
|
|
||||||
srv.Name = name
|
srv.Name = name
|
||||||
srv.Version = version
|
srv.Version = version
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if srv.Htpasswd != "" {
|
if srv.Htpasswd != "" {
|
||||||
if _, err = os.Stat(srv.Htpasswd); err != nil {
|
if _, err := os.Stat(srv.Htpasswd); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
stderr("%s\n", err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if srv.FileName != "" {
|
cam, err := camera.New(camera.Options{
|
||||||
if _, err = os.Stat(srv.FileName); err != nil {
|
Index: srv.Index,
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
Rotate: srv.Rotate,
|
||||||
os.Exit(1)
|
Flip: srv.Flip,
|
||||||
}
|
Width: srv.Width,
|
||||||
}
|
Height: srv.Height,
|
||||||
|
Timestamp: srv.Timestamp,
|
||||||
cam, err := camera.New(camera.Options{srv.Index, srv.Rotate, srv.FrameWidth, srv.FrameHeight, srv.Timestamp})
|
TimeFormat: srv.TimeFormat,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
stderr("%s\n", err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,11 +76,15 @@ func main() {
|
|||||||
|
|
||||||
defer srv.Reader.Close()
|
defer srv.Reader.Close()
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "Listening on %s\n", srv.Bind)
|
stderr("Listening on %s\n", srv.Bind)
|
||||||
|
|
||||||
err = srv.ListenAndServe()
|
err = srv.ListenAndServe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
stderr("%s\n", err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stderr(format string, a ...any) {
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, format, a...)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
// +build cv2 cv4
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/jamiealquiza/envy"
|
|
||||||
|
|
||||||
"github.com/gen2brain/cam2ip/camera"
|
|
||||||
"github.com/gen2brain/cam2ip/server"
|
|
||||||
"github.com/gen2brain/cam2ip/video"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
name = "cam2ip"
|
|
||||||
version = "1.6"
|
|
||||||
)
|
|
||||||
|
|
||||||
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.StringVar(&srv.FileName, "video-file", "", "Use video file instead of camera")
|
|
||||||
|
|
||||||
envy.Parse("CAM2IP")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
srv.Name = name
|
|
||||||
srv.Version = version
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if srv.Htpasswd != "" {
|
|
||||||
if _, err = os.Stat(srv.Htpasswd); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if srv.FileName != "" {
|
|
||||||
if _, err = os.Stat(srv.FileName); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if srv.FileName != "" {
|
|
||||||
vid, err := video.New(video.Options{srv.FileName, srv.Rotate})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
srv.Reader = vid
|
|
||||||
} else {
|
|
||||||
cam, err := camera.New(camera.Options{srv.Index, srv.Rotate, srv.FrameWidth, srv.FrameHeight, srv.Timestamp})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
srv.Reader = cam
|
|
||||||
}
|
|
||||||
|
|
||||||
defer srv.Reader.Close()
|
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "Listening on %s\n", srv.Bind)
|
|
||||||
|
|
||||||
err = srv.ListenAndServe()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
33
go.mod
33
go.mod
@@ -2,19 +2,24 @@ module github.com/gen2brain/cam2ip
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/abbot/go-http-auth v0.4.0
|
github.com/abbot/go-http-auth v0.4.0
|
||||||
github.com/antonini/golibjpegturbo v0.0.0-20141208033414-c03a2fa1e89a
|
github.com/anthonynsimon/bild v0.14.0
|
||||||
github.com/disintegration/imaging v1.6.1
|
github.com/coder/websocket v1.8.13
|
||||||
github.com/gen2brain/go-opencv v0.0.0-20191005190506-bf186fc94f7a
|
github.com/gen2brain/base64 v0.0.0-20221015184129-317a5c93030c
|
||||||
github.com/jamiealquiza/envy v1.1.0
|
github.com/gen2brain/jpegli v0.3.4
|
||||||
github.com/korandiz/v4l v0.0.0-20180520170035-995f703bfc89
|
github.com/korandiz/v4l v1.1.0
|
||||||
github.com/pbnjay/pixfont v0.0.0-20190130005054-401bb7c6aee2
|
github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567
|
||||||
github.com/spf13/cobra v0.0.5 // indirect
|
github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d
|
||||||
gocv.io/x/gocv v0.20.0
|
go.senan.xyz/flagconf v0.1.9
|
||||||
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad // indirect
|
gocv.io/x/gocv v0.35.0
|
||||||
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a // indirect
|
|
||||||
golang.org/x/net v0.0.0-20190926025831-c00fd9afed17 // indirect
|
|
||||||
goost.org/encoding/base64 v0.0.0-20190928151742-cd6f75493c10
|
|
||||||
nhooyr.io/websocket v1.6.5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
go 1.13
|
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
|
||||||
|
|||||||
194
go.sum
194
go.sum
@@ -1,164 +1,34 @@
|
|||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
|
||||||
github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0=
|
github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0=
|
||||||
github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM=
|
github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM=
|
||||||
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
|
github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync=
|
||||||
github.com/antonini/golibjpegturbo v0.0.0-20141208033414-c03a2fa1e89a h1:+Q4qlzO9KeavJSvWgRKckT3ViTSQ8rR6GroSiXPRXhs=
|
github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs=
|
||||||
github.com/antonini/golibjpegturbo v0.0.0-20141208033414-c03a2fa1e89a/go.mod h1:UOX4aiVZ5WVUBY3D/31H4m6Z8UHgBj5Qr/oldRBBUMY=
|
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
github.com/gen2brain/base64 v0.0.0-20221015184129-317a5c93030c h1:TUjjeJ2rV4KZxH6hIEi/boEQB3v6aKvwdakUJR3AwiE=
|
||||||
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
github.com/gen2brain/base64 v0.0.0-20221015184129-317a5c93030c/go.mod h1:VG58IUyxPWojCtGwqwoZ/6LLXwClu1tssqa5ktOxI9o=
|
||||||
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
github.com/gen2brain/jpegli v0.3.4 h1:wFoUHIjfPJGGeuW3r9dqy0MTT1TtvJuWf6EqfHPPGFM=
|
||||||
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
github.com/gen2brain/jpegli v0.3.4/go.mod h1:tVnF7NPyufTo8noFlW5lurUUwZW8trwBENOItzuk2BM=
|
||||||
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
|
github.com/hybridgroup/mjpeg v0.0.0-20140228234708-4680f319790e/go.mod h1:eagM805MRKrioHYuU7iKLUyFPVKqVV6um5DAvCkUtXs=
|
||||||
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
|
github.com/korandiz/v4l v1.1.0 h1:VbzaWlhqNzVPfHEYEM+V8T7184ndiEzljJgDHSHc7pc=
|
||||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
github.com/korandiz/v4l v1.1.0/go.mod h1:pftxPG7hkuUgepioAY6PAE81mShaVjzd95X/WF4Izus=
|
||||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567 h1:pKjmNHL7BCXhgsnSlN6Ov3WAN2jbJMCx6IvrMN9GNfc=
|
||||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567/go.mod h1:ytYavTmrpWG4s7UOfDhP6m4ASL5XA66nrOcUn1e2M78=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d h1:ls+7AYarUlUSetfnN/DKVNcK6W8mQWc6VblmOm4XwX0=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d/go.mod h1:DO7ixpslN6XfbWzeNH9vkS5CF2FQUX81B85rYe9zDxU=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
github.com/disintegration/imaging v1.6.1 h1:JnBbK6ECIZb1NsWIikP9pd8gIlTIRx7fuDNpU9fsxOE=
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
github.com/disintegration/imaging v1.6.1/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
|
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||||
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
go.senan.xyz/flagconf v0.1.9 h1:LBDmqiVFgijfqFXDzH97gPn0qDbg1Dq6/vxsxS/TzC4=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
go.senan.xyz/flagconf v0.1.9/go.mod h1:NqOFfSwJvNWXOTUabcRZ8mPK9+sJmhStJhqtEt74wNQ=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
gocv.io/x/gocv v0.35.0 h1:Qaxb5KdVyy8Spl4S4K0SMZ6CVmKtbfoSGQAxRD3FZlw=
|
||||||
github.com/gen2brain/go-opencv v0.0.0-20191005190506-bf186fc94f7a h1:0arrt5Ke40opD5glNdh9ltrkZ0jaqPWsquGPSE3ukug=
|
gocv.io/x/gocv v0.35.0/go.mod h1:oc6FvfYqfBp99p+yOEzs9tbYF9gOrAQSeL/dyIPefJU=
|
||||||
github.com/gen2brain/go-opencv v0.0.0-20191005190506-bf186fc94f7a/go.mod h1:pOLh42huXUuMoJWvD2K+EeXzvQ9GZ5HN6gdFk5ZwIuU=
|
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||||
github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||||
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
|
||||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
|
||||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
|
||||||
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
|
||||||
github.com/jamiealquiza/envy v1.1.0 h1:Nwh4wqTZ28gDA8zB+wFkhnUpz3CEcO12zotjeqqRoKE=
|
|
||||||
github.com/jamiealquiza/envy v1.1.0/go.mod h1:MP36BriGCLwEHhi1OU8E9569JNZrjWfCvzG7RsPnHus=
|
|
||||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
|
||||||
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
|
||||||
github.com/korandiz/v4l v0.0.0-20180520170035-995f703bfc89 h1:BSg+wmA2aXiD7joWbYpkkLvPPZzdhpp2LAgoh8SMcyE=
|
|
||||||
github.com/korandiz/v4l v0.0.0-20180520170035-995f703bfc89/go.mod h1:jGGdTcTvrj/K3Qvlch8Qzb8PJjeMeJ/XmPbQxc37rJ8=
|
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
|
||||||
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
|
||||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
|
||||||
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
|
||||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
|
||||||
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
|
||||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
|
||||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
|
||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
|
||||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
|
||||||
github.com/pbnjay/pixfont v0.0.0-20190130005054-401bb7c6aee2 h1:Px7Nfku7x0pH6r2G+RlLI5AnXefcH2BAenMxDpZZfIs=
|
|
||||||
github.com/pbnjay/pixfont v0.0.0-20190130005054-401bb7c6aee2/go.mod h1:wG8B9TIIBxEYqwgBb9NEs/Gz5/ywV351SGZXRiVJJUA=
|
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
|
||||||
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
|
||||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
|
||||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
|
||||||
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
|
|
||||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
|
||||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
|
||||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
|
||||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
|
||||||
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/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
|
|
||||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
|
||||||
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
|
|
||||||
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
|
|
||||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
|
||||||
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
|
||||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
|
||||||
gocv.io/x/gocv v0.20.0 h1:2q75zQ8Zel2tB69G6qrmf/E7EdvaCs90qvkHzdSBOAg=
|
|
||||||
gocv.io/x/gocv v0.20.0/go.mod h1:vZETJRwLnl11muQ6iL3q4ju+0oJRrdmYdv5xJTH7WYA=
|
|
||||||
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad h1:5E5raQxcv+6CZ11RrBYQe5WRbUIWpScjh0kvHZkZIrQ=
|
|
||||||
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
|
||||||
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a h1:gHevYm0pO4QUbwy8Dmdr01R5r1BuKtfYqRqF0h/Cbh0=
|
|
||||||
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
|
||||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20190926025831-c00fd9afed17 h1:qPnAdmjNA41t3QBTx2mFGf/SD1IoslhYu7AmdsVzCcs=
|
|
||||||
golang.org/x/net v0.0.0-20190926025831-c00fd9afed17/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA=
|
|
||||||
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
goost.org/encoding/base64 v0.0.0-20190928151742-cd6f75493c10 h1:Z4tmKmcuax8bev3BlmNf3/0tcw6j/BBXFY6Hnq0njew=
|
|
||||||
goost.org/encoding/base64 v0.0.0-20190928151742-cd6f75493c10/go.mod h1:pBZSqyNgDwqnkoXcbBD1wSOStfw5WgjBx9Zcda9RvLA=
|
|
||||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
|
||||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
|
||||||
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
|
|
||||||
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
|
|
||||||
nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg=
|
|
||||||
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// Package handlers.
|
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -13,11 +12,11 @@ type HTML struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewHTML returns new HTML handler.
|
// NewHTML returns new HTML handler.
|
||||||
func NewHTML(width, height float64, nogl bool) *HTML {
|
func NewHTML(width, height float64, noWebGL bool) *HTML {
|
||||||
h := &HTML{}
|
h := &HTML{}
|
||||||
|
|
||||||
tpl := htmlWebGL
|
tpl := htmlWebGL
|
||||||
if nogl {
|
if noWebGL {
|
||||||
tpl = html
|
tpl = html
|
||||||
}
|
}
|
||||||
tpl = strings.Replace(tpl, "{WIDTH}", fmt.Sprintf("%.0f", width), -1)
|
tpl = strings.Replace(tpl, "{WIDTH}", fmt.Sprintf("%.0f", width), -1)
|
||||||
@@ -32,12 +31,13 @@ func (h *HTML) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
if r.Method != "GET" && r.Method != "HEAD" {
|
if r.Method != "GET" && r.Method != "HEAD" {
|
||||||
msg := fmt.Sprintf("405 Method Not Allowed (%s)", r.Method)
|
msg := fmt.Sprintf("405 Method Not Allowed (%s)", r.Method)
|
||||||
http.Error(w, msg, http.StatusMethodNotAllowed)
|
http.Error(w, msg, http.StatusMethodNotAllowed)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write(h.Template)
|
_, _ = w.Write(h.Template)
|
||||||
}
|
}
|
||||||
|
|
||||||
var html = `<html>
|
var html = `<html>
|
||||||
@@ -45,7 +45,11 @@ var html = `<html>
|
|||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<title>cam2ip</title>
|
<title>cam2ip</title>
|
||||||
<script>
|
<script>
|
||||||
ws = new WebSocket("ws://" + window.location.host + "/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();
|
var image = new Image();
|
||||||
|
|
||||||
ws.onopen = function() {
|
ws.onopen = function() {
|
||||||
@@ -78,11 +82,15 @@ var htmlWebGL = `<html>
|
|||||||
<script>
|
<script>
|
||||||
var texture, vloc, tloc, vertexBuff, textureBuff;
|
var texture, vloc, tloc, vertexBuff, textureBuff;
|
||||||
|
|
||||||
ws = new WebSocket("ws://" + window.location.host + "/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();
|
var image = new Image();
|
||||||
|
|
||||||
ws.onopen = function() {
|
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 =
|
var vertexShaderSrc =
|
||||||
"attribute vec2 aVertex;" +
|
"attribute vec2 aVertex;" +
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package handlers provides HTTP handlers for the cam2ip application.
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -17,10 +18,11 @@ func NewIndex() *Index {
|
|||||||
func (i *Index) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (i *Index) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "GET" && r.Method != "HEAD" {
|
if r.Method != "GET" && r.Method != "HEAD" {
|
||||||
http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Write([]byte(`<html>
|
_, _ = w.Write([]byte(`<html>
|
||||||
<head><title>cam2ip</title></head>
|
<head><title>cam2ip</title></head>
|
||||||
<body>
|
<body>
|
||||||
<h1>cam2ip</h1>
|
<h1>cam2ip</h1>
|
||||||
|
|||||||
@@ -5,23 +5,24 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gen2brain/cam2ip/image"
|
"github.com/gen2brain/cam2ip/image"
|
||||||
"github.com/gen2brain/cam2ip/reader"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// JPEG handler.
|
// JPEG handler.
|
||||||
type JPEG struct {
|
type JPEG struct {
|
||||||
reader reader.ImageReader
|
reader ImageReader
|
||||||
|
quality int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewJPEG returns new JPEG handler.
|
// NewJPEG returns new JPEG handler.
|
||||||
func NewJPEG(reader reader.ImageReader) *JPEG {
|
func NewJPEG(reader ImageReader, quality int) *JPEG {
|
||||||
return &JPEG{reader}
|
return &JPEG{reader, quality}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP handles requests on incoming connections.
|
// ServeHTTP handles requests on incoming connections.
|
||||||
func (j *JPEG) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (j *JPEG) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "GET" && r.Method != "HEAD" {
|
if r.Method != "GET" && r.Method != "HEAD" {
|
||||||
http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,12 +33,16 @@ func (j *JPEG) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
img, err := j.reader.Read()
|
img, err := j.reader.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("jpeg: read: %v", err)
|
log.Printf("jpeg: read: %v", err)
|
||||||
|
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = image.NewEncoder(w).Encode(img)
|
err = image.NewEncoder(w, j.quality).Encode(img)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("jpeg: encode: %v", err)
|
log.Printf("jpeg: encode: %v", err)
|
||||||
|
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// Package handlers.
|
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -10,40 +9,41 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gen2brain/cam2ip/image"
|
"github.com/gen2brain/cam2ip/image"
|
||||||
"github.com/gen2brain/cam2ip/reader"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MJPEG handler.
|
// MJPEG handler.
|
||||||
type MJPEG struct {
|
type MJPEG struct {
|
||||||
reader reader.ImageReader
|
reader ImageReader
|
||||||
delay int
|
delay int
|
||||||
|
quality int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMJPEG returns new MJPEG handler.
|
// NewMJPEG returns new MJPEG handler.
|
||||||
func NewMJPEG(reader reader.ImageReader, delay int) *MJPEG {
|
func NewMJPEG(reader ImageReader, delay, quality int) *MJPEG {
|
||||||
return &MJPEG{reader, delay}
|
return &MJPEG{reader, delay, quality}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP handles requests on incoming connections.
|
// ServeHTTP handles requests on incoming connections.
|
||||||
func (m *MJPEG) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (m *MJPEG) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "GET" && r.Method != "HEAD" {
|
if r.Method != "GET" && r.Method != "HEAD" {
|
||||||
http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mimeWriter := multipart.NewWriter(w)
|
mimeWriter := multipart.NewWriter(w)
|
||||||
mimeWriter.SetBoundary("--boundary")
|
_ = mimeWriter.SetBoundary("--boundary")
|
||||||
|
|
||||||
w.Header().Add("Connection", "close")
|
w.Header().Add("Connection", "close")
|
||||||
w.Header().Add("Cache-Control", "no-store, no-cache")
|
w.Header().Add("Cache-Control", "no-store, no-cache")
|
||||||
w.Header().Add("Content-Type", fmt.Sprintf("multipart/x-mixed-replace;boundary=%s", mimeWriter.Boundary()))
|
w.Header().Add("Content-Type", fmt.Sprintf("multipart/x-mixed-replace;boundary=%s", mimeWriter.Boundary()))
|
||||||
|
|
||||||
cn := w.(http.CloseNotifier).CloseNotify()
|
done := r.Context().Done()
|
||||||
|
|
||||||
loop:
|
loop:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-cn:
|
case <-done:
|
||||||
break loop
|
break loop
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -62,15 +62,17 @@ loop:
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = image.NewEncoder(partWriter).Encode(img)
|
err = image.NewEncoder(partWriter, m.quality).Encode(img)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("mjpeg: encode: %v", err)
|
log.Printf("mjpeg: encode: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(time.Duration(m.delay) * time.Millisecond)
|
if m.delay > 0 {
|
||||||
|
time.Sleep(time.Duration(m.delay) * time.Millisecond)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mimeWriter.Close()
|
_ = mimeWriter.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// Package reader.
|
package handlers
|
||||||
package reader
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
@@ -7,21 +7,21 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"nhooyr.io/websocket"
|
"github.com/coder/websocket"
|
||||||
|
|
||||||
"github.com/gen2brain/cam2ip/image"
|
"github.com/gen2brain/cam2ip/image"
|
||||||
"github.com/gen2brain/cam2ip/reader"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Socket handler.
|
// Socket handler.
|
||||||
type Socket struct {
|
type Socket struct {
|
||||||
reader reader.ImageReader
|
reader ImageReader
|
||||||
delay int
|
delay int
|
||||||
|
quality int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSocket returns new socket handler.
|
// NewSocket returns new socket handler.
|
||||||
func NewSocket(reader reader.ImageReader, delay int) *Socket {
|
func NewSocket(reader ImageReader, delay, quality int) *Socket {
|
||||||
return &Socket{reader, delay}
|
return &Socket{reader, delay, quality}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP handles requests on incoming connections.
|
// ServeHTTP handles requests on incoming connections.
|
||||||
@@ -29,6 +29,7 @@ func (s *Socket) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
conn, err := websocket.Accept(w, r, nil)
|
conn, err := websocket.Accept(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("socket: accept: %v", err)
|
log.Printf("socket: accept: %v", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ func (s *Socket) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
w := new(bytes.Buffer)
|
w := new(bytes.Buffer)
|
||||||
|
|
||||||
err = image.NewEncoder(w).Encode(img)
|
err = image.NewEncoder(w, s.quality).Encode(img)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("socket: encode: %v", err)
|
log.Printf("socket: encode: %v", err)
|
||||||
continue
|
continue
|
||||||
@@ -56,8 +57,10 @@ func (s *Socket) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(time.Duration(s.delay) * time.Millisecond)
|
if s.delay > 0 {
|
||||||
|
time.Sleep(time.Duration(s.delay) * time.Millisecond)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.Close(websocket.StatusNormalClosure, "")
|
_ = conn.Close(websocket.StatusNormalClosure, "")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// +build !amd64
|
//go:build !amd64
|
||||||
|
|
||||||
package image
|
package image
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// +build amd64
|
//go:build amd64
|
||||||
|
|
||||||
package image
|
package image
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"goost.org/encoding/base64"
|
"github.com/gen2brain/base64"
|
||||||
)
|
)
|
||||||
|
|
||||||
func EncodeToString(src []byte) string {
|
func EncodeToString(src []byte) string {
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
// +build !jpeg
|
//go:build !libjpeg && !jpegli
|
||||||
|
|
||||||
// Package image.
|
// Package image.
|
||||||
package image
|
package image
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
jpeg "github.com/antonini/golibjpegturbo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewDecoder returns a new Decoder.
|
// NewDecoder returns a new Decoder.
|
||||||
|
|||||||
31
image/decode_jpegli.go
Normal file
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
// +build jpeg
|
//go:build libjpeg
|
||||||
|
|
||||||
// Package image.
|
// Package image.
|
||||||
package image
|
package image
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
"image/jpeg"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/pixiv/go-libjpeg/jpeg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewDecoder returns a new Decoder.
|
// NewDecoder returns a new Decoder.
|
||||||
@@ -21,5 +22,9 @@ type Decoder struct {
|
|||||||
|
|
||||||
// Decode decodes image from JPEG.
|
// Decode decodes image from JPEG.
|
||||||
func (d Decoder) Decode() (image.Image, error) {
|
func (d Decoder) Decode() (image.Image, error) {
|
||||||
return jpeg.Decode(d.r)
|
return jpeg.Decode(d.r, &jpeg.DecoderOptions{
|
||||||
|
DCTMethod: jpeg.DCTIFast,
|
||||||
|
DisableFancyUpsampling: true,
|
||||||
|
DisableBlockSmoothing: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
// +build !jpeg
|
//go:build !libjpeg && !jpegli
|
||||||
|
|
||||||
// Package image.
|
// Package image.
|
||||||
package image
|
package image
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
jpeg "github.com/antonini/golibjpegturbo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewEncoder returns a new Encoder.
|
// NewEncoder returns a new Encoder.
|
||||||
func NewEncoder(w io.Writer) *Encoder {
|
func NewEncoder(w io.Writer, quality int) *Encoder {
|
||||||
return &Encoder{w}
|
return &Encoder{w, quality}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encoder struct.
|
// Encoder struct.
|
||||||
type Encoder struct {
|
type Encoder struct {
|
||||||
w io.Writer
|
w io.Writer
|
||||||
|
quality int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode encodes image to JPEG.
|
// Encode encodes image to JPEG.
|
||||||
func (e Encoder) Encode(img image.Image) error {
|
func (e Encoder) Encode(img image.Image) error {
|
||||||
err := jpeg.Encode(e.w, img, &jpeg.Options{Quality: 75})
|
err := jpeg.Encode(e.w, img, &jpeg.Options{Quality: e.quality})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
// +build jpeg
|
|
||||||
|
|
||||||
// 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
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 |
90
make.bash
90
make.bash
@@ -5,46 +5,32 @@ MINGW="/usr/i686-w64-mingw32"
|
|||||||
MINGW64="/usr/x86_64-w64-mingw32"
|
MINGW64="/usr/x86_64-w64-mingw32"
|
||||||
RPI="/usr/armv6j-hardfloat-linux-gnueabi"
|
RPI="/usr/armv6j-hardfloat-linux-gnueabi"
|
||||||
RPI3="/usr/armv7a-hardfloat-linux-gnueabi"
|
RPI3="/usr/armv7a-hardfloat-linux-gnueabi"
|
||||||
|
APPLE="/usr/x86_64-apple-darwin14"
|
||||||
|
ANDROID="/usr/arm-linux-androideabi"
|
||||||
|
|
||||||
mkdir -p build
|
mkdir -p build
|
||||||
|
|
||||||
LIBRARY_PATH="$CHROOT/usr/lib:$CHROOT/lib" \
|
LIBRARY_PATH="$CHROOT/usr/lib64:$CHROOT/lib64" \
|
||||||
PKG_CONFIG_PATH="$CHROOT/usr/lib/pkgconfig" \
|
PKG_CONFIG_PATH="$CHROOT/usr/lib64/pkgconfig" \
|
||||||
PKG_CONFIG_LIBDIR="$CHROOT/usr/lib/pkgconfig" \
|
PKG_CONFIG_LIBDIR="$CHROOT/usr/lib64/pkgconfig" \
|
||||||
CGO_LDFLAGS="-L$CHROOT/usr/lib -L$CHROOT/lib" \
|
CGO_LDFLAGS="-L$CHROOT/usr/lib64 -L$CHROOT/lib" \
|
||||||
CGO_CFLAGS="-I$CHROOT/usr/include" \
|
CGO_CFLAGS="-I$CHROOT/usr/include" \
|
||||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -tags cv2 -o build/cam2ip.linux.amd64.cv2 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip/cmd/cam2ip
|
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/lib:$CHROOT/lib" \
|
LIBRARY_PATH="$CHROOT/usr/lib64:$CHROOT/lib64" \
|
||||||
PKG_CONFIG_PATH="$CHROOT/usr/lib/pkgconfig" \
|
PKG_CONFIG_PATH="$CHROOT/usr/lib64/pkgconfig" \
|
||||||
PKG_CONFIG_LIBDIR="$CHROOT/usr/lib/pkgconfig" \
|
PKG_CONFIG_LIBDIR="$CHROOT/usr/lib64/pkgconfig" \
|
||||||
CGO_LDFLAGS="-L$CHROOT/usr/lib -L$CHROOT/lib" \
|
CGO_LDFLAGS="-L$CHROOT/usr/lib64 -L$CHROOT/lib64" \
|
||||||
CGO_CFLAGS="-I$CHROOT/usr/include" \
|
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
|
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 -tags "cv2 pkgconfig" -o build/cam2ip.386.cv2.exe -ldflags "-linkmode external -s -w '-extldflags=-static'" 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 -o build/cam2ip.386.exe -ldflags "-linkmode external -s -w '-extldflags=-static'" github.com/gen2brain/cam2ip/cmd/cam2ip
|
|
||||||
|
|
||||||
PKG_CONFIG="/usr/bin/x86_64-w64-mingw32-pkg-config" \
|
PKG_CONFIG="/usr/bin/x86_64-w64-mingw32-pkg-config" \
|
||||||
PKG_CONFIG_PATH="$MINGW64/usr/lib/pkgconfig" \
|
PKG_CONFIG_PATH="$MINGW64/usr/lib/pkgconfig" \
|
||||||
PKG_CONFIG_LIBDIR="$MINGW64/usr/lib/pkgconfig" \
|
PKG_CONFIG_LIBDIR="$MINGW64/usr/lib/pkgconfig" \
|
||||||
CGO_LDFLAGS="-L$MINGW64/usr/lib" \
|
CGO_LDFLAGS="-L$MINGW64/usr/lib" \
|
||||||
CGO_CFLAGS="-I$MINGW64/usr/include" \
|
CGO_CFLAGS="-I$MINGW64/usr/include" \
|
||||||
CC="x86_64-w64-mingw32-gcc" CXX="x86_64-w64-mingw32-g++" \
|
CC="x86_64-w64-mingw32-gcc" CXX="x86_64-w64-mingw32-g++" \
|
||||||
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -tags "cv2 pkgconfig" -o build/cam2ip.amd64.cv2.exe -ldflags "-linkmode external -s -w '-extldflags=-static'" github.com/gen2brain/cam2ip/cmd/cam2ip
|
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/x86_64-w64-mingw32-pkg-config" \
|
PKG_CONFIG="/usr/bin/x86_64-w64-mingw32-pkg-config" \
|
||||||
PKG_CONFIG_PATH="$MINGW64/usr/lib/pkgconfig" \
|
PKG_CONFIG_PATH="$MINGW64/usr/lib/pkgconfig" \
|
||||||
@@ -54,29 +40,13 @@ CGO_CFLAGS="-I$MINGW64/usr/include" \
|
|||||||
CC="x86_64-w64-mingw32-gcc" CXX="x86_64-w64-mingw32-g++" \
|
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
|
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/armv6j-hardfloat-linux-gnueabi-pkg-config" \
|
PKG_CONFIG="/usr/bin/armv7a-hardfloat-linux-gnueabi-pkg-config" \
|
||||||
PKG_CONFIG_PATH="$RPI/usr/lib/pkgconfig" \
|
PKG_CONFIG_PATH="$RPI3/usr/lib/pkgconfig" \
|
||||||
PKG_CONFIG_LIBDIR="$RPI/usr/lib/pkgconfig" \
|
PKG_CONFIG_LIBDIR="$RPI3/usr/lib/pkgconfig" \
|
||||||
CGO_LDFLAGS="-L$RPI/usr/lib" \
|
CGO_LDFLAGS="-L$RPI3/usr/lib" \
|
||||||
CGO_CFLAGS="-I$RPI/usr/include" \
|
CGO_CFLAGS="-I$RPI3/usr/include" \
|
||||||
CC="armv6j-hardfloat-linux-gnueabi-gcc" CXX="armv6j-hardfloat-linux-gnueabi-g++" \
|
CC="armv7a-hardfloat-linux-gnueabi-gcc" CXX="armv7a-hardfloat-linux-gnueabi-g++" \
|
||||||
CGO_ENABLED=1 GOOS=linux GOARCH=arm go build -tags cv2 -o build/cam2ip.linux.arm.cv2 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip/cmd/cam2ip
|
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/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 -o build/cam2ip.linux.arm -ldflags "-linkmode external -s -w" 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=0 GOOS=linux GOARCH=arm go build -tags jpeg -o build/cam2ip.linux.arm.nocgo -ldflags "-s -w" github.com/gen2brain/cam2ip/cmd/cam2ip
|
|
||||||
|
|
||||||
PKG_CONFIG="/usr/bin/armv7a-hardfloat-linux-gnueabi-pkg-config" \
|
PKG_CONFIG="/usr/bin/armv7a-hardfloat-linux-gnueabi-pkg-config" \
|
||||||
PKG_CONFIG_PATH="$RPI3/usr/lib/pkgconfig" \
|
PKG_CONFIG_PATH="$RPI3/usr/lib/pkgconfig" \
|
||||||
@@ -84,12 +54,18 @@ PKG_CONFIG_LIBDIR="$RPI3/usr/lib/pkgconfig" \
|
|||||||
CGO_LDFLAGS="-L$RPI3/usr/lib" \
|
CGO_LDFLAGS="-L$RPI3/usr/lib" \
|
||||||
CGO_CFLAGS="-I$RPI3/usr/include" \
|
CGO_CFLAGS="-I$RPI3/usr/include" \
|
||||||
CC="armv7a-hardfloat-linux-gnueabi-gcc" CXX="armv7a-hardfloat-linux-gnueabi-g++" \
|
CC="armv7a-hardfloat-linux-gnueabi-gcc" CXX="armv7a-hardfloat-linux-gnueabi-g++" \
|
||||||
CGO_ENABLED=1 GOOS=linux GOARCH=arm go build -tags cv2 -o build/cam2ip.linux.arm7.cv2 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip/cmd/cam2ip
|
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="/usr/bin/armv7a-hardfloat-linux-gnueabi-pkg-config" \
|
PKG_CONFIG_PATH="$APPLE/SDK/MacOSX10.10.sdk/usr/lib/pkgconfig" \
|
||||||
PKG_CONFIG_PATH="$RPI3/usr/lib/pkgconfig" \
|
PKG_CONFIG_LIBDIR="$APPLE/SDK/MacOSX10.10.sdk/usr/lib/pkgconfig" \
|
||||||
PKG_CONFIG_LIBDIR="$RPI3/usr/lib/pkgconfig" \
|
CGO_LDFLAGS="-L$APPLE/SDK/MacOSX10.10.sdk/usr/lib -mmacosx-version-min=10.10" \
|
||||||
CGO_LDFLAGS="-L$RPI3/usr/lib" \
|
CGO_CFLAGS="-I$APPLE/SDK/MacOSX10.10.sdk/usr/include -mmacosx-version-min=10.10" \
|
||||||
CGO_CFLAGS="-I$RPI3/usr/include" \
|
CC="$APPLE/bin/x86_64-apple-darwin14-clang" CXX="$APPLE/bin/x86_64-apple-darwin14-clang++" \
|
||||||
CC="armv7a-hardfloat-linux-gnueabi-gcc" CXX="armv7a-hardfloat-linux-gnueabi-g++" \
|
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
|
||||||
CGO_ENABLED=1 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="$ANDROID/lib/pkgconfig" \
|
||||||
|
PKG_CONFIG_LIBDIR="$ANDROID/lib/pkgconfig" \
|
||||||
|
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,11 +5,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/abbot/go-http-auth"
|
"github.com/abbot/go-http-auth"
|
||||||
|
|
||||||
"github.com/gen2brain/cam2ip/handlers"
|
"github.com/gen2brain/cam2ip/handlers"
|
||||||
"github.com/gen2brain/cam2ip/reader"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server struct.
|
// Server struct.
|
||||||
@@ -17,28 +17,31 @@ type Server struct {
|
|||||||
Name string
|
Name string
|
||||||
Version string
|
Version string
|
||||||
|
|
||||||
Bind string
|
|
||||||
Htpasswd string
|
|
||||||
|
|
||||||
Index int
|
Index int
|
||||||
Delay int
|
Delay int
|
||||||
|
|
||||||
FrameWidth float64
|
Width float64
|
||||||
FrameHeight float64
|
Height float64
|
||||||
|
|
||||||
Rotate int
|
Quality int
|
||||||
|
Rotate int
|
||||||
|
Flip string
|
||||||
|
|
||||||
NoWebGL bool
|
NoWebGL bool
|
||||||
Timestamp bool
|
|
||||||
|
|
||||||
FileName string
|
Timestamp bool
|
||||||
|
TimeFormat string
|
||||||
|
|
||||||
Reader reader.ImageReader
|
Bind string
|
||||||
|
Htpasswd string
|
||||||
|
|
||||||
|
Reader handlers.ImageReader
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer returns new Server.
|
// NewServer returns new Server.
|
||||||
func NewServer() *Server {
|
func NewServer() *Server {
|
||||||
s := &Server{}
|
s := &Server{}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,10 +53,10 @@ func (s *Server) ListenAndServe() error {
|
|||||||
basic = auth.NewBasicAuthenticator(realm, auth.HtpasswdFileProvider(s.Htpasswd))
|
basic = auth.NewBasicAuthenticator(realm, auth.HtpasswdFileProvider(s.Htpasswd))
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Handle("/html", newAuthHandler(handlers.NewHTML(s.FrameWidth, s.FrameHeight, s.NoWebGL), basic))
|
http.Handle("/html", newAuthHandler(handlers.NewHTML(s.Width, s.Height, s.NoWebGL), basic))
|
||||||
http.Handle("/jpeg", newAuthHandler(handlers.NewJPEG(s.Reader), basic))
|
http.Handle("/jpeg", newAuthHandler(handlers.NewJPEG(s.Reader, s.Quality), basic))
|
||||||
http.Handle("/mjpeg", newAuthHandler(handlers.NewMJPEG(s.Reader, s.Delay), basic))
|
http.Handle("/mjpeg", newAuthHandler(handlers.NewMJPEG(s.Reader, s.Delay, s.Quality), basic))
|
||||||
http.Handle("/socket", newAuthHandler(handlers.NewSocket(s.Reader, s.Delay), basic))
|
http.Handle("/socket", newAuthHandler(handlers.NewSocket(s.Reader, s.Delay, s.Quality), basic))
|
||||||
|
|
||||||
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
@@ -61,9 +64,12 @@ func (s *Server) ListenAndServe() error {
|
|||||||
|
|
||||||
http.Handle("/", newAuthHandler(handlers.NewIndex(), basic))
|
http.Handle("/", newAuthHandler(handlers.NewIndex(), basic))
|
||||||
|
|
||||||
srv := &http.Server{}
|
srv := &http.Server{
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
listener, err := net.Listen("tcp4", s.Bind)
|
listener, err := net.Listen("tcp", s.Bind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -78,6 +84,7 @@ func newAuthHandler(handler http.Handler, authenticator *auth.BasicAuth) http.Ha
|
|||||||
w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", authenticator.Realm))
|
w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", authenticator.Realm))
|
||||||
if authenticator.CheckAuth(r) == "" {
|
if authenticator.CheckAuth(r) == "" {
|
||||||
http.Error(w, "401 Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "401 Unauthorized", http.StatusUnauthorized)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
// +build cv2,!cv4
|
|
||||||
|
|
||||||
// Package video.
|
|
||||||
package video
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
|
||||||
"github.com/gen2brain/go-opencv/opencv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Options.
|
|
||||||
type Options struct {
|
|
||||||
Filename string
|
|
||||||
Rotate int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Video represents video.
|
|
||||||
type Video struct {
|
|
||||||
opts Options
|
|
||||||
video *opencv.Capture
|
|
||||||
frame *opencv.IplImage
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns new Video for given path.
|
|
||||||
func New(opts Options) (video *Video, err error) {
|
|
||||||
video = &Video{}
|
|
||||||
video.opts = opts
|
|
||||||
|
|
||||||
video.video = opencv.NewFileCapture(opts.Filename)
|
|
||||||
if video.video == nil {
|
|
||||||
err = fmt.Errorf("video: can not open video %s", opts.Filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read reads next frame from video and returns image.
|
|
||||||
func (v *Video) Read() (img image.Image, err error) {
|
|
||||||
if v.video.GrabFrame() {
|
|
||||||
v.frame = v.video.RetrieveFrame(1)
|
|
||||||
if v.frame == nil {
|
|
||||||
err = fmt.Errorf("video: can not retrieve frame")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
img = v.frame.ToImage()
|
|
||||||
if v.opts.Rotate == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v.opts.Rotate {
|
|
||||||
case 90:
|
|
||||||
img = imaging.Rotate90(img)
|
|
||||||
case 180:
|
|
||||||
img = imaging.Rotate180(img)
|
|
||||||
case 270:
|
|
||||||
img = imaging.Rotate270(img)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err = fmt.Errorf("video: can not grab frame")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes video.
|
|
||||||
func (v *Video) Close() (err error) {
|
|
||||||
if v.video == nil {
|
|
||||||
err = fmt.Errorf("video: video is not opened")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
v.frame.Release()
|
|
||||||
v.video.Release()
|
|
||||||
v.video = nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
// +build cv4,!cv2
|
|
||||||
|
|
||||||
// Package video.
|
|
||||||
package video
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
|
||||||
"gocv.io/x/gocv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Options.
|
|
||||||
type Options struct {
|
|
||||||
Filename string
|
|
||||||
Rotate int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Video represents video.
|
|
||||||
type Video struct {
|
|
||||||
opts Options
|
|
||||||
video *gocv.VideoCapture
|
|
||||||
frame *gocv.Mat
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns new Video for given path.
|
|
||||||
func New(opts Options) (video *Video, err error) {
|
|
||||||
video = &Video{}
|
|
||||||
video.opts = opts
|
|
||||||
|
|
||||||
mat := gocv.NewMat()
|
|
||||||
video.frame = &mat
|
|
||||||
|
|
||||||
video.video, err = gocv.VideoCaptureFile(opts.Filename)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("video: can not open video %s: %s", opts.Filename, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read reads next frame from video and returns image.
|
|
||||||
func (v *Video) Read() (img image.Image, err error) {
|
|
||||||
ok := v.video.Read(v.frame)
|
|
||||||
if !ok {
|
|
||||||
err = fmt.Errorf("video: can not grab frame")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.frame == nil {
|
|
||||||
err = fmt.Errorf("video: can not retrieve frame")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
img, e := v.frame.ToImage()
|
|
||||||
if e != nil {
|
|
||||||
err = fmt.Errorf("video: %v", e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.opts.Rotate == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v.opts.Rotate {
|
|
||||||
case 90:
|
|
||||||
img = imaging.Rotate90(img)
|
|
||||||
case 180:
|
|
||||||
img = imaging.Rotate180(img)
|
|
||||||
case 270:
|
|
||||||
img = imaging.Rotate270(img)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes video.
|
|
||||||
func (v *Video) Close() (err error) {
|
|
||||||
if v.video == nil {
|
|
||||||
err = fmt.Errorf("video: video is not opened")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
v.frame.Close()
|
|
||||||
err = v.video.Close()
|
|
||||||
v.video = nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package video
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"image/jpeg"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestVideo(t *testing.T) {
|
|
||||||
video, err := New(video.Options{"test.mp4", 0})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer video.Close()
|
|
||||||
|
|
||||||
tmpdir, err := ioutil.TempDir(os.TempDir(), "cam2ip")
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer os.RemoveAll(tmpdir)
|
|
||||||
|
|
||||||
var i int
|
|
||||||
var n int = 10
|
|
||||||
|
|
||||||
timeout := time.After(time.Duration(n) * time.Second)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-timeout:
|
|
||||||
//fmt.Printf("Fps: %d\n", i/n)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
img, err := video.Read()
|
|
||||||
if err != nil {
|
|
||||||
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()
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user