mirror of
https://github.com/openlibrecommunity/olcrtc.git
synced 2026-06-01 22:18:52 +02:00
style: reformat python files
This commit is contained in:
+83
-16
@@ -2,33 +2,89 @@
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
import aiohttp
|
||||
from urllib.parse import quote
|
||||
|
||||
import aiohttp
|
||||
|
||||
API_BASE = "https://cloud-api.yandex.ru/telemost_front/v2/telemost"
|
||||
CONFERENCE_ID = "75047680642749"
|
||||
CONFERENCE_ID = "02789996238784"
|
||||
ROOM_URL = f"https://telemost.yandex.ru/j/{CONFERENCE_ID}"
|
||||
|
||||
|
||||
async def get_telemost_info():
|
||||
print("\n--- Yandex Telemost Info ---")
|
||||
async with aiohttp.ClientSession() as session:
|
||||
print(f"[1/3] Fetching connection info...")
|
||||
url = f"{API_BASE}/conferences/{quote(ROOM_URL, safe='')}/connection"
|
||||
params = {"next_gen_media_platform_allowed": "true", "display_name": "InfoBot", "waiting_room_supported": "true"}
|
||||
headers = {"User-Agent": "Mozilla/5.0 (Linux x86_64)", "Client-Instance-Id": str(uuid.uuid4()), "X-Telemost-Client-Version": "187.1.0", "idempotency-key": str(uuid.uuid4())}
|
||||
|
||||
params = {
|
||||
"next_gen_media_platform_allowed": "true",
|
||||
"display_name": "InfoBot",
|
||||
"waiting_room_supported": "true",
|
||||
}
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Linux x86_64)",
|
||||
"Client-Instance-Id": str(uuid.uuid4()),
|
||||
"X-Telemost-Client-Version": "187.1.0",
|
||||
"idempotency-key": str(uuid.uuid4()),
|
||||
}
|
||||
|
||||
try:
|
||||
async with session.get(url, params=params, headers=headers) as resp:
|
||||
if resp.status != 200: print(f" X API Fail: {resp.status}"); return
|
||||
if resp.status != 200:
|
||||
print(f" X API Fail: {resp.status}")
|
||||
return
|
||||
conn_info = await resp.json()
|
||||
print(" :P Connection data received")
|
||||
print(json.dumps(conn_info, indent=2))
|
||||
except Exception as e: print(f" X Error: {e}"); return
|
||||
except Exception as e:
|
||||
print(f" X Error: {e}")
|
||||
return
|
||||
|
||||
print(f"\n[2/3] Connecting to signaling...")
|
||||
try:
|
||||
async with session.ws_connect(conn_info["client_configuration"]["media_server_url"]) as ws:
|
||||
await ws.send_json({"uid": str(uuid.uuid4()), "hello": {"participantMeta": {"name": "InfoBot", "role": "SPEAKER", "sendAudio": False, "sendVideo": False}, "participantAttributes": {"name": "InfoBot", "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": ["DISABLED"], "selfVadStatus": ["FROM_SERVER"], "dataChannelSharing": ["TO_RTP"]}, "sdkInfo": {"implementation": "python", "version": "1.0.0", "userAgent": "OlcRTC-InfoBot"}, "sdkInitializationId": str(uuid.uuid4()), "disablePublisher": False, "disableSubscriber": False}})
|
||||
async with session.ws_connect(
|
||||
conn_info["client_configuration"]["media_server_url"]
|
||||
) as ws:
|
||||
await ws.send_json(
|
||||
{
|
||||
"uid": str(uuid.uuid4()),
|
||||
"hello": {
|
||||
"participantMeta": {
|
||||
"name": "InfoBot",
|
||||
"role": "SPEAKER",
|
||||
"sendAudio": False,
|
||||
"sendVideo": False,
|
||||
},
|
||||
"participantAttributes": {
|
||||
"name": "InfoBot",
|
||||
"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": ["DISABLED"],
|
||||
"selfVadStatus": ["FROM_SERVER"],
|
||||
"dataChannelSharing": ["TO_RTP"],
|
||||
},
|
||||
"sdkInfo": {
|
||||
"implementation": "python",
|
||||
"version": "1.0.0",
|
||||
"userAgent": "OlcRTC-InfoBot",
|
||||
},
|
||||
"sdkInitializationId": str(uuid.uuid4()),
|
||||
"disablePublisher": False,
|
||||
"disableSubscriber": False,
|
||||
},
|
||||
}
|
||||
)
|
||||
print(" :P Signaling established")
|
||||
|
||||
print("\n[3/3] Collecting media details...")
|
||||
@@ -37,7 +93,8 @@ async def get_telemost_info():
|
||||
try:
|
||||
m = await asyncio.wait_for(ws.receive(), 1)
|
||||
if m.type == aiohttp.WSMsgType.TEXT:
|
||||
d = json.loads(m.data); uid = d.get("uid")
|
||||
d = json.loads(m.data)
|
||||
uid = d.get("uid")
|
||||
print(f" -> Message: {list(d.keys())}")
|
||||
if "serverHello" in d:
|
||||
print("\n--- Server Hello / Telemetry ---")
|
||||
@@ -46,13 +103,23 @@ async def get_telemost_info():
|
||||
print("\n--- SDP Offer (Codecs & Quality) ---")
|
||||
print(d["subscriberSdpOffer"].get("sdp"))
|
||||
elif "webrtcIceCandidate" in d:
|
||||
print(f" -> ICE: {d['webrtcIceCandidate'].get('candidate')}")
|
||||
if uid: await ws.send_json({"uid": uid, "ack": {"status": {"code": "OK"}}})
|
||||
except: continue
|
||||
except Exception as e: print(f" X Signaling Fail: {e}")
|
||||
print(
|
||||
f" -> ICE: {d['webrtcIceCandidate'].get('candidate')}"
|
||||
)
|
||||
if uid:
|
||||
await ws.send_json(
|
||||
{"uid": uid, "ack": {"status": {"code": "OK"}}}
|
||||
)
|
||||
except:
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f" X Signaling Fail: {e}")
|
||||
|
||||
print("\n--- INFO COLLECTION COMPLETE ---")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try: asyncio.run(get_telemost_info())
|
||||
except KeyboardInterrupt: pass
|
||||
try:
|
||||
asyncio.run(get_telemost_info())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
@@ -3,19 +3,29 @@
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
import time
|
||||
import uuid
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
import websockets
|
||||
from urllib.parse import quote
|
||||
from aiortc import RTCPeerConnection, RTCSessionDescription, RTCIceCandidate, RTCConfiguration, RTCIceServer
|
||||
from aiortc import (
|
||||
RTCConfiguration,
|
||||
RTCIceCandidate,
|
||||
RTCIceServer,
|
||||
RTCPeerConnection,
|
||||
RTCSessionDescription,
|
||||
)
|
||||
|
||||
CONFERENCE_ID = "75047680642749"
|
||||
CONFERENCE_ID = "02789996238784"
|
||||
API_BASE = "https://cloud-api.yandex.ru/telemost_front/v2/telemost"
|
||||
ICE_SERVER = RTCIceServer(urls=["stun:stun.rtc.yandex.net:3478"])
|
||||
TEST_MESSAGES = ["Hello Yandex Telemost!", "Hello world", "X" * 100, "Final test"]
|
||||
|
||||
def _gen_uuid() -> str: return str(uuid.uuid4())
|
||||
|
||||
def _gen_uuid() -> str:
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def _get_conn_info(display_name: str) -> dict:
|
||||
url = f"{API_BASE}/conferences/{quote(f'https://telemost.yandex.ru/j/{CONFERENCE_ID}', safe='')}/connection"
|
||||
@@ -25,11 +35,16 @@ def _get_conn_info(display_name: str) -> dict:
|
||||
"X-Telemost-Client-Version": "187.1.0",
|
||||
"idempotency-key": _gen_uuid(),
|
||||
}
|
||||
params = {"next_gen_media_platform_allowed": "true", "display_name": display_name, "waiting_room_supported": "true"}
|
||||
params = {
|
||||
"next_gen_media_platform_allowed": "true",
|
||||
"display_name": display_name,
|
||||
"waiting_room_supported": "true",
|
||||
}
|
||||
resp = requests.get(url, params=params, headers=headers)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
async def _create_peer(name: str, is_server: bool = False, stats: dict = None) -> dict:
|
||||
info = _get_conn_info(name)
|
||||
ws = await websockets.connect(info["client_configuration"]["media_server_url"])
|
||||
@@ -39,11 +54,13 @@ async def _create_peer(name: str, is_server: bool = False, stats: dict = None) -
|
||||
dc_ready = asyncio.Event()
|
||||
|
||||
@dc_pub.on("open")
|
||||
def on_open(): dc_ready.set()
|
||||
|
||||
def on_open():
|
||||
dc_ready.set()
|
||||
|
||||
@dc_pub.on("message")
|
||||
def on_pub_msg(msg): stats["recv"] += 1
|
||||
|
||||
def on_pub_msg(msg):
|
||||
stats["recv"] += 1
|
||||
|
||||
@pc_sub.on("datachannel")
|
||||
def on_sub_dc(channel):
|
||||
@channel.on("message")
|
||||
@@ -53,21 +70,48 @@ async def _create_peer(name: str, is_server: bool = False, stats: dict = None) -
|
||||
try:
|
||||
dc_pub.send(f"Echo: {m}")
|
||||
stats["sent"] += 1
|
||||
except: pass
|
||||
except:
|
||||
pass
|
||||
|
||||
await ws.send(json.dumps({
|
||||
"uid": _gen_uuid(),
|
||||
"hello": {
|
||||
"participantMeta": {"name": name, "role": "SPEAKER", "sendAudio": False, "sendVideo": False},
|
||||
"participantAttributes": {"name": name, "role": "SPEAKER"},
|
||||
"sendAudio": False, "sendVideo": False, "sendSharing": False,
|
||||
"participantId": info["peer_id"], "roomId": info["room_id"],
|
||||
"serviceName": "telemost", "credentials": info["credentials"],
|
||||
"capabilitiesOffer": {"offerAnswerMode": ["SEPARATE"], "initialSubscriberOffer": ["ON_HELLO"], "slotsMode": ["FROM_CONTROLLER"], "simulcastMode": ["DISABLED"], "selfVadStatus": ["FROM_SERVER"], "dataChannelSharing": ["TO_RTP"]},
|
||||
"sdkInfo": {"implementation": "python", "version": "1.0.0", "userAgent": f"OlcRTC-{name}"},
|
||||
"sdkInitializationId": _gen_uuid(), "disablePublisher": False, "disableSubscriber": False
|
||||
}
|
||||
}))
|
||||
await ws.send(
|
||||
json.dumps(
|
||||
{
|
||||
"uid": _gen_uuid(),
|
||||
"hello": {
|
||||
"participantMeta": {
|
||||
"name": name,
|
||||
"role": "SPEAKER",
|
||||
"sendAudio": False,
|
||||
"sendVideo": False,
|
||||
},
|
||||
"participantAttributes": {"name": name, "role": "SPEAKER"},
|
||||
"sendAudio": False,
|
||||
"sendVideo": False,
|
||||
"sendSharing": False,
|
||||
"participantId": info["peer_id"],
|
||||
"roomId": info["room_id"],
|
||||
"serviceName": "telemost",
|
||||
"credentials": info["credentials"],
|
||||
"capabilitiesOffer": {
|
||||
"offerAnswerMode": ["SEPARATE"],
|
||||
"initialSubscriberOffer": ["ON_HELLO"],
|
||||
"slotsMode": ["FROM_CONTROLLER"],
|
||||
"simulcastMode": ["DISABLED"],
|
||||
"selfVadStatus": ["FROM_SERVER"],
|
||||
"dataChannelSharing": ["TO_RTP"],
|
||||
},
|
||||
"sdkInfo": {
|
||||
"implementation": "python",
|
||||
"version": "1.0.0",
|
||||
"userAgent": f"OlcRTC-{name}",
|
||||
},
|
||||
"sdkInitializationId": _gen_uuid(),
|
||||
"disablePublisher": False,
|
||||
"disableSubscriber": False,
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
async def _ws_loop():
|
||||
pub_sdp_sent = False
|
||||
@@ -75,55 +119,127 @@ async def _create_peer(name: str, is_server: bool = False, stats: dict = None) -
|
||||
try:
|
||||
data = json.loads(await ws.recv())
|
||||
uid = data.get("uid")
|
||||
|
||||
|
||||
if "serverHello" in data:
|
||||
await ws.send(json.dumps({"uid": uid, "ack": {"status": {"code": "OK"}}}))
|
||||
|
||||
await ws.send(
|
||||
json.dumps({"uid": uid, "ack": {"status": {"code": "OK"}}})
|
||||
)
|
||||
|
||||
elif "subscriberSdpOffer" in data and not pub_sdp_sent:
|
||||
sdp = data["subscriberSdpOffer"]
|
||||
await pc_sub.setRemoteDescription(RTCSessionDescription(sdp=sdp["sdp"], type="offer"))
|
||||
await pc_sub.setRemoteDescription(
|
||||
RTCSessionDescription(sdp=sdp["sdp"], type="offer")
|
||||
)
|
||||
ans = await pc_sub.createAnswer()
|
||||
await pc_sub.setLocalDescription(ans)
|
||||
await ws.send(json.dumps({"uid": _gen_uuid(), "subscriberSdpAnswer": {"pcSeq": sdp["pcSeq"], "sdp": pc_sub.localDescription.sdp}}))
|
||||
await ws.send(json.dumps({"uid": uid, "ack": {"status": {"code": "OK"}}}))
|
||||
await ws.send(
|
||||
json.dumps(
|
||||
{
|
||||
"uid": _gen_uuid(),
|
||||
"subscriberSdpAnswer": {
|
||||
"pcSeq": sdp["pcSeq"],
|
||||
"sdp": pc_sub.localDescription.sdp,
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
await ws.send(
|
||||
json.dumps({"uid": uid, "ack": {"status": {"code": "OK"}}})
|
||||
)
|
||||
await asyncio.sleep(0.3)
|
||||
pub_offer = await pc_pub.createOffer()
|
||||
await pc_pub.setLocalDescription(pub_offer)
|
||||
await ws.send(json.dumps({"uid": _gen_uuid(), "publisherSdpOffer": {"pcSeq": 1, "sdp": pc_pub.localDescription.sdp}}))
|
||||
await ws.send(
|
||||
json.dumps(
|
||||
{
|
||||
"uid": _gen_uuid(),
|
||||
"publisherSdpOffer": {
|
||||
"pcSeq": 1,
|
||||
"sdp": pc_pub.localDescription.sdp,
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
pub_sdp_sent = True
|
||||
|
||||
|
||||
elif "publisherSdpAnswer" in data:
|
||||
await pc_pub.setRemoteDescription(RTCSessionDescription(sdp=data["publisherSdpAnswer"]["sdp"], type="answer"))
|
||||
await ws.send(json.dumps({"uid": uid, "ack": {"status": {"code": "OK"}}}))
|
||||
|
||||
await pc_pub.setRemoteDescription(
|
||||
RTCSessionDescription(
|
||||
sdp=data["publisherSdpAnswer"]["sdp"], type="answer"
|
||||
)
|
||||
)
|
||||
await ws.send(
|
||||
json.dumps({"uid": uid, "ack": {"status": {"code": "OK"}}})
|
||||
)
|
||||
|
||||
elif "webrtcIceCandidate" in data:
|
||||
cand = data["webrtcIceCandidate"]
|
||||
parts = cand["candidate"].split()
|
||||
if len(parts) >= 8:
|
||||
ice = RTCIceCandidate(component=int(parts[1]), foundation=parts[0].replace("candidate:", ""), ip=parts[4], port=int(parts[5]), priority=int(parts[3]), protocol=parts[2], type=parts[7], sdpMid=cand["sdpMid"], sdpMLineIndex=cand["sdpMlineIndex"])
|
||||
await (pc_sub if cand.get("target") == "SUBSCRIBER" else pc_pub).addIceCandidate(ice)
|
||||
except Exception: break
|
||||
ice = RTCIceCandidate(
|
||||
component=int(parts[1]),
|
||||
foundation=parts[0].replace("candidate:", ""),
|
||||
ip=parts[4],
|
||||
port=int(parts[5]),
|
||||
priority=int(parts[3]),
|
||||
protocol=parts[2],
|
||||
type=parts[7],
|
||||
sdpMid=cand["sdpMid"],
|
||||
sdpMLineIndex=cand["sdpMlineIndex"],
|
||||
)
|
||||
await (
|
||||
pc_sub if cand.get("target") == "SUBSCRIBER" else pc_pub
|
||||
).addIceCandidate(ice)
|
||||
except Exception:
|
||||
break
|
||||
|
||||
async def _on_ice(event, target):
|
||||
if event.candidate:
|
||||
await ws.send(json.dumps({"uid": _gen_uuid(), "webrtcIceCandidate": {"candidate": event.candidate.candidate, "sdpMid": event.candidate.sdpMid, "sdpMlineIndex": event.candidate.sdpMLineIndex, "target": target, "pcSeq": 1}}))
|
||||
await ws.send(
|
||||
json.dumps(
|
||||
{
|
||||
"uid": _gen_uuid(),
|
||||
"webrtcIceCandidate": {
|
||||
"candidate": event.candidate.candidate,
|
||||
"sdpMid": event.candidate.sdpMid,
|
||||
"sdpMlineIndex": event.candidate.sdpMLineIndex,
|
||||
"target": target,
|
||||
"pcSeq": 1,
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
pc_sub.on("icecandidate", lambda e: asyncio.create_task(_on_ice(e, "SUBSCRIBER")))
|
||||
pc_pub.on("icecandidate", lambda e: asyncio.create_task(_on_ice(e, "PUBLISHER")))
|
||||
|
||||
return {"dc": dc_pub, "ready": dc_ready, "task": asyncio.create_task(_ws_loop()), "ws": ws, "pc_sub": pc_sub, "pc_pub": pc_pub}
|
||||
|
||||
return {
|
||||
"dc": dc_pub,
|
||||
"ready": dc_ready,
|
||||
"task": asyncio.create_task(_ws_loop()),
|
||||
"ws": ws,
|
||||
"pc_sub": pc_sub,
|
||||
"pc_pub": pc_pub,
|
||||
}
|
||||
|
||||
|
||||
async def run_poc() -> dict:
|
||||
print("\n--- Yandex Telemost PoC ---")
|
||||
results = {"server_ok": False, "client_ok": False, "sent": 0, "recv": 0, "errors": []}
|
||||
results = {
|
||||
"server_ok": False,
|
||||
"client_ok": False,
|
||||
"sent": 0,
|
||||
"recv": 0,
|
||||
"errors": [],
|
||||
}
|
||||
s_stats, c_stats = {"sent": 0, "recv": 0}, {"sent": 0, "recv": 0}
|
||||
|
||||
|
||||
print("[1/3] Connecting Server & Client...")
|
||||
try:
|
||||
server = await _create_peer("Server", is_server=True, stats=s_stats)
|
||||
await asyncio.wait_for(server["ready"].wait(), 10.0)
|
||||
results["server_ok"] = True
|
||||
|
||||
|
||||
client = await _create_peer("Client", is_server=False, stats=c_stats)
|
||||
await asyncio.wait_for(client["ready"].wait(), 10.0)
|
||||
results["client_ok"] = True
|
||||
@@ -145,24 +261,34 @@ async def run_poc() -> dict:
|
||||
|
||||
await asyncio.sleep(2)
|
||||
results["sent"], results["recv"] = c_stats["sent"], c_stats["recv"]
|
||||
|
||||
|
||||
print("\n[3/3] Cleaning up...")
|
||||
for p in (server, client):
|
||||
p["task"].cancel()
|
||||
await p["ws"].close()
|
||||
await p["pc_sub"].close()
|
||||
await p["pc_pub"].close()
|
||||
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def print_results(res: dict):
|
||||
print("\n--- TEST RESULTS ---")
|
||||
print(f"Server: {':P' if res['server_ok'] else 'X'} / Client: {':P' if res['client_ok'] else 'X'}")
|
||||
print(
|
||||
f"Server: {':P' if res['server_ok'] else 'X'} / Client: {':P' if res['client_ok'] else 'X'}"
|
||||
)
|
||||
print(f"Messages: Sent {res['sent']} / Recv {res['recv']}")
|
||||
if res['errors']:
|
||||
for e in res['errors']: print(f" Error: {e}")
|
||||
print(f"\n{':P SUCCESS' if res['sent'] and res['sent'] == res['recv'] else 'X FAILED'}\n")
|
||||
if res["errors"]:
|
||||
for e in res["errors"]:
|
||||
print(f" Error: {e}")
|
||||
print(
|
||||
f"\n{':P SUCCESS' if res['sent'] and res['sent'] == res['recv'] else 'X FAILED'}\n"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try: res = asyncio.run(run_poc()); print_results(res)
|
||||
except KeyboardInterrupt: pass
|
||||
try:
|
||||
res = asyncio.run(run_poc())
|
||||
print_results(res)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
+252
-140
@@ -26,7 +26,7 @@ from aiortc.mediastreams import MediaStreamTrack
|
||||
from av import VideoFrame
|
||||
from pyzbar.pyzbar import decode as qr_decode
|
||||
|
||||
CONFERENCE_ID = "75047680642749"
|
||||
CONFERENCE_ID = "02789996238784"
|
||||
CONFERENCE_URL = f"https://telemost.yandex.ru/j/{CONFERENCE_ID}"
|
||||
API_BASE = "https://cloud-api.yandex.ru/telemost_front/v2/telemost"
|
||||
FPS = 10
|
||||
@@ -44,18 +44,30 @@ def _uid() -> str:
|
||||
|
||||
def _encode(text: str) -> np.ndarray:
|
||||
payload = base64.b64encode(zlib.compress(text.encode())).decode()
|
||||
qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=8, border=3)
|
||||
qr = qrcode.QRCode(
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=8, border=3
|
||||
)
|
||||
qr.add_data(payload)
|
||||
qr.make(fit=True)
|
||||
arr = np.array(qr.make_image(fill_color="black", back_color="white").convert("RGB"), dtype=np.uint8)
|
||||
arr = np.array(
|
||||
qr.make_image(fill_color="black", back_color="white").convert("RGB"),
|
||||
dtype=np.uint8,
|
||||
)
|
||||
h, w = arr.shape[:2]
|
||||
return arr if (h % 2 == 0 and w % 2 == 0) else cv2.resize(arr, (w + w % 2, h + h % 2))
|
||||
return (
|
||||
arr if (h % 2 == 0 and w % 2 == 0) else cv2.resize(arr, (w + w % 2, h + h % 2))
|
||||
)
|
||||
|
||||
|
||||
def _decode(frame: VideoFrame) -> str | None:
|
||||
arr = frame.to_ndarray(format="rgb24")
|
||||
gray = cv2.cvtColor(arr, cv2.COLOR_RGB2GRAY)
|
||||
for img in [gray, cv2.resize(gray, (gray.shape[1] * 2, gray.shape[0] * 2), interpolation=cv2.INTER_CUBIC)]:
|
||||
for img in [
|
||||
gray,
|
||||
cv2.resize(
|
||||
gray, (gray.shape[1] * 2, gray.shape[0] * 2), interpolation=cv2.INTER_CUBIC
|
||||
),
|
||||
]:
|
||||
for obj in qr_decode(img):
|
||||
try:
|
||||
return zlib.decompress(base64.b64decode(obj.data)).decode()
|
||||
@@ -119,7 +131,9 @@ class _VideoChannelTrack(MediaStreamTrack):
|
||||
return frame
|
||||
|
||||
|
||||
async def _connect_peer(name: str, conn: dict, is_sender: bool, on_video_message=None) -> dict:
|
||||
async def _connect_peer(
|
||||
name: str, conn: dict, is_sender: bool, on_video_message=None
|
||||
) -> dict:
|
||||
default_ice = [RTCIceServer(urls=["stun:stun.rtc.yandex.net:3478"])]
|
||||
track = _VideoChannelTrack() if is_sender else None
|
||||
|
||||
@@ -149,22 +163,40 @@ async def _connect_peer(name: str, conn: dict, is_sender: bool, on_video_message
|
||||
def _setup(pc_sub, pc_pub) -> None:
|
||||
@pc_sub.on("track")
|
||||
def on_track(remote_track):
|
||||
if remote_track.kind != "video" or on_video_message is None:
|
||||
print(
|
||||
f" [{name}] Got remote track: kind={remote_track.kind} id={remote_track.id}"
|
||||
)
|
||||
if remote_track.kind != "video":
|
||||
return
|
||||
|
||||
async def _loop():
|
||||
frame_count = 0
|
||||
last_hash = None
|
||||
while True:
|
||||
try:
|
||||
frame = await asyncio.wait_for(remote_track.recv(), timeout=30.0)
|
||||
frame_hash = hashlib.md5(frame.to_ndarray(format="rgb24").tobytes()).hexdigest()
|
||||
frame = await asyncio.wait_for(remote_track.recv(), timeout=5.0)
|
||||
frame_count += 1
|
||||
if frame_count <= 3:
|
||||
print(
|
||||
f" [{name}] Frame #{frame_count}: {frame.width}x{frame.height}"
|
||||
)
|
||||
frame_hash = hashlib.md5(
|
||||
frame.to_ndarray(format="rgb24").tobytes()
|
||||
).hexdigest()
|
||||
if frame_hash == last_hash:
|
||||
continue
|
||||
last_hash = frame_hash
|
||||
msg = _decode(frame)
|
||||
if msg:
|
||||
on_video_message(msg)
|
||||
except Exception:
|
||||
if on_video_message:
|
||||
msg = _decode(frame)
|
||||
if msg:
|
||||
on_video_message(msg)
|
||||
except asyncio.TimeoutError:
|
||||
print(
|
||||
f" [{name}] No frames received after {frame_count} total"
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
print(f" [{name}] Track error after {frame_count} frames: {e}")
|
||||
return
|
||||
|
||||
asyncio.create_task(_loop())
|
||||
@@ -182,106 +214,151 @@ async def _connect_peer(name: str, conn: dict, is_sender: bool, on_video_message
|
||||
@pc_sub.on("icecandidate")
|
||||
async def on_sub_ice(event):
|
||||
if event.candidate:
|
||||
await _send({
|
||||
"uid": _uid(),
|
||||
"webrtcIceCandidate": {
|
||||
"candidate": event.candidate.candidate,
|
||||
"sdpMid": event.candidate.sdpMid,
|
||||
"sdpMlineIndex": event.candidate.sdpMLineIndex,
|
||||
"usernameFragment": "",
|
||||
"target": "SUBSCRIBER",
|
||||
"pcSeq": 1,
|
||||
},
|
||||
})
|
||||
await _send(
|
||||
{
|
||||
"uid": _uid(),
|
||||
"webrtcIceCandidate": {
|
||||
"candidate": event.candidate.candidate,
|
||||
"sdpMid": event.candidate.sdpMid,
|
||||
"sdpMlineIndex": event.candidate.sdpMLineIndex,
|
||||
"usernameFragment": "",
|
||||
"target": "SUBSCRIBER",
|
||||
"pcSeq": 1,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@pc_pub.on("icecandidate")
|
||||
async def on_pub_ice(event):
|
||||
if event.candidate:
|
||||
await _send({
|
||||
"uid": _uid(),
|
||||
"webrtcIceCandidate": {
|
||||
"candidate": event.candidate.candidate,
|
||||
"sdpMid": event.candidate.sdpMid,
|
||||
"sdpMlineIndex": event.candidate.sdpMLineIndex,
|
||||
"usernameFragment": "",
|
||||
"target": "PUBLISHER",
|
||||
"pcSeq": 1,
|
||||
},
|
||||
})
|
||||
await _send(
|
||||
{
|
||||
"uid": _uid(),
|
||||
"webrtcIceCandidate": {
|
||||
"candidate": event.candidate.candidate,
|
||||
"sdpMid": event.candidate.sdpMid,
|
||||
"sdpMlineIndex": event.candidate.sdpMLineIndex,
|
||||
"usernameFragment": "",
|
||||
"target": "PUBLISHER",
|
||||
"pcSeq": 1,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
_setup(pc_sub_ref[0], pc_pub_ref[0])
|
||||
|
||||
await _send({
|
||||
"uid": _uid(),
|
||||
"hello": {
|
||||
"participantMeta": {
|
||||
"name": name,
|
||||
"role": "SPEAKER",
|
||||
"description": "",
|
||||
await _send(
|
||||
{
|
||||
"uid": _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": conn["peer_id"],
|
||||
"roomId": conn["room_id"],
|
||||
"serviceName": "telemost",
|
||||
"credentials": conn["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": _uid(),
|
||||
"disablePublisher": not is_sender,
|
||||
"disableSubscriber": False,
|
||||
"disableSubscriberAudio": True,
|
||||
},
|
||||
"participantAttributes": {
|
||||
"name": name,
|
||||
"role": "SPEAKER",
|
||||
"description": "",
|
||||
},
|
||||
"sendAudio": False,
|
||||
"sendVideo": is_sender,
|
||||
"sendSharing": False,
|
||||
"participantId": conn["peer_id"],
|
||||
"roomId": conn["room_id"],
|
||||
"serviceName": "telemost",
|
||||
"credentials": conn["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": _uid(),
|
||||
"disablePublisher": not is_sender,
|
||||
"disableSubscriber": False,
|
||||
"disableSubscriberAudio": True,
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
async def _ws_loop():
|
||||
pub_sdp_sent = False
|
||||
@@ -294,13 +371,21 @@ async def _connect_peer(name: str, conn: dict, is_sender: bool, on_video_message
|
||||
continue
|
||||
|
||||
if "serverHello" in data:
|
||||
raw_ice = data["serverHello"].get("rtcConfiguration", {}).get("iceServers", [])
|
||||
raw_ice = (
|
||||
data["serverHello"]
|
||||
.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))
|
||||
pc_sub_ref[0] = RTCPeerConnection(
|
||||
RTCConfiguration(iceServers=ice)
|
||||
)
|
||||
pc_pub_ref[0] = RTCPeerConnection(
|
||||
RTCConfiguration(iceServers=ice)
|
||||
)
|
||||
if is_sender and track is not None:
|
||||
pc_pub_ref[0].addTrack(track)
|
||||
_setup(pc_sub_ref[0], pc_pub_ref[0])
|
||||
@@ -316,28 +401,35 @@ async def _connect_peer(name: str, conn: dict, is_sender: bool, on_video_message
|
||||
)
|
||||
answer = await pc_sub_ref[0].createAnswer()
|
||||
await pc_sub_ref[0].setLocalDescription(answer)
|
||||
await _send({
|
||||
"uid": _uid(),
|
||||
"subscriberSdpAnswer": {
|
||||
"pcSeq": sdp["pcSeq"],
|
||||
"sdp": pc_sub_ref[0].localDescription.sdp,
|
||||
},
|
||||
})
|
||||
await _send(
|
||||
{
|
||||
"uid": _uid(),
|
||||
"subscriberSdpAnswer": {
|
||||
"pcSeq": sdp["pcSeq"],
|
||||
"sdp": pc_sub_ref[0].localDescription.sdp,
|
||||
},
|
||||
}
|
||||
)
|
||||
await _ack(uid)
|
||||
|
||||
if not is_sender:
|
||||
await _send({
|
||||
"uid": _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": {},
|
||||
},
|
||||
})
|
||||
if not pub_sdp_sent:
|
||||
await _send(
|
||||
{
|
||||
"uid": _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": {},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if is_sender and not pub_sdp_sent:
|
||||
await asyncio.sleep(0.3)
|
||||
@@ -357,20 +449,24 @@ async def _connect_peer(name: str, conn: dict, is_sender: bool, on_video_message
|
||||
for t in pc_pub_ref[0].getTransceivers()
|
||||
if t.sender.track
|
||||
]
|
||||
await _send({
|
||||
"uid": _uid(),
|
||||
"publisherSdpOffer": {
|
||||
"pcSeq": 1,
|
||||
"sdp": pc_pub_ref[0].localDescription.sdp,
|
||||
"tracks": tracks,
|
||||
},
|
||||
})
|
||||
await _send(
|
||||
{
|
||||
"uid": _uid(),
|
||||
"publisherSdpOffer": {
|
||||
"pcSeq": 1,
|
||||
"sdp": pc_pub_ref[0].localDescription.sdp,
|
||||
"tracks": tracks,
|
||||
},
|
||||
}
|
||||
)
|
||||
pub_sdp_sent = True
|
||||
continue
|
||||
|
||||
if "publisherSdpAnswer" in data:
|
||||
await pc_pub_ref[0].setRemoteDescription(
|
||||
RTCSessionDescription(sdp=data["publisherSdpAnswer"]["sdp"], type="answer")
|
||||
RTCSessionDescription(
|
||||
sdp=data["publisherSdpAnswer"]["sdp"], type="answer"
|
||||
)
|
||||
)
|
||||
await _ack(uid)
|
||||
continue
|
||||
@@ -382,7 +478,11 @@ async def _connect_peer(name: str, conn: dict, is_sender: bool, on_video_message
|
||||
if len(parts) < 8:
|
||||
continue
|
||||
try:
|
||||
tcp_type = parts[parts.index("tcptype") + 1] if "tcptype" in parts else None
|
||||
tcp_type = (
|
||||
parts[parts.index("tcptype") + 1]
|
||||
if "tcptype" in parts
|
||||
else None
|
||||
)
|
||||
ice = RTCIceCandidate(
|
||||
component=int(parts[1]),
|
||||
foundation=parts[0].replace("candidate:", ""),
|
||||
@@ -421,7 +521,13 @@ async def _connect_peer(name: str, conn: dict, is_sender: bool, on_video_message
|
||||
|
||||
async def run_poc() -> dict:
|
||||
print("\n--- Yandex Telemost VideoChannel PoC ---")
|
||||
results = {"server_ok": False, "client_ok": False, "sent": 0, "recv": 0, "errors": []}
|
||||
results = {
|
||||
"server_ok": False,
|
||||
"client_ok": False,
|
||||
"sent": 0,
|
||||
"recv": 0,
|
||||
"errors": [],
|
||||
}
|
||||
recv_events = [asyncio.Event() for _ in TEST_MESSAGES]
|
||||
last_message = [None]
|
||||
|
||||
@@ -439,7 +545,9 @@ async def run_poc() -> dict:
|
||||
recv_events[i].set()
|
||||
break
|
||||
|
||||
server = await _connect_peer("OlcRTC-Server", receiver_conn, is_sender=False, on_video_message=on_msg)
|
||||
server = await _connect_peer(
|
||||
"OlcRTC-Server", receiver_conn, is_sender=False, on_video_message=on_msg
|
||||
)
|
||||
client = await _connect_peer("OlcRTC-Client", sender_conn, is_sender=True)
|
||||
await asyncio.wait_for(server["subscriber_connected"].wait(), timeout=20.0)
|
||||
await asyncio.wait_for(client["publisher_connected"].wait(), timeout=20.0)
|
||||
@@ -498,11 +606,15 @@ async def run_poc() -> dict:
|
||||
|
||||
def print_results(res: dict):
|
||||
print("\n--- TEST RESULTS ---")
|
||||
print(f"Server: {':P' if res['server_ok'] else 'X'} / Client: {':P' if res['client_ok'] else 'X'}")
|
||||
print(
|
||||
f"Server: {':P' if res['server_ok'] else 'X'} / Client: {':P' if res['client_ok'] else 'X'}"
|
||||
)
|
||||
print(f"Messages: Sent {res['sent']} / Recv {res['recv']}")
|
||||
for err in res.get("errors", []):
|
||||
print(f" Error: {err}")
|
||||
print(f"\n{':P SUCCESS' if res['sent'] and res['sent'] == res['recv'] else 'X FAILED'}\n")
|
||||
print(
|
||||
f"\n{':P SUCCESS' if res['sent'] and res['sent'] == res['recv'] else 'X FAILED'}\n"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user