Skip to content

Commit

Permalink
Added async websocket for frontends, updated readme and apache accord…
Browse files Browse the repository at this point in the history
…ingly
  • Loading branch information
dud1337 committed Mar 21, 2021
1 parent c394488 commit a225f92
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 20 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ MAINTAINER dud1337 <grant@dud.li>
#

USER root
EXPOSE 8138 8139
EXPOSE 8138 8139 8140

# Install VLC & Pulse Audio
RUN apt update && apt install -y vlc python3-pip
Expand Down
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
███ (_)\/|_|| | | ███
███ ███
███ ███
v0.10 █████████ 20210315
v0.11 █████████ 20210321
```
## Table of Contents
* [About](#about)
Expand All @@ -28,20 +28,21 @@ v0.10 █████████ 20210315


## About
Morph Ovum is your free and open source community radio service.
Morph Ovum is a free and open source community radio service.

* HTTP streams your radio's audio
* No sound card and no desktop environment required
* 3 audio sources (*all optional*):
* Music (*e.g. smooth jazz*)
* Ambience (*e.g. distant thunderstorms*)
* Clips: Approximately every n minutes, play an audio clip (*e.g. "you're listening to morph ovum!"*)
* Provides a [Flash-RESTful](https://flask-restful.readthedocs.io/en/latest/index.html)-based API for real-time control
* HTTP streams your radio's audio
* Provides a websocket for frontends to avoid polling
* Plays anything [VLC Media Player](https://www.videolan.org/vlc/) can play:
* Your personal library
* An [airsonic](https://github.com/airsonic/airsonic) playlist
* An internet radio station
* A YouTube playlist
* YouTube videos
* ...


Expand All @@ -55,14 +56,15 @@ services:
stdin_open: true
tty: true
ports:
- '8138:8138'
- '8139:8139'
- '8138:8138'
- '8139:8139'
- '8140:8140'
# volumes:
# - /path/to/your/music:/fm/music
# - /path/to/your/ambience:/fm/ambience
# - /path/to/your/clips:/fm/clips
# - /path/to/your/playlists:/fm/playlists
# - /path/to/your/config_dir:/fm/conf
# - /path/to/your/config_dir:/fm/conf
environment:
- MORPH_OVUM_PASSWORD=changeme # not used if config_dir is given
- TZ="Europe/Zurich"
Expand Down Expand Up @@ -216,12 +218,14 @@ The API listens by default on http://127.0.0.1:8139/api if ran via the above doc
### Python Requirements
Obtained by docker container automatically. See `requirements.txt`.
```
asyncio
decorator
flask_limiter
flask_restful
python-vlc
pyyaml
requests
websockets
```
### File Purposes
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ services:
ports:
- '8138:8138'
- '8139:8139'
- '8140:8140'
volumes:
- ./samples/music:/fm/music
- ./samples/ambience:/fm/ambience
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
asyncio
decorator
flask_limiter
flask_restful
python-vlc
pyyaml
requests
websockets
3 changes: 3 additions & 0 deletions res/morph_ovum.vhosts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@

ProxyPass / http://127.0.0.1:8139/
ProxyPassReverse / http://127.0.0.1:8139/

# Websocket. Requires: a2enmod proxy_wstunnel
ProxyPass /notify ws://127.0.0.1:8140
</VirtualHost>
5 changes: 2 additions & 3 deletions src/io_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def wp_funcs(self, url, music_or_ambience, wp_type):
return self.make_output_data('url is missing protocol (http or https)', err=True)

if re.search('http(?:s?)://?(?:www\.)?youtu\.?be(?:\.com)?', url):
if not re.search('http(?:s?)://(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]{11})(\?(amp;)?[\w\=]*|$)', url):
if not re.search('http(?:s?)://(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]{11})((amp;)?[\w\=]*|$)', url):
return self.make_output_data('YouTube url "' + url + '" abnormal', err=True)

try:
Expand Down Expand Up @@ -473,5 +473,4 @@ def clips_now(self):

now = datetime.datetime.today()
self.audio_players.clips_thread.clip_schedule = now
return self.make_output_data('clip scheduled for ' + str(now[:-7:]))

return self.make_output_data('clip scheduled for ' + str(now)[:-7:])
83 changes: 74 additions & 9 deletions src/player_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
#
# 1. Defines config.yaml loader & validation functions
# 2. Define functions for VLC library classes
# 3. Define AudioPlayers class
# 3. Define nofication websockets server class
# 4. Define AudioPlayers class
# 4 VLC Instances
# 1 Streams the pulse audio virtual sink
# 3 Audio players that play to the virtual sink
# 1. Music
# 2. Ambience
# 3. Clips
# 1 ClipsThread
# 4. Define ClipsThread Thread
# 5. Define ClipsThread Thread
# Manages clip playing times
#
######################################################################
Expand All @@ -26,6 +27,8 @@
import yaml
import datetime
import socket
import asyncio
import websockets
from threading import Thread
from time import sleep
from random import choice, normalvariate, shuffle
Expand Down Expand Up @@ -368,9 +371,64 @@ def audio_file_dir_walk(directory, allowed_file_extensions={'mp3','wav'}):

return music_file_list


######################################################################
#
# 3. Define nofication websockets server class
#
######################################################################
class NotificationWebsocketsServer:
'''Creates a websocket server for clients to connect to and receive
updates about media list changes. Allows not to have to poll for updates'''
users = set()
music_change = False
ambience_change = False

def __init__(self):
new_loop = asyncio.new_event_loop()
start_server = websockets.serve(self.handler, '0.0.0.0', 8140, loop=new_loop)
t = Thread(target=self.start_loop, args=(new_loop, start_server))
t.start()

async def register(self, websocket):
self.users.add(websocket)

async def unregister(self, websocket):
self.users.remove(websocket)

async def handler(self, websocket, path):
await self.register(websocket)
await websocket.send('welcome')
try:
while True:
await asyncio.sleep(1)
if self.music_change:
self.music_change = False
await self.music_change_notify()
if self.ambience_change:
self.ambience_change = False
await self.ambience_change_notify()
finally:
await self.unregister(websocket)

def start_loop(self, loop, server):
loop.run_until_complete(server)
loop.run_forever()

async def music_change_notify(self):
if self.users:
for user in self.users:
await user.send('music_changed')

async def ambience_change_notify(self):
if self.users:
for user in self.users:
await user.send('ambience_changed')


######################################################################
#
# 3. Define AudioPlayers Class
# 4. Define AudioPlayers Class
#
######################################################################
class AudioPlayers:
Expand All @@ -397,23 +455,29 @@ def __init__(self, config_data):
self.mp_music.set_media_list(self.ml_music)
self.mp_ambience.set_media_list(self.ml_ambience)

# 3. Event managers (Song history)
# 3. Event managers (Song history & websocket notifications)
self.notification_server = NotificationWebsocketsServer()
self.em_music = self.mp_music.get_media_player().event_manager()
self.history_music = []
self.em_ambience = self.mp_ambience.get_media_player().event_manager()
self.history_ambience = []

def update_history_list(event, self, music_or_ambience):
'''callback function for event managers to store track history'''
def media_changed_event(event, self, music_or_ambience):
'''callback function for event managers to store track history and notify websockets clients of track changes'''
# update history
mp = getattr(self, 'mp_' + music_or_ambience)
history = getattr(self, 'history_' + music_or_ambience)
if len(history) == 101:
history = history[1::]
history.append(media_list_player_get_song(mp))
setattr(self, 'history_' + music_or_ambience, history)

self.em_music.event_attach(vlc.EventType.MediaPlayerMediaChanged, update_history_list, self, 'music')
self.em_ambience.event_attach(vlc.EventType.MediaPlayerMediaChanged, update_history_list, self, 'ambience')
# set notificatoin server thread to notify
if self.notification_server.users:
setattr(self.notification_server, music_or_ambience + '_change', True)

self.em_music.event_attach(vlc.EventType.MediaPlayerMediaChanged, media_changed_event, self, 'music')
self.em_ambience.event_attach(vlc.EventType.MediaPlayerMediaChanged, media_changed_event, self, 'ambience')

# 4. Clips need a special Thread
self.clips_thread = Clips(self)
Expand Down Expand Up @@ -488,9 +552,10 @@ def nice_quit(self):
self.mp_vaudio.stop()
self.mp_music.get_media_player().audio_set_volume(100)


######################################################################
#
# 4. Define ClipsThread Thread
# 5. Define ClipsThread Thread
# Manages clip playing times
#
######################################################################
Expand Down

0 comments on commit a225f92

Please sign in to comment.