Commit cddaf5d4 authored by Alexander Pushkov's avatar Alexander Pushkov

Add Python+GStreamer backend, 0.2.0

parent 8a38d01c
......@@ -3,22 +3,24 @@
Requirements:
* Liquidsoap (1.1.1 recommended) with some plugins (?)
* Python 3.4+
* Python 3.6+
```bash
sudo apt-get install liquidsoap liquidsoap-plugin-all python3
apt-get install liquidsoap liquidsoap-plugin-all python3
pip3 install youtube-dl
```
## Usage
```bash
liquidsoap app.liq
# in separate window:
python3 -m rrrrradio
```
radiotwitter reads some env variables. You can do:
**rrrrr** reads some env variables. You can do:
```bash
export MUSIC_PATH=~/Music
export PORT=8841
export ICECAST_HOST=example.net
export ICECAST_PORT=8080
......@@ -30,7 +32,7 @@ liquidsoap app.liq
## Live streams
You need an Icecast-compatible client to stream to radiotwitter.
You need an Icecast-compatible client to stream to **rrrrr**.
* **Username:** *any* \*
* **Password:** *desired username*
......@@ -44,7 +46,12 @@ You can modify the authorization scheme by changing the [auth.py](auth.py) file.
## Prior Art
The Liquidsoap part is mostly a copycat of [Densetos's][densetos] examples
(in Russian).
The Liquidsoap part is mostly a mixture of various developers' code:
[densetos]: https://dev.densetos.com/
- [densetos' Provodach](https://provoda.ch/about) (RU; blog with code samples doesn't work anymore)
- [djazz's Paraspite Radio](https://github.com/daniel-j/parasprite-radio/)
- [C. Schirnen's mopidy-stream](https://github.com/schinken/docker-container/blob/master/mopidy-stream/)
GStreamer part takes some ideas from [Mopidy](https://www.mopidy.com/) (and can
be replaced with it, if needed – just use `wavpackenc ! udpsink host=127.0.0.1
port=5004` as a sink.)
......@@ -3,17 +3,16 @@
set("log.file.path", "/dev/null")
set("log.stdout", true)
__version__ = "0.1.0"
__version__ = "0.2.0"
%include "util.liq"
PORT = int_of_string(envdef("PORT", "8001"))
MUSIC_PATH = envdef("MUSIC_PATH", "~/Music")
ICECAST_HOST = envdef("ICECAST_HOST", "localhost")
ICECAST_PORT = int_of_string(envdef("ICECAST_PORT", "8000"))
ICECAST_PASSWORD = envdef("ICECAST_PASSWORD", "hackme")
RADIO_URL = envdef("RADIO_URL", "https://iwannadie.club")
RADIO_DESCRIPTION = envdef("RADIO_DESCRIPTION", "TEAM RRRRRRRRRRADIO RADIO")
RADIO_DESCRIPTION = envdef("RADIO_DESCRIPTION", "Powered by Liquidsoap/#{liquidsoap.version} rrrrradio/#{__version__}")
ignore([PORT])
......
[general]
duration=0
bufferSecs=10
realtime=yes
[input]
device=jack
sampleRate=44100
bitsPerSample=16
channel=2
jackClientName=darkice
[icecast2-0]
format=mp3
bitrateMode=cbr
bitrate=320
server=localhost
password=ale
port=8081
mountPoint=live
name=example live
description=This is my station where I do what I want
genre=Music
current_user = ref ""
live.current_user = ref ""
def authorize(username, password) =
def live.authorize(username, password) =
# username is sometimes unmodifiable (hardcoded to "stream" in some clients),
# so we use only password
# so we only use password
ret = get_process_lines("./auth.py #{string.utf8.escape(password)}")
ret = list.hd(ret)
if ret != "__unauthorized__" then
current_user := ret
log("User '#{!current_user}' started streaming")
live.current_user := ret
log("User '#{!live.current_user}' started streaming")
true
else
log("Somebody tried to log in, but supplied wrong password")
......@@ -16,16 +16,16 @@ def authorize(username, password) =
end
end
def deauthorize() =
log("User '#{!current_user}' stopped streaming")
current_user := ""
def live.deauthorize() =
log("User '#{!live.current_user}' stopped streaming")
live.current_user := ""
end
live.core = audio_to_stereo(id="live", input.harbor(
"/live",
port=PORT,
auth=authorize,
on_disconnect=deauthorize,
auth=live.authorize,
on_disconnect=live.deauthorize,
buffer=10.,
max=15.,
))
......
music = crossfade(
start_next=2.,
fade_out=3.,
fade_in=1.,
mksafe(audio_to_stereo(playlist(
reload=86400,
prefix="replay_gain:",
MUSIC_PATH
)))
music = input.gstreamer.audio(
pipeline="udpsrc port=5004 ! wavpackparse ! wavpackdec ! audioconvert ! audioresample"
)
music = random([music])
import sys
import asyncio
from .player import Player
from .util import async_prompt, async_print
player = Player()
async def interact():
while True:
[cmd, *args] = (await async_prompt(":) ")).split(" ")
if cmd == "/queue" or cmd == "/q":
await player.queue(args[0])
elif cmd == "/skip" or cmd == "/s":
await player.skip()
elif cmd == "/help":
print("Commands: /queue /skip /help")
if sys.platform == "win32":
loop = asyncio.ProactorEventLoop()
asyncio.set_event_loop(loop)
else:
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(
player(),
interact()
))
loop.close()
import asyncio
from functools import partial
from youtube_dl import YoutubeDL
class Player:
def __init__(self):
self.urls = asyncio.Queue()
self.media_urls = asyncio.Queue()
self.proc = None
@staticmethod
def _mkpipeline(url):
return [
'souphttpsrc',
'user-agent=Mozilla/5.0 (X11; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0.2',
f'location={url}', '!',
'decodebin', '!',
# Audio may come in different sample rates, but LiqSoap doesn't
# play well with that, so let's resample to a constant rate:
'audioresample', '!',
'audio/x-raw, rate=48000', '!',
'audioconvert', '!',
'wavpackenc', '!',
'udpsink',
'host=127.0.0.1',
'port=5004',
]
async def _control_gstreamer_process(self):
while True:
url = await self.media_urls.get()
self.proc = await asyncio.create_subprocess_exec(
"gst-launch-1.0",
*self._mkpipeline(url),
# stdout=asyncio.subprocess.DEVNULL,
# stderr=asyncio.subprocess.DEVNULL
)
await self.proc.wait()
self.proc = None
async def _process_urls(self):
loop = asyncio.get_event_loop()
while True:
url = await self.urls.get()
with YoutubeDL({ "quiet": True }) as ydl:
f = partial(ydl.extract_info, url, download=False)
info_dict = await loop.run_in_executor(None, f)
if info_dict.get("requested_formats") is not None:
f = info_dict["requested_formats"][-1]
media_url = f["url"] + f.get("play_path", "")
else:
# For RTMP URLs, also include the playpath
media_url = info_dict["url"] + info_dict.get("play_path", "")
await self.media_urls.put(media_url)
def __call__(self):
return asyncio.gather(
self._control_gstreamer_process(),
self._process_urls(),
)
async def queue(self, url):
await self.urls.put(url)
async def skip(self):
if self.proc is not None:
self.proc.terminate()
self.proc = None
import os
import asyncio
import sys
from asyncio.streams import StreamWriter, FlowControlMixin
reader, writer = None, None
async def stdio(loop=None):
if loop is None:
loop = asyncio.get_event_loop()
reader = asyncio.StreamReader()
reader_protocol = asyncio.StreamReaderProtocol(reader)
writer_transport, writer_protocol = await loop.connect_write_pipe(FlowControlMixin, os.fdopen(1, 'wb'))
writer = StreamWriter(writer_transport, writer_protocol, None, loop)
await loop.connect_read_pipe(lambda: reader_protocol, sys.stdin)
return reader, writer
async def async_print(message):
if isinstance(message, str):
message = message.encode('utf8')
global reader, writer
if (reader, writer) == (None, None):
reader, writer = await stdio()
writer.write(message + b"\n")
await writer.drain()
async def async_prompt(message):
if isinstance(message, str):
message = message.encode('utf8')
global reader, writer
if (reader, writer) == (None, None):
reader, writer = await stdio()
writer.write(message)
await writer.drain()
line = await reader.readline()
return line.decode('utf8').replace('\r', '').replace('\n', '')
#!/usr/bin/env liquidsoap
# Try Python setup with default output instead of streaming it to Icecast.
set("log.file.path", "/dev/null")
set("log.stdout", true)
out(mksafe(input.gstreamer.audio(
pipeline="udpsrc port=5004 ! wavpackparse ! wavpackdec ! audioconvert ! audioresample"
)))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment