MT#55283 demo scripts/lib

Change-Id: Ib28ff8e2d7a0bf0518c8a99103277ae488acc8b7
pull/2123/head
Richard Fuchs 2 months ago
parent d0b380272c
commit 7403746ccd

@ -0,0 +1,149 @@
# Generated by YCM Generator at 2026-05-08 07:21:11.987806
# This file is NOT licensed under the GPLv3, which is the license for the rest
# of YouCompleteMe.
#
# Here's the license text for this file:
#
# This is free and unencumbered software released into the public domain.
#
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
#
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
# For more information, please refer to <http://unlicense.org/>
import os
import ycm_core
flags = [
'-x',
'c',
'-I/usr/include/glib-2.0',
'-I/usr/include/sysprof-6',
'-I/usr/lib/x86_64-linux-gnu/glib-2.0/include',
'-Wall',
]
# Set this to the absolute path to the folder (NOT the file!) containing the
# compile_commands.json file to use that instead of 'flags'. See here for
# more details: http://clang.llvm.org/docs/JSONCompilationDatabase.html
#
# You can get CMake to generate this file for you by adding:
# set( CMAKE_EXPORT_COMPILE_COMMANDS 1 )
# to your CMakeLists.txt file.
#
# Most projects will NOT need to set this to anything; you can just change the
# 'flags' list of compilation flags. Notice that YCM itself uses that approach.
compilation_database_folder = ''
if os.path.exists( compilation_database_folder ):
database = ycm_core.CompilationDatabase( compilation_database_folder )
else:
database = None
SOURCE_EXTENSIONS = [ '.C', '.cpp', '.cxx', '.cc', '.c', '.m', '.mm' ]
def DirectoryOfThisScript():
return os.path.dirname( os.path.abspath( __file__ ) )
def MakeRelativePathsInFlagsAbsolute( flags, working_directory ):
if not working_directory:
return list( flags )
new_flags = []
make_next_absolute = False
path_flags = [ '-isystem', '-I', '-iquote', '--sysroot=' ]
for flag in flags:
new_flag = flag
if make_next_absolute:
make_next_absolute = False
if not flag.startswith( '/' ):
new_flag = os.path.join( working_directory, flag )
for path_flag in path_flags:
if flag == path_flag:
make_next_absolute = True
break
if flag.startswith( path_flag ):
path = flag[ len( path_flag ): ]
new_flag = path_flag + os.path.join( working_directory, path )
break
if new_flag:
new_flags.append( new_flag )
return new_flags
def IsHeaderFile( filename ):
extension = os.path.splitext( filename )[ 1 ]
return extension in [ '.H', '.h', '.hxx', '.hpp', '.hh' ]
def GetCompilationInfoForFile( filename ):
# The compilation_commands.json file generated by CMake does not have entries
# for header files. So we do our best by asking the db for flags for a
# corresponding source file, if any. If one exists, the flags for that file
# should be good enough.
if IsHeaderFile( filename ):
basename = os.path.splitext( filename )[ 0 ]
for extension in SOURCE_EXTENSIONS:
replacement_file = basename + extension
if os.path.exists( replacement_file ):
compilation_info = database.GetCompilationInfoForFile(
replacement_file )
if compilation_info.compiler_flags_:
return compilation_info
return None
return database.GetCompilationInfoForFile( filename )
def FlagsForFile( filename, **kwargs ):
if database:
# Bear in mind that compilation_info.compiler_flags_ does NOT return a
# python list, but a "list-like" StringVec object
compilation_info = GetCompilationInfoForFile( filename )
if not compilation_info:
return None
final_flags = MakeRelativePathsInFlagsAbsolute(
compilation_info.compiler_flags_,
compilation_info.compiler_working_dir_ )
else:
relative_to = DirectoryOfThisScript()
final_flags = MakeRelativePathsInFlagsAbsolute( flags, relative_to )
return {
'flags': final_flags,
'do_cache': True
}
def Settings( **kwargs ):
language = kwargs[ 'language' ]
if language == 'cfamily':
return {
'flags': flags
}
return {}

Binary file not shown.

