sippy 2.2.3__tar.gz → 2.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. {sippy-2.2.3 → sippy-2.4.0}/MANIFEST.in +1 -0
  2. {sippy-2.2.3/sippy.egg-info → sippy-2.4.0}/PKG-INFO +5 -1
  3. sippy-2.4.0/requirements.txt +7 -0
  4. {sippy-2.2.3 → sippy-2.4.0}/setup.py +6 -3
  5. {sippy-2.2.3 → sippy-2.4.0}/sippy/B2B/Route.py +75 -22
  6. {sippy-2.2.3 → sippy-2.4.0}/sippy/B2B/SimpleAPI.py +7 -3
  7. {sippy-2.2.3 → sippy-2.4.0}/sippy/MyConfigParser.py +7 -3
  8. sippy-2.4.0/sippy/Rtp/Codecs/G711.py +42 -0
  9. sippy-2.4.0/sippy/Rtp/Codecs/G722.py +53 -0
  10. sippy-2.4.0/sippy/Rtp/Codecs/GenCodec.py +18 -0
  11. sippy-2.4.0/sippy/Rtp/Conf.py +8 -0
  12. sippy-2.4.0/sippy/Rtp/Core/AudioChunk.py +54 -0
  13. sippy-2.4.0/sippy/Rtp/EPoint.py +244 -0
  14. sippy-2.4.0/sippy/Rtp/Handlers.py +14 -0
  15. sippy-2.4.0/sippy/Rtp/Ingest.py +79 -0
  16. sippy-2.4.0/sippy/Rtp/OutputWorker.py +204 -0
  17. sippy-2.4.0/sippy/Rtp/Params.py +104 -0
  18. sippy-2.4.0/sippy/Rtp/Singletons.py +61 -0
  19. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/Client/udp.py +2 -5
  20. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/Session/side.py +2 -3
  21. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/session.py +18 -10
  22. {sippy-2.2.3 → sippy-2.4.0}/sippy/Security/SipNonce.py +26 -12
  23. sippy-2.4.0/sippy/Security/__init__.py +0 -0
  24. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipAuthorization.py +3 -16
  25. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipCSeq.py +10 -0
  26. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipCallId.py +2 -4
  27. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipCiscoGUID.py +3 -6
  28. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipFrom.py +2 -4
  29. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipVia.py +2 -4
  30. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipWWWAuthenticate.py +2 -9
  31. sippy-2.4.0/sippy/Time/__init__.py +0 -0
  32. {sippy-2.2.3 → sippy-2.4.0}/sippy/UA.py +2 -5
  33. sippy-2.4.0/sippy/UI/Auth.py +64 -0
  34. sippy-2.4.0/sippy/UI/Controller.py +380 -0
  35. sippy-2.4.0/sippy/UI/Lib.py +248 -0
  36. sippy-2.4.0/sippy/UI/SystemConfiguration.py +5 -0
  37. sippy-2.4.0/sippy/UI/__init__.py +0 -0
  38. sippy-2.4.0/sippy/UI/static/active_calls.js +17 -0
  39. sippy-2.4.0/sippy/UI/static/config_box.css +29 -0
  40. sippy-2.4.0/sippy/UI/static/lib.js +146 -0
  41. sippy-2.4.0/sippy/UI/static/login.css +103 -0
  42. sippy-2.4.0/sippy/UI/static/ui.css +50 -0
  43. sippy-2.4.0/sippy/UI/templates/login.html +81 -0
  44. sippy-2.4.0/sippy/UI/templates/redir2login.html +14 -0
  45. sippy-2.4.0/sippy/UaStateConnected.py +311 -0
  46. {sippy-2.2.3 → sippy-2.4.0}/sippy/UaStateGeneric.py +26 -0
  47. {sippy-2.2.3 → sippy-2.4.0}/sippy/UacStateIdle.py +2 -1
  48. {sippy-2.2.3 → sippy-2.4.0}/sippy/UacStateUpdating.py +32 -25
  49. sippy-2.4.0/sippy/UasStateGeneric.py +79 -0
  50. {sippy-2.2.3 → sippy-2.4.0}/sippy/UasStateIdle.py +2 -1
  51. sippy-2.4.0/sippy/UasStateProgressing.py +84 -0
  52. sippy-2.4.0/sippy/UasStateRinging.py +64 -0
  53. sippy-2.4.0/sippy/UasStateTrying.py +89 -0
  54. sippy-2.4.0/sippy/UasStateUpdating.py +103 -0
  55. {sippy-2.2.3 → sippy-2.4.0}/sippy/b2bua.py +7 -1
  56. {sippy-2.2.3 → sippy-2.4.0/sippy.egg-info}/PKG-INFO +5 -1
  57. {sippy-2.2.3 → sippy-2.4.0}/sippy.egg-info/SOURCES.txt +31 -0
  58. sippy-2.4.0/sippy.egg-info/requires.txt +7 -0
  59. sippy-2.4.0/tests/__init__.py +0 -0
  60. sippy-2.4.0/tests/test_HashOracle.py +30 -0
  61. sippy-2.4.0/tests/test_RtpEPoint.py +61 -0
  62. sippy-2.4.0/tests/test_RtpParams.py +66 -0
  63. sippy-2.2.3/requirements.txt +0 -3
  64. sippy-2.2.3/sippy/UaStateConnected.py +0 -296
  65. sippy-2.2.3/sippy/UasStateRinging.py +0 -144
  66. sippy-2.2.3/sippy/UasStateTrying.py +0 -137
  67. sippy-2.2.3/sippy/UasStateUpdating.py +0 -128
  68. sippy-2.2.3/sippy.egg-info/requires.txt +0 -3
  69. {sippy-2.2.3 → sippy-2.4.0}/LICENSE +0 -0
  70. {sippy-2.2.3 → sippy-2.4.0}/README.md +0 -0
  71. {sippy-2.2.3 → sippy-2.4.0}/pyproject.toml +0 -0
  72. {sippy-2.2.3 → sippy-2.4.0}/setup.cfg +0 -0
  73. {sippy-2.2.3 → sippy-2.4.0}/sippy/B2B/States.py +0 -0
  74. {sippy-2.2.3 → sippy-2.4.0}/sippy/B2B/Transforms.py +0 -0
  75. {sippy-2.2.3 → sippy-2.4.0}/sippy/B2B/__init__.py +0 -0
  76. {sippy-2.2.3 → sippy-2.4.0}/sippy/CCEvents.py +0 -0
  77. {sippy-2.2.3 → sippy-2.4.0}/sippy/CLIManager.py +0 -0
  78. {sippy-2.2.3 → sippy-2.4.0}/sippy/Core/EventDispatcher.py +0 -0
  79. {sippy-2.2.3 → sippy-2.4.0}/sippy/Core/Exceptions.py +0 -0
  80. {sippy-2.2.3 → sippy-2.4.0}/sippy/Core/__init__.py +0 -0
  81. {sippy-2.2.3 → sippy-2.4.0}/sippy/ESipHeaderCSV.py +0 -0
  82. {sippy-2.2.3 → sippy-2.4.0}/sippy/ESipHeaderIgnore.py +0 -0
  83. {sippy-2.2.3 → sippy-2.4.0}/sippy/Exceptions/RtpProxyError.py +0 -0
  84. {sippy-2.2.3 → sippy-2.4.0}/sippy/Exceptions/SdpParseError.py +0 -0
  85. {sippy-2.2.3 → sippy-2.4.0}/sippy/Exceptions/SipHandlingError.py +0 -0
  86. {sippy-2.2.3 → sippy-2.4.0}/sippy/Exceptions/SipParseError.py +0 -0
  87. {sippy-2.2.3 → sippy-2.4.0}/sippy/Exceptions/__init__.py +0 -0
  88. {sippy-2.2.3 → sippy-2.4.0}/sippy/External_command.py +0 -0
  89. {sippy-2.2.3 → sippy-2.4.0}/sippy/FakeAccounting.py +0 -0
  90. {sippy-2.2.3 → sippy-2.4.0}/sippy/Math/__init__.py +0 -0
  91. {sippy-2.2.3 → sippy-2.4.0}/sippy/Math/recfilter.py +0 -0
  92. {sippy-2.2.3 → sippy-2.4.0}/sippy/MsgBody.py +0 -0
  93. {sippy-2.2.3 → sippy-2.4.0}/sippy/MultipartMixBody.py +0 -0
  94. {sippy-2.2.3 → sippy-2.4.0}/sippy/Network_server.py +0 -0
  95. {sippy-2.2.3 → sippy-2.4.0}/sippy/RadiusAccounting.py +0 -0
  96. {sippy-2.2.3 → sippy-2.4.0}/sippy/RadiusAuthorisation.py +0 -0
  97. {sippy-2.2.3 → sippy-2.4.0}/sippy/Radius_client.py +0 -0
  98. {sippy-2.2.3/sippy/Rtp_proxy/Client/Worker → sippy-2.4.0/sippy/Rtp/Codecs}/__init__.py +0 -0
  99. {sippy-2.2.3/sippy/Security → sippy-2.4.0/sippy/Rtp/Core}/__init__.py +0 -0
  100. {sippy-2.2.3/sippy/Time → sippy-2.4.0/sippy/Rtp}/__init__.py +0 -0
  101. {sippy-2.2.3/tests → sippy-2.4.0/sippy/Rtp_proxy/Client/Worker}/__init__.py +0 -0
  102. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/Client/Worker/external.py +0 -0
  103. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/Client/Worker/internal.py +0 -0
  104. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/Client/__init__.py +0 -0
  105. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/Client/internal.py +0 -0
  106. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/Client/local.py +0 -0
  107. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/Client/net.py +0 -0
  108. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/Client/stream.py +0 -0
  109. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/Cmd/__init__.py +0 -0
  110. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/Cmd/sequencer.py +0 -0
  111. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/Session/__init__.py +0 -0
  112. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/Session/subcommand.py +0 -0
  113. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/Session/subcommand_ice.py +0 -0
  114. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/Session/update.py +0 -0
  115. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/Session/webrtc.py +0 -0
  116. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/__init__.py +0 -0
  117. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/client.py +0 -0
  118. {sippy-2.2.3 → sippy-2.4.0}/sippy/Rtp_proxy/cmd.py +0 -0
  119. {sippy-2.2.3 → sippy-2.4.0}/sippy/SdpBody.py +0 -0
  120. {sippy-2.2.3 → sippy-2.4.0}/sippy/SdpConnecton.py +0 -0
  121. {sippy-2.2.3 → sippy-2.4.0}/sippy/SdpGeneric.py +0 -0
  122. {sippy-2.2.3 → sippy-2.4.0}/sippy/SdpMedia.py +0 -0
  123. {sippy-2.2.3 → sippy-2.4.0}/sippy/SdpMediaDescription.py +0 -0
  124. {sippy-2.2.3 → sippy-2.4.0}/sippy/SdpOrigin.py +0 -0
  125. {sippy-2.2.3 → sippy-2.4.0}/sippy/Signal.py +0 -0
  126. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipAddress.py +0 -0
  127. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipAddressHF.py +0 -0
  128. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipAllow.py +0 -0
  129. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipAlso.py +0 -0
  130. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipCCDiversion.py +0 -0
  131. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipConf.py +0 -0
  132. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipContact.py +0 -0
  133. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipContentLength.py +0 -0
  134. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipContentType.py +0 -0
  135. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipDiversion.py +0 -0
  136. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipExpires.py +0 -0
  137. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipGenericHF.py +0 -0
  138. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipHeader.py +0 -0
  139. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipLogger.py +0 -0
  140. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipMaxForwards.py +0 -0
  141. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipMsg.py +0 -0
  142. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipNumericHF.py +0 -0
  143. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipPAssertedIdentity.py +0 -0
  144. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipPath.py +0 -0
  145. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipProxyAuthenticate.py +0 -0
  146. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipProxyAuthorization.py +0 -0
  147. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipRAck.py +0 -0
  148. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipRSeq.py +0 -0
  149. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipReason.py +0 -0
  150. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipRecordRoute.py +0 -0
  151. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipReferTo.py +0 -0
  152. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipReferredBy.py +0 -0
  153. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipRegistrationAgent.py +0 -0
  154. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipReplaces.py +0 -0
  155. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipRequest.py +0 -0
  156. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipResponse.py +0 -0
  157. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipRoute.py +0 -0
  158. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipServer.py +0 -0
  159. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipSupported.py +0 -0
  160. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipTo.py +0 -0
  161. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipTransactionManager.py +0 -0
  162. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipURL.py +0 -0
  163. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipUserAgent.py +0 -0
  164. {sippy-2.2.3 → sippy-2.4.0}/sippy/SipWarning.py +0 -0
  165. {sippy-2.2.3 → sippy-2.4.0}/sippy/StatefulProxy.py +0 -0
  166. {sippy-2.2.3 → sippy-2.4.0}/sippy/Time/MonoTime.py +0 -0
  167. {sippy-2.2.3 → sippy-2.4.0}/sippy/Time/Timeout.py +0 -0
  168. {sippy-2.2.3 → sippy-2.4.0}/sippy/Time/clock_dtime.py +0 -0
  169. {sippy-2.2.3 → sippy-2.4.0}/sippy/UaStateDead.py +0 -0
  170. {sippy-2.2.3 → sippy-2.4.0}/sippy/UaStateDisconnected.py +0 -0
  171. {sippy-2.2.3 → sippy-2.4.0}/sippy/UaStateFailed.py +0 -0
  172. {sippy-2.2.3 → sippy-2.4.0}/sippy/UacStateCancelling.py +0 -0
  173. {sippy-2.2.3 → sippy-2.4.0}/sippy/UacStateRinging.py +0 -0
  174. {sippy-2.2.3 → sippy-2.4.0}/sippy/UacStateTrying.py +0 -0
  175. {sippy-2.2.3 → sippy-2.4.0}/sippy/Udp_server.py +0 -0
  176. {sippy-2.2.3 → sippy-2.4.0}/sippy/Wss_server.py +0 -0
  177. {sippy-2.2.3 → sippy-2.4.0}/sippy/XMPP_server.py +0 -0
  178. {sippy-2.2.3 → sippy-2.4.0}/sippy/__init__.py +0 -0
  179. {sippy-2.2.3 → sippy-2.4.0}/sippy/b2bua_simple.py +0 -0
  180. {sippy-2.2.3 → sippy-2.4.0}/sippy/dictionary +0 -0
  181. {sippy-2.2.3 → sippy-2.4.0}/sippy/misc.py +0 -0
  182. {sippy-2.2.3 → sippy-2.4.0}/sippy.egg-info/dependency_links.txt +0 -0
  183. {sippy-2.2.3 → sippy-2.4.0}/sippy.egg-info/entry_points.txt +0 -0
  184. {sippy-2.2.3 → sippy-2.4.0}/sippy.egg-info/top_level.txt +0 -0
  185. {sippy-2.2.3 → sippy-2.4.0}/tests/test_B2BRoute.py +0 -0
  186. {sippy-2.2.3 → sippy-2.4.0}/tests/test_B2BTransforms.py +0 -0
  187. {sippy-2.2.3 → sippy-2.4.0}/tests/test_Multipart.py +0 -0
  188. {sippy-2.2.3 → sippy-2.4.0}/tests/test_SdbBody.py +0 -0
  189. {sippy-2.2.3 → sippy-2.4.0}/tests/test_Udp_server.py +0 -0
