diff --git a/poetry.lock b/poetry.lock index 4744b1b..274f16d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1712,6 +1712,19 @@ files = [ {file = "pyhumps-3.8.0.tar.gz", hash = "sha256:498026258f7ee1a8e447c2e28526c0bea9407f9a59c03260aee4bd6c04d681a3"}, ] +[[package]] +name = "pymedooze" +version = "0.1.0b3" +description = "Python wrapper for medooze media-server." +optional = false +python-versions = ">=3.9,<4.0" +files = [ + {file = "pymedooze-0.1.0b3.tar.gz", hash = "sha256:efe8918cbf217477a7244aa7686df132179a599341008d48f4fc7657b34138be"}, +] + +[package.dependencies] +semanticsdp = ">=0.1.0b3,<0.2.0" + [[package]] name = "pypika-tortoise" version = "0.1.6" @@ -1916,6 +1929,17 @@ files = [ httpx = ">=0.27.0,<0.28.0" python-dateutil = ">=2.8.2,<3.0.0" +[[package]] +name = "semanticsdp" +version = "0.1.0b5" +description = "Python port of medooze/semantic-sdp-js" +optional = false +python-versions = ">=3.9,<4.0" +files = [ + {file = "semanticsdp-0.1.0b5-py3-none-any.whl", hash = "sha256:ea6e9ab64754af7590c6afb3c082ac378df7b85a9cd4994384b9b41246a1e096"}, + {file = "semanticsdp-0.1.0b5.tar.gz", hash = "sha256:4658ad86aabb29be251913bfb5c4afecc312de79101aa3f129340f883e13648a"}, +] + [[package]] name = "setuptools" version = "69.1.1" @@ -2380,4 +2404,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "2f385ba951cf4e1ff99d1fca6e27d9ec8ca9278cb952331480b735dd60677fb1" +content-hash = "cc33c982864a4f552c392e441069bdb9b1f53f2ed515cbad4b6302b23dff4be1" diff --git a/pyproject.toml b/pyproject.toml index 705d68f..3479914 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,11 @@ fake-s3 = "1.0.2" types-protobuf = "^4.24.0.4" pytest-httpx = "^0.30.0" + +[tool.poetry.group.extras.dependencies] +pymedooze = "^0.1.0b3" +semanticsdp = ">=0.1.0b5" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/yepcord/voice_gateway/default_sdp.py b/yepcord/voice_gateway/default_sdp.py new file mode 100644 index 0000000..c6685c1 --- /dev/null +++ b/yepcord/voice_gateway/default_sdp.py @@ -0,0 +1,420 @@ +DEFAULT_SDP = { + "version": 0, + "streams": [], + "medias": [ + { + "id": "0", + "type": "audio", + "direction": "sendrecv", + "codecs": [ + { + "codec": "opus", + "type": 111, + "channels": 2, + "params": { + "minptime": "10", + "useinbandfec": "1" + }, + "rtcpfbs": [ + { + "id": "transport-cc" + } + ] + } + ], + "extensions": { + "1": "urn:ietf:params:rtp-hdrext:ssrc-audio-level", + "2": "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", + "3": "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", + "4": "urn:ietf:params:rtp-hdrext:sdes:mid" + } + }, + { + "id": "1", + "type": "video", + "direction": "sendrecv", + "codecs": [ + { + "codec": "VP8", + "type": 96, + "rtx": 97, + "rtcpfbs": [ + { + "id": "goog-remb" + }, + { + "id": "transport-cc" + }, + { + "id": "ccm", + "params": ["fir"] + }, + { + "id": "nack" + }, + { + "id": "nack", + "params": ["pli"] + } + ] + }, + { + "codec": "VP9", + "type": 98, + "rtx": 99, + "params": { + "profile-id": "0" + }, + "rtcpfbs": [ + { + "id": "goog-remb" + }, + { + "id": "transport-cc" + }, + { + "id": "ccm", + "params": ["fir"] + }, + { + "id": "nack" + }, + { + "id": "nack", + "params": ["pli"] + } + ] + }, + { + "codec": "VP9", + "type": 100, + "rtx": 101, + "params": { + "profile-id": "2" + }, + "rtcpfbs": [ + { + "id": "goog-remb" + }, + { + "id": "transport-cc" + }, + { + "id": "ccm", + "params": ["fir"] + }, + { + "id": "nack" + }, + { + "id": "nack", + "params": ["pli"] + } + ] + }, + { + "codec": "VP9", + "type": 102, + "rtx": 122, + "params": { + "profile-id": "1" + }, + "rtcpfbs": [ + { + "id": "goog-remb" + }, + { + "id": "transport-cc" + }, + { + "id": "ccm", + "params": ["fir"] + }, + { + "id": "nack" + }, + { + "id": "nack", + "params": ["pli"] + } + ] + }, + { + "codec": "H264", + "type": 127, + "rtx": 121, + "params": { + "level-asymmetry-allowed": "1", + "packetization-mode": "1", + "profile-level-id": "42001f" + }, + "rtcpfbs": [ + { + "id": "goog-remb" + }, + { + "id": "transport-cc" + }, + { + "id": "ccm", + "params": ["fir"] + }, + { + "id": "nack" + }, + { + "id": "nack", + "params": ["pli"] + } + ] + }, + { + "codec": "H264", + "type": 125, + "rtx": 107, + "params": { + "level-asymmetry-allowed": "1", + "packetization-mode": "0", + "profile-level-id": "42001f" + }, + "rtcpfbs": [ + { + "id": "goog-remb" + }, + { + "id": "transport-cc" + }, + { + "id": "ccm", + "params": ["fir"] + }, + { + "id": "nack" + }, + { + "id": "nack", + "params": ["pli"] + } + ] + }, + { + "codec": "H264", + "type": 108, + "rtx": 109, + "params": { + "level-asymmetry-allowed": "1", + "packetization-mode": "1", + "profile-level-id": "42e01f" + }, + "rtcpfbs": [ + { + "id": "goog-remb" + }, + { + "id": "transport-cc" + }, + { + "id": "ccm", + "params": ["fir"] + }, + { + "id": "nack" + }, + { + "id": "nack", + "params": ["pli"] + } + ] + }, + { + "codec": "H264", + "type": 124, + "rtx": 120, + "params": { + "level-asymmetry-allowed": "1", + "packetization-mode": "0", + "profile-level-id": "42e01f" + }, + "rtcpfbs": [ + { + "id": "goog-remb" + }, + { + "id": "transport-cc" + }, + { + "id": "ccm", + "params": ["fir"] + }, + { + "id": "nack" + }, + { + "id": "nack", + "params": ["pli"] + } + ] + }, + { + "codec": "H264", + "type": 123, + "rtx": 119, + "params": { + "level-asymmetry-allowed": "1", + "packetization-mode": "1", + "profile-level-id": "4d001f" + }, + "rtcpfbs": [ + { + "id": "goog-remb" + }, + { + "id": "transport-cc" + }, + { + "id": "ccm", + "params": ["fir"] + }, + { + "id": "nack" + }, + { + "id": "nack", + "params": ["pli"] + } + ] + }, + { + "codec": "H264", + "type": 35, + "rtx": 36, + "params": { + "level-asymmetry-allowed": "1", + "packetization-mode": "0", + "profile-level-id": "4d001f" + }, + "rtcpfbs": [ + { + "id": "goog-remb" + }, + { + "id": "transport-cc" + }, + { + "id": "ccm", + "params": ["fir"] + }, + { + "id": "nack" + }, + { + "id": "nack", + "params": ["pli"] + } + ] + }, + { + "codec": "H264", + "type": 37, + "rtx": 38, + "params": { + "level-asymmetry-allowed": "1", + "packetization-mode": "1", + "profile-level-id": "f4001f" + }, + "rtcpfbs": [ + { + "id": "goog-remb" + }, + { + "id": "transport-cc" + }, + { + "id": "ccm", + "params": ["fir"] + }, + { + "id": "nack" + }, + { + "id": "nack", + "params": ["pli"] + } + ] + }, + { + "codec": "H264", + "type": 39, + "rtx": 40, + "params": { + "level-asymmetry-allowed": "1", + "packetization-mode": "0", + "profile-level-id": "f4001f" + }, + "rtcpfbs": [ + { + "id": "goog-remb" + }, + { + "id": "transport-cc" + }, + { + "id": "ccm", + "params": ["fir"] + }, + { + "id": "nack" + }, + { + "id": "nack", + "params": ["pli"] + } + ] + }, + { + "codec": "H264", + "type": 114, + "rtx": 115, + "params": { + "level-asymmetry-allowed": "1", + "packetization-mode": "1", + "profile-level-id": "64001f" + }, + "rtcpfbs": [ + { + "id": "goog-remb" + }, + { + "id": "transport-cc" + }, + { + "id": "ccm", + "params": ["fir"] + }, + { + "id": "nack" + }, + { + "id": "nack", + "params": ["pli"] + } + ] + } + ], + "extensions": { + "2": "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", + "3": "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", + "4": "urn:ietf:params:rtp-hdrext:sdes:mid", + "5": "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", + "6": "http://www.webrtc.org/experiments/rtp-hdrext/video-content-type", + "7": "http://www.webrtc.org/experiments/rtp-hdrext/video-timing", + "8": "http://www.webrtc.org/experiments/rtp-hdrext/color-space", + "10": "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id", + "11": "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id", + "13": "urn:3gpp:video-orientation", + "14": "urn:ietf:params:rtp-hdrext:toffset" + } + } + ], + "candidates": [] +} diff --git a/yepcord/voice_gateway/gateway.py b/yepcord/voice_gateway/gateway.py index 9ceb797..d4c1580 100644 --- a/yepcord/voice_gateway/gateway.py +++ b/yepcord/voice_gateway/gateway.py @@ -6,10 +6,19 @@ from quart import Websocket from yepcord.yepcord.enums import VoiceGatewayOp +from .default_sdp import DEFAULT_SDP from .events import Event, ReadyEvent, SpeakingEvent, UdpSessionDescriptionEvent, RtcSessionDescriptionEvent from .schemas import SelectProtocol from ..gateway.utils import require_auth +try: + from semanticsdp import SDPInfo, DTLSInfo, Setup + from pymedooze import MediaServer + + _DISABLED = False +except ImportError: + _DISABLED = True + class GatewayClient: def __init__(self, ws: Websocket, gw: Gateway): @@ -23,6 +32,8 @@ def __init__(self, ws: Websocket, gw: Gateway): self.rtx_ssrc = 0 self.mode: Optional[str] = None self.key: Optional[bytes] = None + self.sdp: Optional[SDPInfo] = None + self.transport = None self._gw = gw @@ -37,11 +48,6 @@ async def handle_IDENTIFY(self, data: dict): if data["token"] != "idk_token": return await self.ws.close(4004) - # async with ClientSession() as sess: - # p = await sess.get("http://192.168.1.155:10000/getLocalPort") - # p = int(await p.text()) - # print(f"Got port {p}") - self.user_id = int(data["user_id"]) self.session_id = data["session_id"] self.guild_id = data["server_id"] @@ -54,9 +60,12 @@ async def handle_IDENTIFY(self, data: dict): self.rtx_ssrc = self._gw.ssrc self._gw.ssrc += 1 - port = 3791 # TODO: get port + self.sdp = SDPInfo.from_dict(DEFAULT_SDP) + self.sdp.dtls = DTLSInfo( + setup=Setup.ACTPASS, hash="sha-256", fingerprint=self._gw.endpoint.get_dtls_fingerprint() + ) - await self.esend(ReadyEvent(self.ssrc, self.video_ssrc, self.rtx_ssrc, port)) + await self.esend(ReadyEvent(self.ssrc, self.video_ssrc, self.rtx_ssrc, self._gw.endpoint.get_local_port())) @require_auth(4003) async def handle_HEARTBEAT(self, data: dict): @@ -71,8 +80,30 @@ async def handle_SELECT_PROTOCOL(self, data: dict): return await self.ws.close(4012) if d.protocol == "webrtc": - - answer = ... # TODO: generate answer + offer = SDPInfo.parse(f"m=audio\n{d.sdp}") + self.sdp.ice = offer.ice + self.sdp.dtls = offer.dtls + + self.transport = self._gw.endpoint.create_transport(self.sdp) + self.transport.set_remote_properties(self.sdp) + self.transport.set_local_properties(self.sdp) + + dtls = self.transport.get_local_dtls() + ice = self.transport.get_local_ice() + port = self._gw.endpoint.get_local_port() + fp = f"{dtls.hash} {dtls.fingerprint}" + candidate = self.transport.get_local_candidates()[0] + + answer = ( + f"m=audio {port} ICE/SDP\n" + + f"a=fingerprint:{fp}\n" + + f"c=IN IP4 127.0.0.1\n" + + f"a=rtcp:{port}\n" + + f"a=ice-ufrag:{ice.ufrag}\n" + + f"a=ice-pwd:{ice.pwd}\n" + + f"a=fingerprint:{fp}\n" + + f"a=candidate:1 1 {candidate.transport} {candidate.foundation} {candidate.address} {candidate.port} typ host\n" + ) await self.esend(RtcSessionDescriptionEvent(answer)) elif d.protocol == "udp": @@ -97,7 +128,14 @@ class Gateway: def __init__(self): self.ssrc = 1 + if not _DISABLED: + MediaServer.initialize() + MediaServer.set_port_range(3690, 3960) + self.endpoint = MediaServer.create_endpoint("127.0.0.1") + async def sendHello(self, ws: Websocket) -> None: + if _DISABLED: + return await ws.close(4005) client = GatewayClient(ws, self) setattr(ws, "_yepcord_client", client) await ws.send_json({"op": VoiceGatewayOp.HELLO, "d": {"v": 7, "heartbeat_interval": 13750}})