Files
olcrtc/code/telemost_poc_datachannel.py
2026-04-20 06:17:54 +03:00

169 lines
7.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""PoC: Yandex Telemost DataChannel via Websocket and AIORTC."""
import asyncio
import json
import uuid
import time
import requests
import websockets
from urllib.parse import quote
from aiortc import RTCPeerConnection, RTCSessionDescription, RTCIceCandidate, RTCConfiguration, RTCIceServer
CONFERENCE_ID = "75047680642749"
API_BASE = "https://cloud-api.yandex.ru/telemost_front/v2/telemost"
ICE_SERVER = RTCIceServer(urls=["stun:stun.rtc.yandex.net:3478"])
TEST_MESSAGES = ["Hello Yandex Telemost!", "Hello world", "X" * 100, "Final test"]
def _gen_uuid() -> str: return str(uuid.uuid4())
def _get_conn_info(display_name: str) -> dict:
url = f"{API_BASE}/conferences/{quote(f'https://telemost.yandex.ru/j/{CONFERENCE_ID}', safe='')}/connection"
headers = {
"User-Agent": "Mozilla/5.0 (Linux x86_64)",
"Client-Instance-Id": _gen_uuid(),
"X-Telemost-Client-Version": "187.1.0",
"idempotency-key": _gen_uuid(),
}
params = {"next_gen_media_platform_allowed": "true", "display_name": display_name, "waiting_room_supported": "true"}
resp = requests.get(url, params=params, headers=headers)
resp.raise_for_status()
return resp.json()
async def _create_peer(name: str, is_server: bool = False, stats: dict = None) -> dict:
info = _get_conn_info(name)
ws = await websockets.connect(info["client_configuration"]["media_server_url"])
pc_sub = RTCPeerConnection(RTCConfiguration(iceServers=[ICE_SERVER]))
pc_pub = RTCPeerConnection(RTCConfiguration(iceServers=[ICE_SERVER]))
dc_pub = pc_pub.createDataChannel("olcrtc", ordered=True)
dc_ready = asyncio.Event()
@dc_pub.on("open")
def on_open(): dc_ready.set()
@dc_pub.on("message")
def on_pub_msg(msg): stats["recv"] += 1
@pc_sub.on("datachannel")
def on_sub_dc(channel):
@channel.on("message")
def on_msg(m):
stats["recv"] += 1
if is_server and channel.label == "olcrtc":
try:
dc_pub.send(f"Echo: {m}")
stats["sent"] += 1
except: pass
await ws.send(json.dumps({
"uid": _gen_uuid(),
"hello": {
"participantMeta": {"name": name, "role": "SPEAKER", "sendAudio": False, "sendVideo": False},
"participantAttributes": {"name": name, "role": "SPEAKER"},
"sendAudio": False, "sendVideo": False, "sendSharing": False,
"participantId": info["peer_id"], "roomId": info["room_id"],
"serviceName": "telemost", "credentials": info["credentials"],
"capabilitiesOffer": {"offerAnswerMode": ["SEPARATE"], "initialSubscriberOffer": ["ON_HELLO"], "slotsMode": ["FROM_CONTROLLER"], "simulcastMode": ["DISABLED"], "selfVadStatus": ["FROM_SERVER"], "dataChannelSharing": ["TO_RTP"]},
"sdkInfo": {"implementation": "python", "version": "1.0.0", "userAgent": f"OlcRTC-{name}"},
"sdkInitializationId": _gen_uuid(), "disablePublisher": False, "disableSubscriber": False
}
}))
async def _ws_loop():
pub_sdp_sent = False
while True:
try:
data = json.loads(await ws.recv())
uid = data.get("uid")
if "serverHello" in data:
await ws.send(json.dumps({"uid": uid, "ack": {"status": {"code": "OK"}}}))
elif "subscriberSdpOffer" in data and not pub_sdp_sent:
sdp = data["subscriberSdpOffer"]
await pc_sub.setRemoteDescription(RTCSessionDescription(sdp=sdp["sdp"], type="offer"))
ans = await pc_sub.createAnswer()
await pc_sub.setLocalDescription(ans)
await ws.send(json.dumps({"uid": _gen_uuid(), "subscriberSdpAnswer": {"pcSeq": sdp["pcSeq"], "sdp": pc_sub.localDescription.sdp}}))
await ws.send(json.dumps({"uid": uid, "ack": {"status": {"code": "OK"}}}))
await asyncio.sleep(0.3)
pub_offer = await pc_pub.createOffer()
await pc_pub.setLocalDescription(pub_offer)
await ws.send(json.dumps({"uid": _gen_uuid(), "publisherSdpOffer": {"pcSeq": 1, "sdp": pc_pub.localDescription.sdp}}))
pub_sdp_sent = True
elif "publisherSdpAnswer" in data:
await pc_pub.setRemoteDescription(RTCSessionDescription(sdp=data["publisherSdpAnswer"]["sdp"], type="answer"))
await ws.send(json.dumps({"uid": uid, "ack": {"status": {"code": "OK"}}}))
elif "webrtcIceCandidate" in data:
cand = data["webrtcIceCandidate"]
parts = cand["candidate"].split()
if len(parts) >= 8:
ice = RTCIceCandidate(component=int(parts[1]), foundation=parts[0].replace("candidate:", ""), ip=parts[4], port=int(parts[5]), priority=int(parts[3]), protocol=parts[2], type=parts[7], sdpMid=cand["sdpMid"], sdpMLineIndex=cand["sdpMlineIndex"])
await (pc_sub if cand.get("target") == "SUBSCRIBER" else pc_pub).addIceCandidate(ice)
except Exception: break
async def _on_ice(event, target):
if event.candidate:
await ws.send(json.dumps({"uid": _gen_uuid(), "webrtcIceCandidate": {"candidate": event.candidate.candidate, "sdpMid": event.candidate.sdpMid, "sdpMlineIndex": event.candidate.sdpMLineIndex, "target": target, "pcSeq": 1}}))
pc_sub.on("icecandidate", lambda e: asyncio.create_task(_on_ice(e, "SUBSCRIBER")))
pc_pub.on("icecandidate", lambda e: asyncio.create_task(_on_ice(e, "PUBLISHER")))
return {"dc": dc_pub, "ready": dc_ready, "task": asyncio.create_task(_ws_loop()), "ws": ws, "pc_sub": pc_sub, "pc_pub": pc_pub}
async def run_poc() -> dict:
print("\n--- Yandex Telemost PoC ---")
results = {"server_ok": False, "client_ok": False, "sent": 0, "recv": 0, "errors": []}
s_stats, c_stats = {"sent": 0, "recv": 0}, {"sent": 0, "recv": 0}
print("[1/3] Connecting Server & Client...")
try:
server = await _create_peer("Server", is_server=True, stats=s_stats)
await asyncio.wait_for(server["ready"].wait(), 10.0)
results["server_ok"] = True
client = await _create_peer("Client", is_server=False, stats=c_stats)
await asyncio.wait_for(client["ready"].wait(), 10.0)
results["client_ok"] = True
print(" :P Peers connected")
except Exception as e:
results["errors"].append(str(e))
return results
print("\n[2/3] Exchanging messages...")
await asyncio.sleep(1)
for idx, msg in enumerate(TEST_MESSAGES, 1):
try:
client["dc"].send(msg)
c_stats["sent"] += 1
print(f" -> Sent: {msg}")
await asyncio.sleep(0.5)
except Exception as e:
results["errors"].append(f"Sending {idx} failed: {str(e)}")
await asyncio.sleep(2)
results["sent"], results["recv"] = c_stats["sent"], c_stats["recv"]
print("\n[3/3] Cleaning up...")
for p in (server, client):
p["task"].cancel()
await p["ws"].close()
await p["pc_sub"].close()
await p["pc_pub"].close()
return results
def print_results(res: dict):
print("\n--- TEST RESULTS ---")
print(f"Server: {':P' if res['server_ok'] else 'X'} / Client: {':P' if res['client_ok'] else 'X'}")
print(f"Messages: Sent {res['sent']} / Recv {res['recv']}")
if res['errors']:
for e in res['errors']: print(f" Error: {e}")
print(f"\n{':P SUCCESS' if res['sent'] and res['sent'] == res['recv'] else 'X FAILED'}\n")
if __name__ == "__main__":
try: res = asyncio.run(run_poc()); print_results(res)
except KeyboardInterrupt: pass