98 Commits
1.0 ... master

Author SHA1 Message Date
Milan Nikolic
5fcc525cec Merge pull request #60 from tacoverflow/hotfix/use-secure-websockets-if-https-is-used
Update html.go: use wss if https is used
2025-09-17 06:01:34 +02:00
tacoverflow
1e6eae1e14 Update html.go: use wss if https is used 2025-09-16 05:22:03 -07:00
Milan Nikolic
3fe0d88418 Update README.md 2025-06-15 08:48:39 +02:00
Milan Nikolic
6826193e2c Update timestamp 2025-06-15 08:47:13 +02:00
Milan Nikolic
d84884f26b Update server 2025-06-15 08:34:19 +02:00
Milan Nikolic
bf647116a3 Update Dockerfile 2025-06-15 08:32:50 +02:00
Milan Nikolic
8667fe4b48 Add support for YUYV/YUY2 format 2025-06-15 08:32:19 +02:00
Milan Nikolic
7e4e58029a Add some options 2025-06-14 01:45:32 +02:00
Milan Nikolic
fd5cb861cd Update flags 2025-06-14 00:06:04 +02:00
Milan Nikolic
26b04f44ad Remove properties 2025-06-13 21:35:27 +02:00
Milan Nikolic
f556285ad5 Move reader 2025-06-13 21:20:11 +02:00
Milan Nikolic
933b5eef22 Update handlers 2025-06-13 20:36:55 +02:00
Milan Nikolic
e5ee1a2049 Update url for websocket library 2025-06-13 20:10:12 +02:00
Milan Nikolic
7ed9c4c442 Add functions for rotate and timestamp 2025-06-13 19:51:23 +02:00
Milan Nikolic
711ad2f102 Add jpegli 2025-06-13 18:42:00 +02:00
Milan Nikolic
84135f3304 Add benchmark 2025-06-13 17:58:10 +02:00
Milan Nikolic
e01c80ca67 Update modules 2025-06-13 17:03:18 +02:00
Milan Nikolic
4a09c9b803 Change libjpeg library, rename tag 2025-06-13 17:01:56 +02:00
Milan Nikolic
948fe29079 Change image library 2025-06-12 21:58:20 +02:00
Milan Nikolic
074d14ad01 Drop support for cv2, rename tag 2025-06-12 21:19:52 +02:00
Milan Nikolic
b302c77f20 Use fork 2024-01-30 16:15:24 +01:00
Milan Nikolic
a1b32804da Build nocgo version by default 2024-01-30 16:03:59 +01:00
Milan Nikolic
880b41dea2 Remove video 2024-01-30 15:55:20 +01:00
Milan Nikolic
244b4f51fc Update dependencies 2024-01-30 15:53:45 +01:00
Milan Nikolic
c177a0bb77 Update modules 2023-03-21 10:22:12 +01:00
Milan Nikolic
071a6c4f3c Merge pull request #44 from stackcoder/patch-1
Listen on IPv4 and IPv6
2023-03-17 18:20:49 +01:00
stackcoder
30e30117a1 Listen on IPv4 and IPv6 2023-03-17 14:32:44 +01:00
Milan Nikolic
fd8152f7a4 Update modules 2023-03-07 11:39:56 +01:00
Milan Nikolic
7dc02de8f4 Update modules 2023-02-21 13:51:25 +01:00
Milan Nikolic
6e046d47d0 Update dependencies 2023-01-24 12:25:03 +01:00
Milan Nikolic
df2f672da6 Update Go version 2022-10-15 21:02:58 +02:00
Milan Nikolic
1ea7956db5 Update dependencies 2022-10-15 21:00:17 +02:00
Milan Nikolic
fa99c12ec1 Add new tags 2022-10-15 20:56:14 +02:00
Milan Nikolic
f58b475549 Use base64 fork 2022-10-15 20:55:52 +02:00
Milan Nikolic
d4ea63f95d Merge pull request #28 from Bothan-tarot/master
Handling IE11
2021-01-28 16:19:03 +01:00
thibault.dupuy
7b60039e66 Handle IE11 2021-01-21 15:51:16 +01:00
thibault.dupuy
89298de3e9 Handle IE11 2021-01-21 15:39:21 +01:00
Milan Nikolic
fc22ce5871 Support for Android, API >= 24, WIP 2020-11-03 19:36:26 +01:00
Milan Nikolic
19ea541157 Add support for Android, API >= 24, WIP 2020-04-14 15:52:33 +02:00
Milan Nikolic
be5863de1d Add darwin build 2019-10-06 21:46:23 +02:00
Milan Nikolic
7c3fe4be3c Update README.md 2019-10-06 16:13:52 +02:00
Milan Nikolic
cc5d3ad202 Format imports 2019-10-06 16:13:16 +02:00
Milan Nikolic
cb2f566154 Update README.md 2019-10-06 16:12:18 +02:00
Milan Nikolic
3b86bc0d69 Update Dockerfile 2019-10-06 06:52:35 +02:00
Milan Nikolic
8bfe9c787e Update README.md 2019-10-06 06:26:26 +02:00
Milan Nikolic
12b5f29452 Update version 2019-10-06 06:09:06 +02:00
Milan Nikolic
adf2a742e6 Add timestamp option 2019-10-06 05:48:12 +02:00
Milan Nikolic
1c342a67df Support env vars 2019-10-06 05:14:06 +02:00
Milan Nikolic
9e6d20863a Move main files to cmd 2019-10-06 03:29:40 +02:00
Milan Nikolic
3844a46486 Move options to file 2019-10-06 03:14:07 +02:00
Milan Nikolic
2628bbefbf Allow HEAD 2019-10-06 02:48:39 +02:00
Milan Nikolic
3b0b066cc1 Move context 2019-10-06 02:43:58 +02:00
Milan Nikolic
277d6c5b48 Add index 2019-10-06 02:41:34 +02:00
Milan Nikolic
1baf0deb39 Protect websocket when htpasswd is used 2019-10-06 02:06:27 +02:00
Milan Nikolic
50f28f57db Fix build tags 2019-10-06 02:00:31 +02:00
Milan Nikolic
b4e1af7729 Change websocket library 2019-10-06 01:52:13 +02:00
Milan Nikolic
1c8347fee8 Rename cv3 -> cv4 2019-10-05 23:00:52 +02:00
Milan Nikolic
63dd32115c Update go-opencv 2019-10-05 22:53:13 +02:00
Milan Nikolic
773b8920b0 Merge pull request #19 from RandomErrorMessage/master
bugfix + cleanup
2019-09-29 13:36:14 +02:00
RandomErrorMessage
01b9c564ac re-added goost after fixing upstream amd64 build bug 2019-09-28 18:41:23 -07:00
RandomErrorMessage
f1df08acc7 encoding/base64 is stdlib, re-ordered 2019-09-27 02:38:31 -07:00
RandomErrorMessage
5b7d51a9ac updated go modules, removed goost base64 dependency 2019-09-27 02:31:01 -07:00
RandomErrorMessage
6b28dfec25 removed unused function parameter 2019-09-27 02:30:44 -07:00
RandomErrorMessage
ff09bbeff4 bugfix: missing string addition in javascript 2019-09-27 02:30:29 -07:00
Milan Nikolic
4d73010019 Merge pull request #14 from iDigitalFlame/patch-1
Remove the string replacement of the bind address/port
2018-11-30 23:25:40 +01:00
iDigitalFlame
13385ba650 Remove the string replacement of the bind address/port
Removes the bind address in favor of using JS to auto detect the location and use instead.  "window.location.host" will point to the proper endpoint.
If the server is at "10.10.10.10", 'new WebSocket("ws://" + window.location.host + "/socket");' would be created at "ws://10.10.10.10". The "window.location.host" property also includes the port number as well.
This should fix the current open issue with websockets on HTML not rendering properly when the "-bind" option is omitted.
2018-11-30 21:42:03 +00:00
Milan Nikolic
09932a978c Update README.md 2018-11-17 21:18:31 +01:00
Milan Nikolic
fb1388a10a Add Dockerfile and image 2018-11-17 18:04:51 +01:00
Milan Nikolic
8ebd729dcd Add support for go modules 2018-11-17 18:00:29 +01:00
Milan Nikolic
309f6d339f Use fork with pkgconfig 2018-11-17 17:59:39 +01:00
Milan Nikolic
3d617687fa Update README.md 2018-10-29 16:38:26 +01:00
Milan Nikolic
d5870110d9 Add support for native VFW implementation on Windows 2018-10-29 16:27:46 +01:00
Milan Nikolic
72f12931da Update README.md 2018-10-28 16:43:55 +01:00
Milan Nikolic
bb676e4e44 New release 2018-10-28 14:37:20 +01:00
Milan Nikolic
ec60387edf Add support for native V4L implementation on Linux/RPi 2018-10-28 14:23:04 +01:00
Milan Nikolic
0503355005 Use asm implementation of base64 on amd64 2018-10-28 13:44:47 +01:00
Milan Nikolic
c9d77f03e7 Add rotate option 2018-10-10 04:32:00 +02:00
Milan Nikolic
37b19fcfe8 Update README.md 2018-09-08 20:17:32 +02:00
Milan Nikolic
5006c48690 Update README.md 2018-09-08 20:16:18 +02:00
Milan Nikolic
d340fa2dc3 Allow use of native image/jpeg 2018-07-17 14:51:18 +02:00
Milan Nikolic
fa5233255e Fix cv3 build 2018-07-02 17:31:12 +02:00
Milan Nikolic
ac555cbf7b Update README.md 2018-04-17 13:56:23 +02:00
Milan Nikolic
e1f03b55a1 Reuse frame 2018-03-14 14:33:01 +01:00
Milan Nikolic
3c0c949f31 Add support for OpenCV 3 2018-03-14 13:45:38 +01:00
Milan Nikolic
8a740337ab Update README.md 2018-01-29 21:39:17 +01:00
Milan Nikolic
cbc7d21b23 Shorten options 2018-01-27 03:50:50 +01:00
Milan Nikolic
45ca5bdedb New release 2018-01-27 03:28:09 +01:00
Milan Nikolic
173deebc88 Add video file reader 2018-01-27 03:22:33 +01:00
Milan Nikolic
7392d8d9b8 WebGL is now default 2018-01-27 01:59:46 +01:00
Milan Nikolic
1c9dfcb84c Move encoder 2018-01-27 01:47:04 +01:00
Milan Nikolic
f65a7cf1aa Fix golibjpegturbo missing repo 2018-01-10 11:39:04 +01:00
Milan Nikolic
a9c82d2d6e Add option to draw images with WebGL 2017-10-06 10:29:21 +02:00
Milan Nikolic
f36a47b9a2 update README.md 2017-10-05 21:49:41 +02:00
Milan Nikolic
29e9f6a562 Format HTML 2017-10-05 21:44:19 +02:00
Milan Nikolic
24fffe2c3f Format HTML, improve performance a bit 2017-10-05 20:40:33 +02:00
Milan Nikolic
04492f988a Update README.md 2017-10-05 19:57:03 +02:00
Milan Nikolic
3dc3aa1796 Remove CSS and add table 2017-10-05 17:36:20 +02:00
Milan Nikolic
f0e85361bc Add Android build 2017-10-04 03:17:00 +02:00
32 changed files with 1926 additions and 371 deletions