@ -0,0 +1,11 @@
all: pysip_lite/belle-wrap.so
pysip_lite/belle-wrap.so: src/belle-wrap.o
$(LD) -g -shared src/belle-wrap.o -o pysip_lite/belle-wrap.so $(shell pkg-config --libs belle-sip glib-2.0) -lc
src/belle-wrap.o: src/belle-wrap.c
gcc -fPIC -O3 -g -Wall $(shell pkg-config --cflags belle-sip glib-2.0) src/belle-wrap.c -c -o src/belle-wrap.o
.PHONY: clean
clean:
rm -f src/belle-wrap.o pysip_lite/belle-wrap.so

@ -0,0 +1,16 @@
# Python demo scripts
This directory contains a set of simple Python scripts to demonstrate features
of rtpengine, from basic media proxying to more advanced media manipulation and
playback features.
Each script acts as a SIP client or server of some sort, and uses rtpengine to
manipulate media and SDP aspects. The scripts are not meant to be comprehensive
feature-wise, but rather are meant to be playgrounds that can easily be
modified and extended.
## pysip-lite
A light-weight SIP module for Python is provided, which is required to run the
demo scripts. The module is based on belle-sip (from the Linphone SDK) and
provides a simple, asyncio-based API to make and receive SIP calls.

@ -0,0 +1,179 @@
import asyncio
import aiohttp
import pysip_lite
import argparse
import string
import random
parser = argparse.ArgumentParser(
description="Registers as a SIP UA and accepts any incoming calls.",
epilog="Example: --uri sip:bench2user000005@guest02-snail.lab.sipwise.com --pw testuser --rtpe http://localhost:9911/ng-plain",
)
parser.add_argument(
"--uri", required=True, help="SIP URI to register UA", metavar="SIP-URI"
)
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(
"--ringing",
required=False,
help="Time to wait between ringing and answering",
metavar="SECONDS",
type=float,
default=5,
)
parser.add_argument(
"--teardown",
required=False,
help="Hang up call after this much time",
metavar="SECONDS",
type=float,
)
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=15060,
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(args.uri, args.pw)
rtpe = args.rtpe
suffix = "_" + "".join(
random.choices(string.ascii_uppercase + string.digits, k=5)
)
async def teardown(call: pysip_lite.Call, s: float) -> None:
await asyncio.sleep(s)
try:
call.stop()
except:
pass
async def handle_call(call: pysip_lite.Call) -> None:
print("new call")
sdp: str = call.sdp()
try:
print("set ringing")
call.ringing()
await asyncio.sleep(args.ringing)
print("wait...")
req = {
"command": "publish",
"call-id": call.call_id() + suffix,
"from-tag": call.from_tag(),
"sdp": sdp,
"audio player": "force",
"flags": ["bidirectional"],
}
async with aiohttp.ClientSession() as session:
async with session.post(rtpe, json=req) as post:
resp: dict = await post.json()
print("do answer")
call.answer(resp["sdp"])
print("call running")
if args.teardown:
asyncio.create_task(teardown(call, args.teardown))
await call.finished()
except Exception as e:
print(f"Exception: {e.args}")
print("call end")
req = {
"command": "delete",
"call-id": call.call_id() + suffix,
}
async with aiohttp.ClientSession() as session:
async with session.post(rtpe, json=req) as post:
await post.json()
def log(s: str) -> None:
print(s)
running = True
async def main() -> None:
global running, ua
logger = pysip_lite.Logger(log, 1 if args.debug else 8)
print("registering...")
ok = await ua.register()
print("registered!")
assert ok
ua.listen()
while running:
call = await ua.receive()
if call:
asyncio.create_task(handle_call(call))
await logger
async def close() -> None:
global running, ua
print("unregistering and closing...")
running = False
await ua.unregister()
eventloop = asyncio.new_event_loop()
mt = eventloop.create_task(main())
try:
eventloop.run_until_complete(mt)
except KeyboardInterrupt as e:
eventloop.run_until_complete(close())
eventloop.run_until_complete(mt)
eventloop.close()

