euler314 commited on
Commit
66a180a
·
verified ·
1 Parent(s): ebc517d

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +12 -0
  2. README.md +27 -6
  3. app.py +109 -0
  4. requirements.txt +2 -0
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY app.py .
9
+
10
+ EXPOSE 7860
11
+
12
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,11 +1,32 @@
1
  ---
2
- title: TCP
3
- emoji: 📉
4
- colorFrom: yellow
5
- colorTo: purple
6
  sdk: docker
 
7
  pinned: false
8
- short_description: p2p(TCP)
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Sound Transport Relay
3
+ emoji: "🔊"
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
 
9
  ---
10
 
11
+ # Sound Transport Relay (HF)
12
+
13
+ This Space runs a simple WebSocket relay at `/ws`. Clients should connect with:
14
+
15
+ ```json
16
+ {"role":"sender","code":"123456789"}
17
+ ```
18
+
19
+ or
20
+
21
+ ```json
22
+ {"role":"receiver","code":"123456789"}
23
+ ```
24
+
25
+ After both sides connect with the same code, the relay forwards all text/binary
26
+ messages both directions.
27
+
28
+ Notes
29
+ - HF free Spaces can sleep when idle; the first connection after sleep will have
30
+ a cold-start delay.
31
+ - This relay does not authenticate or validate passwords. Your app should handle
32
+ authentication and encryption end-to-end.
app.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import asyncio
3
+ import json
4
+ import re
5
+
6
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
7
+ from fastapi.responses import PlainTextResponse
8
+
9
+ app = FastAPI()
10
+ CODE_RE = re.compile(r"^\d{9}$")
11
+
12
+ # pending[code] = {"sender": ws|None, "receiver": ws|None, "event": asyncio.Event()}
13
+ PENDING = {}
14
+ PENDING_LOCK = asyncio.Lock()
15
+
16
+
17
+ @app.get("/")
18
+ async def root():
19
+ return PlainTextResponse("ok")
20
+
21
+
22
+ async def register(code, role, ws):
23
+ async with PENDING_LOCK:
24
+ entry = PENDING.get(code)
25
+ if entry is None:
26
+ entry = {"sender": None, "receiver": None, "event": asyncio.Event()}
27
+ PENDING[code] = entry
28
+ if entry.get(role) is not None:
29
+ return None, "role already connected"
30
+ entry[role] = ws
31
+ if entry.get("sender") is not None and entry.get("receiver") is not None:
32
+ entry["event"].set()
33
+ return entry, None
34
+
35
+
36
+ async def unregister(code, role, ws):
37
+ async with PENDING_LOCK:
38
+ entry = PENDING.get(code)
39
+ if entry is None:
40
+ return
41
+ if entry.get(role) is ws:
42
+ entry[role] = None
43
+ if entry.get("sender") is None and entry.get("receiver") is None:
44
+ PENDING.pop(code, None)
45
+
46
+
47
+ async def forward(src, dst):
48
+ try:
49
+ while True:
50
+ msg = await src.receive()
51
+ if msg.get("type") == "websocket.disconnect":
52
+ break
53
+ if msg.get("bytes") is not None:
54
+ await dst.send_bytes(msg["bytes"])
55
+ elif msg.get("text") is not None:
56
+ await dst.send_text(msg["text"])
57
+ except WebSocketDisconnect:
58
+ pass
59
+ except Exception:
60
+ pass
61
+
62
+
63
+ @app.websocket("/ws")
64
+ async def ws_relay(ws: WebSocket):
65
+ await ws.accept()
66
+ code = None
67
+ role = None
68
+ entry = None
69
+ try:
70
+ raw = await ws.receive_text()
71
+ try:
72
+ payload = json.loads(raw)
73
+ except json.JSONDecodeError:
74
+ await ws.send_text(json.dumps({"error": "invalid json"}))
75
+ return
76
+ role = payload.get("role")
77
+ code = payload.get("code")
78
+ if role not in ("sender", "receiver"):
79
+ await ws.send_text(json.dumps({"error": "invalid role"}))
80
+ return
81
+ if not isinstance(code, str) or not CODE_RE.match(code):
82
+ await ws.send_text(json.dumps({"error": "invalid code"}))
83
+ return
84
+ entry, err = await register(code, role, ws)
85
+ if err:
86
+ await ws.send_text(json.dumps({"error": err}))
87
+ return
88
+ await ws.send_text(json.dumps({"status": "waiting"}))
89
+ await entry["event"].wait()
90
+ other = entry["receiver"] if role == "sender" else entry["sender"]
91
+ if other is None:
92
+ await ws.send_text(json.dumps({"error": "peer missing"}))
93
+ return
94
+ await ws.send_text(json.dumps({"status": "paired"}))
95
+ await other.send_text(json.dumps({"status": "paired"}))
96
+ task_a = asyncio.create_task(forward(ws, other))
97
+ task_b = asyncio.create_task(forward(other, ws))
98
+ done, pending = await asyncio.wait(
99
+ {task_a, task_b}, return_when=asyncio.FIRST_COMPLETED
100
+ )
101
+ for task in pending:
102
+ task.cancel()
103
+ finally:
104
+ if code and role:
105
+ await unregister(code, role, ws)
106
+ try:
107
+ await ws.close()
108
+ except Exception:
109
+ pass
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fastapi==0.110.0
2
+ uvicorn[standard]==0.29.0