Created
October 19, 2016 15:24
-
-
Save ednisley/749c9072d452e946a9b50c5e3b2c9489 to your computer and use it in GitHub Desktop.
Python source code: Raspberry Pi streaming radio player -- improved pipe handling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from evdev import InputDevice,ecodes,KeyEvent | |
import subprocess32 as subp | |
import select | |
import re | |
import sys | |
import time | |
import logging | |
Media = {'KEY_KP7' : ['Classical',False,['mplayer','--quiet','-playlist','http://stream2137.init7.net/listen.pls']], | |
'KEY_KP8' : ['Jazz',False,['mplayer','--quiet','-playlist','http://stream2138.init7.net/listen.pls']], | |
'KEY_KP9' : ['WMHT',False,['mplayer','--quiet','http://live.str3am.com:2070/wmht1']], | |
'KEY_KP4' : ['Classic 1000',True,['mplayer','--quiet','-playlist','http://listen.radionomy.com/1000classicalhits.m3u']], | |
'KEY_KP5' : ['DCNY 911',False,['mplayer','--quiet','-playlist','http://www.broadcastify.com/scripts/playlists/1/12305/-5857889408.m3u']], | |
'KEY_KP6' : ['WAMC',False,['mplayer','--quiet','http://pubint.ic.llnwd.net/stream/pubint_wamc']], | |
'KEY_KP1' : ['60s',True,['mplayer','--quiet','-playlist','http://listen.radionomy.com/all60sallthetime-keepfreemusiccom.m3u']], | |
'KEY_KP2' : ['50-70s',True,['mplayer','--quiet','-playlist','http://listen.radionomy.com/golden-50-70s-hits.m3u']], | |
'KEY_KP3' : ['Soft Rock',True,['mplayer','--quiet','-playlist','http://listen.radionomy.com/softrockradio.m3u']], | |
'KEY_KP0' : ['Zen',True,['mplayer','--quiet','-playlist','http://listen.radionomy.com/zen-for-you.m3u']] | |
} | |
CurrentKC = 'KEY_KP3' | |
Controls = {'KEY_KPSLASH' : '//////', | |
'KEY_KPASTERISK' : '******', | |
'KEY_KPENTER' : ' ', | |
'KEY_KPMINUS' : '<', | |
'KEY_KPPLUS' : '>', | |
'KEY_VOLUMEUP' : '*', | |
'KEY_VOLUMEDOWN' : '/' | |
} | |
MuteStrings = ["TargetSpot","[Unknown]","Advert:","+++","---","SRR","Srr","ZEN FOR"] | |
MuteDelay = 8.0 # delay before non-music track; varies with buffering | |
UnMuteDelay = 7.5 # delay after non-music track | |
Muted = False # keep track of muted state | |
MixerChannel = 'PCM' # which amixer thing to use | |
logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s',filename='/tmp/Streamer.log',level=logging.INFO) | |
logger = logging.getLogger() | |
# set up event inputs and polling objects | |
# This requires some udev magic to create the symlinks | |
k = InputDevice('/dev/input/keypad') | |
k.grab() | |
kp = select.poll() | |
kp.register(k.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR) | |
v = InputDevice('/dev/input/volume') | |
v.grab() | |
vp = select.poll() | |
vp.register(v.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR) | |
# set up file for output tracing | |
lw = open('/tmp/mp.log','w') # mplayer piped output | |
# set the mixer output low enough that the initial stream won't wake the dead | |
subp.call(['amixer','sset',MixerChannel,'10']) | |
# Start the player with the default stream, set up for polling | |
print 'Starting mplayer on',Media[CurrentKC][0],' -> ',Media[CurrentKC][-1][-1] | |
logging.info('Starting mplayer on %s -> %s',Media[CurrentKC][0],Media[CurrentKC][-1][-1]) | |
p = subp.Popen(Media[CurrentKC][-1], | |
stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT) | |
pp = select.poll() # this may be valid for other invocations, but is not pretty | |
pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR) | |
print ' ... running' | |
#-------------------- | |
#--- Play the streams | |
while True: | |
# pluck next line from mplayer and decode it | |
if [] != pp.poll(10): | |
text = p.stdout.readline() | |
if 'ICY Info: ' in text: # these lines may contain track names | |
lw.write(text) | |
lw.flush() | |
trkinfo = text.split(';') # also splits at semicolon embedded in track name | |
# logging.info('Raw split line: %s', trkinfo) | |
for ln in trkinfo: | |
if 'StreamTitle' in ln: # this part contains a track name | |
NeedMute = False # assume a listenable track | |
if 2 == ln.count("'"): # exactly two single quotes = probably none embedded in track name | |
trkhit = re.search(r"StreamTitle='(.*)'",ln) | |
if trkhit: # true for valid search results | |
TrackName = trkhit.group(1) # the string between the two quotes | |
print 'Track name: ', TrackName | |
logging.info('Track name: [%s]', TrackName) | |
if Media[CurrentKC][1] and ( (len(TrackName) == 0) or any(m in TrackName for m in MuteStrings) ) : | |
NeedMute = True | |
else: | |
print ' ... semicolon in track name: ', ln | |
logging.info('Semicolon in track name: [' + ln + ']') | |
else: | |
print ' ... quotes in track name: ', ln | |
logging.info('Quotes in track name: [' + ln + ']') | |
if NeedMute: | |
print ' ... muting:', | |
if Media[CurrentKC][1] and not Muted: | |
time.sleep(MuteDelay) # brute-force assumption about buffer leadtime | |
subp.call(['amixer','-q','sset',MixerChannel,'mute']) | |
Muted = True | |
print 'done' | |
logging.info('Track muted') | |
else: | |
print ' ... unmuting:', | |
if Muted: | |
if Media[CurrentKC][1]: | |
time.sleep(UnMuteDelay) # another brute-force timing assumption | |
Muted = False | |
subp.call(['amixer','-q','sset',MixerChannel,'unmute']) | |
print 'done' | |
logging.info('Track unmuted') | |
elif 'Exiting.' in text: # mplayer just imploded | |
lw.write(text) | |
lw.flush() | |
print 'Got EOF / stream cutoff' | |
logging.info('EOF or stream cutoff') | |
print ' ... killing dead mplayer' | |
pp.unregister(p.stdout.fileno()) | |
p.terminate() # p.kill() | |
p.wait() | |
# print ' ... flushing pipes' | |
# lw.truncate(0) | |
print ' ... discarding keys' | |
while [] != kp.poll(0): | |
kev = k.read | |
print ' ... restarting mplayer: ',Media[CurrentKC][0] | |
logging.info('Restarting mplayer') | |
p = subp.Popen(Media[CurrentKC][-1], | |
stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT) | |
pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR) | |
print ' ... running' | |
logging.info(' ... running') | |
# accept pending events from volume control knob | |
if [] != vp.poll(10): | |
vev = v.read() | |
lw.write('Volume') | |
lw.flush() | |
for e in vev: | |
if e.type == ecodes.EV_KEY: | |
kc = KeyEvent(e).keycode | |
# print 'Volume kc: ',kc | |
if kc in Controls: | |
print 'Vol Control: ',kc | |
try: | |
p.stdin.write(Controls[kc]) | |
except Exception as e: | |
print "Can't send control: ",e | |
print ' ... restarting player: ',Media[CurrentKC][0] | |
logging.info('Error sending volume, restarting player') | |
pp.unregister(p.stdout.fileno()) | |
p = subp.Popen(Media[CurrentKC][-1], | |
stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT) | |
pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR) | |
print ' ... running' | |
logging.info(' ... running') | |
# accept pending events from keypad | |
if [] != kp.poll(10): | |
kev = k.read() | |
lw.write("Keypad") | |
lw.flush() | |
for e in kev: | |
if e.type == ecodes.EV_KEY: | |
kc = KeyEvent(e).keycode | |
if kc == 'KEY_NUMLOCK': # discard these, as we don't care | |
continue | |
# print 'Got: ',kc | |
if (kc == 'KEY_BACKSPACE') and (KeyEvent(e).keystate == KeyEvent.key_hold): | |
if True: | |
print 'Backspace = shutdown!' | |
p.kill() | |
logging.shutdown() | |
q = subp.call(['sudo','shutdown','-P','now']) | |
q.wait() | |
time.sleep(5) | |
print "Oddly, we did not die..." | |
else: | |
print 'BS = bail from main!' | |
logging.shutdown() | |
sys.exit(0) | |
break | |
if KeyEvent(e).keystate != KeyEvent.key_down: # discard key up & other rubbish | |
continue | |
if kc in Controls: | |
print 'Control:', kc | |
try: | |
p.stdin.write(Controls[kc]) | |
except Exception as e: | |
print "Can't send control: ",e | |
print ' ... restarting player: ',Media[CurrentKC][0] | |
logging.info('Error sending controls, restarting player') | |
pp.unregister(p.stdout.fileno()) | |
p.terminate() # p.kill() | |
p.wait() | |
p = subp.Popen(Media[CurrentKC][-1], | |
stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT) | |
pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR) | |
print ' ... running' | |
logging.info(' ... running') | |
if kc in Media: | |
print 'Switching stream to ',Media[kc][0],' -> ',Media[kc][-1][-1] | |
logging.info('Switching stream: ' + Media[kc][0] + ' -> ' + Media[kc][-1][-1]) | |
CurrentKC = kc | |
print ' ... halting player' | |
try: | |
p.communicate(input='q') | |
except Exception as e: | |
print 'Perhaps mplayer died?',e | |
print ' ... killing it for sure' | |
pp.unregister(p.stdout.fileno()) | |
p.terminate() # p.kill() | |
p.wait() | |
# print ' ... flushing pipes' | |
# lw.truncate(0) | |
print ' ... restarting player: ',Media[CurrentKC][0] | |
p = subp.Popen(Media[CurrentKC][-1], | |
stdin=subp.PIPE,stdout=subp.PIPE,stderr=subp.STDOUT) | |
pp.register(p.stdout.fileno(),select.POLLIN + select.POLLPRI + select.POLLERR) | |
print ' ... running' | |
logging.info(' ... running') | |
print 'Out of loop!' | |
logging.shutdown() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
More details on my blog at http://wp.me/poZKh-6eT