@@ -2,3 +2,4 @@ include README.md
2
2
  include requirements.txt
3
3
  include sippy/dictionary
4
4
  recursive-include sippy/UI/static *
5
+ recursive-include sippy/UI/templates *
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sippy
3
- Version: 2.2.3
3
+ Version: 2.4.0
4
4
  Summary: RFC3261 SIP Stack and Back-to-Back User Agent (B2BUA)
5
5
  Home-page: http://www.b2bua.org/
6
6
  Author: Sippy Software, Inc.
@@ -13,8 +13,12 @@ Classifier: Programming Language :: Python
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: ElPeriodic>=1.1
16
+ Requires-Dist: rtpsynth>=1.3.0
17
+ Requires-Dist: g722>=1.2.3
16
18
  Requires-Dist: pycryptodome
17
19
  Requires-Dist: websockets
20
+ Requires-Dist: flask
21
+ Requires-Dist: flask-login
18
22
  Dynamic: author
19
23
  Dynamic: author-email
20
24
  Dynamic: classifier
@@ -0,0 +1,7 @@
1
+ ElPeriodic>=1.1
2
+ rtpsynth>=1.3.0
3
+ g722>=1.2.3
4
+ pycryptodome
5
+ websockets
6
+ flask
7
+ flask-login
@@ -1,19 +1,22 @@
1
1
  #!/usr/bin/env python
