Merge pull request #4 from UniversityRadioYork/player-hardening

Player Hardening
This commit is contained in:
Matthew Stratford 2020-10-30 19:36:42 +00:00 committed by GitHub
commit 92d48993e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 326 additions and 82 deletions

7
.vscode/launch.json vendored
View file

@ -1,6 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Launch Player Standalone",
"type": "python",
"request": "launch",
"program": "./player.py",
"console": "integratedTerminal"
},
{
"name": "Python: Launch Server Standalone",
"type": "python",

View file

@ -1,4 +1,4 @@
pygame==2.0.0.dev20
pygame==2.0.0.dev24
flask
mutagen
sounddevice

365
player.py
View file

@ -1,101 +1,239 @@
# This is the player. Reliability is critical here, so we're catching
# literally every exception possible and handling it.
# It is key that whenever the parent server tells us to do something
# that we respond with something, FAIL or OKAY. The server doesn't like to be kept waiting.
from queue import Empty
import multiprocessing
import setproctitle
import copy
import json
import time
from pygame import mixer
from state_manager import StateManager
from mutagen.mp3 import MP3
from pygame import mixer
import time
import json
import copy
import os
import setproctitle
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
class Player():
state = None
running = False
out_q = None
last_msg = None
__default_state = {
"initialised": False,
"filename": "",
"channel": -1,
"playing": False,
"paused": False,
"loaded": False,
"pos": 0,
"pos_offset": 0,
"pos_true": 0,
"remaining": 0,
"length": 0,
"loop": False,
"output": None
}
@property
def isInit(self):
try:
mixer.music.get_busy()
except:
return False
else:
return True
@property
def isPlaying(self):
if self.isInit:
return (not self.isPaused) and bool(mixer.music.get_busy())
return False
@property
def isPaused(self):
return self.state.state["paused"]
@property
def isLoaded(self):
if not self.state.state["filename"]:
return False
if self.isPlaying:
return True
def isPlaying(self):
return bool(mixer.music.get_busy())
# Because Pygame/SDL is annoying
# We're not playing now, so we can quickly test run
# If that works, we're loaded.
try:
position = self.state.state["pos"]
mixer.music.set_volume(0)
mixer.music.play(0)
except:
try:
mixer.music.set_volume(1)
except:
pass
return False
if position > 0:
self.pause()
else:
self.stop()
mixer.music.set_volume(1)
return True
def play(self):
@property
def status(self):
res = json.dumps(self.state.state)
return res
mixer.music.play(0)
def play(self, pos=0):
# if not self.isPlaying:
try:
mixer.music.play(0, pos)
self.state.update("pos_offset", pos)
except:
return False
self.state.update("paused", False)
return True
# return False
def pause(self):
mixer.music.pause()
# if self.isPlaying:
try:
mixer.music.pause()
except:
return False
self.state.update("paused", True)
return True
# return False
def unpause(self):
mixer.music.play(0, self.state.state["pos"])
if not self.isPlaying:
try:
self.play(self.state.state["pos_true"])
except:
return False
self.state.update("paused", False)
return True
return False
def stop(self):
mixer.music.stop()
# if self.isPlaying or self.isPaused:
try:
mixer.music.stop()
except:
return False
self.state.update("pos", 0)
self.state.update("pos_offset", 0)
self.state.update("pos_true", 0)
self.state.update("paused", False)
return True
# return False
def seek(self, pos):
if self.isPlaying():
mixer.music.play(0, pos)
if self.isPlaying:
try:
self.play(pos)
except:
return False
return True
else:
self.updateState(pos)
self.state.update("paused", True)
self._updateState(pos=pos)
return True
def load(self, filename):
if not self.isPlaying():
if not self.isPlaying:
self.unload()
self.state.update("filename", filename)
mixer.music.load(filename)
if ".mp3" in filename:
song = MP3(filename)
self.state.update("length", song.info.length)
else:
self.state.update("length", mixer.Sound(filename).get_length()/1000)
try:
mixer.music.load(filename)
except:
# We couldn't load that file.
print("Couldn't load file:", filename)
return False
try:
if ".mp3" in filename:
song = MP3(filename)
self.state.update("length", song.info.length)
else:
self.state.update("length", mixer.Sound(filename).get_length()/1000)
except:
return False
return True
def unload(self):
if not self.isPlaying:
try:
mixer.music.unload()
self.state.update("paused", False)
self.state.update("filename", "")
except:
return False
return not self.isLoaded
def quit(self):
mixer.quit()
self.state.update("paused", False)
def output(self, name=None):
self.quit()
self.state.update("output", name)
self.state.update("filename", "")
try:
if name:
mixer.init(44100, -16, 1, 1024, devicename=name)
else:
mixer.init(44100, -16, 1, 1024)
except:
return "FAIL:Failed to init mixer, check sound devices."
return False
return True
def _updateState(self, pos=None):
self.state.update("initialised", self.isInit)
if self.isInit:
# TODO: get_pos returns the time since the player started playing
# This is NOT the same as the position through the song.
if self.isPlaying:
# Get one last update in, incase we're about to pause/stop it.
self.state.update("pos", max(0, mixer.music.get_pos()/1000))
self.state.update("playing", self.isPlaying)
self.state.update("loaded", self.isLoaded)
if (pos):
self.state.update("pos", max(0, pos))
self.state.update("pos_true", self.state.state["pos"] + self.state.state["pos_offset"])
self.state.update("remaining", self.state.state["length"] - self.state.state["pos_true"])
def _retMsg(self, msg, okay_str=False):
response = self.last_msg + ":"
if msg == True:
response += "OKAY"
elif isinstance(msg, str):
if okay_str:
response += "OKAY:" + msg
else:
response += "FAIL:" + msg
else:
self.state.update("output", name)
return "OK"
def updateState(self, pos=None):
self.state.update("playing", self.isPlaying())
if (pos):
self.state.update("pos", max(0, pos))
else:
self.state.update("pos", max(0, mixer.music.get_pos()/1000))
self.state.update("remaining", self.state.state["length"] - self.state.state["pos"])
def getDetails(self):
res = "RESP:DETAILS: " + json.dumps(self.state.state)
return res
response += "FAIL"
if self.out_q:
self.out_q.put(response)
def __init__(self, channel, in_q, out_q):
self.running = True
self.out_q = out_q
setproctitle.setproctitle("BAPSicle - Player " + str(channel))
self.state = StateManager("channel" + str(channel), self.__default_state)
@ -108,49 +246,128 @@ class Player():
print("Setting output to: " + loaded_state["output"])
self.output(loaded_state["output"])
else:
print("Using default output device.")
self.output()
if loaded_state["filename"]:
print("Loading filename: " + loaded_state["filename"])
self.load(loaded_state["filename"])
if loaded_state["pos"] != 0:
print("Seeking to pos: " + str(loaded_state["pos"]))
self.seek(loaded_state["pos"])
if loaded_state["pos_true"] != 0:
print("Seeking to pos_true: " + str(loaded_state["pos_true"]))
self.seek(loaded_state["pos_true"])
if loaded_state["playing"] == True:
print("Resuming.")
self.unpause()
if loaded_state["playing"] == True:
print("Resuming.")
self.unpause()
else:
print("No file was previously loaded.")
while self.running:
time.sleep(0.01)
incoming_msg = in_q.get()
if (not incoming_msg):
continue
if self.isInit():
self.updateState()
if (incoming_msg == 'PLAY'):
self.play()
if (incoming_msg == 'PAUSE'):
self.pause()
if (incoming_msg == 'UNPAUSE'):
self.unpause()
if (incoming_msg == 'STOP'):
self.stop()
if (incoming_msg == 'QUIT'):
self.quit()
self.running = False
if (incoming_msg.startswith("SEEK")):
split = incoming_msg.split(":")
self.seek(float(split[1]))
if (incoming_msg.startswith("LOAD")):
split = incoming_msg.split(":")
self.load(split[1])
if (incoming_msg == 'DETAILS'):
out_q.put(self.getDetails())
time.sleep(0.1)
self._updateState()
try:
try:
self.last_msg = in_q.get_nowait()
except Empty:
# The incomming message queue was empty,
# skip message processing
pass
else:
if (incoming_msg.startswith("OUTPUT")):
split = incoming_msg.split(":")
out_q.put(self.output(split[1]))
# We got a message.
# Output re-inits the mixer, so we can do this any time.
if (self.last_msg.startswith("OUTPUT")):
split = self.last_msg.split(":")
self._retMsg(self.output(split[1]))
elif self.isInit:
if (self.last_msg == 'LOADED?'):
self._retMsg(self.isLoaded)
continue
elif (self.last_msg == 'PLAY'):
self._retMsg(self.play())
elif (self.last_msg == 'PAUSE'):
self._retMsg(self.pause())
elif (self.last_msg == 'UNPAUSE'):
self._retMsg(self.unpause())
elif (self.last_msg == 'STOP'):
self._retMsg(self.stop())
elif (self.last_msg == 'QUIT'):
self.running = False
continue
elif (self.last_msg.startswith("SEEK")):
split = self.last_msg.split(":")
self._retMsg(self.seek(float(split[1])))
elif (self.last_msg.startswith("LOAD")):
split = self.last_msg.split(":")
self._retMsg(self.load(split[1]))
elif (self.last_msg == 'UNLOAD'):
self._retMsg(self.unload())
elif (self.last_msg == 'STATUS'):
self._retMsg(self.status, True)
else:
self._retMsg("Unknown Command")
else:
if (self.last_msg == 'STATUS'):
self._retMsg(self.status)
else:
self._retMsg(False)
# Catch the player being killed externally.
except KeyboardInterrupt:
break
except SystemExit:
break
except:
raise
print("Quiting player ", channel)
self.quit()
self._retMsg("EXIT")
def showOutput(in_q, out_q):
print("Starting showOutput().")
while True:
time.sleep(0.01)
last_msg = out_q.get()
print(last_msg)
if __name__ == "__main__":
in_q = multiprocessing.Queue()
out_q = multiprocessing.Queue()
outputProcess = multiprocessing.Process(
target=showOutput,
args=(in_q, out_q),
).start()
playerProcess = multiprocessing.Process(
target=Player,
args=(-1, in_q, out_q),
).start()
# Do some testing
in_q.put("LOADED?")
in_q.put("PLAY")
in_q.put("LOAD:\\Users\\matth\\Documents\\GitHub\\bapsicle\\dev\\test.mp3")
in_q.put("LOADED?")
in_q.put("PLAY")
print("Entering infinite loop.")
while True:
pass

View file

@ -45,7 +45,7 @@ def ui_index():
def ui_config():
channel_states = []
for i in range(3):
channel_states.append(details(i))
channel_states.append(status(i))
devices = sd.query_devices()
outputs = []
@ -67,7 +67,7 @@ def ui_config():
def ui_status():
channel_states = []
for i in range(3):
channel_states.append(details(i))
channel_states.append(status(i))
data = {
'channels': channel_states,
@ -123,15 +123,29 @@ def output(channel, name):
return ui_status()
@app.route("/player/<int:channel>/details")
def details(channel):
@app.route("/player/<int:channel>/unload")
def unload(channel):
channel_to_q[channel].put("DETAILS")
channel_to_q[channel].put("UNLOAD")
return ui_status()
@app.route("/player/<int:channel>/status")
def status(channel):
channel_to_q[channel].put("STATUS")
while True:
response = channel_from_q[channel].get()
if response.startswith("STATUS:"):
response = response[7:]
response = response[response.index(":")+1:]
try:
response = json.loads(response)
except:
pass
if response and response.startswith("RESP:DETAILS"):
return json.loads(response.strip("RESP:DETAILS:"))
return response
@app.route("/player/all/stop")
@ -169,6 +183,7 @@ def stopServer():
q.put("QUIT")
for player in channel_p:
player.join()
global app
app = None

View file

@ -7,6 +7,11 @@
<code>
{% for player in data.channels %}
<a href="/player/{{player.channel}}/play">Play</a>
{% if player.paused %}
<a href="/player/{{player.channel}}/unpause">UnPause</a>
{% else %}
<a href="/player/{{player.channel}}/pause">Pause</a>
{% endif %}
<a href="/player/{{player.channel}}/stop">Stop</a>
<a href="/player/{{player.channel}}/seek/50">Seek 50</a>
{{player}}<br>