Skip to content

Commit

Permalink
Added support for pulse audio, added some debugging messages around d…
Browse files Browse the repository at this point in the history
…atabase interactions, and upped the timeouts for database interactions in a few spots.
  • Loading branch information
mmcc-xx committed Jun 9, 2023
1 parent 4765dc7 commit 16b80a7
Show file tree
Hide file tree
Showing 10 changed files with 46 additions and 36 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ and proving real time identification of the birds it hears.

# Newest stuff
(listed newest first)
- Added support for PulseAudio input, meaning you can plug a mic into your soundcard and use that for audio. It wasn't that
hard to add support for it in the code, but it kind of is a pain to set up. The intersection of Linux and audio inevitably
involves pain. There's a new environment variable in the docker-compose file, and I'm going to add a wiki page for what else
you need to do.
- Added some additional debug output around db calls, and upped some timeouts. At least one user is getting database lock
problems and I'm trying to get to the bottom of that.
- I just pushed new back end and front end images that provide a more robust mechanism for restarting tasks, and therefore
loading preference and stream definition changes. More robust here means "doesn't break everyone's installs"
- Added installation and usage documentation in the Wiki
Expand All @@ -17,7 +23,6 @@ to the length of your recording length setting to restart.
- Pushed an image for V2.4 of the model for amd64 - arm64 will be done in a couple hours. New model is supposed to be more
better (see the BirdNET-Analyzer repo for infomation) but it is definitely more slower. So V2.3 is still there under the
same image name, the new model is at mmcc73/birdnetserver2.4:latest and mmcc73/birdnetserver2.4_arm64:latest. This has been noted in the docker-compose file
- Added annual report. Also, BirdNET released a new model - I need to look into what it will take to pick that up.

# BirdCAGE
BirdCAGE is an application for monitoring the bird songs in audio streams. Security cameras often provide
Expand Down
2 changes: 1 addition & 1 deletion birdcage_backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
FROM python:3.10

# Install system dependencies
RUN apt-get update && apt-get install -y ffmpeg
RUN apt-get update && apt-get install -y ffmpeg pulseaudio

COPY requirements.txt /
# Install Python packages from requirements.txt
Expand Down
2 changes: 1 addition & 1 deletion birdcage_backend/app/models/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def create_commands_table():


def check_command_value(command_name):
connection = sqlite3.connect(DATABASE_FILE)
connection = sqlite3.connect(DATABASE_FILE, timeout=20)
cursor = connection.cursor()

cursor.execute('SELECT value FROM commands WHERE name = ?', (command_name,))
Expand Down
16 changes: 6 additions & 10 deletions birdcage_backend/app/models/detections.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,10 @@ def create_detections_table():


def add_detection(timestamp, stream_id, streamname, scientific_name, common_name, confidence, filename):
connection = sqlite3.connect(DATABASE_FILE)
cursor = connection.cursor()

cursor.execute('''
INSERT INTO detections (timestamp, stream_id, streamname, scientific_name, common_name, confidence, filename)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (timestamp, stream_id, streamname, scientific_name, common_name, confidence, filename))

connection.commit()
with sqlite3.connect(DATABASE_FILE, timeout=20) as connection:
cursor = connection.cursor()
cursor.execute('''
INSERT INTO detections (timestamp, stream_id, streamname, scientific_name, common_name, confidence, filename)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (timestamp, stream_id, streamname, scientific_name, common_name, confidence, filename))
connection.close()

22 changes: 8 additions & 14 deletions birdcage_backend/app/models/recording_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def create_recording_metadata_table():


def get_metadata_by_filename(filename):
connection = sqlite3.connect(DATABASE_FILE)
connection = sqlite3.connect(DATABASE_FILE, timeout=20)
cursor = connection.cursor()

cursor.execute("SELECT * FROM recording_metadata WHERE filename = ?", (filename,))
Expand All @@ -40,21 +40,15 @@ def get_metadata_by_filename(filename):


def delete_metadata_by_filename(filename):
connection = sqlite3.connect(DATABASE_FILE)
cursor = connection.cursor()

cursor.execute("DELETE FROM recording_metadata WHERE filename = ?", (filename,))

connection.commit()
with sqlite3.connect(DATABASE_FILE, timeout=20) as connection:
cursor = connection.cursor()
cursor.execute("DELETE FROM recording_metadata WHERE filename = ?", (filename,))
connection.close()


def set_metadata(filename, stream_id, streamname, timestamp):
connection = sqlite3.connect(DATABASE_FILE)
cursor = connection.cursor()

cursor.execute('''INSERT INTO recording_metadata (filename, stream_id, streamname, timestamp)
VALUES (?, ?, ?, ?)''', (filename, stream_id, streamname, timestamp))

connection.commit()
with sqlite3.connect(DATABASE_FILE, timeout=20) as connection:
cursor = connection.cursor()
cursor.execute('''INSERT INTO recording_metadata (filename, stream_id, streamname, timestamp)
VALUES (?, ?, ?, ?)''', (filename, stream_id, streamname, timestamp))
connection.close()
22 changes: 17 additions & 5 deletions birdcage_backend/app/stream_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ def record_stream_ffmpeg(stream_url, protocol, transport, seconds, output_filena
.run()
)

elif protocol == 'pulse':
(
ffmpeg
.input(stream_url, f='pulse')
.output(output_filename, format='wav', t=seconds, loglevel='warning')
.run()
)

