more refactoring, trashtalker is now more OOP

This commit is contained in:
Maff 2019-05-06 19:45:52 +01:00
parent afb2ac83b6
commit c86ec2c23c
1 changed files with 107 additions and 111 deletions

View File

@ -35,42 +35,92 @@ from random import shuffle
class State: class State:
lib=None lib=None
running=False running=False
def start(self): sip_ringing=180
sip_answer=200
def Log(level, source, line, error=False):
pfx='*'
if error:
pfx='!'
print("%s %s: %s" % (pfx*level, source, line))
sys.stdout.flush()
def preinit(self):
self.Log(1, "preinit", "initialising from environment")
self.LOG_LEVEL=int(getenv('TT_LOG_LEVEL', 0))
self.port=int(getenv('TT_LISTEN_PORT', 55060))
self.source=getenv('TT_MEDIA_SOURCE', '/opt/media/')
assert self.source.startswith('/'), "TT_MEDIA_SOURCE must specify an absolute path!"
def init(self):
self.lib=pj.Lib()
self.cfg_ua=pj.UAConfig()
self.cfg_md=pj.MediaConfig()
self.cfg_ua.max_calls, self.cfg_ua.user_agent = 32, "TrashTalker/1.0"
self.cfg_md.no_vad, self.cfg_md.enable_ice = True, False
self.cfg_lg=pj.LogConfig(
level=self.LOG_LEVEL,
callback=PJLog)
self.lib.init(
ua_cfg=cfg_ua,
media_cfg=cfg_md,
log_cfg=cfg_lg)
self.lib.set_null_snd_dev()
self.lib.start(
with_thread=True)
self.transport=self.lib.create_transport(
pj.TransportType.UDP,
pj.TransportConfig(self.port))
self.account=self.lib.create_account_for_transport(
self.transport,
cb=AccountCb())
self.uri="sip:%s:%s" % (self.transport.info().host, self.transport.info().port)
def media_init(self):
self.Log(3, "playlist-load", "loading playlist files from media path %s" % self.source)
if not self.source.endswith('/'):
self.Log(4, "playlist-load", "appending trailing / to TT_MEDIA_SOURCE")
self.source="%s/" % self.source
self.playlist=listdir(self.source)
self.playlist[:]=[self.source+file for file in self.playlist]
assert (len(self.playlist) > 1), "playlist path %s must contain more than one audio file" % self.source
self.Log(3, "playlist-load", "loaded %s media items from path %s" % (len(self.playlist), self.source))
def deinit(self):
self.lib.hangup_all()
self.lib.handle_events(timeout=250)
try:
self.account.delete()
self.lib.destroy()
self.lib=self.account=self.transport=None
except AttributeError:
self.Log(1, "deinit", "Got an AttributeError exception during shutdown.", error=True)
pass
except pj.Error as e:
self.Log(1, "deinit", "Got a PJError exception during shutdown: %s" % str(e), error=True)
pass
def run(self):
self.running=True self.running=True
while self.running:
sleep(0.2)
def stop(self): def stop(self):
self.running=False self.running=False
class PJStates:
init=0
deinit=1
class SIPStates:
ringing=180
answer=200
#Utility and state definitions #Utility and state definitions
state=State() state=State()
# Logging # Logging
def PJLog(level, line, length): def PJLog(level, line, length):
Log(level+1, "pjsip", line) global state
def Log(level, source, line, error=False): state.Log(level+1, "pjsip", line)
pfx='*'
if error:
pfx='!'
print("%s %s: %s" % (pfx*level, source, line))
sys.stdout.flush()
# Signal handling # Signal handling
def sighandle(_signo, _stack_frame): def sighandle(_signo, _stack_frame):
global state global state
Log(1, "sighandler", "caught signal %s" % _signo) state.Log(1, "sighandler", "caught signal %s" % _signo)
if _signo == 1: if _signo == 1:
#SIGHUP #SIGHUP
Log(1, "sighandler", "SIGHUP invoked playlist reload") state.Log(1, "sighandler", "SIGHUP invoked playlist reload")
MediaLoadPlaylist() state.media_init()
elif _signo == 2: elif _signo == 2:
#SIGINT #SIGINT
Log(1, "sighandler", "SIGINT invoked current call flush") state.Log(1, "sighandler", "SIGINT invoked current call flush")
state.lib.hangup_all() state.lib.hangup_all()
elif _signo == 15: elif _signo == 15:
#SIGTERM #SIGTERM
Log(1, "sighandler", "SIGTERM invoked app shutdown") state.Log(1, "sighandler", "SIGTERM invoked app shutdown")
state.stop() state.stop()
pass pass
@ -81,11 +131,12 @@ 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):
Log(2, "event-call-in", "caller %s dialled in" % call.info().remote_uri) global state
state.Log(2, "event-call-in", "caller %s dialled in" % call.info().remote_uri)
call.set_callback(CallCb(call)) call.set_callback(CallCb(call))
#Answer call with SIP/2.0 180 RINGING #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 #This kicks in the EARLY media state, allowing us to initialise the playlist before the call connects
call.answer(SIPStates.ringing) call.answer(state.sip_ringing)
# Call Callback class # Call Callback class
class CallCb(pj.CallCallback): class CallCb(pj.CallCallback):
def __init__(self, call=None): def __init__(self, call=None):
@ -98,50 +149,50 @@ class CallCb(pj.CallCallback):
self.instmedia=state.lib.create_playlist( self.instmedia=state.lib.create_playlist(
loop=True, filelist=self.playlist, label="trashtalklist") loop=True, filelist=self.playlist, label="trashtalklist")
self.slotmedia=state.lib.playlist_get_slot(self.instmedia) self.slotmedia=state.lib.playlist_get_slot(self.instmedia)
Log(4, "call-media-create", "created playlist for current call") state.Log(4, "call-media-create", "created playlist for current call")
def connect_media(self): def connect_media(self):
global state global state
self.slotcall=self.call.info().conf_slot self.slotcall=self.call.info().conf_slot
state.lib.conf_connect(self.slotmedia, self.slotcall) state.lib.conf_connect(self.slotmedia, self.slotcall)
Log(4, "call-media-connect", "connected playlist media endpoint to call") state.Log(4, "call-media-connect", "connected playlist media endpoint to call")
def disconnect_media(self): def disconnect_media(self):
global state global state
state.lib.conf_disconnect(self.slotmedia, self.slotcall) state.lib.conf_disconnect(self.slotmedia, self.slotcall)
Log(4, "call-media-disconnect", "disconnected playlist media endpoint from call") state.Log(4, "call-media-disconnect", "disconnected playlist media endpoint from call")
def destroy_media(self): def destroy_media(self):
global state global state
state.lib.playlist_destroy(self.instmedia) state.lib.playlist_destroy(self.instmedia)
self.instmedia=None self.instmedia=None
self.slotmedia=None self.slotmedia=None
self.playlist=None self.playlist=None
Log(4, "call-media-destroy", "destroyed playlist endpoint and object") state.Log(4, "call-media-destroy", "destroyed playlist endpoint and object")
def on_state(self): def on_state(self):
global state global state
Log(2, "event-state-change", "SIP/2.0 %s (%s), call %s in call with party %s" % 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))
#EARLY media state allows us to init the playlist while the call establishes #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:
self.create_media() self.create_media()
Log(3, "event-call-state-early", "initialised new trashtalk playlist instance") state.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(state.sip_answer)
#CONFIRMED state indicates the call is connected #CONFIRMED state indicates the call is connected
elif self.call.info().state == pj.CallState.CONFIRMED: elif self.call.info().state == pj.CallState.CONFIRMED:
Log(3, "event-call-state-confirmed", "answered call") state.Log(3, "event-call-state-confirmed", "answered call")
self.connect_media() self.connect_media()
Log(3, "event-call-conf-joined", "joined trashtalk to call") state.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) #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:
Log(3, "event-call-state-disconnected", "call disconnected") state.Log(3, "event-call-state-disconnected", "call disconnected")
self.disconnect_media() self.disconnect_media()
self.destroy_media() self.destroy_media()
Log(3, "event-call-conf-left", "removed trashtalk instance from call and destroyed it") state.Log(3, "event-call-conf-left", "removed trashtalk instance from call and destroyed it")
def on_dtmf_digit(self, digit): def on_dtmf_digit(self, digit):
global state global state
Log(3, "dtmf-digit", "received DTMF signal: %s" % digit) state.Log(3, "dtmf-digit", "received DTMF signal: %s" % digit)
if digit == '*': if digit == '*':
self.disconnect_media() self.disconnect_media()
self.destroy_media() self.destroy_media()
@ -150,106 +201,51 @@ class CallCb(pj.CallCallback):
#I'm not sure what this is for, as all media handling is actually done within the SIP events above #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):
global state
if self.call.info().media_state == pj.MediaState.ACTIVE: if self.call.info().media_state == pj.MediaState.ACTIVE:
Log(4, "event-media-state-change", "Media State transitioned to ACTIVE") state.Log(4, "event-media-state-change", "Media State transitioned to ACTIVE")
else: else:
Log(4, "event-media-state-change", "Media State transitioned to INACTIVE") state.Log(4, "event-media-state-change", "Media State transitioned to INACTIVE")
# Main logic functions # Main logic functions
def PJControl(action):
global state
if action == PJStates.init:
state.lib=pj.Lib()
state.cfg_ua=pj.UAConfig()
state.cfg_md=pj.MediaConfig()
state.cfg_ua.max_calls, state.cfg_ua.user_agent = 32, "TrashTalker/1.0"
state.cfg_md.no_vad, state.cfg_md.enable_ice = True, False
state.lib.init(
ua_cfg=state.cfg_ua,
media_cfg=state.cfg_md,
log_cfg=pj.LogConfig(
level=state.LOG_LEVEL,
callback=PJLog
)
)
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)
elif action == PJStates.deinit:
state.lib.hangup_all()
# allow time for cleanup before destroying objects
state.lib.handle_events(timeout=250)
try:
state.account.delete()
state.lib.destroy()
state.lib=state.account=state.transport=None
except AttributeError:
Log(1, "deinit", "AttributeError when clearing down pjsip, this is likely fine", error=True)
pass
except pj.Error as e:
Log(1, "deinit", "pjsip error when clearing down: %s" % str(e), error=True)
pass
def WaitLoop():
global state
while state.running:
sleep(0.2)
def MediaLoadPlaylist():
Log(3, "playlist-load", "loading playlist files")
global state
if not state.source.endswith('/'):
Log(4, "playlist-load", "appending trailing / to TT_MEDIA_SOURCE")
state.source="%s/" % state.source
state.playlist=listdir(state.source)
state.playlist[:]=[state.source+file for file in state.playlist]
assert (len(state.playlist) > 1), "playlist path %s must contain more than one audio file" % state.source
Log(3, "playlist-load",
"load playlist from %s, got %s files" % (state.source, len(state.playlist)))
def main(): def main():
global state global state
Log(1, "init", "initialising trashtalker") #Try to pre-init
state.LOG_LEVEL=int(getenv('TT_LOG_LEVEL', 0)) try:
#TT_MEDIA_SOURCE and TT_LISTEN_PORT can be configured via env. variables state.preinit()
state.source=getenv('TT_MEDIA_SOURCE', '/opt/media/') except AssertionError as e:
state.port=int(getenv('TT_LISTEN_PORT', 55060)) state.Log(1, "preinit", "AssertionError while pre-initialising: %s" % str(e))
state.start() raise Exception("Unable to start up TrashTalker. Check all configuration parameters are correct, and review logs.")
#Attach signal handlers
signal(SIGHUP, sighandle) signal(SIGHUP, sighandle)
signal(SIGINT, sighandle) signal(SIGINT, sighandle)
signal(SIGTERM, sighandle) signal(SIGTERM, sighandle)
assert state.source.startswith('/'), "Environment variable TT_MEDIA_PATH must be an absolute path!" #Try to initialise media
try: try:
MediaLoadPlaylist() state.media_init()
except: except:
Log(2, "playlist-load", "exception encountered while loading playlist from path %s" % state.source, error=True) state.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 to initialise main process; only fault here should be if the configured listening port is unavailable to us
try: try:
PJControl(PJStates.init) state.init()
except: except:
Log(2, "pj-init", "Unable to initialise pjsip library; please check media path and SIP listening port are correct", error=True) state.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; please check media path and SIP listening port are correct") 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)) state.Log(1, "init-complete", "trashtalker listening on uri %s and serving %s media items from %s" % (state.uri, len(state.playlist), state.source))
#Enter main loop
try: try:
WaitLoop() state.run()
except pj.Error as e: except pj.Error as e:
Log(2, "pjsip-error", "trashtalker encountered pjsip exception %s" % str(e), error=True) state.Log(2, "pjsip-error", "trashtalker encountered pjsip exception %s" % str(e), error=True)
state.stop() state.stop()
pass pass
except KeyboardInterrupt: except KeyboardInterrupt:
state.stop() state.stop()
pass pass
Log(1, "deinit", "main loop exited, shutting down") state.Log(1, "deinit", "main loop exited, shutting down")
PJControl(PJStates.deinit) state.deinit()
Log(1, "deinit-complete", "trashtalker has shut down") state.Log(1, "deinit", "trashtalker has shut down")
if __name__ == "__main__": if __name__ == "__main__":
main() main()