remote-mic/audio/ffmpeg.go

103 lines
2.2 KiB
Go

package audio
import (
"os/exec"
"fmt"
"regexp"
"runtime"
"io"
)
type microphone struct {
command *exec.Cmd
audioStream io.Reader
diagnosticOutput io.Reader
Format string
Device string
Encoding Encoding
}
func NewMicrophone() (*microphone, error) {
if runtime.GOOS != "darwin" {
panic(fmt.Errorf("audio.microphone is only supported on macOS"))
}
format := "avfoundation"
device, err := getMicDevice(format)
if err != nil {
return nil, err
}
return &microphone{
Format: format,
Device: device,
Encoding: defaultEncoding,
}, nil
}
func (m *microphone) Start() error {
m.command = exec.Command("ffmpeg", "-re",
"-f", m.Format,
"-i", fmt.Sprintf(":%s", m.Device),
"-f", m.Encoding.Codec,
"-ar", fmt.Sprintf("%d", m.Encoding.Bitrate),
"-ac", fmt.Sprintf("%d", m.Encoding.Channels),
"-",
)
// these should never fail, unless you use the module incorrectly
audioStream, err := m.command.StdoutPipe()
if err != nil {
panic(err)
}
diagnosticOutput, err := m.command.StderrPipe()
if err != nil {
panic(err)
}
m.audioStream, m.diagnosticOutput = audioStream, diagnosticOutput
return m.command.Start()
}
func (m *microphone) Stop() error {
return m.command.Process.Kill()
}
func (m *microphone) AudioStream() io.Reader {
if m.audioStream == nil {
panic(fmt.Errorf("AudioStream() called before Start()"))
}
return m.audioStream
}
func (m *microphone) DiagnosticOutput() io.Reader {
if m.diagnosticOutput == nil {
panic(fmt.Errorf("DiagnosticOutput() called before Start()"))
}
return m.diagnosticOutput
}
func getMicDevice(format string) (string, error) {
cmd := exec.Command("ffmpeg",
"-f", format,
"-list_devices", "true",
"-i", "\"\"",
)
// ignore error, this ffmpeg command returns exit status 1
outputBytes, _ := cmd.CombinedOutput()
output := string(outputBytes)
parsedOutput := parseDeviceNumber(output)
if parsedOutput == "" {
return "", fmt.Errorf("Command: %s\nFailed to parse FFmpeg output:\n%s", cmd, output)
}
return parsedOutput, nil
}
func parseDeviceNumber(ffmpegOutput string) string {
re := regexp.MustCompile(`\[(\d)\].*?[Mm]icrophone`)
matches := re.FindStringSubmatch(ffmpegOutput)
if (len(matches) == 2) {
return matches[1]
}
return ""
}