From becb4c922e6cef272980eb1318e69e87bb9ed0fc Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Tue, 14 Mar 2023 19:58:46 +0200 Subject: [PATCH 01/15] add voice gateway server --- run_all.py | 1 + src/gateway/events.py | 55 ++++++++++++++++++++++++- src/gateway/gateway.py | 13 ++++++ src/rest_api/routes/other.py | 5 +++ src/voice_gateway/__init__.py | 0 src/voice_gateway/events.py | 58 ++++++++++++++++++++++++++ src/voice_gateway/gateway.py | 76 +++++++++++++++++++++++++++++++++++ src/voice_gateway/main.py | 44 ++++++++++++++++++++ src/yepcord/enums.py | 17 +++++++- 9 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 src/voice_gateway/__init__.py create mode 100644 src/voice_gateway/events.py create mode 100644 src/voice_gateway/gateway.py create mode 100644 src/voice_gateway/main.py diff --git a/run_all.py b/run_all.py index ac9b996..38c98e7 100644 --- a/run_all.py +++ b/run_all.py @@ -58,6 +58,7 @@ def stop(self, kill=False): Process("IMT", file="main.py", wd="src/pubsub"), Process("HttpApi", app="src.rest_api.main:app", port=8000), Process("Gateway", app="src.gateway.main:app", port=8001), + Process("VoiceGateway", app="src.voice_gateway.main:app", port=8099), Process("CDN", app="src.cdn.main:app", port=8003), Process("RemoteAuth", app="src.remote_auth.main:app", port=8002), ] diff --git a/src/gateway/events.py b/src/gateway/events.py index 095cf33..5f1e600 100644 --- a/src/gateway/events.py +++ b/src/gateway/events.py @@ -2,6 +2,7 @@ from time import time from typing import List +from ..yepcord.classes.guild import GuildId from ..yepcord.classes.user import GuildMember from ..yepcord.config import Config from ..yepcord.snowflake import Snowflake @@ -818,4 +819,56 @@ class ScheduledEventUserRemoveEvent(ScheduledEventUserAddEvent): NAME = "GUILD_SCHEDULED_EVENT_USER_REMOVE" class GuildScheduledEventDeleteEvent(GuildScheduledEventCreateEvent): - NAME = "GUILD_SCHEDULED_EVENT_DELETE" \ No newline at end of file + NAME = "GUILD_SCHEDULED_EVENT_DELETE" + +class VoiceStateUpdate(DispatchEvent): + NAME = "VOICE_STATE_UPDATE" + + def __init__(self, core, user_id, session_id, channel_id, guild_id, **kwargs): + self.core = core + self.user_id = user_id + self.session_id = session_id + self.channel_id = channel_id + self.guild_id = guild_id + self.kwargs = kwargs + + async def json(self) -> dict: + data = { + "t": self.NAME, + "op": self.OP, + "d": { + "user_id": str(self.user_id), + "channel_id": str(self.channel_id), + "deaf": False, + "mute": False, + "session_id": self.session_id, + **self.kwargs + } + } + if self.guild_id: + data["d"]["guild_id"] = str(self.guild_id) + data["d"]["member"] = await (await self.core.getGuildMember(GuildId(self.guild_id), self.user_id)).json + return data + +class VoiceServerUpdate(DispatchEvent): + NAME = "VOICE_SERVER_UPDATE" + + def __init__(self, guild_id, channel_id): + self.guild_id = guild_id + self.channel_id = channel_id + + async def json(self) -> dict: + data = { + "t": self.NAME, + "op": self.OP, + "d": { + "token": "idk_token", + "endpoint": "127.0.0.1:8099" + } + } + if self.guild_id: + data["d"]["guild_id"] = str(self.guild_id) + if self.channel_id: + data["d"]["channel_id"] = str(self.channel_id) + + return data \ No newline at end of file diff --git a/src/gateway/gateway.py b/src/gateway/gateway.py index 9d3ee5a..09bee7c 100644 --- a/src/gateway/gateway.py +++ b/src/gateway/gateway.py @@ -586,6 +586,19 @@ async def process(self, ws, data): members = await self.core.getGuildMembersGw(GuildId(guild_id), query, limit) presences = [] # TODO: add presences await cl.esend(GuildMembersChunkEvent(members, presences, guild_id)) + elif op == GatewayOp.VOICE_STATE: + d = data["d"] + if not (channel_id := d.get("channel_id", 0)): return + if not (cl := await self.getClientFromSocket(ws)): return + self_mute = bool(d.get("self_mute")) + self_deaf = bool(d.get("self_deaf")) + guild_id = int(d.get("guild_id", 0)) + + print(f"Connecting to voice with session_id={cl.sid}") + + await cl.esend(VoiceStateUpdate(self.core, cl.id, cl.sid, channel_id, guild_id, self_mute=self_mute, self_deaf=self_deaf)) + await cl.esend(VoiceServerUpdate(guild_id, channel_id)) + else: print("-"*16) print(f" Unknown op code: {op}") diff --git a/src/rest_api/routes/other.py b/src/rest_api/routes/other.py index 48867b4..1ee27a6 100644 --- a/src/rest_api/routes/other.py +++ b/src/rest_api/routes/other.py @@ -140,6 +140,11 @@ async def api_gateway(): return c_json("{\"url\": \"wss://gateway.yepcord.ml\"}") +@other.get("/api/v9/activities/guilds//shelf") +async def api_activities_shelf(guild_id): + return c_json("\"activity_bundle_items\": []") + + # OAuth diff --git a/src/voice_gateway/__init__.py b/src/voice_gateway/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/voice_gateway/events.py b/src/voice_gateway/events.py new file mode 100644 index 0000000..b56345c --- /dev/null +++ b/src/voice_gateway/events.py @@ -0,0 +1,58 @@ +from src.yepcord.enums import VoiceGatewayOp + + +class Event: + OP: int + + async def json(self) -> dict: ... + +class ReadyEvent(Event): + OP = VoiceGatewayOp.READY + + def __init__(self, ssrc: int): + self.ssrc = ssrc + + async def json(self) -> dict: + return { + "op": self.OP, + "d": { + "ssrc": self.ssrc, + "ip": "127.0.0.1", + "port": 9999, + "modes": ["xsalsa20_poly1305", "xsalsa20_poly1305_suffix", "xsalsa20_poly1305_lite"] + } + } + +class SessionDescriptionEvent(Event): + OP = VoiceGatewayOp.SESSION_DESCRIPTION + + def __init__(self, mode: str, key: bytes): + self.mode = mode + self.key = key + + async def json(self) -> dict: + return { + "op": self.OP, + "d": { + "secret_key": [b for b in self.key], + "mode": self.mode + } + } + +class SpeakingEvent(Event): + OP = VoiceGatewayOp.SPEAKING + + def __init__(self, ssrc: int, speaking: int, delay: int): + self.ssrc = ssrc + self.speaking = speaking + self.delay = delay + + async def json(self) -> dict: + return { + "op": self.OP, + "d": { + "ssrc": self.ssrc, + "speaking": self.speaking, + "delay": self.delay + } + } \ No newline at end of file diff --git a/src/voice_gateway/gateway.py b/src/voice_gateway/gateway.py new file mode 100644 index 0000000..14851e0 --- /dev/null +++ b/src/voice_gateway/gateway.py @@ -0,0 +1,76 @@ +from os import urandom +from typing import Any, Optional + +from src.yepcord.enums import VoiceGatewayOp +from .events import Event, ReadyEvent, SessionDescriptionEvent, SpeakingEvent + + +class GatewayClient: + def __init__(self, ws, user_id: int, session_id: str, guild_id: int, token: str, ssrc: int): + self.ws = ws + self.user_id = user_id + self.session_id = session_id + self.guild_id = guild_id + self.token = token + self.ssrc = ssrc + self.mode: Optional[str] = None + self.key: Optional[bytes] = None + + async def send(self, data: dict): + await self.ws.send_json(data) + + async def esend(self, event: Event): + await self.send(await event.json()) + +class Gateway: + def __init__(self): + self._clients: dict[Any, GatewayClient] = {} + self._ssrc = 1 + + async def sendws(self, ws, op: int, **data) -> None: + await ws.send_json({"op": op, **data}) + + async def sendHello(self, ws) -> None: + await self.sendws(ws, VoiceGatewayOp.HELLO, d={"heartbeat_interval": 13750}) + + async def process(self, ws, data: dict) -> None: + op = data["op"] + if op == VoiceGatewayOp.IDENTIFY: + if ws in self._clients: + return await ws.close(4005) + d = data["d"] + print(f"Connected to voice with session_id={d['session_id']}") + if d["token"] != "idk_token": + return await ws.close(4004) + self._clients[ws] = client = GatewayClient(ws, int(d["user_id"]), d["session_id"], d["server_id"], d["token"], self._ssrc) + await client.esend(ReadyEvent(self._ssrc)) + self._ssrc += 1 + elif op == VoiceGatewayOp.HEARTBEAT: + if ws not in self._clients: + return await ws.close(4003) + await self.sendws(ws, VoiceGatewayOp.HEARTBEAT_ACK, d=data["d"]) + elif op == VoiceGatewayOp.SELECT_PROTOCOL: + if ws not in self._clients: + return await ws.close(4003) + client = self._clients[ws] + d = data["d"] + if d["protocol"] != "udp": + return await ws.close(4012) + client.mode = d["data"]["mode"] + if client.mode not in ("xsalsa20_poly1305", "xsalsa20_poly1305_suffix", "xsalsa20_poly1305_lite"): + return await ws.close(4016) + client.key = urandom(32) + await client.esend(SessionDescriptionEvent(client.mode, client.key)) + elif op == VoiceGatewayOp.SPEAKING: + if ws not in self._clients: + return await ws.close(4003) + client = self._clients[ws] + d = data["d"] + if client.ssrc != d["ssrc"] or d["ssrc"] > 7 or d["ssrc"] < 1: + return await ws.close(4014) + await client.esend(SpeakingEvent(client.ssrc, d["speaking"], d["delay"])) + + + async def disconnect(self, ws) -> None: + if ws in self._clients: + del self._clients[ws] \ No newline at end of file diff --git a/src/voice_gateway/main.py b/src/voice_gateway/main.py new file mode 100644 index 0000000..bf6bad6 --- /dev/null +++ b/src/voice_gateway/main.py @@ -0,0 +1,44 @@ +from quart import Quart, websocket + +from ..yepcord.config import Config +from ..yepcord.classes.other import ZlibCompressor +from ..yepcord.core import Core +from ..yepcord.utils import b64decode +from json import loads as jloads +from asyncio import CancelledError +from .gateway import Gateway + + +class YEPcord(Quart): + pass # Maybe it will be needed in the future + + +app = YEPcord("YEPcord-VoiceGateway") +gw = Gateway() + + +@app.after_request +async def set_cors_headers(response): + response.headers['Server'] = "YEPcord Voice Gateway" + response.headers['Access-Control-Allow-Origin'] = "*" + response.headers['Access-Control-Allow-Headers'] = "*" + response.headers['Access-Control-Allow-Methods'] = "*" + response.headers['Content-Security-Policy'] = "connect-src *;" + return response + + +@app.websocket("/") +async def ws_gateway(): + ws = websocket._get_current_object() + await gw.sendHello(ws) + while True: + try: + data = await ws.receive() + await gw.process(ws, jloads(data)) + except CancelledError: + await gw.disconnect(ws) + break # TODO: Handle disconnect + +if __name__ == "__main__": + from uvicorn import run as urun + urun('main:app', host="0.0.0.0", port=8099, reload=True, use_colors=False) \ No newline at end of file diff --git a/src/yepcord/enums.py b/src/yepcord/enums.py index 79ce233..589f2a0 100644 --- a/src/yepcord/enums.py +++ b/src/yepcord/enums.py @@ -244,4 +244,19 @@ class ScheduledEventStatus(E): SCHEDULED = 1 ACTIVE = 2 COMPLETED = 3 - CANCELED = 4 \ No newline at end of file + CANCELED = 4 + + +# https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-opcodes +class VoiceGatewayOp(E): + IDENTIFY = 0 + SELECT_PROTOCOL = 1 + READY = 2 + HEARTBEAT = 3 + SESSION_DESCRIPTION = 4 + SPEAKING = 5 + HEARTBEAT_ACK = 6 + RESUME = 7 + HELLO = 8 + RESUMED = 9 + CLIENT_DISCONNECT = 13 From 75600a19208c86cb44234f30a4dc336041ce2eeb Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Sat, 18 Mar 2023 17:40:38 +0200 Subject: [PATCH 02/15] it not working :( --- src/gateway/gateway.py | 3 ++- src/voice_gateway/events.py | 44 +++++++++++++++++++++++++++------- src/voice_gateway/gateway.py | 46 +++++++++++++++++++++++++++--------- src/voice_gateway/schemas.py | 24 +++++++++++++++++++ 4 files changed, 97 insertions(+), 20 deletions(-) create mode 100644 src/voice_gateway/schemas.py diff --git a/src/gateway/gateway.py b/src/gateway/gateway.py index 09bee7c..00c1fb4 100644 --- a/src/gateway/gateway.py +++ b/src/gateway/gateway.py @@ -592,7 +592,8 @@ async def process(self, ws, data): if not (cl := await self.getClientFromSocket(ws)): return self_mute = bool(d.get("self_mute")) self_deaf = bool(d.get("self_deaf")) - guild_id = int(d.get("guild_id", 0)) + guild_id = d.get("guild_id", 0) + guild_id = int(guild_id) if guild_id is not None else None print(f"Connecting to voice with session_id={cl.sid}") diff --git a/src/voice_gateway/events.py b/src/voice_gateway/events.py index b56345c..a4c8e10 100644 --- a/src/voice_gateway/events.py +++ b/src/voice_gateway/events.py @@ -9,21 +9,32 @@ async def json(self) -> dict: ... class ReadyEvent(Event): OP = VoiceGatewayOp.READY - def __init__(self, ssrc: int): + def __init__(self, ssrc: int, video_ssrc: int, rtx_ssrc: int, port: int): self.ssrc = ssrc + self.video_ssrc = video_ssrc + self.rtx_ssrc = rtx_ssrc + self.port = port async def json(self) -> dict: return { "op": self.OP, "d": { "ssrc": self.ssrc, - "ip": "127.0.0.1", - "port": 9999, - "modes": ["xsalsa20_poly1305", "xsalsa20_poly1305_suffix", "xsalsa20_poly1305_lite"] + "ip": "192.168.1.155", + "port": self.port, + "modes": ["xsalsa20_poly1305", "xsalsa20_poly1305_suffix", "xsalsa20_poly1305_lite", "xsalsa20_poly1305_lite_rtpsize", "aead_aes256_gcm", "aead_aes256_gcm_rtpsize"], + "streams": [{ + "active": False, + "quality": 0, + "rid": "", + "rtx_ssrc": self.rtx_ssrc, + "ssrc": self.video_ssrc, + "type": "video" + }] } } -class SessionDescriptionEvent(Event): +class UdpSessionDescriptionEvent(Event): OP = VoiceGatewayOp.SESSION_DESCRIPTION def __init__(self, mode: str, key: bytes): @@ -39,13 +50,30 @@ async def json(self) -> dict: } } +class RtcSessionDescriptionEvent(Event): + OP = VoiceGatewayOp.SESSION_DESCRIPTION + + def __init__(self, sdp: str): + self.sdp = sdp + + async def json(self) -> dict: + return { + "op": self.OP, + "d": { + "audio_codec": "opus", + "video_codec": "H264", + "media_session_id": "50d1809fc221526fd39fba2de2f5e64d", + "sdp": self.sdp + } + } + class SpeakingEvent(Event): OP = VoiceGatewayOp.SPEAKING - def __init__(self, ssrc: int, speaking: int, delay: int): + def __init__(self, ssrc: int, user_id: int, speaking: int): self.ssrc = ssrc self.speaking = speaking - self.delay = delay + self.user_id = user_id async def json(self) -> dict: return { @@ -53,6 +81,6 @@ async def json(self) -> dict: "d": { "ssrc": self.ssrc, "speaking": self.speaking, - "delay": self.delay + "user_id": str(self.user_id) } } \ No newline at end of file diff --git a/src/voice_gateway/gateway.py b/src/voice_gateway/gateway.py index 14851e0..f2708ee 100644 --- a/src/voice_gateway/gateway.py +++ b/src/voice_gateway/gateway.py @@ -1,8 +1,14 @@ from os import urandom from typing import Any, Optional +from aiohttp import ClientSession +from aiortc import RTCSessionDescription, RTCPeerConnection +from aiortc.contrib.media import MediaBlackhole, MediaRecorder, MediaPlayer +from aiortc.sdp import SessionDescription + from src.yepcord.enums import VoiceGatewayOp -from .events import Event, ReadyEvent, SessionDescriptionEvent, SpeakingEvent +from .events import Event, ReadyEvent, SpeakingEvent, UdpSessionDescriptionEvent, RtcSessionDescriptionEvent +from .schemas import SelectProtocol class GatewayClient: @@ -13,6 +19,8 @@ def __init__(self, ws, user_id: int, session_id: str, guild_id: int, token: str, self.guild_id = guild_id self.token = token self.ssrc = ssrc + self.video_ssrc = 0 + self.rtx_ssrc = 0 self.mode: Optional[str] = None self.key: Optional[bytes] = None @@ -26,12 +34,13 @@ class Gateway: def __init__(self): self._clients: dict[Any, GatewayClient] = {} self._ssrc = 1 + self._pcs: set[RTCPeerConnection] = set() async def sendws(self, ws, op: int, **data) -> None: await ws.send_json({"op": op, **data}) async def sendHello(self, ws) -> None: - await self.sendws(ws, VoiceGatewayOp.HELLO, d={"heartbeat_interval": 13750}) + await self.sendws(ws, VoiceGatewayOp.HELLO, d={"v": 7, "heartbeat_interval": 13750}) async def process(self, ws, data: dict) -> None: op = data["op"] @@ -42,9 +51,17 @@ async def process(self, ws, data: dict) -> None: print(f"Connected to voice with session_id={d['session_id']}") if d["token"] != "idk_token": return await 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._clients[ws] = client = GatewayClient(ws, int(d["user_id"]), d["session_id"], d["server_id"], d["token"], self._ssrc) - await client.esend(ReadyEvent(self._ssrc)) self._ssrc += 1 + client.video_ssrc = self._ssrc + self._ssrc += 1 + client.rtx_ssrc = self._ssrc + self._ssrc += 1 + await client.esend(ReadyEvent(client.ssrc, client.video_ssrc, client.rtx_ssrc, p)) elif op == VoiceGatewayOp.HEARTBEAT: if ws not in self._clients: return await ws.close(4003) @@ -53,14 +70,21 @@ async def process(self, ws, data: dict) -> None: if ws not in self._clients: return await ws.close(4003) client = self._clients[ws] - d = data["d"] - if d["protocol"] != "udp": + try: + d = SelectProtocol(**data["d"]) + except: return await ws.close(4012) - client.mode = d["data"]["mode"] - if client.mode not in ("xsalsa20_poly1305", "xsalsa20_poly1305_suffix", "xsalsa20_poly1305_lite"): - return await ws.close(4016) - client.key = urandom(32) - await client.esend(SessionDescriptionEvent(client.mode, client.key)) + if d.protocol == "webrtc": + + answer = ... + + await client.esend(RtcSessionDescriptionEvent(answer)) + elif d.protocol == "udp": + client.mode = d.data.mode + if client.mode not in ("xsalsa20_poly1305", "xsalsa20_poly1305_suffix", "xsalsa20_poly1305_lite", "xsalsa20_poly1305_lite_rtpsize", "aead_aes256_gcm", "aead_aes256_gcm_rtpsize"): + return await ws.close(4016) + client.key = urandom(32) + await client.esend(UdpSessionDescriptionEvent(client.mode, client.key)) elif op == VoiceGatewayOp.SPEAKING: if ws not in self._clients: return await ws.close(4003) @@ -68,7 +92,7 @@ async def process(self, ws, data: dict) -> None: d = data["d"] if client.ssrc != d["ssrc"] or d["ssrc"] > 7 or d["ssrc"] < 1: return await ws.close(4014) - await client.esend(SpeakingEvent(client.ssrc, d["speaking"], d["delay"])) + await client.esend(SpeakingEvent(client.ssrc, client.user_id, d["speaking"])) async def disconnect(self, ws) -> None: diff --git a/src/voice_gateway/schemas.py b/src/voice_gateway/schemas.py new file mode 100644 index 0000000..bc68f3c --- /dev/null +++ b/src/voice_gateway/schemas.py @@ -0,0 +1,24 @@ +from typing import Literal, Union, Optional + +from pydantic import BaseModel + + +class UdpProtocolData(BaseModel): + address: str + port: str + mode: str + + +class CodecsData(BaseModel): + name: Literal["opus", "VP9", "VP8", "H264"] + type: Literal["audio", "video"] + priority: int + payload_type: int + rtx_payload_type: Optional[int] = None + + +class SelectProtocol(BaseModel): + protocol: Literal["udp", "webrtc"] + data: Union[str, UdpProtocolData] + sdp: Optional[str] = None + codecs: Optional[list[CodecsData]] From 86148f0c4ea25ad68711fbafa1ab7c6e0b0e6453 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Fri, 8 Mar 2024 18:46:14 +0200 Subject: [PATCH 03/15] update voice gateway --- src/voice_gateway/gateway.py | 100 ------------------ src/voice_gateway/main.py | 44 -------- yepcord/asgi.py | 5 +- yepcord/gateway/events.py | 57 +++++++++- yepcord/gateway/gateway.py | 22 ++++ yepcord/gateway/utils.py | 15 +-- yepcord/remote_auth/main.py | 2 +- {src => yepcord}/voice_gateway/__init__.py | 0 {src => yepcord}/voice_gateway/events.py | 13 ++- yepcord/voice_gateway/gateway.py | 116 +++++++++++++++++++++ yepcord/voice_gateway/main.py | 44 ++++++++ {src => yepcord}/voice_gateway/schemas.py | 0 yepcord/yepcord/enums.py | 20 ++++ 13 files changed, 279 insertions(+), 159 deletions(-) delete mode 100644 src/voice_gateway/gateway.py delete mode 100644 src/voice_gateway/main.py rename {src => yepcord}/voice_gateway/__init__.py (100%) rename {src => yepcord}/voice_gateway/events.py (89%) create mode 100644 yepcord/voice_gateway/gateway.py create mode 100644 yepcord/voice_gateway/main.py rename {src => yepcord}/voice_gateway/schemas.py (100%) diff --git a/src/voice_gateway/gateway.py b/src/voice_gateway/gateway.py deleted file mode 100644 index f2708ee..0000000 --- a/src/voice_gateway/gateway.py +++ /dev/null @@ -1,100 +0,0 @@ -from os import urandom -from typing import Any, Optional - -from aiohttp import ClientSession -from aiortc import RTCSessionDescription, RTCPeerConnection -from aiortc.contrib.media import MediaBlackhole, MediaRecorder, MediaPlayer -from aiortc.sdp import SessionDescription - -from src.yepcord.enums import VoiceGatewayOp -from .events import Event, ReadyEvent, SpeakingEvent, UdpSessionDescriptionEvent, RtcSessionDescriptionEvent -from .schemas import SelectProtocol - - -class GatewayClient: - def __init__(self, ws, user_id: int, session_id: str, guild_id: int, token: str, ssrc: int): - self.ws = ws - self.user_id = user_id - self.session_id = session_id - self.guild_id = guild_id - self.token = token - self.ssrc = ssrc - self.video_ssrc = 0 - self.rtx_ssrc = 0 - self.mode: Optional[str] = None - self.key: Optional[bytes] = None - - async def send(self, data: dict): - await self.ws.send_json(data) - - async def esend(self, event: Event): - await self.send(await event.json()) - -class Gateway: - def __init__(self): - self._clients: dict[Any, GatewayClient] = {} - self._ssrc = 1 - self._pcs: set[RTCPeerConnection] = set() - - async def sendws(self, ws, op: int, **data) -> None: - await ws.send_json({"op": op, **data}) - - async def sendHello(self, ws) -> None: - await self.sendws(ws, VoiceGatewayOp.HELLO, d={"v": 7, "heartbeat_interval": 13750}) - - async def process(self, ws, data: dict) -> None: - op = data["op"] - if op == VoiceGatewayOp.IDENTIFY: - if ws in self._clients: - return await ws.close(4005) - d = data["d"] - print(f"Connected to voice with session_id={d['session_id']}") - if d["token"] != "idk_token": - return await 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._clients[ws] = client = GatewayClient(ws, int(d["user_id"]), d["session_id"], d["server_id"], d["token"], self._ssrc) - self._ssrc += 1 - client.video_ssrc = self._ssrc - self._ssrc += 1 - client.rtx_ssrc = self._ssrc - self._ssrc += 1 - await client.esend(ReadyEvent(client.ssrc, client.video_ssrc, client.rtx_ssrc, p)) - elif op == VoiceGatewayOp.HEARTBEAT: - if ws not in self._clients: - return await ws.close(4003) - await self.sendws(ws, VoiceGatewayOp.HEARTBEAT_ACK, d=data["d"]) - elif op == VoiceGatewayOp.SELECT_PROTOCOL: - if ws not in self._clients: - return await ws.close(4003) - client = self._clients[ws] - try: - d = SelectProtocol(**data["d"]) - except: - return await ws.close(4012) - if d.protocol == "webrtc": - - answer = ... - - await client.esend(RtcSessionDescriptionEvent(answer)) - elif d.protocol == "udp": - client.mode = d.data.mode - if client.mode not in ("xsalsa20_poly1305", "xsalsa20_poly1305_suffix", "xsalsa20_poly1305_lite", "xsalsa20_poly1305_lite_rtpsize", "aead_aes256_gcm", "aead_aes256_gcm_rtpsize"): - return await ws.close(4016) - client.key = urandom(32) - await client.esend(UdpSessionDescriptionEvent(client.mode, client.key)) - elif op == VoiceGatewayOp.SPEAKING: - if ws not in self._clients: - return await ws.close(4003) - client = self._clients[ws] - d = data["d"] - if client.ssrc != d["ssrc"] or d["ssrc"] > 7 or d["ssrc"] < 1: - return await ws.close(4014) - await client.esend(SpeakingEvent(client.ssrc, client.user_id, d["speaking"])) - - - async def disconnect(self, ws) -> None: - if ws in self._clients: - del self._clients[ws] \ No newline at end of file diff --git a/src/voice_gateway/main.py b/src/voice_gateway/main.py deleted file mode 100644 index bf6bad6..0000000 --- a/src/voice_gateway/main.py +++ /dev/null @@ -1,44 +0,0 @@ -from quart import Quart, websocket - -from ..yepcord.config import Config -from ..yepcord.classes.other import ZlibCompressor -from ..yepcord.core import Core -from ..yepcord.utils import b64decode -from json import loads as jloads -from asyncio import CancelledError -from .gateway import Gateway - - -class YEPcord(Quart): - pass # Maybe it will be needed in the future - - -app = YEPcord("YEPcord-VoiceGateway") -gw = Gateway() - - -@app.after_request -async def set_cors_headers(response): - response.headers['Server'] = "YEPcord Voice Gateway" - response.headers['Access-Control-Allow-Origin'] = "*" - response.headers['Access-Control-Allow-Headers'] = "*" - response.headers['Access-Control-Allow-Methods'] = "*" - response.headers['Content-Security-Policy'] = "connect-src *;" - return response - - -@app.websocket("/") -async def ws_gateway(): - ws = websocket._get_current_object() - await gw.sendHello(ws) - while True: - try: - data = await ws.receive() - await gw.process(ws, jloads(data)) - except CancelledError: - await gw.disconnect(ws) - break # TODO: Handle disconnect - -if __name__ == "__main__": - from uvicorn import run as urun - urun('main:app', host="0.0.0.0", port=8099, reload=True, use_colors=False) \ No newline at end of file diff --git a/yepcord/asgi.py b/yepcord/asgi.py index b47f9ed..3af00f7 100644 --- a/yepcord/asgi.py +++ b/yepcord/asgi.py @@ -23,6 +23,7 @@ import yepcord.gateway.main as gateway import yepcord.cdn.main as cdn import yepcord.remote_auth.main as remote_auth +import yepcord.voice_gateway.main as voice_gateway from yepcord.rest_api.routes import auth from yepcord.rest_api.routes import users_me from yepcord.rest_api.routes import users @@ -77,8 +78,8 @@ app.route("/api/v9/", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])(rest_api.other_api_endpoints) app.websocket("/gateway", strict_slashes=False)(gateway.ws_gateway) -remote_auth.ws_gateway.__name__ = "ws_ra_gateway" -app.websocket("/remote-auth", strict_slashes=False)(remote_auth.ws_gateway) +app.websocket("/remote-auth", strict_slashes=False)(remote_auth.ws_gateway_remote_auth) +app.websocket("/voice", strict_slashes=False)(voice_gateway.ws_gateway_voice) app.get("/media/avatars//.")(cdn.get_avatar) app.get("/media/banners//.")(cdn.get_banner) diff --git a/yepcord/gateway/events.py b/yepcord/gateway/events.py index aa448d9..30f4154 100644 --- a/yepcord/gateway/events.py +++ b/yepcord/gateway/events.py @@ -20,7 +20,7 @@ from base64 import b64encode from time import time -from typing import List, TYPE_CHECKING +from typing import List, TYPE_CHECKING, Optional from ..yepcord.config import Config from ..yepcord.enums import GatewayOp @@ -29,7 +29,7 @@ from ..yepcord.snowflake import Snowflake if TYPE_CHECKING: # pragma: no cover - from ..yepcord.models import Channel, Invite, GuildMember, UserData, User, UserSettings + from ..yepcord.models import Channel, Invite, GuildMember, UserData, User, UserSettings, Guild from ..yepcord.core import Core from .gateway import GatewayClient from .presences import Presence @@ -1037,3 +1037,56 @@ class InteractionFailureEvent(InteractionSuccessEvent): NAME = "INTERACTION_FAILURE" +class VoiceStateUpdate(DispatchEvent): + NAME = "VOICE_STATE_UPDATE" + + def __init__(self, user_id: int, session_id: str, channel: Channel, guild: Optional[Guild], + member: Optional[GuildMember], **kwargs): + self.user_id = user_id + self.session_id = session_id + self.channel = channel + self.guild = guild + self.member = member + self.kwargs = kwargs + + async def json(self) -> dict: + data = { + "t": self.NAME, + "op": self.OP, + "d": { + "user_id": str(self.user_id), + "channel_id": str(self.channel.id), + "deaf": False, + "mute": False, + "session_id": self.session_id, + **self.kwargs + } + } + if self.guild: + data["d"]["guild_id"] = str(self.guild.id) + data["d"]["member"] = await self.member.ds_json() + return data + + +class VoiceServerUpdate(DispatchEvent): + NAME = "VOICE_SERVER_UPDATE" + + def __init__(self, channel: Channel, guild: Optional[Guild]): + self.channel = channel + self.guild = guild + + async def json(self) -> dict: + data = { + "t": self.NAME, + "op": self.OP, + "d": { + "token": "idk_token", + "endpoint": "127.0.0.1:8000/voice" + } + } + if self.guild: + data["d"]["guild_id"] = str(self.guild.id) + if self.channel: + data["d"]["channel_id"] = str(self.channel.id) + + return data diff --git a/yepcord/gateway/gateway.py b/yepcord/gateway/gateway.py index d973d95..97fda25 100644 --- a/yepcord/gateway/gateway.py +++ b/yepcord/gateway/gateway.py @@ -175,6 +175,28 @@ async def handle_GUILD_MEMBERS(self, data: dict) -> None: presences = [] # TODO: add presences await self.esend(GuildMembersChunkEvent(members, presences, guild_id)) + @require_auth + async def handle_VOICE_STATE(self, data: dict) -> None: + self_mute = bool(data.get("self_mute")) + self_deaf = bool(data.get("self_deaf")) + + if not (channel := await getCore().getChannel(data.get("channel_id"))): return + if not await getCore().getUserByChannel(channel, self.user_id): return + + guild = None + member = None + if guild_id := data.get("guild_id"): + if (guild := await getCore().getGuild(guild_id)) is None \ + or (member := await getCore().getGuildMember(guild, self.user_id)) is None: + return + + print(f"Connecting to voice with session_id={self.sid}") + + await self.esend(VoiceStateUpdate( + self.id, self.sid, channel, guild, member, self_mute=self_mute, self_deaf=self_deaf + )) + await self.esend(VoiceServerUpdate(channel, guild)) + class GatewayEvents: BOTS_EVENTS_BLACKLIST = {"MESSAGE_ACK"} diff --git a/yepcord/gateway/utils.py b/yepcord/gateway/utils.py index bf1f1b5..09962fc 100644 --- a/yepcord/gateway/utils.py +++ b/yepcord/gateway/utils.py @@ -24,13 +24,16 @@ from ..yepcord.utils import b64decode -def require_auth(func): - async def wrapped(self, *args, **kwargs): - if self.user_id is None: - return self.ws.close(4005) - return await func(self, *args, **kwargs) +def require_auth(func_or_code): + def decorator(func): + async def wrapped(self, *args, **kwargs): + if self.user_id is None: + return await self.ws.close(func_or_code if isinstance(func_or_code, int) else 4005) + return await func(self, *args, **kwargs) - return wrapped + return wrapped + + return decorator if isinstance(func_or_code, int) else decorator(func_or_code) class TokenType(Enum): diff --git a/yepcord/remote_auth/main.py b/yepcord/remote_auth/main.py index 2d201e9..514c090 100644 --- a/yepcord/remote_auth/main.py +++ b/yepcord/remote_auth/main.py @@ -55,7 +55,7 @@ async def set_cors_headers(response): # pragma: no cover @app.websocket("/") -async def ws_gateway(): +async def ws_gateway_remote_auth(): # noinspection PyProtectedMember,PyUnresolvedReferences ws = websocket._get_current_object() setattr(ws, "connected", True) diff --git a/src/voice_gateway/__init__.py b/yepcord/voice_gateway/__init__.py similarity index 100% rename from src/voice_gateway/__init__.py rename to yepcord/voice_gateway/__init__.py diff --git a/src/voice_gateway/events.py b/yepcord/voice_gateway/events.py similarity index 89% rename from src/voice_gateway/events.py rename to yepcord/voice_gateway/events.py index a4c8e10..72a9055 100644 --- a/src/voice_gateway/events.py +++ b/yepcord/voice_gateway/events.py @@ -1,4 +1,4 @@ -from src.yepcord.enums import VoiceGatewayOp +from yepcord.yepcord.enums import VoiceGatewayOp class Event: @@ -6,6 +6,7 @@ class Event: async def json(self) -> dict: ... + class ReadyEvent(Event): OP = VoiceGatewayOp.READY @@ -20,9 +21,10 @@ async def json(self) -> dict: "op": self.OP, "d": { "ssrc": self.ssrc, - "ip": "192.168.1.155", + "ip": "127.0.0.1", "port": self.port, - "modes": ["xsalsa20_poly1305", "xsalsa20_poly1305_suffix", "xsalsa20_poly1305_lite", "xsalsa20_poly1305_lite_rtpsize", "aead_aes256_gcm", "aead_aes256_gcm_rtpsize"], + "modes": ["xsalsa20_poly1305", "xsalsa20_poly1305_suffix", "xsalsa20_poly1305_lite", + "xsalsa20_poly1305_lite_rtpsize", "aead_aes256_gcm", "aead_aes256_gcm_rtpsize"], "streams": [{ "active": False, "quality": 0, @@ -34,6 +36,7 @@ async def json(self) -> dict: } } + class UdpSessionDescriptionEvent(Event): OP = VoiceGatewayOp.SESSION_DESCRIPTION @@ -50,6 +53,7 @@ async def json(self) -> dict: } } + class RtcSessionDescriptionEvent(Event): OP = VoiceGatewayOp.SESSION_DESCRIPTION @@ -67,6 +71,7 @@ async def json(self) -> dict: } } + class SpeakingEvent(Event): OP = VoiceGatewayOp.SPEAKING @@ -83,4 +88,4 @@ async def json(self) -> dict: "speaking": self.speaking, "user_id": str(self.user_id) } - } \ No newline at end of file + } diff --git a/yepcord/voice_gateway/gateway.py b/yepcord/voice_gateway/gateway.py new file mode 100644 index 0000000..9ceb797 --- /dev/null +++ b/yepcord/voice_gateway/gateway.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +from os import urandom +from typing import Optional + +from quart import Websocket + +from yepcord.yepcord.enums import VoiceGatewayOp +from .events import Event, ReadyEvent, SpeakingEvent, UdpSessionDescriptionEvent, RtcSessionDescriptionEvent +from .schemas import SelectProtocol +from ..gateway.utils import require_auth + + +class GatewayClient: + def __init__(self, ws: Websocket, gw: Gateway): + self.ws = ws + self.user_id = None + self.session_id = None + self.guild_id = None + self.token = None + self.ssrc = 0 + self.video_ssrc = 0 + self.rtx_ssrc = 0 + self.mode: Optional[str] = None + self.key: Optional[bytes] = None + + self._gw = gw + + async def send(self, data: dict): + await self.ws.send_json(data) + + async def esend(self, event: Event): + await self.send(await event.json()) + + async def handle_IDENTIFY(self, data: dict): + print(f"Connected to voice with session_id={data['session_id']}") + 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"] + self.token = data["token"] + + self.ssrc = self._gw.ssrc + self._gw.ssrc += 1 + self.video_ssrc = self._gw.ssrc + self._gw.ssrc += 1 + self.rtx_ssrc = self._gw.ssrc + self._gw.ssrc += 1 + + port = 3791 # TODO: get port + + await self.esend(ReadyEvent(self.ssrc, self.video_ssrc, self.rtx_ssrc, port)) + + @require_auth(4003) + async def handle_HEARTBEAT(self, data: dict): + await self.send({"op": VoiceGatewayOp.HEARTBEAT_ACK, "d": data["d"]}) + + @require_auth(4003) + async def handle_SELECT_PROTOCOL(self, data: dict): + try: + d = SelectProtocol(**data) + except Exception as e: + print(e) + return await self.ws.close(4012) + + if d.protocol == "webrtc": + + answer = ... # TODO: generate answer + + await self.esend(RtcSessionDescriptionEvent(answer)) + elif d.protocol == "udp": + self.mode = d.data.mode + if self.mode not in ("xsalsa20_poly1305", "xsalsa20_poly1305_suffix", "xsalsa20_poly1305_lite", + "xsalsa20_poly1305_lite_rtpsize", "aead_aes256_gcm", "aead_aes256_gcm_rtpsize"): + return await self.ws.close(4016) + self.key = urandom(32) + await self.esend(UdpSessionDescriptionEvent(self.mode, self.key)) + + @require_auth(4003) + async def handle_SPEAKING(self, data: dict): + if self.ssrc != data["ssrc"] or data["ssrc"] < 1: + return await self.ws.close(4014) + await self.esend(SpeakingEvent(self.ssrc, self.user_id, data["speaking"])) + + async def handle_VOICE_BACKEND_VERSION(self, data: dict) -> None: + await self.send({"op": VoiceGatewayOp.VOICE_BACKEND_VERSION, "d": {"voice": "0.11.0", "rtc_worker": "0.4.11"}}) + + +class Gateway: + def __init__(self): + self.ssrc = 1 + + async def sendHello(self, ws: Websocket) -> None: + client = GatewayClient(ws, self) + setattr(ws, "_yepcord_client", client) + await ws.send_json({"op": VoiceGatewayOp.HELLO, "d": {"v": 7, "heartbeat_interval": 13750}}) + + async def process(self, ws: Websocket, data: dict) -> None: + op = data["op"] + if (client := getattr(ws, "_yepcord_client", None)) is None: + return await ws.close(4005) + + func = getattr(client, f"handle_{VoiceGatewayOp.reversed()[op]}", None) + if func: + return await func(data.get("d")) + else: + print("-" * 16) + print(f" [Voice] Unknown op code: {op}") + print(f" [Voice] Data: {data}") diff --git a/yepcord/voice_gateway/main.py b/yepcord/voice_gateway/main.py new file mode 100644 index 0000000..68535c1 --- /dev/null +++ b/yepcord/voice_gateway/main.py @@ -0,0 +1,44 @@ +from asyncio import CancelledError + +from quart import Quart, websocket, Websocket + +from .gateway import Gateway + + +class YEPcord(Quart): + pass # Maybe it will be needed in the future + + +app = YEPcord("YEPcord-VoiceGateway") +gw = Gateway() + + +@app.after_request +async def set_cors_headers(response): + response.headers['Server'] = "YEPcord Voice Gateway" + response.headers['Access-Control-Allow-Origin'] = "*" + response.headers['Access-Control-Allow-Headers'] = "*" + response.headers['Access-Control-Allow-Methods'] = "*" + response.headers['Content-Security-Policy'] = "connect-src *;" + return response + + +@app.websocket("/") +async def ws_gateway_voice(): + # noinspection PyProtectedMember,PyUnresolvedReferences + ws: Websocket = websocket._get_current_object() + await gw.sendHello(ws) + while True: + try: + data = await ws.receive_json() + await gw.process(ws, data) + except CancelledError: + raise + +# ? +# register_tortoise( +# app, +# db_url=Config.DB_CONNECT_STRING, +# modules={"models": ["yepcord.yepcord.models"]}, +# generate_schemas=False, +# ) diff --git a/src/voice_gateway/schemas.py b/yepcord/voice_gateway/schemas.py similarity index 100% rename from src/voice_gateway/schemas.py rename to yepcord/voice_gateway/schemas.py diff --git a/yepcord/yepcord/enums.py b/yepcord/yepcord/enums.py index 061ccf4..abc5728 100644 --- a/yepcord/yepcord/enums.py +++ b/yepcord/yepcord/enums.py @@ -296,6 +296,26 @@ class ScheduledEventStatus(E): CANCELED = 4 +# https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-opcodes +class VoiceGatewayOp(E): + IDENTIFY = 0 + SELECT_PROTOCOL = 1 + READY = 2 + HEARTBEAT = 3 + SESSION_DESCRIPTION = 4 + SPEAKING = 5 + HEARTBEAT_ACK = 6 + RESUME = 7 + HELLO = 8 + RESUMED = 9 + CLIENT_DISCONNECT = 13 + UNKNOWN = 12 + SESSION_UPDATE = 14 + MEDIA_SINK_WANTS = 15 + VOICE_BACKEND_VERSION = 16 + CHANNEL_OPTIONS_UPDATE = 17 + + class ApplicationScope(E): ACTIVITIES_READ = "activities.read" ACTIVITIES_WRITE = "activities.write" From 179c46a321192da4b10590211a578213bf2f7820 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Tue, 12 Mar 2024 17:24:47 +0200 Subject: [PATCH 04/15] fix some errors/warnings --- yepcord/asgi.py | 1 + yepcord/cdn/main.py | 2 +- yepcord/gateway/events.py | 2 ++ yepcord/gateway/gateway.py | 2 +- yepcord/rest_api/models/channels.py | 1 + yepcord/rest_api/models/oauth2.py | 4 ++-- yepcord/rest_api/routes/other.py | 5 +++++ yepcord/rest_api/utils.py | 2 +- yepcord/rest_api/y_blueprint.py | 2 +- yepcord/yepcord/classes/gifs.py | 4 ++-- yepcord/yepcord/core.py | 5 +++-- yepcord/yepcord/gateway_dispatcher.py | 2 +- yepcord/yepcord/models/audit_log_entry.py | 2 +- yepcord/yepcord/models/channel.py | 2 +- yepcord/yepcord/models/userdata.py | 1 - yepcord/yepcord/mq_broker.py | 4 ++-- 16 files changed, 25 insertions(+), 16 deletions(-) diff --git a/yepcord/asgi.py b/yepcord/asgi.py index fddae44..1677927 100644 --- a/yepcord/asgi.py +++ b/yepcord/asgi.py @@ -21,6 +21,7 @@ from quart_schema import RequestSchemaValidationError, QuartSchema import yepcord.rest_api.main as rest_api import yepcord.gateway.main as gateway +import yepcord.voice_gateway.main as voice_gateway import yepcord.cdn.main as cdn import yepcord.remote_auth.main as remote_auth from yepcord.rest_api.routes import auth, connections diff --git a/yepcord/cdn/main.py b/yepcord/cdn/main.py index 83b097c..1f3461d 100644 --- a/yepcord/cdn/main.py +++ b/yepcord/cdn/main.py @@ -166,7 +166,7 @@ async def get_sticker(query_args: CdnImageSizeQuery, sticker_id: int, format_: s break else: sticker = await getStorage().getSticker(sticker_id, query_args.size, format_, - sticker.format in (StickerFormat.APNG, StickerFormat.GIF)) + sticker.format in (StickerFormat.APNG, StickerFormat.GIF)) if not sticker: return b'', 404 return sticker, 200, {"Content-Type": f"image/{format_}"} diff --git a/yepcord/gateway/events.py b/yepcord/gateway/events.py index c1b6afa..f8d5716 100644 --- a/yepcord/gateway/events.py +++ b/yepcord/gateway/events.py @@ -1054,6 +1054,8 @@ async def json(self) -> dict: "token_data": None, } } + + class VoiceStateUpdate(DispatchEvent): NAME = "VOICE_STATE_UPDATE" diff --git a/yepcord/gateway/gateway.py b/yepcord/gateway/gateway.py index f5537aa..b2f74ae 100644 --- a/yepcord/gateway/gateway.py +++ b/yepcord/gateway/gateway.py @@ -20,7 +20,7 @@ import warnings from json import dumps as jdumps -from typing import Optional, Union +from typing import Union from quart import Websocket from redis.asyncio import Redis diff --git a/yepcord/rest_api/models/channels.py b/yepcord/rest_api/models/channels.py index a3760e7..6fa87cd 100644 --- a/yepcord/rest_api/models/channels.py +++ b/yepcord/rest_api/models/channels.py @@ -248,6 +248,7 @@ def validate_title(cls, value: str): raise EmbedErr(makeEmbedError(27, f"title", {"length": "256"})) return value + # noinspection PyUnusedLocal @field_validator("type") def validate_type(cls, value: Optional[str]): return "rich" diff --git a/yepcord/rest_api/models/oauth2.py b/yepcord/rest_api/models/oauth2.py index 87eb82d..e8a3810 100644 --- a/yepcord/rest_api/models/oauth2.py +++ b/yepcord/rest_api/models/oauth2.py @@ -27,12 +27,12 @@ class AppAuthorizeGetQs(BaseModel): client_id: Optional[int] = None scope: Optional[str] = None - def __init__(self, *args, **kwargs): + def __init__(self, **kwargs): if "client_id" not in kwargs or not kwargs.get("client_id", "").strip(): raise InvalidDataErr(400, Errors.make(50035, {"client_id": { "code": "BASE_TYPE_REQUIRED", "message": "This field is required" }})) - super().__init__(*args, **kwargs) + super().__init__(**kwargs) class AppAuthorizePostQs(BaseModel): diff --git a/yepcord/rest_api/routes/other.py b/yepcord/rest_api/routes/other.py index 735ffe7..d01f14e 100644 --- a/yepcord/rest_api/routes/other.py +++ b/yepcord/rest_api/routes/other.py @@ -116,6 +116,7 @@ async def api_outboundpromotions(): return [] +# noinspection PyUnusedLocal @other.get("/api/v9/users/@me/applications//entitlements") async def api_users_me_applications_id_entitlements(aid): return [] @@ -187,6 +188,7 @@ async def api_users_me_settingsproto_3(): return {"settings": ""} +# noinspection PyUnusedLocal @other.route("/api/v9/users/@me/settings-proto/", methods=["GET", "PATCH"]) async def api_users_me_settingsproto_type(t): raise InvalidDataErr(400, Errors.make(50035, {"type": { @@ -196,16 +198,19 @@ async def api_users_me_settingsproto_type(t): }})) +# noinspection PyUnusedLocal @other.get("/api/v9/applications//skus") async def application_skus(app_id: int): return [] +# noinspection PyUnusedLocal @other.get("/api/v9/applications//subscription-group-listings") async def application_sub_group_list(app_id: int): return {"items": []} +# noinspection PyUnusedLocal @other.get("/api/v9/applications//listings") async def application_listings(app_id: int): return [] diff --git a/yepcord/rest_api/utils.py b/yepcord/rest_api/utils.py index 5fc2ec0..1d84564 100644 --- a/yepcord/rest_api/utils.py +++ b/yepcord/rest_api/utils.py @@ -29,7 +29,7 @@ import yepcord.yepcord.models as models from ..yepcord.classes.captcha import Captcha from ..yepcord.config import Config -from ..yepcord.ctx import Ctx, getCore, getCDNStorage +from ..yepcord.ctx import getCore, getCDNStorage from ..yepcord.enums import MessageType from ..yepcord.errors import Errors, InvalidDataErr from ..yepcord.models import Session, User, Channel, Attachment, Authorization, Bot, Webhook, Message diff --git a/yepcord/rest_api/y_blueprint.py b/yepcord/rest_api/y_blueprint.py index 45b7dc5..fb08752 100644 --- a/yepcord/rest_api/y_blueprint.py +++ b/yepcord/rest_api/y_blueprint.py @@ -22,7 +22,7 @@ from fast_depends import inject from flask.sansio.scaffold import T_route, setupmethod from quart import Blueprint, g -from quart_schema import validate_request, DataSource, validate_querystring +from quart_schema import validate_request, validate_querystring validate_funcs = {"body": validate_request, "qs": validate_querystring} diff --git a/yepcord/yepcord/classes/gifs.py b/yepcord/yepcord/classes/gifs.py index a13f5a5..03a374a 100644 --- a/yepcord/yepcord/classes/gifs.py +++ b/yepcord/yepcord/classes/gifs.py @@ -68,8 +68,8 @@ class Gifs(Singleton): def __init__(self, key: str = None, keep_searches: int = 100): self._key = key self._categories = [] - self._last_searches = [] - self._last_suggestions = [] + self._last_searches: list[GifSearchResult] = [] + self._last_suggestions: list[GifSuggestion] = [] self._last_update = 0 self._keep_searches = keep_searches diff --git a/yepcord/yepcord/core.py b/yepcord/yepcord/core.py index d41793f..46f337a 100644 --- a/yepcord/yepcord/core.py +++ b/yepcord/yepcord/core.py @@ -310,7 +310,8 @@ async def getChannelMessagesCount(self, channel: Channel) -> int: async def getPrivateChannels(self, user: User, with_hidden: bool = False) -> list[Channel]: channels = await Channel.filter(recipients__id=user.id).select_related("owner").all() - channels = [channel for channel in channels if not await self.isDmChannelHidden(user, channel)] + if not with_hidden: + channels = [channel for channel in channels if not await self.isDmChannelHidden(user, channel)] return [await self.setLastMessageIdForChannel(channel) for channel in channels] async def getChannelMessages(self, channel: Channel, limit: int, before: int = 0, after: int = 0) -> list[Message]: @@ -775,7 +776,7 @@ async def getGuildMembersGw(self, guild: Guild, query: str, limit: int, user_ids # noinspection PyUnresolvedReferences return await GuildMember.filter( Q(guild=guild) & - (Q(nick__startswith=query) | Q(user__userdatas__username__istartswith=query)) #& + (Q(nick__startswith=query) | Q(user__userdatas__username__istartswith=query)) #& #((GuildMember.user.id in user_ids) if user_ids else (GuildMember.user.id not in [0])) ).select_related("user").limit(limit).all() diff --git a/yepcord/yepcord/gateway_dispatcher.py b/yepcord/yepcord/gateway_dispatcher.py index 350fab0..8ecc6f3 100644 --- a/yepcord/yepcord/gateway_dispatcher.py +++ b/yepcord/yepcord/gateway_dispatcher.py @@ -76,7 +76,7 @@ async def dispatchSub(self, user_ids: list[int], guild_id: int = None, role_id: "role_id": role_id, }) - async def dispatchUnsub(self, user_ids: list[int], guild_id: int = None, role_id: int = None, delete = False) -> None: + async def dispatchUnsub(self, user_ids: list[int], guild_id: int = None, role_id: int = None, delete=False) -> None: await self.dispatchSys("unsub", { "user_ids": user_ids, "guild_id": guild_id, diff --git a/yepcord/yepcord/models/audit_log_entry.py b/yepcord/yepcord/models/audit_log_entry.py index a7d1ad9..397e04e 100644 --- a/yepcord/yepcord/models/audit_log_entry.py +++ b/yepcord/yepcord/models/audit_log_entry.py @@ -220,7 +220,7 @@ async def role_delete(user: models.User, role: models.Role) -> AuditLogEntry: @staticmethod async def bot_add(user: models.User, guild: models.Guild, bot: models.User) -> AuditLogEntry: return await AuditLogEntry.create(id=Snowflake.makeId(), guild=guild, user=user, target_id=bot.id, - action_type=AuditLogEntryType.BOT_ADD) + action_type=AuditLogEntryType.BOT_ADD) @staticmethod async def integration_create(user: models.User, guild: models.Guild, bot: models.User) -> AuditLogEntry: diff --git a/yepcord/yepcord/models/channel.py b/yepcord/yepcord/models/channel.py index ea67708..0950cba 100644 --- a/yepcord/yepcord/models/channel.py +++ b/yepcord/yepcord/models/channel.py @@ -72,7 +72,7 @@ async def ds_json(self, user_id: int=None, with_ids: bool=True) -> dict: userdata = await recipient.data recipients.append(userdata.ds_json) - base_data = { + base_data: dict = { "id": str(self.id), "type": self.type, } diff --git a/yepcord/yepcord/models/userdata.py b/yepcord/yepcord/models/userdata.py index 366a790..3554c8e 100644 --- a/yepcord/yepcord/models/userdata.py +++ b/yepcord/yepcord/models/userdata.py @@ -19,7 +19,6 @@ from datetime import date, timedelta from typing import Optional -from quart import g from tortoise import fields from tortoise.validators import MinValueValidator, MaxValueValidator diff --git a/yepcord/yepcord/mq_broker.py b/yepcord/yepcord/mq_broker.py index 3325048..953f4b6 100644 --- a/yepcord/yepcord/mq_broker.py +++ b/yepcord/yepcord/mq_broker.py @@ -174,8 +174,8 @@ def _handle(func): def getBroker() -> Union[RabbitBroker, RedisBroker, KafkaBroker, NatsBroker, WsBroker]: broker_type = Config.MESSAGE_BROKER["type"].lower() - assert broker_type in ("rabbitmq", "redis", "sqs", "kafka", "nats", "ws",), \ - "MESSAGE_BROKER.type must be one of ('rabbitmq', 'redis', 'sqs', 'kafka', 'nats', 'ws')" + assert broker_type in ("rabbitmq", "redis", "kafka", "nats", "ws",), \ + "MESSAGE_BROKER.type must be one of ('rabbitmq', 'redis', 'kafka', 'nats', 'ws')" if broker_type == "ws": warnings.warn("'ws' message broker type is used. This message broker type should not be used in production!") From d955d8b30dba2309494397edc53f8c2a97904cfa Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Tue, 12 Mar 2024 18:51:51 +0200 Subject: [PATCH 05/15] still not working --- poetry.lock | 26 +- pyproject.toml | 5 + yepcord/voice_gateway/default_sdp.py | 420 +++++++++++++++++++++++++++ yepcord/voice_gateway/gateway.py | 56 +++- 4 files changed, 497 insertions(+), 10 deletions(-) create mode 100644 yepcord/voice_gateway/default_sdp.py 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}}) From 11812ee33df196540292e59ad144c2654f421d33 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Tue, 12 Mar 2024 18:57:42 +0200 Subject: [PATCH 06/15] remove broken code & deps --- pyproject.toml | 5 ---- yepcord/voice_gateway/gateway.py | 51 +++++--------------------------- 2 files changed, 8 insertions(+), 48 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3479914..705d68f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,11 +77,6 @@ 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/gateway.py b/yepcord/voice_gateway/gateway.py index d4c1580..f8a78e2 100644 --- a/yepcord/voice_gateway/gateway.py +++ b/yepcord/voice_gateway/gateway.py @@ -11,14 +11,6 @@ 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): @@ -32,8 +24,6 @@ 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 @@ -60,12 +50,7 @@ async def handle_IDENTIFY(self, data: dict): self.rtx_ssrc = self._gw.ssrc self._gw.ssrc += 1 - 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, self._gw.endpoint.get_local_port())) + await self.esend(ReadyEvent(self.ssrc, self.video_ssrc, self.rtx_ssrc, 0)) @require_auth(4003) async def handle_HEARTBEAT(self, data: dict): @@ -80,29 +65,16 @@ async def handle_SELECT_PROTOCOL(self, data: dict): return await self.ws.close(4012) if d.protocol == "webrtc": - 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"m=audio {...} ICE/SDP\n" + + f"a=fingerprint:{...}\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" + f"a=rtcp:{...}\n" + + f"a=ice-ufrag:{...}\n" + + f"a=ice-pwd:{...}\n" + + f"a=fingerprint:{...}\n" + + f"a=candidate:1 1 {...} {...} {...} {...} typ host\n" ) await self.esend(RtcSessionDescriptionEvent(answer)) @@ -128,14 +100,7 @@ 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}}) From 501b016df9f7b15295d7e2fb022171f1140c4429 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Tue, 12 Mar 2024 19:01:37 +0200 Subject: [PATCH 07/15] fix poetry lock file --- poetry.lock | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/poetry.lock b/poetry.lock index 274f16d..4744b1b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1712,19 +1712,6 @@ 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" @@ -1929,17 +1916,6 @@ 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" @@ -2404,4 +2380,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "cc33c982864a4f552c392e441069bdb9b1f53f2ed515cbad4b6302b23dff4be1" +content-hash = "2f385ba951cf4e1ff99d1fca6e27d9ec8ca9278cb952331480b735dd60677fb1" From b6f83852dcb53a900690ea147e8b3c1f7231f7a3 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Fri, 15 Mar 2024 18:34:23 +0200 Subject: [PATCH 08/15] idk --- poetry.lock | 13 +++++- pyproject.toml | 1 + yepcord/voice_gateway/gateway.py | 74 ++++++++++++++++++++++++++++---- yepcord/voice_gateway/utils.py | 40 +++++++++++++++++ yepcord/yepcord/enums.py | 2 +- 5 files changed, 119 insertions(+), 11 deletions(-) create mode 100644 yepcord/voice_gateway/utils.py diff --git a/poetry.lock b/poetry.lock index 4744b1b..f2eca71 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1916,6 +1916,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 +2391,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 = "bf743d7dc835dde9db90bf7c8e748e6f788b45fbe7245e10e51de455dd077fd9" diff --git a/pyproject.toml b/pyproject.toml index 705d68f..8375030 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,7 @@ yc-protobuf3-to-dict = "^0.3.0" s3lite = "^0.1.4" fast-depends = ">=2.4.2" faststream = {extras = ["kafka", "nats", "rabbit", "redis"], version = "^0.4.7"} +semanticsdp = "^0.1.0b5" [tool.poetry.group.dev.dependencies] pytest-cov = "4.1.0" diff --git a/yepcord/voice_gateway/gateway.py b/yepcord/voice_gateway/gateway.py index f8a78e2..2fc1b33 100644 --- a/yepcord/voice_gateway/gateway.py +++ b/yepcord/voice_gateway/gateway.py @@ -3,12 +3,15 @@ from os import urandom from typing import Optional +from httpx import AsyncClient from quart import Websocket +from semanticsdp import SDPInfo 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 .utils import convert_rtp_properties from ..gateway.utils import require_auth @@ -24,6 +27,7 @@ def __init__(self, ws: Websocket, gw: Gateway): self.rtx_ssrc = 0 self.mode: Optional[str] = None self.key: Optional[bytes] = None + self.sdp = SDPInfo.from_dict(DEFAULT_SDP) self._gw = gw @@ -50,11 +54,13 @@ async def handle_IDENTIFY(self, data: dict): self.rtx_ssrc = self._gw.ssrc self._gw.ssrc += 1 - await self.esend(ReadyEvent(self.ssrc, self.video_ssrc, self.rtx_ssrc, 0)) + await self.esend( + ReadyEvent(self.ssrc, self.video_ssrc, self.rtx_ssrc, await self._gw.get_channel_port(self.guild_id)) + ) @require_auth(4003) async def handle_HEARTBEAT(self, data: dict): - await self.send({"op": VoiceGatewayOp.HEARTBEAT_ACK, "d": data["d"]}) + await self.send({"op": VoiceGatewayOp.HEARTBEAT_ACK, "d": data}) @require_auth(4003) async def handle_SELECT_PROTOCOL(self, data: dict): @@ -65,16 +71,42 @@ async def handle_SELECT_PROTOCOL(self, data: dict): return await self.ws.close(4012) if d.protocol == "webrtc": + offer = SDPInfo.parse(f"m=audio\n{d.sdp}") + self.sdp.ice = offer.ice + self.sdp.dtls = offer.dtls + + port = await self._gw.get_channel_port(self.guild_id) + _, fingerprint = await self._gw.get_media_server_info() + + data = { + "local": {"ice": {"ufrag": urandom(8).hex(), "pwd": urandom(16).hex()}}, + "remote": { + "ice": {"ufrag": self.sdp.ice.ufrag, "pwd": self.sdp.ice.pwd}, + "dtls": { + "hash": self.sdp.dtls.hash, + "fingerprint": self.sdp.dtls.fingerprint, + "setup": self.sdp.dtls.setup.value + }, + }, + "props": convert_rtp_properties(self.sdp), + "strpProtectionProfiles": "", + "disableSTUNKeepAlive": False, + "disableREMB": False, + } + + async with AsyncClient() as cl: + resp = await cl.post(f"http://127.0.0.1:9999/v1/channels/{self.guild_id}/users/{self.user_id}", + json=data) answer = ( - f"m=audio {...} ICE/SDP\n" + - f"a=fingerprint:{...}\n" + + f"m=audio {port} ICE/SDP\n" + + f"a=fingerprint:sha-256 {fingerprint}\n" + f"c=IN IP4 127.0.0.1\n" + - f"a=rtcp:{...}\n" + - f"a=ice-ufrag:{...}\n" + - f"a=ice-pwd:{...}\n" + - f"a=fingerprint:{...}\n" + - f"a=candidate:1 1 {...} {...} {...} {...} typ host\n" + f"a=rtcp:{port}\n" + + f"a=ice-ufrag:{data['local']['ice']['ufrag']}\n" + + f"a=ice-pwd:{data['local']['ice']['pwd']}\n" + + f"a=fingerprint:sha-256 {fingerprint}\n" + + f"a=candidate:1 1 UDP 2130706431 127.0.0.1 {port} typ host\n" ) await self.esend(RtcSessionDescriptionEvent(answer)) @@ -99,6 +131,30 @@ async def handle_VOICE_BACKEND_VERSION(self, data: dict) -> None: class Gateway: def __init__(self): self.ssrc = 1 + self.address: Optional[str] = None + self.fingerprint: Optional[str] = None + + self.channels = {} + + async def get_media_server_info(self) -> tuple[str, str]: + if self.address is None or self.fingerprint is None: + async with AsyncClient() as cl: + resp = await cl.get("http://127.0.0.1:9999/v1") + j = resp.json() + return j["address"], j["fingerprint"] + self.address = j["address"] + self.fingerprint = j["fingerprint"] + + return self.address, self.fingerprint + + async def get_channel_port(self, channel_id: int) -> int: + if channel_id not in self.channels: + async with AsyncClient() as cl: + resp = await cl.post(f"http://127.0.0.1:9999/v1/channels/{channel_id}") + return resp.json()["port"] + self.channels[channel_id] = resp.json()["port"] + + return self.channels[channel_id] async def sendHello(self, ws: Websocket) -> None: client = GatewayClient(ws, self) diff --git a/yepcord/voice_gateway/utils.py b/yepcord/voice_gateway/utils.py new file mode 100644 index 0000000..2a82f76 --- /dev/null +++ b/yepcord/voice_gateway/utils.py @@ -0,0 +1,40 @@ +from typing import Optional + +from semanticsdp import MediaInfo, SDPInfo + + +def get_sdp_media(sdp: SDPInfo, media_type: str) -> Optional[MediaInfo]: + for media in sdp.medias: + if media.type == media_type: + return media + + +def _to_properties(media: MediaInfo, props: dict) -> None: + if not media: + return + + props[f"{media.type}.codecs.length"] = str(len(media.codecs)) + props[f"{media.type}.ext.length"] = str(len(media.extensions)) + + for idx, codec in enumerate(media.codecs.values()): + item = f"{media.type}.codecs.{idx}" + props[f"{item}.codec"] = str(codec.codec) + props[f"{item}.pt"] = str(codec.type) + if codec.rtx: + props[f"{item}.rtx"] = str(codec.rtx) + + for idx, (ext_id, ext_uri) in enumerate(media.extensions.items()): + props[f"{media.type}.ext.{idx}.id"] = str(ext_id) + props[f"{media.type}.ext.{idx}.uri"] = str(ext_uri) + + +def convert_rtp_properties(sdp: SDPInfo) -> dict: + audio = get_sdp_media(sdp, "audio") + video = get_sdp_media(sdp, "video") + + props = {} + + _to_properties(audio, props) + _to_properties(video, props) + + return props diff --git a/yepcord/yepcord/enums.py b/yepcord/yepcord/enums.py index 6b97f87..7fbba2a 100644 --- a/yepcord/yepcord/enums.py +++ b/yepcord/yepcord/enums.py @@ -309,7 +309,7 @@ class VoiceGatewayOp(E): HELLO = 8 RESUMED = 9 CLIENT_DISCONNECT = 13 - UNKNOWN = 12 + VIDEO = 12 SESSION_UPDATE = 14 MEDIA_SINK_WANTS = 15 VOICE_BACKEND_VERSION = 16 From ff44df59148b968ef1fffd7b9d41ad169d58abd0 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Sun, 17 Mar 2024 19:51:26 +0200 Subject: [PATCH 09/15] testing with pion-webrtc as backend --- yepcord/voice_gateway/gateway.py | 78 +++++++++++++++++++++----------- yepcord/yepcord/enums.py | 2 +- 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/yepcord/voice_gateway/gateway.py b/yepcord/voice_gateway/gateway.py index 2fc1b33..652294b 100644 --- a/yepcord/voice_gateway/gateway.py +++ b/yepcord/voice_gateway/gateway.py @@ -1,11 +1,15 @@ from __future__ import annotations +import json +from asyncio import get_event_loop from os import urandom -from typing import Optional +from typing import Optional, Any +import websockets from httpx import AsyncClient from quart import Websocket from semanticsdp import SDPInfo +from websockets import WebSocketClientProtocol from yepcord.yepcord.enums import VoiceGatewayOp from .default_sdp import DEFAULT_SDP @@ -30,6 +34,38 @@ def __init__(self, ws: Websocket, gw: Gateway): self.sdp = SDPInfo.from_dict(DEFAULT_SDP) self._gw = gw + self._ws: Optional[WebSocketClientProtocol] = None + + async def connect_pion(self): + async with websockets.connect("ws://127.0.0.1:8088/voice") as websocket: + self._ws = websocket + while True: + msg = json.loads(await websocket.recv()) + print(f" From pion: {msg}") + if msg["op"] == VoiceGatewayOp.SESSION_DESCRIPTION: + s = SDPInfo.parse(msg["d"]["sdp"]) + fp = s.dtls.fingerprint + ufrag = s.ice.ufrag + pwd = s.ice.pwd + msg["d"]["sdp"] = ( + f"m=audio 3791 ICE/SDP\n" + + f"a=fingerprint:sha-256 {fp}\n" + + f"c=IN IP4 192.168.0.114\n" + + f"a=rtcp:3791\n" + + f"a=ice-ufrag:{ufrag}\n" + + f"a=ice-pwd:{pwd}\n" + + f"a=fingerprint:sha-256 {fp}\n" + + f"a=candidate:366543523 1 udp 2130706431 192.168.0.114 3791 typ host\n" + ) + print("patched sdp") + await self.ws.send_json(msg) + + async def fwd(self, op: int, d: Any) -> None: + if self._ws is None: + return await self.ws.close(4004) + data = {"op": op, "d": d} + print(f" To pion: {data}") + return await self._ws.send(json.dumps(data)) async def send(self, data: dict): await self.ws.send_json(data) @@ -38,6 +74,8 @@ async def esend(self, event: Event): await self.send(await event.json()) async def handle_IDENTIFY(self, data: dict): + return await self.fwd(VoiceGatewayOp.IDENTIFY, data) + print(f"Connected to voice with session_id={data['session_id']}") if data["token"] != "idk_token": return await self.ws.close(4004) @@ -58,11 +96,13 @@ async def handle_IDENTIFY(self, data: dict): ReadyEvent(self.ssrc, self.video_ssrc, self.rtx_ssrc, await self._gw.get_channel_port(self.guild_id)) ) - @require_auth(4003) + #@require_auth(4003) async def handle_HEARTBEAT(self, data: dict): + return await self.fwd(VoiceGatewayOp.HEARTBEAT, data) + await self.send({"op": VoiceGatewayOp.HEARTBEAT_ACK, "d": data}) - @require_auth(4003) + #@require_auth(4003) async def handle_SELECT_PROTOCOL(self, data: dict): try: d = SelectProtocol(**data) @@ -74,29 +114,11 @@ async def handle_SELECT_PROTOCOL(self, data: dict): offer = SDPInfo.parse(f"m=audio\n{d.sdp}") self.sdp.ice = offer.ice self.sdp.dtls = offer.dtls - - port = await self._gw.get_channel_port(self.guild_id) - _, fingerprint = await self._gw.get_media_server_info() - - data = { - "local": {"ice": {"ufrag": urandom(8).hex(), "pwd": urandom(16).hex()}}, - "remote": { - "ice": {"ufrag": self.sdp.ice.ufrag, "pwd": self.sdp.ice.pwd}, - "dtls": { - "hash": self.sdp.dtls.hash, - "fingerprint": self.sdp.dtls.fingerprint, - "setup": self.sdp.dtls.setup.value - }, - }, - "props": convert_rtp_properties(self.sdp), - "strpProtectionProfiles": "", - "disableSTUNKeepAlive": False, - "disableREMB": False, - } - async with AsyncClient() as cl: - resp = await cl.post(f"http://127.0.0.1:9999/v1/channels/{self.guild_id}/users/{self.user_id}", - json=data) + sdp = "v=0\r\n"+str(self.sdp)+"\r\n" + print("gen sdp") + + return await self.fwd(VoiceGatewayOp.SELECT_PROTOCOL, data | {"data": sdp, "sdp": sdp}) answer = ( f"m=audio {port} ICE/SDP\n" + @@ -118,13 +140,15 @@ async def handle_SELECT_PROTOCOL(self, data: dict): self.key = urandom(32) await self.esend(UdpSessionDescriptionEvent(self.mode, self.key)) - @require_auth(4003) + #@require_auth(4003) async def handle_SPEAKING(self, data: dict): if self.ssrc != data["ssrc"] or data["ssrc"] < 1: return await self.ws.close(4014) await self.esend(SpeakingEvent(self.ssrc, self.user_id, data["speaking"])) async def handle_VOICE_BACKEND_VERSION(self, data: dict) -> None: + return await self.fwd(VoiceGatewayOp.VOICE_BACKEND_VERSION, data) + await self.send({"op": VoiceGatewayOp.VOICE_BACKEND_VERSION, "d": {"voice": "0.11.0", "rtc_worker": "0.4.11"}}) @@ -159,6 +183,8 @@ async def get_channel_port(self, channel_id: int) -> int: async def sendHello(self, ws: Websocket) -> None: client = GatewayClient(ws, self) setattr(ws, "_yepcord_client", client) + return get_event_loop().create_task(client.connect_pion()) + await ws.send_json({"op": VoiceGatewayOp.HELLO, "d": {"v": 7, "heartbeat_interval": 13750}}) async def process(self, ws: Websocket, data: dict) -> None: diff --git a/yepcord/yepcord/enums.py b/yepcord/yepcord/enums.py index 7fbba2a..c5da06a 100644 --- a/yepcord/yepcord/enums.py +++ b/yepcord/yepcord/enums.py @@ -308,8 +308,8 @@ class VoiceGatewayOp(E): RESUME = 7 HELLO = 8 RESUMED = 9 - CLIENT_DISCONNECT = 13 VIDEO = 12 + CLIENT_DISCONNECT = 13 SESSION_UPDATE = 14 MEDIA_SINK_WANTS = 15 VOICE_BACKEND_VERSION = 16 From 17f6758c7c08a21acf2022969c1a24a4a023f4e5 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Mon, 18 Mar 2024 14:54:30 +0200 Subject: [PATCH 10/15] testing with pion-webrtc as backend --- config.example.py | 3 + yepcord/voice_gateway/events.py | 5 +- yepcord/voice_gateway/gateway.py | 164 ++++++++++++++----------------- yepcord/yepcord/config.py | 1 + 4 files changed, 80 insertions(+), 93 deletions(-) diff --git a/config.example.py b/config.example.py index eb9db8e..a27eb6d 100644 --- a/config.example.py +++ b/config.example.py @@ -113,3 +113,6 @@ "client_secret": None, }, } + +# Voice workers addresses +VOICE_WORKERS = [] diff --git a/yepcord/voice_gateway/events.py b/yepcord/voice_gateway/events.py index 72a9055..a0d1e6b 100644 --- a/yepcord/voice_gateway/events.py +++ b/yepcord/voice_gateway/events.py @@ -10,10 +10,11 @@ async def json(self) -> dict: ... class ReadyEvent(Event): OP = VoiceGatewayOp.READY - def __init__(self, ssrc: int, video_ssrc: int, rtx_ssrc: int, port: int): + def __init__(self, ssrc: int, video_ssrc: int, rtx_ssrc: int, ip: str, port: int): self.ssrc = ssrc self.video_ssrc = video_ssrc self.rtx_ssrc = rtx_ssrc + self.ip = ip self.port = port async def json(self) -> dict: @@ -21,7 +22,7 @@ async def json(self) -> dict: "op": self.OP, "d": { "ssrc": self.ssrc, - "ip": "127.0.0.1", + "ip": self.ip, "port": self.port, "modes": ["xsalsa20_poly1305", "xsalsa20_poly1305_suffix", "xsalsa20_poly1305_lite", "xsalsa20_poly1305_lite_rtpsize", "aead_aes256_gcm", "aead_aes256_gcm_rtpsize"], diff --git a/yepcord/voice_gateway/gateway.py b/yepcord/voice_gateway/gateway.py index 652294b..6296c53 100644 --- a/yepcord/voice_gateway/gateway.py +++ b/yepcord/voice_gateway/gateway.py @@ -1,22 +1,46 @@ from __future__ import annotations -import json -from asyncio import get_event_loop from os import urandom -from typing import Optional, Any +from typing import Optional -import websockets from httpx import AsyncClient from quart import Websocket -from semanticsdp import SDPInfo -from websockets import WebSocketClientProtocol +from semanticsdp import SDPInfo, Setup 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 .utils import convert_rtp_properties from ..gateway.utils import require_auth +from ..yepcord.config import Config + + +class GoRpc: + def __init__(self, rpc_addr: str): + self._address = f"http://{rpc_addr}/rpc" + + async def create_endpoint(self, channel_id: int) -> Optional[int]: + async with AsyncClient() as cl: + resp = await cl.post(self._address, json={ + # TODO: auto port allocation (on golang side) + "id": 0, "method": "Rpc.CreateApi", "params": [{"channel_id": str(channel_id), "port": 3791}] + }) + j = resp.json() + if j["error"] is None: + return j["result"] + print(j["error"]) + + async def create_peer_connection(self, channel_id: int, session_id: int, offer: str) -> Optional[str]: + async with AsyncClient() as cl: + resp = await cl.post(self._address, json={ + "id": 0, "method": "Rpc.NewPeerConnection", "params": [ + {"channel_id": str(channel_id), "session_id": str(session_id), "offer": offer} + ] + }) + j = resp.json() + if j["error"] is None: + return j["result"] + print(j["error"]) class GatewayClient: @@ -34,38 +58,6 @@ def __init__(self, ws: Websocket, gw: Gateway): self.sdp = SDPInfo.from_dict(DEFAULT_SDP) self._gw = gw - self._ws: Optional[WebSocketClientProtocol] = None - - async def connect_pion(self): - async with websockets.connect("ws://127.0.0.1:8088/voice") as websocket: - self._ws = websocket - while True: - msg = json.loads(await websocket.recv()) - print(f" From pion: {msg}") - if msg["op"] == VoiceGatewayOp.SESSION_DESCRIPTION: - s = SDPInfo.parse(msg["d"]["sdp"]) - fp = s.dtls.fingerprint - ufrag = s.ice.ufrag - pwd = s.ice.pwd - msg["d"]["sdp"] = ( - f"m=audio 3791 ICE/SDP\n" + - f"a=fingerprint:sha-256 {fp}\n" + - f"c=IN IP4 192.168.0.114\n" + - f"a=rtcp:3791\n" + - f"a=ice-ufrag:{ufrag}\n" + - f"a=ice-pwd:{pwd}\n" + - f"a=fingerprint:sha-256 {fp}\n" + - f"a=candidate:366543523 1 udp 2130706431 192.168.0.114 3791 typ host\n" - ) - print("patched sdp") - await self.ws.send_json(msg) - - async def fwd(self, op: int, d: Any) -> None: - if self._ws is None: - return await self.ws.close(4004) - data = {"op": op, "d": d} - print(f" To pion: {data}") - return await self._ws.send(json.dumps(data)) async def send(self, data: dict): await self.ws.send_json(data) @@ -74,15 +66,13 @@ async def esend(self, event: Event): await self.send(await event.json()) async def handle_IDENTIFY(self, data: dict): - return await self.fwd(VoiceGatewayOp.IDENTIFY, data) - print(f"Connected to voice with session_id={data['session_id']}") if data["token"] != "idk_token": return await self.ws.close(4004) self.user_id = int(data["user_id"]) self.session_id = data["session_id"] - self.guild_id = data["server_id"] + self.guild_id = int(data["server_id"]) self.token = data["token"] self.ssrc = self._gw.ssrc @@ -92,18 +82,24 @@ async def handle_IDENTIFY(self, data: dict): self.rtx_ssrc = self._gw.ssrc self._gw.ssrc += 1 - await self.esend( - ReadyEvent(self.ssrc, self.video_ssrc, self.rtx_ssrc, await self._gw.get_channel_port(self.guild_id)) - ) + ip = "127.0.0.1" # TODO + port = 0 + rpc = self._gw.rpc(self.guild_id) + if rpc is not None: + port = await rpc.create_endpoint(self.guild_id) - #@require_auth(4003) - async def handle_HEARTBEAT(self, data: dict): - return await self.fwd(VoiceGatewayOp.HEARTBEAT, data) + await self.esend(ReadyEvent(self.ssrc, self.video_ssrc, self.rtx_ssrc, ip, port)) + @require_auth(4003) + async def handle_HEARTBEAT(self, data: dict): await self.send({"op": VoiceGatewayOp.HEARTBEAT_ACK, "d": data}) - #@require_auth(4003) + @require_auth(4003) async def handle_SELECT_PROTOCOL(self, data: dict): + rpc = self._gw.rpc(self.guild_id) + if rpc is None: + return + try: d = SelectProtocol(**data) except Exception as e: @@ -114,22 +110,25 @@ async def handle_SELECT_PROTOCOL(self, data: dict): offer = SDPInfo.parse(f"m=audio\n{d.sdp}") self.sdp.ice = offer.ice self.sdp.dtls = offer.dtls - - sdp = "v=0\r\n"+str(self.sdp)+"\r\n" - print("gen sdp") - - return await self.fwd(VoiceGatewayOp.SELECT_PROTOCOL, data | {"data": sdp, "sdp": sdp}) - - answer = ( - f"m=audio {port} ICE/SDP\n" + - f"a=fingerprint:sha-256 {fingerprint}\n" + - f"c=IN IP4 127.0.0.1\n" + - f"a=rtcp:{port}\n" + - f"a=ice-ufrag:{data['local']['ice']['ufrag']}\n" + - f"a=ice-pwd:{data['local']['ice']['pwd']}\n" + - f"a=fingerprint:sha-256 {fingerprint}\n" + - f"a=candidate:1 1 UDP 2130706431 127.0.0.1 {port} typ host\n" - ) + self.sdp.dtls.setup = Setup.ACTIVE + + sdp = "v=0\r\n" + str(self.sdp) + "\r\n" + + answer = await rpc.create_peer_connection(self.guild_id, self.session_id, sdp) + + sdp = SDPInfo.parse(answer) + c = sdp.candidates[0] + port = c.port + answer = "\n".join([ + f"m=audio {port} ICE/SDP", + f"a=fingerprint:{sdp.dtls.hash} {sdp.dtls.fingerprint}", + f"c=IN IP4 {c.address}", + f"a=rtcp:{port}", + f"a=ice-ufrag:{sdp.ice.ufrag}", + f"a=ice-pwd:{sdp.ice.pwd}", + f"a=fingerprint:{sdp.dtls.hash} {sdp.dtls.fingerprint}", + f"a=candidate:{c.foundation} 1 {c.transport} {c.priority} {c.address} {port} typ host", + ]) + "\n" await self.esend(RtcSessionDescriptionEvent(answer)) elif d.protocol == "udp": @@ -140,51 +139,34 @@ async def handle_SELECT_PROTOCOL(self, data: dict): self.key = urandom(32) await self.esend(UdpSessionDescriptionEvent(self.mode, self.key)) - #@require_auth(4003) + @require_auth(4003) async def handle_SPEAKING(self, data: dict): if self.ssrc != data["ssrc"] or data["ssrc"] < 1: return await self.ws.close(4014) await self.esend(SpeakingEvent(self.ssrc, self.user_id, data["speaking"])) async def handle_VOICE_BACKEND_VERSION(self, data: dict) -> None: - return await self.fwd(VoiceGatewayOp.VOICE_BACKEND_VERSION, data) - await self.send({"op": VoiceGatewayOp.VOICE_BACKEND_VERSION, "d": {"voice": "0.11.0", "rtc_worker": "0.4.11"}}) class Gateway: def __init__(self): self.ssrc = 1 - self.address: Optional[str] = None - self.fingerprint: Optional[str] = None - self.channels = {} + self._rpcs: dict[int, GoRpc] = {} - async def get_media_server_info(self) -> tuple[str, str]: - if self.address is None or self.fingerprint is None: - async with AsyncClient() as cl: - resp = await cl.get("http://127.0.0.1:9999/v1") - j = resp.json() - return j["address"], j["fingerprint"] - self.address = j["address"] - self.fingerprint = j["fingerprint"] + def rpc(self, guild_id: int) -> Optional[GoRpc]: + if not (workers := Config.VOICE_WORKERS): + return + idx = guild_id % len(workers) + if idx not in self._rpcs: + self._rpcs[idx] = GoRpc(workers[idx]) - return self.address, self.fingerprint - - async def get_channel_port(self, channel_id: int) -> int: - if channel_id not in self.channels: - async with AsyncClient() as cl: - resp = await cl.post(f"http://127.0.0.1:9999/v1/channels/{channel_id}") - return resp.json()["port"] - self.channels[channel_id] = resp.json()["port"] - - return self.channels[channel_id] + return self._rpcs[idx] async def sendHello(self, ws: Websocket) -> None: client = GatewayClient(ws, self) setattr(ws, "_yepcord_client", client) - return get_event_loop().create_task(client.connect_pion()) - await ws.send_json({"op": VoiceGatewayOp.HELLO, "d": {"v": 7, "heartbeat_interval": 13750}}) async def process(self, ws: Websocket, data: dict) -> None: diff --git a/yepcord/yepcord/config.py b/yepcord/yepcord/config.py index 0e45bf0..5335aa6 100644 --- a/yepcord/yepcord/config.py +++ b/yepcord/yepcord/config.py @@ -133,6 +133,7 @@ class ConfigModel(BaseModel): BCRYPT_ROUNDS: int = 15 CAPTCHA: ConfigCaptcha = Field(default_factory=ConfigCaptcha) CONNECTIONS: ConfigConnections = Field(default_factory=ConfigConnections) + VOICE_WORKERS: list[str] = Field(default_factory=list) @field_validator("KEY") def validate_key(cls, value: str) -> str: From 896bcd4ce5ebc0e80c09b7169c3d8129a8dd2e4c Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Mon, 18 Mar 2024 18:08:39 +0200 Subject: [PATCH 11/15] implement connecting/disconnecting from voice channel --- config.example.py | 1 + yepcord/gateway/events.py | 30 ++++++------- yepcord/gateway/gateway.py | 61 +++++++++++++++++++++------ yepcord/voice_gateway/gateway.py | 52 +++++++++-------------- yepcord/voice_gateway/go_rpc.py | 30 +++++++++++++ yepcord/voice_gateway/main.py | 16 ++++--- yepcord/yepcord/config.py | 1 + yepcord/yepcord/gateway_dispatcher.py | 6 ++- yepcord/yepcord/models/__init__.py | 1 + yepcord/yepcord/models/voice_state.py | 37 ++++++++++++++++ 10 files changed, 164 insertions(+), 71 deletions(-) create mode 100644 yepcord/voice_gateway/go_rpc.py create mode 100644 yepcord/yepcord/models/voice_state.py diff --git a/config.example.py b/config.example.py index a27eb6d..dbf76fc 100644 --- a/config.example.py +++ b/config.example.py @@ -13,6 +13,7 @@ PUBLIC_HOST = "127.0.0.1:8080" GATEWAY_HOST = "127.0.0.1:8000/gateway" +VOICE_GATEWAY_HOST = "127.0.0.1:8000/voice" CDN_HOST = "127.0.0.1:8000/media" STORAGE = { diff --git a/yepcord/gateway/events.py b/yepcord/gateway/events.py index f8d5716..2c2b7b1 100644 --- a/yepcord/gateway/events.py +++ b/yepcord/gateway/events.py @@ -29,7 +29,7 @@ from ..yepcord.snowflake import Snowflake if TYPE_CHECKING: # pragma: no cover - from ..yepcord.models import Channel, Invite, GuildMember, UserData, User, UserSettings, Guild + from ..yepcord.models import Channel, Invite, GuildMember, UserData, User, UserSettings, Guild, VoiceState from ..yepcord.core import Core from .gateway import GatewayClient from .presences import Presence @@ -173,16 +173,16 @@ def __init__(self, friends_presences: list[dict], guilds_ids: list[int]): self.guilds_ids = guilds_ids async def json(self) -> dict: - g = [{"voice_states": [], "id": str(i), "embedded_activities": []} for i in self.guilds_ids] # TODO + g = [{"voice_states": [], "id": str(i), "embedded_activities": []} for i in self.guilds_ids] # TODO: add voice states return { "t": self.NAME, "op": self.OP, "d": { "merged_presences": { - "guilds": [[]], # TODO + "guilds": [[]], "friends": self.friends_presences }, - "merged_members": [[]], # TODO + "merged_members": [[]], "guilds": g } } @@ -1059,7 +1059,7 @@ async def json(self) -> dict: class VoiceStateUpdate(DispatchEvent): NAME = "VOICE_STATE_UPDATE" - def __init__(self, user_id: int, session_id: str, channel: Channel, guild: Optional[Guild], + def __init__(self, user_id: int, session_id: str, channel: Optional[Channel], guild: Optional[Guild], member: Optional[GuildMember], **kwargs): self.user_id = user_id self.session_id = session_id @@ -1074,7 +1074,7 @@ async def json(self) -> dict: "op": self.OP, "d": { "user_id": str(self.user_id), - "channel_id": str(self.channel.id), + "channel_id": str(self.channel.id) if self.channel is not None else None, "deaf": False, "mute": False, "session_id": self.session_id, @@ -1090,22 +1090,22 @@ async def json(self) -> dict: class VoiceServerUpdate(DispatchEvent): NAME = "VOICE_SERVER_UPDATE" - def __init__(self, channel: Channel, guild: Optional[Guild]): - self.channel = channel - self.guild = guild + def __init__(self, voice_state: VoiceState): + self.state = voice_state async def json(self) -> dict: data = { "t": self.NAME, "op": self.OP, "d": { - "token": "idk_token", - "endpoint": "127.0.0.1:8000/voice" + "token": f"{self.state.id}.{self.state.token}", + "endpoint": Config.VOICE_GATEWAY_HOST } } - if self.guild: - data["d"]["guild_id"] = str(self.guild.id) - if self.channel: - data["d"]["channel_id"] = str(self.channel.id) + if self.state.guild: + data["d"]["guild_id"] = str(self.state.guild.id) + if self.state.channel: + data["d"]["channel_id"] = str(self.state.channel.id) + print(data) return data diff --git a/yepcord/gateway/gateway.py b/yepcord/gateway/gateway.py index b2f74ae..37341dc 100644 --- a/yepcord/gateway/gateway.py +++ b/yepcord/gateway/gateway.py @@ -31,8 +31,10 @@ from ..yepcord.classes.fakeredis import FakeRedis from ..yepcord.core import Core from ..yepcord.ctx import getCore -from ..yepcord.enums import GatewayOp +from ..yepcord.enums import GatewayOp, GuildPermissions +from ..yepcord.gateway_dispatcher import GatewayDispatcher from ..yepcord.models import Session, User, UserSettings, Bot, GuildMember +from ..yepcord.models.voice_state import VoiceState from ..yepcord.mq_broker import getBroker @@ -180,22 +182,53 @@ async def handle_VOICE_STATE(self, data: dict) -> None: self_mute = bool(data.get("self_mute")) self_deaf = bool(data.get("self_deaf")) - if not (channel := await getCore().getChannel(data.get("channel_id"))): return - if not await getCore().getUserByChannel(channel, self.user_id): return - - guild = None - member = None - if guild_id := data.get("guild_id"): - if (guild := await getCore().getGuild(guild_id)) is None \ - or (member := await getCore().getGuildMember(guild, self.user_id)) is None: + voice_state = await VoiceState.get_or_none(user__id=self.user_id).select_related("channel", "guild") + if voice_state is not None: + if voice_state.session_id != self.sid and data["channel_id"] is None: return + if str(voice_state.guild.id) != data["guild_id"] and voice_state.session_id == self.sid: + member = await getCore().getGuildMember(voice_state.guild, self.user_id) + voice_event = VoiceStateUpdate( + self.id, self.sid, None, voice_state.guild, member, self_mute=self_mute, self_deaf=self_deaf + ) + await self.gateway.broker.publish(channel="yepcord_events", message={ + "data": await voice_event.json(), + "event": voice_event.NAME, + **(await GatewayDispatcher.getChannelFilter(voice_state.channel, GuildPermissions.VIEW_CHANNEL)) + }) + if data["guild_id"] is None: + return await voice_state.delete() - print(f"Connecting to voice with session_id={self.sid}") + if data["channel_id"] is None or data["guild_id"] is None: + return + if not (channel := await getCore().getChannel(data["channel_id"])): return + if not await getCore().getUserByChannel(channel, self.user_id): return + if (guild := await getCore().getGuild(data["guild_id"])) is None or channel.guild != guild or \ + (member := await getCore().getGuildMember(guild, self.user_id)) is None: + return - await self.esend(VoiceStateUpdate( - self.id, self.sid, channel, guild, member, self_mute=self_mute, self_deaf=self_deaf - )) - await self.esend(VoiceServerUpdate(channel, guild)) + if voice_state is not None: + await voice_state.update(guild=guild, channel=channel, session_id=self.sid) + else: + voice_state = await VoiceState.create(guild=guild, channel=channel, user=member.user, session_id=self.sid) + + if member is None: + member = await getCore().getGuildMember(voice_state.guild, self.user_id) + + voice_event = VoiceStateUpdate( + self.id, self.sid, voice_state.channel, voice_state.guild, member, self_mute=self_mute, self_deaf=self_deaf + ) + await self.gateway.mcl_yepcordEventsCallback({ + "data": await voice_event.json(), + "event": voice_event.NAME, + "user_ids": None, + "guild_id": None, + "role_ids": None, + "session_id": None, + "exclude": [], + } | await GatewayDispatcher.getChannelFilter(voice_state.channel, GuildPermissions.VIEW_CHANNEL)) + await self.esend(VoiceServerUpdate(voice_state)) + print("should connect now") class GatewayEvents: diff --git a/yepcord/voice_gateway/gateway.py b/yepcord/voice_gateway/gateway.py index 6296c53..990d856 100644 --- a/yepcord/voice_gateway/gateway.py +++ b/yepcord/voice_gateway/gateway.py @@ -10,37 +10,11 @@ from yepcord.yepcord.enums import VoiceGatewayOp from .default_sdp import DEFAULT_SDP from .events import Event, ReadyEvent, SpeakingEvent, UdpSessionDescriptionEvent, RtcSessionDescriptionEvent +from .go_rpc import GoRpc from .schemas import SelectProtocol from ..gateway.utils import require_auth from ..yepcord.config import Config - - -class GoRpc: - def __init__(self, rpc_addr: str): - self._address = f"http://{rpc_addr}/rpc" - - async def create_endpoint(self, channel_id: int) -> Optional[int]: - async with AsyncClient() as cl: - resp = await cl.post(self._address, json={ - # TODO: auto port allocation (on golang side) - "id": 0, "method": "Rpc.CreateApi", "params": [{"channel_id": str(channel_id), "port": 3791}] - }) - j = resp.json() - if j["error"] is None: - return j["result"] - print(j["error"]) - - async def create_peer_connection(self, channel_id: int, session_id: int, offer: str) -> Optional[str]: - async with AsyncClient() as cl: - resp = await cl.post(self._address, json={ - "id": 0, "method": "Rpc.NewPeerConnection", "params": [ - {"channel_id": str(channel_id), "session_id": str(session_id), "offer": offer} - ] - }) - j = resp.json() - if j["error"] is None: - return j["result"] - print(j["error"]) +from ..yepcord.models import VoiceState class GatewayClient: @@ -49,7 +23,7 @@ def __init__(self, ws: Websocket, gw: Gateway): self.user_id = None self.session_id = None self.guild_id = None - self.token = None + self.channel_id = None self.ssrc = 0 self.video_ssrc = 0 self.rtx_ssrc = 0 @@ -67,13 +41,25 @@ async def esend(self, event: Event): async def handle_IDENTIFY(self, data: dict): print(f"Connected to voice with session_id={data['session_id']}") - if data["token"] != "idk_token": + try: + token = data["token"].split(".") + if len(token) != 2: + raise ValueError() + state_id, token = token + state_id = int(state_id) + + state = await VoiceState.get_or_none(id=state_id, token=token).select_related("user", "guild", "channel") + if state is None: + raise ValueError + except ValueError: return await self.ws.close(4004) self.user_id = int(data["user_id"]) self.session_id = data["session_id"] self.guild_id = int(data["server_id"]) - self.token = data["token"] + self.channel_id = state.channel.id + if self.user_id != state.user.id or self.guild_id != state.guild.id: + return await self.ws.close(4004) self.ssrc = self._gw.ssrc self._gw.ssrc += 1 @@ -86,7 +72,7 @@ async def handle_IDENTIFY(self, data: dict): port = 0 rpc = self._gw.rpc(self.guild_id) if rpc is not None: - port = await rpc.create_endpoint(self.guild_id) + port = await rpc.create_endpoint(self.channel_id) await self.esend(ReadyEvent(self.ssrc, self.video_ssrc, self.rtx_ssrc, ip, port)) @@ -114,7 +100,7 @@ async def handle_SELECT_PROTOCOL(self, data: dict): sdp = "v=0\r\n" + str(self.sdp) + "\r\n" - answer = await rpc.create_peer_connection(self.guild_id, self.session_id, sdp) + answer = await rpc.create_peer_connection(self.channel_id, self.session_id, sdp) sdp = SDPInfo.parse(answer) c = sdp.candidates[0] diff --git a/yepcord/voice_gateway/go_rpc.py b/yepcord/voice_gateway/go_rpc.py new file mode 100644 index 0000000..88a09da --- /dev/null +++ b/yepcord/voice_gateway/go_rpc.py @@ -0,0 +1,30 @@ +from typing import Optional + +from httpx import AsyncClient + + +class GoRpc: + def __init__(self, rpc_addr: str): + self._address = f"http://{rpc_addr}/rpc" + + async def create_endpoint(self, channel_id: int) -> Optional[int]: + async with AsyncClient() as cl: + resp = await cl.post(self._address, json={ + "id": 0, "method": "Rpc.CreateApi", "params": [{"channel_id": str(channel_id)}] + }) + j = resp.json() + if j["error"] is None: + return j["result"] + print(j["error"]) + + async def create_peer_connection(self, channel_id: int, session_id: int, offer: str) -> Optional[str]: + async with AsyncClient() as cl: + resp = await cl.post(self._address, json={ + "id": 0, "method": "Rpc.NewPeerConnection", "params": [ + {"channel_id": str(channel_id), "session_id": str(session_id), "offer": offer} + ] + }) + j = resp.json() + if j["error"] is None: + return j["result"] + print(j["error"]) diff --git a/yepcord/voice_gateway/main.py b/yepcord/voice_gateway/main.py index 68535c1..07e560d 100644 --- a/yepcord/voice_gateway/main.py +++ b/yepcord/voice_gateway/main.py @@ -1,8 +1,10 @@ from asyncio import CancelledError from quart import Quart, websocket, Websocket +from tortoise.contrib.quart import register_tortoise from .gateway import Gateway +from ..yepcord.config import Config class YEPcord(Quart): @@ -35,10 +37,10 @@ async def ws_gateway_voice(): except CancelledError: raise -# ? -# register_tortoise( -# app, -# db_url=Config.DB_CONNECT_STRING, -# modules={"models": ["yepcord.yepcord.models"]}, -# generate_schemas=False, -# ) + +register_tortoise( + app, + db_url=Config.DB_CONNECT_STRING, + modules={"models": ["yepcord.yepcord.models"]}, + generate_schemas=False, +) diff --git a/yepcord/yepcord/config.py b/yepcord/yepcord/config.py index 5335aa6..38e48fa 100644 --- a/yepcord/yepcord/config.py +++ b/yepcord/yepcord/config.py @@ -123,6 +123,7 @@ class ConfigModel(BaseModel): MIGRATIONS_DIR: str = "./migrations" KEY: str = "XUJHVU0nUn51TifQuy9H1j0gId0JqhQ+PUz16a2WOXE=" PUBLIC_HOST: str = "127.0.0.1:8080" + VOICE_GATEWAY_HOST: str = "127.0.0.1:8000/voice" GATEWAY_HOST: str = "127.0.0.1:8080/gateway" CDN_HOST: str = "127.0.0.1:8080/media" STORAGE: ConfigStorage = Field(default_factory=ConfigStorage) diff --git a/yepcord/yepcord/gateway_dispatcher.py b/yepcord/yepcord/gateway_dispatcher.py index 8ecc6f3..55b5cd7 100644 --- a/yepcord/yepcord/gateway_dispatcher.py +++ b/yepcord/yepcord/gateway_dispatcher.py @@ -120,7 +120,8 @@ async def sendStickersUpdateEvent(self, guild: Guild) -> None: stickers = [await sticker.ds_json() for sticker in stickers] await self.dispatch(StickersUpdateEvent(guild.id, stickers), guild_id=guild.id) - async def getChannelFilter(self, channel: Channel, permissions: int = 0) -> dict: + @staticmethod + async def getChannelFilter(channel: Channel, permissions: int = 0) -> dict: if channel.type in {ChannelType.DM, ChannelType.GROUP_DM}: return {"user_ids": await channel.recipients.all().values_list("id", flat=True)} @@ -157,7 +158,8 @@ async def getChannelFilter(self, channel: Channel, permissions: int = 0) -> dict return {"role_ids": result_roles, "user_ids": list(user_ids), "exclude": list(excluded_user_ids)} - async def getRolesByPermissions(self, guild_id: int, permissions: int = 0) -> list[int]: + @staticmethod + async def getRolesByPermissions(guild_id: int, permissions: int = 0) -> list[int]: return await Role.filter(guild__id=guild_id).annotate(perms=RawSQL(f"permissions & {permissions}"))\ .filter(perms=permissions).values_list("id", flat=True) diff --git a/yepcord/yepcord/models/__init__.py b/yepcord/yepcord/models/__init__.py index 7fd8bfb..67259e8 100644 --- a/yepcord/yepcord/models/__init__.py +++ b/yepcord/yepcord/models/__init__.py @@ -27,6 +27,7 @@ from .guild_event import GuildEvent from .guild_template import GuildTemplate from .role import Role +from .voice_state import VoiceState from .message import Message from .attachment import Attachment diff --git a/yepcord/yepcord/models/voice_state.py b/yepcord/yepcord/models/voice_state.py new file mode 100644 index 0000000..572f971 --- /dev/null +++ b/yepcord/yepcord/models/voice_state.py @@ -0,0 +1,37 @@ +""" + YEPCord: Free open source selfhostable fully discord-compatible chat + Copyright (C) 2022-2024 RuslanUC + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +""" +from os import urandom +from typing import Optional + +from tortoise import fields + +import yepcord.yepcord.models as models +from ._utils import SnowflakeField, Model + + +def gen_token(): + return urandom(32).hex() + + +class VoiceState(Model): + id: int = SnowflakeField(pk=True) + guild: models.Guild = fields.ForeignKeyField("models.Guild", default=None, null=True) + channel: models.Channel = fields.ForeignKeyField("models.Channel") + user: models.User = fields.ForeignKeyField("models.User") + session_id: str = fields.CharField(max_length=64) + token: Optional[str] = fields.CharField(max_length=128, default=gen_token) From 98295ed2c1b31618bbc2e3dd508f6872ecb6547c Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Thu, 21 Mar 2024 17:49:48 +0200 Subject: [PATCH 12/15] add voice state disconnect on gateway disconnect --- poetry.lock | 132 +++++++++++++------------- pyproject.toml | 2 +- yepcord/gateway/events.py | 19 +++- yepcord/gateway/gateway.py | 18 +++- yepcord/voice_gateway/gateway.py | 4 +- yepcord/yepcord/models/voice_state.py | 20 ++++ 6 files changed, 121 insertions(+), 74 deletions(-) diff --git a/poetry.lock b/poetry.lock index f2eca71..43cb140 100644 --- a/poetry.lock +++ b/poetry.lock @@ -550,63 +550,63 @@ files = [ [[package]] name = "coverage" -version = "7.4.3" +version = "7.4.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"}, - {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"}, - {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"}, - {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"}, - {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"}, - {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"}, - {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"}, - {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"}, - {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, - {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, - {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, - {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, - {file = "coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454"}, - {file = "coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e"}, - {file = "coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2"}, - {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e"}, - {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6"}, - {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c"}, - {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0"}, - {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1"}, - {file = "coverage-7.4.3-cp38-cp38-win32.whl", hash = "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f"}, - {file = "coverage-7.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9"}, - {file = "coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f"}, - {file = "coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c"}, - {file = "coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e"}, - {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765"}, - {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee"}, - {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501"}, - {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f"}, - {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45"}, - {file = "coverage-7.4.3-cp39-cp39-win32.whl", hash = "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9"}, - {file = "coverage-7.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa"}, - {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, - {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, + {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, + {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, + {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, + {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, + {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, + {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, + {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, + {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, + {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, + {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, + {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, + {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, ] [package.dependencies] @@ -1918,29 +1918,29 @@ python-dateutil = ">=2.8.2,<3.0.0" [[package]] name = "semanticsdp" -version = "0.1.0b5" +version = "0.1.0b6" 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"}, + {file = "semanticsdp-0.1.0b6-py3-none-any.whl", hash = "sha256:f1429c0901d7f506b2ea27a066dd52fa3b926d06ccbb935891048e2048ccaac3"}, + {file = "semanticsdp-0.1.0b6.tar.gz", hash = "sha256:c5a6a9b3b7eef8e1c7430cfd9b1f65ce4c61d7c35db1456860234c42bb1089a5"}, ] [[package]] name = "setuptools" -version = "69.1.1" +version = "69.2.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"}, - {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"}, + {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, + {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -2375,20 +2375,20 @@ python-dateutil = ">=2.8.2,<3.0.0" [[package]] name = "zipp" -version = "3.17.0" +version = "3.18.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, + {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, + {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "bf743d7dc835dde9db90bf7c8e748e6f788b45fbe7245e10e51de455dd077fd9" +content-hash = "0c77fad61a1146477f635db407742ce647690cb8c4ff103a034338c9c71f8975" diff --git a/pyproject.toml b/pyproject.toml index 8375030..29ac1aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ yc-protobuf3-to-dict = "^0.3.0" s3lite = "^0.1.4" fast-depends = ">=2.4.2" faststream = {extras = ["kafka", "nats", "rabbit", "redis"], version = "^0.4.7"} -semanticsdp = "^0.1.0b5" +semanticsdp = "^0.1.0b6" [tool.poetry.group.dev.dependencies] pytest-cov = "4.1.0" diff --git a/yepcord/gateway/events.py b/yepcord/gateway/events.py index 2c2b7b1..0b906d1 100644 --- a/yepcord/gateway/events.py +++ b/yepcord/gateway/events.py @@ -24,12 +24,12 @@ from ..yepcord.config import Config from ..yepcord.enums import GatewayOp -from ..yepcord.models import Emoji, Application, Integration, ConnectedAccount +from ..yepcord.models import Emoji, Application, Integration, ConnectedAccount, VoiceState from ..yepcord.models.interaction import Interaction from ..yepcord.snowflake import Snowflake if TYPE_CHECKING: # pragma: no cover - from ..yepcord.models import Channel, Invite, GuildMember, UserData, User, UserSettings, Guild, VoiceState + from ..yepcord.models import Channel, Invite, GuildMember, UserData, User, UserSettings, Guild from ..yepcord.core import Core from .gateway import GatewayClient from .presences import Presence @@ -173,7 +173,19 @@ def __init__(self, friends_presences: list[dict], guilds_ids: list[int]): self.guilds_ids = guilds_ids async def json(self) -> dict: - g = [{"voice_states": [], "id": str(i), "embedded_activities": []} for i in self.guilds_ids] # TODO: add voice states + g = [ + { + "voice_states": [ + state.ds_json() for state in await VoiceState.filter( + guild__id=i, last_heartbeat__gt=int(time()-30) + ).select_related("user", "channel") + ], + "id": str(i), + "embedded_activities": [] + } + for i in self.guilds_ids + ] + return { "t": self.NAME, "op": self.OP, @@ -1107,5 +1119,4 @@ async def json(self) -> dict: if self.state.channel: data["d"]["channel_id"] = str(self.state.channel.id) - print(data) return data diff --git a/yepcord/gateway/gateway.py b/yepcord/gateway/gateway.py index 37341dc..38fb3ae 100644 --- a/yepcord/gateway/gateway.py +++ b/yepcord/gateway/gateway.py @@ -55,10 +55,24 @@ def __init__(self, ws, gateway: Gateway): def connected(self): return self._connected - def disconnect(self) -> None: + async def disconnect(self) -> None: self._connected = False self.ws = None + state = await VoiceState.get_or_none(user__id=self.user_id, session_id=self.sid)\ + .select_related("channel", "guild") + if state is not None: + member = await getCore().getGuildMember(state.guild, self.user_id) + voice_event = VoiceStateUpdate( + self.id, self.sid, None, state.guild, member, self_mute=True, self_deaf=True + ) + await self.gateway.broker.publish(channel="yepcord_events", message={ + "data": await voice_event.json(), + "event": voice_event.NAME, + **(await GatewayDispatcher.getChannelFilter(state.channel, GuildPermissions.VIEW_CHANNEL)) + }) + await state.delete() + async def send(self, data: dict): self.seq += 1 data["s"] = self.seq @@ -459,7 +473,7 @@ async def process(self, ws: Websocket, data: dict): print(f" Data: {data}") async def disconnect(self, ws: Websocket): - getattr(ws, "_yepcord_client").disconnect() + await getattr(ws, "_yepcord_client").disconnect() async def getFriendsPresences(self, uid: int) -> list[dict]: presences = [] diff --git a/yepcord/voice_gateway/gateway.py b/yepcord/voice_gateway/gateway.py index 990d856..795ac16 100644 --- a/yepcord/voice_gateway/gateway.py +++ b/yepcord/voice_gateway/gateway.py @@ -1,9 +1,9 @@ from __future__ import annotations from os import urandom +from time import time from typing import Optional -from httpx import AsyncClient from quart import Websocket from semanticsdp import SDPInfo, Setup @@ -78,6 +78,8 @@ async def handle_IDENTIFY(self, data: dict): @require_auth(4003) async def handle_HEARTBEAT(self, data: dict): + await VoiceState.filter(user__id=self.user_id, session_id=self.session_id).update(last_heartbeat=int(time())) + await self.send({"op": VoiceGatewayOp.HEARTBEAT_ACK, "d": data}) @require_auth(4003) diff --git a/yepcord/yepcord/models/voice_state.py b/yepcord/yepcord/models/voice_state.py index 572f971..54aa204 100644 --- a/yepcord/yepcord/models/voice_state.py +++ b/yepcord/yepcord/models/voice_state.py @@ -16,6 +16,7 @@ along with this program. If not, see . """ from os import urandom +from time import time from typing import Optional from tortoise import fields @@ -28,6 +29,10 @@ def gen_token(): return urandom(32).hex() +def gen_cur_time(): + return int(time()) + + class VoiceState(Model): id: int = SnowflakeField(pk=True) guild: models.Guild = fields.ForeignKeyField("models.Guild", default=None, null=True) @@ -35,3 +40,18 @@ class VoiceState(Model): user: models.User = fields.ForeignKeyField("models.User") session_id: str = fields.CharField(max_length=64) token: Optional[str] = fields.CharField(max_length=128, default=gen_token) + last_heartbeat: int = fields.BigIntField(default=gen_cur_time) + + def ds_json(self) -> dict: + return { + "user_id": self.user.id, + "suppress": False, + "session_id": self.session_id, + "self_video": False, + "self_mute": False, + "self_deaf": False, + "request_to_speak_timestamp": None, + "mute": False, + "deaf": False, + "channel_id": self.channel.id + } From 062993f483822e7df198bd1137fc63a72afebf7d Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Sun, 7 Apr 2024 17:43:18 +0300 Subject: [PATCH 13/15] add VIDEO (op 12) handler --- yepcord/voice_gateway/default_sdp.py | 3 +++ yepcord/voice_gateway/gateway.py | 36 ++++++++++++++++++++++++---- yepcord/voice_gateway/go_rpc.py | 31 ++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/yepcord/voice_gateway/default_sdp.py b/yepcord/voice_gateway/default_sdp.py index c6685c1..1e1ac7a 100644 --- a/yepcord/voice_gateway/default_sdp.py +++ b/yepcord/voice_gateway/default_sdp.py @@ -418,3 +418,6 @@ ], "candidates": [] } + +DEFAULT_SDP_DS = {'version': 3, 'streams': [], 'medias': [{'id': '0', 'type': 'audio', 'direction': 'recvonly', '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'}, 'codecs': [{'codec': 'opus', 'type': 111, 'rtx': None, 'channels': 2, 'params': {'minptime': '10', 'useinbandfec': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}]}, {'codec': 'G722', 'type': 9, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMU', 'type': 0, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMA', 'type': 8, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'CN', 'type': 13, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 110, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 126, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '1', 'type': 'video', 'direction': 'recvonly', 'extensions': {14: 'urn:ietf:params:rtp-hdrext:toffset', 2: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', 13: 'urn:3gpp:video-orientation', 3: 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', 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', 4: 'urn:ietf:params:rtp-hdrext:sdes:mid', 10: 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', 11: 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id'}, 'codecs': [{'codec': 'VP8', 'type': 96, 'rtx': 97, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 98, 'rtx': 99, 'channels': None, 'params': {'profile-id': '0'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 100, 'rtx': 101, 'channels': None, 'params': {'profile-id': '2'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 35, 'rtx': 36, 'channels': None, 'params': {'profile-id': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 37, 'rtx': 38, 'channels': None, 'params': {'profile-id': '3'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 102, 'rtx': 103, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 104, 'rtx': 105, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 106, 'rtx': 107, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 108, 'rtx': 109, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 127, 'rtx': 125, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 39, 'rtx': 40, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 41, 'rtx': 42, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 43, 'rtx': 44, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 45, 'rtx': 46, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 47, 'rtx': 48, 'channels': None, 'params': {'profile': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'flexfec-03', 'type': 49, 'rtx': None, 'channels': None, 'params': {'repair-window': '10000000'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'goog-remb', 'params': []}]}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '2', 'type': 'audio', 'direction': 'recvonly', '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'}, 'codecs': [{'codec': 'opus', 'type': 111, 'rtx': None, 'channels': 2, 'params': {'minptime': '10', 'useinbandfec': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}]}, {'codec': 'G722', 'type': 9, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMU', 'type': 0, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMA', 'type': 8, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'CN', 'type': 13, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 110, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 126, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '3', 'type': 'audio', 'direction': 'recvonly', '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'}, 'codecs': [{'codec': 'opus', 'type': 111, 'rtx': None, 'channels': 2, 'params': {'minptime': '10', 'useinbandfec': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}]}, {'codec': 'G722', 'type': 9, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMU', 'type': 0, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMA', 'type': 8, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'CN', 'type': 13, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 110, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 126, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '4', 'type': 'audio', 'direction': 'recvonly', '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'}, 'codecs': [{'codec': 'opus', 'type': 111, 'rtx': None, 'channels': 2, 'params': {'minptime': '10', 'useinbandfec': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}]}, {'codec': 'G722', 'type': 9, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMU', 'type': 0, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMA', 'type': 8, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'CN', 'type': 13, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 110, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 126, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '5', 'type': 'audio', 'direction': 'recvonly', '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'}, 'codecs': [{'codec': 'opus', 'type': 111, 'rtx': None, 'channels': 2, 'params': {'minptime': '10', 'useinbandfec': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}]}, {'codec': 'G722', 'type': 9, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMU', 'type': 0, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMA', 'type': 8, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'CN', 'type': 13, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 110, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 126, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '6', 'type': 'audio', 'direction': 'recvonly', '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'}, 'codecs': [{'codec': 'opus', 'type': 111, 'rtx': None, 'channels': 2, 'params': {'minptime': '10', 'useinbandfec': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}]}, {'codec': 'G722', 'type': 9, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMU', 'type': 0, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMA', 'type': 8, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'CN', 'type': 13, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 110, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 126, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '7', 'type': 'audio', 'direction': 'recvonly', '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'}, 'codecs': [{'codec': 'opus', 'type': 111, 'rtx': None, 'channels': 2, 'params': {'minptime': '10', 'useinbandfec': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}]}, {'codec': 'G722', 'type': 9, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMU', 'type': 0, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMA', 'type': 8, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'CN', 'type': 13, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 110, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 126, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '8', 'type': 'audio', 'direction': 'recvonly', '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'}, 'codecs': [{'codec': 'opus', 'type': 111, 'rtx': None, 'channels': 2, 'params': {'minptime': '10', 'useinbandfec': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}]}, {'codec': 'G722', 'type': 9, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMU', 'type': 0, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMA', 'type': 8, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'CN', 'type': 13, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 110, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 126, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '9', 'type': 'audio', 'direction': 'recvonly', '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'}, 'codecs': [{'codec': 'opus', 'type': 111, 'rtx': None, 'channels': 2, 'params': {'minptime': '10', 'useinbandfec': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}]}, {'codec': 'G722', 'type': 9, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMU', 'type': 0, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMA', 'type': 8, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'CN', 'type': 13, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 110, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 126, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '10', 'type': 'audio', 'direction': 'recvonly', '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'}, 'codecs': [{'codec': 'opus', 'type': 111, 'rtx': None, 'channels': 2, 'params': {'minptime': '10', 'useinbandfec': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}]}, {'codec': 'G722', 'type': 9, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMU', 'type': 0, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMA', 'type': 8, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'CN', 'type': 13, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 110, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 126, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '11', 'type': 'audio', 'direction': 'recvonly', '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'}, 'codecs': [{'codec': 'opus', 'type': 111, 'rtx': None, 'channels': 2, 'params': {'minptime': '10', 'useinbandfec': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}]}, {'codec': 'G722', 'type': 9, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMU', 'type': 0, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'PCMA', 'type': 8, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'CN', 'type': 13, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 110, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}, {'codec': 'telephone-event', 'type': 126, 'rtx': None, 'channels': None, 'params': {}, 'rtcpfbs': []}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '12', 'type': 'video', 'direction': 'recvonly', 'extensions': {14: 'urn:ietf:params:rtp-hdrext:toffset', 2: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', 13: 'urn:3gpp:video-orientation', 3: 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', 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', 4: 'urn:ietf:params:rtp-hdrext:sdes:mid', 10: 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', 11: 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id'}, 'codecs': [{'codec': 'VP8', 'type': 96, 'rtx': 97, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 98, 'rtx': 99, 'channels': None, 'params': {'profile-id': '0'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 100, 'rtx': 101, 'channels': None, 'params': {'profile-id': '2'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 35, 'rtx': 36, 'channels': None, 'params': {'profile-id': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 37, 'rtx': 38, 'channels': None, 'params': {'profile-id': '3'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 102, 'rtx': 103, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 104, 'rtx': 105, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 106, 'rtx': 107, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 108, 'rtx': 109, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 127, 'rtx': 125, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 39, 'rtx': 40, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 41, 'rtx': 42, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 43, 'rtx': 44, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 45, 'rtx': 46, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 47, 'rtx': 48, 'channels': None, 'params': {'profile': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'flexfec-03', 'type': 49, 'rtx': None, 'channels': None, 'params': {'repair-window': '10000000'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'goog-remb', 'params': []}]}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '13', 'type': 'video', 'direction': 'recvonly', 'extensions': {14: 'urn:ietf:params:rtp-hdrext:toffset', 2: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', 13: 'urn:3gpp:video-orientation', 3: 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', 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', 4: 'urn:ietf:params:rtp-hdrext:sdes:mid', 10: 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', 11: 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id'}, 'codecs': [{'codec': 'VP8', 'type': 96, 'rtx': 97, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 98, 'rtx': 99, 'channels': None, 'params': {'profile-id': '0'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 100, 'rtx': 101, 'channels': None, 'params': {'profile-id': '2'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 35, 'rtx': 36, 'channels': None, 'params': {'profile-id': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 37, 'rtx': 38, 'channels': None, 'params': {'profile-id': '3'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 102, 'rtx': 103, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 104, 'rtx': 105, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 106, 'rtx': 107, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 108, 'rtx': 109, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 127, 'rtx': 125, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 39, 'rtx': 40, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 41, 'rtx': 42, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 43, 'rtx': 44, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 45, 'rtx': 46, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 47, 'rtx': 48, 'channels': None, 'params': {'profile': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'flexfec-03', 'type': 49, 'rtx': None, 'channels': None, 'params': {'repair-window': '10000000'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'goog-remb', 'params': []}]}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '14', 'type': 'video', 'direction': 'recvonly', 'extensions': {14: 'urn:ietf:params:rtp-hdrext:toffset', 2: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', 13: 'urn:3gpp:video-orientation', 3: 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', 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', 4: 'urn:ietf:params:rtp-hdrext:sdes:mid', 10: 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', 11: 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id'}, 'codecs': [{'codec': 'VP8', 'type': 96, 'rtx': 97, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 98, 'rtx': 99, 'channels': None, 'params': {'profile-id': '0'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 100, 'rtx': 101, 'channels': None, 'params': {'profile-id': '2'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 35, 'rtx': 36, 'channels': None, 'params': {'profile-id': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 37, 'rtx': 38, 'channels': None, 'params': {'profile-id': '3'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 102, 'rtx': 103, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 104, 'rtx': 105, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 106, 'rtx': 107, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 108, 'rtx': 109, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 127, 'rtx': 125, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 39, 'rtx': 40, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 41, 'rtx': 42, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 43, 'rtx': 44, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 45, 'rtx': 46, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 47, 'rtx': 48, 'channels': None, 'params': {'profile': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'flexfec-03', 'type': 49, 'rtx': None, 'channels': None, 'params': {'repair-window': '10000000'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'goog-remb', 'params': []}]}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '15', 'type': 'video', 'direction': 'recvonly', 'extensions': {14: 'urn:ietf:params:rtp-hdrext:toffset', 2: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', 13: 'urn:3gpp:video-orientation', 3: 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', 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', 4: 'urn:ietf:params:rtp-hdrext:sdes:mid', 10: 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', 11: 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id'}, 'codecs': [{'codec': 'VP8', 'type': 96, 'rtx': 97, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 98, 'rtx': 99, 'channels': None, 'params': {'profile-id': '0'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 100, 'rtx': 101, 'channels': None, 'params': {'profile-id': '2'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 35, 'rtx': 36, 'channels': None, 'params': {'profile-id': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 37, 'rtx': 38, 'channels': None, 'params': {'profile-id': '3'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 102, 'rtx': 103, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 104, 'rtx': 105, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 106, 'rtx': 107, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 108, 'rtx': 109, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 127, 'rtx': 125, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 39, 'rtx': 40, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 41, 'rtx': 42, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 43, 'rtx': 44, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 45, 'rtx': 46, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 47, 'rtx': 48, 'channels': None, 'params': {'profile': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'flexfec-03', 'type': 49, 'rtx': None, 'channels': None, 'params': {'repair-window': '10000000'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'goog-remb', 'params': []}]}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '16', 'type': 'video', 'direction': 'recvonly', 'extensions': {14: 'urn:ietf:params:rtp-hdrext:toffset', 2: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', 13: 'urn:3gpp:video-orientation', 3: 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', 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', 4: 'urn:ietf:params:rtp-hdrext:sdes:mid', 10: 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', 11: 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id'}, 'codecs': [{'codec': 'VP8', 'type': 96, 'rtx': 97, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 98, 'rtx': 99, 'channels': None, 'params': {'profile-id': '0'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 100, 'rtx': 101, 'channels': None, 'params': {'profile-id': '2'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 35, 'rtx': 36, 'channels': None, 'params': {'profile-id': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 37, 'rtx': 38, 'channels': None, 'params': {'profile-id': '3'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 102, 'rtx': 103, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 104, 'rtx': 105, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 106, 'rtx': 107, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 108, 'rtx': 109, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 127, 'rtx': 125, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 39, 'rtx': 40, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 41, 'rtx': 42, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 43, 'rtx': 44, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 45, 'rtx': 46, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 47, 'rtx': 48, 'channels': None, 'params': {'profile': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'flexfec-03', 'type': 49, 'rtx': None, 'channels': None, 'params': {'repair-window': '10000000'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'goog-remb', 'params': []}]}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '17', 'type': 'video', 'direction': 'recvonly', 'extensions': {14: 'urn:ietf:params:rtp-hdrext:toffset', 2: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', 13: 'urn:3gpp:video-orientation', 3: 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', 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', 4: 'urn:ietf:params:rtp-hdrext:sdes:mid', 10: 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', 11: 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id'}, 'codecs': [{'codec': 'VP8', 'type': 96, 'rtx': 97, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 98, 'rtx': 99, 'channels': None, 'params': {'profile-id': '0'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 100, 'rtx': 101, 'channels': None, 'params': {'profile-id': '2'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 35, 'rtx': 36, 'channels': None, 'params': {'profile-id': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 37, 'rtx': 38, 'channels': None, 'params': {'profile-id': '3'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 102, 'rtx': 103, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 104, 'rtx': 105, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 106, 'rtx': 107, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 108, 'rtx': 109, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 127, 'rtx': 125, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 39, 'rtx': 40, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 41, 'rtx': 42, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 43, 'rtx': 44, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 45, 'rtx': 46, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 47, 'rtx': 48, 'channels': None, 'params': {'profile': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'flexfec-03', 'type': 49, 'rtx': None, 'channels': None, 'params': {'repair-window': '10000000'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'goog-remb', 'params': []}]}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '18', 'type': 'video', 'direction': 'recvonly', 'extensions': {14: 'urn:ietf:params:rtp-hdrext:toffset', 2: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', 13: 'urn:3gpp:video-orientation', 3: 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', 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', 4: 'urn:ietf:params:rtp-hdrext:sdes:mid', 10: 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', 11: 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id'}, 'codecs': [{'codec': 'VP8', 'type': 96, 'rtx': 97, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 98, 'rtx': 99, 'channels': None, 'params': {'profile-id': '0'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 100, 'rtx': 101, 'channels': None, 'params': {'profile-id': '2'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 35, 'rtx': 36, 'channels': None, 'params': {'profile-id': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 37, 'rtx': 38, 'channels': None, 'params': {'profile-id': '3'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 102, 'rtx': 103, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 104, 'rtx': 105, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 106, 'rtx': 107, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 108, 'rtx': 109, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 127, 'rtx': 125, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 39, 'rtx': 40, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 41, 'rtx': 42, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 43, 'rtx': 44, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 45, 'rtx': 46, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 47, 'rtx': 48, 'channels': None, 'params': {'profile': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'flexfec-03', 'type': 49, 'rtx': None, 'channels': None, 'params': {'repair-window': '10000000'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'goog-remb', 'params': []}]}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '19', 'type': 'video', 'direction': 'recvonly', 'extensions': {14: 'urn:ietf:params:rtp-hdrext:toffset', 2: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', 13: 'urn:3gpp:video-orientation', 3: 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', 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', 4: 'urn:ietf:params:rtp-hdrext:sdes:mid', 10: 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', 11: 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id'}, 'codecs': [{'codec': 'VP8', 'type': 96, 'rtx': 97, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 98, 'rtx': 99, 'channels': None, 'params': {'profile-id': '0'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 100, 'rtx': 101, 'channels': None, 'params': {'profile-id': '2'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 35, 'rtx': 36, 'channels': None, 'params': {'profile-id': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 37, 'rtx': 38, 'channels': None, 'params': {'profile-id': '3'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 102, 'rtx': 103, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 104, 'rtx': 105, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 106, 'rtx': 107, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 108, 'rtx': 109, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 127, 'rtx': 125, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 39, 'rtx': 40, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 41, 'rtx': 42, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 43, 'rtx': 44, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 45, 'rtx': 46, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 47, 'rtx': 48, 'channels': None, 'params': {'profile': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'flexfec-03', 'type': 49, 'rtx': None, 'channels': None, 'params': {'repair-window': '10000000'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'goog-remb', 'params': []}]}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '20', 'type': 'video', 'direction': 'recvonly', 'extensions': {14: 'urn:ietf:params:rtp-hdrext:toffset', 2: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', 13: 'urn:3gpp:video-orientation', 3: 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', 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', 4: 'urn:ietf:params:rtp-hdrext:sdes:mid', 10: 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', 11: 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id'}, 'codecs': [{'codec': 'VP8', 'type': 96, 'rtx': 97, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 98, 'rtx': 99, 'channels': None, 'params': {'profile-id': '0'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 100, 'rtx': 101, 'channels': None, 'params': {'profile-id': '2'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 35, 'rtx': 36, 'channels': None, 'params': {'profile-id': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 37, 'rtx': 38, 'channels': None, 'params': {'profile-id': '3'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 102, 'rtx': 103, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 104, 'rtx': 105, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 106, 'rtx': 107, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 108, 'rtx': 109, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 127, 'rtx': 125, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 39, 'rtx': 40, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 41, 'rtx': 42, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 43, 'rtx': 44, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 45, 'rtx': 46, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 47, 'rtx': 48, 'channels': None, 'params': {'profile': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'flexfec-03', 'type': 49, 'rtx': None, 'channels': None, 'params': {'repair-window': '10000000'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'goog-remb', 'params': []}]}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}, {'id': '21', 'type': 'video', 'direction': 'recvonly', 'extensions': {14: 'urn:ietf:params:rtp-hdrext:toffset', 2: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', 13: 'urn:3gpp:video-orientation', 3: 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', 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', 4: 'urn:ietf:params:rtp-hdrext:sdes:mid', 10: 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', 11: 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id'}, 'codecs': [{'codec': 'VP8', 'type': 96, 'rtx': 97, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 98, 'rtx': 99, 'channels': None, 'params': {'profile-id': '0'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 100, 'rtx': 101, 'channels': None, 'params': {'profile-id': '2'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 35, 'rtx': 36, 'channels': None, 'params': {'profile-id': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'VP9', 'type': 37, 'rtx': 38, 'channels': None, 'params': {'profile-id': '3'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 102, 'rtx': 103, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 104, 'rtx': 105, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 106, 'rtx': 107, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 108, 'rtx': 109, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '42e01f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 127, 'rtx': 125, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 39, 'rtx': 40, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': '4d001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 41, 'rtx': 42, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '1', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'H264', 'type': 43, 'rtx': 44, 'channels': None, 'params': {'level-asymmetry-allowed': '1', 'packetization-mode': '0', 'profile-level-id': 'f4001f'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 45, 'rtx': 46, 'channels': None, 'params': {}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'AV1', 'type': 47, 'rtx': 48, 'channels': None, 'params': {'profile': '1'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'ccm', 'params': ['fir']}, {'id': 'nack', 'params': ['pli']}, {'id': 'nack', 'params': []}, {'id': 'goog-remb', 'params': []}]}, {'codec': 'flexfec-03', 'type': 49, 'rtx': None, 'channels': None, 'params': {'repair-window': '10000000'}, 'rtcpfbs': [{'id': 'transport-cc', 'params': []}, {'id': 'goog-remb', 'params': []}]}], 'rids': [], 'simulcast': None, 'bitrate': 0, 'control': None, 'datachannel': None}], 'candidates': [{'foundation': 1965517400, 'component_id': 1, 'transport': 'udp', 'priority': 2113937151, 'address': 'd0967c9a-64ea-4833-9f72-9ea27eabac8f.local', 'port': 40744, 'type': 'host', 'rel_addr': None, 'rel_port': None}], 'ice': {'ufrag': '5Wts', 'pwd': 'R5RUz66kR8fDxWTeGii7sjan', 'lite': False, 'end_of_candidates': False}, 'dtls': {'setup': 'actpass', 'hash': 'sha-256', 'fingerprint': '1C:4D:E0:B8:86:59:C3:7A:ED:4E:0A:BA:F6:EA:98:9E:D9:8B:F4:D3:5A:1B:B7:76:84:C2:B1:42:DD:9D:79:02'}, 'crypto': None, 'extmap_allow_mixed': True} + diff --git a/yepcord/voice_gateway/gateway.py b/yepcord/voice_gateway/gateway.py index 795ac16..41de5c5 100644 --- a/yepcord/voice_gateway/gateway.py +++ b/yepcord/voice_gateway/gateway.py @@ -3,12 +3,13 @@ from os import urandom from time import time from typing import Optional +from uuid import uuid4 from quart import Websocket -from semanticsdp import SDPInfo, Setup +from semanticsdp import SDPInfo, Setup, Direction, StreamInfo, TrackInfo from yepcord.yepcord.enums import VoiceGatewayOp -from .default_sdp import DEFAULT_SDP +from .default_sdp import DEFAULT_SDP, DEFAULT_SDP_DS from .events import Event, ReadyEvent, SpeakingEvent, UdpSessionDescriptionEvent, RtcSessionDescriptionEvent from .go_rpc import GoRpc from .schemas import SelectProtocol @@ -29,7 +30,7 @@ def __init__(self, ws: Websocket, gw: Gateway): self.rtx_ssrc = 0 self.mode: Optional[str] = None self.key: Optional[bytes] = None - self.sdp = SDPInfo.from_dict(DEFAULT_SDP) + self.sdp = SDPInfo.from_dict(DEFAULT_SDP_DS) self._gw = gw @@ -84,8 +85,7 @@ async def handle_HEARTBEAT(self, data: dict): @require_auth(4003) async def handle_SELECT_PROTOCOL(self, data: dict): - rpc = self._gw.rpc(self.guild_id) - if rpc is None: + if (rpc := self._gw.rpc(self.guild_id)) is None: return try: @@ -133,6 +133,32 @@ async def handle_SPEAKING(self, data: dict): return await self.ws.close(4014) await self.esend(SpeakingEvent(self.ssrc, self.user_id, data["speaking"])) + @require_auth + async def handle_VIDEO(self, data: dict): + if (rpc := self._gw.rpc(self.guild_id)) is None: + return + + if (audio_ssrc := data.get("audio_ssrc", 0)) < 1: + return await self.send({"op": VoiceGatewayOp.MEDIA_SINK_WANTS, "d": {"any": "100"}}) # ? + + track_id = str(uuid4()) + self.sdp.version += 1 + self.sdp.medias[0].direction = Direction.SENDRECV + self.sdp.streams["-"] = StreamInfo( + id="-", + tracks={track_id: TrackInfo( + media="audio", + id=track_id, + media_id="0", + ssrcs=[audio_ssrc] + )} + ) + + sdp = "v=0\r\n" + str(self.sdp) + "\r\n" + await rpc.renegotiate(self.channel_id, self.session_id, sdp) + + await self.send({"op": VoiceGatewayOp.MEDIA_SINK_WANTS, "d": {"any": "100"}}) + async def handle_VOICE_BACKEND_VERSION(self, data: dict) -> None: await self.send({"op": VoiceGatewayOp.VOICE_BACKEND_VERSION, "d": {"voice": "0.11.0", "rtc_worker": "0.4.11"}}) diff --git a/yepcord/voice_gateway/go_rpc.py b/yepcord/voice_gateway/go_rpc.py index 88a09da..1d8d2e9 100644 --- a/yepcord/voice_gateway/go_rpc.py +++ b/yepcord/voice_gateway/go_rpc.py @@ -8,6 +8,11 @@ def __init__(self, rpc_addr: str): self._address = f"http://{rpc_addr}/rpc" async def create_endpoint(self, channel_id: int) -> Optional[int]: + """ + Sends request to pion webrtc server to create new webrtc endpoint. + :param channel_id: Id of channel/guild to associate created endpoint with + :return: Endpoint port answer on success or None on error + """ async with AsyncClient() as cl: resp = await cl.post(self._address, json={ "id": 0, "method": "Rpc.CreateApi", "params": [{"channel_id": str(channel_id)}] @@ -18,6 +23,13 @@ async def create_endpoint(self, channel_id: int) -> Optional[int]: print(j["error"]) async def create_peer_connection(self, channel_id: int, session_id: int, offer: str) -> Optional[str]: + """ + Sends request to pion webrtc server to create new peerConnection. + :param channel_id: Id of channel/guild user associated with + :param session_id: Id of voice session + :param offer: Sdp offer + :return: Sdp answer on success or None on error + """ async with AsyncClient() as cl: resp = await cl.post(self._address, json={ "id": 0, "method": "Rpc.NewPeerConnection", "params": [ @@ -28,3 +40,22 @@ async def create_peer_connection(self, channel_id: int, session_id: int, offer: if j["error"] is None: return j["result"] print(j["error"]) + + async def renegotiate(self, channel_id: int, session_id: int, offer: str) -> Optional[str]: + """ + Sends re-negotiate request to pion webrtc server. + :param channel_id: Id of channel/guild user associated with + :param session_id: Id of voice session + :param offer: Sdp offer + :return: Sdp answer on success or None on error + """ + async with AsyncClient() as cl: + resp = await cl.post(self._address, json={ + "id": 0, "method": "Rpc.ReNegotiate", "params": [ + {"channel_id": str(channel_id), "session_id": str(session_id), "offer": offer} + ] + }) + j = resp.json() + if j["error"] is None: + return j["result"] + print(j["error"]) From 68079ccfb8468932dcf303bcdf56527af37e2ce7 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Wed, 8 May 2024 18:24:32 +0300 Subject: [PATCH 14/15] add webrtc track to other vc clients --- yepcord/voice_gateway/gateway.py | 69 ++++++++++++++++++++++++++++++-- yepcord/voice_gateway/main.py | 1 + 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/yepcord/voice_gateway/gateway.py b/yepcord/voice_gateway/gateway.py index 41de5c5..8ef8dce 100644 --- a/yepcord/voice_gateway/gateway.py +++ b/yepcord/voice_gateway/gateway.py @@ -1,15 +1,16 @@ from __future__ import annotations +from collections import defaultdict from os import urandom from time import time from typing import Optional from uuid import uuid4 from quart import Websocket -from semanticsdp import SDPInfo, Setup, Direction, StreamInfo, TrackInfo +from semanticsdp import SDPInfo, Setup, Direction, StreamInfo, TrackInfo, MediaInfo, CodecInfo, RTCPFeedbackInfo from yepcord.yepcord.enums import VoiceGatewayOp -from .default_sdp import DEFAULT_SDP, DEFAULT_SDP_DS +from .default_sdp import DEFAULT_SDP_DS from .events import Event, ReadyEvent, SpeakingEvent, UdpSessionDescriptionEvent, RtcSessionDescriptionEvent from .go_rpc import GoRpc from .schemas import SelectProtocol @@ -30,7 +31,10 @@ def __init__(self, ws: Websocket, gw: Gateway): self.rtx_ssrc = 0 self.mode: Optional[str] = None self.key: Optional[bytes] = None + self.sdp = SDPInfo.from_dict(DEFAULT_SDP_DS) + self.need_sync = False + self.other_media_ids: dict[int, int] = {} # ssrc to media_id self._gw = gw @@ -75,6 +79,7 @@ async def handle_IDENTIFY(self, data: dict): if rpc is not None: port = await rpc.create_endpoint(self.channel_id) + self._gw.channels[self.channel_id][self.user_id] = self await self.esend(ReadyEvent(self.ssrc, self.video_ssrc, self.rtx_ssrc, ip, port)) @require_auth(4003) @@ -141,6 +146,16 @@ async def handle_VIDEO(self, data: dict): if (audio_ssrc := data.get("audio_ssrc", 0)) < 1: return await self.send({"op": VoiceGatewayOp.MEDIA_SINK_WANTS, "d": {"any": "100"}}) # ? + if audio_ssrc == self.ssrc: + if not self.need_sync: + return + + self.sdp.version += 1 + sdp = "v=0\r\n" + str(self.sdp) + "\r\n" + await rpc.renegotiate(self.channel_id, self.session_id, sdp) + + self.need_sync = False + track_id = str(uuid4()) self.sdp.version += 1 self.sdp.medias[0].direction = Direction.SENDRECV @@ -159,6 +174,42 @@ async def handle_VIDEO(self, data: dict): await self.send({"op": VoiceGatewayOp.MEDIA_SINK_WANTS, "d": {"any": "100"}}) + for client in self._gw.channels[self.channel_id].values(): + if client is self: + continue + + next_media_id = max([int(media.id) for media in client.sdp.medias]) + 1 + client.sdp.medias.append(MediaInfo( + id=str(next_media_id), + type="audio", + direction=Direction.RECVONLY, + 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" + }, + codecs={ + 111: CodecInfo( + codec="opus", + type=111, + channels=2, + params={"minptime": "10", "useinbandfec": "1"}, + rtcpfbs={RTCPFeedbackInfo(id="transport-cc", params=[])} + ), + 9: CodecInfo(codec="G722", type=9), + 0: CodecInfo(codec="PCMU", type=0), + 8: CodecInfo(codec="PCMA", type=8), + 13: CodecInfo(codec="CN", type=13), + 110: CodecInfo(codec="telephone-event", type=110), + 126: CodecInfo(codec="telephone-event", type=126), + }, + )) + client.other_media_ids[audio_ssrc] = next_media_id + client.need_sync = True + + await client.send({"op": VoiceGatewayOp.VIDEO, "d": data}) + async def handle_VOICE_BACKEND_VERSION(self, data: dict) -> None: await self.send({"op": VoiceGatewayOp.VOICE_BACKEND_VERSION, "d": {"voice": "0.11.0", "rtc_worker": "0.4.11"}}) @@ -166,7 +217,7 @@ async def handle_VOICE_BACKEND_VERSION(self, data: dict) -> None: class Gateway: def __init__(self): self.ssrc = 1 - self.channels = {} + self.channels: defaultdict[int, dict[int, GatewayClient]] = defaultdict(dict) self._rpcs: dict[int, GoRpc] = {} def rpc(self, guild_id: int) -> Optional[GoRpc]: @@ -195,3 +246,15 @@ async def process(self, ws: Websocket, data: dict) -> None: print("-" * 16) print(f" [Voice] Unknown op code: {op}") print(f" [Voice] Data: {data}") + + async def disconnect(self, ws: Websocket) -> None: + client: GatewayClient + if (client := getattr(ws, "_yepcord_client", None)) is None: + return + if client.channel_id not in self.channels or client.user_id not in self.channels[client.channel_id]: + return + + del self.channels[client.channel_id][client.user_id] + for cl in self.channels[client.channel_id].values(): + await cl.send({"op": VoiceGatewayOp.CLIENT_DISCONNECT, "d": {"user_id": str(client.user_id)}}) + # TODO: remove disconnected client from sdp diff --git a/yepcord/voice_gateway/main.py b/yepcord/voice_gateway/main.py index 07e560d..ff91ac1 100644 --- a/yepcord/voice_gateway/main.py +++ b/yepcord/voice_gateway/main.py @@ -35,6 +35,7 @@ async def ws_gateway_voice(): data = await ws.receive_json() await gw.process(ws, data) except CancelledError: + await gw.disconnect(ws) raise From 4d2439f5035f8fe71c9c4e5fcf4938d176523aed Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Fri, 10 May 2024 16:55:22 +0300 Subject: [PATCH 15/15] send SPEAKING to all users in vc --- poetry.lock | 13 ++++++++++++- pyproject.toml | 1 + yepcord/voice_gateway/gateway.py | 6 +++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9af3e03..3f82de1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2061,6 +2061,17 @@ files = [ httpx = ">=0.27.0,<0.28.0" python-dateutil = ">=2.8.2,<3.0.0" +[[package]] +name = "semanticsdp" +version = "0.1.0b9" +description = "Python port of medooze/semantic-sdp-js" +optional = false +python-versions = "<4.0,>=3.9" +files = [ + {file = "semanticsdp-0.1.0b9-py3-none-any.whl", hash = "sha256:36432a31bc781a24ab2dece9aa961b10e2d09b927f7d257bf4a1e6b6479d2733"}, + {file = "semanticsdp-0.1.0b9.tar.gz", hash = "sha256:805a218472f6ceae12c1a36f6c323762c8b9885dbabc0b45bd87db692e195541"}, +] + [[package]] name = "shellingham" version = "1.5.4" @@ -2520,4 +2531,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "12c6a4fed2dc22c9c5398abba113aa7d96e4e54af57e0b8b8d251d49e16dd985" +content-hash = "8341f97e28a754ed9e4377efe137c7cafa72d7612f3c8c946a08840744735b83" diff --git a/pyproject.toml b/pyproject.toml index d85c881..131c7ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,7 @@ yc-protobuf3-to-dict = "^0.3.0" s3lite = "^0.1.4" fast-depends = ">=2.4.2" faststream = {extras = ["kafka", "nats", "rabbit", "redis"], version = "^0.5.4"} +semanticsdp = "^0.1.0b9" [tool.poetry.group.dev.dependencies] pytest = "^8.2.0" diff --git a/yepcord/voice_gateway/gateway.py b/yepcord/voice_gateway/gateway.py index 8ef8dce..19952b7 100644 --- a/yepcord/voice_gateway/gateway.py +++ b/yepcord/voice_gateway/gateway.py @@ -136,7 +136,11 @@ async def handle_SELECT_PROTOCOL(self, data: dict): async def handle_SPEAKING(self, data: dict): if self.ssrc != data["ssrc"] or data["ssrc"] < 1: return await self.ws.close(4014) - await self.esend(SpeakingEvent(self.ssrc, self.user_id, data["speaking"])) + + for client in self._gw.channels[self.channel_id].values(): + if client is self: + continue + await client.esend(SpeakingEvent(self.ssrc, self.user_id, data["speaking"])) @require_auth async def handle_VIDEO(self, data: dict):