mirror of
https://github.com/openlibrecommunity/olcrtc.git
synced 2026-05-26 07:08:11 +00:00
chore(code): remove legacy implementation files
This commit is contained in:
415
code/dcsend.py
415
code/dcsend.py
@@ -1,415 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
import websockets
|
||||
import requests
|
||||
from urllib.parse import quote
|
||||
from aiortc import RTCPeerConnection, RTCSessionDescription, RTCIceCandidate, RTCConfiguration, RTCIceServer
|
||||
|
||||
CONFERENCE_ID = "75047680642749"
|
||||
CONFERENCE_URL = f"https://telemost.yandex.ru/j/{CONFERENCE_ID}"
|
||||
API_BASE = "https://cloud-api.yandex.ru/telemost_front/v2/telemost"
|
||||
|
||||
CHUNK_SIZE = 7168
|
||||
HEADER_SIZE = 1024
|
||||
|
||||
def generate_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def get_connection_info(display_name):
|
||||
url = f"{API_BASE}/conferences/{quote(CONFERENCE_URL, safe='')}/connection"
|
||||
params = {
|
||||
"next_gen_media_platform_allowed": "true",
|
||||
"display_name": display_name,
|
||||
"waiting_room_supported": "true"
|
||||
}
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0",
|
||||
"Accept": "*/*",
|
||||
"content-type": "application/json",
|
||||
"Client-Instance-Id": generate_uuid(),
|
||||
"X-Telemost-Client-Version": "187.1.0",
|
||||
"idempotency-key": generate_uuid(),
|
||||
"Origin": "https://telemost.yandex.ru",
|
||||
"Referer": "https://telemost.yandex.ru/"
|
||||
}
|
||||
|
||||
response = requests.get(url, params=params, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def chunk_data(data, transfer_id):
|
||||
total_size = len(data)
|
||||
chunk_count = (total_size + CHUNK_SIZE - 1) // CHUNK_SIZE
|
||||
packets = []
|
||||
|
||||
for i in range(chunk_count):
|
||||
start = i * CHUNK_SIZE
|
||||
end = min(start + CHUNK_SIZE, total_size)
|
||||
chunk = data[start:end]
|
||||
|
||||
header = {"tid": transfer_id, "idx": i, "total": chunk_count, "size": total_size}
|
||||
header_json = json.dumps(header).encode()
|
||||
header_padded = header_json.ljust(HEADER_SIZE, b'\x00')
|
||||
|
||||
packets.append(header_padded + chunk)
|
||||
|
||||
return packets
|
||||
|
||||
class ChunkedReceiver:
|
||||
def __init__(self):
|
||||
self.buffers = {}
|
||||
self.completed = {}
|
||||
|
||||
def handle_chunk(self, packet):
|
||||
if len(packet) < HEADER_SIZE:
|
||||
return None
|
||||
|
||||
header_bytes = packet[:HEADER_SIZE].rstrip(b'\x00')
|
||||
chunk_data = packet[HEADER_SIZE:]
|
||||
|
||||
try:
|
||||
header = json.loads(header_bytes)
|
||||
except:
|
||||
return None
|
||||
|
||||
tid = header["tid"]
|
||||
idx = header["idx"]
|
||||
total = header["total"]
|
||||
|
||||
if tid not in self.buffers:
|
||||
self.buffers[tid] = {"chunks": {}, "total": total, "received": 0}
|
||||
|
||||
buf = self.buffers[tid]
|
||||
|
||||
if idx not in buf["chunks"]:
|
||||
buf["chunks"][idx] = chunk_data
|
||||
buf["received"] += 1
|
||||
|
||||
if buf["received"] == buf["total"]:
|
||||
complete = b"".join(buf["chunks"][i] for i in range(buf["total"]))
|
||||
self.completed[tid] = complete
|
||||
del self.buffers[tid]
|
||||
return tid
|
||||
|
||||
return None
|
||||
|
||||
async def create_peer(name, is_server=False):
|
||||
conn_info = get_connection_info(name)
|
||||
room_id = conn_info["room_id"]
|
||||
peer_id = conn_info["peer_id"]
|
||||
credentials = conn_info["credentials"]
|
||||
ws_url = conn_info["client_configuration"]["media_server_url"]
|
||||
|
||||
pc_sub = RTCPeerConnection(RTCConfiguration(
|
||||
iceServers=[RTCIceServer(urls=["stun:stun.rtc.yandex.net:3478"])]
|
||||
))
|
||||
|
||||
pc_pub = RTCPeerConnection(RTCConfiguration(
|
||||
iceServers=[RTCIceServer(urls=["stun:stun.rtc.yandex.net:3478"])]
|
||||
))
|
||||
|
||||
dc_pub = pc_pub.createDataChannel("dcsend", ordered=True)
|
||||
dc_pub_open = asyncio.Event()
|
||||
|
||||
receiver = ChunkedReceiver()
|
||||
|
||||
stats = {
|
||||
"sent": 0,
|
||||
"received": 0,
|
||||
"bytes_sent": 0,
|
||||
"bytes_received": 0,
|
||||
"messages": []
|
||||
}
|
||||
|
||||
@dc_pub.on("open")
|
||||
def on_pub_open():
|
||||
dc_pub_open.set()
|
||||
|
||||
@dc_pub.on("message")
|
||||
def on_pub_msg(msg):
|
||||
if isinstance(msg, str):
|
||||
stats["messages"].append(("text", msg))
|
||||
else:
|
||||
tid = receiver.handle_chunk(msg)
|
||||
if tid and tid in receiver.completed:
|
||||
data = receiver.completed[tid]
|
||||
stats["messages"].append(("data", data))
|
||||
del receiver.completed[tid]
|
||||
|
||||
@pc_sub.on("datachannel")
|
||||
def on_sub_dc(channel):
|
||||
@channel.on("message")
|
||||
def on_message(message):
|
||||
if isinstance(message, str):
|
||||
stats["messages"].append(("text", message))
|
||||
|
||||
if is_server and message.startswith("GET "):
|
||||
url = message[4:].strip()
|
||||
asyncio.create_task(handle_request(url, dc_pub, stats))
|
||||
else:
|
||||
tid = receiver.handle_chunk(message)
|
||||
if tid and tid in receiver.completed:
|
||||
data = receiver.completed[tid]
|
||||
stats["messages"].append(("data", data))
|
||||
del receiver.completed[tid]
|
||||
|
||||
ws = await websockets.connect(ws_url)
|
||||
|
||||
hello_msg = {
|
||||
"uid": generate_uuid(),
|
||||
"hello": {
|
||||
"participantMeta": {"name": name, "role": "SPEAKER", "sendAudio": False, "sendVideo": False},
|
||||
"participantAttributes": {"name": name, "role": "SPEAKER"},
|
||||
"sendAudio": False,
|
||||
"sendVideo": False,
|
||||
"sendSharing": False,
|
||||
"participantId": peer_id,
|
||||
"roomId": room_id,
|
||||
"serviceName": "telemost",
|
||||
"credentials": 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"DCSend-{name}"},
|
||||
"sdkInitializationId": generate_uuid(),
|
||||
"disablePublisher": False,
|
||||
"disableSubscriber": False
|
||||
}
|
||||
}
|
||||
|
||||
await ws.send(json.dumps(hello_msg))
|
||||
|
||||
publisher_sdp_sent = False
|
||||
|
||||
async def ws_handler():
|
||||
nonlocal publisher_sdp_sent
|
||||
while True:
|
||||
try:
|
||||
data = json.loads(await ws.recv())
|
||||
|
||||
if "serverHello" in data:
|
||||
await ws.send(json.dumps({"uid": data["uid"], "ack": {"status": {"code": "OK"}}}))
|
||||
|
||||
if "subscriberSdpOffer" in data and not publisher_sdp_sent:
|
||||
await pc_sub.setRemoteDescription(RTCSessionDescription(
|
||||
sdp=data["subscriberSdpOffer"]["sdp"], type="offer"
|
||||
))
|
||||
|
||||
answer = await pc_sub.createAnswer()
|
||||
await pc_sub.setLocalDescription(answer)
|
||||
|
||||
await ws.send(json.dumps({
|
||||
"uid": generate_uuid(),
|
||||
"subscriberSdpAnswer": {
|
||||
"pcSeq": data["subscriberSdpOffer"]["pcSeq"],
|
||||
"sdp": pc_sub.localDescription.sdp
|
||||
}
|
||||
}))
|
||||
|
||||
await ws.send(json.dumps({"uid": data["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": generate_uuid(),
|
||||
"publisherSdpOffer": {
|
||||
"pcSeq": 1,
|
||||
"sdp": pc_pub.localDescription.sdp
|
||||
}
|
||||
}))
|
||||
publisher_sdp_sent = True
|
||||
|
||||
if "publisherSdpAnswer" in data:
|
||||
await pc_pub.setRemoteDescription(RTCSessionDescription(
|
||||
sdp=data["publisherSdpAnswer"]["sdp"], type="answer"
|
||||
))
|
||||
await ws.send(json.dumps({"uid": data["uid"], "ack": {"status": {"code": "OK"}}}))
|
||||
|
||||
if "webrtcIceCandidate" in data:
|
||||
cand = data["webrtcIceCandidate"]
|
||||
try:
|
||||
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"]
|
||||
)
|
||||
|
||||
if cand.get("target") == "SUBSCRIBER":
|
||||
await pc_sub.addIceCandidate(ice)
|
||||
elif cand.get("target") == "PUBLISHER":
|
||||
await pc_pub.addIceCandidate(ice)
|
||||
except:
|
||||
pass
|
||||
|
||||
except:
|
||||
break
|
||||
|
||||
@pc_sub.on("icecandidate")
|
||||
async def on_sub_ice(event):
|
||||
if event.candidate:
|
||||
await ws.send(json.dumps({
|
||||
"uid": generate_uuid(),
|
||||
"webrtcIceCandidate": {
|
||||
"candidate": event.candidate.candidate,
|
||||
"sdpMid": event.candidate.sdpMid,
|
||||
"sdpMlineIndex": event.candidate.sdpMLineIndex,
|
||||
"target": "SUBSCRIBER",
|
||||
"pcSeq": 1
|
||||
}
|
||||
}))
|
||||
|
||||
@pc_pub.on("icecandidate")
|
||||
async def on_pub_ice(event):
|
||||
if event.candidate:
|
||||
await ws.send(json.dumps({
|
||||
"uid": generate_uuid(),
|
||||
"webrtcIceCandidate": {
|
||||
"candidate": event.candidate.candidate,
|
||||
"sdpMid": event.candidate.sdpMid,
|
||||
"sdpMlineIndex": event.candidate.sdpMLineIndex,
|
||||
"target": "PUBLISHER",
|
||||
"pcSeq": 1
|
||||
}
|
||||
}))
|
||||
|
||||
ws_task = asyncio.create_task(ws_handler())
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"dc_pub": dc_pub,
|
||||
"dc_pub_open": dc_pub_open,
|
||||
"stats": stats,
|
||||
"ws": ws,
|
||||
"ws_task": ws_task,
|
||||
"pc_sub": pc_sub,
|
||||
"pc_pub": pc_pub
|
||||
}
|
||||
|
||||
async def handle_request(url, dc, stats):
|
||||
try:
|
||||
if not url.startswith(('http://', 'https://')):
|
||||
url = 'https://' + url
|
||||
|
||||
print(f" -> Fetching {url}...")
|
||||
response = requests.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.content
|
||||
print(f" -> Got {len(data)} bytes, sending...")
|
||||
|
||||
transfer_id = generate_uuid()
|
||||
packets = chunk_data(data, transfer_id)
|
||||
|
||||
for packet in packets:
|
||||
while dc.bufferedAmount > CHUNK_SIZE * 2:
|
||||
await asyncio.sleep(0.001)
|
||||
dc.send(packet)
|
||||
stats["sent"] += 1
|
||||
stats["bytes_sent"] += len(packet)
|
||||
|
||||
print(f" :P Sent {len(packets)} chunks")
|
||||
|
||||
except Exception as e:
|
||||
print(f" X Error: {e}")
|
||||
try:
|
||||
dc.send(f"ERROR: {str(e)}")
|
||||
except:
|
||||
pass
|
||||
|
||||
async def run_dcsend():
|
||||
print(r"""
|
||||
DCSend - DataChannel Transfer
|
||||
Request/Response over Yandex Telemost SFU
|
||||
by zowue for olc
|
||||
""")
|
||||
|
||||
print("[1/3] Creating server peer...")
|
||||
try:
|
||||
server = await create_peer("Server", is_server=True)
|
||||
await asyncio.wait_for(server["dc_pub_open"].wait(), timeout=10.0)
|
||||
print(" :P Server ready")
|
||||
except Exception as e:
|
||||
print(f" X Error: {e}")
|
||||
return
|
||||
|
||||
print("\n[2/3] Creating client peer...")
|
||||
try:
|
||||
client = await create_peer("Client", is_server=False)
|
||||
await asyncio.wait_for(client["dc_pub_open"].wait(), timeout=10.0)
|
||||
print(" :P Client ready")
|
||||
except Exception as e:
|
||||
print(f" X Error: {e}")
|
||||
return
|
||||
|
||||
print("\n[3/3] Starting transfer...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
url = "zarazaex.xyz/curl.txt"
|
||||
print(f" -> Client requesting: {url}")
|
||||
|
||||
client["dc_pub"].send(f"GET {url}")
|
||||
|
||||
print(" -> Waiting for response...")
|
||||
|
||||
for _ in range(30):
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
for msg_type, msg_data in client["stats"]["messages"]:
|
||||
if msg_type == "data":
|
||||
print(f"\n :P Received {len(msg_data)} bytes")
|
||||
print("\n--- Response Content ---")
|
||||
try:
|
||||
print(msg_data.decode('utf-8'))
|
||||
except:
|
||||
print(f"[Binary data: {len(msg_data)} bytes]")
|
||||
print("--- End ---\n")
|
||||
|
||||
client["stats"]["messages"].clear()
|
||||
|
||||
print("\nCleaning up...")
|
||||
server["ws_task"].cancel()
|
||||
client["ws_task"].cancel()
|
||||
await server["ws"].close()
|
||||
await client["ws"].close()
|
||||
await server["pc_sub"].close()
|
||||
await server["pc_pub"].close()
|
||||
await client["pc_sub"].close()
|
||||
await client["pc_pub"].close()
|
||||
|
||||
print(":P Transfer complete")
|
||||
return
|
||||
|
||||
elif msg_type == "text" and msg_data.startswith("ERROR"):
|
||||
print(f"\n X {msg_data}\n")
|
||||
client["stats"]["messages"].clear()
|
||||
return
|
||||
|
||||
print("\n X Timeout waiting for response")
|
||||
|
||||
async def main():
|
||||
await run_dcsend()
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nTransfer interrupted.")
|
||||
455
code/dcstream.py
455
code/dcstream.py
@@ -1,455 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
import websockets
|
||||
import requests
|
||||
from urllib.parse import quote
|
||||
from aiortc import RTCPeerConnection, RTCSessionDescription, RTCIceCandidate, RTCConfiguration, RTCIceServer
|
||||
|
||||
CONFERENCE_ID = "75047680642749"
|
||||
CONFERENCE_URL = f"https://telemost.yandex.ru/j/{CONFERENCE_ID}"
|
||||
API_BASE = "https://cloud-api.yandex.ru/telemost_front/v2/telemost"
|
||||
|
||||
CHUNK_SIZE = 8152
|
||||
BUFFER_THRESHOLD = 262144
|
||||
SEND_DELAY = 0.0005
|
||||
|
||||
def generate_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def get_connection_info(display_name):
|
||||
url = f"{API_BASE}/conferences/{quote(CONFERENCE_URL, safe='')}/connection"
|
||||
params = {
|
||||
"next_gen_media_platform_allowed": "true",
|
||||
"display_name": display_name,
|
||||
"waiting_room_supported": "true"
|
||||
}
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0",
|
||||
"Accept": "*/*",
|
||||
"content-type": "application/json",
|
||||
"Client-Instance-Id": generate_uuid(),
|
||||
"X-Telemost-Client-Version": "187.1.0",
|
||||
"idempotency-key": generate_uuid(),
|
||||
"Origin": "https://telemost.yandex.ru",
|
||||
"Referer": "https://telemost.yandex.ru/"
|
||||
}
|
||||
|
||||
response = requests.get(url, params=params, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
class StreamReceiver:
|
||||
def __init__(self):
|
||||
self.streams = {}
|
||||
self.data = {}
|
||||
|
||||
def handle_packet(self, packet):
|
||||
if len(packet) < 40:
|
||||
return
|
||||
|
||||
try:
|
||||
stream_id = packet[:36].decode().strip()
|
||||
seq = int.from_bytes(packet[36:40], 'big')
|
||||
chunk = packet[40:]
|
||||
|
||||
if stream_id not in self.streams:
|
||||
self.streams[stream_id] = {}
|
||||
self.data[stream_id] = []
|
||||
|
||||
self.streams[stream_id][seq] = chunk
|
||||
except:
|
||||
pass
|
||||
|
||||
def get_data(self, stream_id):
|
||||
if stream_id not in self.streams:
|
||||
return b""
|
||||
|
||||
result = []
|
||||
seq = 0
|
||||
while seq in self.streams[stream_id]:
|
||||
result.append(self.streams[stream_id][seq])
|
||||
seq += 1
|
||||
|
||||
return b"".join(result)
|
||||
|
||||
async def create_peer(name, is_server=False):
|
||||
conn_info = get_connection_info(name)
|
||||
room_id = conn_info["room_id"]
|
||||
peer_id = conn_info["peer_id"]
|
||||
credentials = conn_info["credentials"]
|
||||
ws_url = conn_info["client_configuration"]["media_server_url"]
|
||||
|
||||
pc_sub = RTCPeerConnection(RTCConfiguration(
|
||||
iceServers=[RTCIceServer(urls=["stun:stun.rtc.yandex.net:3478"])]
|
||||
))
|
||||
|
||||
pc_pub = RTCPeerConnection(RTCConfiguration(
|
||||
iceServers=[RTCIceServer(urls=["stun:stun.rtc.yandex.net:3478"])]
|
||||
))
|
||||
|
||||
dc_pub = pc_pub.createDataChannel("dcstream", ordered=True)
|
||||
dc_pub_open = asyncio.Event()
|
||||
|
||||
receiver = StreamReceiver()
|
||||
|
||||
stats = {
|
||||
"sent": 0,
|
||||
"received": 0,
|
||||
"bytes_sent": 0,
|
||||
"bytes_received": 0,
|
||||
"start_time": None,
|
||||
"commands": []
|
||||
}
|
||||
|
||||
@dc_pub.on("open")
|
||||
def on_pub_open():
|
||||
dc_pub_open.set()
|
||||
|
||||
@dc_pub.on("message")
|
||||
def on_pub_msg(msg):
|
||||
if isinstance(msg, str):
|
||||
stats["commands"].append(msg)
|
||||
else:
|
||||
receiver.handle_packet(msg)
|
||||
stats["received"] += 1
|
||||
stats["bytes_received"] += len(msg)
|
||||
|
||||
@pc_sub.on("datachannel")
|
||||
def on_sub_dc(channel):
|
||||
@channel.on("message")
|
||||
def on_message(message):
|
||||
if isinstance(message, str):
|
||||
stats["commands"].append(message)
|
||||
|
||||
if is_server and message.startswith("STREAM "):
|
||||
url = message[7:].strip()
|
||||
asyncio.create_task(stream_data(url, dc_pub, stats))
|
||||
else:
|
||||
receiver.handle_packet(message)
|
||||
stats["received"] += 1
|
||||
stats["bytes_received"] += len(message)
|
||||
|
||||
ws = await websockets.connect(ws_url)
|
||||
|
||||
hello_msg = {
|
||||
"uid": generate_uuid(),
|
||||
"hello": {
|
||||
"participantMeta": {"name": name, "role": "SPEAKER", "sendAudio": False, "sendVideo": False},
|
||||
"participantAttributes": {"name": name, "role": "SPEAKER"},
|
||||
"sendAudio": False,
|
||||
"sendVideo": False,
|
||||
"sendSharing": False,
|
||||
"participantId": peer_id,
|
||||
"roomId": room_id,
|
||||
"serviceName": "telemost",
|
||||
"credentials": 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"DCStream-{name}"},
|
||||
"sdkInitializationId": generate_uuid(),
|
||||
"disablePublisher": False,
|
||||
"disableSubscriber": False
|
||||
}
|
||||
}
|
||||
|
||||
await ws.send(json.dumps(hello_msg))
|
||||
|
||||
publisher_sdp_sent = False
|
||||
|
||||
async def ws_handler():
|
||||
nonlocal publisher_sdp_sent
|
||||
while True:
|
||||
try:
|
||||
data = json.loads(await ws.recv())
|
||||
|
||||
if "serverHello" in data:
|
||||
await ws.send(json.dumps({"uid": data["uid"], "ack": {"status": {"code": "OK"}}}))
|
||||
|
||||
if "subscriberSdpOffer" in data and not publisher_sdp_sent:
|
||||
await pc_sub.setRemoteDescription(RTCSessionDescription(
|
||||
sdp=data["subscriberSdpOffer"]["sdp"], type="offer"
|
||||
))
|
||||
|
||||
answer = await pc_sub.createAnswer()
|
||||
await pc_sub.setLocalDescription(answer)
|
||||
|
||||
await ws.send(json.dumps({
|
||||
"uid": generate_uuid(),
|
||||
"subscriberSdpAnswer": {
|
||||
"pcSeq": data["subscriberSdpOffer"]["pcSeq"],
|
||||
"sdp": pc_sub.localDescription.sdp
|
||||
}
|
||||
}))
|
||||
|
||||
await ws.send(json.dumps({"uid": data["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": generate_uuid(),
|
||||
"publisherSdpOffer": {
|
||||
"pcSeq": 1,
|
||||
"sdp": pc_pub.localDescription.sdp
|
||||
}
|
||||
}))
|
||||
publisher_sdp_sent = True
|
||||
|
||||
if "publisherSdpAnswer" in data:
|
||||
await pc_pub.setRemoteDescription(RTCSessionDescription(
|
||||
sdp=data["publisherSdpAnswer"]["sdp"], type="answer"
|
||||
))
|
||||
await ws.send(json.dumps({"uid": data["uid"], "ack": {"status": {"code": "OK"}}}))
|
||||
|
||||
if "webrtcIceCandidate" in data:
|
||||
cand = data["webrtcIceCandidate"]
|
||||
try:
|
||||
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"]
|
||||
)
|
||||
|
||||
if cand.get("target") == "SUBSCRIBER":
|
||||
await pc_sub.addIceCandidate(ice)
|
||||
elif cand.get("target") == "PUBLISHER":
|
||||
await pc_pub.addIceCandidate(ice)
|
||||
except:
|
||||
pass
|
||||
|
||||
except:
|
||||
break
|
||||
|
||||
@pc_sub.on("icecandidate")
|
||||
async def on_sub_ice(event):
|
||||
if event.candidate:
|
||||
await ws.send(json.dumps({
|
||||
"uid": generate_uuid(),
|
||||
"webrtcIceCandidate": {
|
||||
"candidate": event.candidate.candidate,
|
||||
"sdpMid": event.candidate.sdpMid,
|
||||
"sdpMlineIndex": event.candidate.sdpMLineIndex,
|
||||
"target": "SUBSCRIBER",
|
||||
"pcSeq": 1
|
||||
}
|
||||
}))
|
||||
|
||||
@pc_pub.on("icecandidate")
|
||||
async def on_pub_ice(event):
|
||||
if event.candidate:
|
||||
await ws.send(json.dumps({
|
||||
"uid": generate_uuid(),
|
||||
"webrtcIceCandidate": {
|
||||
"candidate": event.candidate.candidate,
|
||||
"sdpMid": event.candidate.sdpMid,
|
||||
"sdpMlineIndex": event.candidate.sdpMLineIndex,
|
||||
"target": "PUBLISHER",
|
||||
"pcSeq": 1
|
||||
}
|
||||
}))
|
||||
|
||||
ws_task = asyncio.create_task(ws_handler())
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"dc_pub": dc_pub,
|
||||
"dc_pub_open": dc_pub_open,
|
||||
"stats": stats,
|
||||
"receiver": receiver,
|
||||
"ws": ws,
|
||||
"ws_task": ws_task,
|
||||
"pc_sub": pc_sub,
|
||||
"pc_pub": pc_pub
|
||||
}
|
||||
|
||||
async def stream_data(url, dc, stats):
|
||||
try:
|
||||
if not url.startswith(('http://', 'https://')):
|
||||
url = 'https://' + url
|
||||
|
||||
print(f" -> Streaming {url}...")
|
||||
|
||||
stream_id = generate_uuid()
|
||||
seq = 0
|
||||
|
||||
response = requests.get(url, stream=True, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
|
||||
if stats["start_time"] is None:
|
||||
import time
|
||||
stats["start_time"] = time.time()
|
||||
|
||||
dc.send(f"START {stream_id} {total_size}")
|
||||
|
||||
bytes_sent = 0
|
||||
last_progress = 0
|
||||
stall_count = 0
|
||||
|
||||
for chunk in response.iter_content(chunk_size=CHUNK_SIZE):
|
||||
if not chunk:
|
||||
break
|
||||
|
||||
wait_count = 0
|
||||
while dc.bufferedAmount > BUFFER_THRESHOLD:
|
||||
await asyncio.sleep(0.01)
|
||||
wait_count += 1
|
||||
if wait_count > 500:
|
||||
stall_count += 1
|
||||
if stall_count > 3:
|
||||
print(f" X Buffer stalled at {dc.bufferedAmount} bytes")
|
||||
raise Exception("Buffer overflow")
|
||||
wait_count = 0
|
||||
|
||||
packet = stream_id.encode().ljust(36) + seq.to_bytes(4, 'big') + chunk
|
||||
dc.send(packet)
|
||||
|
||||
stats["sent"] += 1
|
||||
stats["bytes_sent"] += len(packet)
|
||||
bytes_sent += len(chunk)
|
||||
seq += 1
|
||||
|
||||
await asyncio.sleep(SEND_DELAY)
|
||||
|
||||
if total_size > 0:
|
||||
progress = (bytes_sent / total_size) * 100
|
||||
if progress - last_progress >= 5:
|
||||
print(f" -> Sending: {bytes_sent / 1024 / 1024:.2f} MB / {total_size / 1024 / 1024:.2f} MB ({progress:.1f}%) [buffer: {dc.bufferedAmount}]")
|
||||
last_progress = progress
|
||||
|
||||
dc.send(f"END {stream_id}")
|
||||
|
||||
import time
|
||||
elapsed = time.time() - stats["start_time"]
|
||||
mbps = (stats["bytes_sent"] * 8) / (elapsed * 1_000_000) if elapsed > 0 else 0
|
||||
|
||||
print(f" :P Streamed {stats['bytes_sent']} bytes in {elapsed:.2f}s ({mbps:.2f} Mbps)")
|
||||
|
||||
except Exception as e:
|
||||
print(f" X Error: {e}")
|
||||
try:
|
||||
dc.send(f"ERROR {str(e)}")
|
||||
except:
|
||||
pass
|
||||
|
||||
async def run_stream():
|
||||
print(r"""
|
||||
DCStream - High-Speed DataChannel
|
||||
Optimized streaming over Yandex Telemost
|
||||
by zarazaex for olc
|
||||
""")
|
||||
|
||||
print("[1/3] Creating server peer...")
|
||||
try:
|
||||
server = await create_peer("Server", is_server=True)
|
||||
await asyncio.wait_for(server["dc_pub_open"].wait(), timeout=10.0)
|
||||
print(" :P Server ready")
|
||||
except Exception as e:
|
||||
print(f" X Error: {e}")
|
||||
return
|
||||
|
||||
print("\n[2/3] Creating client peer...")
|
||||
try:
|
||||
client = await create_peer("Client", is_server=False)
|
||||
await asyncio.wait_for(client["dc_pub_open"].wait(), timeout=10.0)
|
||||
print(" :P Client ready")
|
||||
except Exception as e:
|
||||
print(f" X Error: {e}")
|
||||
return
|
||||
|
||||
print("\n[3/3] Starting stream...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
url = "https://raw.githubusercontent.com/openlibrecommunity/olcng/refs/heads/master/olcng.apk"
|
||||
print(f" -> Client requesting: {url}")
|
||||
|
||||
stream_id = None
|
||||
total_size = 0
|
||||
last_received = 0
|
||||
|
||||
client["dc_pub"].send(f"STREAM {url}")
|
||||
|
||||
print(" -> Receiving stream...")
|
||||
|
||||
for i in range(300):
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
for cmd in client["stats"]["commands"]:
|
||||
if cmd.startswith("START "):
|
||||
parts = cmd.split()
|
||||
stream_id = parts[1]
|
||||
total_size = int(parts[2])
|
||||
print(f" -> Stream started, expecting {total_size} bytes ({total_size / 1024 / 1024:.2f} MB)")
|
||||
|
||||
elif cmd.startswith("END "):
|
||||
if stream_id:
|
||||
final_data = client["receiver"].get_data(stream_id)
|
||||
print(f"\n :P Received {len(final_data)} bytes ({len(final_data) / 1024 / 1024:.2f} MB)")
|
||||
|
||||
if len(final_data) < 1024:
|
||||
print("\n--- Stream Content ---")
|
||||
try:
|
||||
print(final_data.decode('utf-8'))
|
||||
except:
|
||||
print(f"[Binary data: {len(final_data)} bytes]")
|
||||
print("--- End ---\n")
|
||||
else:
|
||||
print(f" -> Large file received, skipping content display")
|
||||
|
||||
print("\nCleaning up...")
|
||||
server["ws_task"].cancel()
|
||||
client["ws_task"].cancel()
|
||||
await server["ws"].close()
|
||||
await client["ws"].close()
|
||||
await server["pc_sub"].close()
|
||||
await server["pc_pub"].close()
|
||||
await client["pc_sub"].close()
|
||||
await client["pc_pub"].close()
|
||||
|
||||
print(":P Stream complete")
|
||||
return
|
||||
|
||||
elif cmd.startswith("ERROR"):
|
||||
print(f"\n X {cmd}\n")
|
||||
return
|
||||
|
||||
client["stats"]["commands"].clear()
|
||||
|
||||
if stream_id and total_size > 0:
|
||||
if stream_id in client["receiver"].streams:
|
||||
current_received = len(client["receiver"].streams[stream_id]) * CHUNK_SIZE
|
||||
if current_received > last_received:
|
||||
progress = (current_received / total_size) * 100 if total_size > 0 else 0
|
||||
print(f" -> Receiving: {current_received / 1024 / 1024:.2f} MB / {total_size / 1024 / 1024:.2f} MB ({progress:.1f}%) [packets: {client['stats']['received']}]")
|
||||
last_received = current_received
|
||||
|
||||
print("\n X Timeout waiting for stream")
|
||||
|
||||
async def main():
|
||||
await run_stream()
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nStream interrupted.")
|
||||
144
code/flood.py
144
code/flood.py
@@ -1,144 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
import requests
|
||||
from urllib.parse import quote
|
||||
import websockets
|
||||
|
||||
CONFERENCE_ID = "75047680642749"
|
||||
CONFERENCE_URL = f"https://telemost.yandex.ru/j/{CONFERENCE_ID}"
|
||||
API_BASE = "https://cloud-api.yandex.ru/telemost_front/v2/telemost"
|
||||
|
||||
def generate_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def get_connection_info(display_name):
|
||||
url = f"{API_BASE}/conferences/{quote(CONFERENCE_URL, safe='')}/connection"
|
||||
params = {
|
||||
"next_gen_media_platform_allowed": "true",
|
||||
"display_name": display_name,
|
||||
"waiting_room_supported": "true"
|
||||
}
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0",
|
||||
"Accept": "*/*",
|
||||
"content-type": "application/json",
|
||||
"Client-Instance-Id": generate_uuid(),
|
||||
"X-Telemost-Client-Version": "187.1.0",
|
||||
"idempotency-key": generate_uuid(),
|
||||
"Origin": "https://telemost.yandex.ru",
|
||||
"Referer": "https://telemost.yandex.ru/"
|
||||
}
|
||||
|
||||
response = requests.get(url, params=params, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def connect_peer(name, peer_num):
|
||||
try:
|
||||
conn_info = get_connection_info(name)
|
||||
room_id = conn_info["room_id"]
|
||||
peer_id = conn_info["peer_id"]
|
||||
credentials = conn_info["credentials"]
|
||||
ws_url = conn_info["client_configuration"]["media_server_url"]
|
||||
|
||||
ws = await websockets.connect(ws_url)
|
||||
|
||||
hello_msg = {
|
||||
"uid": generate_uuid(),
|
||||
"hello": {
|
||||
"participantMeta": {
|
||||
"name": name,
|
||||
"role": "SPEAKER",
|
||||
"description": "",
|
||||
"sendAudio": False,
|
||||
"sendVideo": False
|
||||
},
|
||||
"participantAttributes": {
|
||||
"name": name,
|
||||
"role": "SPEAKER",
|
||||
"description": ""
|
||||
},
|
||||
"sendAudio": False,
|
||||
"sendVideo": False,
|
||||
"sendSharing": False,
|
||||
"participantId": peer_id,
|
||||
"roomId": room_id,
|
||||
"serviceName": "telemost",
|
||||
"credentials": credentials,
|
||||
"capabilitiesOffer": {
|
||||
"offerAnswerMode": ["SEPARATE"],
|
||||
"initialSubscriberOffer": ["ON_HELLO"],
|
||||
"slotsMode": ["FROM_CONTROLLER"],
|
||||
"simulcastMode": ["DISABLED"],
|
||||
"selfVadStatus": ["FROM_SERVER"]
|
||||
},
|
||||
"sdkInfo": {
|
||||
"implementation": "python",
|
||||
"version": "1.0.0",
|
||||
"userAgent": "SEXEVEN-Bot"
|
||||
},
|
||||
"sdkInitializationId": generate_uuid(),
|
||||
"disablePublisher": False,
|
||||
"disableSubscriber": False
|
||||
}
|
||||
}
|
||||
|
||||
await ws.send(json.dumps(hello_msg))
|
||||
|
||||
async def keep_alive():
|
||||
while True:
|
||||
try:
|
||||
data = json.loads(await ws.recv())
|
||||
if "serverHello" in data:
|
||||
await ws.send(json.dumps({"uid": data["uid"], "ack": {"status": {"code": "OK"}}}))
|
||||
except:
|
||||
break
|
||||
|
||||
print(f"[:P] {name} connected (#{peer_num})")
|
||||
|
||||
await keep_alive()
|
||||
|
||||
except Exception as e:
|
||||
print(f"[✗] {name} failed: {e}")
|
||||
|
||||
async def main():
|
||||
print(r"""
|
||||
|
||||
SEXEVEN FLOOD стойте пацаны это не флуд это просто мемчик
|
||||
Connecting 40 peers to conference
|
||||
|
||||
""")
|
||||
|
||||
print(f"Target: {CONFERENCE_URL}\n")
|
||||
print("Starting flood...\n")
|
||||
|
||||
tasks = []
|
||||
|
||||
for i in range(1, 412):
|
||||
suffix = "МЕМЫ" * i
|
||||
name = f"СТОЙ ДАВАЙ ДРУЖИТЬ И ШУТИТЬ ПРОСТО Я ЖЕ ХОТЕЛ ПРОСТО А ТЫ ЗАЧЕМ ТО ГОВОРИШЬ СТОЙ ТЧО ЗАЧЕМ {suffix}"
|
||||
|
||||
task = asyncio.create_task(connect_peer(name, i))
|
||||
tasks.append(task)
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
print(f"\n[*] All 40 peers launched!")
|
||||
print(f"[*] Keeping connections alive (Ctrl+C to stop)...\n")
|
||||
|
||||
try:
|
||||
await asyncio.gather(*tasks)
|
||||
except KeyboardInterrupt:
|
||||
print("\n[*] Stopping flood...")
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\nFlood stopped.")
|
||||
565
code/info.py
565
code/info.py
@@ -1,565 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
import requests
|
||||
from urllib.parse import quote
|
||||
import websockets
|
||||
from datetime import datetime
|
||||
|
||||
CONFERENCE_ID = "75047680642749"
|
||||
CONFERENCE_URL = f"https://telemost.yandex.ru/j/{CONFERENCE_ID}"
|
||||
API_BASE = "https://cloud-api.yandex.ru/telemost_front/v2/telemost"
|
||||
|
||||
session = requests.Session()
|
||||
|
||||
def log(msg, level="INFO"):
|
||||
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||
log_msg = f"[{timestamp}] [{level}] {msg}"
|
||||
print(log_msg)
|
||||
|
||||
def generate_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def get_connection_info(display_name):
|
||||
url = f"{API_BASE}/conferences/{quote(CONFERENCE_URL, safe='')}/connection"
|
||||
params = {
|
||||
"next_gen_media_platform_allowed": "true",
|
||||
"display_name": display_name,
|
||||
"waiting_room_supported": "true"
|
||||
}
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0",
|
||||
"Accept": "*/*",
|
||||
"content-type": "application/json",
|
||||
"Client-Instance-Id": generate_uuid(),
|
||||
"X-Telemost-Client-Version": "187.1.0",
|
||||
"idempotency-key": generate_uuid(),
|
||||
"Origin": "https://telemost.yandex.ru",
|
||||
"Referer": "https://telemost.yandex.ru/"
|
||||
}
|
||||
|
||||
log(f"GET {url}", "HTTP")
|
||||
log(f"Params: {params}", "DEBUG")
|
||||
|
||||
response = session.get(url, params=params, headers=headers)
|
||||
response.raise_for_status()
|
||||
|
||||
log(f"Response: {response.status_code}", "HTTP")
|
||||
log(f"Cookies: {list(session.cookies.keys())}", "DEBUG")
|
||||
|
||||
return response.json()
|
||||
|
||||
def get_participants_list(peer_ids=None):
|
||||
url = f"{API_BASE}/conferences/{quote(CONFERENCE_URL, safe='')}/request-states"
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0",
|
||||
"Accept": "*/*",
|
||||
"content-type": "application/json",
|
||||
"Client-Instance-Id": generate_uuid(),
|
||||
"idempotency-key": generate_uuid(),
|
||||
"Origin": "https://telemost.yandex.ru",
|
||||
"Referer": "https://telemost.yandex.ru/"
|
||||
}
|
||||
|
||||
if peer_ids:
|
||||
payload = {
|
||||
"peers": [{"peer_id": pid} for pid in peer_ids],
|
||||
"permissions": {},
|
||||
"conference": {"version": -1}
|
||||
}
|
||||
else:
|
||||
payload = {
|
||||
"peers": [],
|
||||
"permissions": {},
|
||||
"conference": {"version": -1}
|
||||
}
|
||||
|
||||
log(f"POST {url}", "HTTP")
|
||||
log(f"Payload: {json.dumps(payload)[:100]}...", "DEBUG")
|
||||
|
||||
response = session.post(url, json=payload, headers=headers)
|
||||
response.raise_for_status()
|
||||
|
||||
log(f"Response: {response.status_code}", "HTTP")
|
||||
|
||||
result = response.json()
|
||||
log(f"Response body: {json.dumps(result)[:500]}...", "DEBUG")
|
||||
|
||||
return result
|
||||
|
||||
async def collect_webrtc_info():
|
||||
log(r"""
|
||||
|
||||
WebRTC Full Information Collector
|
||||
Complete conference & peer analysis
|
||||
by zowue for olc
|
||||
|
||||
""", "INFO")
|
||||
|
||||
info = {
|
||||
"connection": {},
|
||||
"participants": {},
|
||||
"webrtc": {
|
||||
"ice": {},
|
||||
"sdp": {},
|
||||
"datachannel": {},
|
||||
"audio": {},
|
||||
"video": {}
|
||||
},
|
||||
"server": {}
|
||||
}
|
||||
|
||||
log("[1/5] Getting connection info...")
|
||||
try:
|
||||
conn_info = get_connection_info("InfoCollector")
|
||||
|
||||
cookies_dict = session.cookies.get_dict()
|
||||
if cookies_dict:
|
||||
log(f" :P Cookies: {', '.join(cookies_dict.keys())}", "INFO")
|
||||
|
||||
info["connection"] = {
|
||||
"connection_type": conn_info.get("connection_type"),
|
||||
"uri": conn_info.get("uri"),
|
||||
"room_id": conn_info.get("room_id"),
|
||||
"safe_room_id": conn_info.get("safe_room_id"),
|
||||
"peer_id": conn_info.get("peer_id"),
|
||||
"session_id": conn_info.get("session_id"),
|
||||
"peer_session_id": conn_info.get("peer_session_id"),
|
||||
"expiration_time": conn_info.get("expiration_time"),
|
||||
"conference_limit": conn_info.get("conference_limit"),
|
||||
"media_platform": conn_info.get("media_platform")
|
||||
}
|
||||
|
||||
client_config = conn_info.get("client_configuration", {})
|
||||
info["connection"]["client_config"] = {
|
||||
"media_server_url": client_config.get("media_server_url"),
|
||||
"service_name": client_config.get("service_name"),
|
||||
"session_timeout_ms": client_config.get("goloom_session_open_ms"),
|
||||
"reconnect_wait_ms": client_config.get("wait_time_to_reconnect_ms")
|
||||
}
|
||||
|
||||
log(f" :P Room: {info['connection']['room_id']}")
|
||||
log(f" :P Peer: {info['connection']['peer_id']}")
|
||||
log(f" :P Limit: {info['connection']['conference_limit']}")
|
||||
except Exception as e:
|
||||
log(f" X Error: {e}", "ERROR")
|
||||
return info
|
||||
|
||||
log("\n[2/5] Connecting to WebSocket...")
|
||||
try:
|
||||
ws_url = client_config.get("media_server_url")
|
||||
log(f" -> WS URL: {ws_url}", "DEBUG")
|
||||
ws = await websockets.connect(ws_url)
|
||||
|
||||
hello_msg = {
|
||||
"uid": generate_uuid(),
|
||||
"hello": {
|
||||
"participantMeta": {"name": "InfoCollector", "role": "SPEAKER", "sendAudio": False, "sendVideo": False},
|
||||
"participantAttributes": {"name": "InfoCollector", "role": "SPEAKER"},
|
||||
"sendAudio": False,
|
||||
"sendVideo": False,
|
||||
"sendSharing": False,
|
||||
"participantId": conn_info["peer_id"],
|
||||
"roomId": conn_info["room_id"],
|
||||
"serviceName": "telemost",
|
||||
"credentials": conn_info["credentials"],
|
||||
"capabilitiesOffer": {
|
||||
"offerAnswerMode": ["SEPARATE"],
|
||||
"initialSubscriberOffer": ["ON_HELLO"],
|
||||
"slotsMode": ["FROM_CONTROLLER"],
|
||||
"simulcastMode": ["STATIC"],
|
||||
"selfVadStatus": ["FROM_SERVER"],
|
||||
"dataChannelSharing": ["TO_RTP"]
|
||||
},
|
||||
"sdkInfo": {"implementation": "python", "version": "1.0.0", "userAgent": "InfoCollector"},
|
||||
"sdkInitializationId": generate_uuid(),
|
||||
"disablePublisher": False,
|
||||
"disableSubscriber": False
|
||||
}
|
||||
}
|
||||
|
||||
await ws.send(json.dumps(hello_msg))
|
||||
log(" :P Connected")
|
||||
log(f" -> Sent hello message", "DEBUG")
|
||||
|
||||
log("\n[3/5] Collecting participants...")
|
||||
info["participants"]["list"] = []
|
||||
collected_peer_ids = set()
|
||||
|
||||
log("\n[4/5] Collecting WebRTC details...")
|
||||
|
||||
for i in range(25):
|
||||
try:
|
||||
data = json.loads(await asyncio.wait_for(ws.recv(), timeout=1.0))
|
||||
|
||||
msg_type = next((k for k in data.keys() if k != "uid"), "unknown")
|
||||
log(f" <- WS message #{i+1}: {msg_type}", "WS")
|
||||
|
||||
if "updateDescription" in data:
|
||||
update_desc = data["updateDescription"]
|
||||
log(f" -> updateDescription: {json.dumps(update_desc)[:200]}...", "DEBUG")
|
||||
|
||||
if "description" in update_desc:
|
||||
for desc_item in update_desc["description"]:
|
||||
peer_id = desc_item.get("id")
|
||||
|
||||
if peer_id:
|
||||
log(f" -> Found peer in updateDescription: {peer_id}", "DEBUG")
|
||||
|
||||
if peer_id not in collected_peer_ids:
|
||||
collected_peer_ids.add(peer_id)
|
||||
|
||||
meta = desc_item.get("meta", {})
|
||||
peer_data = {
|
||||
"peer_id": peer_id,
|
||||
"display_name": meta.get("name"),
|
||||
"role": meta.get("role"),
|
||||
"send_audio": meta.get("sendAudio"),
|
||||
"send_video": meta.get("sendVideo"),
|
||||
"send_sharing": desc_item.get("sendSharing", False)
|
||||
}
|
||||
|
||||
info["participants"]["list"].append(peer_data)
|
||||
audio_status = "ON" if peer_data['send_audio'] else "OFF"
|
||||
video_status = "ON" if peer_data['send_video'] else "OFF"
|
||||
log(f" :P {peer_data['display_name']} (A:{audio_status} V:{video_status})")
|
||||
|
||||
await ws.send(json.dumps({"uid": data["uid"], "ack": {"status": {"code": "OK"}}}))
|
||||
|
||||
if "serverHello" in data:
|
||||
server_hello = data["serverHello"]
|
||||
|
||||
if "capabilitiesAnswer" in server_hello:
|
||||
caps = server_hello["capabilitiesAnswer"]
|
||||
info["webrtc"]["capabilities"] = {
|
||||
"offerAnswerMode": caps.get("offerAnswerMode"),
|
||||
"initialSubscriberOffer": caps.get("initialSubscriberOffer"),
|
||||
"slotsMode": caps.get("slotsMode"),
|
||||
"simulcastMode": caps.get("simulcastMode"),
|
||||
"selfVadStatus": caps.get("selfVadStatus"),
|
||||
"dataChannelSharing": caps.get("dataChannelSharing"),
|
||||
"videoEncoderConfig": caps.get("videoEncoderConfig"),
|
||||
"dataChannelVideoCodec": caps.get("dataChannelVideoCodec"),
|
||||
"bandwidthLimitationReason": caps.get("bandwidthLimitationReason"),
|
||||
"publisherVp9": caps.get("publisherVp9"),
|
||||
"svcMode": caps.get("svcMode")
|
||||
}
|
||||
|
||||
if "servingComponents" in server_hello:
|
||||
info["server"]["components"] = []
|
||||
for comp in server_hello["servingComponents"]:
|
||||
info["server"]["components"].append({
|
||||
"type": comp.get("type"),
|
||||
"host": comp.get("host"),
|
||||
"version": comp.get("version")
|
||||
})
|
||||
|
||||
if "rtcConfiguration" in server_hello:
|
||||
rtc_config = server_hello["rtcConfiguration"]
|
||||
info["webrtc"]["ice"]["servers"] = []
|
||||
|
||||
for server in rtc_config.get("iceServers", []):
|
||||
info["webrtc"]["ice"]["servers"].append({
|
||||
"urls": server.get("urls"),
|
||||
"has_credentials": bool(server.get("credential"))
|
||||
})
|
||||
|
||||
if "pingPongConfiguration" in server_hello:
|
||||
ping_config = server_hello["pingPongConfiguration"]
|
||||
info["webrtc"]["ping"] = {
|
||||
"interval_ms": ping_config.get("pingInterval"),
|
||||
"ack_timeout_ms": ping_config.get("ackTimeout")
|
||||
}
|
||||
|
||||
if "telemetryConfiguration" in server_hello:
|
||||
telem_config = server_hello["telemetryConfiguration"]
|
||||
info["webrtc"]["telemetry"] = {
|
||||
"sending_interval_ms": telem_config.get("sendingInterval")
|
||||
}
|
||||
|
||||
await ws.send(json.dumps({"uid": data["uid"], "ack": {"status": {"code": "OK"}}}))
|
||||
log(" :P Server hello")
|
||||
|
||||
if "subscriberSdpOffer" in data:
|
||||
sdp = data["subscriberSdpOffer"]["sdp"]
|
||||
|
||||
info["webrtc"]["sdp"]["raw"] = sdp
|
||||
info["webrtc"]["sdp"]["lines"] = sdp.count('\r\n')
|
||||
|
||||
info["webrtc"]["audio"]["codecs"] = []
|
||||
info["webrtc"]["video"]["codecs"] = []
|
||||
info["webrtc"]["datachannel"]["supported"] = False
|
||||
|
||||
for line in sdp.split('\r\n'):
|
||||
if line.startswith('a=rtpmap:') and 'm=audio' in sdp[:sdp.find(line)]:
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
codec_info = parts[1].split('/')
|
||||
info["webrtc"]["audio"]["codecs"].append({
|
||||
"name": codec_info[0],
|
||||
"rate": int(codec_info[1]) if len(codec_info) > 1 else None,
|
||||
"channels": int(codec_info[2]) if len(codec_info) > 2 else None
|
||||
})
|
||||
|
||||
if line.startswith('a=rtpmap:') and 'm=video' in sdp[:sdp.find(line)]:
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
codec_info = parts[1].split('/')
|
||||
codec_name = codec_info[0].upper()
|
||||
if codec_name not in ['RTX', 'RED', 'ULPFEC']:
|
||||
if codec_name not in [c["name"] for c in info["webrtc"]["video"]["codecs"]]:
|
||||
info["webrtc"]["video"]["codecs"].append({
|
||||
"name": codec_name,
|
||||
"rate": int(codec_info[1]) if len(codec_info) > 1 else None
|
||||
})
|
||||
|
||||
if 'm=application' in line and 'SCTP' in line:
|
||||
info["webrtc"]["datachannel"]["supported"] = True
|
||||
|
||||
if 'sctp-port:' in line:
|
||||
info["webrtc"]["datachannel"]["sctp_port"] = int(line.split(':')[1].strip())
|
||||
|
||||
if 'max-message-size:' in line:
|
||||
size = int(line.split(':')[1].strip())
|
||||
info["webrtc"]["datachannel"]["max_message_size"] = size
|
||||
info["webrtc"]["datachannel"]["max_message_size_mb"] = size / 1024 / 1024
|
||||
|
||||
if line.startswith('a=fmtp:') and 'opus' in sdp[max(0, sdp.find(line)-200):sdp.find(line)].lower():
|
||||
params = line.split(':', 1)[1].strip().split(';')
|
||||
info["webrtc"]["audio"]["opus_params"] = {}
|
||||
for param in params:
|
||||
if '=' in param:
|
||||
key, val = param.strip().split('=')
|
||||
info["webrtc"]["audio"]["opus_params"][key] = val
|
||||
|
||||
if line.startswith('a=extmap:'):
|
||||
if "rtp_extensions" not in info["webrtc"]:
|
||||
info["webrtc"]["rtp_extensions"] = []
|
||||
ext_info = line.split()[1]
|
||||
info["webrtc"]["rtp_extensions"].append(ext_info)
|
||||
|
||||
await ws.send(json.dumps({"uid": data["uid"], "ack": {"status": {"code": "OK"}}}))
|
||||
log(" :P SDP analyzed")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
log(f" -> Timeout on message #{i+1}, continuing...", "DEBUG")
|
||||
break
|
||||
except Exception as e:
|
||||
log(f" X Error processing message: {e}", "ERROR")
|
||||
break
|
||||
|
||||
info["participants"]["count"] = len(info["participants"]["list"])
|
||||
|
||||
await ws.close()
|
||||
log(" :P Closed")
|
||||
|
||||
log("\n -> Fetching all participants via API...")
|
||||
try:
|
||||
all_participants = get_participants_list()
|
||||
|
||||
log(f" -> API returned: peers={len(all_participants.get('peers', []))}, permissions={bool(all_participants.get('permissions'))}", "DEBUG")
|
||||
|
||||
if "peers" in all_participants and all_participants["peers"]:
|
||||
log(f" :P Found {len(all_participants['peers'])} participants via API")
|
||||
|
||||
for peer in all_participants["peers"]:
|
||||
peer_id = peer.get("peer_id")
|
||||
if peer_id and peer_id not in collected_peer_ids:
|
||||
peer_data = {
|
||||
"peer_id": peer_id,
|
||||
"peer_type": peer.get("peer_type"),
|
||||
"send_audio": None,
|
||||
"send_video": None,
|
||||
"send_sharing": None
|
||||
}
|
||||
|
||||
if "state" in peer and "user_data" in peer["state"]:
|
||||
user_data = peer["state"]["user_data"]
|
||||
peer_data["display_name"] = user_data.get("display_name")
|
||||
peer_data["role"] = user_data.get("role")
|
||||
peer_data["uid"] = user_data.get("uid")
|
||||
if "avatar_placeholder" in user_data:
|
||||
peer_data["avatar"] = user_data["avatar_placeholder"]
|
||||
|
||||
info["participants"]["list"].append(peer_data)
|
||||
collected_peer_ids.add(peer_id)
|
||||
log(f" :P {peer_data.get('display_name', 'Unknown')} ({peer_data.get('role', 'N/A')})")
|
||||
|
||||
info["participants"]["count"] = len(info["participants"]["list"])
|
||||
else:
|
||||
log(f" X No peers in API response", "WARN")
|
||||
|
||||
if "permissions" in all_participants:
|
||||
info["conference"] = {"permissions": all_participants["permissions"]}
|
||||
if "conference" in all_participants:
|
||||
info["conference"]["state"] = all_participants["conference"].get("state")
|
||||
|
||||
except Exception as e:
|
||||
log(f" X API request failed: {e}", "ERROR")
|
||||
|
||||
log(f"\n :P Total participants: {info['participants']['count']}")
|
||||
|
||||
except Exception as e:
|
||||
log(f" X Error: {e}", "ERROR")
|
||||
|
||||
return info
|
||||
|
||||
def print_full_report(info):
|
||||
print("CONNECTION INFO")
|
||||
conn = info["connection"]
|
||||
print(f"Type: {conn.get('connection_type')}")
|
||||
print(f"URI: {conn.get('uri')}")
|
||||
print(f"Room ID: {conn.get('room_id')}")
|
||||
print(f"Peer ID: {conn.get('peer_id')}")
|
||||
print(f"Session ID: {conn.get('session_id')}")
|
||||
print(f"Media Platform: {conn.get('media_platform')}")
|
||||
print(f"Max Participants: {conn.get('conference_limit')}")
|
||||
|
||||
if "client_config" in conn:
|
||||
cfg = conn["client_config"]
|
||||
print(f"\nClient Configuration:")
|
||||
print(f" Media Server: {cfg.get('media_server_url')}")
|
||||
print(f" Session Timeout: {cfg.get('session_timeout_ms')}ms ({cfg.get('session_timeout_ms', 0)/1000/60:.1f}min)")
|
||||
print(f" Reconnect Wait: {cfg.get('reconnect_wait_ms')}ms")
|
||||
|
||||
print("PARTICIPANTS")
|
||||
if "list" in info["participants"] and info["participants"]["list"]:
|
||||
for i, peer in enumerate(info["participants"]["list"], 1):
|
||||
print(f"\n{i}. {peer.get('display_name', 'Unknown')}")
|
||||
print(f" Peer ID: {peer.get('peer_id')}")
|
||||
print(f" Role: {peer.get('role')}")
|
||||
if peer.get('uid'):
|
||||
print(f" UID: {peer.get('uid')}")
|
||||
print(f" Audio: {peer.get('send_audio')}")
|
||||
print(f" Video: {peer.get('send_video')}")
|
||||
print(f" Sharing: {peer.get('send_sharing')}")
|
||||
if "avatar" in peer:
|
||||
avatar = peer["avatar"]
|
||||
print(f" Avatar: {avatar.get('abbreviation')} ({avatar.get('background_color')})")
|
||||
print(f"\nTotal: {info['participants'].get('count', 0)} participants")
|
||||
else:
|
||||
print("No participants in conference")
|
||||
|
||||
if "conference" in info and "permissions" in info["conference"]:
|
||||
print("\n" + "=" * 70)
|
||||
print("CONFERENCE PERMISSIONS")
|
||||
print("=" * 70)
|
||||
perms = info["conference"]["permissions"]
|
||||
print(f"Version: {perms.get('version')}")
|
||||
|
||||
if "public_role_permissions" in perms:
|
||||
print("\nRole Permissions:")
|
||||
for role_perm in perms["public_role_permissions"]:
|
||||
role = role_perm.get("role")
|
||||
allowed = role_perm.get("allowed", [])
|
||||
print(f" {role}: {', '.join(allowed)}")
|
||||
|
||||
if "personal_allowed" in perms:
|
||||
print(f"\nPersonal: {', '.join(perms['personal_allowed'])}")
|
||||
|
||||
if "state" in info["conference"]:
|
||||
state = info["conference"]["state"]
|
||||
print("\nConference State:")
|
||||
print(f" Access Level: {state.get('access_level')}")
|
||||
print(f" Recording: {state.get('local_recording_allowed')}")
|
||||
print(f" Cloud Recording: {state.get('cloud_recording_allowed')}")
|
||||
print(f" Chat: {state.get('chat_allowed')}")
|
||||
print(f" Control: {state.get('control_allowed')}")
|
||||
print(f" Broadcast: {state.get('broadcast_allowed')}")
|
||||
|
||||
print("WEBRTC CAPABILITIES")
|
||||
if "capabilities" in info["webrtc"]:
|
||||
caps = info["webrtc"]["capabilities"]
|
||||
print(f"Offer/Answer Mode: {caps.get('offerAnswerMode')}")
|
||||
print(f"Initial Subscriber Offer: {caps.get('initialSubscriberOffer')}")
|
||||
print(f"Slots Mode: {caps.get('slotsMode')}")
|
||||
print(f"Simulcast Mode: {caps.get('simulcastMode')}")
|
||||
print(f"VAD Status: {caps.get('selfVadStatus')}")
|
||||
print(f"DataChannel Sharing: {caps.get('dataChannelSharing')}")
|
||||
print(f"Video Encoder Config: {caps.get('videoEncoderConfig')}")
|
||||
print(f"DC Video Codec: {caps.get('dataChannelVideoCodec')}")
|
||||
print(f"Bandwidth Limitation: {caps.get('bandwidthLimitationReason')}")
|
||||
print(f"Publisher VP9: {caps.get('publisherVp9')}")
|
||||
print(f"SVC Mode: {caps.get('svcMode')}")
|
||||
|
||||
print("AUDIO")
|
||||
if "codecs" in info["webrtc"]["audio"]:
|
||||
print("Codecs:")
|
||||
for codec in info["webrtc"]["audio"]["codecs"]:
|
||||
print(f" - {codec['name']}: {codec.get('rate', 'N/A')}Hz, {codec.get('channels', 'N/A')} channels")
|
||||
|
||||
if "opus_params" in info["webrtc"]["audio"]:
|
||||
print("\nOpus Parameters:")
|
||||
for key, val in info["webrtc"]["audio"]["opus_params"].items():
|
||||
print(f" {key}: {val}")
|
||||
|
||||
print("VIDEO")
|
||||
if "codecs" in info["webrtc"]["video"]:
|
||||
print("Codecs:")
|
||||
for codec in info["webrtc"]["video"]["codecs"]:
|
||||
print(f" - {codec['name']}: {codec.get('rate', 'N/A')}Hz")
|
||||
|
||||
print("DATACHANNEL")
|
||||
dc = info["webrtc"]["datachannel"]
|
||||
print(f"Supported: {':P YES' if dc.get('supported') else 'X NO'}")
|
||||
if dc.get("supported"):
|
||||
print(f"SCTP Port: {dc.get('sctp_port')}")
|
||||
print(f"Max Message Size: {dc.get('max_message_size_mb', 0):.0f}MB ({dc.get('max_message_size', 0):,} bytes)")
|
||||
print(f"Note: Actual limit is 8KB due to server fragmentation")
|
||||
|
||||
print("ICE/NETWORK")
|
||||
if "servers" in info["webrtc"]["ice"]:
|
||||
stun_count = sum(1 for s in info["webrtc"]["ice"]["servers"] if any('stun:' in u for u in s.get("urls", [])))
|
||||
turn_count = sum(1 for s in info["webrtc"]["ice"]["servers"] if any('turn:' in u for u in s.get("urls", [])))
|
||||
|
||||
print(f"STUN Servers: {stun_count}")
|
||||
print(f"TURN Servers: {turn_count}")
|
||||
print("\nServers:")
|
||||
for server in info["webrtc"]["ice"]["servers"]:
|
||||
for url in server.get("urls", []):
|
||||
cred_status = "with credentials" if server.get("has_credentials") else "no credentials"
|
||||
print(f" - {url} ({cred_status})")
|
||||
|
||||
if "ping" in info["webrtc"]:
|
||||
ping = info["webrtc"]["ping"]
|
||||
print(f"\nPing Configuration:")
|
||||
print(f" Interval: {ping.get('interval_ms')}ms")
|
||||
print(f" ACK Timeout: {ping.get('ack_timeout_ms')}ms")
|
||||
|
||||
if "rtp_extensions" in info["webrtc"]:
|
||||
print(f"\nRTP Extensions: {len(info['webrtc']['rtp_extensions'])}")
|
||||
for ext in info["webrtc"]["rtp_extensions"][:5]:
|
||||
print(f" - {ext}")
|
||||
|
||||
print("SERVER COMPONENTS")
|
||||
if "components" in info["server"]:
|
||||
for comp in info["server"]["components"]:
|
||||
print(f"{comp.get('type'):20} {comp.get('host'):30} v{comp.get('version')}")
|
||||
|
||||
if "telemetry" in info["webrtc"]:
|
||||
telem = info["webrtc"]["telemetry"]
|
||||
print(f"\nTelemetry:")
|
||||
print(f" Sending Interval: {telem.get('sending_interval_ms')}ms")
|
||||
|
||||
print("SDP STATISTICS")
|
||||
if "lines" in info["webrtc"]["sdp"]:
|
||||
print(f"Total SDP Lines: {info['webrtc']['sdp']['lines']}")
|
||||
print(f"SDP Size: {len(info['webrtc']['sdp'].get('raw', ''))} bytes")
|
||||
|
||||
async def main():
|
||||
log(f"Starting WebRTC info collection...")
|
||||
|
||||
info = await collect_webrtc_info()
|
||||
|
||||
log("\n[5/5] Generating report...\n")
|
||||
print_full_report(info)
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
log("\n\nInfo collection interrupted.", "WARN")
|
||||
@@ -1,592 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
import websockets
|
||||
import requests
|
||||
import qrcode
|
||||
import cv2
|
||||
import numpy as np
|
||||
import base64
|
||||
import os
|
||||
import time
|
||||
from urllib.parse import quote
|
||||
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
|
||||
from aiortc import RTCPeerConnection, RTCSessionDescription, RTCIceCandidate, RTCConfiguration, RTCIceServer
|
||||
from aiortc.mediastreams import MediaStreamTrack
|
||||
from av import VideoFrame
|
||||
from PIL import Image
|
||||
from pyzbar import pyzbar
|
||||
from fractions import Fraction
|
||||
|
||||
CONFERENCE_ID = "75047680642749"
|
||||
CONFERENCE_URL = f"https://telemost.yandex.ru/j/{CONFERENCE_ID}"
|
||||
API_BASE = "https://cloud-api.yandex.ru/telemost_front/v2/telemost"
|
||||
|
||||
QR_SIZE = 600
|
||||
CHUNK_SIZE = 400
|
||||
FRAME_RATE = 1
|
||||
SHARED_KEY = os.urandom(32)
|
||||
|
||||
def gen_uid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def get_connection_info(display_name):
|
||||
url = f"{API_BASE}/conferences/{quote(CONFERENCE_URL, safe='')}/connection"
|
||||
params = {
|
||||
"next_gen_media_platform_allowed": "true",
|
||||
"display_name": display_name,
|
||||
"waiting_room_supported": "true"
|
||||
}
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0",
|
||||
"Accept": "*/*",
|
||||
"content-type": "application/json",
|
||||
"Client-Instance-Id": gen_uid(),
|
||||
"X-Telemost-Client-Version": "187.1.0",
|
||||
"idempotency-key": gen_uid(),
|
||||
"Origin": "https://telemost.yandex.ru",
|
||||
"Referer": "https://telemost.yandex.ru/"
|
||||
}
|
||||
r = requests.get(url, params=params, headers=headers)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def encrypt_payload(tag_str, data_bytes):
|
||||
nonce = os.urandom(12)
|
||||
chacha = ChaCha20Poly1305(SHARED_KEY)
|
||||
ciphertext = chacha.encrypt(nonce, data_bytes, None)
|
||||
blob = nonce + ciphertext
|
||||
tag_bytes = tag_str.encode('ascii').ljust(4, b'\x00')[:4]
|
||||
len_bytes = len(blob).to_bytes(4, 'big')
|
||||
return tag_bytes + len_bytes + blob
|
||||
|
||||
def decrypt_payload(envelope):
|
||||
tag = envelope[:4].decode('ascii').strip('\x00')
|
||||
length = int.from_bytes(envelope[4:8], 'big')
|
||||
blob = envelope[8:8+length]
|
||||
nonce = blob[:12]
|
||||
ciphertext = blob[12:]
|
||||
chacha = ChaCha20Poly1305(SHARED_KEY)
|
||||
data = chacha.decrypt(nonce, ciphertext, None)
|
||||
return tag, data
|
||||
|
||||
def make_qr_frame(data, pts):
|
||||
qr = qrcode.QRCode(
|
||||
version=None,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_M,
|
||||
box_size=12,
|
||||
border=4
|
||||
)
|
||||
qr.add_data(data)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color="black", back_color="white").resize(
|
||||
(QR_SIZE, QR_SIZE), Image.NEAREST
|
||||
)
|
||||
arr = np.array(img.convert('RGB'))
|
||||
frame = VideoFrame.from_ndarray(arr, format="rgb24")
|
||||
frame.pts = pts
|
||||
frame.time_base = Fraction(1, FRAME_RATE)
|
||||
return frame
|
||||
|
||||
def chunk_data(data, tid):
|
||||
b64 = base64.b64encode(data).decode()
|
||||
n = (len(b64) + CHUNK_SIZE - 1) // CHUNK_SIZE
|
||||
return [json.dumps({"tid": tid, "idx": i, "total": n,
|
||||
"data": b64[i * CHUNK_SIZE:(i + 1) * CHUNK_SIZE]})
|
||||
for i in range(n)]
|
||||
|
||||
class QRVideoTrack(MediaStreamTrack):
|
||||
kind = "video"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._frames = []
|
||||
self._idx = 0
|
||||
self._pts = 0
|
||||
|
||||
def set_data(self, chunks):
|
||||
self._frames = [make_qr_frame(c, i) for i, c in enumerate(chunks)]
|
||||
self._idx = 0
|
||||
self._pts = 0
|
||||
|
||||
async def recv(self):
|
||||
await asyncio.sleep(1.0 / FRAME_RATE)
|
||||
if not self._frames:
|
||||
f = make_qr_frame("WAIT", self._pts)
|
||||
self._pts += 1
|
||||
return f
|
||||
f = self._frames[self._idx]
|
||||
f.pts = self._pts
|
||||
f.time_base = Fraction(1, FRAME_RATE)
|
||||
self._pts += 1
|
||||
self._idx = (self._idx + 1) % len(self._frames)
|
||||
return f
|
||||
|
||||
class DualReceiver:
|
||||
def __init__(self):
|
||||
self._bufs = {}
|
||||
self.vc_result = None
|
||||
self.dc_result = None
|
||||
self._cv2_detector = cv2.QRCodeDetector()
|
||||
|
||||
def feed_frame(self, frame):
|
||||
if self.vc_result is not None:
|
||||
return False
|
||||
|
||||
try:
|
||||
arr = frame.to_ndarray(format="rgb24")
|
||||
h, w = arr.shape[:2]
|
||||
gray = cv2.cvtColor(arr, cv2.COLOR_RGB2GRAY)
|
||||
|
||||
variants = [
|
||||
gray,
|
||||
cv2.resize(gray, (w * 2, h * 2), interpolation=cv2.INTER_CUBIC),
|
||||
cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1],
|
||||
]
|
||||
|
||||
decoded = set()
|
||||
for v in variants:
|
||||
try:
|
||||
for code in pyzbar.decode(v):
|
||||
decoded.add(code.data.decode('utf-8'))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
val, _, _ = self._cv2_detector.detectAndDecode(v)
|
||||
if val:
|
||||
decoded.add(val)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for raw in decoded:
|
||||
try:
|
||||
pkt = json.loads(raw)
|
||||
tid = pkt["tid"]
|
||||
idx = pkt["idx"]
|
||||
total = pkt["total"]
|
||||
data = pkt["data"]
|
||||
if tid not in self._bufs:
|
||||
self._bufs[tid] = {}
|
||||
if idx not in self._bufs[tid]:
|
||||
self._bufs[tid][idx] = data
|
||||
if len(self._bufs[tid]) == total:
|
||||
b64 = "".join(self._bufs[tid][i] for i in range(total))
|
||||
self.vc_result = base64.b64decode(b64)
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
async def process_track(track, receiver):
|
||||
while True:
|
||||
try:
|
||||
frame = await asyncio.wait_for(track.recv(), timeout=30.0)
|
||||
if receiver.feed_frame(frame):
|
||||
return
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def make_ice_servers(raw_list):
|
||||
result = []
|
||||
for s in raw_list:
|
||||
urls = s.get("urls", [])
|
||||
cred = s.get("credential", "")
|
||||
user = s.get("username", "")
|
||||
if cred:
|
||||
result.append(RTCIceServer(urls=urls, credential=cred, username=user))
|
||||
else:
|
||||
result.append(RTCIceServer(urls=urls))
|
||||
return result or [RTCIceServer(urls=["stun:stun.rtc.yandex.net:3478"])]
|
||||
|
||||
async def connect_peer(name, conn):
|
||||
room_id = conn["room_id"]
|
||||
peer_id = conn["peer_id"]
|
||||
credentials = conn["credentials"]
|
||||
ws_url = conn["client_configuration"]["media_server_url"]
|
||||
is_sender = "Sender" in name
|
||||
|
||||
default_ice = [RTCIceServer(urls=["stun:stun.rtc.yandex.net:3478"])]
|
||||
|
||||
video_track = QRVideoTrack() if is_sender else None
|
||||
receiver_obj = DualReceiver() if not is_sender else None
|
||||
track_tasks = []
|
||||
|
||||
pc_sub_ref = [RTCPeerConnection(RTCConfiguration(iceServers=default_ice))]
|
||||
pc_pub_ref = [RTCPeerConnection(RTCConfiguration(iceServers=default_ice))]
|
||||
|
||||
dc_pub_ref = []
|
||||
dc_open_event = asyncio.Event()
|
||||
|
||||
if is_sender:
|
||||
pc_pub_ref[0].addTrack(video_track)
|
||||
dc = pc_pub_ref[0].createDataChannel("invisible", ordered=True)
|
||||
dc_pub_ref.append(dc)
|
||||
|
||||
@dc.on("open")
|
||||
def on_open():
|
||||
dc_open_event.set()
|
||||
|
||||
ws = await websockets.connect(
|
||||
ws_url,
|
||||
additional_headers={
|
||||
"Origin": "https://telemost.yandex.ru",
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0"
|
||||
}
|
||||
)
|
||||
|
||||
async def send(obj):
|
||||
await ws.send(json.dumps(obj))
|
||||
|
||||
async def ack(uid):
|
||||
await send({"uid": uid, "ack": {"status": {"code": "OK", "description": ""}}})
|
||||
|
||||
def setup_pc(pc_sub, pc_pub):
|
||||
if not is_sender:
|
||||
@pc_sub.on("datachannel")
|
||||
def on_datachannel(channel):
|
||||
@channel.on("message")
|
||||
def on_message(message):
|
||||
if receiver_obj is not None:
|
||||
receiver_obj.dc_result = message
|
||||
|
||||
@pc_sub.on("track")
|
||||
def on_track(track):
|
||||
if track.kind == "video" and receiver_obj is not None:
|
||||
t = asyncio.ensure_future(process_track(track, receiver_obj))
|
||||
track_tasks.append(t)
|
||||
|
||||
@pc_sub.on("icecandidate")
|
||||
async def on_sub_ice(e):
|
||||
if e.candidate:
|
||||
await send({
|
||||
"uid": gen_uid(),
|
||||
"webrtcIceCandidate": {
|
||||
"candidate": e.candidate.candidate,
|
||||
"sdpMid": e.candidate.sdpMid,
|
||||
"sdpMlineIndex": e.candidate.sdpMLineIndex,
|
||||
"usernameFragment": "",
|
||||
"target": "SUBSCRIBER",
|
||||
"pcSeq": 1
|
||||
}
|
||||
})
|
||||
|
||||
@pc_pub.on("icecandidate")
|
||||
async def on_pub_ice(e):
|
||||
if e.candidate:
|
||||
await send({
|
||||
"uid": gen_uid(),
|
||||
"webrtcIceCandidate": {
|
||||
"candidate": e.candidate.candidate,
|
||||
"sdpMid": e.candidate.sdpMid,
|
||||
"sdpMlineIndex": e.candidate.sdpMLineIndex,
|
||||
"usernameFragment": "",
|
||||
"target": "PUBLISHER",
|
||||
"pcSeq": 1
|
||||
}
|
||||
})
|
||||
|
||||
setup_pc(pc_sub_ref[0], pc_pub_ref[0])
|
||||
|
||||
hello = {
|
||||
"uid": gen_uid(),
|
||||
"hello": {
|
||||
"participantMeta": {
|
||||
"name": name, "role": "SPEAKER", "description": "",
|
||||
"sendAudio": False, "sendVideo": is_sender
|
||||
},
|
||||
"participantAttributes": {"name": name, "role": "SPEAKER", "description": ""},
|
||||
"sendAudio": False,
|
||||
"sendVideo": is_sender,
|
||||
"sendSharing": False,
|
||||
"participantId": peer_id,
|
||||
"roomId": room_id,
|
||||
"serviceName": "telemost",
|
||||
"credentials": credentials,
|
||||
"capabilitiesOffer": {
|
||||
"offerAnswerMode": ["SEPARATE"],
|
||||
"initialSubscriberOffer": ["ON_HELLO"],
|
||||
"slotsMode": ["FROM_CONTROLLER"],
|
||||
"simulcastMode": ["DISABLED", "STATIC"],
|
||||
"selfVadStatus": ["FROM_SERVER", "FROM_CLIENT"],
|
||||
"dataChannelSharing": ["TO_RTP"],
|
||||
"videoEncoderConfig": ["NO_CONFIG", "ONLY_INIT_CONFIG", "RUNTIME_CONFIG"],
|
||||
"dataChannelVideoCodec": ["VP8", "UNIQUE_CODEC_FROM_TRACK_DESCRIPTION"]
|
||||
},
|
||||
"sdkInfo": {
|
||||
"implementation": "browser",
|
||||
"version": "5.27.0",
|
||||
"userAgent": "Mozilla/5.0",
|
||||
"hwConcurrency": 24
|
||||
},
|
||||
"sdkInitializationId": gen_uid(),
|
||||
"disablePublisher": not is_sender,
|
||||
"disableSubscriber": False,
|
||||
"disableSubscriberAudio": True
|
||||
}
|
||||
}
|
||||
|
||||
await send(hello)
|
||||
pub_sdp_sent = False
|
||||
|
||||
async def ws_loop():
|
||||
nonlocal pub_sdp_sent
|
||||
try:
|
||||
async for raw in ws:
|
||||
msg = json.loads(raw)
|
||||
keys = [k for k in msg if k != "uid"]
|
||||
if not keys:
|
||||
continue
|
||||
mtype = keys[0]
|
||||
uid = msg.get("uid", "")
|
||||
|
||||
if mtype == "ack":
|
||||
pass
|
||||
|
||||
elif mtype == "serverHello":
|
||||
sh = msg["serverHello"]
|
||||
raw_ice = sh.get("rtcConfiguration", {}).get("iceServers", [])
|
||||
if raw_ice:
|
||||
ice = make_ice_servers(raw_ice)
|
||||
old_sub = pc_sub_ref[0]
|
||||
old_pub = pc_pub_ref[0]
|
||||
pc_sub_ref[0] = RTCPeerConnection(RTCConfiguration(iceServers=ice))
|
||||
pc_pub_ref[0] = RTCPeerConnection(RTCConfiguration(iceServers=ice))
|
||||
|
||||
if is_sender:
|
||||
pc_pub_ref[0].addTrack(video_track)
|
||||
dc = pc_pub_ref[0].createDataChannel("invisible", ordered=True)
|
||||
dc_pub_ref.clear()
|
||||
dc_pub_ref.append(dc)
|
||||
@dc.on("open")
|
||||
def on_open():
|
||||
dc_open_event.set()
|
||||
|
||||
setup_pc(pc_sub_ref[0], pc_pub_ref[0])
|
||||
await old_sub.close()
|
||||
await old_pub.close()
|
||||
await ack(uid)
|
||||
|
||||
elif mtype == "subscriberSdpOffer":
|
||||
offer_sdp = msg["subscriberSdpOffer"]["sdp"]
|
||||
pc_seq = msg["subscriberSdpOffer"]["pcSeq"]
|
||||
pc_sub = pc_sub_ref[0]
|
||||
pc_pub = pc_pub_ref[0]
|
||||
|
||||
await pc_sub.setRemoteDescription(
|
||||
RTCSessionDescription(sdp=offer_sdp, type="offer")
|
||||
)
|
||||
answer = await pc_sub.createAnswer()
|
||||
await pc_sub.setLocalDescription(answer)
|
||||
|
||||
await send({
|
||||
"uid": gen_uid(),
|
||||
"subscriberSdpAnswer": {
|
||||
"pcSeq": pc_seq,
|
||||
"sdp": pc_sub.localDescription.sdp
|
||||
}
|
||||
})
|
||||
await ack(uid)
|
||||
|
||||
if not is_sender:
|
||||
await send({
|
||||
"uid": gen_uid(),
|
||||
"setSlots": {
|
||||
"slots": [{"width": 1280, "height": 720}],
|
||||
"audioSlotsCount": 0,
|
||||
"key": 1,
|
||||
"shutdownAllVideo": None,
|
||||
"withSelfView": False,
|
||||
"selfViewVisibility": "ON_LOADING_THEN_SHOW",
|
||||
"gridConfig": {}
|
||||
}
|
||||
})
|
||||
|
||||
if is_sender and not pub_sdp_sent:
|
||||
await asyncio.sleep(0.3)
|
||||
pub_offer = await pc_pub.createOffer()
|
||||
await pc_pub.setLocalDescription(pub_offer)
|
||||
tracks_info = []
|
||||
for t in pc_pub.getTransceivers():
|
||||
if t.sender.track:
|
||||
tracks_info.append({
|
||||
"mid": t.mid,
|
||||
"transceiverMid": t.mid,
|
||||
"kind": t.sender.track.kind.upper(),
|
||||
"priority": 0,
|
||||
"label": "QRVideoTrack",
|
||||
"codecs": {},
|
||||
"groupId": 1,
|
||||
"description": ""
|
||||
})
|
||||
await send({
|
||||
"uid": gen_uid(),
|
||||
"publisherSdpOffer": {
|
||||
"pcSeq": 1,
|
||||
"sdp": pc_pub.localDescription.sdp,
|
||||
"tracks": tracks_info
|
||||
}
|
||||
})
|
||||
pub_sdp_sent = True
|
||||
|
||||
elif mtype == "publisherSdpAnswer":
|
||||
await pc_pub_ref[0].setRemoteDescription(
|
||||
RTCSessionDescription(sdp=msg["publisherSdpAnswer"]["sdp"], type="answer")
|
||||
)
|
||||
await ack(uid)
|
||||
|
||||
elif mtype == "webrtcIceCandidate":
|
||||
cand = msg["webrtcIceCandidate"]
|
||||
parts = cand.get("candidate", "").split()
|
||||
if len(parts) >= 8:
|
||||
try:
|
||||
ice_c = RTCIceCandidate(
|
||||
component=int(parts[1]),
|
||||
foundation=parts[0].replace("candidate:", ""),
|
||||
ip=parts[4],
|
||||
port=int(parts[5]),
|
||||
priority=int(parts[3]),
|
||||
protocol=parts[2].lower(),
|
||||
type=parts[7],
|
||||
sdpMid=cand.get("sdpMid", "0"),
|
||||
sdpMLineIndex=cand.get("sdpMlineIndex", 0)
|
||||
)
|
||||
if cand.get("target") == "SUBSCRIBER":
|
||||
await pc_sub_ref[0].addIceCandidate(ice_c)
|
||||
elif cand.get("target") == "PUBLISHER":
|
||||
await pc_pub_ref[0].addIceCandidate(ice_c)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
elif mtype in ("setSlots", "slotsConfig", "slotsMeta", "vadActivity",
|
||||
"updateDescription", "upsertDescription", "sdkCodecsInfo",
|
||||
"pingPong", "selfQualityReport", "upsertParticipantsQualityReport",
|
||||
"removeDescription"):
|
||||
await ack(uid)
|
||||
else:
|
||||
if uid:
|
||||
await ack(uid)
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ws_task = asyncio.create_task(ws_loop())
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"ws": ws,
|
||||
"ws_task": ws_task,
|
||||
"pc_pub_ref": pc_pub_ref,
|
||||
"pc_sub_ref": pc_sub_ref,
|
||||
"video_track": video_track,
|
||||
"receiver": receiver_obj,
|
||||
"track_tasks": track_tasks,
|
||||
"dc_pub_ref": dc_pub_ref,
|
||||
"dc_open_event": dc_open_event
|
||||
}
|
||||
|
||||
async def run():
|
||||
print("ChaCha20-Poly1305 over Telemost DC + VC")
|
||||
print("text + video encrypted transfer")
|
||||
print(" by zarazaex for olc\n")
|
||||
|
||||
sender_conn = get_connection_info("QR_Sender")
|
||||
receiver_conn = get_connection_info("QR_Receiver")
|
||||
|
||||
print("[1/4] Generating payloads...")
|
||||
text_data = "do you want to pet my pu... cat".encode('utf-8')
|
||||
video_data = os.urandom(2048)
|
||||
|
||||
print(f"-> Text payload: {len(text_data)} bytes")
|
||||
print(f"-> Video payload: {len(video_data)} bytes\n")
|
||||
|
||||
print("[2/4] Creating sender peer...")
|
||||
sender = await connect_peer("QR_Sender", sender_conn)
|
||||
await sender["dc_open_event"].wait()
|
||||
print(":P Sender ready\n")
|
||||
|
||||
print("[3/4] Creating receiver peer...")
|
||||
receiver = await connect_peer("QR_Receiver", receiver_conn)
|
||||
await asyncio.sleep(5)
|
||||
print(":P Receiver ready\n")
|
||||
|
||||
print("[4/4] Encrypting and sending...\n")
|
||||
|
||||
enc_text = encrypt_payload("TEXT", text_data)
|
||||
print(f"[TEXT] Original data ({len(text_data)} bytes):")
|
||||
print(f" UTF-8: {text_data.decode('utf-8')}")
|
||||
print(f" HEX: {text_data.hex()}")
|
||||
print(f"[TEXT] Encrypted envelope ({len(enc_text)} bytes):")
|
||||
print(f" Tag: {enc_text[:4].hex().upper()}")
|
||||
print(f" Len: {int.from_bytes(enc_text[4:8], 'big')}")
|
||||
print(f" Blob: {enc_text[8:72].hex()}...\n")
|
||||
|
||||
print("-> Sending TEXT...")
|
||||
sender["dc_pub_ref"][0].send(enc_text)
|
||||
print(":P Text sent\n")
|
||||
|
||||
enc_video = encrypt_payload("VID\x00", video_data)
|
||||
print(f"[VIDEO] Original data ({len(video_data)} bytes):")
|
||||
print(f" HEX: {video_data[:64].hex()}...")
|
||||
print(f"[VIDEO] Encrypted envelope ({len(enc_video)} bytes):")
|
||||
print(f" Tag: {enc_video[:4].hex().upper()}")
|
||||
print(f" Len: {int.from_bytes(enc_video[4:8], 'big')}")
|
||||
print(f" Blob: {enc_video[8:72].hex()}...\n")
|
||||
|
||||
print("-> Sending VIDEO...")
|
||||
tid = gen_uid()
|
||||
chunks = chunk_data(enc_video, tid)
|
||||
sender["video_track"].set_data(chunks)
|
||||
print(":P Video sent\n")
|
||||
|
||||
print("-> Waiting for receiver...")
|
||||
|
||||
for i in range(120):
|
||||
await asyncio.sleep(1)
|
||||
if receiver["receiver"].dc_result is not None and receiver["receiver"].vc_result is not None:
|
||||
break
|
||||
|
||||
dc_res = receiver["receiver"].dc_result
|
||||
vc_res = receiver["receiver"].vc_result
|
||||
|
||||
if dc_res:
|
||||
print(f"[Receiver] <- received 'TEXT': {len(dc_res)} bytes")
|
||||
if vc_res:
|
||||
print(f"[Receiver] <- received 'VID': {len(vc_res)} bytes")
|
||||
|
||||
print("\n--- Received & Decrypted ---")
|
||||
|
||||
if dc_res:
|
||||
tag, dec_text = decrypt_payload(dc_res)
|
||||
print(f"[TEXT] Decrypted ({len(dec_text)} bytes):")
|
||||
print(f"UTF-8: {dec_text.decode('utf-8')}")
|
||||
print(f"HEX: {dec_text.hex()}")
|
||||
|
||||
if vc_res:
|
||||
tag, dec_vid = decrypt_payload(vc_res)
|
||||
print(f"[VIDEO] Decrypted ({len(dec_vid)} bytes):")
|
||||
print(f"HEX: {dec_vid[:64].hex()}...")
|
||||
|
||||
print("\nCleaning up...")
|
||||
for p in [sender, receiver]:
|
||||
p["ws_task"].cancel()
|
||||
try:
|
||||
await p["ws"].close()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
await p["pc_pub_ref"][0].close()
|
||||
await p["pc_sub_ref"][0].close()
|
||||
except Exception:
|
||||
pass
|
||||
print(":P Done")
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(run())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
1054
code/limits.py
1054
code/limits.py
File diff suppressed because it is too large
Load Diff
573
code/olcrtc.py
573
code/olcrtc.py
@@ -1,573 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# ===========================================
|
||||
# AI GENERATED / AI GENERATED / AI GENERATED
|
||||
# ===========================================
|
||||
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
import struct
|
||||
import socket
|
||||
import logging
|
||||
from urllib.parse import quote
|
||||
import websockets
|
||||
import requests
|
||||
from aiortc import RTCPeerConnection, RTCSessionDescription, RTCIceCandidate, RTCConfiguration, RTCIceServer
|
||||
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
|
||||
import os
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='[%(levelname)s] %(message)s')
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
logging.getLogger('aiortc').setLevel(logging.ERROR)
|
||||
logging.getLogger('aioice').setLevel(logging.ERROR)
|
||||
logging.getLogger('av').setLevel(logging.ERROR)
|
||||
|
||||
API_BASE = "https://cloud-api.yandex.ru/telemost_front/v2/telemost"
|
||||
CHUNK_SIZE = 7168
|
||||
BUFFER_THRESHOLD = 16384
|
||||
|
||||
def gen_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def get_connection_info(room_url, display_name):
|
||||
url = f"{API_BASE}/conferences/{quote(room_url, safe='')}/connection"
|
||||
params = {
|
||||
"next_gen_media_platform_allowed": "true",
|
||||
"display_name": display_name,
|
||||
"waiting_room_supported": "true"
|
||||
}
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0",
|
||||
"Accept": "*/*",
|
||||
"content-type": "application/json",
|
||||
"Client-Instance-Id": gen_uuid(),
|
||||
"X-Telemost-Client-Version": "187.1.0",
|
||||
"idempotency-key": gen_uuid(),
|
||||
"Origin": "https://telemost.yandex.ru",
|
||||
"Referer": "https://telemost.yandex.ru/"
|
||||
}
|
||||
r = requests.get(url, params=params, headers=headers)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
class Crypto:
|
||||
def __init__(self, key):
|
||||
self.cipher = ChaCha20Poly1305(key)
|
||||
|
||||
def encrypt(self, data):
|
||||
nonce = os.urandom(12)
|
||||
ct = self.cipher.encrypt(nonce, data, None)
|
||||
return nonce + ct
|
||||
|
||||
def decrypt(self, blob):
|
||||
nonce = blob[:12]
|
||||
ct = blob[12:]
|
||||
return self.cipher.decrypt(nonce, ct, None)
|
||||
|
||||
class Multiplexer:
|
||||
def __init__(self, on_send):
|
||||
self.streams = {}
|
||||
self.next_id = 1
|
||||
self.on_send = on_send
|
||||
|
||||
def open_stream(self):
|
||||
sid = self.next_id
|
||||
self.next_id += 1
|
||||
self.streams[sid] = {
|
||||
"recv_buf": b"",
|
||||
"send_queue": asyncio.Queue(),
|
||||
"closed": False
|
||||
}
|
||||
return sid
|
||||
|
||||
def close_stream(self, sid):
|
||||
if sid in self.streams:
|
||||
self.streams[sid]["closed"] = True
|
||||
|
||||
async def send_data(self, sid, data):
|
||||
if sid not in self.streams or self.streams[sid]["closed"]:
|
||||
return
|
||||
|
||||
log.debug(f"MUX send sid={sid} len={len(data)}")
|
||||
for i in range(0, len(data), CHUNK_SIZE):
|
||||
chunk = data[i:i+CHUNK_SIZE]
|
||||
frame = struct.pack("!HH", sid, len(chunk)) + chunk
|
||||
await self.on_send(frame)
|
||||
|
||||
async def send_close(self, sid):
|
||||
frame = struct.pack("!HH", sid, 0)
|
||||
await self.on_send(frame)
|
||||
self.close_stream(sid)
|
||||
|
||||
def handle_frame(self, frame):
|
||||
if len(frame) < 4:
|
||||
log.warning(f"MUX frame too short: {len(frame)}b")
|
||||
return
|
||||
|
||||
sid, length = struct.unpack("!HH", frame[:4])
|
||||
|
||||
if length == 0:
|
||||
log.debug(f"MUX close sid={sid}")
|
||||
self.close_stream(sid)
|
||||
return
|
||||
|
||||
data = frame[4:4+length]
|
||||
|
||||
if sid not in self.streams:
|
||||
log.warning(f"MUX recv sid={sid} not found, opening it")
|
||||
self.streams[sid] = {
|
||||
"recv_buf": b"",
|
||||
"send_queue": asyncio.Queue(),
|
||||
"closed": False
|
||||
}
|
||||
|
||||
self.streams[sid]["recv_buf"] += data
|
||||
log.debug(f"MUX recv sid={sid} len={len(data)} total_buf={len(self.streams[sid]['recv_buf'])}")
|
||||
|
||||
def read_stream(self, sid, max_n=None):
|
||||
if sid not in self.streams:
|
||||
return b""
|
||||
|
||||
buf = self.streams[sid]["recv_buf"]
|
||||
if not buf:
|
||||
return b""
|
||||
|
||||
if max_n is None:
|
||||
result = buf
|
||||
self.streams[sid]["recv_buf"] = b""
|
||||
else:
|
||||
result = buf[:max_n]
|
||||
self.streams[sid]["recv_buf"] = buf[max_n:]
|
||||
|
||||
return result
|
||||
|
||||
def stream_closed(self, sid):
|
||||
return sid not in self.streams or self.streams[sid]["closed"]
|
||||
|
||||
class RTCPeer:
|
||||
def __init__(self, room_url, name, crypto):
|
||||
self.room_url = room_url
|
||||
self.name = name
|
||||
self.crypto = crypto
|
||||
self.dc = None
|
||||
self.dc_ready = asyncio.Event()
|
||||
self.mux = None
|
||||
|
||||
async def connect(self):
|
||||
conn = get_connection_info(self.room_url, self.name)
|
||||
room_id = conn["room_id"]
|
||||
peer_id = conn["peer_id"]
|
||||
credentials = conn["credentials"]
|
||||
ws_url = conn["client_configuration"]["media_server_url"]
|
||||
|
||||
pc_sub = RTCPeerConnection(RTCConfiguration(
|
||||
iceServers=[RTCIceServer(urls=["stun:stun.rtc.yandex.net:3478"])]
|
||||
))
|
||||
pc_pub = RTCPeerConnection(RTCConfiguration(
|
||||
iceServers=[RTCIceServer(urls=["stun:stun.rtc.yandex.net:3478"])]
|
||||
))
|
||||
|
||||
self.dc = pc_pub.createDataChannel("olcrtc", ordered=True)
|
||||
|
||||
@self.dc.on("open")
|
||||
def on_open():
|
||||
self.dc_ready.set()
|
||||
|
||||
@self.dc.on("message")
|
||||
def on_msg(msg):
|
||||
if isinstance(msg, bytes):
|
||||
try:
|
||||
plain = self.crypto.decrypt(msg)
|
||||
self.mux.handle_frame(plain)
|
||||
log.debug(f"DC received {len(msg)}b encrypted, {len(plain)}b plain")
|
||||
except Exception as e:
|
||||
log.error(f"DC decrypt error: {e}")
|
||||
|
||||
@pc_sub.on("datachannel")
|
||||
def on_dc(ch):
|
||||
log.info(f"Received datachannel: {ch.label}")
|
||||
@ch.on("message")
|
||||
def on_message(msg):
|
||||
if isinstance(msg, bytes):
|
||||
try:
|
||||
plain = self.crypto.decrypt(msg)
|
||||
self.mux.handle_frame(plain)
|
||||
log.debug(f"SUB DC received {len(msg)}b encrypted, {len(plain)}b plain")
|
||||
except Exception as e:
|
||||
log.error(f"SUB DC decrypt error: {e}")
|
||||
|
||||
ws = await websockets.connect(ws_url)
|
||||
|
||||
hello = {
|
||||
"uid": gen_uuid(),
|
||||
"hello": {
|
||||
"participantMeta": {"name": self.name, "role": "SPEAKER", "sendAudio": False, "sendVideo": False},
|
||||
"participantAttributes": {"name": self.name, "role": "SPEAKER"},
|
||||
"sendAudio": False,
|
||||
"sendVideo": False,
|
||||
"sendSharing": False,
|
||||
"participantId": peer_id,
|
||||
"roomId": room_id,
|
||||
"serviceName": "telemost",
|
||||
"credentials": 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-{self.name}"},
|
||||
"sdkInitializationId": gen_uuid(),
|
||||
"disablePublisher": False,
|
||||
"disableSubscriber": False
|
||||
}
|
||||
}
|
||||
|
||||
await ws.send(json.dumps(hello))
|
||||
|
||||
pub_sent = False
|
||||
|
||||
async def ws_loop():
|
||||
nonlocal pub_sent
|
||||
while True:
|
||||
try:
|
||||
msg = json.loads(await ws.recv())
|
||||
|
||||
if "serverHello" in msg:
|
||||
await ws.send(json.dumps({"uid": msg["uid"], "ack": {"status": {"code": "OK"}}}))
|
||||
|
||||
if "subscriberSdpOffer" in msg and not pub_sent:
|
||||
await pc_sub.setRemoteDescription(RTCSessionDescription(
|
||||
sdp=msg["subscriberSdpOffer"]["sdp"], type="offer"
|
||||
))
|
||||
ans = await pc_sub.createAnswer()
|
||||
await pc_sub.setLocalDescription(ans)
|
||||
await ws.send(json.dumps({
|
||||
"uid": gen_uuid(),
|
||||
"subscriberSdpAnswer": {
|
||||
"pcSeq": msg["subscriberSdpOffer"]["pcSeq"],
|
||||
"sdp": pc_sub.localDescription.sdp
|
||||
}
|
||||
}))
|
||||
await ws.send(json.dumps({"uid": msg["uid"], "ack": {"status": {"code": "OK"}}}))
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
offer = await pc_pub.createOffer()
|
||||
await pc_pub.setLocalDescription(offer)
|
||||
await ws.send(json.dumps({
|
||||
"uid": gen_uuid(),
|
||||
"publisherSdpOffer": {
|
||||
"pcSeq": 1,
|
||||
"sdp": pc_pub.localDescription.sdp
|
||||
}
|
||||
}))
|
||||
pub_sent = True
|
||||
|
||||
if "publisherSdpAnswer" in msg:
|
||||
await pc_pub.setRemoteDescription(RTCSessionDescription(
|
||||
sdp=msg["publisherSdpAnswer"]["sdp"], type="answer"
|
||||
))
|
||||
await ws.send(json.dumps({"uid": msg["uid"], "ack": {"status": {"code": "OK"}}}))
|
||||
|
||||
if "webrtcIceCandidate" in msg:
|
||||
cand = msg["webrtcIceCandidate"]
|
||||
try:
|
||||
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"]
|
||||
)
|
||||
if cand.get("target") == "SUBSCRIBER":
|
||||
await pc_sub.addIceCandidate(ice)
|
||||
elif cand.get("target") == "PUBLISHER":
|
||||
await pc_pub.addIceCandidate(ice)
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
break
|
||||
|
||||
@pc_sub.on("icecandidate")
|
||||
async def on_sub_ice(e):
|
||||
if e.candidate:
|
||||
await ws.send(json.dumps({
|
||||
"uid": gen_uuid(),
|
||||
"webrtcIceCandidate": {
|
||||
"candidate": e.candidate.candidate,
|
||||
"sdpMid": e.candidate.sdpMid,
|
||||
"sdpMlineIndex": e.candidate.sdpMLineIndex,
|
||||
"target": "SUBSCRIBER",
|
||||
"pcSeq": 1
|
||||
}
|
||||
}))
|
||||
|
||||
@pc_pub.on("icecandidate")
|
||||
async def on_pub_ice(e):
|
||||
if e.candidate:
|
||||
await ws.send(json.dumps({
|
||||
"uid": gen_uuid(),
|
||||
"webrtcIceCandidate": {
|
||||
"candidate": e.candidate.candidate,
|
||||
"sdpMid": e.candidate.sdpMid,
|
||||
"sdpMlineIndex": e.candidate.sdpMLineIndex,
|
||||
"target": "PUBLISHER",
|
||||
"pcSeq": 1
|
||||
}
|
||||
}))
|
||||
|
||||
asyncio.create_task(ws_loop())
|
||||
|
||||
async def send_encrypted(data):
|
||||
enc = self.crypto.encrypt(data)
|
||||
while self.dc.bufferedAmount > BUFFER_THRESHOLD:
|
||||
await asyncio.sleep(0.001)
|
||||
self.dc.send(enc)
|
||||
log.debug(f"DC sent {len(data)}b plain, {len(enc)}b encrypted")
|
||||
|
||||
self.mux = Multiplexer(send_encrypted)
|
||||
|
||||
await asyncio.wait_for(self.dc_ready.wait(), timeout=15.0)
|
||||
|
||||
class SOCKS5Server:
|
||||
def __init__(self, peer, host="127.0.0.1", port=1080):
|
||||
self.peer = peer
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
async def handle_client(self, reader, writer):
|
||||
sid = None
|
||||
try:
|
||||
ver = await reader.readexactly(1)
|
||||
if ver[0] != 5:
|
||||
writer.close()
|
||||
return
|
||||
|
||||
nmethods = await reader.readexactly(1)
|
||||
await reader.readexactly(nmethods[0])
|
||||
|
||||
writer.write(b"\x05\x00")
|
||||
await writer.drain()
|
||||
|
||||
req = await reader.readexactly(4)
|
||||
if req[1] != 1:
|
||||
writer.write(b"\x05\x07\x00\x01\x00\x00\x00\x00\x00\x00")
|
||||
await writer.drain()
|
||||
writer.close()
|
||||
return
|
||||
|
||||
atyp = req[3]
|
||||
if atyp == 1:
|
||||
addr = socket.inet_ntoa(await reader.readexactly(4))
|
||||
elif atyp == 3:
|
||||
length = (await reader.readexactly(1))[0]
|
||||
addr = (await reader.readexactly(length)).decode()
|
||||
else:
|
||||
writer.write(b"\x05\x08\x00\x01\x00\x00\x00\x00\x00\x00")
|
||||
await writer.drain()
|
||||
writer.close()
|
||||
return
|
||||
|
||||
port_bytes = await reader.readexactly(2)
|
||||
port = struct.unpack("!H", port_bytes)[0]
|
||||
|
||||
sid = self.peer.mux.open_stream()
|
||||
log.info(f"SOCKS5 connect sid={sid} {addr}:{port}")
|
||||
|
||||
connect_req = json.dumps({"cmd": "connect", "addr": addr, "port": port}).encode()
|
||||
await self.peer.mux.send_data(sid, connect_req)
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
writer.write(b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00")
|
||||
await writer.drain()
|
||||
|
||||
async def client_to_stream():
|
||||
try:
|
||||
while True:
|
||||
data = await reader.read(4096)
|
||||
if not data:
|
||||
break
|
||||
log.debug(f"SOCKS5 sid={sid} client->stream {len(data)}b")
|
||||
await self.peer.mux.send_data(sid, data)
|
||||
await self.peer.mux.send_close(sid)
|
||||
log.debug(f"SOCKS5 sid={sid} client closed")
|
||||
except Exception as e:
|
||||
log.error(f"SOCKS5 sid={sid} client_to_stream error: {e}")
|
||||
|
||||
async def stream_to_client():
|
||||
try:
|
||||
while not self.peer.mux.stream_closed(sid):
|
||||
await asyncio.sleep(0.01)
|
||||
data = self.peer.mux.read_stream(sid)
|
||||
if data:
|
||||
log.debug(f"SOCKS5 sid={sid} stream->client {len(data)}b")
|
||||
writer.write(data)
|
||||
await writer.drain()
|
||||
log.debug(f"SOCKS5 sid={sid} stream closed")
|
||||
except Exception as e:
|
||||
log.error(f"SOCKS5 sid={sid} stream_to_client error: {e}")
|
||||
|
||||
await asyncio.gather(client_to_stream(), stream_to_client())
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"SOCKS5 sid={sid} error: {e}")
|
||||
finally:
|
||||
try:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
except:
|
||||
pass
|
||||
|
||||
async def run(self):
|
||||
server = await asyncio.start_server(self.handle_client, self.host, self.port)
|
||||
print(f"SOCKS5 proxy listening on {self.host}:{self.port}")
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
|
||||
class ProxyServer:
|
||||
def __init__(self, peer):
|
||||
self.peer = peer
|
||||
self.connections = {}
|
||||
|
||||
async def handle_stream(self, sid, req):
|
||||
try:
|
||||
cmd = req.get("cmd")
|
||||
if cmd == "connect":
|
||||
addr = req["addr"]
|
||||
port = req["port"]
|
||||
|
||||
log.info(f"SERVER connect sid={sid} {addr}:{port}")
|
||||
|
||||
try:
|
||||
r, w = await asyncio.open_connection(addr, port)
|
||||
self.connections[sid] = (r, w)
|
||||
log.info(f"SERVER sid={sid} connected")
|
||||
|
||||
async def remote_to_stream():
|
||||
try:
|
||||
while True:
|
||||
data = await r.read(4096)
|
||||
if not data:
|
||||
break
|
||||
log.debug(f"SERVER sid={sid} remote->stream {len(data)}b")
|
||||
await self.peer.mux.send_data(sid, data)
|
||||
await self.peer.mux.send_close(sid)
|
||||
log.debug(f"SERVER sid={sid} remote closed")
|
||||
except Exception as e:
|
||||
log.error(f"SERVER sid={sid} remote_to_stream error: {e}")
|
||||
|
||||
asyncio.create_task(remote_to_stream())
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"SERVER sid={sid} connect failed: {e}")
|
||||
await self.peer.mux.send_close(sid)
|
||||
except Exception as e:
|
||||
log.error(f"SERVER sid={sid} handle_stream error: {e}")
|
||||
|
||||
async def run(self):
|
||||
log.info("SERVER proxy loop started")
|
||||
while True:
|
||||
await asyncio.sleep(0.01)
|
||||
for sid in list(self.peer.mux.streams.keys()):
|
||||
data = self.peer.mux.read_stream(sid)
|
||||
if data:
|
||||
if sid in self.connections:
|
||||
r, w = self.connections[sid]
|
||||
try:
|
||||
log.debug(f"SERVER sid={sid} stream->remote {len(data)}b")
|
||||
w.write(data)
|
||||
await w.drain()
|
||||
except Exception as e:
|
||||
log.error(f"SERVER sid={sid} write error: {e}")
|
||||
await self.peer.mux.send_close(sid)
|
||||
else:
|
||||
try:
|
||||
req = json.loads(data.decode())
|
||||
await self.handle_stream(sid, req)
|
||||
except Exception as e:
|
||||
log.error(f"SERVER sid={sid} parse error: {e}")
|
||||
|
||||
if self.peer.mux.stream_closed(sid) and sid in self.connections:
|
||||
log.debug(f"SERVER sid={sid} cleanup")
|
||||
r, w = self.connections[sid]
|
||||
try:
|
||||
w.close()
|
||||
await w.wait_closed()
|
||||
except:
|
||||
pass
|
||||
del self.connections[sid]
|
||||
|
||||
async def run_server(room_url, key):
|
||||
crypto = Crypto(key)
|
||||
peer = RTCPeer(room_url, "OlcRTC-Server", crypto)
|
||||
|
||||
log.info("Connecting to Telemost...")
|
||||
await peer.connect()
|
||||
log.info("Connected to Telemost")
|
||||
|
||||
proxy = ProxyServer(peer)
|
||||
await proxy.run()
|
||||
|
||||
async def run_client(room_url, key, socks_port):
|
||||
crypto = Crypto(key)
|
||||
peer = RTCPeer(room_url, "OlcRTC-Client", crypto)
|
||||
|
||||
log.info("Connecting to Telemost...")
|
||||
await peer.connect()
|
||||
log.info("Connected to Telemost")
|
||||
|
||||
socks = SOCKS5Server(peer, port=socks_port)
|
||||
await socks.run()
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="OlcRTC - SOCKS5 over WebRTC DataChannel")
|
||||
parser.add_argument("--srv", action="store_true", help="Run as server")
|
||||
parser.add_argument("--cnc", action="store_true", help="Run as client")
|
||||
parser.add_argument("--id", required=True, help="Telemost room ID")
|
||||
parser.add_argument("--provider", default="telemost", help="Provider (telemost only)")
|
||||
parser.add_argument("--socks-port", type=int, default=1080, help="SOCKS5 port (client only)")
|
||||
parser.add_argument("--key", help="Shared encryption key (hex)")
|
||||
parser.add_argument("--debug", action="store_true", help="Enable debug logging")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.debug:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
if args.provider != "telemost":
|
||||
log.error("Only telemost provider supported in MVP")
|
||||
return
|
||||
|
||||
room_url = f"https://telemost.yandex.ru/j/{args.id}"
|
||||
|
||||
if args.key:
|
||||
key = bytes.fromhex(args.key)
|
||||
else:
|
||||
key = os.urandom(32)
|
||||
log.info(f"Generated key: {key.hex()}")
|
||||
|
||||
if args.srv:
|
||||
log.info(f"Starting server mode, room: {args.id}")
|
||||
asyncio.run(run_server(room_url, key))
|
||||
elif args.cnc:
|
||||
log.info(f"Starting client mode, room: {args.id}, SOCKS5 port: {args.socks_port}")
|
||||
asyncio.run(run_client(room_url, key, args.socks_port))
|
||||
else:
|
||||
log.error("Specify --srv or --cnc")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
625
code/vcsend.py
625
code/vcsend.py
@@ -1,625 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
import websockets
|
||||
import requests
|
||||
import qrcode
|
||||
import cv2
|
||||
import numpy as np
|
||||
from urllib.parse import quote
|
||||
from aiortc import RTCPeerConnection, RTCSessionDescription, RTCIceCandidate, RTCConfiguration, RTCIceServer
|
||||
from aiortc.mediastreams import MediaStreamTrack
|
||||
from av import VideoFrame
|
||||
import base64
|
||||
from PIL import Image
|
||||
from pyzbar import pyzbar
|
||||
from fractions import Fraction
|
||||
|
||||
CONFERENCE_ID = "75047680642749"
|
||||
CONFERENCE_URL = f"https://telemost.yandex.ru/j/{CONFERENCE_ID}"
|
||||
API_BASE = "https://cloud-api.yandex.ru/telemost_front/v2/telemost"
|
||||
|
||||
QR_SIZE = 600
|
||||
CHUNK_SIZE = 400
|
||||
FRAME_RATE = 1
|
||||
|
||||
|
||||
def gen_uid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def get_connection_info(display_name):
|
||||
url = f"{API_BASE}/conferences/{quote(CONFERENCE_URL, safe='')}/connection"
|
||||
params = {
|
||||
"next_gen_media_platform_allowed": "true",
|
||||
"display_name": display_name,
|
||||
"waiting_room_supported": "true"
|
||||
}
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0",
|
||||
"Accept": "*/*",
|
||||
"content-type": "application/json",
|
||||
"Client-Instance-Id": gen_uid(),
|
||||
"X-Telemost-Client-Version": "187.1.0",
|
||||
"idempotency-key": gen_uid(),
|
||||
"Origin": "https://telemost.yandex.ru",
|
||||
"Referer": "https://telemost.yandex.ru/"
|
||||
}
|
||||
r = requests.get(url, params=params, headers=headers)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def make_qr_frame(data, pts):
|
||||
qr = qrcode.QRCode(
|
||||
version=None,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_M,
|
||||
box_size=12,
|
||||
border=4
|
||||
)
|
||||
qr.add_data(data)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color="black", back_color="white").resize(
|
||||
(QR_SIZE, QR_SIZE), Image.NEAREST
|
||||
)
|
||||
arr = np.array(img.convert('RGB'))
|
||||
frame = VideoFrame.from_ndarray(arr, format="rgb24")
|
||||
frame.pts = pts
|
||||
frame.time_base = Fraction(1, FRAME_RATE)
|
||||
return frame
|
||||
|
||||
|
||||
def chunk_data(data, tid):
|
||||
b64 = base64.b64encode(data).decode()
|
||||
n = (len(b64) + CHUNK_SIZE - 1) // CHUNK_SIZE
|
||||
return [json.dumps({"tid": tid, "idx": i, "total": n,
|
||||
"data": b64[i * CHUNK_SIZE:(i + 1) * CHUNK_SIZE]})
|
||||
for i in range(n)]
|
||||
|
||||
|
||||
class QRVideoTrack(MediaStreamTrack):
|
||||
kind = "video"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._frames = []
|
||||
self._idx = 0
|
||||
self._pts = 0
|
||||
|
||||
def set_data(self, chunks):
|
||||
self._frames = [make_qr_frame(c, i) for i, c in enumerate(chunks)]
|
||||
self._idx = 0
|
||||
self._pts = 0
|
||||
print(f" -> QRVideoTrack: {len(self._frames)} frames ready")
|
||||
|
||||
async def recv(self):
|
||||
await asyncio.sleep(1.0 / FRAME_RATE)
|
||||
if not self._frames:
|
||||
f = make_qr_frame("WAIT", self._pts)
|
||||
self._pts += 1
|
||||
return f
|
||||
f = self._frames[self._idx]
|
||||
f.pts = self._pts
|
||||
f.time_base = Fraction(1, FRAME_RATE)
|
||||
self._pts += 1
|
||||
self._idx = (self._idx + 1) % len(self._frames)
|
||||
if self._pts % 10 == 0:
|
||||
print(f" -> QR sending frame {self._idx}/{len(self._frames)} pts={self._pts}")
|
||||
return f
|
||||
|
||||
|
||||
class QRReceiver:
|
||||
def __init__(self):
|
||||
self._bufs = {}
|
||||
self.result = None
|
||||
self._frame_count = 0
|
||||
self._cv2_detector = cv2.QRCodeDetector()
|
||||
|
||||
def feed_frame(self, frame):
|
||||
try:
|
||||
self._frame_count += 1
|
||||
arr = frame.to_ndarray(format="rgb24")
|
||||
h, w = arr.shape[:2]
|
||||
|
||||
if self._frame_count <= 3:
|
||||
cv2.imwrite(f"/tmp/qr_recv_{self._frame_count}.png",
|
||||
cv2.cvtColor(arr, cv2.COLOR_RGB2BGR))
|
||||
print(f" -> [recv] saved /tmp/qr_recv_{self._frame_count}.png {w}x{h}")
|
||||
|
||||
gray = cv2.cvtColor(arr, cv2.COLOR_RGB2GRAY)
|
||||
|
||||
variants = [gray]
|
||||
up2 = cv2.resize(gray, (w * 2, h * 2), interpolation=cv2.INTER_CUBIC)
|
||||
variants.append(up2)
|
||||
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
||||
variants.append(thresh)
|
||||
variants.append(cv2.resize(thresh, (w * 2, h * 2), interpolation=cv2.INTER_NEAREST))
|
||||
|
||||
decoded = set()
|
||||
for v in variants:
|
||||
try:
|
||||
for code in pyzbar.decode(v):
|
||||
decoded.add(code.data.decode('utf-8'))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
val, _, _ = self._cv2_detector.detectAndDecode(v)
|
||||
if val:
|
||||
decoded.add(val)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for raw in decoded:
|
||||
try:
|
||||
pkt = json.loads(raw)
|
||||
tid = pkt["tid"]
|
||||
idx = pkt["idx"]
|
||||
total = pkt["total"]
|
||||
data = pkt["data"]
|
||||
if tid not in self._bufs:
|
||||
self._bufs[tid] = {}
|
||||
if idx not in self._bufs[tid]:
|
||||
self._bufs[tid][idx] = data
|
||||
print(f" -> QR chunk {idx+1}/{total} tid={tid[:8]}")
|
||||
if len(self._bufs[tid]) == total:
|
||||
b64 = "".join(self._bufs[tid][i] for i in range(total))
|
||||
self.result = base64.b64decode(b64)
|
||||
print(f" -> QR COMPLETE: {len(self.result)} bytes")
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self._frame_count % 30 == 0:
|
||||
print(f" -> [recv] {self._frame_count} frames, no QR, size={w}x{h}")
|
||||
|
||||
except Exception as e:
|
||||
print(f" -> [recv] feed_frame err: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def process_track(track, receiver, name):
|
||||
print(f" -> [{name}] video processor started")
|
||||
count = 0
|
||||
while True:
|
||||
try:
|
||||
frame = await asyncio.wait_for(track.recv(), timeout=30.0)
|
||||
count += 1
|
||||
if count <= 5 or count % 50 == 0:
|
||||
print(f" -> [{name}] frame #{count} {frame.width}x{frame.height}")
|
||||
if receiver.feed_frame(frame):
|
||||
return
|
||||
except asyncio.TimeoutError:
|
||||
print(f" -> [{name}] track frozen after {count} frames")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f" -> [{name}] track err: {e}")
|
||||
return
|
||||
|
||||
|
||||
def make_ice_servers(raw_list):
|
||||
result = []
|
||||
for s in raw_list:
|
||||
urls = s.get("urls", [])
|
||||
cred = s.get("credential", "")
|
||||
user = s.get("username", "")
|
||||
if cred:
|
||||
result.append(RTCIceServer(urls=urls, credential=cred, username=user))
|
||||
else:
|
||||
result.append(RTCIceServer(urls=urls))
|
||||
return result or [RTCIceServer(urls=["stun:stun.rtc.yandex.net:3478"])]
|
||||
|
||||
|
||||
async def connect_peer(name, conn):
|
||||
room_id = conn["room_id"]
|
||||
peer_id = conn["peer_id"]
|
||||
credentials = conn["credentials"]
|
||||
ws_url = conn["client_configuration"]["media_server_url"]
|
||||
is_sender = "Sender" in name
|
||||
|
||||
print(f"\n -> [{name}] room={room_id} peer={peer_id[:8]} sender={is_sender}")
|
||||
|
||||
default_ice = [RTCIceServer(urls=["stun:stun.rtc.yandex.net:3478"])]
|
||||
|
||||
video_track = QRVideoTrack() if is_sender else None
|
||||
receiver_obj = QRReceiver() if not is_sender else None
|
||||
track_tasks = []
|
||||
|
||||
# используем списки чтобы можно было переприсваивать в замыканиях
|
||||
pc_sub_ref = [RTCPeerConnection(RTCConfiguration(iceServers=default_ice))]
|
||||
pc_pub_ref = [RTCPeerConnection(RTCConfiguration(iceServers=default_ice))]
|
||||
|
||||
if is_sender:
|
||||
pc_pub_ref[0].addTrack(video_track)
|
||||
|
||||
ws = await websockets.connect(
|
||||
ws_url,
|
||||
additional_headers={
|
||||
"Origin": "https://telemost.yandex.ru",
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0"
|
||||
}
|
||||
)
|
||||
print(f" -> [{name}] WS connected")
|
||||
|
||||
async def send(obj):
|
||||
await ws.send(json.dumps(obj))
|
||||
|
||||
async def ack(uid):
|
||||
await send({"uid": uid, "ack": {"status": {"code": "OK", "description": ""}}})
|
||||
|
||||
def setup_pc(pc_sub, pc_pub):
|
||||
@pc_sub.on("track")
|
||||
def on_track(track):
|
||||
print(f" -> [{name}] GOT TRACK kind={track.kind}")
|
||||
if track.kind == "video" and receiver_obj is not None:
|
||||
t = asyncio.ensure_future(process_track(track, receiver_obj, name))
|
||||
track_tasks.append(t)
|
||||
|
||||
@pc_sub.on("connectionstatechange")
|
||||
async def _s():
|
||||
print(f" -> [{name}] sub={pc_sub.connectionState}")
|
||||
|
||||
@pc_pub.on("connectionstatechange")
|
||||
async def _p():
|
||||
print(f" -> [{name}] pub={pc_pub.connectionState}")
|
||||
|
||||
@pc_sub.on("iceconnectionstatechange")
|
||||
async def _si():
|
||||
print(f" -> [{name}] sub ICE={pc_sub.iceConnectionState}")
|
||||
|
||||
@pc_pub.on("iceconnectionstatechange")
|
||||
async def _pi():
|
||||
print(f" -> [{name}] pub ICE={pc_pub.iceConnectionState}")
|
||||
|
||||
@pc_sub.on("icecandidate")
|
||||
async def on_sub_ice(e):
|
||||
if e.candidate:
|
||||
await send({
|
||||
"uid": gen_uid(),
|
||||
"webrtcIceCandidate": {
|
||||
"candidate": e.candidate.candidate,
|
||||
"sdpMid": e.candidate.sdpMid,
|
||||
"sdpMlineIndex": e.candidate.sdpMLineIndex,
|
||||
"usernameFragment": "",
|
||||
"target": "SUBSCRIBER",
|
||||
"pcSeq": 1
|
||||
}
|
||||
})
|
||||
print(f" -> [{name}] >> sub ICE sent")
|
||||
|
||||
@pc_pub.on("icecandidate")
|
||||
async def on_pub_ice(e):
|
||||
if e.candidate:
|
||||
await send({
|
||||
"uid": gen_uid(),
|
||||
"webrtcIceCandidate": {
|
||||
"candidate": e.candidate.candidate,
|
||||
"sdpMid": e.candidate.sdpMid,
|
||||
"sdpMlineIndex": e.candidate.sdpMLineIndex,
|
||||
"usernameFragment": "",
|
||||
"target": "PUBLISHER",
|
||||
"pcSeq": 1
|
||||
}
|
||||
})
|
||||
print(f" -> [{name}] >> pub ICE sent")
|
||||
|
||||
setup_pc(pc_sub_ref[0], pc_pub_ref[0])
|
||||
|
||||
hello = {
|
||||
"uid": gen_uid(),
|
||||
"hello": {
|
||||
"participantMeta": {
|
||||
"name": name, "role": "SPEAKER", "description": "",
|
||||
"sendAudio": False, "sendVideo": is_sender
|
||||
},
|
||||
"participantAttributes": {"name": name, "role": "SPEAKER", "description": ""},
|
||||
"sendAudio": False,
|
||||
"sendVideo": is_sender,
|
||||
"sendSharing": False,
|
||||
"participantId": peer_id,
|
||||
"roomId": room_id,
|
||||
"serviceName": "telemost",
|
||||
"credentials": credentials,
|
||||
"capabilitiesOffer": {
|
||||
"offerAnswerMode": ["SEPARATE"],
|
||||
"initialSubscriberOffer": ["ON_HELLO"],
|
||||
"slotsMode": ["FROM_CONTROLLER"],
|
||||
"simulcastMode": ["DISABLED", "STATIC"],
|
||||
"selfVadStatus": ["FROM_SERVER", "FROM_CLIENT"],
|
||||
"dataChannelSharing": ["TO_RTP"],
|
||||
"videoEncoderConfig": ["NO_CONFIG", "ONLY_INIT_CONFIG", "RUNTIME_CONFIG"],
|
||||
"dataChannelVideoCodec": ["VP8", "UNIQUE_CODEC_FROM_TRACK_DESCRIPTION"],
|
||||
"bandwidthLimitationReason": ["BANDWIDTH_REASON_DISABLED", "BANDWIDTH_REASON_ENABLED"],
|
||||
"sdkDefaultDeviceManagement": ["SDK_DEFAULT_DEVICE_MANAGEMENT_DISABLED", "SDK_DEFAULT_DEVICE_MANAGEMENT_ENABLED"],
|
||||
"joinOrderLayout": ["JOIN_ORDER_LAYOUT_DISABLED", "JOIN_ORDER_LAYOUT_ENABLED"],
|
||||
"pinLayout": ["PIN_LAYOUT_DISABLED"],
|
||||
"sendSelfViewVideoSlot": ["SEND_SELF_VIEW_VIDEO_SLOT_DISABLED", "SEND_SELF_VIEW_VIDEO_SLOT_ENABLED"],
|
||||
"serverLayoutTransition": ["SERVER_LAYOUT_TRANSITION_DISABLED"],
|
||||
"sdkPublisherOptimizeBitrate": ["SDK_PUBLISHER_OPTIMIZE_BITRATE_DISABLED", "SDK_PUBLISHER_OPTIMIZE_BITRATE_FULL", "SDK_PUBLISHER_OPTIMIZE_BITRATE_ONLY_SELF"],
|
||||
"sdkNetworkLostDetection": ["SDK_NETWORK_LOST_DETECTION_DISABLED"],
|
||||
"sdkNetworkPathMonitor": ["SDK_NETWORK_PATH_MONITOR_DISABLED"],
|
||||
"publisherVp9": ["PUBLISH_VP9_DISABLED", "PUBLISH_VP9_ENABLED"],
|
||||
"svcMode": ["SVC_MODE_DISABLED", "SVC_MODE_L3T3", "SVC_MODE_L3T3_KEY"],
|
||||
"subscriberOfferAsyncAck": ["SUBSCRIBER_OFFER_ASYNC_ACK_DISABLED", "SUBSCRIBER_OFFER_ASYNC_ACK_ENABLED"],
|
||||
"androidBluetoothRoutingFix": ["ANDROID_BLUETOOTH_ROUTING_FIX_DISABLED"],
|
||||
"fixedIceCandidatesPoolSize": ["FIXED_ICE_CANDIDATES_POOL_SIZE_DISABLED"],
|
||||
"sdkAndroidTelecomIntegration": ["SDK_ANDROID_TELECOM_INTEGRATION_DISABLED"],
|
||||
"setActiveCodecsMode": ["SET_ACTIVE_CODECS_MODE_DISABLED", "SET_ACTIVE_CODECS_MODE_VIDEO_ONLY"],
|
||||
"subscriberDtlsPassiveMode": ["SUBSCRIBER_DTLS_PASSIVE_MODE_DISABLED"],
|
||||
"publisherOpusDred": ["PUBLISHER_OPUS_DRED_DISABLED"],
|
||||
"publisherOpusLowBitrate": ["PUBLISHER_OPUS_LOW_BITRATE_DISABLED"],
|
||||
"sdkAndroidDestroySessionOnTaskRemoved": ["SDK_ANDROID_DESTROY_SESSION_ON_TASK_REMOVED_DISABLED"],
|
||||
"svcModes": ["FALSE"],
|
||||
"reportTelemetryModes": ["TRUE"],
|
||||
"keepDefaultDevicesModes": ["FALSE"]
|
||||
},
|
||||
"sdkInfo": {
|
||||
"implementation": "browser",
|
||||
"version": "5.27.0",
|
||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0",
|
||||
"hwConcurrency": 24
|
||||
},
|
||||
"sdkInitializationId": gen_uid(),
|
||||
"disablePublisher": not is_sender,
|
||||
"disableSubscriber": False,
|
||||
"disableSubscriberAudio": True
|
||||
}
|
||||
}
|
||||
|
||||
await send(hello)
|
||||
print(f" -> [{name}] hello sent")
|
||||
|
||||
pub_sdp_sent = False
|
||||
|
||||
async def ws_loop():
|
||||
nonlocal pub_sdp_sent
|
||||
|
||||
try:
|
||||
async for raw in ws:
|
||||
msg = json.loads(raw)
|
||||
keys = [k for k in msg if k != "uid"]
|
||||
if not keys:
|
||||
continue
|
||||
mtype = keys[0]
|
||||
uid = msg.get("uid", "")
|
||||
print(f" -> [{name}] << {mtype}")
|
||||
|
||||
if mtype == "ack":
|
||||
pass
|
||||
|
||||
elif mtype == "serverHello":
|
||||
sh = msg["serverHello"]
|
||||
raw_ice = sh.get("rtcConfiguration", {}).get("iceServers", [])
|
||||
if raw_ice:
|
||||
ice = make_ice_servers(raw_ice)
|
||||
old_sub = pc_sub_ref[0]
|
||||
old_pub = pc_pub_ref[0]
|
||||
pc_sub_ref[0] = RTCPeerConnection(RTCConfiguration(iceServers=ice))
|
||||
pc_pub_ref[0] = RTCPeerConnection(RTCConfiguration(iceServers=ice))
|
||||
if is_sender and video_track:
|
||||
pc_pub_ref[0].addTrack(video_track)
|
||||
setup_pc(pc_sub_ref[0], pc_pub_ref[0])
|
||||
await old_sub.close()
|
||||
await old_pub.close()
|
||||
print(f" -> [{name}] PC recreated with TURN")
|
||||
await ack(uid)
|
||||
|
||||
elif mtype == "subscriberSdpOffer":
|
||||
offer_sdp = msg["subscriberSdpOffer"]["sdp"]
|
||||
pc_seq = msg["subscriberSdpOffer"]["pcSeq"]
|
||||
pc_sub = pc_sub_ref[0]
|
||||
pc_pub = pc_pub_ref[0]
|
||||
|
||||
await pc_sub.setRemoteDescription(
|
||||
RTCSessionDescription(sdp=offer_sdp, type="offer")
|
||||
)
|
||||
answer = await pc_sub.createAnswer()
|
||||
await pc_sub.setLocalDescription(answer)
|
||||
|
||||
await send({
|
||||
"uid": gen_uid(),
|
||||
"subscriberSdpAnswer": {
|
||||
"pcSeq": pc_seq,
|
||||
"sdp": pc_sub.localDescription.sdp
|
||||
}
|
||||
})
|
||||
print(f" -> [{name}] >> subscriberSdpAnswer")
|
||||
await ack(uid)
|
||||
|
||||
if not is_sender:
|
||||
await send({
|
||||
"uid": gen_uid(),
|
||||
"setSlots": {
|
||||
"slots": [
|
||||
{"width": 1280, "height": 720},
|
||||
{"width": 640, "height": 360}
|
||||
],
|
||||
"audioSlotsCount": 0,
|
||||
"key": 1,
|
||||
"shutdownAllVideo": None,
|
||||
"withSelfView": False,
|
||||
"selfViewVisibility": "ON_LOADING_THEN_SHOW",
|
||||
"gridConfig": {}
|
||||
}
|
||||
})
|
||||
print(f" -> [{name}] >> setSlots (запросили маршрутизацию видео у сервера!)")
|
||||
|
||||
if is_sender and not pub_sdp_sent:
|
||||
await asyncio.sleep(0.3)
|
||||
pub_offer = await pc_pub.createOffer()
|
||||
await pc_pub.setLocalDescription(pub_offer)
|
||||
tracks_info = []
|
||||
for t in pc_pub.getTransceivers():
|
||||
if t.sender.track:
|
||||
tracks_info.append({
|
||||
"mid": t.mid,
|
||||
"transceiverMid": t.mid,
|
||||
"kind": t.sender.track.kind.upper(),
|
||||
"priority": 0,
|
||||
"label": "QRVideoTrack",
|
||||
"codecs": {},
|
||||
"groupId": 1,
|
||||
"description": ""
|
||||
})
|
||||
await send({
|
||||
"uid": gen_uid(),
|
||||
"publisherSdpOffer": {
|
||||
"pcSeq": 1,
|
||||
"sdp": pc_pub.localDescription.sdp,
|
||||
"tracks": tracks_info
|
||||
}
|
||||
})
|
||||
pub_sdp_sent = True
|
||||
print(f" -> [{name}] >> publisherSdpOffer")
|
||||
|
||||
elif mtype == "publisherSdpAnswer":
|
||||
await pc_pub_ref[0].setRemoteDescription(
|
||||
RTCSessionDescription(sdp=msg["publisherSdpAnswer"]["sdp"], type="answer")
|
||||
)
|
||||
print(f" -> [{name}] publisher answer set")
|
||||
await ack(uid)
|
||||
|
||||
elif mtype == "webrtcIceCandidate":
|
||||
cand = msg["webrtcIceCandidate"]
|
||||
candidate_str = cand.get("candidate", "")
|
||||
target = cand.get("target", "")
|
||||
sdp_mid = cand.get("sdpMid", "0")
|
||||
sdp_mline = cand.get("sdpMlineIndex", 0)
|
||||
|
||||
if not candidate_str:
|
||||
continue
|
||||
parts = candidate_str.split()
|
||||
if len(parts) < 8:
|
||||
continue
|
||||
|
||||
try:
|
||||
tcptype = None
|
||||
if "tcptype" in parts:
|
||||
ti = parts.index("tcptype")
|
||||
tcptype = parts[ti + 1]
|
||||
|
||||
ice_c = RTCIceCandidate(
|
||||
component=int(parts[1]),
|
||||
foundation=parts[0].replace("candidate:", ""),
|
||||
ip=parts[4],
|
||||
port=int(parts[5]),
|
||||
priority=int(parts[3]),
|
||||
protocol=parts[2].lower(),
|
||||
type=parts[7],
|
||||
tcpType=tcptype,
|
||||
sdpMid=sdp_mid,
|
||||
sdpMLineIndex=sdp_mline
|
||||
)
|
||||
if target == "SUBSCRIBER":
|
||||
await pc_sub_ref[0].addIceCandidate(ice_c)
|
||||
print(f" -> [{name}] sub ICE: {parts[2]} {parts[4]}:{parts[5]}")
|
||||
elif target == "PUBLISHER":
|
||||
await pc_pub_ref[0].addIceCandidate(ice_c)
|
||||
print(f" -> [{name}] pub ICE: {parts[2]} {parts[4]}:{parts[5]}")
|
||||
except Exception as e:
|
||||
print(f" -> [{name}] ICE err: {e}")
|
||||
|
||||
elif mtype in ("setSlots", "slotsConfig", "slotsMeta", "vadActivity",
|
||||
"updateDescription", "upsertDescription", "sdkCodecsInfo",
|
||||
"pingPong", "selfQualityReport", "upsertParticipantsQualityReport"):
|
||||
await ack(uid)
|
||||
|
||||
else:
|
||||
print(f" -> [{name}] unhandled: {mtype}")
|
||||
if uid:
|
||||
await ack(uid)
|
||||
|
||||
except websockets.exceptions.ConnectionClosed as e:
|
||||
print(f" -> [{name}] WS closed: {e}")
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f" -> [{name}] WS err: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
ws_task = asyncio.create_task(ws_loop())
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"ws": ws,
|
||||
"ws_task": ws_task,
|
||||
"pc_pub_ref": pc_pub_ref,
|
||||
"pc_sub_ref": pc_sub_ref,
|
||||
"video_track": video_track,
|
||||
"receiver": receiver_obj,
|
||||
"track_tasks": track_tasks
|
||||
}
|
||||
|
||||
|
||||
async def run():
|
||||
print("""
|
||||
VCSend - Video QR Transfer
|
||||
Request/Response over Yandex Telemost SFU
|
||||
by zowue for olc
|
||||
""")
|
||||
|
||||
print("[0/3] Getting conference info...")
|
||||
sender_conn = get_connection_info("QR_Sender")
|
||||
receiver_conn = get_connection_info("QR_Receiver")
|
||||
print(f" -> sender room: {sender_conn['room_id']}")
|
||||
print(f" -> receiver room: {receiver_conn['room_id']}")
|
||||
|
||||
print("\n[1/3] Connecting sender...")
|
||||
sender = await connect_peer("QR_Sender", sender_conn)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
print("\n[2/3] Connecting receiver...")
|
||||
receiver = await connect_peer("QR_Receiver", receiver_conn)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
print("\n[3/3] Transfer...")
|
||||
url = "zarazaex.xyz/curl.txt"
|
||||
if not url.startswith("http"):
|
||||
url = "https://" + url
|
||||
|
||||
print(f" -> fetching {url}")
|
||||
resp = requests.get(url, timeout=10)
|
||||
resp.raise_for_status()
|
||||
data = resp.content
|
||||
print(f" -> got {len(data)} bytes")
|
||||
|
||||
tid = gen_uid()
|
||||
chunks = chunk_data(data, tid)
|
||||
sender["video_track"].set_data(chunks)
|
||||
print(f" -> {len(chunks)} QR frames set, waiting for decode...")
|
||||
|
||||
for i in range(300):
|
||||
await asyncio.sleep(1)
|
||||
if i % 15 == 0:
|
||||
print(f" -> {i}s elapsed")
|
||||
if receiver["receiver"] and receiver["receiver"].result is not None:
|
||||
result = receiver["receiver"].result
|
||||
print(f"\n :P got {len(result)} bytes\n")
|
||||
print("--- content ---")
|
||||
try:
|
||||
print(result.decode("utf-8"))
|
||||
except Exception:
|
||||
print(f"[binary {len(result)} bytes]")
|
||||
print("--- end ---\n")
|
||||
break
|
||||
else:
|
||||
print(" X timeout — check /tmp/qr_recv_*.png")
|
||||
|
||||
for p in [sender, receiver]:
|
||||
p["ws_task"].cancel()
|
||||
try:
|
||||
await p["ws"].close()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
await p["pc_pub_ref"][0].close()
|
||||
await p["pc_sub_ref"][0].close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(run())
|
||||
except KeyboardInterrupt:
|
||||
print("\ninterrupted")
|
||||
Reference in New Issue
Block a user