mirror of
https://github.com/stoatchat/python-client-sdk.git
synced 2026-07-25 16:35:33 -04:00
Compare commits
1 Commits
feat/livekit
...
voice
| Author | SHA1 | Date | |
|---|---|---|---|
| b61bb49949 |
@@ -1,3 +1,5 @@
|
||||
aiohttp==3.7.4.post0
|
||||
ulid-py==1.1.0
|
||||
aenum==3.1.0
|
||||
typing_extensions
|
||||
pynacl==1.4.0
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, cast
|
||||
import asyncio
|
||||
|
||||
from .enums import ChannelType
|
||||
from .messageable import Messageable
|
||||
from .permissions import ChannelPermissions
|
||||
from .voice_client import VoiceClient
|
||||
from .errors import FeatureDisabled
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .message import Message
|
||||
@@ -179,6 +182,24 @@ class VoiceChannel(Channel):
|
||||
"""
|
||||
await self.state.http.set_channel_role_permissions(self.id, role.id, permissions.value)
|
||||
|
||||
async def connect(self):
|
||||
token = (await self.state.http.connect_to_voice(self.id))["token"]
|
||||
voso = self.state.api_info["features"]["voso"]
|
||||
|
||||
if not voso["enabled"]:
|
||||
raise FeatureDisabled("vortex is disabled.")
|
||||
|
||||
voice_client = VoiceClient(self.state.http.session, voso["ws"], self.id, token, self.state)
|
||||
|
||||
await voice_client.start()
|
||||
await voice_client.send_authenticate()
|
||||
print("auth")
|
||||
await voice_client.send_initialize_transport()
|
||||
print("init")
|
||||
await voice_client.send_connect_transport()
|
||||
print("connect")
|
||||
return voice_client
|
||||
|
||||
def channel_factory(data: ChannelPayload, state: State) -> Channel:
|
||||
if data["channel_type"] == "SavedMessage":
|
||||
return SavedMessageChannel(data, state)
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ class Client:
|
||||
|
||||
self.api_info = api_info
|
||||
self.http = HttpClient(self.session, self.token, self.api_url, self.api_info)
|
||||
self.state = State(self.http, api_info, self.max_messages)
|
||||
self.state = State(self.http, api_info, self.max_messages, self.dispatch)
|
||||
self.websocket = WebsocketHandler(self.session, self.token, api_info["ws"], self.dispatch, self.state)
|
||||
await self.websocket.start()
|
||||
|
||||
|
||||
+4
-1
@@ -28,7 +28,7 @@ if TYPE_CHECKING:
|
||||
from .types import Member
|
||||
from .types import Message as MessagePayload
|
||||
from .types import (MessageReplyPayload, MessageWithUserData, Role, Server,
|
||||
ServerBans, ServerInvite, TextChannel)
|
||||
ServerBans, ServerInvite, TextChannel, JoinCallResponse)
|
||||
from .types import User as UserPayload
|
||||
from .types import UserProfile, VoiceChannel
|
||||
|
||||
@@ -352,3 +352,6 @@ class HttpClient:
|
||||
|
||||
def delete_role(self, server_id: str, role_id: str) -> Request[None]:
|
||||
return self.request("DELETE", f"/servers/{server_id}/roles/{role_id}")
|
||||
|
||||
def connect_to_voice(self, channel_id) -> Request[JoinCallResponse]:
|
||||
return self.request("POST", f"/channels/{channel_id}/join_call")
|
||||
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
import time
|
||||
import random
|
||||
import bitarray
|
||||
|
||||
class SecureRTP:
|
||||
def __init__(self, salt: str, hash: str):
|
||||
self.salt = salt
|
||||
self.hash = hash
|
||||
self.roc = 0
|
||||
self.seq = 0
|
||||
self.packet_counter = 0
|
||||
self.ssrc = random.randbytes(4) # 32 bytes
|
||||
self.version = [1, 0] # binary for 2
|
||||
|
||||
# documentation of header can be found at https://datatracker.ietf.org/doc/html/rfc3550#section-5.1
|
||||
|
||||
def create_packet(self, data: bytes) -> bytes:
|
||||
arr = bitarray.bitarray()
|
||||
|
||||
arr.extend(self.version)
|
||||
arr.extend([0, 0])
|
||||
arr.extend([0, 0, 0, 0])
|
||||
arr.append(0)
|
||||
arr.extend([0, 0, 0, 0, 0, 0, 0])
|
||||
|
||||
seq = list(map(int, bin(self.seq)[2:]))
|
||||
arr.extend(([0] * (16 - len(seq))) + seq)
|
||||
|
||||
timestamp = list(map(int, bin(int(time.monotonic()))[2:]))
|
||||
|
||||
arr.extend(([0] * (32 - len(timestamp))) + timestamp)
|
||||
arr.extend(self.ssrc)
|
||||
|
||||
arr.extend(data)
|
||||
|
||||
return arr.tobytes()
|
||||
+4
-3
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Callable, Optional
|
||||
|
||||
from .channel import Channel, channel_factory
|
||||
from .member import Member
|
||||
@@ -22,12 +22,13 @@ if TYPE_CHECKING:
|
||||
__all__ = ("State",)
|
||||
|
||||
class State:
|
||||
__slots__ = ("http", "api_info", "max_messages", "users", "channels", "servers", "messages")
|
||||
__slots__ = ("http", "api_info", "max_messages", "users", "channels", "servers", "messages", "dispatch")
|
||||
|
||||
def __init__(self, http: HttpClient, api_info: ApiInfo, max_messages: int):
|
||||
def __init__(self, http: HttpClient, api_info: ApiInfo, max_messages: int, dispatch: Callable[..., None]):
|
||||
self.http = http
|
||||
self.api_info = api_info
|
||||
self.max_messages = max_messages
|
||||
self.dispatch = dispatch
|
||||
|
||||
self.users: dict[str, User] = {}
|
||||
self.channels: dict[str, Channel] = {}
|
||||
|
||||
@@ -10,3 +10,4 @@ from .message import *
|
||||
from .role import *
|
||||
from .server import *
|
||||
from .user import *
|
||||
from .voice import *
|
||||
|
||||
@@ -7,13 +7,14 @@ if TYPE_CHECKING:
|
||||
from .message import Message
|
||||
from .user import User
|
||||
|
||||
|
||||
|
||||
__all__ = (
|
||||
"VosoFeature",
|
||||
"ApiInfo",
|
||||
"Autumn",
|
||||
"GetServerMembers",
|
||||
"MessageWithUserData",
|
||||
"JoinCallResponse"
|
||||
)
|
||||
|
||||
|
||||
@@ -50,3 +51,6 @@ class MessageWithUserData(TypedDict):
|
||||
messages: list[Message]
|
||||
members: list[Member]
|
||||
users: list[User]
|
||||
|
||||
class JoinCallResponse(TypedDict):
|
||||
token: str
|
||||
|
||||
Executable
+192
@@ -0,0 +1,192 @@
|
||||
from typing import Optional, TypedDict, Union, Literal
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
TransportProtocol = Literal["TCP", "UDP"]
|
||||
|
||||
class BaseWS(TypedDict):
|
||||
id: NotRequired[int]
|
||||
|
||||
class AuthenticationCommandData(TypedDict):
|
||||
token: str
|
||||
roomId: str
|
||||
|
||||
class AuthenticateCommand(BaseWS):
|
||||
type: Literal["Authenticate"]
|
||||
|
||||
data: AuthenticationCommandData
|
||||
|
||||
class RTPCodecFeedback(TypedDict):
|
||||
parameter: str
|
||||
type: str
|
||||
|
||||
class RTPCodec(TypedDict):
|
||||
channels: int
|
||||
clockRate: int
|
||||
kind: str
|
||||
mimeType: str
|
||||
parameters: dict
|
||||
preferredPayloadType: int
|
||||
rtcpFeedback: list[RTPCodecFeedback]
|
||||
|
||||
class RTPHeaderExtension(TypedDict):
|
||||
direction: str
|
||||
kind: str
|
||||
preferredEncrypt: bool
|
||||
preferredId: int
|
||||
uri: str
|
||||
|
||||
class RTPCapabilities(TypedDict):
|
||||
codecs: list[RTPCodec]
|
||||
headerExtensions: list[RTPHeaderExtension]
|
||||
|
||||
class AuthenticationResponseData(TypedDict):
|
||||
roomId: str
|
||||
userId: str
|
||||
version: str
|
||||
rtpCapabilities: RTPCapabilities
|
||||
|
||||
class AuthenticateResponse(BaseWS):
|
||||
type: Literal["Authenticate"]
|
||||
data: AuthenticationResponseData
|
||||
|
||||
class RoomInfoCommand(BaseWS):
|
||||
type: Literal["RoomInfo"]
|
||||
|
||||
class VoiceUser(TypedDict):
|
||||
audio: bool
|
||||
|
||||
class RoomInfoResponseData(TypedDict):
|
||||
id: str
|
||||
users: dict[str, VoiceUser]
|
||||
videoAllowed: bool
|
||||
|
||||
class RoomInfoResponse(BaseWS):
|
||||
type: Literal["RoomInfo"]
|
||||
data: RoomInfoResponseData
|
||||
|
||||
class InitializeTransportCommandData(TypedDict):
|
||||
mode: Literal["SplitWebRTC", "CombinedWebRTC", "CombinedRTP"]
|
||||
rtpCapabilities: RTPCapabilities
|
||||
|
||||
class InitializeTransportCommand(BaseWS):
|
||||
type: Literal["InitializeTransports"]
|
||||
data: InitializeTransportCommandData
|
||||
|
||||
class IceParameters(TypedDict):
|
||||
usernameFragment: str
|
||||
password: str
|
||||
iceLite: NotRequired[bool]
|
||||
|
||||
class IceCandidate(TypedDict):
|
||||
foundation: str
|
||||
priority: str
|
||||
ip: str
|
||||
protocol: TransportProtocol
|
||||
tcp_type: NotRequired[Literal["passive"]]
|
||||
|
||||
class DTLSFingerprints(TypedDict):
|
||||
algorithm: Literal["Sha1", "Sha224", "Sha256", "Sha384", "Sha512"]
|
||||
value: str
|
||||
|
||||
class DtlsParameters(TypedDict):
|
||||
role: Literal["auto", "client", "server"]
|
||||
fingerprints: list[DTLSFingerprints]
|
||||
|
||||
class SctpParameters(TypedDict):
|
||||
port: int
|
||||
OS: int
|
||||
MIS: int
|
||||
max_message_size: int
|
||||
|
||||
class TransportInfo(TypedDict):
|
||||
id: str
|
||||
iceParameters: IceParameters
|
||||
iceCandidates: list[IceCandidate]
|
||||
dtlsParameters: DtlsParameters
|
||||
sctpParameters: Optional[SctpParameters]
|
||||
|
||||
class InitializeTransportResponseDataSplitWebRTC(TypedDict):
|
||||
recvTransport: TransportInfo
|
||||
sendTransport: TransportInfo
|
||||
|
||||
class InitializeTransportResponseDataCombinedWebRTC(TypedDict):
|
||||
transport: TransportInfo
|
||||
|
||||
class InitializeTransportResponseDataCombinedRtp(TypedDict):
|
||||
ip: bytes
|
||||
port: int
|
||||
protocol: TransportProtocol
|
||||
id: str
|
||||
srtpCryptoSuite: Literal["AES_CM_128_HMAC_SHA1_80", "AES_CM_128_HMAC_SHA1_32"]
|
||||
|
||||
InitializeTransportResponseData = Union[InitializeTransportResponseDataSplitWebRTC, InitializeTransportResponseDataCombinedWebRTC, InitializeTransportResponseDataCombinedRtp]
|
||||
|
||||
class InitializeTransportResponse(BaseWS):
|
||||
type: Literal["InitializeTransports"]
|
||||
data: InitializeTransportResponseData
|
||||
|
||||
class SrtpParameters(TypedDict):
|
||||
cryptoSuite: Literal["AES_CM_128_HMAC_SHA1_80", "AES_CM_128_HMAC_SHA1_32"]
|
||||
keyBase64: str
|
||||
|
||||
class ConnectTransportData(TypedDict):
|
||||
id: str
|
||||
dtlsParameters: NotRequired[DtlsParameters]
|
||||
srtpParameters: NotRequired[SrtpParameters]
|
||||
|
||||
class ConnectTransportCommand(BaseWS):
|
||||
type: Literal["ConnectTransport"]
|
||||
data: ConnectTransportData
|
||||
|
||||
class ConnectTransportResponse(BaseWS):
|
||||
type: Literal["ConnectTransport"]
|
||||
|
||||
class RTPEncodingRtx(TypedDict):
|
||||
ssrc: int
|
||||
|
||||
class RTPEncoding(TypedDict, total=False):
|
||||
ssrc: int
|
||||
rid: str
|
||||
codec_payload_type: int
|
||||
rtx: RTPEncodingRtx
|
||||
dtx: bool
|
||||
scalability_mode: str
|
||||
scale_resolution_down_by: float
|
||||
max_bitrate: int
|
||||
|
||||
class RTPHeaderExtensionParameters(TypedDict):
|
||||
uri: str
|
||||
id: int
|
||||
encrypt: bool
|
||||
|
||||
class RtcpParameters(TypedDict):
|
||||
cname: NotRequired[str]
|
||||
reduceSize: bool
|
||||
mux: NotRequired[bool]
|
||||
|
||||
class RTPParameters(TypedDict):
|
||||
mid: NotRequired[str]
|
||||
codecs: list[RTPCodec]
|
||||
headerExtensions: list[RTPHeaderExtensionParameters]
|
||||
encodings: list[RTPEncoding]
|
||||
rtcp: RtcpParameters
|
||||
|
||||
class StartProduceCommandData(TypedDict):
|
||||
type: Literal["audio", "video", "saudio", "svideo"]
|
||||
rtpParameters: RTPParameters
|
||||
|
||||
class StartProduceCommand(BaseWS):
|
||||
type: Literal["StartProduce"]
|
||||
data: StartProduceCommandData
|
||||
|
||||
class StartProduceResponseData(TypedDict):
|
||||
producerId: str
|
||||
|
||||
class StartProduceResponse(BaseWS):
|
||||
type: Literal["StartProduce"]
|
||||
data: StartProduceResponseData
|
||||
|
||||
WSCommand = Union[AuthenticateCommand, RoomInfoCommand, InitializeTransportCommand, ConnectTransportCommand, StartProduceCommand]
|
||||
WSResponse = Union[AuthenticateResponse, RoomInfoResponse, InitializeTransportResponse, ConnectTransportResponse, StartProduceResponse]
|
||||
|
||||
__all__ = ("BaseWS", "WSCommand", "WSResponse", "AuthenticateCommand", "RoomInfoCommand", "InitializeTransportCommand", "ConnectTransportCommand", "StartProduceCommand", "AuthenticateResponse", "RoomInfoResponse", "InitializeTransportResponse", "ConnectTransportResponse", "StartProduceResponse")
|
||||
Executable
+204
@@ -0,0 +1,204 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import random
|
||||
import string
|
||||
import nacl
|
||||
import nacl.public
|
||||
import nacl.encoding
|
||||
import nacl.utils
|
||||
import base64
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, BinaryIO
|
||||
|
||||
from revolt.types.voice import AuthenticationResponseData, InitializeTransportResponseDataCombinedRtp, RTPCapabilities, RTPHeaderExtensionParameters, StartProduceResponseData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .state import State
|
||||
from .types.voice import *
|
||||
|
||||
__all__ = ("VoiceClient",)
|
||||
|
||||
logger = logging.getLogger("revolt.voice_client")
|
||||
|
||||
class VoiceClient:
|
||||
__slots__ = ("session", "token", "websocket", "dispatch", "state", "ws_url", "channel_id", "loop", "id", "futures", "capabilities", "rtp_info", "ssrc", "cname", "producer_id", "key", "salt")
|
||||
|
||||
def __init__(self, session: aiohttp.ClientSession, ws_url: str, channel_id: str, token: str, state: State):
|
||||
self.id = 0
|
||||
self.session = session
|
||||
self.ws_url = ws_url
|
||||
self.channel_id = channel_id
|
||||
self.websocket: aiohttp.ClientWebSocketResponse
|
||||
self.token = token
|
||||
self.dispatch = state.dispatch
|
||||
self.state = state
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.futures = dict[int, asyncio.Future]()
|
||||
self.ssrc = random.randint(0, 1 << 31)
|
||||
self.cname = "".join(random.choices(string.ascii_letters, k=15))
|
||||
|
||||
self.capabilities: RTPCapabilities
|
||||
self.rtp_info: InitializeTransportResponseDataCombinedRtp
|
||||
self.producer_id: str
|
||||
self.key = nacl.utils.random(16)
|
||||
self.salt = nacl.utils.random(14)
|
||||
|
||||
async def send_payload(self, payload) -> int:
|
||||
self.id += 1
|
||||
payload["id"] = self.id
|
||||
|
||||
logging.info(payload)
|
||||
|
||||
await self.websocket.send_str(json.dumps(payload))
|
||||
|
||||
return self.id
|
||||
|
||||
async def heartbeat(self):
|
||||
while not self.websocket.closed:
|
||||
# logger.info("Sending voice hearbeat")
|
||||
await self.websocket.ping()
|
||||
await asyncio.sleep(15)
|
||||
|
||||
async def send_authenticate(self):
|
||||
payload: AuthenticateCommand = {
|
||||
"type": "Authenticate",
|
||||
"data": {
|
||||
"token": self.token,
|
||||
"roomId": self.channel_id
|
||||
}
|
||||
}
|
||||
|
||||
id = await self.send_payload(payload)
|
||||
response: AuthenticationResponseData = await self.set_future(id)
|
||||
self.capabilities = response["rtpCapabilities"]
|
||||
|
||||
async def send_initialize_transport(self):
|
||||
payload: InitializeTransportCommand = {
|
||||
"type": "InitializeTransports",
|
||||
"data": {
|
||||
"mode": "CombinedRTP",
|
||||
"rtpCapabilities": self.capabilities
|
||||
}
|
||||
}
|
||||
|
||||
id = await self.send_payload(payload)
|
||||
response: InitializeTransportResponseDataCombinedRtp = await self.set_future(id)
|
||||
self.rtp_info = response
|
||||
|
||||
async def send_connect_transport(self):
|
||||
payload: ConnectTransportCommand = {
|
||||
"type": "ConnectTransport",
|
||||
"data": {
|
||||
"srtpParameters": {
|
||||
"cryptoSuite": self.rtp_info["srtpCryptoSuite"],
|
||||
"keyBase64": base64.b64encode(self.salt + self.key).decode(),
|
||||
},
|
||||
"id": self.rtp_info["id"],
|
||||
}
|
||||
}
|
||||
|
||||
await self.send_payload(payload)
|
||||
|
||||
async def send_start_produce(self, type: Literal["audio", "video", "saudio", "svideo"]):
|
||||
header_extensions: list[RTPHeaderExtensionParameters] = [{"uri": d["uri"], "id": d["preferredId"], "encrypt": d["preferredEncrypt"]} for d in self.capabilities["headerExtensions"]]
|
||||
payload: StartProduceCommand = {
|
||||
"type": "StartProduce",
|
||||
"data": {
|
||||
"type": type,
|
||||
"rtpParameters": {
|
||||
"codecs": self.capabilities["codecs"],
|
||||
"headerExtensions": header_extensions,
|
||||
"encodings": [{
|
||||
"ssrc": self.ssrc,
|
||||
}],
|
||||
"mid": "0",
|
||||
"rtcp": {
|
||||
"cname": self.cname,
|
||||
"reduceSize": True
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
id = await self.send_payload(payload)
|
||||
response: StartProduceResponseData = await self.set_future(id)
|
||||
producer_id = response["producerId"]
|
||||
self.producer_id = producer_id
|
||||
|
||||
async def handle_event(self, payload: WSResponse):
|
||||
event_type = payload["type"].lower()
|
||||
logger.debug("Recieved event %s %s", event_type, payload)
|
||||
|
||||
try:
|
||||
func = getattr(self, f"handle_{event_type}")
|
||||
except:
|
||||
logger.debug("Unknown event '%s'", event_type)
|
||||
return
|
||||
|
||||
await func(payload)
|
||||
|
||||
async def handle_authenticate(self, _):
|
||||
logger.info("Successfully authenticated")
|
||||
|
||||
async def set_future(self, id: int):
|
||||
future = asyncio.Future()
|
||||
self.futures[id] = future
|
||||
return await future
|
||||
|
||||
async def websocket_loop(self):
|
||||
async for msg in self.websocket:
|
||||
payload: WSResponse = json.loads(msg.data)
|
||||
print(payload)
|
||||
|
||||
if future := self.futures.get(payload.get("id", "")):
|
||||
future.set_result(payload.get("data"))
|
||||
|
||||
self.loop.create_task(self.handle_event(payload))
|
||||
|
||||
async def start(self):
|
||||
self.websocket = await self.session.ws_connect(self.ws_url)
|
||||
|
||||
asyncio.create_task(self.heartbeat())
|
||||
asyncio.create_task(self.websocket_loop())
|
||||
|
||||
async def send_audio(self, audio: BinaryIO):
|
||||
await self.send_start_produce("audio")
|
||||
|
||||
|
||||
"""
|
||||
{
|
||||
'type': 'StartProduce',
|
||||
'data': {
|
||||
'type': 'audio',
|
||||
'rtpParameters': {
|
||||
'codecs': [
|
||||
{'kind': 'audio', 'mimeType': 'audio/opus', 'preferredPayloadType': 100, 'clockRate': 48000, 'channels': 2, 'parameters': {}, 'rtcpFeedback': [{'type': 'transport-cc', 'parameter': ''}]}
|
||||
],
|
||||
'headerExtensions': [{'uri': 'urn:ietf:params:rtp-hdrext:sdes:mid', 'id': 1, 'encrypt': False}, {'uri': 'urn:ietf:params:rtp-hdrext:sdes:mid', 'id': 1, 'encrypt': False}, {'uri': 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', 'id': 2, 'encrypt': False}, {'uri': 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id', 'id': 3, 'encrypt': False}, {'uri': 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', 'id': 4, 'encrypt': False}, {'uri': 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', 'id': 4, 'encrypt': False}, {'uri': 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', 'id': 5, 'encrypt': False}, {'uri': 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', 'id': 5, 'encrypt': False}, {'uri': 'http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07', 'id': 6, 'encrypt': False}, {'uri': 'urn:ietf:params:rtp-hdrext:framemarking', 'id': 7, 'encrypt': False}, {'uri': 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', 'id': 10, 'encrypt': False}, {'uri': 'urn:3gpp:video-orientation', 'id': 11, 'encrypt': False}, {'uri': 'urn:ietf:params:rtp-hdrext:toffset', 'id': 12, 'encrypt': False}],
|
||||
'encodings': [{'ssrc': 896646738}],
|
||||
'mid': '0',
|
||||
'rtcp': {'cname': 'iUaoUqyQjShruzl', 'reduceSize': True}}
|
||||
},
|
||||
'id': 4}
|
||||
"""
|
||||
"""
|
||||
{
|
||||
"type": "StartProduce",
|
||||
"data": {
|
||||
"type":"audio",
|
||||
"rtpParameters": {
|
||||
"codecs": [
|
||||
{"mimeType":"audio/opus","payloadType":111,"clockRate":48000,"channels":2,"parameters":{"minptime":10,"useinbandfec":1},"rtcpFeedback":[{"type":"transport-cc","parameter":""}]}
|
||||
],
|
||||
"headerExtensions":[{"uri":"urn:ietf:params:rtp-hdrext:sdes:mid","id":4,"encrypt":false,"parameters":{}},{"uri":"http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time","id":2,"encrypt":false,"parameters":{}},{"uri":"http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01","id":3,"encrypt":false,"parameters":{}},{"uri":"urn:ietf:params:rtp-hdrext:ssrc-audio-level","id":1,"encrypt":false,"parameters":{}}],
|
||||
"encodings":[{"ssrc":2276570677,"dtx":false}],
|
||||
"mid":"0"}
|
||||
"rtcp":{"cname":"KAhFB58ELM5Tf+9a","reducedSize":true},
|
||||
}
|
||||
}
|
||||
"id":7,
|
||||
"""
|
||||
Reference in New Issue
Block a user