22
Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
FROM golang:alpine as build
WORKDIR /build
RUN apk add --no-cache git
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o cam2ip -trimpath -ldflags "-s -w" github.com/gen2brain/cam2ip/cmd/cam2ip
FROM scratch
COPY --from=build /build/cam2ip /cam2ip
EXPOSE 56000
ENTRYPOINT ["/cam2ip"]

View File

@@ -1,50 +1,72 @@
## cam2ip
Turn any webcam into ip camera.
Turn any webcam into an IP camera.
Example (in web browser):
http://localhost:56000/mjpeg
http://localhost:56000/html
or
http://localhost:56000/html
http://localhost:56000/mjpeg
You can also use apps like `ffplay` or `vlc`:
ffplay -i http://localhost:56000/mjpeg
### Requirements
* [OpenCV 2.x](http://opencv.org/)
* On Linux/RPi native Go [V4L](https://github.com/korandiz/v4l) implementation is used to capture images.
* On Windows [Video for Windows (VfW)](https://en.wikipedia.org/wiki/Video_for_Windows) framework is used over win32 API.
### Build tags
* `opencv` - use `OpenCV` library to access camera ([gocv](https://github.com/hybridgroup/gocv))
* `libjpeg` - build with `libjpeg` ([go-libjpeg](https://github.com/pixiv/go-libjpeg)) instead of native `image/jpeg`
* `jpegli` - build with `jpegli` ([jpegli](https://github.com/gen2brain/jpegli)) instead of native `image/jpeg`
### Download
Binaries are compiled with static OpenCV library:
- [Linux 64bit](https://github.com/gen2brain/cam2ip/releases/download/1.0/cam2ip-1.0-64bit.tar.gz)
- [Windows 32bit](https://github.com/gen2brain/cam2ip/releases/download/1.0/cam2ip-1.0.zip)
- [RPi 32bit](https://github.com/gen2brain/cam2ip/releases/download/1.0/cam2ip-1.0-RPi.tar.gz)
Download the latest binaries from the [releases](https://github.com/gen2brain/cam2ip/releases).
### Installation
go get -v github.com/gen2brain/cam2ip
go install github.com/gen2brain/cam2ip/cmd/cam2ip@latest
This will install app in `$GOPATH/bin/cam2ip`.
This command will install `cam2ip` in `GOBIN`, you can point `GOBIN` to e.g. `/usr/local/bin` or `~/.local/bin`.
### Run in Docker container
docker run --device=/dev/video0:/dev/video0 -p56000:56000 -it gen2brain/cam2ip # on RPi use gen2brain/cam2ip:arm
### Usage
```
Usage of ./cam2ip:
-bind-addr string
Bind address (default ":56000")
-delay int
Delay between frames, in milliseconds (default 10)
-frame-height float
Frame height (default 480)
-frame-width float
Frame width (default 640)
-htpasswd-file string
Path to htpasswd file, if empty auth is disabled
-index int
Camera index
Usage: cam2ip [<flags>]
--index
Camera index [CAM2IP_INDEX] (default "0")
--delay
Delay between frames, in milliseconds [CAM2IP_DELAY] (default "10")
--width
Frame width [CAM2IP_WIDTH] (default "640")
--height
Frame height [CAM2IP_HEIGHT] (default "480")
--quality
Image quality [CAM2IP_QUALITY] (default "75")
--rotate
Rotate image, valid values are 90, 180, 270 [CAM2IP_ROTATE] (default "0")
--flip
Flip image, valid values are horizontal and vertical [CAM2IP_FLIP] (default "")
--no-webgl
Disable WebGL drawing of image (html handler) [CAM2IP_NO_WEBGL] (default "false")
--timestamp
Draws timestamp on image [CAM2IP_TIMESTAMP] (default "false")
--time-format
Time format [CAM2IP_TIME_FORMAT] (default "2006-01-02 15:04:05")
--bind-addr
Bind address [CAM2IP_BIND_ADDR] (default ":56000")
--htpasswd-file
Path to htpasswd file, if empty auth is disabled [CAM2IP_HTPASSWD_FILE] (default "")
```
### Handlers

View File

@@ -1,58 +0,0 @@
package main
import (
"flag"
"fmt"
"os"
"github.com/gen2brain/cam2ip/camera"
"github.com/gen2brain/cam2ip/server"
)
const (
name = "cam2ip"
version = "1.0"
)
func main() {
srv := server.NewServer()
flag.IntVar(&srv.Index, "index", 0, "Camera index")
flag.IntVar(&srv.Delay, "delay", 10, "Delay between frames, in milliseconds")
flag.Float64Var(&srv.FrameWidth, "frame-width", 640, "Frame width")
flag.Float64Var(&srv.FrameHeight, "frame-height", 480, "Frame height")
flag.StringVar(&srv.Bind, "bind-addr", ":56000", "Bind address")
flag.StringVar(&srv.Htpasswd, "htpasswd-file", "", "Path to htpasswd file, if empty auth is disabled")
flag.Parse()
srv.Name = name
srv.Version = version
var err error
if srv.Htpasswd != "" {
if _, err = os.Stat(srv.Htpasswd); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
os.Exit(1)
}
}
srv.Camera, err = camera.NewCamera(srv.Index)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
os.Exit(1)
}
srv.Camera.SetProperty(camera.PropFrameWidth, srv.FrameWidth)
srv.Camera.SetProperty(camera.PropFrameHeight, srv.FrameHeight)
defer srv.Camera.Close()
fmt.Fprintf(os.Stderr, "Listening on %s\n", srv.Bind)
err = srv.ListenAndServe()
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
os.Exit(1)
}
}

View File

@@ -1,104 +1,99 @@
// Package camera.
package camera
import (
"bytes"
"fmt"
"image"
"github.com/lazywei/go-opencv/opencv"
)
// Property identifiers.
const (
PropPosMsec = iota
PropPosFrames
PropPosAviRatio
PropFrameWidth
PropFrameHeight
PropFps
PropFourcc
PropFrameCount
PropFormat
PropMode
PropBrightness
PropContrast
PropSaturation
PropHue
PropGain
PropExposure
PropConvertRgb
PropWhiteBalanceU
PropRectification
PropMonocrome
PropSharpness
PropAutoExposure
PropGamma
PropTemperature
PropTrigger
PropTriggerDelay
PropWhiteBalanceV
PropZoom
PropFocus
PropGuid
PropIsoSpeed
PropMaxDc1394
PropBacklight
PropPan
PropTilt
PropRoll
PropIris
PropSettings
PropBuffersize
// Options .
type Options struct {
Index int
Rotate int
Flip string
Width float64
Height float64
Timestamp bool
TimeFormat string
}
var (
yuy2FourCC = fourcc("YUY2")
yuyvFourCC = fourcc("YUYV")
mjpgFourCC = fourcc("MJPG")
)
// Camera represents camera.
type Camera struct {
Index int
camera *opencv.Capture
func fourcc(b string) uint32 {
return uint32(b[0]) | (uint32(b[1]) << 8) | (uint32(b[2]) << 16) | (uint32(b[3]) << 24)
}
// NewCamera returns new Camera for given camera index.
func NewCamera(index int) (camera *Camera, err error) {
camera = &Camera{}
camera.Index = index
func bmp24ToRgba(data []byte, dst *image.RGBA) error {
r := bytes.NewReader(data)
camera.camera = opencv.NewCameraCapture(index)
if camera.camera == nil {
err = fmt.Errorf("camera: can not open camera %d", index)
width := dst.Bounds().Dx()
height := dst.Bounds().Dy()
// There are 3 bytes per pixel, and each row is 4-byte aligned.
b := make([]byte, (3*width+3)&^3)
// BMP images are stored bottom-up rather than top-down.
for y := height - 1; y >= 0; y-- {
_, err := r.Read(b)
if err != nil {
return err
}
p := dst.Pix[y*dst.Stride : y*dst.Stride+width*4]
for i, j := 0, 0; i < len(p); i, j = i+4, j+3 {
// BMP images are stored in BGR order rather than RGB order.
p[i+0] = b[j+2]
p[i+1] = b[j+1]
p[i+2] = b[j+0]
p[i+3] = 0xFF
}
}
return
return nil
}
// Read reads next frame from camera and returns image.
func (c *Camera) Read() (img image.Image, err error) {
if c.camera.GrabFrame() {
frame := c.camera.RetrieveFrame(1)
img = frame.ToImage()
} else {
err = fmt.Errorf("camera: can not grab frame")
// yuy2ToYCbCr422 converts a YUY2 (YUYV) byte slice to an image.YCbCr with YCbCrSubsampleRatio422 (I422).
func yuy2ToYCbCr422(data []byte, dst *image.YCbCr) error {
if dst.SubsampleRatio != image.YCbCrSubsampleRatio422 {
return fmt.Errorf("subsample ratio must be 422, got %s", dst.SubsampleRatio.String())
}
return
}
width := dst.Bounds().Dx()
height := dst.Bounds().Dy()
// GetProperty returns the specified camera property.
func (c *Camera) GetProperty(id int) float64 {
return c.camera.GetProperty(id)
}
// SetProperty sets a camera property.
func (c *Camera) SetProperty(id int, value float64) int {
return c.camera.SetProperty(id, value)
}
// Close closes camera.
func (c *Camera) Close() (err error) {
if c.camera == nil {
err = fmt.Errorf("camera: camera is not opened")
if width%2 != 0 {
return fmt.Errorf("width must be even for YUY2")
}
c.camera.Release()
c.camera = nil
return
if len(data) != width*height*2 {
return fmt.Errorf("invalid data length for YUY2")
}
stride := width * 2 // 2 bytes per pixel
for y := 0; y < height; y++ {
for x := 0; x < width; x += 2 {
idx := y*stride + x*2
y0 := data[idx+0]
cb := data[idx+1]
y1 := data[idx+2]
cr := data[idx+3]
// Y plane: every pixel
dst.Y[y*dst.YStride+x+0] = y0
dst.Y[y*dst.YStride+x+1] = y1
// Cb/Cr plane: every 2 pixels (422)
off := y*dst.CStride + x/2
dst.Cb[off] = cb
dst.Cr[off] = cr
}
}
return nil
}

298
camera/camera_android.go Normal file
View File

@@ -0,0 +1,298 @@
//go:build android
// Package camera.
package camera
/*
#include <android/log.h>
#include <media/NdkImageReader.h>
#include <camera/NdkCameraDevice.h>
#include <camera/NdkCameraManager.h>
#define TAG "camera"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
AImage *image;
AImageReader *imageReader;
ANativeWindow *nativeWindow;
ACameraDevice *cameraDevice;
ACameraManager *cameraManager;
ACameraOutputTarget *cameraOutputTarget;
ACameraCaptureSession *cameraCaptureSession;
ACaptureRequest *captureRequest;
ACaptureSessionOutput *captureSessionOutput;
ACaptureSessionOutputContainer *captureSessionOutputContainer;
void device_on_disconnected(void *context, ACameraDevice *device) {
LOGI("camera %s is disconnected.\n", ACameraDevice_getId(device));
}
void device_on_error(void *context, ACameraDevice *device, int error) {
LOGE("error %d on camera %s.\n", error, ACameraDevice_getId(device));
}
ACameraDevice_stateCallbacks deviceStateCallbacks = {
.context = NULL,
.onDisconnected = device_on_disconnected,
.onError = device_on_error,
};
void session_on_ready(void *context, ACameraCaptureSession *session) {
LOGI("session is ready. %p\n", session);
}
void session_on_active(void *context, ACameraCaptureSession *session) {
LOGI("session is activated. %p\n", session);
}
void session_on_closed(void *context, ACameraCaptureSession *session) {
LOGI("session is closed. %p\n", session);
}
ACameraCaptureSession_stateCallbacks captureSessionStateCallbacks = {
.context = NULL,
.onActive = session_on_active,
.onReady = session_on_ready,
.onClosed = session_on_closed,
};
void image_callback(void *context, AImageReader *reader) {
LOGD("image_callback");
media_status_t status = AImageReader_acquireLatestImage(reader, &image);
if(status != AMEDIA_OK) {
LOGE("failed to acquire next image (reason: %d).\n", status);
}
}
AImageReader_ImageListener imageListener = {
.context = NULL,
.onImageAvailable = image_callback,
};
int openCamera(int index, int width, int height) {
ACameraIdList *cameraIdList;
const char *selectedCameraId;
camera_status_t status = ACAMERA_OK;
cameraManager = ACameraManager_create();
status = ACameraManager_getCameraIdList(cameraManager, &cameraIdList);
if(status != ACAMERA_OK) {
LOGE("failed to get camera id list (reason: %d).\n", status);
return status;
}
if(cameraIdList->numCameras < 1) {
LOGE("no camera device detected.\n");
}
if(cameraIdList->numCameras < index+1) {
LOGE("no camera at index %d.\n", index);
}
selectedCameraId = cameraIdList->cameraIds[index];
LOGI("open camera (id: %s, num of cameras: %d).\n", selectedCameraId, cameraIdList->numCameras);
status = ACameraManager_openCamera(cameraManager, selectedCameraId, &deviceStateCallbacks, &cameraDevice);
if(status != ACAMERA_OK) {
LOGE("failed to open camera device (id: %s)\n", selectedCameraId);
return status;
}
status = ACameraDevice_createCaptureRequest(cameraDevice, TEMPLATE_STILL_CAPTURE, &captureRequest);
if(status != ACAMERA_OK) {
LOGE("failed to create snapshot capture request (id: %s)\n", selectedCameraId);
return status;
}
status = ACaptureSessionOutputContainer_create(&captureSessionOutputContainer);
if(status != ACAMERA_OK) {
LOGE("failed to create session output container (id: %s)\n", selectedCameraId);
return status;
}
media_status_t mstatus = AImageReader_new(width, height, AIMAGE_FORMAT_YUV_420_888, 2, &imageReader);
if(mstatus != AMEDIA_OK) {
LOGE("failed to create image reader (reason: %d).\n", mstatus);
return mstatus;
}
mstatus = AImageReader_setImageListener(imageReader, &imageListener);
if(mstatus != AMEDIA_OK) {
LOGE("failed to set image listener (reason: %d).\n", mstatus);
return mstatus;
}
AImageReader_getWindow(imageReader, &nativeWindow);
ANativeWindow_acquire(nativeWindow);
ACameraOutputTarget_create(nativeWindow, &cameraOutputTarget);
ACaptureRequest_addTarget(captureRequest, cameraOutputTarget);
ACaptureSessionOutput_create(nativeWindow, &captureSessionOutput);
ACaptureSessionOutputContainer_add(captureSessionOutputContainer, captureSessionOutput);
status = ACameraDevice_createCaptureSession(cameraDevice, captureSessionOutputContainer, &captureSessionStateCallbacks, &cameraCaptureSession);
if(status != ACAMERA_OK) {
LOGE("failed to create capture session (reason: %d).\n", status);
return status;
}
ACameraManager_deleteCameraIdList(cameraIdList);
ACameraManager_delete(cameraManager);
return ACAMERA_OK;
}
int captureCamera() {
camera_status_t status = ACameraCaptureSession_capture(cameraCaptureSession, NULL, 1, &captureRequest, NULL);
if(status != ACAMERA_OK) {
LOGE("failed to capture image (reason: %d).\n", status);
}
return status;
}
int closeCamera() {
camera_status_t status = ACAMERA_OK;
if(captureRequest != NULL) {
ACaptureRequest_free(captureRequest);
captureRequest = NULL;
}
if(cameraOutputTarget != NULL) {
ACameraOutputTarget_free(cameraOutputTarget);
cameraOutputTarget = NULL;
}
if(cameraDevice != NULL) {
status = ACameraDevice_close(cameraDevice);
if(status != ACAMERA_OK) {
LOGE("failed to close camera device.\n");
return status;
}
cameraDevice = NULL;
}
if(captureSessionOutput != NULL) {
ACaptureSessionOutput_free(captureSessionOutput);
captureSessionOutput = NULL;
}
if(captureSessionOutputContainer != NULL) {
ACaptureSessionOutputContainer_free(captureSessionOutputContainer);
captureSessionOutputContainer = NULL;
}
if(imageReader != NULL) {
AImageReader_delete(imageReader);
imageReader = NULL;
}
if(image != NULL) {
AImage_delete(image);
image = NULL;
}
LOGI("camera closed.\n");
return ACAMERA_OK;
}
int openCamera(int index, int width, int height);
int captureCamera();
int closeCamera();
#cgo android CFLAGS: -D__ANDROID_API__=24
#cgo android LDFLAGS: -lcamera2ndk -lmediandk -llog -landroid
*/
import "C"
import (
"fmt"
"image"
"unsafe"
)
// Camera represents camera.
type Camera struct {
opts Options
img *image.YCbCr
}
// New returns new Camera for given camera index.
func New(opts Options) (camera *Camera, err error) {
camera = &Camera{}
camera.opts = opts
camera.img = image.NewYCbCr(image.Rect(0, 0, int(opts.Width), int(opts.Height)), image.YCbCrSubsampleRatio420)
ret := C.openCamera(C.int(opts.Index), C.int(opts.Width), C.int(opts.Height))
if int(ret) != 0 {
err = fmt.Errorf("camera: can not open camera %d: error %d", opts.Index, int(ret))
return
}
return
}
// Read reads next frame from camera and returns image.
func (c *Camera) Read() (img image.Image, err error) {
ret := C.captureCamera()
if int(ret) != 0 {
err = fmt.Errorf("camera: can not grab frame: error %d", int(ret))
return
}
if C.image == nil {
err = fmt.Errorf("camera: can not retrieve frame")
return
}
var yStride C.int
var yLen, cbLen, crLen C.int
var yPtr, cbPtr, crPtr *C.uint8_t
C.AImage_getPlaneRowStride(C.image, 0, &yStride)
C.AImage_getPlaneData(C.image, 0, &yPtr, &yLen)
C.AImage_getPlaneData(C.image, 1, &cbPtr, &cbLen)
C.AImage_getPlaneData(C.image, 2, &crPtr, &crLen)
c.img.YStride = int(yStride)
c.img.CStride = int(yStride) / 2
c.img.Y = C.GoBytes(unsafe.Pointer(yPtr), yLen)
c.img.Cb = C.GoBytes(unsafe.Pointer(cbPtr), cbLen)
c.img.Cr = C.GoBytes(unsafe.Pointer(crPtr), crLen)
img = c.img
return
}
// Close closes camera.
func (c *Camera) Close() (err error) {
ret := C.closeCamera()
if int(ret) != 0 {
err = fmt.Errorf("camera: can not close camera %d: error %d", c.opts.Index, int(ret))
return
}
return
}

166
camera/camera_linux.go Normal file
View File

@@ -0,0 +1,166 @@
//go:build !opencv && !android
// Package camera.
package camera
import (
"fmt"
"image"
"io"
"slices"
"github.com/korandiz/v4l"
im "github.com/gen2brain/cam2ip/image"
)
// Camera represents camera.
type Camera struct {
opts Options
camera *v4l.Device
config v4l.DeviceConfig
ycbcr *image.YCbCr
}
// New returns new Camera for given camera index.
func New(opts Options) (c *Camera, err error) {
c = &Camera{}
c.opts = opts
devices := v4l.FindDevices()
if len(devices) < opts.Index+1 {
err = fmt.Errorf("camera: no camera at index %d", opts.Index)
return
}
c.camera, err = v4l.Open(devices[opts.Index].Path)
if err != nil {
err = fmt.Errorf("camera: %w", err)
return
}
if c.camera == nil {
err = fmt.Errorf("camera: can not open camera %d", opts.Index)
return
}
configs, e := c.camera.ListConfigs()
if e != nil {
err = fmt.Errorf("camera: can not list configs: %w", e)
return
}
formats := make([]uint32, 0)
for _, config := range configs {
formats = append(formats, config.Format)
}
c.config, err = c.camera.GetConfig()
if err != nil {
err = fmt.Errorf("camera: can not get config: %w", err)
return
}
if slices.Contains(formats, mjpgFourCC) {
c.config.Format = mjpgFourCC
} else if slices.Contains(formats, yuyvFourCC) {
c.config.Format = yuyvFourCC
} else {
err = fmt.Errorf("camera: unsupported format %d", c.config.Format)
return
}
c.config.Width = int(opts.Width)
c.config.Height = int(opts.Height)
err = c.camera.SetConfig(c.config)
if err != nil {
err = fmt.Errorf("camera: format %d: can not set config: %w", c.config.Format, err)
return
}
if c.config.Format == yuyvFourCC {
c.ycbcr = image.NewYCbCr(image.Rect(0, 0, int(c.opts.Width), int(c.opts.Height)), image.YCbCrSubsampleRatio422)
}
err = c.camera.TurnOn()
if err != nil {
err = fmt.Errorf("camera: format %d: can not turn on: %w", c.config.Format, err)
return
}
return
}
// Read reads next frame from camera and returns image.
func (c *Camera) Read() (img image.Image, err error) {
buffer, err := c.camera.Capture()
if err != nil {
err = fmt.Errorf("camera: format %d: can not grab frame: %w", c.config.Format, err)
return
}
switch c.config.Format {
case yuy2FourCC, yuyvFourCC:
data, e := io.ReadAll(buffer)
if e != nil {
err = fmt.Errorf("camera: format %d: can not read buffer: %w", c.config.Format, e)
return
}
e = yuy2ToYCbCr422(data, c.ycbcr)
if e != nil {
err = fmt.Errorf("camera: format %d: can not retrieve frame: %w", c.config.Format, e)
return
}
img = c.ycbcr
case mjpgFourCC:
img, err = im.NewDecoder(buffer).Decode()
if err != nil {
err = fmt.Errorf("camera: format %d: can not decode frame: %w", c.config.Format, err)
return
}
}
if c.opts.Rotate != 0 {
img = im.Rotate(img, c.opts.Rotate)
}
if c.opts.Flip != "" {
img = im.Flip(img, c.opts.Flip)
}
if c.opts.Timestamp {
img = im.Timestamp(img, c.opts.TimeFormat)
}
return
}
// Close closes camera.
func (c *Camera) Close() (err error) {
if c.camera == nil {
err = fmt.Errorf("camera: close: camera is not opened")
return
}
c.camera.TurnOff()
c.camera.Close()
c.camera = nil
return
}

102
camera/camera_opencv.go Normal file
View File

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

View File

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

427
camera/camera_windows.go Normal file
View File

@@ -0,0 +1,427 @@
//go:build !opencv
// Package camera.
package camera
import (
"bytes"
"fmt"
"image"
"runtime"
"syscall"
"unsafe"
im "github.com/gen2brain/cam2ip/image"
)
func init() {
runtime.LockOSThread()
}
// Camera represents camera.
type Camera struct {
opts Options
camera syscall.Handle
rgba *image.RGBA
ycbcr *image.YCbCr
hdr *videoHdr
instance syscall.Handle
className string
format uint32
}
// New returns new Camera for given camera index.
func New(opts Options) (camera *Camera, err error) {
camera = &Camera{}
camera.opts = opts
camera.className = "capWindowClass"
go func(c *Camera) {
fn := func(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
case wmClose:
_ = destroyWindow(hwnd)
case wmDestroy:
postQuitMessage(0)
default:
ret := defWindowProc(hwnd, msg, wparam, lparam)
return ret
}
return 0
}
c.instance, err = getModuleHandle()
if err != nil {
return
}
err = registerClass(c.className, c.instance, fn)
if err != nil {
return
}
hwnd, e := createWindow(0, c.className, "", wsOverlappedWindow, cwUseDefault, cwUseDefault,
int64(c.opts.Width)+100, int64(c.opts.Height)+100, 0, 0, c.instance)
if e != nil {
err = e
return
}
c.camera, err = capCreateCaptureWindow("", wsChild, 0, 0, int64(c.opts.Width), int64(c.opts.Height), hwnd, 0)
if err != nil {
return
}
ret := sendMessage(c.camera, wmCapDriverConnect, uintptr(c.opts.Index), 0)
if int(ret) == 0 {
err = fmt.Errorf("camera: can not open camera %d", c.opts.Index)
return
}
sendMessage(c.camera, wmCapSetPreview, 0, 0)
sendMessage(c.camera, wmCapSetOverlay, 0, 0)
var bi bitmapInfo
size := sendMessage(c.camera, wmCapGetVideoformat, 0, 0)
sendMessage(c.camera, wmCapGetVideoformat, size, uintptr(unsafe.Pointer(&bi)))
bi.BmiHeader.BiWidth = int32(c.opts.Width)
bi.BmiHeader.BiHeight = int32(c.opts.Height)
ret = sendMessage(c.camera, wmCapSetVideoformat, size, uintptr(unsafe.Pointer(&bi)))
if int(ret) == 0 {
err = fmt.Errorf("camera: can not set video format: %dx%d, %d", int(c.opts.Width), int(c.opts.Height), c.format)
return
}
c.format = bi.BmiHeader.BiCompression
sendMessage(c.camera, wmCapSetCallbackFrame, 0, syscall.NewCallback(c.callback))
switch c.format {
case 0:
if bi.BmiHeader.BiBitCount != 24 {
err = fmt.Errorf("camera: unsupported format %d; bitcount: %d", c.format, bi.BmiHeader.BiBitCount)
return
}
c.rgba = image.NewRGBA(image.Rect(0, 0, int(c.opts.Width), int(c.opts.Height)))
case yuy2FourCC, yuyvFourCC:
c.ycbcr = image.NewYCbCr(image.Rect(0, 0, int(c.opts.Width), int(c.opts.Height)), image.YCbCrSubsampleRatio422)
case mjpgFourCC:
default:
err = fmt.Errorf("camera: unsupported format %d", c.format)
return
}
for {
var msg msgW
ok, _ := getMessage(&msg, 0, 0, 0)
if ok {
dispatchMessage(&msg)
} else {
break
}
}
return
}(camera)
return
}
// Read reads next frame from camera and returns image.
func (c *Camera) Read() (img image.Image, err error) {
ret := sendMessage(c.camera, wmCapGrabFrame, 0, 0)
if int(ret) == 0 {
err = fmt.Errorf("camera: can not grab frame")
return
}
data := unsafe.Slice((*byte)(unsafe.Pointer(c.hdr.LpData)), c.hdr.DwBufferLength)
switch c.format {
case 0:
e := bmp24ToRgba(data, c.rgba)
if e != nil {
err = fmt.Errorf("camera: format %d: can not retrieve frame: %w", c.format, e)
return
}
img = c.rgba
case yuy2FourCC, yuyvFourCC:
e := yuy2ToYCbCr422(data, c.ycbcr)
if e != nil {
err = fmt.Errorf("camera: format %d: can not retrieve frame: %w", c.format, e)
return
}
img = c.ycbcr
case mjpgFourCC:
i, e := im.NewDecoder(bytes.NewReader(data)).Decode()
if e != nil {
err = fmt.Errorf("camera: format %d: can not retrieve frame: %w", c.format, e)
return
}
img = i
}
if c.opts.Rotate != 0 {
img = im.Rotate(img, c.opts.Rotate)
}
if c.opts.Flip != "" {
img = im.Flip(img, c.opts.Flip)
}
if c.opts.Timestamp {
img = im.Timestamp(img, c.opts.TimeFormat)
}
return
}
// Close closes camera.
func (c *Camera) Close() (err error) {
sendMessage(c.camera, wmCapSetCallbackFrame, 0, 0)
unregisterClass(c.className, c.instance)
sendMessage(c.camera, wmCapDriverDisconnect, 0, 0)
return destroyWindow(c.camera)
}
// callback function.
func (c *Camera) callback(hwnd syscall.Handle, hdr *videoHdr) uintptr {
if hdr != nil {
c.hdr = hdr
}
return 0
}
var (
user32 = syscall.NewLazyDLL("user32.dll")
kernel32 = syscall.NewLazyDLL("kernel32.dll")
avicap32 = syscall.NewLazyDLL("avicap32.dll")
createWindowExW = user32.NewProc("CreateWindowExW")
destroyWindowW = user32.NewProc("DestroyWindow")
defWindowProcW = user32.NewProc("DefWindowProcW")
dispatchMessageW = user32.NewProc("DispatchMessageW")
getMessageW = user32.NewProc("GetMessageW")
sendMessageW = user32.NewProc("SendMessageW")
postQuitMessageW = user32.NewProc("PostQuitMessage")
registerClassExW = user32.NewProc("RegisterClassExW")
unregisterClassW = user32.NewProc("UnregisterClassW")
getModuleHandleW = kernel32.NewProc("GetModuleHandleW")
capCreateCaptureWindowW = avicap32.NewProc("capCreateCaptureWindowW")
)
const (
wmDestroy = 0x0002
wmClose = 0x0010
wmUser = 0x0400
wmCapStart = wmUser
wmCapSetCallbackFrame = wmCapStart + 5
wmCapDriverConnect = wmCapStart + 10
wmCapDriverDisconnect = wmCapStart + 11
wmCapGetVideoformat = wmCapStart + 44
wmCapSetVideoformat = wmCapStart + 45
wmCapSetPreview = wmCapStart + 50
wmCapSetOverlay = wmCapStart + 51
wmCapGrabFrame = wmCapStart + 60
wmCapGrabFrameNoStop = wmCapStart + 61
wmCapStop = wmCapStart + 68
wmCapAbort = wmCapStart + 69
wsChild = 0x40000000
wsOverlappedWindow = 0x00CF0000
cwUseDefault = 0x7fffffff
)
// wndClassExW https://msdn.microsoft.com/en-us/library/windows/desktop/ms633577.aspx
type wndClassExW struct {
size uint32
style uint32
wndProc uintptr
clsExtra int32
wndExtra int32
instance syscall.Handle
icon syscall.Handle
cursor syscall.Handle
background syscall.Handle
menuName *uint16
className *uint16
iconSm syscall.Handle
}
// msgW https://msdn.microsoft.com/en-us/library/windows/desktop/ms644958.aspx
type msgW struct {
hwnd syscall.Handle
message uint32
wParam uintptr
lParam uintptr
time uint32
pt pointW
}
// https://msdn.microsoft.com/en-us/ecb0f0e1-90c2-48ab-a069-552262b49c7c
type pointW struct {
x, y int32
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183376.aspx
type bitmapInfoHeader struct {
BiSize uint32
BiWidth int32
BiHeight int32
BiPlanes uint16
BiBitCount uint16
BiCompression uint32
BiSizeImage uint32
BiXPelsPerMeter int32
BiYPelsPerMeter int32
BiClrUsed uint32
BiClrImportant uint32
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183375.aspx
type bitmapInfo struct {
BmiHeader bitmapInfoHeader
BmiColors *rgbQuad
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd162938.aspx
type rgbQuad struct {
RgbBlue byte
RgbGreen byte
RgbRed byte
RgbReserved byte
}
// https://docs.microsoft.com/en-us/windows/desktop/api/vfw/ns-vfw-videohdr_tag
type videoHdr struct {
LpData *uint8
DwBufferLength uint32
DwBytesUsed uint32
DwTimeCaptured uint32
DwUser uint64
DwFlags uint32
DwReserved [4]uint64
}
// https://docs.microsoft.com/en-us/windows/desktop/api/libloaderapi/nf-libloaderapi-getmodulehandlea
func getModuleHandle() (syscall.Handle, error) {
ret, _, err := getModuleHandleW.Call(uintptr(0))
if ret == 0 {
return 0, err
}
return syscall.Handle(ret), nil
}
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-createwindowexw
func createWindow(exStyle uint64, className, windowName string, style uint64, x, y, width, height int64,
parent, menu, instance syscall.Handle) (syscall.Handle, error) {
ret, _, err := createWindowExW.Call(uintptr(exStyle), uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(className))),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(windowName))), uintptr(style), uintptr(x), uintptr(y),
uintptr(width), uintptr(height), uintptr(parent), uintptr(menu), uintptr(instance), uintptr(0))
if ret == 0 {
return 0, err
}
return syscall.Handle(ret), nil
}
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-destroywindow
func destroyWindow(hwnd syscall.Handle) error {
ret, _, err := destroyWindowW.Call(uintptr(hwnd))
if ret == 0 {
return err
}
return nil
}
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-defwindowprocw
func defWindowProc(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
ret, _, _ := defWindowProcW.Call(uintptr(hwnd), uintptr(msg), wparam, lparam)
return ret
}
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-dispatchmessagew
func dispatchMessage(msg *msgW) {
dispatchMessageW.Call(uintptr(unsafe.Pointer(msg)))
}
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getmessagew
func getMessage(msg *msgW, hwnd syscall.Handle, msgFilterMin, msgFilterMax uint32) (bool, error) {
ret, _, err := getMessageW.Call(uintptr(unsafe.Pointer(msg)), uintptr(hwnd), uintptr(msgFilterMin), uintptr(msgFilterMax))
if int32(ret) == -1 {
return false, err
}
return int32(ret) != 0, nil
}
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-sendmessage
func sendMessage(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
ret, _, _ := sendMessageW.Call(uintptr(hwnd), uintptr(msg), wparam, lparam, 0, 0)
return ret
}
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-postquitmessage
func postQuitMessage(exitCode int32) {
postQuitMessageW.Call(uintptr(exitCode))
}
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-registerclassexw
func registerClass(className string, instance syscall.Handle, fn interface{}) error {
var wcx wndClassExW
wcx.size = uint32(unsafe.Sizeof(wcx))
wcx.wndProc = syscall.NewCallback(fn)
wcx.instance = instance
wcx.className = syscall.StringToUTF16Ptr(className)
ret, _, err := registerClassExW.Call(uintptr(unsafe.Pointer(&wcx)))
if ret == 0 {
return err
}
return nil
}
// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-unregisterclassw
func unregisterClass(className string, instance syscall.Handle) bool {
ret, _, _ := unregisterClassW.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(className))), uintptr(instance))
return ret != 0
}
// https://docs.microsoft.com/en-us/windows/desktop/api/vfw/nf-vfw-capcreatecapturewindoww
func capCreateCaptureWindow(lpszWindowName string, dwStyle, x, y, width, height int64, parent syscall.Handle, id int64) (syscall.Handle, error) {
ret, _, err := capCreateCaptureWindowW.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(lpszWindowName))),
uintptr(dwStyle), uintptr(x), uintptr(y), uintptr(width), uintptr(height), uintptr(parent), uintptr(id))
if ret == 0 {
return 0, err
}
return syscall.Handle(ret), nil
}

View File

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

90
cmd/cam2ip/main.go Normal file
View File

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

25
go.mod Normal file
View File

@@ -0,0 +1,25 @@
module github.com/gen2brain/cam2ip
require (
github.com/abbot/go-http-auth v0.4.0
github.com/anthonynsimon/bild v0.14.0
github.com/coder/websocket v1.8.13
github.com/gen2brain/base64 v0.0.0-20221015184129-317a5c93030c
github.com/gen2brain/jpegli v0.3.4
github.com/korandiz/v4l v1.1.0
github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567
github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d
go.senan.xyz/flagconf v0.1.9
gocv.io/x/gocv v0.35.0
)
require (
github.com/tetratelabs/wazero v1.9.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.19.0 // indirect
)
go 1.23
toolchain go1.24.3

34
go.sum Normal file
View File

@@ -0,0 +1,34 @@
github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0=
github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM=
github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync=
github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs=
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/gen2brain/base64 v0.0.0-20221015184129-317a5c93030c h1:TUjjeJ2rV4KZxH6hIEi/boEQB3v6aKvwdakUJR3AwiE=
github.com/gen2brain/base64 v0.0.0-20221015184129-317a5c93030c/go.mod h1:VG58IUyxPWojCtGwqwoZ/6LLXwClu1tssqa5ktOxI9o=
github.com/gen2brain/jpegli v0.3.4 h1:wFoUHIjfPJGGeuW3r9dqy0MTT1TtvJuWf6EqfHPPGFM=
github.com/gen2brain/jpegli v0.3.4/go.mod h1:tVnF7NPyufTo8noFlW5lurUUwZW8trwBENOItzuk2BM=
github.com/hybridgroup/mjpeg v0.0.0-20140228234708-4680f319790e/go.mod h1:eagM805MRKrioHYuU7iKLUyFPVKqVV6um5DAvCkUtXs=
github.com/korandiz/v4l v1.1.0 h1:VbzaWlhqNzVPfHEYEM+V8T7184ndiEzljJgDHSHc7pc=
github.com/korandiz/v4l v1.1.0/go.mod h1:pftxPG7hkuUgepioAY6PAE81mShaVjzd95X/WF4Izus=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567 h1:pKjmNHL7BCXhgsnSlN6Ov3WAN2jbJMCx6IvrMN9GNfc=
github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567/go.mod h1:ytYavTmrpWG4s7UOfDhP6m4ASL5XA66nrOcUn1e2M78=
github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d h1:ls+7AYarUlUSetfnN/DKVNcK6W8mQWc6VblmOm4XwX0=
github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d/go.mod h1:DO7ixpslN6XfbWzeNH9vkS5CF2FQUX81B85rYe9zDxU=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
go.senan.xyz/flagconf v0.1.9 h1:LBDmqiVFgijfqFXDzH97gPn0qDbg1Dq6/vxsxS/TzC4=
go.senan.xyz/flagconf v0.1.9/go.mod h1:NqOFfSwJvNWXOTUabcRZ8mPK9+sJmhStJhqtEt74wNQ=
gocv.io/x/gocv v0.35.0 h1:Qaxb5KdVyy8Spl4S4K0SMZ6CVmKtbfoSGQAxRD3FZlw=
gocv.io/x/gocv v0.35.0/go.mod h1:oc6FvfYqfBp99p+yOEzs9tbYF9gOrAQSeL/dyIPefJU=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=

View File

@@ -1,4 +1,3 @@
// Package handlers.
package handlers
import (
@@ -13,19 +12,17 @@ type HTML struct {
}
// NewHTML returns new HTML handler.
func NewHTML(bind string, width, height float64) *HTML {
func NewHTML(width, height float64, noWebGL bool) *HTML {
h := &HTML{}
b := strings.Split(bind, ":")
if b[0] == "" {
bind = "127.0.0.1" + bind
tpl := htmlWebGL
if noWebGL {
tpl = html
}
tpl = strings.Replace(tpl, "{WIDTH}", fmt.Sprintf("%.0f", width), -1)
tpl = strings.Replace(tpl, "{HEIGHT}", fmt.Sprintf("%.0f", height), -1)
html = strings.Replace(html, "{BIND}", bind, -1)
html = strings.Replace(html, "{WIDTH}", fmt.Sprintf("%.0f", width), -1)
html = strings.Replace(html, "{HEIGHT}", fmt.Sprintf("%.0f", height), -1)
h.Template = []byte(html)
h.Template = []byte(tpl)
return h
}
@@ -34,76 +31,148 @@ func (h *HTML) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" && r.Method != "HEAD" {
msg := fmt.Sprintf("405 Method Not Allowed (%s)", r.Method)
http.Error(w, msg, http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(h.Template)
_, _ = w.Write(h.Template)
}
var html = `<html>
<head>
<meta charset="utf-8"/>
<title>cam2ip</title>
<style>
body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
background-color: #000000;
}
div {
width: 100%;
height: 100%;
position: relative;
}
canvas {
height: auto;
width: auto;
max-height: 100%;
max-width: 100%;
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
box-sizing: border-box;
margin: auto;
}
</style>
<script>
var url = "ws://{BIND}/socket";
ws = new WebSocket(url);
if (location.protocol === 'https:') {
ws = new WebSocket("wss://" + window.location.host + "/socket");
} else {
ws = new WebSocket("ws://" + window.location.host + "/socket");
}
var image = new Image();
ws.onopen = function() {
console.log("onopen");
var context = document.getElementById("canvas").getContext("2d", {alpha: false});
image.onload = function() {
context.drawImage(image, 0, 0);
}
}
ws.onmessage = function(e) {
var context = document.getElementById("canvas").getContext("2d");
var image = new Image();
image.onload = function() {
context.drawImage(image, 0, 0);
}
image.setAttribute("src", "data:image/jpeg;base64," + e.data);
}
ws.onclose = function(e) {
console.log("onclose");
}
ws.onerror = function(e) {
console.log("onerror");
}
</script>
</head>
<body>
<div><canvas id="canvas" width="{WIDTH}" height="{HEIGHT}"></canvas></div>
<body style="background-color: #000000">
<table style="width:100%; height:100%">
<tr style="height:100%">
<td style="height:100%; text-align:center">
<canvas id="canvas" width="{WIDTH}" height="{HEIGHT}"></canvas>
</td>
</tr>
</table>
</body>
</html>`
var htmlWebGL = `<html>
<head>
<meta charset="utf-8"/>
<title>cam2ip</title>
<script>
var texture, vloc, tloc, vertexBuff, textureBuff;
if (location.protocol === 'https:') {
ws = new WebSocket("wss://" + window.location.host + "/socket");
} else {
ws = new WebSocket("ws://" + window.location.host + "/socket");
}
var image = new Image();
ws.onopen = function() {
var gl = document.getElementById('canvas').getContext('webgl',{antialias:false}) || canvas.getContext('experimental-webgl');
var vertexShaderSrc =
"attribute vec2 aVertex;" +
"attribute vec2 aUV;" +
"varying vec2 vTex;" +
"void main(void) {" +
" gl_Position = vec4(aVertex, 0.0, 1.0);" +
" vTex = aUV;" +
"}";
var fragmentShaderSrc =
"precision mediump float;" +
"varying vec2 vTex;" +
"uniform sampler2D sampler0;" +
"void main(void){" +
" gl_FragColor = texture2D(sampler0, vTex);"+
"}";
var vertShaderObj = gl.createShader(gl.VERTEX_SHADER);
var fragShaderObj = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(vertShaderObj, vertexShaderSrc);
gl.shaderSource(fragShaderObj, fragmentShaderSrc);
gl.compileShader(vertShaderObj);
gl.compileShader(fragShaderObj);
var program = gl.createProgram();
gl.attachShader(program, vertShaderObj);
gl.attachShader(program, fragShaderObj);
gl.linkProgram(program);
gl.useProgram(program);
gl.viewport(0, 0, {WIDTH}, {HEIGHT});
vertexBuff = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuff);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, 1, -1, -1, 1, -1, 1, 1]), gl.STATIC_DRAW);
textureBuff = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, textureBuff);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 1, 0, 0, 1, 0, 1, 1]), gl.STATIC_DRAW);
vloc = gl.getAttribLocation(program, "aVertex");
tloc = gl.getAttribLocation(program, "aUV");
texture = gl.createTexture();
image.onload = function() {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuff);
gl.enableVertexAttribArray(vloc);
gl.vertexAttribPointer(vloc, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, textureBuff);
gl.enableVertexAttribArray(tloc);
gl.vertexAttribPointer(tloc, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
}
}
ws.onmessage = function(e) {
image.setAttribute("src", "data:image/jpeg;base64," + e.data);
}
</script>
</head>
<body style="background-color: #000000">
<table style="width:100%; height:100%">
<tr style="height:100%">
<td style="height:100%; text-align:center">
<canvas id="canvas" width="{WIDTH}" height="{HEIGHT}"></canvas>
</td>
</tr>
</table>
</body>
</html>`

34
handlers/index.go Normal file
View File

@@ -0,0 +1,34 @@
// Package handlers provides HTTP handlers for the cam2ip application.
package handlers
import (
"net/http"
)
// Index handler.
type Index struct {
}
// NewIndex returns new Index handler.
func NewIndex() *Index {
return &Index{}
}
// ServeHTTP handles requests on incoming connections.
func (i *Index) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" && r.Method != "HEAD" {
http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed)
return
}
_, _ = w.Write([]byte(`<html>
<head><title>cam2ip</title></head>
<body>
<h1>cam2ip</h1>
<p><a href='/html'>html</a></p>
<p><a href='/jpeg'>jpeg</a></p>
<p><a href='/mjpeg'>mjpeg</a></p>
</body>
</html>`))
}

View File

@@ -4,23 +4,25 @@ import (
"log"
"net/http"
"github.com/gen2brain/cam2ip/camera"
"github.com/gen2brain/cam2ip/image"
)
// JPEG handler.
type JPEG struct {
camera *camera.Camera
reader ImageReader
quality int
}
// NewJPEG returns new JPEG handler.
func NewJPEG(camera *camera.Camera) *JPEG {
return &JPEG{camera}
func NewJPEG(reader ImageReader, quality int) *JPEG {
return &JPEG{reader, quality}
}
// ServeHTTP handles requests on incoming connections.
func (j *JPEG) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
if r.Method != "GET" && r.Method != "HEAD" {
http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed)
return
}
@@ -28,17 +30,19 @@ func (j *JPEG) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Cache-Control", "no-store, no-cache")
w.Header().Add("Content-Type", "image/jpeg")
img, err := j.camera.Read()
img, err := j.reader.Read()
if err != nil {
log.Printf("jpeg: read: %v", err)
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
return
}
enc := camera.NewEncoder(w)
err = enc.Encode(img)
err = image.NewEncoder(w, j.quality).Encode(img)
if err != nil {
log.Printf("jpeg: encode: %v", err)
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
return
}
}

View File

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

14
handlers/reader.go Normal file
View File

@@ -0,0 +1,14 @@
package handlers
import (
"image"
)
// ImageReader interface
type ImageReader interface {
// Read reads next frame from camera/video and returns image.
Read() (img image.Image, err error)
// Close closes camera/video.
Close() error
}

View File

@@ -2,52 +2,65 @@ package handlers
import (
"bytes"
"encoding/base64"
"context"
"log"
"net/http"
"time"
"golang.org/x/net/websocket"
"github.com/coder/websocket"
"github.com/gen2brain/cam2ip/camera"
"github.com/gen2brain/cam2ip/image"
)
// Socket handler.
type Socket struct {
camera *camera.Camera
delay int
reader ImageReader
delay int
quality int
}
// NewSocket returns new socket handler.
func NewSocket(camera *camera.Camera, delay int) websocket.Handler {
s := &Socket{camera, delay}
return websocket.Handler(s.write)
func NewSocket(reader ImageReader, delay, quality int) *Socket {
return &Socket{reader, delay, quality}
}
// write writes images to socket
func (s *Socket) write(ws *websocket.Conn) {
// ServeHTTP handles requests on incoming connections.
func (s *Socket) ServeHTTP(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Accept(w, r, nil)
if err != nil {
log.Printf("socket: accept: %v", err)
return
}
ctx := context.Background()
for {
img, err := s.camera.Read()
img, err := s.reader.Read()
if err != nil {
log.Printf("socket: read: %v", err)
continue
break
}
w := new(bytes.Buffer)
enc := camera.NewEncoder(w)
err = enc.Encode(img)
err = image.NewEncoder(w, s.quality).Encode(img)
if err != nil {
log.Printf("socket: encode: %v", err)
continue
}
b64 := base64.StdEncoding.EncodeToString(w.Bytes())
b64 := image.EncodeToString(w.Bytes())
_, err = ws.Write([]byte(b64))
err = conn.Write(ctx, websocket.MessageText, []byte(b64))
if err != nil {
break
}
time.Sleep(time.Duration(s.delay) * time.Millisecond)
if s.delay > 0 {
time.Sleep(time.Duration(s.delay) * time.Millisecond)
}
}
_ = conn.Close(websocket.StatusNormalClosure, "")
}

11
image/base64.go Normal file
View File

@@ -0,0 +1,11 @@
//go:build !amd64
package image
import (
"encoding/base64"
)
func EncodeToString(src []byte) string {
return base64.StdEncoding.EncodeToString(src)
}

11
image/base64_amd64.go Normal file
View File

@@ -0,0 +1,11 @@
//go:build amd64
package image
import (
"github.com/gen2brain/base64"
)
func EncodeToString(src []byte) string {
return base64.StdEncoding.EncodeToString(src)
}

25
image/decode.go Normal file
View File

@@ -0,0 +1,25 @@
//go:build !libjpeg && !jpegli
// Package image.
package image
import (
"image"
"image/jpeg"
"io"
)
// NewDecoder returns a new Decoder.
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{r}
}
// Decoder struct.
type Decoder struct {
r io.Reader
}
// Decode decodes image from JPEG.
func (d Decoder) Decode() (image.Image, error) {
return jpeg.Decode(d.r)
}

31
image/decode_jpegli.go Normal file
View File

@@ -0,0 +1,31 @@
//go:build jpegli
// Package image.
package image
import (
"image"
"io"
"github.com/gen2brain/jpegli"
)
// NewDecoder returns a new Decoder.
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{r}
}
// Decoder struct.
type Decoder struct {
r io.Reader
}
// Decode decodes image from JPEG.
func (d Decoder) Decode() (image.Image, error) {
return jpegli.DecodeWithOptions(d.r, &jpegli.DecodingOptions{
DCTMethod: jpegli.DCTIFast,
FancyUpsampling: false,
BlockSmoothing: false,
ArithCode: true,
})
}

30
image/decode_libjpeg.go Normal file
View File

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

31
image/encode.go Normal file
View File

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

36
image/encode_jpegli.go Normal file
View File

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

32
image/encode_libjpeg.go Normal file
View File

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

48
image/image.go Normal file
View File

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

43
image/image_test.go Normal file
View File

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

BIN
image/testdata/test.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -2,29 +2,70 @@
CHROOT="/usr/x86_64-pc-linux-gnu-static"
MINGW="/usr/i686-w64-mingw32"
MINGW64="/usr/x86_64-w64-mingw32"
RPI="/usr/armv6j-hardfloat-linux-gnueabi"
RPI3="/usr/armv7a-hardfloat-linux-gnueabi"
APPLE="/usr/x86_64-apple-darwin14"
ANDROID="/usr/arm-linux-androideabi"
mkdir -p build
LIBRARY_PATH="$CHROOT/usr/lib:$CHROOT/lib" \
PKG_CONFIG_PATH="$CHROOT/usr/lib/pkgconfig" \
PKG_CONFIG_LIBDIR="$CHROOT/usr/lib/pkgconfig" \
CGO_LDFLAGS="-L$CHROOT/usr/lib -L$CHROOT/lib" \
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -v -x -o build/cam2ip.amd64 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip
LIBRARY_PATH="$CHROOT/usr/lib64:$CHROOT/lib64" \
PKG_CONFIG_PATH="$CHROOT/usr/lib64/pkgconfig" \
PKG_CONFIG_LIBDIR="$CHROOT/usr/lib64/pkgconfig" \
CGO_LDFLAGS="-L$CHROOT/usr/lib64 -L$CHROOT/lib" \
CGO_CFLAGS="-I$CHROOT/usr/include" \
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -tags cv2,turbo -o build/cam2ip.linux.amd64.cv2 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip/cmd/cam2ip
LIBRARY_PATH="$CHROOT/usr/lib64:$CHROOT/lib64" \
PKG_CONFIG_PATH="$CHROOT/usr/lib64/pkgconfig" \
PKG_CONFIG_LIBDIR="$CHROOT/usr/lib64/pkgconfig" \
CGO_LDFLAGS="-L$CHROOT/usr/lib64 -L$CHROOT/lib64" \
CGO_CFLAGS="-I$CHROOT/usr/include" \
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o build/cam2ip.linux.amd64 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip/cmd/cam2ip
PKG_CONFIG="/usr/bin/i686-w64-mingw32-pkg-config" \
PKG_CONFIG_PATH="$MINGW/usr/lib/pkgconfig" \
PKG_CONFIG_LIBDIR="$MINGW/usr/lib/pkgconfig" \
CGO_LDFLAGS="-L$MINGW/usr/lib -L$MINGW/usr/include" \
CGO_CFLAGS="-I$MINGW/usr/include" \
CC="i686-w64-mingw32-gcc" CXX="i686-w64-mingw32-g++" \
CGO_ENABLED=1 GOOS=windows GOARCH=386 go build -v -x -o build/cam2ip.exe -ldflags "-linkmode external -s -w '-extldflags=-static'" github.com/gen2brain/cam2ip
PKG_CONFIG="/usr/bin/x86_64-w64-mingw32-pkg-config" \
PKG_CONFIG_PATH="$MINGW64/usr/lib/pkgconfig" \
PKG_CONFIG_LIBDIR="$MINGW64/usr/lib/pkgconfig" \
CGO_LDFLAGS="-L$MINGW64/usr/lib" \
CGO_CFLAGS="-I$MINGW64/usr/include" \
CC="x86_64-w64-mingw32-gcc" CXX="x86_64-w64-mingw32-g++" \
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -tags cv2,turbo,pkgconfig -o build/cam2ip.amd64.cv2.exe -ldflags "-linkmode external -s -w '-extldflags=-static'" github.com/gen2brain/cam2ip/cmd/cam2ip
PKG_CONFIG="/usr/bin/armv6j-hardfloat-linux-gnueabi-pkg-config" \
PKG_CONFIG_PATH="$RPI/usr/lib/pkgconfig" \
PKG_CONFIG_LIBDIR="$RPI/usr/lib/pkgconfig" \
CGO_LDFLAGS="-L$RPI/usr/lib -L$RPI/usr/include" \
CGO_CFLAGS="-I$RPI/usr/include" \
CC="armv6j-hardfloat-linux-gnueabi-gcc" CXX="armv6j-hardfloat-linux-gnueabi-g++" \
CGO_ENABLED=1 GOOS=linux GOARCH=arm go build -v -x -o build/cam2ip.arm -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip
PKG_CONFIG="/usr/bin/x86_64-w64-mingw32-pkg-config" \
PKG_CONFIG_PATH="$MINGW64/usr/lib/pkgconfig" \
PKG_CONFIG_LIBDIR="$MINGW64/usr/lib/pkgconfig" \
CGO_LDFLAGS="-L$MINGW64/usr/lib" \
CGO_CFLAGS="-I$MINGW64/usr/include" \
CC="x86_64-w64-mingw32-gcc" CXX="x86_64-w64-mingw32-g++" \
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -o build/cam2ip.amd64.exe -ldflags "-linkmode external -s -w '-extldflags=-static'" github.com/gen2brain/cam2ip/cmd/cam2ip
PKG_CONFIG="/usr/bin/armv7a-hardfloat-linux-gnueabi-pkg-config" \
PKG_CONFIG_PATH="$RPI3/usr/lib/pkgconfig" \
PKG_CONFIG_LIBDIR="$RPI3/usr/lib/pkgconfig" \
CGO_LDFLAGS="-L$RPI3/usr/lib" \
CGO_CFLAGS="-I$RPI3/usr/include" \
CC="armv7a-hardfloat-linux-gnueabi-gcc" CXX="armv7a-hardfloat-linux-gnueabi-g++" \
CGO_ENABLED=1 GOOS=linux GOARCH=arm go build -tags cv2,turbo -o build/cam2ip.linux.arm7.cv2 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip/cmd/cam2ip
PKG_CONFIG="/usr/bin/armv7a-hardfloat-linux-gnueabi-pkg-config" \
PKG_CONFIG_PATH="$RPI3/usr/lib/pkgconfig" \
PKG_CONFIG_LIBDIR="$RPI3/usr/lib/pkgconfig" \
CGO_LDFLAGS="-L$RPI3/usr/lib" \
CGO_CFLAGS="-I$RPI3/usr/include" \
CC="armv7a-hardfloat-linux-gnueabi-gcc" CXX="armv7a-hardfloat-linux-gnueabi-g++" \
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -o build/cam2ip.linux.arm7 -ldflags "-linkmode external -s -w" github.com/gen2brain/cam2ip/cmd/cam2ip
PKG_CONFIG_PATH="$APPLE/SDK/MacOSX10.10.sdk/usr/lib/pkgconfig" \
PKG_CONFIG_LIBDIR="$APPLE/SDK/MacOSX10.10.sdk/usr/lib/pkgconfig" \
CGO_LDFLAGS="-L$APPLE/SDK/MacOSX10.10.sdk/usr/lib -mmacosx-version-min=10.10" \
CGO_CFLAGS="-I$APPLE/SDK/MacOSX10.10.sdk/usr/include -mmacosx-version-min=10.10" \
CC="$APPLE/bin/x86_64-apple-darwin14-clang" CXX="$APPLE/bin/x86_64-apple-darwin14-clang++" \
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -tags cv2,turbo -o build/cam2ip.darwin.amd64 -ldflags "-linkmode external -s -w '-extldflags=-mmacosx-version-min=10.10'" github.com/gen2brain/cam2ip/cmd/cam2ip
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

View File

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