Merge pull request #1 from 9130_dummy_audio into main

Reviewed-on: #1
This commit is contained in:
Jono Targett 2023-06-15 12:49:11 +09:30
commit 4fdd5739ca
20 changed files with 313 additions and 39 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1
data/sampleaudio/SOURCE Normal file
View File

@ -0,0 +1 @@
https://www2.cs.uic.edu/~i101/SoundFiles/

Binary file not shown.

Binary file not shown.

BIN
data/sampleaudio/taunt.mp3 Normal file

Binary file not shown.

52
fileradio.py Normal file
View File

@ -0,0 +1,52 @@
from streamer import Streamer, is_alive
import subprocess
import time
import sys
import os
class FileRadio(Streamer):
REST_PATH = 'sample'
def __init__(self, path):
super().__init__()
self.path = path
self.basename = os.path.basename(self.path)
self.name, self.ext = os.path.splitext(self.basename)
def _stream_thread(self):
self.playback = subprocess.Popen(
[
'/usr/bin/ffmpeg',
'-re', # http://trac.ffmpeg.org/wiki/StreamingGuide#The-reflag
'-stream_loop', '-1', # Loop the stream indefinitely
'-i', self.path,
'-c', 'copy',
'-f', 'rtsp',
self.stream_address('localhost')
],
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
while self.run:
if not is_alive(self.playback):
print('Playback failed, aborting stream.', file=sys.stderr)
break
time.sleep(0.1)
self.run = False
self.playback.kill()
self.playback.wait()
self.playback = None
if __name__ == '__main__':
fr = FileRadio('./data/sampleaudio/taunt.mp3')
fr.start_stream()
while True:
time.sleep(1)

View File

@ -38,10 +38,10 @@ udpMaxPayloadSize: 1472
externalAuthenticationURL: externalAuthenticationURL:
# Enable the HTTP API. # Enable the HTTP API.
api: no api: yes
# Enable Prometheus-compatible metrics. # Enable Prometheus-compatible metrics.
metrics: no metrics: yes
# Enable pprof-compatible endpoint to monitor performances. # Enable pprof-compatible endpoint to monitor performances.
pprof: no pprof: no

View File

@ -1,7 +1,12 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
import os
import sys
import requests
import SoapySDR as soapy import SoapySDR as soapy
from radio import Radio from radio import Radio
from tuuube import Tuuube
from fileradio import FileRadio
from flask import Flask, jsonify from flask import Flask, jsonify
from flasgger import Swagger from flasgger import Swagger
@ -12,9 +17,29 @@ swag = Swagger(app)
radios = {} radios = {}
@app.route('/report')
def report():
"""Get streams report from the RTSP relay.
---
responses:
200:
description: JSON
"""
try:
r = requests.get("http://localhost:9997/v1/paths/list")
j = r.json()
for item in j['items']:
del j['items'][item]['conf']
del j['items'][item]['confName']
return jsonify(j)
except:
return "Shat the bed", 500
@app.route('/radio/report') @app.route('/radio/report')
def report(): def radio_report():
"""List radio devices available to the system. """List radio devices available to the system.
--- ---
responses: responses:
@ -115,7 +140,7 @@ def configure(radio, frequency):
def start_stream(radio): def start_stream(radio):
"""Start the radio stream. """Start the radio stream.
Once the stream has been started, connect to the stream at: Once the stream has been started, connect to the stream at:
rtsp://[host]:8554/radio/[radio]/stream rtsp://[host]:8554/radio/[radio]
--- ---
parameters: parameters:
- name: radio - name: radio
@ -169,6 +194,55 @@ def radio_info(radio):
return str(e), 400 return str(e), 400
tubes = {}
@app.route('/tuuube/<id>/start')
def start_tuuube_stream(id):
"""Start streaming from a youtube source.
Once the stream has been started, connect to the stream at:
rtsp://[host]:8554/tuuube/[id]
---
parameters:
- name: id
description:
The youtube video ID. That is the part following the `watch?v=` in the URL. For example, `dQw4w9WgXcQ`.\n
Other good options are - \n
`BaW_jenozKc`, yt_dlp package test video.\n
`b2je8uBlgFM`, stereo audio test.\n
`LDU_Txk06tM`, crab rave, a commonly used audio fidelity test.\n
`sPT_epMLkwQ`, Kilsyth CFA major factory fire dispatch radio traffic.
in: path
type: string
required: true
"""
if id not in tubes:
tubes[id] = Tuuube(id)
try:
tubes[id].start_stream()
return "", 200
except Exception as e:
return str(e), 400
@app.route('/tuuube/<id>/end')
def end_tuuube_stream(id):
"""Terminate the youtube stream.
---
parameters:
- name: id
description: The youtube video ID.
in: path
type: string
required: true
"""
try:
tubes[id].end_stream()
return "", 200
except Exception as e:
return str(e), 400
if __name__ == '__main__': if __name__ == '__main__':
import subprocess import subprocess
@ -181,6 +255,13 @@ if __name__ == '__main__':
stderr=subprocess.DEVNULL stderr=subprocess.DEVNULL
) )
for path, _, files in os.walk('./data/sampleaudio'):
for file in files:
name,ext = os.path.splitext(file)
if ext == '.mp3':
tubes[name] = FileRadio(f"{path}/{file}")
tubes[name].start_stream()
app.run( app.run(
host='0.0.0.0', host='0.0.0.0',
threaded=True, threaded=True,
@ -193,6 +274,11 @@ if __name__ == '__main__':
radios[radio].end_stream() radios[radio].end_stream()
radios = None radios = None
for tube in tubes:
if tubes[tube].is_streaming():
tubes[tube].end_stream()
tubes = None
print('Killing RTSP relay...') print('Killing RTSP relay...')
rtsp_relay.kill() rtsp_relay.kill()
rtsp_relay.wait() # Necessary? rtsp_relay.wait() # Necessary?

View File

@ -7,19 +7,19 @@ import subprocess
import struct import struct
from soapyhelpers import * from soapyhelpers import *
from samplerates import * from samplerates import *
from streamer import Streamer, is_alive
class Radio: class Radio(Streamer):
REST_PATH = 'radio'
FORMAT = 'CS16' FORMAT = 'CS16'
SAMPLES = 8192 SAMPLES = 8192
PORT = 8554
def __init__(self, name, device_info): def __init__(self, name, device_info):
super().__init__()
self.name = name self.name = name
self.device_info = device_info self.device_info = device_info
self.run = False
self.thread = None
self.device = soapy.Device(device_info) self.device = soapy.Device(device_info)
if self.device is None: if self.device is None:
raise RuntimeError("Failed to connect to radio device") raise RuntimeError("Failed to connect to radio device")
@ -83,31 +83,9 @@ class Radio:
'uarts': self.device.listUARTs(), 'uarts': self.device.listUARTs(),
} }
def is_streaming(self):
return True if (self.thread and self.thread.is_alive()) else False
def start_stream(self):
if self.is_streaming():
raise RuntimeError('Stream thread is already running')
self.run = True
self.thread = Thread(target=self._stream_thread, daemon=True, args=())
self.thread.start()
def end_stream(self):
if self.thread is None:
raise RuntimeError('No stream thread to terminate')
self.run = False
self.thread.join()
self.thread = None
def _stream_thread(self): def _stream_thread(self):
self._init_stream() self._init_stream()
def is_alive(subprocess):
return (subprocess.poll() is None)
while self.run: while self.run:
# Check that the child processes are still running # Check that the child processes are still running
if (not is_alive(self.demod)) or (not is_alive(self.playback)): if (not is_alive(self.demod)) or (not is_alive(self.playback)):
@ -136,8 +114,12 @@ class Radio:
def _init_stream(self): def _init_stream(self):
self.playback = subprocess.Popen( self.playback = subprocess.Popen(
[ [
'/usr/bin/ffmpeg', '-f', 's16le', '-ar', str(self.output_rate), '-ac', '2', '-i', '-', '/usr/bin/ffmpeg',
'-f', 'rtsp', f"rtsp://localhost{self._stream_path()}" '-f', 's16le',
'-ar', str(self.output_rate),
'-ac', '2',
'-i', '-',
'-f', 'rtsp', self.stream_address()
], ],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
@ -145,7 +127,12 @@ class Radio:
) )
self.demod = subprocess.Popen( self.demod = subprocess.Popen(
['/usr/bin/python3', 'fm_demod.py', '-f', 'CS16', '-s', str(self.sample_rate), '-d', str(self.output_rate)], [
'/usr/bin/python3', 'fm_demod.py',
'-f', 'CS16',
'-s', str(self.sample_rate),
'-d', str(self.output_rate)
],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=self.playback.stdin, stdout=self.playback.stdin,
stderr=subprocess.DEVNULL stderr=subprocess.DEVNULL
@ -176,8 +163,6 @@ class Radio:
self.playback.wait() self.playback.wait()
self.playback = None self.playback = None
def _stream_path(self):
return f":{Radio.PORT}/radio/{self.name}"
""" """

View File

@ -3,3 +3,5 @@ flasgger==0.9.5
Flask==2.2.3 Flask==2.2.3
numpy==1.17.4 numpy==1.17.4
prefixed==0.7.0 prefixed==0.7.0
PyYAML==6.0
requests==2.22.0

8
scripts/wav2mp3.sh Executable file
View File

@ -0,0 +1,8 @@
#! /bin/bash
for arg in "$@"; do
path="${arg%/*}"
file="${arg##*/}"
filename="${file%%.*}"
ffmpeg -i $path/$file -vn -ar 44100 -ac 2 -b:a 100k $path/$filename.mp3
done

View File

@ -8,6 +8,10 @@ setup() {
sudo xargs apt-get install -y < requirements.apt sudo xargs apt-get install -y < requirements.apt
sudo pip install -r requirements.pip sudo pip install -r requirements.pip
# Requires yt-dlp, but a very recent version (as youtube broke their API for the pip version)
# https://stackoverflow.com/a/75504772
python3 -m pip install --force-reinstall https://github.com/yt-dlp/yt-dlp/archive/master.tar.gz
# Install device driver # Install device driver
./scripts/SDRplay_RSP_API-Linux-3.07.1.run ./scripts/SDRplay_RSP_API-Linux-3.07.1.run

51
streamer.py Normal file
View File

@ -0,0 +1,51 @@
from threading import Thread
import yaml
with open('mediamtx.yml', 'r') as config_file:
MEDIASERVER_CONFIG = yaml.safe_load(config_file)
def is_alive(subprocess):
return True if (subprocess and subprocess.poll() is None) else False
class Streamer:
PROTOCOL = 'rtsp'
REST_PATH = 'stream'
def __init__(self):
self.run = False
self.thread = None
self.name = None
def stream_path(self):
return f"{MEDIASERVER_CONFIG['rtspAddress']}/{type(self).REST_PATH}/{self.name}"
def stream_address(self, host):
return f"{Streamer.PROTOCOL}://{host}{self.stream_path()}"
def is_streaming(self):
return True if (self.thread and self.thread.is_alive()) else False
def start_stream(self):
if self.is_streaming():
raise RuntimeError('Stream thread is already running')
self.run = True
self.thread = Thread(target=self._stream_thread, daemon=True, args=())
self.thread.start()
def end_stream(self):
if self.thread is None:
raise RuntimeError('No stream thread to terminate')
self.run = False
self.thread.join()
self.thread = None
if __name__ == '__main__':
from pprint import pprint
pprint(MEDIASERVER_CONFIG)

85
tuuube.py Executable file
View File

@ -0,0 +1,85 @@
#! /usr/bin/env python3
"""
We aren't using either the apt or the pip repositories for the youtube_dl
as there is a known bug affecting those two versions. The youtube API has changed
since their release, causing downloads to fail.
Make sure you use the ./setup.sh script to obtain the latest github release of
yt_dlp, as this version carries the latest fixes.
"""
#import youtube_dl
import yt_dlp as youtube_dl
from streamer import Streamer, is_alive
import subprocess
import time
import sys
import os
class Tuuube(Streamer):
REST_PATH = 'tuuube'
def __init__(self, name):
super().__init__()
self.name = name
self.playback = None
def source_path(self):
return f"/tmp/{self.name}.mp3"
def _stream_thread(self):
if not os.path.exists(self.source_path()) or not os.path.isfile(self.source_path()):
ydl_opts = {
'format': 'bestaudio/best',
'outtmpl': f'/tmp/{self.name}.%(ext)s', # yt_dlp will append %(ext) if not specified,
'postprocessors': [{ # resulting in `/tmp/file.mp3.mp3` :/
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': '192',
}],
}
try:
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
ydl.download([f'https://www.youtube.com/watch?v={self.name}'])
except Exception as e:
print(f'File sourcing failed, aborting stream. {e}', file=sys.stderr)
self.run = False
return
self.playback = subprocess.Popen(
[
'/usr/bin/ffmpeg',
'-re',
'-stream_loop', '-1',
'-i', self.source_path(),
'-c', 'copy',
'-f', 'rtsp',
self.stream_address('localhost')
],
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
while self.run:
if not is_alive(self.playback):
print('Playback failed, aborting stream.', file=sys.stderr)
break
time.sleep(0.1)
self.run = False
self.playback.kill()
self.playback.wait()
self.playback = None
if __name__ == '__main__':
tube = Tuuube('BaW_jenozKc')
tube.start_stream()
while True:
print(tube.is_streaming())
time.sleep(1)