diff --git a/config.example.py b/config.example.py index eb9db8e..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 = { @@ -113,3 +114,6 @@ "client_secret": None, }, } + +# Voice workers addresses +VOICE_WORKERS = [] 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/asgi.py b/yepcord/asgi.py index dc887e5..fb85e4c 100644 --- a/yepcord/asgi.py +++ b/yepcord/asgi.py @@ -1,6 +1,6 @@ """ YEPCord: Free open source selfhostable fully discord-compatible chat - Copyright (C) 2022-2024 RuslanUC + Copyright (C) 2022-2023 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 @@ -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 @@ -78,8 +79,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.register_blueprint(cdn.cdn, url_prefix="/media") diff --git a/yepcord/gateway/events.py b/yepcord/gateway/events.py index 379df0d..0b906d1 100644 --- a/yepcord/gateway/events.py +++ b/yepcord/gateway/events.py @@ -20,16 +20,16 @@ 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 -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 + 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,16 +173,28 @@ 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": [ + 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, "d": { "merged_presences": { - "guilds": [[]], # TODO + "guilds": [[]], "friends": self.friends_presences }, - "merged_members": [[]], # TODO + "merged_members": [[]], "guilds": g } } @@ -1054,3 +1066,57 @@ async def json(self) -> dict: "token_data": None, } } + + +class VoiceStateUpdate(DispatchEvent): + NAME = "VOICE_STATE_UPDATE" + + 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 + 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) if self.channel is not None else None, + "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, voice_state: VoiceState): + self.state = voice_state + + async def json(self) -> dict: + data = { + "t": self.NAME, + "op": self.OP, + "d": { + "token": f"{self.state.id}.{self.state.token}", + "endpoint": Config.VOICE_GATEWAY_HOST + } + } + 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) + + return data diff --git a/yepcord/gateway/gateway.py b/yepcord/gateway/gateway.py index cd44d54..38fb3ae 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 @@ -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 @@ -53,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 @@ -175,6 +191,59 @@ 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")) + + 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() + + 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 + + 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: BOTS_EVENTS_BLACKLIST = {"MESSAGE_ACK"} @@ -404,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/gateway/utils.py b/yepcord/gateway/utils.py index f4092e6..7feff91 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 f95436b..63df399 100644 --- a/yepcord/remote_auth/main.py +++ b/yepcord/remote_auth/main.py @@ -53,7 +53,7 @@ async def set_cors_headers(response): # pragma: no cover @app.websocket("/") -async def ws_gateway(): +async def ws_gateway_remote_auth(): version = websocket.args.get("v", "") version = int(version) if version.isdigit() else 1 diff --git a/yepcord/voice_gateway/__init__.py b/yepcord/voice_gateway/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yepcord/voice_gateway/default_sdp.py b/yepcord/voice_gateway/default_sdp.py new file mode 100644 index 0000000..1e1ac7a --- /dev/null +++ b/yepcord/voice_gateway/default_sdp.py @@ -0,0 +1,423 @@ +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": [] +} + +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/events.py b/yepcord/voice_gateway/events.py new file mode 100644 index 0000000..a0d1e6b --- /dev/null +++ b/yepcord/voice_gateway/events.py @@ -0,0 +1,92 @@ +from yepcord.yepcord.enums import VoiceGatewayOp + + +class Event: + OP: int + + async def json(self) -> dict: ... + + +class ReadyEvent(Event): + OP = VoiceGatewayOp.READY + + 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: + return { + "op": self.OP, + "d": { + "ssrc": self.ssrc, + "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"], + "streams": [{ + "active": False, + "quality": 0, + "rid": "", + "rtx_ssrc": self.rtx_ssrc, + "ssrc": self.video_ssrc, + "type": "video" + }] + } + } + + +class UdpSessionDescriptionEvent(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 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, user_id: int, speaking: int): + self.ssrc = ssrc + self.speaking = speaking + self.user_id = user_id + + async def json(self) -> dict: + return { + "op": self.OP, + "d": { + "ssrc": self.ssrc, + "speaking": self.speaking, + "user_id": str(self.user_id) + } + } diff --git a/yepcord/voice_gateway/gateway.py b/yepcord/voice_gateway/gateway.py new file mode 100644 index 0000000..19952b7 --- /dev/null +++ b/yepcord/voice_gateway/gateway.py @@ -0,0 +1,264 @@ +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, MediaInfo, CodecInfo, RTCPFeedbackInfo + +from yepcord.yepcord.enums import VoiceGatewayOp +from .default_sdp import DEFAULT_SDP_DS +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 +from ..yepcord.models import VoiceState + + +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.channel_id = None + self.ssrc = 0 + self.video_ssrc = 0 + 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 + + 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']}") + 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.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 + self.video_ssrc = self._gw.ssrc + self._gw.ssrc += 1 + self.rtx_ssrc = self._gw.ssrc + self._gw.ssrc += 1 + + 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.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) + 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) + async def handle_SELECT_PROTOCOL(self, data: dict): + if (rpc := self._gw.rpc(self.guild_id)) is None: + return + + try: + d = SelectProtocol(**data) + except Exception as e: + print(e) + 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.sdp.dtls.setup = Setup.ACTIVE + + sdp = "v=0\r\n" + str(self.sdp) + "\r\n" + + answer = await rpc.create_peer_connection(self.channel_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": + 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) + + 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): + 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"}}) # ? + + 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 + 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"}}) + + 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"}}) + + +class Gateway: + def __init__(self): + self.ssrc = 1 + self.channels: defaultdict[int, dict[int, GatewayClient]] = defaultdict(dict) + self._rpcs: dict[int, GoRpc] = {} + + 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._rpcs[idx] + + 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}") + + 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/go_rpc.py b/yepcord/voice_gateway/go_rpc.py new file mode 100644 index 0000000..1d8d2e9 --- /dev/null +++ b/yepcord/voice_gateway/go_rpc.py @@ -0,0 +1,61 @@ +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]: + """ + 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)}] + }) + 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]: + """ + 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": [ + {"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"]) + + 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"]) diff --git a/yepcord/voice_gateway/main.py b/yepcord/voice_gateway/main.py new file mode 100644 index 0000000..ff91ac1 --- /dev/null +++ b/yepcord/voice_gateway/main.py @@ -0,0 +1,47 @@ +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): + 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: + await gw.disconnect(ws) + raise + + +register_tortoise( + app, + db_url=Config.DB_CONNECT_STRING, + modules={"models": ["yepcord.yepcord.models"]}, + generate_schemas=False, +) diff --git a/yepcord/voice_gateway/schemas.py b/yepcord/voice_gateway/schemas.py new file mode 100644 index 0000000..bc68f3c --- /dev/null +++ b/yepcord/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]] 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/classes/gifs.py b/yepcord/yepcord/classes/gifs.py index 86695da..6e557e3 100644 --- a/yepcord/yepcord/classes/gifs.py +++ b/yepcord/yepcord/classes/gifs.py @@ -69,8 +69,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/config.py b/yepcord/yepcord/config.py index 0e45bf0..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) @@ -133,6 +134,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: diff --git a/yepcord/yepcord/core.py b/yepcord/yepcord/core.py index f18dc27..4a49230 100644 --- a/yepcord/yepcord/core.py +++ b/yepcord/yepcord/core.py @@ -310,9 +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 with_hidden and 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]: diff --git a/yepcord/yepcord/enums.py b/yepcord/yepcord/enums.py index f6b8d6d..c5da06a 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 + VIDEO = 12 + CLIENT_DISCONNECT = 13 + 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" 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/channel.py b/yepcord/yepcord/models/channel.py index d071b36..3210159 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/voice_state.py b/yepcord/yepcord/models/voice_state.py new file mode 100644 index 0000000..54aa204 --- /dev/null +++ b/yepcord/yepcord/models/voice_state.py @@ -0,0 +1,57 @@ +""" + 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 time import time +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() + + +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) + 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) + 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 + } diff --git a/yepcord/yepcord/mq_broker.py b/yepcord/yepcord/mq_broker.py index 3e058ae..e6f3726 100644 --- a/yepcord/yepcord/mq_broker.py +++ b/yepcord/yepcord/mq_broker.py @@ -175,8 +175,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!")