video_device.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. package usbcapture
  2. import (
  3. "bufio"
  4. "bytes"
  5. "context"
  6. _ "embed"
  7. "errors"
  8. "fmt"
  9. "log"
  10. "mime/multipart"
  11. "net/http"
  12. "net/textproto"
  13. "os/exec"
  14. "regexp"
  15. "strconv"
  16. "strings"
  17. "syscall"
  18. "github.com/vladimirvivien/go4vl/device"
  19. "github.com/vladimirvivien/go4vl/v4l2"
  20. )
  21. /*
  22. 1920 x 1080 60fps = 55Mbps //Edge not support
  23. 1920 x 1080 30fps = 50Mbps
  24. 1920 x 1080 25fps = 40Mbps
  25. 1920 x 1080 20fps = 30Mbps
  26. 1920 x 1080 10fps = 15Mbps
  27. 1360 x 768 60fps = 28Mbps
  28. 1360 x 768 30fps = 25Mbps
  29. 1360 x 768 25fps = 20Mbps
  30. 1360 x 768 20fps = 18Mbps
  31. 1360 x 768 10fps = 10Mbps
  32. */
  33. // Struct to store the size and fps info
  34. type FormatInfo struct {
  35. Format string
  36. Sizes []SizeInfo
  37. }
  38. type SizeInfo struct {
  39. Width int
  40. Height int
  41. FPS []int
  42. }
  43. //go:embed stream_takeover.jpg
  44. var endOfStreamJPG []byte
  45. // start video capture
  46. func (i *Instance) StartVideoCapture(openWithResolution *CaptureResolution) error {
  47. if i.Capturing {
  48. return fmt.Errorf("video capture already started")
  49. }
  50. if openWithResolution.FPS == 0 {
  51. openWithResolution.FPS = 25 //Default to 25 FPS
  52. }
  53. devName := i.Config.VideoDeviceName
  54. if openWithResolution == nil {
  55. return fmt.Errorf("resolution not provided")
  56. }
  57. frameRate := openWithResolution.FPS
  58. buffSize := 8 //No. of frames to buffer
  59. //Default to MJPEG
  60. //Other formats that are commonly supported are YUYV, H264, MJPEG
  61. format := "mjpeg"
  62. //Check if the video device is a capture device
  63. isCaptureDev, err := CheckVideoCaptureDevice(devName)
  64. if err != nil {
  65. return fmt.Errorf("failed to check video device: %w", err)
  66. }
  67. if !isCaptureDev {
  68. return fmt.Errorf("device %s is not a video capture device", devName)
  69. }
  70. //Check if the selected FPS is valid in the provided Resolutions
  71. resolutionIsSupported, err := deviceSupportResolution(i.Config.VideoDeviceName, openWithResolution)
  72. if err != nil {
  73. return err
  74. }
  75. if !resolutionIsSupported {
  76. return errors.New("this device do not support the required resolution settings")
  77. }
  78. //Open the video device
  79. camera, err := device.Open(devName,
  80. device.WithIOType(v4l2.IOTypeMMAP),
  81. device.WithPixFormat(v4l2.PixFormat{
  82. PixelFormat: getFormatType(format),
  83. Width: uint32(openWithResolution.Width),
  84. Height: uint32(openWithResolution.Height),
  85. Field: v4l2.FieldAny,
  86. }),
  87. device.WithFPS(uint32(frameRate)),
  88. device.WithBufferSize(uint32(buffSize)),
  89. )
  90. if err != nil {
  91. return fmt.Errorf("failed to open video device: %w", err)
  92. }
  93. i.camera = camera
  94. caps := camera.Capability()
  95. log.Printf("device [%s] opened\n", devName)
  96. log.Printf("device info: %s", caps.String())
  97. // Should get something like this:
  98. //2025/03/16 15:45:25 device info: driver: uvcvideo; card: USB Video: USB Video; bus info: usb-0000:00:14.0-2
  99. // set device format
  100. currFmt, err := camera.GetPixFormat()
  101. if err != nil {
  102. return fmt.Errorf("failed to get current pixel format: %w", err)
  103. }
  104. log.Printf("Current format: %s", currFmt)
  105. //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
  106. i.pixfmt = currFmt.PixelFormat
  107. i.width = int(currFmt.Width)
  108. i.height = int(currFmt.Height)
  109. i.streamInfo = fmt.Sprintf("%s - %s [%dx%d] %d fps",
  110. caps.Card,
  111. v4l2.PixelFormats[currFmt.PixelFormat],
  112. currFmt.Width, currFmt.Height, frameRate,
  113. )
  114. // start capture
  115. ctx, cancel := context.WithCancel(context.TODO())
  116. if err := camera.Start(ctx); err != nil {
  117. log.Fatalf("stream capture: %s", err)
  118. }
  119. i.cameraStartContext = cancel
  120. // video stream
  121. i.frames_buff = camera.GetOutput()
  122. log.Printf("device capture started (buffer size set %d)", camera.BufferCount())
  123. i.Capturing = true
  124. return nil
  125. }
  126. // start http service
  127. func (i *Instance) ServeVideoStream(w http.ResponseWriter, req *http.Request) {
  128. //Check if the access count is already 1, if so, kick out the previous access
  129. if i.accessCount >= 1 {
  130. log.Println("Another client is already connected, kicking out the previous client...")
  131. if i.videoTakeoverChan != nil {
  132. i.videoTakeoverChan <- true
  133. }
  134. log.Println("Previous client kicked out, taking over the stream...")
  135. }
  136. i.accessCount++
  137. defer func() { i.accessCount-- }()
  138. // Set up the multipart response
  139. mimeWriter := multipart.NewWriter(w)
  140. w.Header().Set("Content-Type", fmt.Sprintf("multipart/x-mixed-replace; boundary=%s", mimeWriter.Boundary()))
  141. partHeader := make(textproto.MIMEHeader)
  142. partHeader.Add("Content-Type", "image/jpeg")
  143. var frame []byte
  144. //Chrome MJPEG decoder cannot decode the first frame from MS2109 capture card for unknown reason
  145. //Thus we are discarding the first frame here
  146. if i.frames_buff != nil {
  147. select {
  148. case <-i.frames_buff:
  149. // Discard the first frame
  150. default:
  151. // No frame to discard
  152. }
  153. }
  154. // Streaming loop
  155. for frame = range i.frames_buff {
  156. if len(frame) == 0 {
  157. log.Print("skipping empty frame")
  158. continue
  159. }
  160. partWriter, err := mimeWriter.CreatePart(partHeader)
  161. if err != nil {
  162. log.Printf("failed to create multi-part writer: %s", err)
  163. return
  164. }
  165. if _, err := partWriter.Write(frame); err != nil {
  166. if errors.Is(err, syscall.EPIPE) {
  167. //broken pipe, the client browser has exited
  168. return
  169. }
  170. log.Printf("failed to write image: %s", err)
  171. }
  172. select {
  173. case <-req.Context().Done():
  174. // Client disconnected, exit the loop
  175. return
  176. case <-i.videoTakeoverChan:
  177. // Another client is taking over, exit the loop
  178. //Send the endofstream.jpg as last frame before exit
  179. endFrameHeader := make(textproto.MIMEHeader)
  180. endFrameHeader.Add("Content-Type", "image/jpeg")
  181. endFrameHeader.Add("Content-Length", fmt.Sprint(len(endOfStreamJPG)))
  182. partWriter, err := mimeWriter.CreatePart(endFrameHeader)
  183. if err == nil {
  184. partWriter.Write(endOfStreamJPG)
  185. }
  186. log.Println("Video stream taken over by another client, exiting...")
  187. return
  188. default:
  189. // Continue streaming
  190. }
  191. }
  192. }
  193. // StopCapture stops the video capture and closes the camera device
  194. func (i *Instance) StopCapture() error {
  195. if i.camera != nil {
  196. i.cameraStartContext()
  197. i.camera.Close()
  198. i.camera = nil
  199. }
  200. i.Capturing = false
  201. return nil
  202. }
  203. // IsCaptureCardVideoInterface checks if the given video device is a capture card with multiple input interfaces
  204. func IsCaptureCardVideoInterface(device string) bool {
  205. if ok, _ := CheckVideoCaptureDevice(device); !ok {
  206. return false
  207. }
  208. formats, err := GetV4L2FormatInfo(device)
  209. if err != nil {
  210. return false
  211. }
  212. count := 0
  213. for _, f := range formats {
  214. count += len(f.Sizes)
  215. }
  216. return count > 1
  217. }
  218. // CheckVideoCaptureDevice checks if the given video device is a video capture device
  219. func CheckVideoCaptureDevice(device string) (bool, error) {
  220. // Run v4l2-ctl to get device capabilities
  221. cmd := exec.Command("v4l2-ctl", "--device", device, "--all")
  222. output, err := cmd.CombinedOutput()
  223. if err != nil {
  224. return false, fmt.Errorf("failed to execute v4l2-ctl: %w", err)
  225. }
  226. // Convert output to string and check for the "Video Capture" capability
  227. outputStr := string(output)
  228. if strings.Contains(outputStr, "Video Capture") {
  229. return true, nil
  230. }
  231. return false, nil
  232. }
  233. // GetDefaultVideoDevice returns the first available video capture device, e.g., /dev/video0
  234. func GetDefaultVideoDevice() (string, error) {
  235. // List all /dev/video* devices and return the first one that is a video capture device
  236. for i := 0; i < 10; i++ {
  237. device := fmt.Sprintf("/dev/video%d", i)
  238. isCapture, err := CheckVideoCaptureDevice(device)
  239. if err != nil {
  240. continue
  241. }
  242. if isCapture {
  243. return device, nil
  244. }
  245. }
  246. return "", fmt.Errorf("no video capture device found")
  247. }
  248. // deviceSupportResolution checks if the given video device supports the specified resolution and frame rate
  249. func deviceSupportResolution(devicePath string, resolution *CaptureResolution) (bool, error) {
  250. formatInfo, err := GetV4L2FormatInfo(devicePath)
  251. if err != nil {
  252. return false, err
  253. }
  254. // Yes, this is an O(N^3) operation, but a video decices rarely have supported resolution
  255. // more than 20 combinations. The compute time should be fine
  256. for _, res := range formatInfo {
  257. for _, size := range res.Sizes {
  258. //Check if there is a matching resolution
  259. if size.Height == resolution.Height && size.Width == resolution.Width {
  260. //Matching resolution. Check if the required FPS is supported
  261. for _, fps := range size.FPS {
  262. if fps == resolution.FPS {
  263. return true, nil
  264. }
  265. }
  266. }
  267. }
  268. }
  269. return false, nil
  270. }
  271. // PrintV4L2FormatInfo prints the supported formats, resolutions, and frame rates of the given video device
  272. func PrintV4L2FormatInfo(devicePath string) {
  273. // Check if the device is a video capture device
  274. isCapture, err := CheckVideoCaptureDevice(devicePath)
  275. if err != nil {
  276. fmt.Printf("Error checking device: %v\n", err)
  277. return
  278. }
  279. if !isCapture {
  280. fmt.Printf("Device %s is not a video capture device\n", devicePath)
  281. return
  282. }
  283. // Get format info
  284. formats, err := GetV4L2FormatInfo(devicePath)
  285. if err != nil {
  286. fmt.Printf("Error getting format info: %v\n", err)
  287. return
  288. }
  289. // Print format info
  290. for _, format := range formats {
  291. fmt.Printf("Format: %s\n", format.Format)
  292. for _, size := range format.Sizes {
  293. fmt.Printf(" Size: %dx%d\n", size.Width, size.Height)
  294. fmt.Printf(" FPS: %v\n", size.FPS)
  295. }
  296. }
  297. }
  298. // Function to run the v4l2-ctl command and parse the output
  299. func GetV4L2FormatInfo(devicePath string) ([]FormatInfo, error) {
  300. // Run the v4l2-ctl command to list formats
  301. cmd := exec.Command("v4l2-ctl", "--list-formats-ext", "-d", devicePath)
  302. var out bytes.Buffer
  303. cmd.Stdout = &out
  304. err := cmd.Run()
  305. if err != nil {
  306. return nil, err
  307. }
  308. // Parse the output
  309. var formats []FormatInfo
  310. var currentFormat *FormatInfo
  311. scanner := bufio.NewScanner(&out)
  312. formatRegex := regexp.MustCompile(`\[(\d+)\]: '(\S+)'`)
  313. sizeRegex := regexp.MustCompile(`Size: Discrete (\d+)x(\d+)`)
  314. intervalRegex := regexp.MustCompile(`Interval: Discrete (\d+\.\d+)s \((\d+\.\d+) fps\)`)
  315. for scanner.Scan() {
  316. line := scanner.Text()
  317. // Match format line
  318. if matches := formatRegex.FindStringSubmatch(line); matches != nil {
  319. if currentFormat != nil {
  320. formats = append(formats, *currentFormat)
  321. }
  322. // Start a new format entry
  323. currentFormat = &FormatInfo{
  324. Format: matches[2],
  325. }
  326. }
  327. // Match size line
  328. if matches := sizeRegex.FindStringSubmatch(line); matches != nil {
  329. width, _ := strconv.Atoi(matches[1])
  330. height, _ := strconv.Atoi(matches[2])
  331. // Initialize the size entry
  332. sizeInfo := SizeInfo{
  333. Width: width,
  334. Height: height,
  335. }
  336. // Match FPS intervals for the current size
  337. for scanner.Scan() {
  338. line = scanner.Text()
  339. if fpsMatches := intervalRegex.FindStringSubmatch(line); fpsMatches != nil {
  340. fps, _ := strconv.ParseFloat(fpsMatches[2], 32)
  341. sizeInfo.FPS = append(sizeInfo.FPS, int(fps))
  342. } else {
  343. // Stop parsing FPS intervals when no more matches are found
  344. break
  345. }
  346. }
  347. // Add the size information to the current format
  348. currentFormat.Sizes = append(currentFormat.Sizes, sizeInfo)
  349. }
  350. }
  351. // Append the last format if present
  352. if currentFormat != nil {
  353. formats = append(formats, *currentFormat)
  354. }
  355. if err := scanner.Err(); err != nil {
  356. return nil, err
  357. }
  358. return formats, nil
  359. }
  360. func getFormatType(fmtStr string) v4l2.FourCCType {
  361. switch strings.ToLower(fmtStr) {
  362. case "jpeg":
  363. return v4l2.PixelFmtJPEG
  364. case "mpeg":
  365. return v4l2.PixelFmtMPEG
  366. case "mjpeg":
  367. return v4l2.PixelFmtMJPEG
  368. case "h264", "h.264":
  369. return v4l2.PixelFmtH264
  370. case "yuyv":
  371. return v4l2.PixelFmtYUYV
  372. case "rgb":
  373. return v4l2.PixelFmtRGB24
  374. }
  375. return v4l2.PixelFmtMPEG
  376. }