diff --git a/python/.ycm_extra_conf.py b/python/.ycm_extra_conf.py
new file mode 100644
index 000000000..7d77c4666
--- /dev/null
+++ b/python/.ycm_extra_conf.py
@@ -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
+
+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 {}
diff --git a/python/263655_2064400-lq.mp3 b/python/263655_2064400-lq.mp3
new file mode 100644
index 000000000..2f09cf26f
Binary files /dev/null and b/python/263655_2064400-lq.mp3 differ
diff --git a/python/Makefile b/python/Makefile
new file mode 100644
index 000000000..9475b34ca
--- /dev/null
+++ b/python/Makefile
@@ -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
diff --git a/python/README.md b/python/README.md
new file mode 100644
index 000000000..28d2b6705
--- /dev/null
+++ b/python/README.md
@@ -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.
diff --git a/python/demos/1_simple_answer.py b/python/demos/1_simple_answer.py
new file mode 100644
index 000000000..95cd0da11
--- /dev/null
+++ b/python/demos/1_simple_answer.py
@@ -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()
diff --git a/python/demos/2_simple_invite.py b/python/demos/2_simple_invite.py
new file mode 100644
index 000000000..a8f870000
--- /dev/null
+++ b/python/demos/2_simple_invite.py
@@ -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()
diff --git a/python/demos/3_gui.py b/python/demos/3_gui.py
new file mode 100644
index 000000000..c58a531ec
--- /dev/null
+++ b/python/demos/3_gui.py
@@ -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()
diff --git a/python/demos/4_b2b.py b/python/demos/4_b2b.py
new file mode 100644
index 000000000..0780516a8
--- /dev/null
+++ b/python/demos/4_b2b.py
@@ -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()
diff --git a/python/pysip_lite/.gitignore b/python/pysip_lite/.gitignore
new file mode 100644
index 000000000..5ddc4ed4e
--- /dev/null
+++ b/python/pysip_lite/.gitignore
@@ -0,0 +1,2 @@
+__pycache__
+*.so
diff --git a/python/pysip_lite/Call.py b/python/pysip_lite/Call.py
new file mode 100644
index 000000000..f580f2fc9
--- /dev/null
+++ b/python/pysip_lite/Call.py
@@ -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 # ?
diff --git a/python/pysip_lite/Logger.py b/python/pysip_lite/Logger.py
new file mode 100644
index 000000000..01c3d7291
--- /dev/null
+++ b/python/pysip_lite/Logger.py
@@ -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
diff --git a/python/pysip_lite/Provider.py b/python/pysip_lite/Provider.py
new file mode 100644
index 000000000..6f22baf8d
--- /dev/null
+++ b/python/pysip_lite/Provider.py
@@ -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)
diff --git a/python/pysip_lite/_UserAgent.py b/python/pysip_lite/_UserAgent.py
new file mode 100644
index 000000000..4dab6a806
--- /dev/null
+++ b/python/pysip_lite/_UserAgent.py
@@ -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
diff --git a/python/pysip_lite/_Waiter.py b/python/pysip_lite/_Waiter.py
new file mode 100644
index 000000000..28a81fd5a
--- /dev/null
+++ b/python/pysip_lite/_Waiter.py
@@ -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"
diff --git a/python/pysip_lite/__init__.py b/python/pysip_lite/__init__.py
new file mode 100644
index 000000000..59e3a7b2a
--- /dev/null
+++ b/python/pysip_lite/__init__.py
@@ -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
diff --git a/python/pysip_lite/_lib.py b/python/pysip_lite/_lib.py
new file mode 100644
index 000000000..36c34492a
--- /dev/null
+++ b/python/pysip_lite/_lib.py
@@ -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")
diff --git a/python/pysip_lite/_types.py b/python/pysip_lite/_types.py
new file mode 100644
index 000000000..6ba893a0c
--- /dev/null
+++ b/python/pysip_lite/_types.py
@@ -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)
diff --git a/python/src/.gitignore b/python/src/.gitignore
new file mode 100644
index 000000000..5761abcfd
--- /dev/null
+++ b/python/src/.gitignore
@@ -0,0 +1 @@
+*.o
diff --git a/python/src/belle-wrap.c b/python/src/belle-wrap.c
new file mode 100644
index 000000000..6040c7f6a
--- /dev/null
+++ b/python/src/belle-wrap.c
@@ -0,0 +1,1242 @@
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+
+// XXX replace char* with char[] below
+
+struct call_info;
+
+struct auth_info {
+ char *username;
+ char *domain;
+ char *passwd;
+};
+
+struct transaction {
+ int response_waker;
+
+ // handler:
+ void (*response)(struct transaction *, int code, const belle_sip_response_event_t *,
+ belle_sip_response_t *, belle_sip_client_transaction_t *, belle_sip_provider_t *);
+
+ // for register & invite:
+ bool auth;
+ bool done;
+
+ // for invite:
+ struct call *call;
+};
+
+struct listener {
+ char *username;
+ char *domain;
+ int incoming_waker;
+ GQueue calls; // struct call
+};
+
+// corresponds to a sip_dialog_t
+struct call {
+ belle_sip_provider_t *sip_provider;
+ belle_sip_dialog_t *dlg;
+ belle_sip_server_transaction_t *strans; // pending received invite
+ belle_sip_client_transaction_t *ctrans; // pending outgoing invite
+ enum {
+ CS_INC_PENDING = 0,
+ CS_INC_RINGING, // 180 sent
+ CS_INC_MEDIA, // 183 sent
+ CS_OUT_PENDING, // INVITE sent
+ CS_OUT_RINGING, // 180 received
+ CS_OUT_MEDIA, // 183 received
+ CS_ESTABLISHED,
+ CS_DEAD,
+ } state;
+ int terminate_waker;
+};
+
+struct call_info {
+ char call_id[256];
+ char body[8192]; // SDP etc
+ char content_type[256];
+ char from_addr[256];
+ char to_addr[256];
+ char from_tag[256];
+ char to_tag[256];
+ struct call *call;
+};
+
+
+static belle_sip_stack_t *sipstack;
+static belle_sip_main_loop_t *main_loop;
+static pthread_mutex_t main_mutex = PTHREAD_MUTEX_INITIALIZER;
+static pthread_mutex_t run_mutex = PTHREAD_MUTEX_INITIALIZER;
+static int wake_fds[2];
+
+static pthread_mutex_t auth_mutex = PTHREAD_MUTEX_INITIALIZER;
+static GHashTable *auth_info;
+
+static pthread_mutex_t listen_mutex = PTHREAD_MUTEX_INITIALIZER;
+static GHashTable *listeners;
+
+static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER;
+static GQueue logs = G_QUEUE_INIT;
+static int log_fd = -1;
+
+
+
+#define FD_TO_PTR(x) ((void *) ((long) (x)))
+#define PTR_TO_FD(x) ((int) ((long) (x)))
+
+
+
+static struct auth_info *get_auth_info(const char *un, const char *dom) {
+ if (!un || !dom)
+ return NULL;
+
+ const struct auth_info req = {.username = (char *) un, .domain = (char *) dom};
+ return g_hash_table_lookup(auth_info, &req);
+}
+
+
+#define CONCAT2(a, b) a ## b
+#define CONCAT(a, b) CONCAT2(a, b)
+
+typedef struct {
+ pthread_mutex_t *m;
+} lock_t;
+static void auto_unlock(lock_t *l) {
+ pthread_mutex_unlock(l->m);
+}
+static lock_t auto_lock(pthread_mutex_t *l) {
+ pthread_mutex_lock(l);
+ return (lock_t) {l};
+}
+G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(lock_t, auto_unlock);
+#define LOCK(m) g_auto(lock_t) CONCAT(__l, __COUNTER__) __attribute__((unused)) = auto_lock(m)
+
+
+static void set_auth_info(const char *un, const char *dom, const char *pw) {
+ if (!un || !dom || !pw)
+ return; // XXX fail?
+
+ LOCK(&auth_mutex);
+
+ struct auth_info *ai = get_auth_info(un, dom);
+ if (!ai) {
+ ai = g_new0(struct auth_info, 1);
+ ai->username = g_strdup(un);
+ ai->domain = g_strdup(dom);
+ g_hash_table_insert(auth_info, ai, ai);
+ }
+
+ g_free(ai->passwd);
+ ai->passwd = g_strdup(pw);
+}
+
+
+static void log_handler(const char *domain, BctbxLogLevel lev, const char *fmt, va_list args)
+{
+ char *s = g_strdup_vprintf(fmt, args);
+ char *p = g_strdup_printf("[%d] [%s] %s", lev, domain, s);
+ g_free(s);
+
+ LOCK(&log_mutex);
+ g_queue_push_tail(&logs, p);
+
+ if (log_fd != -1)
+ (void) write(log_fd, "1", 1);
+}
+
+
+static void log_int(BctbxLogLevel lev, const char *fmt, ...) {
+ va_list ap;
+
+ va_start(ap, fmt);
+ log_handler("internal", lev, fmt, ap);
+ va_end(ap);
+}
+
+
+#define ASSERT_RET_VAL(a, v, m, ...) do { \
+ if (G_UNLIKELY(!(a))) { \
+ log_int(BCTBX_LOG_TRACE, "[%s:%d]" m, __FUNCTION__, __LINE__, ##__VA_ARGS__); \
+ return v; \
+ } \
+} while (0)
+
+#define ASSERT_RET(a, m, ...) ASSERT_RET_VAL(a, (void)0, m, ##__VA_ARGS__)
+#define ASSERT_RET_NULL(a, m, ...) ASSERT_RET_VAL(a, NULL, m, ##__VA_ARGS__)
+#define ASSERT_RET_FALSE(a, m, ...) ASSERT_RET_VAL(a, false, m, ##__VA_ARGS__)
+
+
+static void auth_resp(struct transaction *t, const belle_sip_response_event_t *event,
+ belle_sip_client_transaction_t *trans,
+ belle_sip_provider_t *sip_provider)
+{
+ struct call *c = NULL;
+
+ belle_sip_dialog_t *dlg = belle_sip_response_event_get_dialog(event);
+ if (dlg) {
+ c = belle_sip_dialog_get_application_data(dlg);
+ ASSERT_RET(c, "auth response without dialog");
+ belle_sip_dialog_set_application_data(dlg, NULL);
+ }
+
+ belle_sip_request_t *req = belle_sip_client_transaction_create_authenticated_request(trans,
+ NULL, NULL);
+ ASSERT_RET(req, "failed to cast transaction to request");
+
+ // create new transaction and move transaction & call objects
+ belle_sip_transaction_set_application_data(BELLE_SIP_TRANSACTION(trans), NULL);
+
+ trans = belle_sip_provider_create_client_transaction(sip_provider, req);
+ ASSERT_RET(trans, "failed to create client transaction");
+
+ belle_sip_transaction_set_application_data(BELLE_SIP_TRANSACTION(trans), t);
+ t->auth = true;
+
+ if (dlg) {
+ belle_sip_dialog_set_application_data(dlg, NULL);
+
+ dlg = belle_sip_provider_create_dialog(sip_provider, BELLE_SIP_TRANSACTION(trans));
+ belle_sip_dialog_set_application_data(dlg, c);
+
+ if (c->dlg)
+ belle_sip_object_unref(c->dlg);
+
+ c->dlg = dlg;
+ belle_sip_object_ref(c->dlg);
+ }
+
+ belle_sip_client_transaction_send_request_to(trans, NULL);
+}
+
+
+static void reg_response(struct transaction *t, int code,
+ const belle_sip_response_event_t *event,
+ belle_sip_response_t *res,
+ belle_sip_client_transaction_t *trans, belle_sip_provider_t *sip_provider)
+{
+ if (code == 401 && !t->auth)
+ auth_resp(t, event, trans, sip_provider);
+ else if (code == 200) {
+ if (!t->done) {
+ t->done = true;
+ write(t->response_waker, "1", 1);
+
+ belle_sip_refresher_t *rfr = belle_sip_client_transaction_create_refresher(trans);
+ ASSERT_RET(rfr, "failed to create refresher");
+ // XXX remove refresher on unreg
+ }
+ }
+ else if (code >= 400) {
+ if (!t->done) {
+ t->done = true;
+ write(t->response_waker, "0", 1);
+ }
+ }
+ else {
+ // XXX ?
+ }
+}
+
+
+static void transaction_free(struct transaction *t) {
+ g_free(t);
+}
+
+
+static void res_event(void *user_ctx, const belle_sip_response_event_t *event)
+{
+ ASSERT_RET(event, "empty event");
+
+ belle_sip_provider_t *sip_provider = user_ctx;
+
+ belle_sip_response_t *res = belle_sip_response_event_get_response(event);
+ ASSERT_RET(res, "no response from event");
+
+ int code = belle_sip_response_get_status_code(res);
+
+ belle_sip_client_transaction_t *trans = belle_sip_response_event_get_client_transaction(event);
+ ASSERT_RET(trans, "no transaction from event");
+
+ struct transaction *t = belle_sip_transaction_get_application_data(BELLE_SIP_TRANSACTION(trans));
+
+ ASSERT_RET(t, "no user transaction object");
+
+ t->response(t, code, event, res, trans, sip_provider);
+}
+
+
+static void ua_unlisten(const char *un, const char *dom) {
+ const struct listener lx = {.username = (char *) un, .domain = (char *) dom};
+
+ LOCK(&listen_mutex);
+
+ struct listener *l = NULL;
+ g_hash_table_steal_extended(listeners, &lx, (void **) &l, NULL);
+
+ if (!l)
+ return;
+
+ write(l->incoming_waker, "0", 1);
+ g_free(l);
+}
+
+
+static void ua_listen(const char *un, const char *dom, int fd) {
+ if (fd == -1) {
+ ua_unlisten(un, dom);
+ return;
+ }
+
+ struct listener *l = g_new0(struct listener, 1);
+ l->username = g_strdup(un);
+ l->domain = g_strdup(dom);
+ l->incoming_waker = fd;
+
+ LOCK(&listen_mutex);
+ ASSERT_RET(g_hash_table_lookup(listeners, l) == NULL, "duplicate listener");
+ g_hash_table_insert(listeners, l, l);
+}
+
+
+static struct listener *get_listener(const char *un, const char *dom) {
+ const struct listener l = {.username = (char *) un, .domain = (char *) dom};
+ return g_hash_table_lookup(listeners, &l);
+}
+
+
+static void req_invite(belle_sip_provider_t *sip_provider, belle_sip_request_t *req,
+ const belle_sip_request_event_t *event)
+{
+ belle_sip_message_t *msg = BELLE_SIP_MESSAGE(req);
+
+ belle_sip_server_transaction_t *trans = belle_sip_request_event_get_server_transaction(event);
+ ASSERT_RET(trans == NULL, "server transaction already exists");
+
+ trans = belle_sip_provider_create_server_transaction(sip_provider, req);
+ ASSERT_RET(trans, "failed to create server transaction");
+
+ belle_sip_header_to_t *to = belle_sip_message_get_header_by_type(msg, belle_sip_header_to_t);
+ ASSERT_RET(to, "failed to create TO header");
+
+ belle_sip_header_address_t *toa = BELLE_SIP_HEADER_ADDRESS(to);
+ ASSERT_RET(toa, "failed to cast to header address");
+
+ belle_sip_uri_t *to_uri = belle_sip_header_address_get_uri(toa);
+ ASSERT_RET(to_uri, "failed to get URI");
+
+ belle_sip_dialog_t *dlg = belle_sip_request_event_get_dialog(event);
+ ASSERT_RET(!dlg, "dialog already exists");
+
+ LOCK(&listen_mutex);
+ struct listener *l = get_listener(belle_sip_uri_get_user(to_uri), belle_sip_uri_get_host(to_uri));
+ if (!l) {
+ belle_sip_response_t *res = belle_sip_response_create_from_request(req, 410);
+ ASSERT_RET(res, "failed to create response");
+
+ belle_sip_provider_send_response(sip_provider, res);
+
+ return;
+ }
+
+ dlg = belle_sip_provider_create_dialog(sip_provider, BELLE_SIP_TRANSACTION(trans));
+ ASSERT_RET(dlg, "failed to create dialog");
+
+ struct call *c = g_new0(struct call, 1);
+ c->terminate_waker = -1;
+
+ c->sip_provider = sip_provider;
+ belle_sip_object_ref(sip_provider);
+
+ c->dlg = dlg;
+ belle_sip_object_ref(dlg);
+
+ c->strans = trans;
+ belle_sip_object_ref(trans);
+
+ belle_sip_dialog_set_application_data(dlg, c);
+
+ // call belongs to listener
+ g_queue_push_tail(&l->calls, c);
+
+ write(l->incoming_waker, "1", 1);
+}
+
+
+static void req_cancel(belle_sip_provider_t *sip_provider, belle_sip_request_t *req,
+ const belle_sip_request_event_t *event)
+{
+ belle_sip_dialog_t *dlg = belle_sip_request_event_get_dialog(event);
+ ASSERT_RET(dlg, "no dialog");
+
+ struct call *c = belle_sip_dialog_get_application_data(dlg);
+ ASSERT_RET(c, "no call object");
+
+ if (c->state != CS_DEAD) {
+ if (c->terminate_waker != -1)
+ write(c->terminate_waker, "1", 1);
+
+ c->state = CS_DEAD;
+ }
+
+ // send "cancelled"
+ belle_sip_request_t *ireq = belle_sip_transaction_get_request(BELLE_SIP_TRANSACTION(c->strans));
+ ASSERT_RET(ireq, "no request from transaction");
+
+ belle_sip_response_t *res = belle_sip_response_create_from_request(ireq, 487);
+ ASSERT_RET(res, "failed to create response");
+
+ belle_sip_message_t *msg = BELLE_SIP_MESSAGE(res);
+ ASSERT_RET(msg, "failed to cast to message");
+
+ belle_sip_message_add_header(msg, BELLE_SIP_HEADER(belle_sip_header_contact_new()));
+
+ belle_sip_server_transaction_send_response(c->strans, res);
+
+ // ok to cancel
+ res = belle_sip_response_create_from_request(req, 200);
+ ASSERT_RET(res, "failed to create response");
+
+ belle_sip_provider_send_response(c->sip_provider, res);
+}
+
+
+static void req_bye(belle_sip_provider_t *sip_provider, belle_sip_request_t *req,
+ const belle_sip_request_event_t *event)
+{
+ belle_sip_dialog_t *dlg = belle_sip_request_event_get_dialog(event);
+ if (!dlg) {
+ // XXX send resp?
+ return;
+ }
+
+ struct call *c = belle_sip_dialog_get_application_data(dlg);
+ ASSERT_RET(c, "no application call object");
+
+ if (c->state != CS_DEAD) {
+ if (c->terminate_waker != -1)
+ write(c->terminate_waker, "1", 1);
+
+ c->state = CS_DEAD;
+ }
+
+ belle_sip_server_transaction_t *trans = belle_sip_request_event_get_server_transaction(event);
+ ASSERT_RET(!trans, "server transaction already exists");
+
+ trans = belle_sip_provider_create_server_transaction(sip_provider, req);
+ ASSERT_RET(trans, "failed to create transaction");
+
+ belle_sip_response_t *res = belle_sip_response_create_from_request(req, 200);
+ ASSERT_RET(res, "failed to create response");
+
+ belle_sip_server_transaction_send_response(trans, res);
+}
+
+
+static void req_event(void *user_ctx, const belle_sip_request_event_t *event)
+{
+ ASSERT_RET(event, "empty event");
+
+ belle_sip_provider_t *sip_provider = user_ctx;
+
+ belle_sip_request_t *req = belle_sip_request_event_get_request(event);
+ ASSERT_RET(req, "no request from event");
+
+ const char *method = belle_sip_request_get_method(req);
+ ASSERT_RET(method, "empty method");
+
+ if (!strcmp(method, "OPTIONS")) {
+ belle_sip_response_t *res = belle_sip_response_create_from_request(req, 200);
+ ASSERT_RET(res, "failed to create response");
+
+ belle_sip_provider_send_response(sip_provider, res);
+ }
+ else if (!strcmp(method, "INVITE"))
+ req_invite(sip_provider, req, event);
+ else if (!strcmp(method, "CANCEL"))
+ req_cancel(sip_provider, req, event);
+ else if (!strcmp(method, "BYE"))
+ req_bye(sip_provider, req, event);
+ else if (!strcmp(method, "ACK"))
+ {}
+ else {
+ belle_sip_response_t *res = belle_sip_response_create_from_request(req, 501);
+ ASSERT_RET(res, "failed to create response");
+
+ belle_sip_provider_send_response(sip_provider, res);
+ }
+}
+
+
+static void timeout(void *user_ctx, const belle_sip_timeout_event_t *event)
+{
+ printf("%s\n", __FUNCTION__);
+}
+
+
+static void trans_terminated(void *user_ctx, const belle_sip_transaction_terminated_event_t *event)
+{
+ belle_sip_transaction_t *trans = NULL;
+
+ belle_sip_client_transaction_t *ctrans = belle_sip_transaction_terminated_event_get_client_transaction(event);
+
+ if (ctrans)
+ trans = BELLE_SIP_TRANSACTION(ctrans);
+ else {
+ belle_sip_server_transaction_t *strans = belle_sip_transaction_terminated_event_get_server_transaction(event);
+ if (strans)
+ trans = BELLE_SIP_TRANSACTION(strans);
+ }
+
+ ASSERT_RET(trans, "no transaction");
+
+ struct transaction *t = belle_sip_transaction_get_application_data(BELLE_SIP_TRANSACTION(trans));
+
+ if (t)
+ transaction_free(t);
+}
+
+
+static void dlg_terminated(void *user_ctx, const belle_sip_dialog_terminated_event_t *event)
+{
+ belle_sip_dialog_t *dlg = belle_sip_dialog_terminated_event_get_dialog(event);
+ if (!dlg)
+ return;
+
+ struct call *c = belle_sip_dialog_get_application_data(dlg);
+ if (!c)
+ return;
+
+ if (c->state == CS_DEAD)
+ return;
+
+ if (c->terminate_waker != -1)
+ write(c->terminate_waker, "1", 1);
+
+ c->state = CS_DEAD;
+}
+
+
+static void auth_request(void *user_ctx, belle_sip_auth_event_t *auth_event)
+{
+ LOCK(&auth_mutex);
+
+ struct auth_info *ai = get_auth_info(belle_sip_auth_event_get_username(auth_event),
+ belle_sip_auth_event_get_domain(auth_event));
+
+ if (ai)
+ belle_sip_auth_event_set_passwd(auth_event, ai->passwd);
+ // XXX else error
+}
+
+
+static void lst_destroy(void *user_ctx)
+{
+ printf("%s\n", __FUNCTION__);
+}
+
+static void io_error(void *user_ctx, const belle_sip_io_error_event_t *error)
+{
+ printf("%s\n", __FUNCTION__);
+}
+
+static const belle_sip_listener_callbacks_t callbacks = {
+ .process_dialog_terminated = dlg_terminated,
+ .process_io_error = io_error,
+ .process_request_event = req_event,
+ .process_response_event = res_event,
+ .process_timeout = timeout,
+ .process_transaction_terminated = trans_terminated,
+ .process_auth_requested = auth_request,
+ .listener_destroyed = lst_destroy,
+};
+
+
+static void *bg_loop(void *p) {
+ ASSERT_RET_NULL(main_loop, "library not initialised");
+
+ LOCK(&run_mutex);
+
+ while (true) {
+ pthread_mutex_unlock(&run_mutex);
+
+ pthread_mutex_lock(&main_mutex);
+
+ belle_sip_main_loop_run(main_loop);
+
+ pthread_mutex_unlock(&main_mutex);
+
+ pthread_mutex_lock(&run_mutex);
+ }
+
+ return NULL;
+}
+
+
+typedef struct {
+} sip_stack_lock_t;
+
+
+static sip_stack_lock_t lock_sip_stack(void) {
+ assert(main_loop);
+
+ pthread_mutex_lock(&run_mutex);
+
+ (void) write(wake_fds[1], "x", 1);
+
+ pthread_mutex_lock(&main_mutex);
+
+ return (sip_stack_lock_t) {};
+}
+
+
+static void unlock_sip_stack(void) {
+ pthread_mutex_unlock(&run_mutex);
+ pthread_mutex_unlock(&main_mutex);
+}
+
+
+static void auto_unlock_sip_stack(sip_stack_lock_t *s) {
+ unlock_sip_stack();
+}
+G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(sip_stack_lock_t, auto_unlock_sip_stack);
+#define LOCK_SIP_STACK() g_auto(sip_stack_lock_t) CONCAT(__l, __COUNTER__) __attribute__((unused)) = lock_sip_stack()
+
+
+static int pipe_read(void *p, unsigned int ev) {
+ char b;
+ (void) read(wake_fds[0], &b, 1);
+
+ belle_sip_main_loop_quit(main_loop);
+
+ return BELLE_SIP_CONTINUE;
+}
+
+
+static guint auth_info_hash(const void *p) {
+ const struct auth_info *h = p;
+ return g_str_hash(h->username) ^ g_str_hash(h->domain);
+}
+
+
+static gboolean auth_info_eq(const void *p, const void *q) {
+ const struct auth_info *a = p;
+ const struct auth_info *b = q;
+ return g_str_equal(a->username, b->username) && g_str_equal(a->domain, b->domain);
+}
+
+
+static guint listener_hash(const void *p) {
+ const struct listener *h = p;
+ return g_str_hash(h->username) ^ g_str_hash(h->domain);
+}
+
+
+static gboolean listener_eq(const void *p, const void *q) {
+ const struct listener *a = p;
+ const struct listener *b = q;
+ return g_str_equal(a->username, b->username) && g_str_equal(a->domain, b->domain);
+}
+
+
+static void auth_info_free(void *p) {
+ struct auth_info *h = p;
+ g_free(h->username);
+ g_free(h->domain);
+ g_free(h->passwd);
+ g_free(h);
+}
+
+
+bool bsw_init(void) {
+ ASSERT_RET_FALSE(!sipstack, "library already initialised");
+
+ auth_info = g_hash_table_new_full(auth_info_hash, auth_info_eq, auth_info_free, NULL);
+ listeners = g_hash_table_new(listener_hash, listener_eq);
+
+ bctbx_set_log_handler(log_handler);
+
+ sipstack = belle_sip_stack_new(NULL);
+ ASSERT_RET_FALSE(sipstack, "failed to initialise library");
+
+ int ret = pipe2(wake_fds, O_NONBLOCK); // XXX close at exit
+ ASSERT_RET_FALSE(ret == 0, "failed to create pipe");
+
+ main_loop = belle_sip_stack_get_main_loop(sipstack);
+ ASSERT_RET_FALSE(main_loop, "failed to create main loop");
+
+ belle_sip_source_t *src = belle_sip_socket_source_new(pipe_read, NULL,
+ wake_fds[0], BELLE_SIP_EVENT_READ, -1);
+ belle_sip_main_loop_add_source(main_loop, src);
+
+ pthread_t thr;
+ pthread_create(&thr, NULL, bg_loop, NULL);
+
+ return true;
+}
+
+
+void bsw_set_logger(int ll, int fd) {
+ ASSERT_RET(fd != -1, "invalid logging fd");
+
+ lock_sip_stack();
+
+ belle_sip_set_log_level(ll);
+
+ unlock_sip_stack();
+
+ LOCK(&log_mutex);
+
+ ASSERT_RET(log_fd == -1, "logger already set");
+ log_fd = fd;
+
+ for (unsigned int i = 0; i < logs.length; i++)
+ (void) write(fd, "1", 1);
+}
+
+
+bool bsw_get_log(char *o, size_t len) {
+ LOCK(&log_mutex);
+
+ char *s = g_queue_pop_head(&logs);
+
+ if (!s) {
+ pthread_mutex_unlock(&log_mutex);
+ return false;
+ }
+
+ g_strlcpy(o, s, len);
+
+ g_free(s);
+
+ return true;
+}
+
+
+belle_sip_provider_t *bsw_provider(const char *addr, int port, const char *proto) {
+ LOCK_SIP_STACK();
+
+ belle_sip_listening_point_t *lp =
+ belle_sip_stack_create_listening_point(sipstack, addr, port, proto);
+ ASSERT_RET_NULL(lp, "failed to create listening point");
+ belle_sip_provider_t *sip_provider = belle_sip_stack_create_provider(sipstack, lp);
+ ASSERT_RET_NULL(sip_provider, "failed to create provider");
+ belle_sip_listener_t *lstn = belle_sip_listener_create_from_callbacks(&callbacks, sip_provider);
+ ASSERT_RET_NULL(lstn, "failed to create listener");
+ belle_sip_provider_add_sip_listener(sip_provider, lstn);
+
+ return sip_provider;
+}
+
+
+void bsw_register(belle_sip_provider_t *sip_provider, int fd, const char *uri, const char *pw, int dur) {
+ LOCK_SIP_STACK();
+
+ belle_sip_uri_t *u = belle_sip_uri_parse(uri); // "sip:bench2user000005@guest02-snail.lab.sipwise.com"
+ ASSERT_RET(u, "failed to parse URI");
+
+ set_auth_info(belle_sip_uri_get_user(u), belle_sip_uri_get_host(u), pw);
+
+ belle_sip_uri_t *uo = BELLE_SIP_URI(belle_sip_object_clone(BELLE_SIP_OBJECT(u)));
+ ASSERT_RET(uo, "failed to clone URI");
+ belle_sip_uri_set_user(uo, NULL); // "sip:guest02-snail.lab.sipwise.com"
+
+ belle_sip_request_t *req = belle_sip_request_create(
+ uo,
+ "REGISTER",
+ belle_sip_provider_create_call_id(sip_provider),
+ belle_sip_header_cseq_create(1, "REGISTER"),
+ belle_sip_header_from_create2(uri, BELLE_SIP_RANDOM_TAG),
+ belle_sip_header_to_create2(uri, NULL),
+ belle_sip_header_via_new(),
+ 70);
+ ASSERT_RET(req, "failed to create request");
+
+ belle_sip_message_t *msg = BELLE_SIP_MESSAGE(req);
+ ASSERT_RET(msg, "failed to cast to message");
+
+ belle_sip_message_add_header(msg, BELLE_SIP_HEADER(belle_sip_header_expires_create(dur)));
+ belle_sip_message_add_header(msg, BELLE_SIP_HEADER(belle_sip_header_contact_new()));
+
+ belle_sip_client_transaction_t *trans = belle_sip_provider_create_client_transaction(sip_provider, req);
+ ASSERT_RET(trans, "failed to create transaction");
+
+ struct transaction *t = g_new0(struct transaction, 1);
+ t->response = reg_response;
+ t->response_waker = fd;
+
+ belle_sip_transaction_set_application_data(BELLE_SIP_TRANSACTION(trans), t);
+
+ int res = belle_sip_client_transaction_send_request_to(trans, NULL);
+ ASSERT_RET(res == 0, "failed to send request");
+}
+
+
+void bsw_listen(belle_sip_provider_t *sip_provider, int fd, const char *uri)
+{
+ LOCK_SIP_STACK();
+
+ belle_sip_uri_t *u = belle_sip_uri_parse(uri); // "sip:bench2user000005@guest02-snail.lab.sipwise.com"
+ ASSERT_RET(u, "failed to parse URI");
+
+ ua_listen(belle_sip_uri_get_user(u), belle_sip_uri_get_host(u), fd);
+}
+
+
+bool bsw_receive(belle_sip_provider_t *sip_provider, const char *uri, struct call_info *info)
+{
+ LOCK_SIP_STACK();
+
+ belle_sip_uri_t *u = belle_sip_uri_parse(uri); // "sip:bench2user000005@guest02-snail.lab.sipwise.com"
+ ASSERT_RET_FALSE(u, "failed to parse URI");
+
+ LOCK(&listen_mutex);
+
+ struct listener *l = get_listener(belle_sip_uri_get_user(u), belle_sip_uri_get_host(u));
+ if (!l)
+ return false;
+
+ if (l->calls.length == 0)
+ return false;
+
+ // transfer ownership to the external object
+ struct call *c = g_queue_pop_head(&l->calls);
+ info->call = c;
+
+ belle_sip_request_t *req = belle_sip_transaction_get_request(BELLE_SIP_TRANSACTION(c->strans));
+ ASSERT_RET_FALSE(req, "no request from transaction");
+
+ belle_sip_message_t *msg = BELLE_SIP_MESSAGE(req);
+ ASSERT_RET_FALSE(msg, "failed to cast to message");
+
+ const char *s = belle_sip_message_get_body(msg);
+ if (s)
+ g_strlcpy(info->body, s, sizeof(info->body));
+
+ belle_sip_header_content_type_t *ct = belle_sip_message_get_header_by_type(msg,
+ belle_sip_header_content_type_t);
+ if (ct) {
+ s = belle_sip_header_content_type_get_type(ct);
+ ASSERT_RET_FALSE(s, "failed to get content-type");
+ g_strlcpy(info->content_type, s, sizeof(info->content_type));
+ }
+
+ belle_sip_header_from_t *from = belle_sip_message_get_header_by_type(msg, belle_sip_header_from_t);
+ ASSERT_RET_FALSE(from, "no FROM header");
+
+ belle_sip_header_address_t *addr = BELLE_SIP_HEADER_ADDRESS(from);
+ ASSERT_RET_FALSE(addr, "no address in FROM header");
+
+ belle_sip_uri_t *uh = belle_sip_header_address_get_uri(addr);
+ ASSERT_RET_FALSE(uh, "no URI in FROM address");
+
+ const char *un = belle_sip_uri_get_user(uh);
+ const char *dm = belle_sip_uri_get_host(uh);
+ const char *tg = belle_sip_header_from_get_tag(from);
+
+ snprintf(info->from_addr, sizeof(info->from_addr), "%s@%s", un, dm);
+ if (tg)
+ g_strlcpy(info->from_tag, tg, sizeof(info->from_tag));
+
+ belle_sip_header_to_t *to = belle_sip_message_get_header_by_type(msg, belle_sip_header_to_t);
+ ASSERT_RET_FALSE(to, "no TO header");
+
+ addr = BELLE_SIP_HEADER_ADDRESS(to);
+ ASSERT_RET_FALSE(addr, "no address in TO header");
+
+ uh = belle_sip_header_address_get_uri(addr);
+ ASSERT_RET_FALSE(uh, "no URI in TO address");
+
+ un = belle_sip_uri_get_user(uh);
+ dm = belle_sip_uri_get_host(uh);
+ tg = belle_sip_header_to_get_tag(to);
+
+ snprintf(info->to_addr, sizeof(info->to_addr), "%s@%s", un, dm);
+ if (tg)
+ g_strlcpy(info->to_tag, tg, sizeof(info->to_tag));
+
+ belle_sip_header_call_id_t *cidh = belle_sip_message_get_header_by_type(msg, belle_sip_header_call_id_t);
+ ASSERT_RET_FALSE(cidh, "no CALL ID header");
+ const char *cid = belle_sip_header_call_id_get_call_id(cidh);
+ ASSERT_RET_FALSE(cid, "empty CALL ID header");
+
+ g_strlcpy(info->call_id, cid, sizeof(info->call_id));
+
+ return true;
+}
+
+void bsw_call_destroy(struct call *c) {
+ LOCK_SIP_STACK();
+
+ ASSERT_RET(c->dlg, "no dialog associated with call");
+
+ belle_sip_dialog_set_application_data(c->dlg, NULL);
+ if (c->strans)
+ belle_sip_object_unref(c->strans);
+ if (c->ctrans)
+ belle_sip_object_unref(c->ctrans);
+ belle_sip_object_unref(c->dlg);
+
+ // XXX make sure info/pointers from pending invite transaction is removed
+
+ g_free(c);
+}
+
+
+static bool bsw_call_reply(struct call *c, int code, const char *sdp) {
+ if (code == 180) {
+ if (c->state != CS_INC_PENDING)
+ return false;
+ c->state = CS_INC_RINGING;
+ }
+ else if (code == 183) {
+ if (c->state != CS_INC_PENDING && c->state != CS_INC_RINGING)
+ return false;
+ c->state = CS_INC_MEDIA;
+ }
+ else if (code == 200) {
+ if (c->state != CS_INC_PENDING && c->state != CS_INC_RINGING && c->state != CS_INC_MEDIA)
+ return false;
+ c->state = CS_ESTABLISHED;
+ }
+ else if (code >= 400) {
+ if (c->state != CS_INC_PENDING && c->state != CS_INC_RINGING && c->state != CS_INC_MEDIA)
+ return false;
+
+ if (c->terminate_waker != -1)
+ write(c->terminate_waker, "1", 1);
+
+ c->state = CS_DEAD;
+ }
+ else
+ return false;
+
+ belle_sip_request_t *req = belle_sip_transaction_get_request(BELLE_SIP_TRANSACTION(c->strans));
+ ASSERT_RET_FALSE(req, "no request from transaction");
+
+ belle_sip_response_t *res = belle_sip_response_create_from_request(req, code);
+ ASSERT_RET_FALSE(res, "failed to create response");
+
+ belle_sip_message_t *msg = BELLE_SIP_MESSAGE(res);
+ ASSERT_RET_FALSE(msg, "failed to cast to message");
+
+ if (sdp) {
+ belle_sip_message_set_body(msg, sdp, strlen(sdp));
+ belle_sip_message_add_header(msg, BELLE_SIP_HEADER(belle_sip_header_content_type_create(
+ "application", "sdp")));
+ }
+
+ belle_sip_message_add_header(msg, BELLE_SIP_HEADER(belle_sip_header_contact_new()));
+
+ belle_sip_server_transaction_send_response(c->strans, res);
+
+ return true;
+}
+
+
+bool bsw_call_answer(struct call *c, int code, const char *sdp) {
+ LOCK_SIP_STACK();
+
+ ASSERT_RET_FALSE(c->sip_provider, "provider link missing");
+ ASSERT_RET_FALSE(c->dlg, "no dialog associated with call");
+ ASSERT_RET_FALSE(c->strans, "no transaction associated with call");
+
+ return bsw_call_reply(c, code, sdp);
+}
+
+
+bool bsw_call_finished(struct call *c, int fd) {
+ ASSERT_RET_FALSE(fd != -1, "invalid fd");
+
+ LOCK_SIP_STACK();
+
+ if (c->state == CS_DEAD)
+ return true;
+
+ c->terminate_waker = fd;
+
+ return false;
+}
+
+
+static bool bsw_call_bye(struct call *c) {
+ belle_sip_request_t *req = belle_sip_dialog_create_request(c->dlg, "BYE");
+ ASSERT_RET_FALSE(req, "failed to create request");
+
+ belle_sip_client_transaction_t *trans = belle_sip_provider_create_client_transaction(c->sip_provider,
+ req);
+ ASSERT_RET_FALSE(trans, "failed to create transaction");
+
+ belle_sip_client_transaction_send_request(trans);
+
+ if (c->terminate_waker != -1)
+ write(c->terminate_waker, "1", 1);
+
+ c->state = CS_DEAD;
+
+ return true;
+}
+
+
+static bool bsw_call_cancel(struct call *c) {
+ belle_sip_request_t *req = belle_sip_dialog_create_request(c->dlg, "BYE");
+ ASSERT_RET_FALSE(req, "failed to create request");
+
+ belle_sip_client_transaction_t *trans = belle_sip_provider_create_client_transaction(c->sip_provider,
+ req);
+ ASSERT_RET_FALSE(trans, "failed to create transaction");
+
+ belle_sip_client_transaction_send_request(trans);
+
+ if (c->terminate_waker != -1)
+ write(c->terminate_waker, "1", 1);
+
+ c->state = CS_DEAD;
+
+ return true;
+}
+
+
+bool bsw_call_terminate(struct call *c) {
+ LOCK_SIP_STACK();
+
+ ASSERT_RET_FALSE(c->sip_provider, "missing provider link");
+ ASSERT_RET_FALSE(c->dlg, "no dialog associated with call");
+
+ if (c->state == CS_DEAD)
+ return false;
+
+ switch (c->state) {
+ case CS_DEAD:
+ return false;
+
+ case CS_ESTABLISHED:
+ return bsw_call_bye(c);
+
+ case CS_OUT_PENDING:
+ case CS_OUT_RINGING:
+ case CS_OUT_MEDIA:
+ return bsw_call_cancel(c);
+
+ case CS_INC_PENDING:
+ case CS_INC_RINGING:
+ case CS_INC_MEDIA:
+ return bsw_call_reply(c, 407, NULL);
+ }
+
+ return false;
+}
+
+
+static void inv_response(struct transaction *t, int code,
+ const belle_sip_response_event_t *event,
+ belle_sip_response_t *res,
+ belle_sip_client_transaction_t *trans, belle_sip_provider_t *sip_provider)
+{
+ belle_sip_dialog_t *dlg = belle_sip_response_event_get_dialog(event);
+
+ belle_sip_message_t *msg = BELLE_SIP_MESSAGE(res);
+ ASSERT_RET(msg, "failed to cast to message");
+
+ struct call *c = t->call;
+
+ if (code == 407 && !t->auth)
+ auth_resp(t, event, trans, sip_provider);
+ else if (code == 180) {
+ if (c && c->state == CS_OUT_PENDING) {
+ c->state = CS_OUT_RINGING;
+ write(t->response_waker, "1", 1);
+ }
+
+ if (c->ctrans)
+ belle_sip_object_unref(c->ctrans);
+ c->ctrans = trans;
+ belle_sip_object_ref(trans);
+ }
+ else if (code == 183) {
+ if (c && (c->state == CS_OUT_PENDING || c->state == CS_OUT_RINGING)) {
+ c->state = CS_OUT_MEDIA;
+ write(t->response_waker, "1", 1);
+ }
+
+ if (c->ctrans)
+ belle_sip_object_unref(c->ctrans);
+ c->ctrans = trans;
+ belle_sip_object_ref(trans);
+ }
+ else if (code == 200) {
+ if (!t->done) {
+ t->done = true;
+ write(t->response_waker, "1", 1);
+
+ if (c)
+ c->state = CS_ESTABLISHED;
+ }
+
+
+ if (c->ctrans)
+ belle_sip_object_unref(c->ctrans);
+ c->ctrans = trans;
+ belle_sip_object_ref(trans);
+
+ ASSERT_RET(dlg, "no dialog");
+
+ belle_sip_request_t *ack = belle_sip_dialog_create_ack(dlg,
+ belle_sip_dialog_get_local_seq_number(dlg));
+ ASSERT_RET(ack, "failed to create ACK");
+
+ belle_sip_message_remove_header(BELLE_SIP_MESSAGE(ack), "Proxy-Authorization");
+
+ belle_sip_dialog_send_ack(dlg, ack);
+
+ t->call = NULL;
+ }
+ else if (code >= 400) {
+ if (!t->done) {
+ t->done = true;
+ write(t->response_waker, "0", 1);
+
+ if (c)
+ c->state = CS_DEAD;
+ }
+
+ //t->info = NULL;
+ t->call = NULL;
+ }
+ else {
+ // XXX ?
+ }
+}
+
+
+struct call *bsw_call_create(belle_sip_provider_t *sip_provider, struct call_info *info, int fd)
+{
+ ASSERT_RET_NULL(fd != -1, "invalid fd");
+
+ struct call *c = g_new0(struct call, 1);
+ c->terminate_waker = -1;
+ c->state = CS_OUT_PENDING;
+
+ struct transaction *t = g_new0(struct transaction, 1);
+ t->response = inv_response;
+ t->response_waker = fd;
+ t->call = c;
+
+ LOCK_SIP_STACK();
+
+ c->sip_provider = sip_provider;
+ belle_sip_object_ref(sip_provider);
+
+ belle_sip_uri_t *tu = belle_sip_uri_parse(info->to_addr);
+ ASSERT_RET_NULL(tu, "failed to parse URI");
+
+ belle_sip_header_call_id_t *cidh = belle_sip_header_call_id_new();
+ ASSERT_RET_NULL(cidh, "failed to create CALL ID header");
+ belle_sip_header_call_id_set_call_id(cidh, info->call_id);
+
+ belle_sip_header_from_t *fh = belle_sip_header_from_create2(info->from_addr, info->from_tag); // "sip:...@..."
+ ASSERT_RET_NULL(fh, "failed to create FROM header");
+
+ belle_sip_header_address_t *ta = belle_sip_header_address_create(NULL, tu);
+ ASSERT_RET_NULL(ta, "failed to create TO address");
+
+ belle_sip_header_to_t *th = belle_sip_header_to_create(ta, NULL);
+ ASSERT_RET_NULL(th, "failed to create TO header");
+
+ belle_sip_request_t *req = belle_sip_request_create(tu, "INVITE",
+ cidh, belle_sip_header_cseq_create(20, "INVITE"),
+ fh, th, belle_sip_header_via_new(), 70);
+
+ belle_sip_message_t *msg = BELLE_SIP_MESSAGE(req);
+
+ belle_sip_message_add_header(msg, BELLE_SIP_HEADER(belle_sip_header_contact_new()));
+
+ size_t body_len = strlen(info->body);
+ if (body_len) {
+ belle_sip_message_set_body(msg, info->body, strlen(info->body));
+ belle_sip_message_add_header(msg, BELLE_SIP_HEADER(belle_sip_header_content_type_create(
+ "application", "sdp")));
+ }
+
+ belle_sip_client_transaction_t *trans = belle_sip_provider_create_client_transaction(sip_provider, req);
+
+ belle_sip_dialog_t *dlg = belle_sip_provider_create_dialog(sip_provider, BELLE_SIP_TRANSACTION(trans));
+ ASSERT_RET_NULL(dlg, "failed to create dialog");
+
+ c->dlg = dlg;
+ belle_sip_object_ref(dlg);
+
+ belle_sip_transaction_set_application_data(BELLE_SIP_TRANSACTION(trans), t);
+ belle_sip_dialog_set_application_data(dlg, c);
+
+ c->ctrans = trans;
+ belle_sip_object_ref(trans);
+
+ belle_sip_client_transaction_send_request(trans);
+
+ return c;
+}
+
+
+int bsw_call_wait(struct call *call, struct call_info *info) {
+ int ret;
+
+ LOCK_SIP_STACK();
+
+ ASSERT_RET_VAL(call->ctrans, -1, "no client transaction");
+
+ belle_sip_response_t *res = belle_sip_transaction_get_response(BELLE_SIP_TRANSACTION(call->ctrans));
+ ASSERT_RET_VAL(res, -1, "no response from transaction");
+
+ belle_sip_message_t *msg = BELLE_SIP_MESSAGE(res);
+ ASSERT_RET_VAL(msg, -1, "failed to cast to message");
+
+ const char *s = belle_sip_message_get_body(msg);
+ if (s)
+ g_strlcpy(info->body, s, sizeof(info->body));
+
+ belle_sip_header_content_type_t *ct = belle_sip_message_get_header_by_type(msg,
+ belle_sip_header_content_type_t);
+ if (ct) {
+ s = belle_sip_header_content_type_get_type(ct);
+ ASSERT_RET_VAL(s, -1, "failed to get content-type");
+ g_strlcpy(info->content_type, s, sizeof(info->content_type));
+ }
+
+ switch (call->state) {
+ case CS_DEAD:
+ ret = 400;
+ break;
+
+ case CS_OUT_PENDING:
+ ret = 100;
+ break;
+
+ case CS_OUT_RINGING:
+ ret = 180;
+ break;
+
+ case CS_OUT_MEDIA:
+ ret = 183;
+ break;
+
+ case CS_ESTABLISHED:
+ ret = 200;
+ break;
+
+ default:
+ ret = 500;
+ break;
+ }
+
+ return ret;
+}