mirror of https://github.com/sipwise/rtpengine.git
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…
Reference in new issue