@ -0,0 +1,267 @@
import asyncio
import aiohttp
import pysip_lite
import argparse
import string
import random
parser = argparse.ArgumentParser(
description="Registers as a SIP UA and repeatedly makes outgoing calls",
epilog="Example: --uri sip:bench2user000005@guest02-snail.lab.sipwise.com --pw testuser --rtpe http://localhost:9911/ng-plain --to sip:bench2user000000@guest02-snail.lab.sipwise.com",
)
parser.add_argument(
"--uri", required=True, help="SIP URI to register UA", metavar="SIP-URI"
)
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(
"--to", required=True, help="SIP URI to make calls to", metavar="SIP-URI"
)
parser.add_argument(
"--wait",
required=False,
help="Make new call this long after the previous has closed",
metavar="SECONDS",
type=float,
default=5,
)
parser.add_argument(
"--multi",
help="Create multiple calls in parallel",
action="store_true",
)
parser.add_argument(
"--teardown",
required=False,
help="Hang up call after this much time",
metavar="SECONDS",
type=float,
)
parser.add_argument(
"--num",
required=False,
help="Stop after making this many calls",
metavar="NUM",
type=int,
default=0,
)
parser.add_argument(
"--codecs",
required=False,
help="Codecs to offer",
metavar="CODEC,CODEC,CODEC,...",
default="opus,AMR-WB,G722,AMR,PCMA,PCMU",
)
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=15061,
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(args.uri, args.pw)
to = args.to
rtpe = args.rtpe
running = True
async def teardown(call: pysip_lite.Call, s: float, seq: int) -> None:
await asyncio.sleep(s)
print(f"[{seq}] call stop")
try:
call.stop()
except:
print(f"[{seq}] exception")
seqn = 0
async def make_call() -> None:
global seqn
seq = seqn
seqn += 1
print(f"[{seq}] create req rtpengine...")
req = {
"command": "create",
"codecs": {
"offer": args.codecs.split(","),
},
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(rtpe, json=req) as post:
resp: dict = await post.json()
except:
print(f"[{seq}] exception")
return
print(f"[{seq}] send invite...")
call = ua.create(to, resp["call-id"], resp["from-tag"], resp["sdp"])
if not call:
print(f"[{seq}] failed to create call")
return
print(f"[{seq}] wait for answer...")
code: int = await call.wait(180)
if not code or code >= 400:
print(f"[{seq}] call rejected or error")
return
if code == 180:
print(f"[{seq}] ringing!")
code: int = await call.wait()
if not code or code >= 400:
print(f"[{seq}] call rejected or error")
return
if code == 183:
print(f"[{seq}] early media!")
code: int = await call.wait()
if not code or code >= 400:
print(f"[{seq}] call rejected or error")
return
print(f"[{seq}] call answered ({code})!")
print(f"[{seq}] answer req to rtpengine...")
req = {
"command": "create answer",
"call-id": call.call_id(),
"from-tag": call.from_tag(),
"sdp": call.sdp(),
"audio player": "force",
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(rtpe, json=req) as post:
resp: dict = await post.json()
except:
print(f"[{seq}] exception")
return
print(f"[{seq}] running!")
print(f"[{seq}] wait for finish...")
if args.teardown:
asyncio.create_task(teardown(call, args.teardown, seq))
await call.finished()
print(f"[{seq}] delete req to rtpengine...")
req = {
"command": "delete",
"call-id": call.call_id(),
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(rtpe, json=req) as post:
resp: dict = await post.json()
except:
print(f"[{seq}] exception")
return
print(f"[{seq}] call finished")
def log(s: str) -> None:
print(s)
async def main() -> None:
global running, ua
logger = pysip_lite.Logger(log, 1 if args.debug else 8)
print("registering...")
ok = await ua.register()
print("registered!")
assert ok
num_calls = 0
tasks = []
while running and (not args.num or num_calls < args.num):
print("interval wait...")
await asyncio.sleep(args.wait)
if not running:
break
num_calls += 1
if not args.multi:
await make_call()
else:
t = asyncio.create_task(make_call())
tasks.append(t)
await asyncio.gather(*tasks)
await logger
async def close() -> None:
global running, ua
print("unregistering and closing...")
running = False
await ua.unregister()
eventloop = asyncio.new_event_loop()
mt = eventloop.create_task(main())
try:
eventloop.run_until_complete(mt)
except KeyboardInterrupt as e:
eventloop.run_until_complete(close())
eventloop.run_until_complete(mt)
eventloop.close()

@ -0,0 +1,640 @@
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()

@ -0,0 +1,235 @@
import asyncio
import aiohttp
import pysip_lite
import argparse
import string
import random
parser = argparse.ArgumentParser(
description="Register as a SIP UA and act as a proxy to another UA.",
epilog="Example: --uri sip:bench2user000005@guest02-snail.lab.sipwise.com --pw testuser --rtpe http://localhost:9911/ng-plain --to sip:bench2user000001@guest02-snail.lab.sipwise.com",
)
parser.add_argument(
"--uri", required=True, help="SIP URI to register UA", metavar="SIP-URI"
)
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(
"--to", required=True, help="SIP URI to make calls to", metavar="SIP-URI"
)
parser.add_argument(
"--codecs",
required=False,
help="Additional odecs to offer",
metavar="CODEC,CODEC,CODEC,...",
default="",
)
parser.add_argument(
"--audio-player",
help="Force use of audio player",
action="store_true",
)
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=15063,
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(args.uri, args.pw)
to = args.to
rtpe = args.rtpe
suffix = "_" + "".join(
random.choices(string.ascii_uppercase + string.digits, k=5)
)
async def wait_finish(A: pysip_lite.Call, B: pysip_lite.Call) -> None:
await B.finished()
try:
A.stop()
except:
pass
async def answer_with(
code: int, A: pysip_lite.Call, B: pysip_lite.Call
) -> None:
req = {
"command": "answer",
"call-id": A.call_id() + suffix,
"from-tag": A.from_tag(),
"to-tag": B.to_tag(),
"sdp": B.sdp(),
}
if args.audio_player:
req["audio player"] = "force"
async with aiohttp.ClientSession() as session:
async with session.post(rtpe, json=req) as post:
resp: dict = await post.json()
A.answer(resp["sdp"], code)
async def handle_call(A: pysip_lite.Call) -> None:
print("new call")
sdp: str = A.sdp()
try:
print("offer to rtpengine")
req = {
"command": "offer",
"call-id": A.call_id() + suffix,
"from-tag": A.from_tag(),
"sdp": sdp,
"codec": {"transcode": args.codecs.split(",")},
}
if args.audio_player:
req["audio player"] = "force"
async with aiohttp.ClientSession() as session:
async with session.post(rtpe, json=req) as post:
resp: dict = await post.json()
print("invite to B leg")
B = ua.create(to, A.call_id() + "_b2b", A.from_tag(), resp["sdp"])
if not B:
print("failed to create call")
raise RuntimeError("error")
asyncio.create_task(wait_finish(B, A))
asyncio.create_task(wait_finish(A, B))
code: int = await B.wait(180)
while code == 180:
print("ringing")
A.ringing()
code: int = await B.wait(180)
while code == 183:
if B.sdp:
print("early media")
await answer_with(183, A, B)
code: int = await B.wait(183)
if code != 200:
print("rejected or error")
raise RuntimeError("rejected")
print("answered!")
await answer_with(200, A, B)
print("call running")
await A.finished()
B.stop()
except Exception as e:
print(e)
try:
A.stop()
B.stop()
except:
pass
print("call end")
req = {
"command": "delete",
"call-id": A.call_id() + suffix,
}
async with aiohttp.ClientSession() as session:
async with session.post(rtpe, json=req) as post:
await post.json()
def log(s: str) -> None:
print(s)
running = True
async def main() -> None:
global running, ua
logger = pysip_lite.Logger(log, 1 if args.debug else 8)
print("registering...")
ok = await ua.register()
if not ok:
print("failed to register!")
return
print("registered!")
ua.listen()
while running:
call = await ua.receive()
if call:
asyncio.create_task(handle_call(call))
await logger
async def close() -> None:
global running, ua
print("unregistering and closing...")
running = False
await ua.unregister()
eventloop = asyncio.new_event_loop()
mt = eventloop.create_task(main())
try:
eventloop.run_until_complete(mt)
except KeyboardInterrupt as e:
eventloop.run_until_complete(close())
eventloop.run_until_complete(mt)
eventloop.close()

@ -0,0 +1,2 @@
__pycache__
*.so

@ -0,0 +1,127 @@
from ctypes import *
import typing
from ._types import *
from ._lib import *
from ._Waiter import *
class Call:
"""Represents a SIP call."""
_ci: call_info = None
_aw: Waiter = None
def __init__(self, ci: call_info, answer_waiter: Waiter = None):
self._ci = ci
self._aw = answer_waiter
def __del__(self):
lib.bsw_call_destroy(self._ci.call)
def sdp(self) -> str:
"""Returns the (remote) SDP body"""
return self._ci.body.decode()
def call_id(self) -> str:
"""Returns the call ID"""
return self._ci.call_id.decode()
def from_addr(self) -> str:
"""Returns the SIP "from" address"""
return self._ci.from_addr.decode()
def from_tag(self) -> str:
"""Returns the SIP "from" tag"""
return self._ci.from_tag.decode()
def to_tag(self) -> str:
"""Returns the SIP "to" tag"""
return self._ci.to_tag.decode()
def ringing(self, sdp: str = None) -> None:
"""Sets a pending incoming call to "ringing" state by sending a 180
response"""
ok: bool = lib.bsw_call_answer(
self._ci.call, 180, sdp.encode() if sdp else None
)
if not ok:
raise RuntimeError("wrong call state")
def reject(self, code: int) -> None:
"""Rejects a pending incoming call with an error code.
:param code: integer SIP error code (>= 400)
"""
lib.bsw_call_answer(self._ci.call, code, None)
def answer(self, sdp: str = None, code: int = 200) -> None:
"""Answers a pending incoming call.
:param sdp: SDP body to answer with
:param code: SIP response code (defaults to 200, or 183)
"""
ok: bool = lib.bsw_call_answer(
self._ci.call, code, sdp.encode() if sdp else None
)
if not ok:
raise RuntimeError("wrong call state")
async def finished(self) -> None:
"""Wait for an established or pending call to terminate"""
w = Waiter()
done: bool = lib.bsw_call_finished(self._ci.call, w.fd())
if not done:
await w.wait()
def stop(self) -> None:
"""Stop an existing call.
Sends a BYE for an established call, or CANCEL for a pending outgoing
call. For a pending incoming call, sends an appropriate response code.
"""
ok: bool = lib.bsw_call_terminate(self._ci.call)
if not ok:
raise RuntimeError("call not established")
async def wait(self, state: int = 200) -> int:
"""Wait for a state change on an outgoing call.
Returns the new state as a SIP response code.
The state to wait for defaults to 200, which means wait for the call to
be fully established. To wait for a "ringing" state, wait for code
180, and wait for 183 to catch early media.
The returned state can be different from what was waited for (e.g. >=
400 for a failed call when waiting for 200, or 200 when waiting for
18x).
:param state: state to wait for as integer SIP code
"""
assert self._aw
while True:
res: str = await self._aw.wait_val()
if res == "0":
return False
ci = call_info()
code: int = lib.bsw_call_wait(self._ci.call, byref(ci))
if code == 183 or code == 200:
if code == 183 and state >= 200:
continue
# copy remote data
self._ci.body = ci.body
self._ci.content_type = ci.content_type
self._ci.to_tag = ci.to_tag
return code
if code >= 400:
return 400
if code == 180:
if state >= 200:
continue
return 180
return code # ?

@ -0,0 +1,39 @@
import asyncio
import typing
from ._Waiter import Waiter
from ._lib import *
class Logger:
"""Registers a callback to receive log messages and sets the log level.
The object must be "await"ed for to close the logging context.
:param fn: callable (taking one string argument) to pass log messages to
:param log_level: belle-sip log level (1 for full debug)
"""
_fn: typing.Callable[[str], None] = None
_w: Waiter = None
_t: asyncio.Task = None
def __init__(self, fn: typing.Callable[[str], None], log_level: int = 4):
self._fn = fn
self._w = Waiter()
self._t = asyncio.create_task(self._wait())
lib.bsw_set_logger(log_level, self._w.fd())
async def _wait(self):
while self._w:
ok: bool = await self._w.wait()
if not ok:
break
s = c_char_p(b"\0" * 8192)
ok = lib.bsw_get_log(s, 8192)
if ok:
self._fn(s.value.decode())
def __await__(self) -> None:
self._w.close() # wakes up _wait()
yield from self._t

@ -0,0 +1,33 @@
from ._UserAgent import _UserAgent
from ctypes import *
from ._types import *
from ._lib import *
class Provider:
"""Base class required for all SIP communications.
Opens and binds a socket to a local interface address and port.
:param addr: Local IP address (default 0.0.0.0)
:param port: Local port to bind to
:param proto: Protocol (UDP or TCP)
"""
_prov: provider_ptr = None
def __init__(
self, addr: str = "0.0.0.0", port: int = 0, proto: str = "UDP"
) -> None:
self._prov = lib.bsw_provider(addr.encode(), port, proto.encode())
if not self._prov:
raise RuntimeError("failed to create SIP provider")
def user_agent(self, uri: str, pw: str) -> _UserAgent:
"""Create a user agent.
:param uri: SIP uri for the UA
:param pw: Password
"""
return _UserAgent(self, uri, pw)

@ -0,0 +1,111 @@
import typing
from ._Waiter import Waiter
from ._types import *
from ._lib import *
from .Call import *
Provider = typing.NewType("Provider", None)
class _UserAgent:
"""Represents a SIP user agent which can make and receive calls."""
_prov: Provider = None
_uri: str = None
_pw: str = None
_w: Waiter = None
def __init__(self, prov: Provider, uri: str, pw: str):
self._prov = prov
self._uri = uri
self._pw = pw
self._reg = False
async def register(self, dur=600) -> bool:
"""Register the user agent with the given password.
:param dur: Duration of the registration in seconds
"""
w = Waiter()
lib.bsw_register(
self._prov._prov,
w.fd(),
self._uri.encode(),
self._pw.encode(),
dur,
)
res: bool = await w.wait()
if dur:
self._reg = res
return res
async def unregister(self) -> None:
"""Remove an existing registration."""
res: bool = await self.register(dur=0)
self._reg = False
self.unlisten()
def listen(self) -> None:
"""Listen for incoming calls."""
assert self._w is None
self._w = Waiter()
lib.bsw_listen(self._prov._prov, self._w.fd(), self._uri.encode())
def unlisten(self) -> None:
"""Undo the .listen() operation."""
if not self._w:
return
# wake up the listener (self.receive())
lib.bsw_listen(self._prov._prov, -1, self._uri.encode())
self._w = None
async def receive(self) -> Call | None:
"""Wait for an incoming call and returns a new call object.
Returns None when .unlisten() is called.
.listen() must have been called before."""
while self._w is not None:
ok: bool = await self._w.wait()
if not ok:
continue
ci = call_info()
res: bool = lib.bsw_receive(
self._prov._prov, self._uri.encode(), byref(ci)
)
if not res:
continue
return Call(ci)
def create(
self, to_addr: str, call_id: str, from_tag: str, sdp: str
) -> Call:
"""Create a new outgoing call and sends an INVITE.
Returns a new call object.
:param to_addr: SIP uri to make the call to
:param call_id: call ID for the new call
:param from_tag: "From" header tag for the INVITE
:param sdp: complete SDP body
"""
w = Waiter()
ci = call_info(
call_id=call_id.encode(),
from_addr=self._uri.encode(),
from_tag=from_tag.encode(),
to_addr=to_addr.encode(),
)
if sdp is not None:
ci.body = sdp.encode()
# XXX allow 200/ACK SDP exchange
ci.call = lib.bsw_call_create(self._prov._prov, byref(ci), w.fd())
# ok: bool = await w.wait() # XXX allow time-out handling, safe with byref(ci)
# clear for remote SDP
ci.body = b""
ci.content_type = b""
c = Call(ci, w)
return c

@ -0,0 +1,40 @@
import asyncio
import os
class Waiter:
_r: int = None
_w: int = None
def __init__(self):
self._r, self._w = os.pipe()
def __del__(self):
self.close()
if self._r:
os.close(self._r)
self._r = None
def close(self) -> None:
if self._w:
os.close(self._w)
self._w = None
def fd(self) -> int:
return self._w
def _reader(self, r: int, f: asyncio.Future) -> None:
b = os.read(r, 1)
f.set_result(b.decode())
async def wait_val(self) -> str:
f = asyncio.get_event_loop().create_future()
asyncio.get_event_loop().add_reader(
self._r, lambda: self._reader(self._r, f)
)
res: str = await f
asyncio.get_event_loop().remove_reader(self._r)
return res
async def wait(self) -> bool:
return await self.wait_val() == "1"

@ -0,0 +1,7 @@
__name__ = "pysip_lite"
__package__ = "pysip_lite"
__version__ = "1.0.0"
__author__ = "Richard Fuchs"
from .Provider import Provider
from .Logger import Logger

@ -0,0 +1,65 @@
from ctypes import *
from ._types import *
import os
lib: CDLL = CDLL(
os.path.join(os.path.dirname(os.path.abspath(__file__)), "belle-wrap.so")
)
assert lib
# primary lib init
lib.bsw_init.argtypes = []
lib.bsw_init.restype = c_bool
# set up logging callback
lib.bsw_set_logger.argtypes = [c_int, c_int]
lib.bsw_set_logger.restype = None
# receive one log message
lib.bsw_get_log.argtypes = [c_char_p, c_size_t]
lib.bsw_get_log.restype = c_bool
# create a provider
lib.bsw_provider.argtypes = [c_char_p, c_int, c_char_p]
lib.bsw_provider.restype = provider_ptr
# register a UA
lib.bsw_register.argtypes = [provider_ptr, c_int, c_char_p, c_char_p, c_int]
lib.bsw_register.restype = None
# listen for incoming calls
lib.bsw_listen.argtypes = [provider_ptr, c_int, c_char_p]
lib.bsw_listen.restype = None
# wait for one incoming call
lib.bsw_receive.argtypes = [provider_ptr, c_char_p, call_info_ptr]
lib.bsw_receive.restype = c_bool
# destroy a call object
lib.bsw_call_destroy.argtypes = [call_ptr]
lib.bsw_call_destroy.restype = None
# answer one incoming call
lib.bsw_call_answer.argtypes = [call_ptr, c_int, c_char_p]
lib.bsw_call_answer.restype = bool
# wait for end of call
lib.bsw_call_finished.argtypes = [call_ptr, c_int]
lib.bsw_call_finished.restype = bool
# close a running call
lib.bsw_call_terminate.argtypes = [call_ptr]
lib.bsw_call_terminate.restype = bool
# initiate an outgoing call
lib.bsw_call_create.argtypes = [provider_ptr, call_info_ptr, c_int]
lib.bsw_call_create.restype = call_ptr
# return state of outgoing call answer
lib.bsw_call_wait.argtypes = [call_ptr, call_info_ptr]
lib.bsw_call_wait.restype = c_int
if not lib.bsw_init():
raise RuntimeError("lib init failed")

@ -0,0 +1,33 @@
from ctypes import *
# opaque C types
class call(Structure):
pass
class provider_t(Structure):
pass
provider_ptr = POINTER(provider_t)
call_ptr = POINTER(call)
class call_info(Structure):
_fields_ = [
("call_id", c_char * 256),
("body", c_char * 8192),
("content_type", c_char * 256),
("from_addr", c_char * 256),
("to_addr", c_char * 256),
("from_tag", c_char * 256),
("to_tag", c_char * 256),
("call", call_ptr),
]
call_info_ptr = POINTER(call_info)

@ -0,0 +1 @@
*.o

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save