Browse Source

Added concept prototype

TC 2 weeks ago
parent
commit
03d7004e17
5 changed files with 304 additions and 0 deletions
  1. 12 0
      autopush.sh
  2. 10 0
      remdeskd/go.sum
  3. 169 0
      remdeskd/main.go
  4. BIN
      remdeskd/remdeskd
  5. 113 0
      remdeskd/video_device.go

+ 12 - 0
autopush.sh

@@ -0,0 +1,12 @@
+module imuslab.com/remdeskvm/remdeskd
+
+go 1.23.4
+
+require github.com/vladimirvivien/go4vl v0.0.5
+
+require (
+	github.com/gen2brain/x264-go v0.3.1 // indirect
+	github.com/gen2brain/x264-go/x264c v0.0.0-20241022182000-732e1bdb7da2 // indirect
+	github.com/gen2brain/x264-go/yuv v0.0.0-20241022182000-732e1bdb7da2 // indirect
+	golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
+)

+ 10 - 0
remdeskd/go.sum

@@ -0,0 +1,10 @@
+github.com/gen2brain/x264-go v0.3.1 h1:XFi6y8HM5BCMrP4kR/o711iBU++XNfLOY3PG8Alz2kU=
+github.com/gen2brain/x264-go v0.3.1/go.mod h1:+Z4VKXQrUKzbfEv36GyiyjXfixmOp2c820ugsMthW18=
+github.com/gen2brain/x264-go/x264c v0.0.0-20241022182000-732e1bdb7da2 h1:aS40m/Q+6z0ID7EVegxnnw3lWHUceGbg0zucbo7335I=
+github.com/gen2brain/x264-go/x264c v0.0.0-20241022182000-732e1bdb7da2/go.mod h1:r2t0/BO5YgPRwWQpLYO5iB5FLSj1ArTQc/zmAoAV7sQ=
+github.com/gen2brain/x264-go/yuv v0.0.0-20241022182000-732e1bdb7da2 h1:bqM055OxaqM/IPFGU84u0ROJW2wvvC3KGEAQuT+3WBs=
+github.com/gen2brain/x264-go/yuv v0.0.0-20241022182000-732e1bdb7da2/go.mod h1:tVJ40WrnqgDLb+itPvllvybW0/3jz5BzUFrEHvYIoGU=
+github.com/vladimirvivien/go4vl v0.0.5 h1:jHuo/CZOAzYGzrSMOc7anOMNDr03uWH5c1B5kQ+Chnc=
+github.com/vladimirvivien/go4vl v0.0.5/go.mod h1:FP+/fG/X1DUdbZl9uN+l33vId1QneVn+W80JMc17OL8=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

+ 169 - 0
remdeskd/main.go

