Slight refactor

This commit is contained in:
Maff 2019-05-02 21:33:38 +01:00
parent 29d1acddc1
commit bad666456c
2 changed files with 115 additions and 127 deletions

2
.gitignore vendored
View File

@ -114,3 +114,5 @@ dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
.DS_Store
.vscode/

View File

@ -25,41 +25,45 @@ from random import shuffle
## ##
## If you can get this working using PJSUA2, a pull request would be greatly appreciated. ## If you can get this working using PJSUA2, a pull request would be greatly appreciated.
# This feels real hacky but I'm not sure there's a better way of creating a generic object that
# you can add properties to.
class State(object):
pass
state=State()
# Configuration # Configuration
LOG_LEVEL=0 state.LOG_LEVEL=int(getenv('TT_LOG_LEVEL', 0))
#TT_MEDIA_SOURCE and TT_LISTEN_PORT can be configured via env. variables #TT_MEDIA_SOURCE and TT_LISTEN_PORT can be configured via env. variables
sourcepath=getenv('TT_MEDIA_SOURCE', '/opt/media/') state.source=getenv('TT_MEDIA_SOURCE', '/opt/media/')
sipport=int(getenv('TT_LISTEN_PORT', 5062)) state.port=int(getenv('TT_LISTEN_PORT', 5062))
# End configuration # End configuration
# Application scaffolding # Application scaffolding
# logger functions # logger functions
def pjlog(level, str, len): def PJLog(level, line, length):
olog(level+1, "pjsip", str) Log(level+1, "pjsip", line)
#print() is used over bare print because pylint yells at me if I use bare print def Log(level, source, line, error=False):
def elog(sev, source, line): pfx='*'
print("%s %s: %s" % ("!"*sev, source, line)) if error:
pfx='!'
print("%s %s: %s" % (pfx*level, source, line))
sys.stdout.flush() sys.stdout.flush()
def olog(sev, source, line): #Generic signal handler
print("%s %s: %s" % ("*"*sev, source, line))
sys.stdout.flush()
#SIGTERM handler; could be expanded to handle SIGKILL, SIGHUP, SIGUSR1, etc.
#TODO: handle SIGHUP or SIGUSR1 for live-relaoding playlist
def sighandle(_signo, _stack_frame): def sighandle(_signo, _stack_frame):
global mainloop global state
olog(1, "sighandler", "caught signal %s" % _signo) Log(1, "sighandler", "caught signal %s" % _signo)
if _signo == 1: if _signo == 1:
#SIGHUP #SIGHUP
olog(1, "sighandler", "SIGHUP handled, reloading playlist") Log(1, "sighandler", "SIGHUP invoked playlist reload")
loadplaylist() MediaLoadPlaylist()
elif _signo == 2: elif _signo == 2:
#SIGINT #SIGINT
olog(1, "sighandler", "SIGINT handled, hanging up all calls") Log(1, "sighandler", "SIGINT invoked current call flush")
pj.Lib.instance().hangup_all() state.lib.hangup_all()
elif _signo == 15: elif _signo == 15:
#SIGTERM #SIGTERM
olog(1, "sighandler", "SIGTERM handled, closing main loop") Log(1, "sighandler", "SIGTERM invoked app shutdown")
mainloop=False state.running=False
pass pass
# Classes # Classes
@ -72,9 +76,10 @@ class AccountCb(pj.AccountCallback):
pj.AccountCallback.__init__(self, account) pj.AccountCallback.__init__(self, account)
def on_incoming_call(self, call): def on_incoming_call(self, call):
olog(2, "event-call-in", "caller %s dialled in" % call.info().remote_uri) Log(2, "event-call-in", "caller %s dialled in" % call.info().remote_uri)
call.set_callback(CallCb(call)) call.set_callback(CallCb(call))
#brief delay between ringing and connected means we can do playlist setup without a period of silence #Answer call with SIP/2.0 180 RINGING
#This kicks in the EARLY media state, allowing us to initialise the playlist before the call connects
call.answer(SIPStates.ringing) call.answer(SIPStates.ringing)
# Call Callback class # Call Callback class
class CallCb(pj.CallCallback): class CallCb(pj.CallCallback):
@ -82,151 +87,132 @@ class CallCb(pj.CallCallback):
pj.CallCallback.__init__(self, call) pj.CallCallback.__init__(self, call)
def on_state(self): def on_state(self):
olog(3, "event-state-change", "SIP/2.0 %s (%s), call %s in call with party %s" % global state
Log(2, "event-state-change", "SIP/2.0 %s (%s), call %s in call with party %s" %
(self.call.info().last_code, self.call.info().last_reason, (self.call.info().last_code, self.call.info().last_reason,
self.call.info().state_text, self.call.info().remote_uri)) self.call.info().state_text, self.call.info().remote_uri))
#call states not handled so far: CONNECTING #EARLY media state allows us to init the playlist while the call establishes
if self.call.info().state == pj.CallState.EARLY: if self.call.info().state == pj.CallState.EARLY:
global files self.playlist=state.playlist
self.playlist=files
shuffle(self.playlist) shuffle(self.playlist)
self.playlist_instance=pj.Lib.instance().create_playlist( self.instmedia=state.lib.create_playlist(
loop=True, filelist=self.playlist, label="trashtalklist") loop=True, filelist=self.playlist, label="trashtalklist")
self.playlistslot=pj.Lib.instance().playlist_get_slot(self.playlist_instance) self.slotmedia=state.lib.playlist_get_slot(self.instmedia)
olog(4, "event-call-state-early", "initialised new trashtalk playlist instance") Log(3, "event-call-state-early", "initialised new trashtalk playlist instance")
#answer the call once playlist is prepared #answer the call once playlist is prepared
self.call.answer(SIPStates.answer) self.call.answer(SIPStates.answer)
#CONFIRMED state indicates the call is connected
elif self.call.info().state == pj.CallState.CONFIRMED: elif self.call.info().state == pj.CallState.CONFIRMED:
olog(3, "event-call-state-confirmed", "answered call") Log(3, "event-call-state-confirmed", "answered call")
self.confslot=self.call.info().conf_slot self.slotcall=self.call.info().conf_slot
pj.Lib.instance().conf_connect(self.playlistslot, self.confslot) state.lib.conf_connect(self.slotmedia, self.slotcall)
olog(3, "event-call-conf-joined", "joined trashtalk to call") Log(3, "event-call-conf-joined", "joined trashtalk to call")
#DISCONNECTED state indicates the call has ended (whether on our end or the caller's)
elif self.call.info().state == pj.CallState.DISCONNECTED: elif self.call.info().state == pj.CallState.DISCONNECTED:
olog(3, "event-call-state-disconnected", "call disconnected") Log(3, "event-call-state-disconnected", "call disconnected")
pj.Lib.instance().conf_disconnect(self.playlistslot, self.confslot) state.lib.conf_disconnect(self.slotmedia, self.slotcall)
pj.Lib.instance().playlist_destroy(self.playlist_instance) state.lib.playlist_destroy(self.instmedia)
olog(3, "event-call-conf-left", "removed trashtalk instance from call and destroyed it") Log(3, "event-call-conf-left", "removed trashtalk instance from call and destroyed it")
#I'm not sure what this is for, as all media handling is actually done within the SIP events above
def on_media_state(self): def on_media_state(self):
if self.call.info().media_state == pj.MediaState.ACTIVE: if self.call.info().media_state == pj.MediaState.ACTIVE:
olog(4, "event-media-state-change", "Media State transitioned to ACTIVE") Log(4, "event-media-state-change", "Media State transitioned to ACTIVE")
else: else:
olog(4, "event-media-state-change", "Media State transitioned to INACTIVE") Log(4, "event-media-state-change", "Media State transitioned to INACTIVE")
# Main logic functions # Main logic functions
def PjInit(): def PJInit():
global lib global state
global LOG_LEVEL state.lib=pj.Lib()
lib=pj.Lib() state.cfg_ua=pj.UAConfig()
cfg_ua=pj.UAConfig() state.cfg_md=pj.MediaConfig()
#TODO: make max_calls configurable? state.cfg_ua.max_calls, state.cfg_ua.user_agent = 32, "TrashTalker/1.0"
cfg_ua.max_calls=32 state.cfg_md.no_vad, state.cfg_md.enable_ice = True, False
cfg_ua.user_agent="TrashTalker/1.0" state.lib.init(
cfg_media=pj.MediaConfig() ua_cfg=state.cfg_ua,
cfg_media.no_vad=True media_cfg=state.cfg_md,
cfg_media.enable_ice=False log_cfg=pj.LogConfig(
lib.init(ua_cfg=cfg_ua, media_cfg=cfg_media, level=state.LOG_LEVEL,
log_cfg=pj.LogConfig(level=LOG_LEVEL, callback=pjlog)) callback=PJLog
lib.set_null_snd_dev() )
lib.start(with_thread=True) )
state.lib.set_null_snd_dev()
state.lib.start(with_thread=True)
state.transport=state.lib.create_transport(
pj.TransportType.UDP,
pj.TransportConfig(state.port)
)
state.account=state.lib.create_account_for_transport(
state.transport,
cb=AccountCb()
)
state.uri="sip:%s:%s" % (state.transport.info().host, state.transport.info().port)
def PjMediaInit(): def WaitLoop():
global transport global state
global acct while state.running:
global sipuri
global sipport
global lib
transport=lib.create_transport(pj.TransportType.UDP,
pj.TransportConfig(sipport))
acct=lib.create_account_for_transport(transport, cb=AccountCb())
sipuri="sip:%s:%s" % (transport.info().host, transport.info().port)
def TrashTalkerInit():
global mainloop
while mainloop:
sleep(0.2) sleep(0.2)
def PjDeinit(): def PjDeinit():
global transport global state
global acct state.lib.hangup_all()
global sipport
global lib
lib.hangup_all()
# allow time for cleanup before destroying objects # allow time for cleanup before destroying objects
lib.handle_events(timeout=250) state.lib.handle_events(timeout=250)
try: try:
acct.delete() state.account.delete()
lib.destroy() state.lib.destroy()
lib=None state.lib=state.account=state.transport=None
acct=None
transport=None
except AttributeError: except AttributeError:
elog(1, "deinit", "AttributeError when clearing down pjsip, this is likely fine") Log(1, "deinit", "AttributeError when clearing down pjsip, this is likely fine", error=True)
pass pass
except pj.Error as e: except pj.Error as e:
elog(1, "deinit", "pjsip error when clearing down: %s" % str(e)) Log(1, "deinit", "pjsip error when clearing down: %s" % str(e), error=True)
pass pass
def loadplaylist(): def MediaLoadPlaylist():
olog(2, "playlist-load", "loading playlist files") Log(3, "playlist-load", "loading playlist files")
global sourcepath global state
if not sourcepath.endswith('/'): if not state.source.endswith('/'):
olog(1, "playlist-load", "appending trailing / to TT_MEDIA_SOURCE") Log(4, "playlist-load", "appending trailing / to TT_MEDIA_SOURCE")
sourcepath="%s/" % sourcepath state.source="%s/" % state.source
global files state.playlist=listdir(state.source)
files=listdir(sourcepath) state.playlist[:]=[state.source+file for file in state.playlist]
files[:]=[sourcepath+file for file in files] assert (len(state.playlist) > 1), "playlist path %s must contain more than one audio file" % state.source
assert (len(files) > 1), "playlist path %s must contain more than one audio file" % sourcepath Log(3, "playlist-load",
olog(1, "playlist-load", "load playlist from %s, got %s files" % (state.source, len(state.playlist)))
"load playlist from %s, got %s files" % (sourcepath, len(files)))
def main(): def main():
olog(1, "init", "initialising trashtalker") Log(1, "init", "initialising trashtalker")
global mainloop global state
global files state.running=True
global sipuri
global sourcepath
mainloop=True
signal(SIGHUP, sighandle) signal(SIGHUP, sighandle)
signal(SIGINT, sighandle) signal(SIGINT, sighandle)
signal(SIGTERM, sighandle) signal(SIGTERM, sighandle)
assert sourcepath.startswith('/'), "Environment variable TT_MEDIA_PATH must be an absolute path!" assert state.source.startswith('/'), "Environment variable TT_MEDIA_PATH must be an absolute path!"
try: try:
loadplaylist() MediaLoadPlaylist()
except: except:
elog(1, "playlist-load", "exception encountered while loading playlist from path %s" % sourcepath) Log(2, "playlist-load", "exception encountered while loading playlist from path %s" % state.source, error=True)
raise Exception("Unable to load playlist") raise Exception("Unable to load playlist")
try: try:
PjInit() PJInit()
except: except:
elog(1, "pj-init", "Unable to initialise pjsip library") Log(2, "pj-init", "Unable to initialise pjsip library; please check media path and SIP listening port are correct", error=True)
raise Exception("Unable to initialise pjsip library") raise Exception("Unable to initialise pjsip library; please check media path and SIP listening port are correct")
Log(1, "init-complete", "trashtalker listening on uri %s and serving %s media items from %s" % (state.uri, len(state.playlist), state.source))
try: try:
PjMediaInit() WaitLoop()
except:
elog(1, "pj-media-init", "Unable to initialise pjsip media or transport")
raise Exception("Unable to initialise pjsip media or transport")
olog(1, "init-complete", "trashtalker listening on uri %s and serving media from %s" % (sipuri, sourcepath))
try:
TrashTalkerInit()
except pj.Error as e: except pj.Error as e:
elog(1, "pjsip-error", "trashtalker encountered pjsip exception %s" % str(e)) Log(2, "pjsip-error", "trashtalker encountered pjsip exception %s" % str(e), error=True)
mainloop=False state.running=False
pass pass
except KeyboardInterrupt: except KeyboardInterrupt:
mainloop=False state.running=False
pass pass
olog(1, "deinit", "main loop exited, shutting down") Log(1, "deinit", "main loop exited, shutting down")
PjDeinit() PjDeinit()
olog(1, "deinit-complete", "trashtalker has shut down") Log(1, "deinit-complete", "trashtalker has shut down")
lib=None
acct=None
transport=None
sipuri=None
mainloop=False
files=()
if __name__ == "__main__": if __name__ == "__main__":
main() main()