123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- 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 48kHz, 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)
- if i.audiostopchan != nil {
- i.audiostopchan <- true // Signal to stop the audio pipe
- }
- 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")
- }
|