package usbcapture 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 1360 x 768 30fps = 25Mbps 1360 x 768 25fps = 20Mbps 1360 x 768 20fps = 18Mbps 1360 x 768 10fps = 10Mbps */ pixfmt v4l2.FourCCType width = 1920 height = 1080 streamInfo string ) // 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) } } } // startVideoServer starts the video server func startVideoServer() { 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 }