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.
- {sippy-2.2.3/sippy.egg-info → sippy-2.3.0}/PKG-INFO +3 -1
- {sippy-2.2.3 → sippy-2.3.0}/requirements.txt +2 -0
- {sippy-2.2.3 → sippy-2.3.0}/setup.py +6 -3
- sippy-2.3.0/sippy/Rtp/Codecs/G711.py +42 -0
- sippy-2.3.0/sippy/Rtp/Codecs/G722.py +53 -0
- sippy-2.3.0/sippy/Rtp/Codecs/GenCodec.py +18 -0
- sippy-2.3.0/sippy/Rtp/Conf.py +8 -0
- sippy-2.3.0/sippy/Rtp/Core/AudioChunk.py +54 -0
- sippy-2.3.0/sippy/Rtp/EPoint.py +221 -0
- sippy-2.3.0/sippy/Rtp/Handlers.py +14 -0
- sippy-2.3.0/sippy/Rtp/Ingest.py +79 -0
- sippy-2.3.0/sippy/Rtp/OutputWorker.py +204 -0
- sippy-2.3.0/sippy/Rtp/Params.py +104 -0
- sippy-2.3.0/sippy/Rtp/Singletons.py +61 -0
- sippy-2.3.0/sippy/Security/__init__.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipCSeq.py +10 -0
- sippy-2.3.0/sippy/Time/__init__.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/UacStateIdle.py +2 -1
- {sippy-2.2.3 → sippy-2.3.0}/sippy/UasStateIdle.py +2 -1
- {sippy-2.2.3 → sippy-2.3.0/sippy.egg-info}/PKG-INFO +3 -1
- {sippy-2.2.3 → sippy-2.3.0}/sippy.egg-info/SOURCES.txt +15 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy.egg-info/requires.txt +2 -0
- sippy-2.3.0/tests/__init__.py +0 -0
- sippy-2.3.0/tests/test_RtpParams.py +66 -0
- {sippy-2.2.3 → sippy-2.3.0}/LICENSE +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/MANIFEST.in +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/README.md +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/pyproject.toml +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/setup.cfg +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/B2B/Route.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/B2B/SimpleAPI.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/B2B/States.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/B2B/Transforms.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/B2B/__init__.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/CCEvents.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/CLIManager.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Core/EventDispatcher.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Core/Exceptions.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Core/__init__.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/ESipHeaderCSV.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/ESipHeaderIgnore.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Exceptions/RtpProxyError.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Exceptions/SdpParseError.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Exceptions/SipHandlingError.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Exceptions/SipParseError.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Exceptions/__init__.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/External_command.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/FakeAccounting.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Math/__init__.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Math/recfilter.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/MsgBody.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/MultipartMixBody.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/MyConfigParser.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Network_server.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/RadiusAccounting.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/RadiusAuthorisation.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Radius_client.py +0 -0
- {sippy-2.2.3/sippy/Rtp_proxy/Client/Worker → sippy-2.3.0/sippy/Rtp/Codecs}/__init__.py +0 -0
- {sippy-2.2.3/sippy/Security → sippy-2.3.0/sippy/Rtp/Core}/__init__.py +0 -0
- {sippy-2.2.3/sippy/Time → sippy-2.3.0/sippy/Rtp}/__init__.py +0 -0
- {sippy-2.2.3/tests → sippy-2.3.0/sippy/Rtp_proxy/Client/Worker}/__init__.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Client/Worker/external.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Client/Worker/internal.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Client/__init__.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Client/internal.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Client/local.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Client/net.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Client/stream.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Client/udp.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Cmd/__init__.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Cmd/sequencer.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Session/__init__.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Session/side.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Session/subcommand.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Session/subcommand_ice.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Session/update.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/Session/webrtc.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/__init__.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/client.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/cmd.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Rtp_proxy/session.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SdpBody.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SdpConnecton.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SdpGeneric.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SdpMedia.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SdpMediaDescription.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SdpOrigin.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Security/SipNonce.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Signal.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipAddress.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipAddressHF.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipAllow.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipAlso.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipAuthorization.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipCCDiversion.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipCallId.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipCiscoGUID.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipConf.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipContact.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipContentLength.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipContentType.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipDiversion.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipExpires.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipFrom.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipGenericHF.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipHeader.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipLogger.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipMaxForwards.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipMsg.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipNumericHF.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipPAssertedIdentity.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipPath.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipProxyAuthenticate.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipProxyAuthorization.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipRAck.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipRSeq.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipReason.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipRecordRoute.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipReferTo.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipReferredBy.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipRegistrationAgent.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipReplaces.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipRequest.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipResponse.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipRoute.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipServer.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipSupported.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipTo.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipTransactionManager.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipURL.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipUserAgent.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipVia.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipWWWAuthenticate.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/SipWarning.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/StatefulProxy.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Time/MonoTime.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Time/Timeout.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Time/clock_dtime.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/UA.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/UaStateConnected.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/UaStateDead.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/UaStateDisconnected.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/UaStateFailed.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/UaStateGeneric.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/UacStateCancelling.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/UacStateRinging.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/UacStateTrying.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/UacStateUpdating.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/UasStateRinging.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/UasStateTrying.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/UasStateUpdating.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Udp_server.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/Wss_server.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/XMPP_server.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/__init__.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/b2bua.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/b2bua_simple.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/dictionary +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy/misc.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy.egg-info/dependency_links.txt +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy.egg-info/entry_points.txt +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/sippy.egg-info/top_level.txt +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/tests/test_B2BRoute.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/tests/test_B2BTransforms.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/tests/test_Multipart.py +0 -0
- {sippy-2.2.3 → sippy-2.3.0}/tests/test_SdbBody.py +0 -0
- {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.
|
|
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,19 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
from setuptools import setup, find_packages
|
|
3
3
|
|
|
4
|
-
requirements = [
|
|
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.
|
|
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,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)
|