Skip to content

Commit

Permalink
Merge pull request #50 from twt--/rtsp
Browse files Browse the repository at this point in the history
support RTSP by using `ffmpeg` to convert the stream
  • Loading branch information
tphakala authored Mar 11, 2024
2 parents 40f3db0 + b86ac34 commit a074b12
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 3 deletions.
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,14 @@ RUN apt-get update && apt-get install -y \
ca-certificates \
libasound2 \
sox \
ffmpeg \
&& rm -rf /var/lib/apt/lists/*

COPY --from=build /root/src/BirdNET-Go/bin /usr/bin/
COPY --from=build /usr/local/lib/libtensorflowlite_c.so /usr/local/lib/
RUN ldconfig

# Add symlink to /config directory where configs can be stored
# Add symlink to /config directory where configs can be stored
VOLUME /config
RUN mkdir -p /root/.config && ln -s /config /root/.config/birdnet-go

Expand Down
1 change: 1 addition & 0 deletions cmd/realtime/realtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func setupFlags(cmd *cobra.Command, settings *conf.Settings) error {
cmd.Flags().StringVar(&settings.Realtime.AudioExport.Path, "clippath", viper.GetString("realtime.audioexport.path"), "Path to save audio clips")
cmd.Flags().StringVar(&settings.Realtime.Log.Path, "logpath", viper.GetString("realtime.log.path"), "Path to save log files")
cmd.Flags().BoolVar(&settings.Realtime.ProcessingTime, "processingtime", viper.GetBool("realtime.processingtime"), "Report processing time for each detection")
cmd.Flags().StringVar(&settings.Realtime.RTSP, "rtsp", viper.GetString("realtime.rtsp"), "URL of RTSP audio stream to capture")

// Bind flags to the viper settings
if err := viper.BindPFlags(cmd.Flags()); err != nil {
Expand Down
4 changes: 3 additions & 1 deletion internal/conf/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ type Settings struct {
PrivacyFilter struct {
Enabled bool // true to enable privacy filter
}

RTSP string // RTSP stream URL
}

WebServer struct {
Expand Down Expand Up @@ -215,7 +217,7 @@ realtime:
log:
enabled: false # true to enable OBS chat log
path: birdnet.txt # path to OBS chat log
birdweather:
enabled: false # true to enable birdweather uploads
debug: false # true to enable birdweather api debug mode
Expand Down
102 changes: 101 additions & 1 deletion internal/myaudio/capture.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
package myaudio

import (
"context"
"fmt"
"io"
"log"
"os"
"os/exec"
"runtime"
"sync"
"syscall"
"time"

"github.com/gen2brain/malgo"
"github.com/tphakala/birdnet-go/internal/conf"
)

func CaptureAudio(settings *conf.Settings, wg *sync.WaitGroup, quitChan chan struct{}, restartChan chan struct{}, audioBuffer *AudioBuffer) {
defer wg.Done() // Ensure this is called when the goroutine exits
if settings.Realtime.RTSP != "" {
// RTSP audio capture
captureAudioRTSP(settings, wg, quitChan, restartChan, audioBuffer)
} else {
// Default audio capture
captureAudioMalgo(settings, wg, quitChan, restartChan, audioBuffer)
}
}

func captureAudioMalgo(settings *conf.Settings, wg *sync.WaitGroup, quitChan chan struct{}, restartChan chan struct{}, audioBuffer *AudioBuffer) {
defer wg.Done() // Ensure this is called when the goroutine exits
var device *malgo.Device

if settings.Debug {
Expand Down Expand Up @@ -152,3 +165,90 @@ func CaptureAudio(settings *conf.Settings, wg *sync.WaitGroup, quitChan chan str
}
}
}

func captureAudioRTSP(settings *conf.Settings, wg *sync.WaitGroup, quitChan chan struct{}, restartChan chan struct{}, audioBuffer *AudioBuffer) {
defer wg.Done() // Ensure this is called when the goroutine exits

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Start ffmpeg
cmd := exec.CommandContext(ctx, "ffmpeg",
"-i", settings.Realtime.RTSP,
"-loglevel", "error",
"-vn", // No video
"-f", "s16le", // 16-bit signed little-endian PCM
"-ar", "48000", // Sample rate
"-ac", "1", // Single channel (mono)
"pipe:1", // Output raw audio data to standard out
)

// ffmpeg audio data to stdout
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Fatalf("Error creating ffmpeg pipe: %v", err)
}

log.Println("Starting ffmpeg with command: ", cmd.String())
if err := cmd.Start(); err != nil {
log.Fatalf("Error starting FFmpeg: %v", err)
}
defer cmd.Process.Kill()

// Process audio data from ffmpeg
go func() {
defer cancel()
buf := make([]byte, 65536) // TODO: Make buffer size configurable
for {
select {
case <-quitChan:
// Quit signal has been received, stop the command
if err := cmd.Process.Kill(); err != nil {
log.Fatal("failed to kill process: ", err)
}
return
default:
n, err := stdout.Read(buf)
if err != nil {
if err == io.EOF {
log.Println("ffmpeg EOF")
} else {
log.Println("Error reading from ffmpeg: ", err)
}
// Read error, kill the command so it can be restarted
err = cmd.Process.Kill()
if err != nil {
log.Printf("error killing ffmpeg process: ", err)
}
// Send restart signal
restartChan <- struct{}{}
return
}
// Write to ringbuffer when audio data is received
WriteToBuffer(buf[:n])
audioBuffer.Write(buf[:n])
}
}
}()

// Wait for ffmpeg to finish
if err := cmd.Wait(); err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
if status, ok := exitError.Sys().(syscall.WaitStatus); ok && status.Signaled() {
// killed by a signal
if settings.Debug {
log.Println("ffmpeg command was killed")
}
return
}
// exited with an error
log.Println("ffmpeg command stopped unexpectedly, retrying: ", err)
} else {
// Some other error occurred
log.Printf("ffmpeg exited unexpectedly: %v", err)
}
// If we get here, the command exited with an error, so we should retry
time.Sleep(1 * time.Second) // Wait a second, then send restart signal
restartChan <- struct{}{}
}
}

0 comments on commit a074b12

Please sign in to comment.