|
@@ -0,0 +1,211 @@
|
|
|
+package usbcapture
|
|
|
+
|
|
|
+import (
|
|
|
+ "bufio"
|
|
|
+ "fmt"
|
|
|
+ "log"
|
|
|
+ "net/http"
|
|
|
+ "os"
|
|
|
+ "os/exec"
|
|
|
+ "regexp"
|
|
|
+ "strings"
|
|
|
+
|
|
|
+ "github.com/gorilla/websocket"
|
|
|
+)
|
|
|
+
|
|
|
+// upgrader is used to upgrade HTTP connections to WebSocket connections
|
|
|
+var upgrader = websocket.Upgrader{
|
|
|
+ ReadBufferSize: 1024,
|
|
|
+ WriteBufferSize: 1024,
|
|
|
+ CheckOrigin: func(r *http.Request) bool {
|
|
|
+ return true
|
|
|
+ },
|
|
|
+}
|
|
|
+
|
|
|
+// ListCaptureDevices lists all available audio capture devices in the /dev/snd directory.
|
|
|
+func ListCaptureDevices() ([]string, error) {
|
|
|
+ files, err := os.ReadDir("/dev/snd")
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("failed to read /dev/snd: %w", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ var captureDevs []string
|
|
|
+ for _, file := range files {
|
|
|
+ name := file.Name()
|
|
|
+ if strings.HasPrefix(name, "pcm") && strings.HasSuffix(name, "c") {
|
|
|
+ fullPath := "/dev/snd/" + name
|
|
|
+ captureDevs = append(captureDevs, fullPath)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return captureDevs, nil
|
|
|
+}
|
|
|
+
|
|
|
+// FindHDMICaptureCard searches for an HDMI capture card using the `arecord -l` command.
|
|
|
+func FindHDMICapturePCMPath() (string, error) {
|
|
|
+ out, err := exec.Command("arecord", "-l").Output()
|
|
|
+ if err != nil {
|
|
|
+ return "", fmt.Errorf("arecord -l failed: %w", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ lines := strings.Split(string(out), "\n")
|
|
|
+ for _, line := range lines {
|
|
|
+ lower := strings.ToLower(line)
|
|
|
+ if strings.Contains(lower, "ms2109") || strings.Contains(lower, "ms2130") {
|
|
|
+ // Example line:
|
|
|
+ // card 1: MS2109 [MS2109], device 0: USB Audio [USB Audio]
|
|
|
+ parts := strings.Fields(line)
|
|
|
+ var cardNum, devNum string
|
|
|
+ for i := range parts {
|
|
|
+ if parts[i] == "card" && i+1 < len(parts) {
|
|
|
+ cardNum = parts[i+1][:1] // "1"
|
|
|
+ }
|
|
|
+ if parts[i] == "device" && i+1 < len(parts) {
|
|
|
+ devNum = strings.TrimSuffix(parts[i+1], ":") // "0"
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if cardNum != "" && devNum != "" {
|
|
|
+ return fmt.Sprintf("/dev/snd/pcmC%vD%vc", cardNum, devNum), nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return "", fmt.Errorf("no HDMI capture card found")
|
|
|
+}
|
|
|
+
|
|
|
+// Convert a PCM device name to a hardware device name.
|
|
|
+// Example: "pcmC1D0c" -> "hw:1,0"
|
|
|
+func pcmDeviceToHW(dev string) (string, error) {
|
|
|
+ // Regex to extract card and device numbers
|
|
|
+ re := regexp.MustCompile(`pcmC(\d+)D(\d+)[cp]`)
|
|
|
+ matches := re.FindStringSubmatch(dev)
|
|
|
+ if len(matches) < 3 {
|
|
|
+ return "", fmt.Errorf("invalid device format")
|
|
|
+ }
|
|
|
+ card := matches[1]
|
|
|
+ device := matches[2]
|
|
|
+ return fmt.Sprintf("hw:%s,%s", card, device), nil
|
|
|
+}
|
|
|
+
|
|
|
+func GetDefaultAudioConfig() *AudioConfig {
|
|
|
+ return &AudioConfig{
|
|
|
+ SampleRate: 48000,
|
|
|
+ Channels: 2,
|
|
|
+ BytesPerSample: 2, // 16-bit
|
|
|
+ FrameSize: 1920, // 1920 samples per frame = 40ms @ 48kHz
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// AudioStreamingHandler handles incoming WebSocket connections for audio streaming.
|
|
|
+func (i Instance) AudioStreamingHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
+ conn, err := upgrader.Upgrade(w, r, nil)
|
|
|
+ if err != nil {
|
|
|
+ log.Println("Failed to upgrade to websocket:", err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ defer conn.Close()
|
|
|
+
|
|
|
+ if i.audiostopchan != nil {
|
|
|
+ //Another instance already running
|
|
|
+ log.Println("Audio pipe already running, stopping previous instance")
|
|
|
+ i.audiostopchan <- true
|
|
|
+ }
|
|
|
+
|
|
|
+ //Get the capture card audio input
|
|
|
+ pcmdev, err := FindHDMICapturePCMPath()
|
|
|
+ if err != nil {
|
|
|
+ log.Println("Failed to find HDMI capture PCM path:", err)
|
|
|
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ log.Println("Found HDMI capture PCM path:", pcmdev)
|
|
|
+
|
|
|
+ // Convert PCM device to hardware device name
|
|
|
+ hwdev, err := pcmDeviceToHW(pcmdev)
|
|
|
+ if err != nil {
|
|
|
+ log.Println("Failed to convert PCM device to hardware device:", err)
|
|
|
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ log.Println("Using hardware device:", hwdev)
|
|
|
+
|
|
|
+ // Create a buffered reader to read audio data
|
|
|
+ i.audiostopchan = make(chan bool, 1)
|
|
|
+ log.Println("Starting audio pipe with arecord...")
|
|
|
+
|
|
|
+ // Start arecord with 24kHz, 16-bit, stereo
|
|
|
+ cmd := exec.Command("arecord",
|
|
|
+ "-f", "S16_LE", // Format: 16-bit little-endian
|
|
|
+ "-r", fmt.Sprint(i.Config.AudioConfig.SampleRate),
|
|
|
+ "-c", fmt.Sprint(i.Config.AudioConfig.Channels),
|
|
|
+ "-D", hwdev, // Use the hardware device
|
|
|
+ )
|
|
|
+
|
|
|
+ stdout, err := cmd.StdoutPipe()
|
|
|
+ if err != nil {
|
|
|
+ log.Println("Failed to get arecord stdout pipe:", err)
|
|
|
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := cmd.Start(); err != nil {
|
|
|
+ log.Println("Failed to start arecord:", err)
|
|
|
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ reader := bufio.NewReader(stdout)
|
|
|
+ bufferSize := i.Config.AudioConfig.FrameSize * i.Config.AudioConfig.Channels * i.Config.AudioConfig.BytesPerSample
|
|
|
+ log.Printf("Buffer size: %d bytes (FrameSize: %d, Channels: %d, BytesPerSample: %d)",
|
|
|
+ bufferSize, i.Config.AudioConfig.FrameSize, i.Config.AudioConfig.Channels, i.Config.AudioConfig.BytesPerSample)
|
|
|
+ buf := make([]byte, bufferSize*2)
|
|
|
+
|
|
|
+ // Start a goroutine to handle WebSocket messages
|
|
|
+ log.Println("Listening for WebSocket messages...")
|
|
|
+ go func() {
|
|
|
+ _, msg, err := conn.ReadMessage()
|
|
|
+ if err == nil {
|
|
|
+ if string(msg) == "exit" {
|
|
|
+ log.Println("Received exit command from client")
|
|
|
+ i.audiostopchan <- true // Signal to stop the audio pipe
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ log.Println("Starting audio capture loop...")
|
|
|
+ for {
|
|
|
+ select {
|
|
|
+ case <-i.audiostopchan:
|
|
|
+ log.Println("Audio pipe stopped")
|
|
|
+ goto DONE
|
|
|
+ default:
|
|
|
+ n, err := reader.Read(buf)
|
|
|
+ if err != nil {
|
|
|
+ log.Println("Read error:", err)
|
|
|
+ goto DONE
|
|
|
+ }
|
|
|
+
|
|
|
+ if n == 0 {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ //log.Println("Read bytes:", n, "size of buffer:", len(buf))
|
|
|
+ //Send only the bytes read to WebSocket
|
|
|
+ err = conn.WriteMessage(websocket.BinaryMessage, buf[:n])
|
|
|
+ if err != nil {
|
|
|
+ log.Println("WebSocket send error:", err)
|
|
|
+ goto DONE
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+DONE:
|
|
|
+
|
|
|
+ i.audiostopchan <- true // Signal to stop the audio pipe
|
|
|
+ cmd.Process.Kill()
|
|
|
+ log.Println("Audio pipe finished")
|
|
|
+}
|