trashtalker/trashtalker.py

220 lines
8.1 KiB
Python
Raw Normal View History

2019-04-29 14:19:19 +01:00
#!/usr/bin/python2.7
2019-04-29 13:45:28 +01:00
import sys
import pjsua as pj
from time import sleep
2019-05-01 10:53:06 +01:00
from os import listdir, getenv
2019-05-01 16:45:57 +01:00
from signal import signal, SIGHUP, SIGINT, SIGTERM
2019-04-29 13:45:28 +01:00
from random import shuffle
## NOTE:
2019-04-29 15:07:46 +01:00
## This script is designed to run either on the same machine or firewalled/segregated network segment
## as the telephony appliance(s) that will use it. While there should be no security risk to doing so,
## you should not have the SIP endpoint exposed by this application reachable on the public internet.
## This script should be configured to run automatically, and your telephony appliance should be configured
## to treat it as a no-authentication or "IP-based authentication" SIP trunk. Any number or name will
## be recognised and answered automatically by this script.
2019-04-29 13:45:28 +01:00
##
2019-04-29 15:07:46 +01:00
## This script uses the PJSUA library which is officially deprecated
## The reason for this is that I couldn't get this to work with equivalent code for PJSUA2.
## At time of publishing, the only library version of pjsua available in the repos for Debian 9
## is the deprecated PJSUA (python-pjsua)
## Please also be aware that, by default, playlist length is limited to 64 items. I can find no reason
## for this limitation, and it is specific to the python bindings for the PJSUA library.
## If you'd like to have a playlist longer than 64 items, you will need to recompile python-pjsua
## with the appropriate adjustment to _pjsua.c line 2515
##
## If you can get this working using PJSUA2, a pull request would be greatly appreciated.
2019-04-29 13:45:28 +01:00
2019-05-03 19:43:39 +01:00
state=None
2019-04-29 13:45:28 +01:00
# Application scaffolding
# logger functions
2019-05-02 21:33:38 +01:00
def PJLog(level, line, length):
Log(level+1, "pjsip", line)
def Log(level, source, line, error=False):
pfx='*'
if error:
pfx='!'
print("%s %s: %s" % (pfx*level, source, line))
2019-04-29 13:45:28 +01:00
sys.stdout.flush()
2019-05-02 21:33:38 +01:00
#Generic signal handler
2019-04-29 13:45:28 +01:00
def sighandle(_signo, _stack_frame):
2019-05-02 21:33:38 +01:00
global state
Log(1, "sighandler", "caught signal %s" % _signo)
2019-05-01 16:45:57 +01:00
if _signo == 1:
#SIGHUP
2019-05-02 21:33:38 +01:00
Log(1, "sighandler", "SIGHUP invoked playlist reload")
MediaLoadPlaylist()
2019-05-01 16:45:57 +01:00
elif _signo == 2:
#SIGINT
2019-05-02 21:33:38 +01:00
Log(1, "sighandler", "SIGINT invoked current call flush")
state.lib.hangup_all()
2019-05-01 16:45:57 +01:00
elif _signo == 15:
#SIGTERM
2019-05-02 21:33:38 +01:00
Log(1, "sighandler", "SIGTERM invoked app shutdown")
state.running=False
2019-04-29 13:45:28 +01:00
pass
2019-05-03 19:42:00 +01:00
# Utility classes, used basically as enums or generics
class State(object):
running=False
class PJStates:
init=0
deinit=1
2019-04-29 13:45:28 +01:00
class SIPStates:
ringing=180
answer=200
2019-05-03 19:42:00 +01:00
state=State()
# Classes
2019-04-29 14:47:59 +01:00
# Account Callback class
2019-04-29 13:45:28 +01:00
class AccountCb(pj.AccountCallback):
def __init__(self, account=None):
pj.AccountCallback.__init__(self, account)
2019-04-29 14:47:59 +01:00
2019-04-29 13:45:28 +01:00
def on_incoming_call(self, call):
2019-05-02 21:33:38 +01:00
Log(2, "event-call-in", "caller %s dialled in" % call.info().remote_uri)
2019-04-29 13:45:28 +01:00
call.set_callback(CallCb(call))
2019-05-02 21:33:38 +01:00
#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
2019-04-29 13:45:28 +01:00
call.answer(SIPStates.ringing)
2019-04-29 14:47:59 +01:00
# Call Callback class
2019-04-29 13:45:28 +01:00
class CallCb(pj.CallCallback):
def __init__(self, call=None):
pj.CallCallback.__init__(self, call)
def on_state(self):
2019-05-02 21:33:38 +01:00
global state
Log(2, "event-state-change", "SIP/2.0 %s (%s), call %s in call with party %s" %
2019-04-29 13:45:28 +01:00
(self.call.info().last_code, self.call.info().last_reason,
self.call.info().state_text, self.call.info().remote_uri))
2019-05-02 21:33:38 +01:00
#EARLY media state allows us to init the playlist while the call establishes
2019-04-29 14:18:28 +01:00
if self.call.info().state == pj.CallState.EARLY:
2019-05-02 21:33:38 +01:00
self.playlist=state.playlist
2019-04-29 13:45:28 +01:00
shuffle(self.playlist)
2019-05-02 21:33:38 +01:00
self.instmedia=state.lib.create_playlist(
2019-04-29 13:45:28 +01:00
loop=True, filelist=self.playlist, label="trashtalklist")
2019-05-02 21:33:38 +01:00
self.slotmedia=state.lib.playlist_get_slot(self.instmedia)
Log(3, "event-call-state-early", "initialised new trashtalk playlist instance")
2019-04-29 15:07:46 +01:00
#answer the call once playlist is prepared
2019-04-29 14:56:26 +01:00
self.call.answer(SIPStates.answer)
2019-05-02 21:33:38 +01:00
#CONFIRMED state indicates the call is connected
2019-04-29 14:18:28 +01:00
elif self.call.info().state == pj.CallState.CONFIRMED:
2019-05-02 21:33:38 +01:00
Log(3, "event-call-state-confirmed", "answered call")
self.slotcall=self.call.info().conf_slot
state.lib.conf_connect(self.slotmedia, self.slotcall)
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)
2019-04-29 13:45:28 +01:00
elif self.call.info().state == pj.CallState.DISCONNECTED:
2019-05-02 21:33:38 +01:00
Log(3, "event-call-state-disconnected", "call disconnected")
state.lib.conf_disconnect(self.slotmedia, self.slotcall)
state.lib.playlist_destroy(self.instmedia)
Log(3, "event-call-conf-left", "removed trashtalk instance from call and destroyed it")
2019-04-29 13:45:28 +01:00
2019-05-02 21:33:38 +01:00
#I'm not sure what this is for, as all media handling is actually done within the SIP events above
2019-04-29 13:45:28 +01:00
def on_media_state(self):
if self.call.info().media_state == pj.MediaState.ACTIVE:
2019-05-02 21:33:38 +01:00
Log(4, "event-media-state-change", "Media State transitioned to ACTIVE")
2019-04-29 13:45:28 +01:00
else:
2019-05-02 21:33:38 +01:00
Log(4, "event-media-state-change", "Media State transitioned to INACTIVE")
2019-04-29 13:45:28 +01:00
# Main logic functions
2019-05-03 19:42:00 +01:00
def PJControl(action):
2019-05-02 21:33:38 +01:00
global state
2019-05-03 19:42:00 +01:00
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)
2019-05-02 21:33:38 +01:00
)
2019-05-03 19:42:00 +01:00
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
2019-05-02 21:33:38 +01:00
def WaitLoop():
global state
while state.running:
2019-04-29 13:45:28 +01:00
sleep(0.2)
2019-05-02 21:33:38 +01:00
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)))
2019-05-01 12:42:03 +01:00
2019-04-29 13:45:28 +01:00
def main():
2019-05-02 21:33:38 +01:00
global state
2019-05-03 19:42:00 +01:00
Log(1, "init", "initialising trashtalker")
state.LOG_LEVEL=int(getenv('TT_LOG_LEVEL', 0))
#TT_MEDIA_SOURCE and TT_LISTEN_PORT can be configured via env. variables
state.source=getenv('TT_MEDIA_SOURCE', '/opt/media/')
state.port=int(getenv('TT_LISTEN_PORT', 55060))
2019-05-02 21:33:38 +01:00
state.running=True
2019-05-01 16:45:57 +01:00
signal(SIGHUP, sighandle)
signal(SIGINT, sighandle)
2019-04-29 13:45:28 +01:00
signal(SIGTERM, sighandle)
2019-05-02 21:33:38 +01:00
assert state.source.startswith('/'), "Environment variable TT_MEDIA_PATH must be an absolute path!"
2019-04-29 13:45:28 +01:00
try:
2019-05-02 21:33:38 +01:00
MediaLoadPlaylist()
2019-04-29 13:45:28 +01:00
except:
2019-05-02 21:33:38 +01:00
Log(2, "playlist-load", "exception encountered while loading playlist from path %s" % state.source, error=True)
2019-04-29 13:45:28 +01:00
raise Exception("Unable to load playlist")
try:
2019-05-03 19:42:00 +01:00
PJControl(PJStates.init)
2019-04-29 13:45:28 +01:00
except:
2019-05-02 21:33:38 +01:00
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")
Log(1, "init-complete", "trashtalker listening on uri %s and serving %s media items from %s" % (state.uri, len(state.playlist), state.source))
2019-04-29 13:45:28 +01:00
try:
2019-05-02 21:33:38 +01:00
WaitLoop()
2019-04-29 13:45:28 +01:00
except pj.Error as e:
2019-05-02 21:33:38 +01:00
Log(2, "pjsip-error", "trashtalker encountered pjsip exception %s" % str(e), error=True)
state.running=False
2019-04-29 13:45:28 +01:00
pass
2019-04-29 14:33:02 +01:00
except KeyboardInterrupt:
2019-05-02 21:33:38 +01:00
state.running=False
2019-04-29 14:33:02 +01:00
pass
2019-05-02 21:33:38 +01:00
Log(1, "deinit", "main loop exited, shutting down")
2019-05-03 19:42:00 +01:00
PJControl(PJStates.deinit)
2019-05-02 21:33:38 +01:00
Log(1, "deinit-complete", "trashtalker has shut down")
2019-04-29 13:45:28 +01:00
if __name__ == "__main__":
2019-05-02 21:33:38 +01:00
main()