@@ -0,0 +1,169 @@
+package main
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"log"
+	"mime/multipart"
+	"net/http"
+	"net/textproto"
+	"strings"
+	"syscall"
+
+	"github.com/vladimirvivien/go4vl/device"
+	"github.com/vladimirvivien/go4vl/v4l2"
+)
+
+var (
+	camera *device.Device
+	frames <-chan []byte
+	fps    uint32 = 30
+	/*
+		1920 x 1080 60fps = 55Mbps //Edge not support
+		1920 x 1080 30fps = 50Mbps
+		1920 x 1080 25fps = 40Mbps
+		1920 x 1080 20fps = 30Mbps
+		1920 x 1080 10fps = 15Mbps
+
+		1360 x 768 60fps = 28Mbps //Edge not support
+		1360 x 768 30fps = 25Mbps
+		1360 x 768 25fps = 20Mbps
+		1360 x 768 20fps = 18Mbps
+		1360 x 768 10fps = 10Mbps
+	*/
+	width  = 1920
+	height = 1080
+)
+
+// start http service
+func serveVideoStream(w http.ResponseWriter, req *http.Request) {
+	mimeWriter := multipart.NewWriter(w)
+	w.Header().Set("Content-Type", fmt.Sprintf("multipart/x-mixed-replace; boundary=%s", mimeWriter.Boundary()))
+	partHeader := make(textproto.MIMEHeader)
+	partHeader.Add("Content-Type", "image/jpeg")
+
+	var frame []byte
+	for frame = range frames {
+		if len(frame) == 0 {
+			log.Print("skipping empty frame")
+			continue
+		}
+
+		partWriter, err := mimeWriter.CreatePart(partHeader)
+		if err != nil {
+			log.Printf("failed to create multi-part writer: %s", err)
+			return
+		}
+
+		if _, err := partWriter.Write(frame); err != nil {
+			if errors.Is(err, syscall.EPIPE) {
+				//broken pipe, the client browser has exited
+				return
+			}
+			log.Printf("failed to write image: %s", err)
+		}
+
+	}
+}
+
+func main() {
+	port := ":9090"
+	devName := "/dev/video0"
+	frameRate := int(fps)
+	buffSize := 8
+	format := "mjpeg"
+
+	//Check if the video device is a capture device
+	isCaptureDev, err := checkVideoCaptureDevice(devName)
+	if err != nil {
+		panic(err)
+	}
+	if !isCaptureDev {
+		panic("target device is not a capture-able device")
+	}
+	camera, err := device.Open(devName,
+		device.WithIOType(v4l2.IOTypeMMAP),
+		device.WithPixFormat(v4l2.PixFormat{PixelFormat: getFormatType(format), Width: uint32(width), Height: uint32(height), Field: v4l2.FieldAny}),
+		device.WithFPS(uint32(frameRate)),
+		device.WithBufferSize(uint32(buffSize)),
+	)
+
+	if err != nil {
+		log.Fatalf("failed to open device: %s", err)
+	}
+	defer camera.Close()
+
+	//Get video supported sizes
+	formatInfo, err := getV4L2FormatInfo(devName)
+	if err != nil {
+		log.Fatal(err)
+	}
+	for _, format := range formatInfo {
+		fmt.Printf("Format: %s\n", format.Format)
+		for _, size := range format.Sizes {
+			fmt.Printf("  Size: %dx%d\n", size.Width, size.Height)
+			fmt.Printf("    FPS: %v\n", size.FPS)
+		}
+	}
+
+	caps := camera.Capability()
+	log.Printf("device [%s] opened\n", devName)
+	log.Printf("device info: %s", caps.String())
+	//2025/03/16 15:45:25 device info: driver: uvcvideo; card: USB Video: USB Video; bus info: usb-0000:00:14.0-2
+
+	// set device format
+	currFmt, err := camera.GetPixFormat()
+	if err != nil {
+		log.Fatalf("unable to get format: %s", err)
+	}
+	log.Printf("Current format: %s", currFmt)
+	//2025/03/16 15:45:25 Current format: Motion-JPEG [1920x1080]; field=any; bytes per line=0; size image=0; colorspace=Default; YCbCr=Default; Quant=Default; XferFunc=Default
+	pixfmt = currFmt.PixelFormat
+	streamInfo = fmt.Sprintf("%s - %s [%dx%d] %d fps",
+		caps.Card,
+		v4l2.PixelFormats[currFmt.PixelFormat],
+		currFmt.Width, currFmt.Height, frameRate,
+	)
+
+	// start capture
+	ctx, cancel := context.WithCancel(context.TODO())
+	if err := camera.Start(ctx); err != nil {
+		log.Fatalf("stream capture: %s", err)
+	}
+	defer func() {
+		cancel()
+		camera.Close()
+	}()
+
+	// video stream
+	frames = camera.GetOutput()
+
+	log.Printf("device capture started (buffer size set %d)", camera.BufferCount())
+	log.Printf("starting server on port %s", port)
+	log.Println("use url path /webcam")
+
+	// setup http service
+	http.HandleFunc("/stream", serveVideoStream) // returns video feed
+	if err := http.ListenAndServe(port, nil); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func getFormatType(fmtStr string) v4l2.FourCCType {
+	switch strings.ToLower(fmtStr) {
+	case "jpeg":
+		return v4l2.PixelFmtJPEG
+	case "mpeg":
+		return v4l2.PixelFmtMPEG
+	case "mjpeg":
+		return v4l2.PixelFmtMJPEG
+	case "h264", "h.264":
+		return v4l2.PixelFmtH264
+	case "yuyv":
+		return v4l2.PixelFmtYUYV
+	case "rgb":
+		return v4l2.PixelFmtRGB24
+	}
+	return v4l2.PixelFmtMPEG
+}

BIN
remdeskd/remdeskd


+ 113 - 0
remdeskd/video_device.go

@@ -0,0 +1,113 @@
+package main
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"os/exec"
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+// Struct to store the size and fps info
+type FormatInfo struct {
+	Format string
+	Sizes  []SizeInfo
+}
+
+type SizeInfo struct {
+	Width  int
+	Height int
+	FPS    []float64
+}
+
+// CheckVideoCaptureDevice checks if the given video device is a video capture device
+func checkVideoCaptureDevice(device string) (bool, error) {
+	// Run v4l2-ctl to get device capabilities
+	cmd := exec.Command("v4l2-ctl", "--device", device, "--all")
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		return false, fmt.Errorf("failed to execute v4l2-ctl: %w", err)
+	}
+
+	// Convert output to string and check for the "Video Capture" capability
+	outputStr := string(output)
+	if strings.Contains(outputStr, "Video Capture") {
+		return true, nil
+	}
+	return false, nil
+}
+
+// Function to run the v4l2-ctl command and parse the output
+func getV4L2FormatInfo(devicePath string) ([]FormatInfo, error) {
+	// Run the v4l2-ctl command to list formats
+	cmd := exec.Command("v4l2-ctl", "--list-formats-ext", "-d", devicePath)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	if err != nil {
+		return nil, err
+	}
+
+	// Parse the output
+	var formats []FormatInfo
+	var currentFormat *FormatInfo
+	scanner := bufio.NewScanner(&out)
+
+	formatRegex := regexp.MustCompile(`\[(\d+)\]: '(\S+)'`)
+	sizeRegex := regexp.MustCompile(`Size: Discrete (\d+)x(\d+)`)
+	intervalRegex := regexp.MustCompile(`Interval: Discrete (\d+\.\d+)s \((\d+\.\d+) fps\)`)
+
+	for scanner.Scan() {
+		line := scanner.Text()
+
+		// Match format line
+		if matches := formatRegex.FindStringSubmatch(line); matches != nil {
+			if currentFormat != nil {
+				formats = append(formats, *currentFormat)
+			}
+			// Start a new format entry
+			currentFormat = &FormatInfo{
+				Format: matches[2],
+			}
+		}
+
+		// Match size line
+		if matches := sizeRegex.FindStringSubmatch(line); matches != nil {
+			width, _ := strconv.Atoi(matches[1])
+			height, _ := strconv.Atoi(matches[2])
+
+			// Initialize the size entry
+			sizeInfo := SizeInfo{
+				Width:  width,
+				Height: height,
+			}
+
+			// Match FPS intervals for the current size
+			for scanner.Scan() {
+				line = scanner.Text()
+				if fpsMatches := intervalRegex.FindStringSubmatch(line); fpsMatches != nil {
+					fps, _ := strconv.ParseFloat(fpsMatches[2], 64)
+					sizeInfo.FPS = append(sizeInfo.FPS, fps)
+				} else {
+					// Stop parsing FPS intervals when no more matches are found
+					break
+				}
+			}
+			// Add the size information to the current format
+			currentFormat.Sizes = append(currentFormat.Sizes, sizeInfo)
+		}
+	}
+
+	// Append the last format if present
+	if currentFormat != nil {
+		formats = append(formats, *currentFormat)
+	}
+
+	if err := scanner.Err(); err != nil {
+		return nil, err
+	}
+
+	return formats, nil
+}