mirror of https://github.com/sipwise/rtpengine.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
641 lines
14 KiB
641 lines
14 KiB
import asyncio
|
|
import aiohttp
|
|
import aiofiles
|
|
import base64
|
|
from guizero import *
|
|
import pysip_lite
|
|
import typing
|
|
import argparse
|
|
import string
|
|
import random
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Register as a SIP UA and interactively manipulate calls.",
|
|
epilog="Example: --user bench2user000005 --domain guest02-snail.lab.sipwise.com --pw testuser --rtpe http://localhost:9911/ng-plain",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--user",
|
|
required=True,
|
|
help="SIP user name to register UA",
|
|
metavar="USER",
|
|
)
|
|
parser.add_argument(
|
|
"--domain",
|
|
required=True,
|
|
help="SIP user name to register UA",
|
|
metavar="USER",
|
|
)
|
|
parser.add_argument(
|
|
"--pw",
|
|
required=True,
|
|
help="Password for SIP registration",
|
|
metavar="PASSWORD",
|
|
)
|
|
parser.add_argument(
|
|
"--rtpe", required=True, help="HTTP URI for rtpengine", metavar="HTTP-URI"
|
|
)
|
|
parser.add_argument(
|
|
"--addr",
|
|
required=False,
|
|
help="Local SIP IP address to bind to (default 0.0.0.0)",
|
|
default="0.0.0.0",
|
|
metavar="IP",
|
|
)
|
|
parser.add_argument(
|
|
"--port",
|
|
required=False,
|
|
help="Local SIP port to bind to",
|
|
default=15062,
|
|
metavar="NUM",
|
|
type=int,
|
|
)
|
|
parser.add_argument(
|
|
"--proto",
|
|
required=False,
|
|
help="SIP transport protocol (default UDP)",
|
|
default="UDP",
|
|
choices=["UDP", "TCP"],
|
|
metavar="PROTO",
|
|
)
|
|
parser.add_argument(
|
|
"--debug",
|
|
help="Enable debug output",
|
|
action="store_true",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
prov = pysip_lite.Provider(args.addr, args.port, args.proto)
|
|
ua = prov.user_agent(f"sip:{args.user}@{args.domain}", args.pw)
|
|
|
|
rtpe = args.rtpe
|
|
|
|
|
|
class call_entry:
|
|
_value: str = None
|
|
_idx: int = None
|
|
state: str = None
|
|
call: pysip_lite.Call = None
|
|
|
|
def __init__(self, list_val: str, state: str):
|
|
global call_list, call_cnnct
|
|
|
|
self._idx = len(call_list.items)
|
|
self._value = f"{self._idx}: {list_val}"
|
|
call_list.append(self._value)
|
|
call_cnnct.append(self._value)
|
|
self.state = state
|
|
|
|
def update(self, list_val: str, state: str) -> None:
|
|
global call_list, call_cnnct
|
|
|
|
call_list.remove(self._value)
|
|
call_cnnct.remove(self._value)
|
|
self._value = f"{self._idx}: {list_val}"
|
|
call_list.insert(self._idx, self._value)
|
|
call_cnnct.insert(self._idx, self._value)
|
|
self.state = state
|
|
|
|
|
|
calls: typing.List[call_entry] = []
|
|
|
|
suffix = "_" + "".join(
|
|
random.choices(string.ascii_uppercase + string.digits, k=5)
|
|
)
|
|
|
|
running = True
|
|
|
|
|
|
def do_shutdown():
|
|
global running
|
|
running = False
|
|
|
|
|
|
app = App(title="SIP/rtpengine demo", height=800, width=1000)
|
|
|
|
app.when_closed = do_shutdown
|
|
|
|
|
|
def resolve(f: asyncio.Future, val: typing.Any) -> None:
|
|
f.set_result(val)
|
|
|
|
|
|
async def msgbox(text) -> None:
|
|
w = Window(app, height=200, width=400)
|
|
text = Text(w, text=text)
|
|
f = asyncio.get_running_loop().create_future()
|
|
ok_btn = PushButton(w, command=resolve, args=[f, True], text="OK")
|
|
await f
|
|
w.destroy()
|
|
|
|
|
|
async def rtpe_req(req: dict) -> dict:
|
|
print("Request to rtpengine:")
|
|
print(req)
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(rtpe, json=req) as post:
|
|
resp: dict = await post.json()
|
|
except Exception as e:
|
|
await msgbox("Exception during JSON request to rtpengine: " + str(e))
|
|
return None
|
|
|
|
print("Response from rtpengine:")
|
|
print(resp)
|
|
|
|
return resp
|
|
|
|
|
|
async def do_call(uri: str) -> None:
|
|
global call_to_txt, codecs_txt
|
|
|
|
if not uri:
|
|
await msgbox("missing destination URI")
|
|
return
|
|
|
|
if not uri.startswith("sip:"):
|
|
uri = "sip:" + uri
|
|
|
|
if not "@" in uri:
|
|
uri = uri + f"@{args.domain}"
|
|
|
|
req = {
|
|
"command": "create",
|
|
}
|
|
|
|
c: typing.List[str] = codecs_txt.value.split()
|
|
|
|
if c:
|
|
req["codec"] = {"offer": c}
|
|
|
|
resp = await rtpe_req(req)
|
|
if not resp:
|
|
return
|
|
|
|
call = ua.create(uri, resp["call-id"], resp["from-tag"], resp["sdp"])
|
|
c = call_entry(f"pending call (to {uri})", "n")
|
|
c.call = call
|
|
calls.append(c)
|
|
|
|
code: int = await call.wait(180)
|
|
|
|
if code == 180:
|
|
c.update(f"ringing (to {uri})", "or")
|
|
res = await call.wait()
|
|
|
|
if res == 200:
|
|
c.update(f"running (to {uri})", "r")
|
|
|
|
req = {
|
|
"command": "create answer",
|
|
"call-id": call.call_id(),
|
|
"from-tag": call.from_tag(),
|
|
"sdp": call.sdp(),
|
|
"audio player": "force",
|
|
}
|
|
|
|
resp = await rtpe_req(req)
|
|
if not resp:
|
|
call.stop()
|
|
|
|
await call.finished()
|
|
else:
|
|
print("rejected")
|
|
|
|
c.update("terminated", "t")
|
|
|
|
req = {
|
|
"command": "delete",
|
|
"call-id": call.call_id(),
|
|
}
|
|
|
|
await rtpe_req(req)
|
|
|
|
c.call = None
|
|
|
|
|
|
def do_call_btn() -> None:
|
|
asyncio.create_task(do_call(call_to_txt.value))
|
|
|
|
|
|
call_to_box = Box(app, layout="grid")
|
|
call_to_btn = PushButton(
|
|
call_to_box,
|
|
command=do_call_btn,
|
|
text="Call to:",
|
|
grid=[0, 0],
|
|
enabled=False,
|
|
)
|
|
call_to_txt = TextBox(call_to_box, width=80, grid=[1, 0])
|
|
|
|
Text(call_to_box, text="Codecs:", grid=[0, 1])
|
|
codecs_txt = TextBox(
|
|
call_to_box, width=80, grid=[1, 1], text="opus AMR-WB G722 AMR PCMA PCMU"
|
|
)
|
|
|
|
|
|
call_list = ListBox(app, width="fill", multiselect=True)
|
|
|
|
|
|
def get_call(v: str) -> call_entry:
|
|
if v is None:
|
|
return None
|
|
va = v.split(" ")
|
|
i = va[0]
|
|
ia = i.split(":")
|
|
n = int(ia[0])
|
|
return calls[n]
|
|
|
|
|
|
def get_calls(box: ListBox) -> typing.List[call_entry]:
|
|
if not box.value:
|
|
return []
|
|
return [get_call(i) for i in box.value]
|
|
|
|
|
|
async def rtpe_answer(c: call_entry) -> None:
|
|
if c.state != "ir":
|
|
await msgbox("call is not in ringing state")
|
|
return
|
|
|
|
call = c.call
|
|
|
|
f = call.from_addr()
|
|
c.update(f"answering (from {f})", "ira")
|
|
|
|
sdp = call.sdp()
|
|
|
|
req = {
|
|
"command": "publish",
|
|
"call-id": call.call_id(),
|
|
"from-tag": call.from_tag(),
|
|
"sdp": sdp,
|
|
"audio player": "force",
|
|
"flags": ["bidirectional"],
|
|
}
|
|
|
|
resp = await rtpe_req(req)
|
|
if not resp:
|
|
call.reject(500)
|
|
return
|
|
|
|
try:
|
|
call.answer(resp["sdp"])
|
|
except:
|
|
await msgbox("error")
|
|
return
|
|
|
|
c.update(f"running (from {f})", "r")
|
|
|
|
|
|
def do_answer() -> None:
|
|
cs = get_calls(call_list)
|
|
if not cs:
|
|
asyncio.create_task(msgbox("no call selected"))
|
|
return
|
|
|
|
for c in cs:
|
|
asyncio.create_task(rtpe_answer(c))
|
|
|
|
|
|
def do_close() -> None:
|
|
cs = get_calls(call_list)
|
|
if not cs:
|
|
asyncio.create_task(msgbox("no call selected"))
|
|
return
|
|
|
|
for c in cs:
|
|
if c.state == "r":
|
|
c.call.stop()
|
|
elif c.state == "ir":
|
|
c.call.reject(480)
|
|
else:
|
|
asyncio.create_task(msgbox(f"call '{c._value}' is in wrong state"))
|
|
|
|
|
|
ar_buttons = Box(app, layout="grid")
|
|
answer_btn = PushButton(
|
|
ar_buttons, command=do_answer, text="Answer", grid=[0, 0]
|
|
)
|
|
reject_btn = PushButton(
|
|
ar_buttons, command=do_close, text="Close", grid=[1, 0]
|
|
)
|
|
|
|
|
|
async def do_play(fn: str) -> None:
|
|
cs = get_calls(call_list)
|
|
if not cs:
|
|
await msgbox("no call selected")
|
|
return
|
|
|
|
try:
|
|
async with aiofiles.open(fn, "rb") as f:
|
|
b: bytes = await f.read()
|
|
except:
|
|
await msgbox("error reading file")
|
|
return
|
|
|
|
for c in cs:
|
|
call = c.call
|
|
|
|
if call is None or c.state != "r":
|
|
await msgbox("call is not running")
|
|
return
|
|
|
|
req = {
|
|
"command": "play media",
|
|
"call-id": call.call_id(),
|
|
"from-tag": call.from_tag(),
|
|
"blob64": base64.encodebytes(b).decode(),
|
|
}
|
|
|
|
await rtpe_req(req)
|
|
|
|
|
|
def do_play_btn() -> None:
|
|
global play_txt
|
|
asyncio.create_task(do_play(play_txt.value))
|
|
|
|
|
|
play_grid = Box(app, layout="grid")
|
|
play_btn = PushButton(
|
|
play_grid, command=do_play_btn, text="Play:", grid=[0, 0]
|
|
)
|
|
play_txt = TextBox(
|
|
play_grid,
|
|
text="263655_2064400-lq.mp3",
|
|
grid=[1, 0],
|
|
width=80,
|
|
)
|
|
|
|
|
|
async def do_transfer(c1: call_entry, c2: call_entry) -> None:
|
|
global cnnct_info, transfer_chk, bidirect_chk
|
|
|
|
flags = []
|
|
|
|
if not transfer_chk.value:
|
|
flags.append("directional")
|
|
if bidirect_chk.value:
|
|
flags.append("bidirectional")
|
|
|
|
info = "Using 'connect' method"
|
|
info = info + " with flags: " + ", ".join(flags)
|
|
cnnct_info.value = info
|
|
else:
|
|
cnnct_info.value = "Info: using simple 'connect' method"
|
|
|
|
req = {
|
|
"command": "connect",
|
|
"call-id": c1.call.call_id(),
|
|
"from-tag": c1.call.from_tag(),
|
|
"to-call-id": c2.call.call_id(),
|
|
"to-tag": c2.call.from_tag(),
|
|
"flags": flags,
|
|
}
|
|
|
|
await rtpe_req(req)
|
|
|
|
|
|
async def do_mesh(
|
|
from_calls: typing.List[call_entry], to_calls: typing.List[call_entry]
|
|
) -> None:
|
|
global transfer_chk, cnnct_info, bidirect_chk
|
|
|
|
flags = []
|
|
|
|
if transfer_chk.value:
|
|
flags.append("unsubscribe")
|
|
if bidirect_chk.value:
|
|
flags.append("bidirectional")
|
|
|
|
calls = set()
|
|
tags = []
|
|
|
|
for c in from_calls:
|
|
calls.add(c.call.call_id())
|
|
tag = {"from": c.call.from_tag(), "to": []}
|
|
for d in to_calls:
|
|
if c is d:
|
|
continue
|
|
calls.add(d.call.call_id())
|
|
tag["to"].append(d.call.from_tag())
|
|
tags.append(tag)
|
|
|
|
req = {
|
|
"command": "mesh",
|
|
"calls": list(calls),
|
|
"tags": tags,
|
|
"flags": flags,
|
|
}
|
|
|
|
info = "Using 'mesh' method"
|
|
if flags:
|
|
info = info + " with flags: " + ", ".join(flags)
|
|
|
|
cnnct_info.value = info
|
|
|
|
await rtpe_req(req)
|
|
|
|
|
|
async def do_connect() -> None:
|
|
global call_list, call_cnnct, transfer_chk, cnnct_info
|
|
|
|
to_calls = get_calls(call_list)
|
|
if not to_calls:
|
|
await msgbox("no to-call selected")
|
|
return
|
|
|
|
from_calls = get_calls(call_cnnct)
|
|
if not from_calls:
|
|
await msgbox("no from-call selected")
|
|
return
|
|
|
|
if len(to_calls) > 1 or len(from_calls) > 1:
|
|
await do_mesh(from_calls, to_calls)
|
|
return
|
|
|
|
await do_transfer(from_calls[0], to_calls[0])
|
|
|
|
|
|
async def do_disconnect() -> None:
|
|
global call_list, call_cnnct, transfer_chk, bidirect_chk
|
|
|
|
to_calls = get_calls(call_list)
|
|
if not to_calls:
|
|
await msgbox("no to-call selected")
|
|
return
|
|
|
|
from_calls = get_calls(call_cnnct)
|
|
if not from_calls:
|
|
await msgbox("no from-call selected")
|
|
return
|
|
|
|
if len(to_calls) > 1 or len(from_calls) > 1:
|
|
await msgbox("can only disconnect from/to a single call")
|
|
return
|
|
|
|
c1 = from_calls[0]
|
|
c2 = to_calls[0]
|
|
|
|
flags = ["directional"]
|
|
|
|
if bidirect_chk:
|
|
cnnct_info.value = (
|
|
"Info: using 'unsubscribe' method with 'bidirectional' flag"
|
|
)
|
|
flags.append("bidirectional")
|
|
else:
|
|
cnnct_info.value = "Info: using 'unsubscribe' method"
|
|
|
|
await rtpe_req(
|
|
{
|
|
"command": "unsubscribe",
|
|
"call-id": c1.call.call_id(),
|
|
"from-tag": c1.call.from_tag(),
|
|
"to-tag": c2.call.from_tag(),
|
|
"flags": flags,
|
|
}
|
|
)
|
|
|
|
|
|
def do_cnct_btn() -> None:
|
|
asyncio.create_task(do_connect())
|
|
|
|
|
|
def do_dcnt_btn() -> None:
|
|
asyncio.create_task(do_disconnect())
|
|
|
|
|
|
cd_grid = Box(app, layout="grid")
|
|
cnct_btn = PushButton(
|
|
cd_grid, command=do_cnct_btn, text="Connect to:", grid=[0, 0]
|
|
)
|
|
transfer_chk = CheckBox(cd_grid, text="Full transfer", grid=[1, 0])
|
|
dcnt_btn = PushButton(
|
|
cd_grid, command=do_dcnt_btn, text="Disconnect from:", grid=[2, 0]
|
|
)
|
|
bidirect_chk = CheckBox(cd_grid, text="Both ways", grid=[3, 0])
|
|
|
|
call_cnnct = ListBox(app, width="fill", multiselect=True)
|
|
cnnct_info = Text(app, text="Info:", width="fill")
|
|
|
|
|
|
async def do_speak(txt: str) -> None:
|
|
cs = get_calls(call_list)
|
|
if not cs:
|
|
await msgbox("no call selected")
|
|
return
|
|
|
|
try:
|
|
proc = await asyncio.create_subprocess_exec(
|
|
"espeak",
|
|
"--stdout",
|
|
txt,
|
|
stdin=None,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
)
|
|
b, _ = await proc.communicate()
|
|
except Exception as e:
|
|
await msgbox(f"error exexuting espeak: {e}")
|
|
return
|
|
|
|
for c in cs:
|
|
call = c.call
|
|
|
|
req = {
|
|
"command": "play media",
|
|
"call-id": call.call_id(),
|
|
"from-tag": call.from_tag(),
|
|
"blob64": base64.encodebytes(b).decode(),
|
|
}
|
|
|
|
await rtpe_req(req)
|
|
|
|
|
|
def do_speak_btn() -> None:
|
|
global speak_txt
|
|
asyncio.create_task(do_speak(speak_txt.value))
|
|
|
|
|
|
speak_btn = PushButton(
|
|
play_grid, command=do_speak_btn, text="Speak:", grid=[0, 1]
|
|
)
|
|
speak_txt = TextBox(play_grid, grid=[1, 1], width=80)
|
|
|
|
|
|
async def handle_call(call: pysip_lite.Call) -> None:
|
|
c = call_entry("incoming call", "n")
|
|
c.call = call
|
|
calls.append(c)
|
|
|
|
try:
|
|
call.ringing()
|
|
f = call.from_addr()
|
|
c.update(f"ringing (from {f})", "ir")
|
|
|
|
await call.finished()
|
|
except:
|
|
print("some error")
|
|
|
|
c.update("terminated", "t")
|
|
|
|
await rtpe_req(
|
|
{
|
|
"command": "delete",
|
|
"call-id": call.call_id(),
|
|
}
|
|
)
|
|
|
|
c.call = None
|
|
|
|
|
|
async def ua_main() -> None:
|
|
global ua, running
|
|
|
|
ok = await ua.register()
|
|
assert ok
|
|
|
|
call_to_btn.enable()
|
|
|
|
ua.listen()
|
|
|
|
while running:
|
|
call = await ua.receive()
|
|
if call:
|
|
asyncio.create_task(handle_call(call))
|
|
|
|
|
|
def log(s: str) -> None:
|
|
print(s)
|
|
|
|
|
|
async def app_main():
|
|
global running, app
|
|
|
|
logger = pysip_lite.Logger(log, 1 if args.debug else 8)
|
|
|
|
r = eventloop.create_task(ua_main())
|
|
|
|
while running:
|
|
app.update()
|
|
await asyncio.sleep(0.05)
|
|
|
|
call_to_btn.disable()
|
|
|
|
await ua.unregister()
|
|
await r
|
|
await logger
|
|
|
|
|
|
eventloop = asyncio.new_event_loop()
|
|
mt = eventloop.create_task(app_main())
|
|
try:
|
|
eventloop.run_until_complete(mt)
|
|
except KeyboardInterrupt as e:
|
|
running = False
|
|
eventloop.run_until_complete(mt)
|
|
eventloop.close()
|