2
2
  from setuptools import setup, find_packages
3
3
 
4
- requirements = [x.strip() for x in open("requirements.txt", "r").readlines()]
4
+ requirements = [
5
+ line.strip()
6
+ for line in open("requirements.txt", "r", encoding="utf-8").readlines()
7
+ if line.strip() and not line.lstrip().startswith("#")
8
+ ]
5
9
 
6
10
  with open("README.md", "r") as fh:
7
11
  long_description = fh.read()
8
12
 
9
13
  setup(
10
14
  name = "sippy",
11
- version = "2.2.3",
15
+ version = "2.4.0",
12
16
  packages = find_packages(),
13
17
 
14
18
  install_requires = requirements,
15
19
  include_package_data=True,
16
- test_suite = 'tests',
17
20
 
18
21
  entry_points = {
19
22
  'console_scripts': [
@@ -40,32 +40,69 @@ DST_SIP_UA = 'sip-ua'
40
40
  DST_WSS_UA = 'wss-ua'
41
41
 
42
42
  class B2BRoute(object):
43
+ crt_set = False
44
+ params = None
45
+ extra_headers = None
43
46
  rnum = None
47
+ _ui_cats = {'auth': 'Outbound Digest Authentication', 'net': 'Network', 'lim': 'Limits',
48
+ 'num': 'Calling / Called Numbers', 'acct': 'Accounting'}
49
+ _ui_hints = {}
50
+ # _ui_hints['rnum'] = {'hidden': True}
44
51
  addrinfo = None
45
- cld = None
46
- cld_set = False
47
- hostport = None
48
- hostonly = None
49
- credit_time = None
50
- crt_set = False
51
- expires = None
52
- no_progress_expires = None
53
- no_reply_expires = None
54
- forward_on_fail = False
55
- user = None
56
- passw = None
52
+ # _ui_hints['addrinfo'] = {'readonly': True, 'category': _ui_cats['net'], 'hidden': True}
53
+ hostonly:str = None
54
+ # _ui_hints['hostonly'] = {'readonly': True, 'category': _ui_cats['net'], 'hidden': True}
55
+ ainfo = None
56
+ # _ui_hints['ainfo'] = {'readonly': True, 'category': _ui_cats['net'], 'hidden': True}
57
+ hostport:str = None
58
+ _ui_hints['hostport'] = {'name': 'Destination', 'description': 'Destination host:port',
59
+ 'category': _ui_cats['net']}
60
+ user:str = None
61
+ _ui_hints['user'] = {'name': 'SIP Username', 'category': _ui_cats['auth'],
62
+ 'active': lambda o: not o.pass_auth}
63
+ passw:str = None
64
+ _ui_hints['passw'] = {'name': 'SIP Password', 'category': _ui_cats['auth'], 'password': True,
65
+ 'active': lambda o: not o.pass_auth}
57
66
  pass_auth = True
58
- cli = None
67
+ _ui_hints['pass_auth'] = {'name': 'Mode', 'category': _ui_cats['auth'],
68
+ 'values': {False: 'Normal ("UAC")', True: 'Pass-through ("Proxy")'},}
69
+ _cld:str = None
70
+ _ui_hints['cld'] = {'name': 'Called Party (CLD)', 'description': 'Called party number',
71
+ 'category': _ui_cats['num'], 'type': str}
72
+ cld_set = False
73
+ _ui_hints['cld_set'] = {'name': 'CLD Mode', 'description': 'Controls outbound CLD',
74
+ 'category': _ui_cats['num'], 'active': lambda o: not o.cld,
75
+ 'values': lambda o: {True: 'Erase', False: 'Pass-through'}
76
+ if not o.cld else {True: 'Replace'}}
77
+ _cli:str = None
78
+ _ui_hints['cli'] = {'name': 'Calling Party (CLI)', 'description': 'Caller number',
79
+ 'category': _ui_cats['num'], 'type': str}
59
80
  cli_set = False
60
- params = None
61
- ainfo = None
62
- extra_headers = None
81
+ _ui_hints['cli_set'] = {'name': 'CLI Mode', 'description': 'Controls outbound CLI', 'category':
82
+ _ui_cats['num'], 'active': lambda o: not o.cli,
83
+ 'values': lambda o: {True: 'Erase', False: 'Pass-through'}
84
+ if not o.cli else {True: 'Replace'}}
85
+ credit_time:float = None
86
+ _ui_hints['credit_time'] = {'name': 'Credit Time', 'description': 'Allowed call duration, in seconds',
87
+ 'category': _ui_cats['lim']}
88
+ expires:float = None
89
+ _ui_hints['expires'] = {'name': 'Route Expiry', 'description': 'Time to expire the route, in seconds',
90
+ 'category': _ui_cats['lim']}
91
+ no_progress_expires:float = None
92
+ _ui_hints['no_progress_expires'] = {'name': 'No Progress Timeout', 'category': _ui_cats['lim'],
93
+ 'description': 'Time to expire the route if no 180 or 183 provisional response received, in seconds'}
94
+ no_reply_expires:float = None
95
+ _ui_hints['no_reply_expires'] = {'name': 'No Reply Timeout', 'category': _ui_cats['lim'],
96
+ 'description': 'Time to expire the route if no provisional response received, in seconds'}
97
+ forward_on_fail = False
98
+ _ui_hints['forward_on_fail'] = {'name': 'Failed Calls', 'category': _ui_cats['acct'],
99
+ 'description': 'Emit accounting on SIP failures'}
63
100
 
64
101
  def __init__(self, sroute = None, cself = None):
65
102
  if cself != None:
66
103
  self.rnum = cself.rnum
67
104
  self.addrinfo = cself.addrinfo
68
- self.cld = cself.cld
105
+ self._cld = cself.cld
69
106
  self.cld_set = cself.cld_set
70
107
  self.hostport = cself.hostport
71
108
  self.hostonly = cself.hostonly
@@ -78,7 +115,7 @@ class B2BRoute(object):
78
115
  self.user = cself.user
79
116
  self.passw = cself.passw
80
117
  self.pass_auth = cself.pass_auth
81
- self.cli = cself.cli
118
+ self._cli = cself.cli
82
119
  self.cli_set = cself.cli_set
83
120
  self.params = dict(cself.params)
84
121
  self.ainfo = cself.ainfo
@@ -93,7 +130,6 @@ class B2BRoute(object):
93
130
  # as opposed to the Routing:host, which means that CLD should be obtained
94
131
  # from the incoming call leg.
95
132
  self.cld = None
96
- self.cld_set = True
97
133
  else:
98
134
  self.hostport = route[0]
99
135
  if not self.hostport.startswith('['):
@@ -147,7 +183,6 @@ class B2BRoute(object):
147
183
  self.cli = v
148
184
  if len(self.cli) == 0:
149
185
  self.cli = None
150
- self.cli_set = True
151
186
  elif a == 'cnam':
152
187
  caller_name = unquote(v)
153
188
  if len(caller_name) == 0:
@@ -175,9 +210,9 @@ class B2BRoute(object):
175
210
  pass_headers, max_credit_time):
176
211
  self.rnum = rnum
177
212
  if not self.cld_set:
178
- self.cld = default_cld
213
+ self._cld = default_cld
179
214
  if not self.cli_set:
180
- self.cli = default_cli
215
+ self._cli = default_cli
181
216
  if not self.crt_set:
182
217
  self.crt_set = default_credit_time
183
218
  if 'gt' in self.params:
@@ -210,3 +245,21 @@ class B2BRoute(object):
210
245
  if af == AF_INET6:
211
246
  return ((('[%s]' % amatch[0], amatch[1]), same_af))
212
247
  return (((amatch[0], amatch[1]), same_af))
248
+
249
+ @property
250
+ def cli(self):
251
+ return self._cli
252
+
253
+ @cli.setter
254
+ def cli(self, value):
255
+ self._cli = value
256
+ self.cli_set = True
257
+
258
+ @property
259
+ def cld(self):
260
+ return self._cld
261
+
262
+ @cld.setter
263
+ def cld(self, value):
264
+ self._cld = value
265
+ self.cld_set = True
@@ -116,10 +116,11 @@ class B2BSimpleAPI(CLIConnectionManager):
116
116
  clim.send('OK\n')
117
117
  return False
118
118
  if cmd == 'r':
119
- if len(args) != 1:
120
- clim.send('ERROR: syntax error: r [<id>]\n')
119
+ if len(args) not in (1, 2):
120
+ clim.send('ERROR: syntax error: r <id> [<media-index>]\n')
121
121
  return False
122
122
  idx = int(args[0])
123
+ media_index = int(args[1]) if len(args) == 2 else 0
123
124
  dlist = [x for x in ccm.ccmap if x.id == idx]
124
125
  if len(dlist) == 0:
125
126
  clim.send('ERROR: no call with id of %d has been found\n' % idx)
@@ -128,9 +129,12 @@ class B2BSimpleAPI(CLIConnectionManager):
128
129
  if not cc.proxied:
129
130
  continue
130
131
  if cc.state == CCStateConnected:
131
- cc.disconnect(MonoTime().getOffsetCopy(-60), origin = 'media_timeout')
132
+ cc.disconnect(MonoTime().getOffsetCopy(-60), origin = 'media_timeout',
133
+ media_index = media_index)
132
134
  continue
133
135
  if cc.state == CCStateARComplete:
136
+ if cc.rtp_proxy_session is not None:
137
+ cc.rtp_proxy_session.media_timeout_index = media_index
134
138
  cc.uaO.disconnect(MonoTime().getOffsetCopy(-60), origin = 'media_timeout')
135
139
  continue
136
140
  clim.send('OK\n')
@@ -104,9 +104,13 @@ SUPPORTED_OPTIONS = { \
104
104
  '"host:port:cert_file:key_file", where "cert_file" ' \
105
105
  '/ "key_file" are paths to the TLS certificate ' \
106
106
  'and key file respectively in the X.509 PEM format'),
107
- 'pre_auth_proc': ('S', 'internal routine to be executed before authentication '\
108
- 'is being processed. E.g. "HDR2Xattrs[X-foo-hdr]."'), \
109
- 'xmpp_b2bua_id': ('I', 'ID passed to the XMPP socket server')}
107
+ 'pre_auth_proc': ('S', 'internal routine to be executed before authentication '\
108
+ 'is being processed. E.g. "HDR2Xattrs[X-foo-hdr]."'), \
109
+ 'xmpp_b2bua_id': ('I', 'ID passed to the XMPP socket server'), \
110
+ 'ui': ('B', 'enable Web management UI'), \
111
+ 'uiparams': ('S', 'Semicolon-separated list of UI parameters corresponding to ' \
112
+ 'the flask\'s run() routine'), \
113
+ }
110
114
 
111
115
  class MyConfigParser(RawConfigParser):
112
116
  _default_section = None
@@ -0,0 +1,42 @@
1
+ from array import array
2
+
3
+ from rtpsynth.RtpUtils import linear2ulaw, ulaw2linear
4
+
5
+ from ..Core.AudioChunk import AudioChunk, pcm16_from
6
+ from .GenCodec import GenCodec
7
+
8
+ class G711Codec(GenCodec):
9
+ ptype = 0 # G.711u
10
+ ename = 'PCMU'
11
+ chunk_cls = AudioChunk
12
+
13
+ def encode(self, chunk):
14
+ pcm16 = pcm16_from(chunk.audio)
15
+ return linear2ulaw(pcm16)
16
+
17
+ def make_chunk(self, pcm16: array, srate: int):
18
+ return self.chunk_cls(pcm16, srate)
19
+
20
+ def decode(self, ulaw_bytes:bytes, resample:bool=True, sample_rate:int=None):
21
+ pcm16 = ulaw2linear(ulaw_bytes)
22
+ if sample_rate is None:
23
+ sample_rate = self.out_srate
24
+ chunk = self.make_chunk(pcm16, self.srate)
25
+ if resample and sample_rate != self.srate:
26
+ chunk.resample(sample_rate)
27
+ return chunk
28
+
29
+ def e2d_frames(self, enframes:int, out_srate:int=None):
30
+ if out_srate is None:
31
+ out_srate = self.out_srate
32
+ assert out_srate % self.srate == 0
33
+ return enframes * out_srate // self.srate
34
+
35
+ def d2e_frames(self, dnframes:int, in_srate:int=None):
36
+ if in_srate is None:
37
+ in_srate = self.out_srate
38
+ assert in_srate % self.srate == 0
39
+ return dnframes * self.srate // in_srate
40
+
41
+ def silence(self, nframes:int):
42
+ return b'\xff' * nframes
@@ -0,0 +1,53 @@
1
+ from array import array
2
+
3
+ from G722 import G722
4
+
5
+ from ..Core.AudioChunk import AudioChunk, pcm16_from
6
+ from .GenCodec import GenCodec
7
+
8
+ class G722Codec(GenCodec):
9
+ codec:G722
10
+ srate:int = 8000
11
+ default_br:int = 64000
12
+ ptype:int = 9 # G.722
13
+ ename:str = 'G722' # encoding name
14
+ chunk_cls = AudioChunk
15
+
16
+ def __init__(self):
17
+ super().__init__()
18
+ self.codec = G722(self.srate, self.default_br, use_numpy=False)
19
+
20
+ def encode(self, chunk):
21
+ pcm16 = pcm16_from(chunk.audio)
22
+ return self.codec.encode(pcm16)
23
+
24
+ def make_chunk(self, pcm16: array, srate: int):
25
+ return self.chunk_cls(pcm16, srate)
26
+
27
+ def decode(self, audio_enc:bytes, resample:bool=True, sample_rate:int=None):
28
+ pcm16 = array('h', self.codec.decode(audio_enc))
29
+ if sample_rate is None:
30
+ sample_rate = self.out_srate
31
+ chunk = self.make_chunk(pcm16, self.srate)
32
+ if resample and sample_rate != self.srate:
33
+ chunk.resample(sample_rate)
34
+ return chunk
35
+
36
+ def silence(self, nframes:int):
37
+ pcm16 = array('h', [0]) * self.e2d_frames(nframes)
38
+ return self.encode(pcm16)
39
+
40
+ def e2d_frames(self, enframes:int, out_srate:int=None):
41
+ #assert out_srate % self.srate == 0
42
+ if out_srate is None:
43
+ out_srate = self.out_srate
44
+ return enframes * (1 if self.srate == 8000 else 2) * out_srate // self.srate
45
+
46
+ def d2e_frames(self, dnframes:int, in_srate:int=None):
47
+ #assert in_srate % self.srate == 0
48
+ if in_srate is None:
49
+ in_srate = self.out_srate
50
+ return dnframes * self.srate // ((1 if self.srate == 8000 else 2) * in_srate)
51
+
52
+ def e2t(self, frames:int) -> float:
53
+ return (frames * 8) / self.default_br
@@ -0,0 +1,18 @@
1
+ class GenCodec():
2
+ srate:int = 8000 # sample rate
3
+ out_srate:int # sample rate (output)
4
+ crate:int = 8000 # clock rate
5
+ ptype:int # payload type
6
+ ename:str # encoding name
7
+
8
+ def __init__(self):
9
+ assert self.ptype is not None and self.ename is not None
10
+ self.out_srate = self.srate
11
+
12
+ @classmethod
13
+ def rtpmap(cls):
14
+ assert all(hasattr(cls, attr) for attr in ('ptype', 'ename'))
15
+ return f'rtpmap:{cls.ptype} {cls.ename}/{cls.crate}'
16
+
17
+ def e2t(self, frames:int) -> float:
18
+ return frames / self.srate
@@ -0,0 +1,8 @@
1
+ from typing import Optional
2
+
3
+ from sippy.Network_server import RTP_port_allocator
4
+
5
+ class RTPConf():
6
+ palloc: RTP_port_allocator
7
+ def __init__(self, min_port=None, max_port=None):
8
+ self.palloc = RTP_port_allocator(min_port, max_port)
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from array import array
4
+ from typing import Union
5
+ import sys
6
+
7
+ from rtpsynth.RtpUtils import resample_linear
8
+
9
+ PCM16Like = Union[array, bytes, bytearray, memoryview]
10
+
11
+ def pcm16_from(data: PCM16Like) -> array:
12
+ if isinstance(data, array):
13
+ if data.typecode != 'h':
14
+ raise TypeError("PCM16 array must have typecode 'h'")
15
+ return data
16
+ else:
17
+ mv = memoryview(data)
18
+ if mv.format != 'h':
19
+ mv = mv.cast('h')
20
+ pcm = array('h', mv)
21
+ if sys.byteorder == 'big':
22
+ pcm.byteswap()
23
+ return pcm
24
+
25
+ def pcm16_to_bytes(pcm: array) -> bytes:
26
+ if sys.byteorder == 'big':
27
+ pcm = array('h', pcm)
28
+ pcm.byteswap()
29
+ return pcm.tobytes()
30
+
31
+ class AudioChunk():
32
+ debug: bool = False
33
+ samplerate: int
34
+ audio: array
35
+ rtime: float | None = None
36
+ def __init__(self, audio: PCM16Like, samplerate:int):
37
+ self.audio = pcm16_from(audio)
38
+ self.samplerate = samplerate
39
+
40
+ def resample(self, sample_rate:int):
41
+ assert sample_rate != self.samplerate
42
+ self.audio = resample_linear(self.audio, self.samplerate, sample_rate)
43
+ self.samplerate = sample_rate
44
+ return self
45
+
46
+ def duration(self):
47
+ return len(self.audio) / self.samplerate
48
+
49
+ def to_bytes(self) -> bytes:
50
+ return pcm16_to_bytes(self.audio)
51
+
52
+ @property
53
+ def nframes(self) -> int:
54
+ return len(self.audio)
@@ -0,0 +1,244 @@
1
+ from typing import Optional, Tuple
2
+ from uuid import uuid4, UUID
3
+ from threading import Lock
4
+ import errno
5
+ import weakref
6
+
7
+ from rtpsynth.RtpServer import RtpQueueFullError, RtpServer
8
+ from sippy.Time.MonoTime import MonoTime
9
+
10
+ from .Core.AudioChunk import AudioChunk
11
+ from .Handlers import RTPHandlers
12
+ from .Params import RTPParams
13
+ from .Conf import RTPConf
14
+ from .Singletons import acquire_rtp_server, release_rtp_server
15
+
16
+ class RTPEPoint():
17
+ debug: bool = False
18
+ id: UUID
19
+ dl_file = None
20
+ firstframe = True
21
+ writer = None
22
+ rserv = None
23
+ rtp_params:RTPParams
24
+ state_lock: Lock
25
+ handlers: RTPHandlers
26
+ _rtp_server: Optional[RtpServer]
27
+ def __init__(self, rc:RTPConf, rtp_params:RTPParams, audio_in:callable,
28
+ handlers:RTPHandlers=None):
29
+ self.id = uuid4()
30
+ self.rtp_params = rtp_params
31
+ self.handlers = handlers or RTPHandlers()
32
+ self._palloc = rc.palloc
33
+ self._rtp_server = None
34
+ self.state_lock = Lock()
35
+ self.rsess = self.make_rtp_instream(rtp_params, audio_in)
36
+ rserv_opts = self.make_udp_server_opts(rtp_params)
37
+ self.rserv = self.make_udp_server(rserv_opts)
38
+ if self.rtp_params.rtp_target is not None:
39
+ self.writer_setup()
40
+
41
+ def make_writer(self, rtp_params:RTPParams):
42
+ return self.handlers.writer_cls(rtp_params)
43
+
44
+ def make_rtp_instream(self, rtp_params:RTPParams, audio_in:callable):
45
+ return self.handlers.rtp_instream_cls(rtp_params, audio_in)
46
+
47
+ def make_udp_server_opts(self, rtp_params:RTPParams):
48
+ palloc = self._palloc if rtp_params.rtp_lport == 0 else rtp_params.rtp_lport
49
+ return (rtp_params.rtp_laddr, palloc)
50
+
51
+ def _make_rtp_received_cb(self):
52
+ self_ref = weakref.ref(self)
53
+
54
+ def rtp_received_cb(data, address, rtime_ns=None):
55
+ self = self_ref()
56
+ if self is None:
57
+ return
58
+ return self.rtp_received(data, address, rtime_ns)
59
+
60
+ return rtp_received_cb
61
+
62
+ def _make_send_pkt_cb(self):
63
+ self_ref = weakref.ref(self)
64
+
65
+ def send_pkt_cb(pkt):
66
+ self = self_ref()
67
+ if self is None:
68
+ return
69
+ return self.send_pkt(pkt)
70
+
71
+ return send_pkt_cb
72
+
73
+ def make_udp_server(self, rserv_opts):
74
+ rtp_laddr, palloc = rserv_opts
75
+ server_cls = self.handlers.rtp_server_cls
76
+ rtp_server = acquire_rtp_server(server_cls)
77
+ channel = None
78
+ self._rtp_server = rtp_server
79
+ try:
80
+ channel = self._bind_channel(rtp_server, rtp_laddr, palloc)
81
+ target = self.rtp_params.rtp_target
82
+ if target is not None:
83
+ channel.set_target(target[0], target[1])
84
+ return channel
85
+ except Exception:
86
+ release_rtp_server(rtp_server)
87
+ self._rtp_server = None
88
+ if channel is not None:
89
+ channel.close()
90
+ raise
91
+
92
+ def _create_channel(self, rtp_server:RtpServer, bind_host:str, bind_port:int):
93
+ bind_family = self.rtp_params.rtp_family
94
+ ch_kwargs = dict(pkt_in=self._make_rtp_received_cb(), bind_host=bind_host, bind_port=bind_port)
95
+ ch = rtp_server.create_channel(bind_family=bind_family, **ch_kwargs)
96
+ self.rtp_params.rtp_lport = bind_port
97
+ return ch
98
+
99
+ def _bind_channel(self, rtp_server:RtpServer, rtp_laddr:str, palloc, preferred_port:int=None):
100
+ if preferred_port is not None:
101
+ try:
102
+ return self._create_channel(rtp_server, rtp_laddr, preferred_port)
103
+ except OSError as ex:
104
+ if ex.errno != errno.EADDRINUSE:
105
+ raise
106
+ if callable(palloc):
107
+ ntry = -1
108
+ while True:
109
+ ntry += 1
110
+ bind_port = int(palloc(ntry))
111
+ try:
112
+ return self._create_channel(rtp_server, rtp_laddr, bind_port)
113
+ except OSError as ex:
114
+ if ex.errno == errno.EADDRINUSE:
115
+ continue
116
+ raise
117
+ return self._create_channel(rtp_server, rtp_laddr, int(palloc))
118
+
119
+ def _swap_channel(self, old_channel, rtp_params:RTPParams):
120
+ with self.state_lock:
121
+ rtp_server = self._rtp_server
122
+ if rtp_server is None or self.rserv is not old_channel:
123
+ return
124
+ rtp_laddr, palloc = self.make_udp_server_opts(rtp_params)
125
+ preferred_port = old_channel.local_addr[1]
126
+ new_channel = self._bind_channel(rtp_server, rtp_laddr, palloc, preferred_port=preferred_port)
127
+ target = rtp_params.rtp_target
128
+ if target is not None:
129
+ new_channel.set_target(target[0], target[1])
130
+ with self.state_lock:
131
+ if self._rtp_server is None or self.rserv is not old_channel:
132
+ new_channel.close()
133
+ return
134
+ self.rserv = new_channel
135
+ old_channel.close()
136
+
137
+ def writer_setup(self):
138
+ assert self.writer is None
139
+ writer = self.make_writer(self.rtp_params)
140
+ writer.set_pkt_send_f(self._make_send_pkt_cb())
141
+ if self.dl_file is not None:
142
+ writer.enable_datalog(self.dl_file)
143
+ writer.start()
144
+ self.writer = writer
145
+
146
+ def send_pkt(self, pkt):
147
+ with self.state_lock:
148
+ channel = self.rserv
149
+ assert channel is not None
150
+ try:
151
+ channel.send_pkt(pkt)
152
+ except RtpQueueFullError:
153
+ return
154
+
155
+ def rtp_received(self, data, address, rtime_ns=None):
156
+ #self.dprint(f"RTP.Ingest.rtp_received: len(data) = {len(data)}")
157
+ if rtime_ns is None:
158
+ rtime = MonoTime().monot
159
+ else:
160
+ rtime = rtime_ns / 1_000_000_000.0
161
+ with self.state_lock:
162
+ target = self.rtp_params.rtp_target
163
+ if target is not None and address != target:
164
+ if self.debug:
165
+ print(f"InfernRTPIngest.rtp_received: address mismatch {address=} {self.rtp_params.rtp_target=}")
166
+ return
167
+ self.rsess.rtp_received(data, address, rtime)
168
+
169
+ def update(self, rtp_params:RTPParams):
170
+ old_writer = None
171
+ need_new_writer = False
172
+ target_changed = False
173
+ proto_changed = False
174
+ with self.state_lock:
175
+ target_changed = self.rtp_params.rtp_target != rtp_params.rtp_target
176
+ proto_changed = self.rtp_params.rtp_proto != rtp_params.rtp_proto
177
+ self.rtp_params.rtp_proto = rtp_params.rtp_proto
178
+ self.rtp_params.rtp_target = rtp_params.rtp_target
179
+ ptime_changed = self.rtp_params.out_ptime != rtp_params.out_ptime
180
+ self.rtp_params.out_ptime = rtp_params.out_ptime
181
+ self.rtp_params.out_sr = rtp_params.out_sr
182
+ self.rtp_params.codec = rtp_params.codec
183
+ channel = self.rserv
184
+ if self.rtp_params.rtp_target is None:
185
+ old_writer = self.writer
186
+ self.writer = None
187
+ elif self.writer is None:
188
+ need_new_writer = True
189
+ elif ptime_changed or proto_changed:
190
+ old_writer = self.writer
191
+ self.writer = None
192
+ need_new_writer = True
193
+ if old_writer is not None:
194
+ old_writer.end()
195
+ old_writer.join()
196
+ if proto_changed and channel is not None:
197
+ self._swap_channel(channel, rtp_params)
198
+ elif target_changed and channel is not None and self.rtp_params.rtp_target is not None:
199
+ target = self.rtp_params.rtp_target
200
+ channel.set_target(target[0], target[1])
201
+ if need_new_writer:
202
+ new_writer = self.make_writer(rtp_params)
203
+ new_writer.set_pkt_send_f(self._make_send_pkt_cb())
204
+ if self.dl_file is not None:
205
+ new_writer.enable_datalog(self.dl_file)
206
+ new_writer.start()
207
+ with self.state_lock:
208
+ if self.rserv is None or self.rtp_params.rtp_target is None:
209
+ # RTP endpoint has been shut down while swapping writer.
210
+ new_writer.end()
211
+ new_writer.join()
212
+ else:
213
+ self.writer = new_writer
214
+ self.rsess.stream_update()
215
+
216
+ def connect(self, ain:callable):
217
+ self.rsess.stream_connect(ain)
218
+
219
+ def shutdown(self):
220
+ with self.state_lock:
221
+ writer, channel, rtp_server = self.writer, self.rserv, self._rtp_server
222
+ self.writer, self.rserv, self._rtp_server = (None, None, None)
223
+ if writer is not None:
224
+ writer.end()
225
+ writer.join()
226
+ if channel is not None:
227
+ channel.close()
228
+ if rtp_server is not None:
229
+ release_rtp_server(rtp_server)
230
+
231
+ def __del__(self):
232
+ if self.debug:
233
+ print('RTP.EPoint.__del__')
234
+ self.shutdown()
235
+
236
+ def soundout(self, chunk:AudioChunk):
237
+ if self.firstframe:
238
+ if self.debug:
239
+ nframes = chunk.nframes if hasattr(chunk, 'nframes') else len(chunk.audio)
240
+ print(f'RTP.EPoint.soundout[{str(self.id)[:6]}]: {nframes}')
241
+ self.firstframe = False
242
+ with self.state_lock:
243
+ if self.writer is None: return
244
+ return self.writer.soundout(chunk)