elif protocol == 'youtube':
youtube_stream_url = get_youtube_stream_url(stream_url, format_code=91)
if youtube_stream_url is None:
Expand All @@ -83,7 +91,6 @@ def record_stream_ffmpeg(stream_url, protocol, transport, seconds, output_filena

@shared_task(bind=True)
def record_stream(self, stream, preferences):

# indicate that the task is running
task_id = self.request.id
redis_client.hset('task_state', f'{task_id}_status', 'running')
Expand Down Expand Up @@ -137,9 +144,10 @@ def sigterm_handler(signal_number, frame):

if result['status'] == 'success':
# The recording was successful
print(f"Recording successful. File saved to: {result['filepath']}", flush=True)
print(f"Recording successful. File saved to: {result['filepath']} Now setting metadata", flush=True)
set_metadata(os.path.basename(tmp_filename),
stream_id, name, datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
print(f"Metadata set for: {result['filepath']}", flush=True)

else:
# The recording failed
Expand Down Expand Up @@ -238,12 +246,16 @@ def check_results(results, filepath, recording_metadata, preferences, mqttclient
# don't store an mp3 file name if detectionaction is 'log' even if there happens to already be a recording
# from this interval
if detectionaction == 'log':
print("Adding detection", flush=True)
add_detection(timestamp, stream_id, streamname, scientific_name, common_name, confidence_score,
'')
print("Detection added", flush=True)

else: # if detection action is record or alert
print("Adding detection", flush=True)
add_detection(timestamp, stream_id, streamname, scientific_name, common_name, confidence_score,
mp3_filename)
print("Detection added", flush=True)

notify(detectionaction, timestamp, stream_id, streamname, scientific_name, common_name,
confidence_score, mp3path)
Expand All @@ -255,7 +267,6 @@ def check_results(results, filepath, recording_metadata, preferences, mqttclient

@shared_task(bind=True)
def analyze_recordings(self):

# indicate that the task is a-runnin'
task_id = self.request.id
redis_client.hset('task_state', f'{task_id}_status', 'running')
Expand Down Expand Up @@ -357,7 +368,9 @@ def sigterm_handler(signal_number, frame):
# That file has been analyzed and results stored. Delete it and its metadata record
if os.path.exists(file_path):
os.remove(file_path)
print("Deleting metadata", flush=True)
delete_metadata_by_filename(filename)
print("Metadata deleted", flush=True)

else:
print('FAIL')
Expand All @@ -383,7 +396,6 @@ def sigterm_handler(signal_number, frame):

@shared_task(bind=True)
def monitor_tasks(self, task_ids):

def all_tasks_stopped():
for task_id in task_ids:
task_status = redis_client.hget('task_state', f'{task_id}_status').decode()
Expand Down Expand Up @@ -477,4 +489,4 @@ def process_streams():

task_ids = start_tasks()
# Start the monitor_tasks task
monitor_tasks.apply_async(args=[task_ids])
monitor_tasks.apply_async(args=[task_ids])
4 changes: 2 additions & 2 deletions birdcage_backend/app/views/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def get_birds_of_the_week():

@filters_blueprint.route('/api/filters/thresholds/<int:user_id>', methods=['GET'])
def get_thresholds(user_id):
connection = sqlite3.connect(DATABASE_FILE)
connection = sqlite3.connect(DATABASE_FILE, timeout=20)
cursor = connection.cursor()

cursor.execute(
Expand Down Expand Up @@ -77,7 +77,7 @@ def set_thresholds(user_id):

@filters_blueprint.route('/api/filters/overrides/<int:user_id>', methods=['GET'])
def get_overrides(user_id):
connection = sqlite3.connect(DATABASE_FILE)
connection = sqlite3.connect(DATABASE_FILE, timeout=20)
cursor = connection.cursor()

cursor.execute("SELECT species_name, override_type FROM species_overrides WHERE user_id = ?;", (user_id,))
Expand Down
2 changes: 0 additions & 2 deletions birdcage_backend/app/views/streams.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from flask import Blueprint, request, jsonify
import sqlite3
from config import DATABASE_FILE
from ..models.preferences import check_password
from functools import wraps
from app.decorators import admin_required

streams_blueprint = Blueprint('streams', __name__)
Expand Down
2 changes: 2 additions & 0 deletions birdcage_frontend/templates/stream_settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ <h1 class="my-4">Stream Settings</h1>
<option value="rtsp">RTSP</option>
<option value="rtmp">RTMP</option>
<option value="youtube">YouTube</option>
<option value="pulse">PulseAudio Source</option>
</select>
</div>
<div class="form-group">
Expand Down Expand Up @@ -59,6 +60,7 @@ <h2 id="update-stream-title" style="display:none">Update Stream</h2>
<option value="rtsp">RTSP</option>
<option value="rtmp">RTMP</option>
<option value="youtube">YouTube</option>
<option value="pulse">PulseAudio Source</option>
</select>
</div>
<div class="form-group">
Expand Down
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ services:
CONCURRENCY: 10 # this many processes will get started up to handle concurrent tasks. The app needs (number of streams) + 2?
# I think? Maybe +3? Look, I don't really know what I'm doing here.
JWT_SECRET_KEY: TheseAreTheTimesThatTryMensSouls #this is used to sign tokens, so use your own Enlightenment philosopher quote
PULSE_SERVER: 172.17.0.1 #If you are using soundcard input, you need an address here that will get you to the host. This is
#the default gateway address for docker networks, so in most cases will be fine. If you want
#to be fancy you can use a device on a remote machine and put its address here. See the wiki
tmpfs:
- /tmp:size=16M #you might want to increase this size if you are recording a bunch of streams, if your
# streams are particularly hi-res, or if your analyzer might be periodically unavailable
Expand Down

0 comments on commit 16b80a7

Please sign in to comment.