Slight refactor
This commit is contained in:
parent
29d1acddc1
commit
bad666456c
|
@ -114,3 +114,5 @@ dmypy.json
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
|
240
trashtalker.py
240
trashtalker.py
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue