sippy 2.2.3__tar.gz → 2.3.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 (167) hide show
  1. {sippy-2.2.3/sippy.egg-info → sippy-2.3.0}/PKG-INFO +3 -1
  2. {sippy-2.2.3 → sippy-2.3.0}/requirements.txt +2 -0
  3. {sippy-2.2.3 → sippy-2.3.0}/setup.py +6 -3
  4. sippy-2.3.0/sippy/Rtp/Codecs/G711.py +42 -0
  5. sippy-2.3.0/sippy/Rtp/Codecs/G722.py +53 -0
  6. sippy-2.3.0/sippy/Rtp/Codecs/GenCodec.py +18 -0
  7. sippy-2.3.0/sippy/Rtp/Conf.py +8 -0
  8. sippy-2.3.0/sippy/Rtp/Core/AudioChunk.py +54 -0
  9. sippy-2.3.0/sippy/Rtp/EPoint.py +221 -0
  10. sippy-2.3.0/sippy/Rtp/Handlers.py +14 -0
  11. sippy-2.3.0/sippy/Rtp/Ingest.py +79 -0
  12. sippy-2.3.0/sippy/Rtp/OutputWorker.py +204 -0
  13. sippy-2.3.0/sippy/Rtp/Params.py +104 -0
  14. sippy-2.3.0/sippy/Rtp/Singletons.py +61 -0
  15. sippy-2.3.0/sippy/Security/__init__.py +0 -0
  16. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipCSeq.py +10 -0
  17. sippy-2.3.0/sippy/Time/__init__.py +0 -0
  18. {sippy-2.2.3 → sippy-2.3.0}/sippy/UacStateIdle.py +2 -1
  19. {sippy-2.2.3 → sippy-2.3.0}/sippy/UasStateIdle.py +2 -1
  20. {sippy-2.2.3 → sippy-2.3.0/sippy.egg-info}/PKG-INFO +3 -1
  21. {sippy-2.2.3 → sippy-2.3.0}/sippy.egg-info/SOURCES.txt +15 -0
  22. {sippy-2.2.3 → sippy-2.3.0}/sippy.egg-info/requires.txt +2 -0
  23. sippy-2.3.0/tests/__init__.py +0 -0
  24. sippy-2.3.0/tests/test_RtpParams.py +66 -0
  25. {sippy-2.2.3 → sippy-2.3.0}/LICENSE +0 -0
  26. {sippy-2.2.3 → sippy-2.3.0}/MANIFEST.in +0 -0
  27. {sippy-2.2.3 → sippy-2.3.0}/README.md +0 -0
  28. {sippy-2.2.3 → sippy-2.3.0}/pyproject.toml +0 -0
  29. {sippy-2.2.3 → sippy-2.3.0}/setup.cfg +0 -0
  30. {sippy-2.2.3 → sippy-2.3.0}/sippy/B2B/Route.py +0 -0
  31. {sippy-2.2.3 → sippy-2.3.0}/sippy/B2B/SimpleAPI.py +0 -0
  32. {sippy-2.2.3 → sippy-2.3.0}/sippy/B2B/States.py +0 -0
  33. {sippy-2.2.3 → sippy-2.3.0}/sippy/B2B/Transforms.py +0 -0
  34. {sippy-2.2.3 → sippy-2.3.0}/sippy/B2B/__init__.py +0 -0
  35. {sippy-2.2.3 → sippy-2.3.0}/sippy/CCEvents.py +0 -0
  36. {sippy-2.2.3 → sippy-2.3.0}/sippy/CLIManager.py +0 -0
  37. {sippy-2.2.3 → sippy-2.3.0}/sippy/Core/EventDispatcher.py +0 -0
  38. {sippy-2.2.3 → sippy-2.3.0}/sippy/Core/Exceptions.py +0 -0
  39. {sippy-2.2.3 → sippy-2.3.0}/sippy/Core/__init__.py +0 -0
  40. {sippy-2.2.3 → sippy-2.3.0}/sippy/ESipHeaderCSV.py +0 -0
  41. {sippy-2.2.3 → sippy-2.3.0}/sippy/ESipHeaderIgnore.py +0 -0
  42. {sippy-2.2.3 → sippy-2.3.0}/sippy/Exceptions/RtpProxyError.py +0 -0
  43. {sippy-2.2.3 → sippy-2.3.0}/sippy/Exceptions/SdpParseError.py +0 -0
  44. {sippy-2.2.3 → sippy-2.3.0}/sippy/Exceptions/SipHandlingError.py +0 -0
  45. {sippy-2.2.3 → sippy-2.3.0}/sippy/Exceptions/SipParseError.py +0 -0
  46. {sippy-2.2.3 → sippy-2.3.0}/sippy/Exceptions/__init__.py +0 -0
  47. {sippy-2.2.3 → sippy-2.3.0}/sippy/External_command.py +0 -0
  48. {sippy-2.2.3 → sippy-2.3.0}/sippy/FakeAccounting.py +0 -0
  49. {sippy-2.2.3 → sippy-2.3.0}/sippy/Math/__init__.py +0 -0
  50. {sippy-2.2.3 → sippy-2.3.0}/sippy/Math/recfilter.py +0 -0
  51. {sippy-2.2.3 → sippy-2.3.0}/sippy/MsgBody.py +0 -0
  52. {sippy-2.2.3 → sippy-2.3.0}/sippy/MultipartMixBody.py +0 -0
  53. {sippy-2.2.3 → sippy-2.3.0}/sippy/MyConfigParser.py +0 -0
  54. {sippy-2.2.3 → sippy-2.3.0}/sippy/Network_server.py +0 -0
  55. {sippy-2.2.3 → sippy-2.3.0}/sippy/RadiusAccounting.py +0 -0
  56. {sippy-2.2.3 → sippy-2.3.0}/sippy/RadiusAuthorisation.py +0 -0
  57. {sippy-2.2.3 → sippy-2.3.0}/sippy/Radius_client.py +0 -0
  58. {sippy-2.2.3/sippy/Rtp_proxy/Client/Worker → sippy-2.3.0/sippy/Rtp/Codecs}/__init__.py +0 -0
  59. {sippy-2.2.3/sippy/Security → sippy-2.3.0/sippy/Rtp/Core}/__init__.py +0 -0
  60. {sippy-2.2.3/sippy/Time → sippy-2.3.0/sippy/Rtp}/__init__.py +0 -0
  61. {sippy-2.2.3/tests → sippy-2.3.0/sippy/Rtp_proxy/Client/Worker}/__init__.py +0 -0
  62. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Client/Worker/external.py +0 -0
  63. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Client/Worker/internal.py +0 -0
  64. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Client/__init__.py +0 -0
  65. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Client/internal.py +0 -0
  66. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Client/local.py +0 -0
  67. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Client/net.py +0 -0
  68. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Client/stream.py +0 -0
  69. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Client/udp.py +0 -0
  70. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Cmd/__init__.py +0 -0
  71. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Cmd/sequencer.py +0 -0
  72. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Session/__init__.py +0 -0
  73. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Session/side.py +0 -0
  74. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Session/subcommand.py +0 -0
  75. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Session/subcommand_ice.py +0 -0
  76. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Session/update.py +0 -0
  77. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Session/webrtc.py +0 -0
  78. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/__init__.py +0 -0
  79. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/client.py +0 -0
  80. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/cmd.py +0 -0
  81. {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/session.py +0 -0
  82. {sippy-2.2.3 → sippy-2.3.0}/sippy/SdpBody.py +0 -0
  83. {sippy-2.2.3 → sippy-2.3.0}/sippy/SdpConnecton.py +0 -0
  84. {sippy-2.2.3 → sippy-2.3.0}/sippy/SdpGeneric.py +0 -0
  85. {sippy-2.2.3 → sippy-2.3.0}/sippy/SdpMedia.py +0 -0
  86. {sippy-2.2.3 → sippy-2.3.0}/sippy/SdpMediaDescription.py +0 -0
  87. {sippy-2.2.3 → sippy-2.3.0}/sippy/SdpOrigin.py +0 -0
  88. {sippy-2.2.3 → sippy-2.3.0}/sippy/Security/SipNonce.py +0 -0
  89. {sippy-2.2.3 → sippy-2.3.0}/sippy/Signal.py +0 -0
  90. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipAddress.py +0 -0
  91. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipAddressHF.py +0 -0
  92. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipAllow.py +0 -0
  93. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipAlso.py +0 -0
  94. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipAuthorization.py +0 -0
  95. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipCCDiversion.py +0 -0
  96. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipCallId.py +0 -0
  97. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipCiscoGUID.py +0 -0
  98. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipConf.py +0 -0
  99. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipContact.py +0 -0
  100. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipContentLength.py +0 -0
  101. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipContentType.py +0 -0
  102. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipDiversion.py +0 -0
  103. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipExpires.py +0 -0
  104. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipFrom.py +0 -0
  105. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipGenericHF.py +0 -0
  106. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipHeader.py +0 -0
  107. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipLogger.py +0 -0
  108. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipMaxForwards.py +0 -0
  109. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipMsg.py +0 -0
  110. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipNumericHF.py +0 -0
  111. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipPAssertedIdentity.py +0 -0
  112. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipPath.py +0 -0
  113. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipProxyAuthenticate.py +0 -0
  114. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipProxyAuthorization.py +0 -0
  115. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipRAck.py +0 -0
  116. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipRSeq.py +0 -0
  117. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipReason.py +0 -0
  118. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipRecordRoute.py +0 -0
  119. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipReferTo.py +0 -0
  120. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipReferredBy.py +0 -0
  121. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipRegistrationAgent.py +0 -0
  122. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipReplaces.py +0 -0
  123. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipRequest.py +0 -0
  124. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipResponse.py +0 -0
  125. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipRoute.py +0 -0
  126. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipServer.py +0 -0
  127. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipSupported.py +0 -0
  128. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipTo.py +0 -0
  129. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipTransactionManager.py +0 -0
  130. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipURL.py +0 -0
  131. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipUserAgent.py +0 -0
  132. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipVia.py +0 -0
  133. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipWWWAuthenticate.py +0 -0
  134. {sippy-2.2.3 → sippy-2.3.0}/sippy/SipWarning.py +0 -0
  135. {sippy-2.2.3 → sippy-2.3.0}/sippy/StatefulProxy.py +0 -0
  136. {sippy-2.2.3 → sippy-2.3.0}/sippy/Time/MonoTime.py +0 -0
  137. {sippy-2.2.3 → sippy-2.3.0}/sippy/Time/Timeout.py +0 -0
  138. {sippy-2.2.3 → sippy-2.3.0}/sippy/Time/clock_dtime.py +0 -0
  139. {sippy-2.2.3 → sippy-2.3.0}/sippy/UA.py +0 -0
  140. {sippy-2.2.3 → sippy-2.3.0}/sippy/UaStateConnected.py +0 -0
  141. {sippy-2.2.3 → sippy-2.3.0}/sippy/UaStateDead.py +0 -0
  142. {sippy-2.2.3 → sippy-2.3.0}/sippy/UaStateDisconnected.py +0 -0
  143. {sippy-2.2.3 → sippy-2.3.0}/sippy/UaStateFailed.py +0 -0
  144. {sippy-2.2.3 → sippy-2.3.0}/sippy/UaStateGeneric.py +0 -0
  145. {sippy-2.2.3 → sippy-2.3.0}/sippy/UacStateCancelling.py +0 -0
  146. {sippy-2.2.3 → sippy-2.3.0}/sippy/UacStateRinging.py +0 -0
  147. {sippy-2.2.3 → sippy-2.3.0}/sippy/UacStateTrying.py +0 -0
  148. {sippy-2.2.3 → sippy-2.3.0}/sippy/UacStateUpdating.py +0 -0
  149. {sippy-2.2.3 → sippy-2.3.0}/sippy/UasStateRinging.py +0 -0
  150. {sippy-2.2.3 → sippy-2.3.0}/sippy/UasStateTrying.py +0 -0
  151. {sippy-2.2.3 → sippy-2.3.0}/sippy/UasStateUpdating.py +0 -0
  152. {sippy-2.2.3 → sippy-2.3.0}/sippy/Udp_server.py +0 -0
  153. {sippy-2.2.3 → sippy-2.3.0}/sippy/Wss_server.py +0 -0
  154. {sippy-2.2.3 → sippy-2.3.0}/sippy/XMPP_server.py +0 -0
  155. {sippy-2.2.3 → sippy-2.3.0}/sippy/__init__.py +0 -0
  156. {sippy-2.2.3 → sippy-2.3.0}/sippy/b2bua.py +0 -0
  157. {sippy-2.2.3 → sippy-2.3.0}/sippy/b2bua_simple.py +0 -0
  158. {sippy-2.2.3 → sippy-2.3.0}/sippy/dictionary +0 -0
  159. {sippy-2.2.3 → sippy-2.3.0}/sippy/misc.py +0 -0
  160. {sippy-2.2.3 → sippy-2.3.0}/sippy.egg-info/dependency_links.txt +0 -0
  161. {sippy-2.2.3 → sippy-2.3.0}/sippy.egg-info/entry_points.txt +0 -0
  162. {sippy-2.2.3 → sippy-2.3.0}/sippy.egg-info/top_level.txt +0 -0
  163. {sippy-2.2.3 → sippy-2.3.0}/tests/test_B2BRoute.py +0 -0
  164. {sippy-2.2.3 → sippy-2.3.0}/tests/test_B2BTransforms.py +0 -0
  165. {sippy-2.2.3 → sippy-2.3.0}/tests/test_Multipart.py +0 -0
  166. {sippy-2.2.3 → sippy-2.3.0}/tests/test_SdbBody.py +0 -0
  167. {sippy-2.2.3 → sippy-2.3.0}/tests/test_Udp_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sippy
3
- Version: 2.2.3
3
+ Version: 2.3.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,6 +13,8 @@ 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
18
20
  Dynamic: author
@@ -1,3 +1,5 @@
1
1
  ElPeriodic>=1.1
2
+ rtpsynth>=1.3.0
3
+ g722>=1.2.3
2
4
  pycryptodome
3
5
  websockets
@@ -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.3.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': [
@@ -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,221 @@
1
+ from typing import Optional, Tuple
2
+ from uuid import uuid4, UUID
3
+ from threading import Lock
4
+ import errno
5
+
6
+ from rtpsynth.RtpServer import RtpQueueFullError, RtpServer
7
+ from sippy.Time.MonoTime import MonoTime
8
+
9
+ from .Core.AudioChunk import AudioChunk
10
+ from .Handlers import RTPHandlers
11
+ from .Params import RTPParams
12
+ from .Conf import RTPConf
13
+ from .Singletons import acquire_rtp_server, release_rtp_server
14
+
15
+ class RTPEPoint():
16
+ debug: bool = False
17
+ id: UUID
18
+ dl_file = None
19
+ firstframe = True
20
+ rtp_params:RTPParams
21
+ state_lock: Lock
22
+ handlers: RTPHandlers
23
+ _rtp_server: Optional[RtpServer]
24
+ def __init__(self, rc:RTPConf, rtp_params:RTPParams, audio_in:callable,
25
+ handlers:RTPHandlers=None):
26
+ self.id = uuid4()
27
+ self.rtp_params = rtp_params
28
+ self.handlers = handlers or RTPHandlers()
29
+ self._palloc = rc.palloc
30
+ self._rtp_server = None
31
+ self.state_lock = Lock()
32
+ self.writer = None
33
+ self.rsess = self.make_rtp_instream(rtp_params, audio_in)
34
+ rserv_opts = self.make_udp_server_opts(rtp_params)
35
+ self.rserv = self.make_udp_server(rserv_opts)
36
+ if self.rtp_params.rtp_target is not None:
37
+ self.writer_setup()
38
+
39
+ def make_writer(self, rtp_params:RTPParams):
40
+ return self.handlers.writer_cls(rtp_params)
41
+
42
+ def make_rtp_instream(self, rtp_params:RTPParams, audio_in:callable):
43
+ return self.handlers.rtp_instream_cls(rtp_params, audio_in)
44
+
45
+ def make_udp_server_opts(self, rtp_params:RTPParams):
46
+ palloc = self._palloc if rtp_params.rtp_lport == 0 else rtp_params.rtp_lport
47
+ return (rtp_params.rtp_laddr, palloc)
48
+
49
+ def make_udp_server(self, rserv_opts):
50
+ rtp_laddr, palloc = rserv_opts
51
+ server_cls = self.handlers.rtp_server_cls
52
+ rtp_server = acquire_rtp_server(server_cls)
53
+ channel = None
54
+ self._rtp_server = rtp_server
55
+ try:
56
+ channel = self._bind_channel(rtp_server, rtp_laddr, palloc)
57
+ target = self.rtp_params.rtp_target
58
+ if target is not None:
59
+ channel.set_target(target[0], target[1])
60
+ return channel
61
+ except Exception:
62
+ release_rtp_server(rtp_server)
63
+ self._rtp_server = None
64
+ if channel is not None:
65
+ channel.close()
66
+ raise
67
+
68
+ def _create_channel(self, rtp_server:RtpServer, bind_host:str, bind_port:int):
69
+ bind_family = self.rtp_params.rtp_family
70
+ ch_kwargs = dict(pkt_in=self.rtp_received, bind_host=bind_host, bind_port=bind_port)
71
+ ch = rtp_server.create_channel(bind_family=bind_family, **ch_kwargs)
72
+ self.rtp_params.rtp_lport = bind_port
73
+ return ch
74
+
75
+ def _bind_channel(self, rtp_server:RtpServer, rtp_laddr:str, palloc, preferred_port:int=None):
76
+ if preferred_port is not None:
77
+ try:
78
+ return self._create_channel(rtp_server, rtp_laddr, preferred_port)
79
+ except OSError as ex:
80
+ if ex.errno != errno.EADDRINUSE:
81
+ raise
82
+ if callable(palloc):
83
+ ntry = -1
84
+ while True:
85
+ ntry += 1
86
+ bind_port = int(palloc(ntry))
87
+ try:
88
+ return self._create_channel(rtp_server, rtp_laddr, bind_port)
89
+ except OSError as ex:
90
+ if ex.errno == errno.EADDRINUSE:
91
+ continue
92
+ raise
93
+ return self._create_channel(rtp_server, rtp_laddr, int(palloc))
94
+
95
+ def _swap_channel(self, old_channel, rtp_params:RTPParams):
96
+ with self.state_lock:
97
+ rtp_server = self._rtp_server
98
+ if rtp_server is None or self.rserv is not old_channel:
99
+ return
100
+ rtp_laddr, palloc = self.make_udp_server_opts(rtp_params)
101
+ preferred_port = old_channel.local_addr[1]
102
+ new_channel = self._bind_channel(rtp_server, rtp_laddr, palloc, preferred_port=preferred_port)
103
+ target = rtp_params.rtp_target
104
+ if target is not None:
105
+ new_channel.set_target(target[0], target[1])
106
+ with self.state_lock:
107
+ if self._rtp_server is None or self.rserv is not old_channel:
108
+ new_channel.close()
109
+ return
110
+ self.rserv = new_channel
111
+ old_channel.close()
112
+
113
+ def writer_setup(self):
114
+ assert self.writer is None
115
+ writer = self.make_writer(self.rtp_params)
116
+ writer.set_pkt_send_f(self.send_pkt)
117
+ if self.dl_file is not None:
118
+ writer.enable_datalog(self.dl_file)
119
+ writer.start()
120
+ self.writer = writer
121
+
122
+ def send_pkt(self, pkt):
123
+ with self.state_lock:
124
+ channel = self.rserv
125
+ assert channel is not None
126
+ try:
127
+ channel.send_pkt(pkt)
128
+ except RtpQueueFullError:
129
+ return
130
+
131
+ def rtp_received(self, data, address, rtime_ns=None):
132
+ #self.dprint(f"RTP.Ingest.rtp_received: len(data) = {len(data)}")
133
+ if rtime_ns is None:
134
+ rtime = MonoTime().monot
135
+ else:
136
+ rtime = rtime_ns / 1_000_000_000.0
137
+ with self.state_lock:
138
+ target = self.rtp_params.rtp_target
139
+ if target is not None and address != target:
140
+ if self.debug:
141
+ print(f"InfernRTPIngest.rtp_received: address mismatch {address=} {self.rtp_params.rtp_target=}")
142
+ return
143
+ self.rsess.rtp_received(data, address, rtime)
144
+
145
+ def update(self, rtp_params:RTPParams):
146
+ old_writer = None
147
+ need_new_writer = False
148
+ target_changed = False
149
+ proto_changed = False
150
+ with self.state_lock:
151
+ target_changed = self.rtp_params.rtp_target != rtp_params.rtp_target
152
+ proto_changed = self.rtp_params.rtp_proto != rtp_params.rtp_proto
153
+ self.rtp_params.rtp_proto = rtp_params.rtp_proto
154
+ self.rtp_params.rtp_target = rtp_params.rtp_target
155
+ ptime_changed = self.rtp_params.out_ptime != rtp_params.out_ptime
156
+ self.rtp_params.out_ptime = rtp_params.out_ptime
157
+ self.rtp_params.out_sr = rtp_params.out_sr
158
+ self.rtp_params.codec = rtp_params.codec
159
+ channel = self.rserv
160
+ if self.rtp_params.rtp_target is None:
161
+ old_writer = self.writer
162
+ self.writer = None
163
+ elif self.writer is None:
164
+ need_new_writer = True
165
+ elif ptime_changed or proto_changed:
166
+ old_writer = self.writer
167
+ self.writer = None
168
+ need_new_writer = True
169
+ if old_writer is not None:
170
+ old_writer.end()
171
+ old_writer.join()
172
+ if proto_changed and channel is not None:
173
+ self._swap_channel(channel, rtp_params)
174
+ elif target_changed and channel is not None and self.rtp_params.rtp_target is not None:
175
+ target = self.rtp_params.rtp_target
176
+ channel.set_target(target[0], target[1])
177
+ if need_new_writer:
178
+ new_writer = self.make_writer(rtp_params)
179
+ new_writer.set_pkt_send_f(self.send_pkt)
180
+ if self.dl_file is not None:
181
+ new_writer.enable_datalog(self.dl_file)
182
+ new_writer.start()
183
+ with self.state_lock:
184
+ if self.rserv is None or self.rtp_params.rtp_target is None:
185
+ # RTP endpoint has been shut down while swapping writer.
186
+ new_writer.end()
187
+ new_writer.join()
188
+ else:
189
+ self.writer = new_writer
190
+ self.rsess.stream_update()
191
+
192
+ def connect(self, ain:callable):
193
+ self.rsess.stream_connect(ain)
194
+
195
+ def shutdown(self):
196
+ with self.state_lock:
197
+ writer, channel, rtp_server = self.writer, self.rserv, self._rtp_server
198
+ self.writer, self._rtp_server = (None, None)
199
+ if writer is not None:
200
+ writer.end()
201
+ writer.join()
202
+ with self.state_lock:
203
+ self.rserv = None
204
+ if channel is not None:
205
+ channel.close()
206
+ if rtp_server is not None:
207
+ release_rtp_server(rtp_server)
208
+
209
+ def __del__(self):
210
+ if self.debug:
211
+ print('RTP.EPoint.__del__')
212
+
213
+ def soundout(self, chunk:AudioChunk):
214
+ if self.firstframe:
215
+ if self.debug:
216
+ nframes = chunk.nframes if hasattr(chunk, 'nframes') else len(chunk.audio)
217
+ print(f'RTP.EPoint.soundout[{str(self.id)[:6]}]: {nframes}')
218
+ self.firstframe = False
219
+ with self.state_lock:
220
+ if self.writer is None: return
221
+ return self.writer.soundout(chunk)
@@ -0,0 +1,14 @@
1
+ from typing import Type, Any
2
+
3
+ from .OutputWorker import RTPOutputWorker
4
+ from .Ingest import RTPInStream
5
+ from sippy.Udp_server import Udp_server, Udp_server_opts
6
+ from rtpsynth.RtpServer import RtpServer
7
+
8
+
9
+ class RTPHandlers():
10
+ writer_cls: Type[Any] = RTPOutputWorker
11
+ rtp_instream_cls: Type[Any] = RTPInStream
12
+ rtp_server_cls: Type[Any] = RtpServer
13
+ udp_server_cls: Type[Any] = Udp_server
14
+ udp_server_opts_cls: Type[Any] = Udp_server_opts
@@ -0,0 +1,79 @@
1
+ from typing import Optional, Union
2
+ from threading import Lock
3
+
4
+ from rtpsynth.RtpJBuf import RtpJBuf, RTPFrameType, RTPParseError
5
+
6
+ from .Codecs.GenCodec import GenCodec
7
+ from .Params import RTPParams
8
+
9
+
10
+ class RTPInStream():
11
+ debug = False
12
+ out_buffer: bytes = b''
13
+ ob_rtime: float = 0.0
14
+ out_chunk_sz_samples: int
15
+ jb_size: int = 8
16
+ last_output_lseq: Optional[int] = None
17
+ codec: GenCodec
18
+ npkts: int = 0
19
+ def __init__(self, rtp_params:RTPParams, audio_in:callable):
20
+ self.jbuf = RtpJBuf(self.jb_size)
21
+ self.codec = rtp_params.codec()
22
+ self.codec.out_srate = rtp_params.out_sr
23
+ self.out_chunk_sz_samples = int(rtp_params.out_sr / 10) # 0.1s
24
+ self.ring_lock = Lock()
25
+ self.audio_in = audio_in
26
+
27
+ def rtp_received(self, data, address, rtime):
28
+ #self.dprint(f"RTP.Ingest.rtp_received: len(data) = {len(data)}")
29
+ with self.ring_lock:
30
+ self.pkt_proc(data, address, rtime)
31
+
32
+ def stream_update(self):
33
+ with self.ring_lock:
34
+ self.jbuf = RtpJBuf(self.jb_size)
35
+ self.last_output_lseq = None
36
+ return
37
+
38
+ def stream_connect(self, audio_in:callable):
39
+ with self.ring_lock:
40
+ self.audio_in = audio_in
41
+
42
+ def pkt_proc(self, data, address, rtime):
43
+ try:
44
+ res = self.jbuf.udp_in(data, rtime)
45
+ except RTPParseError as e:
46
+ self.dprint(f"RTPParseError: {e}")
47
+ return
48
+ self.npkts += 1
49
+ if self.npkts == 1:
50
+ self.dprint(f"address={address}, rtime={rtime}, len(data) = {len(data)} data={data[:40]}")
51
+ for pkt in res:
52
+ if pkt.content.type == RTPFrameType.ERS:
53
+ self.dprint(f"ERS packet received {pkt.content.lseq_start=}, {pkt.content.lseq_end=} {pkt.content.ts_diff=}")
54
+ self.last_output_lseq = pkt.content.lseq_end
55
+ rtp_data = self.codec.silence(pkt.content.ts_diff)
56
+ else:
57
+ if self.npkts < 10:
58
+ self.dprint(f"{pkt.content.frame.rtp.lseq=}")
59
+ assert self.last_output_lseq is None or pkt.content.frame.rtp.lseq == self.last_output_lseq + 1
60
+ self.last_output_lseq = pkt.content.frame.rtp.lseq
61
+ if self.npkts < 10:
62
+ self.dprint(f"{len(pkt.rtp_data)=}, {type(pkt.rtp_data)=}")
63
+ rtp_data = pkt.rtp_data
64
+ new_rtime = pkt.opaque - self.codec.e2t(len(self.out_buffer))
65
+ self.ob_rtime = new_rtime
66
+ self.out_buffer += rtp_data
67
+ while self.codec.e2d_frames(len(self.out_buffer)) >= self.out_chunk_sz_samples:
68
+ decode_samples = self.codec.d2e_frames(self.out_chunk_sz_samples)
69
+ chunk = self.codec.decode(self.out_buffer[:decode_samples])
70
+ chunk.rtime = self.ob_rtime
71
+ self.ob_rtime += self.codec.e2t(decode_samples)
72
+ self.out_buffer = self.out_buffer[decode_samples:]
73
+ self.audio_in(chunk)
74
+ if self.npkts < 10 and len(res) > 0:
75
+ self.dprint(f"{res=}")
76
+
77
+ def dprint(self, *args):
78
+ if self.debug:
79
+ print(*args)