Merge pull request #4 from UniversityRadioYork/player-hardening
Player Hardening
This commit is contained in:
commit
92d48993e7
5 changed files with 326 additions and 82 deletions
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
|
@ -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",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
pygame==2.0.0.dev20
|
||||
pygame==2.0.0.dev24
|
||||
flask
|
||||
mutagen
|
||||
sounddevice
|
||||
|
|
365
player.py
365
player.py
|
@ -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
|
||||
|
|
29
server.py
29
server.py
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue