WebStudio/shittyserver.py

454 lines
16 KiB
Python
Raw Normal View History

import asyncio
import configparser
import json
2020-04-05 09:53:03 +00:00
import os
import re
import sys
import uuid
from datetime import datetime, timezone
2020-04-10 11:03:08 +00:00
from types import TracebackType
from typing import Optional, Any, Type, Dict, List
import aiohttp
import av # type: ignore
import jack as Jack # type: ignore
import websockets
from aiortc import MediaStreamTrack, RTCPeerConnection, RTCSessionDescription # type: ignore
from aiortc.mediastreams import MediaStreamError # type: ignore
2021-04-28 08:06:02 +00:00
import sentry_sdk # type: ignore
2020-04-10 11:03:08 +00:00
config = configparser.RawConfigParser()
config.read("serverconfig.ini")
2020-04-10 11:03:08 +00:00
2021-04-25 08:48:37 +00:00
if config.get("sentry", "enable") == "True":
sentry_sdk.init(
config.get("sentry", "dsn"),
traces_sample_rate=1.0
)
2020-04-05 09:53:03 +00:00
file_contents_ex = re.compile(r"^ws=\d$")
2020-04-07 10:19:28 +00:00
def write_ob_status(status: bool) -> None:
2020-04-05 09:53:03 +00:00
if not os.path.exists("/music/ob_state.conf"):
print("OB State file does not exist. Bailing.")
return
2020-04-07 11:23:10 +00:00
with open("/music/ob_state.conf", "r+") as fd:
2020-04-05 09:53:03 +00:00
content = fd.read()
if "ws" in content:
content = re.sub(file_contents_ex, "ws=" + str(1 if status else 0), content)
else:
if len(content) > 0 and content[len(content) - 1] != "\n":
2020-04-05 09:53:03 +00:00
content += "\n"
2020-04-07 10:19:28 +00:00
content += "ws=" + str(1 if status else 0) + "\n"
2020-04-05 09:53:03 +00:00
fd.seek(0)
fd.write(content)
fd.truncate()
TurnCredentials = List[Dict[str, Any]]
def get_turn_credentials() -> TurnCredentials:
provider = config.get("shittyserver", "turn_provider")
if provider == "twilio":
from twilio.rest import Client # type: ignore
client = Client(config.get("twilio", "account_sid"), config.get("twilio", "auth_token"))
token = client.tokens.create()
# Twilio's typedef is wrong, reee
# noinspection PyTypeChecker
return token.ice_servers # type: ignore
elif provider == "hardcoded":
2020-05-09 14:00:56 +00:00
raise Exception("Don't use hardcoded anymore!")
else:
raise Exception("unknown provider " + provider)
@Jack.set_error_function # type: ignore
2020-04-07 10:19:28 +00:00
def error(msg: str) -> None:
print("Error:", msg)
@Jack.set_info_function # type: ignore
2020-04-07 10:19:28 +00:00
def info(msg: str) -> None:
print("Info:", msg)
jack = Jack.Client("webstudio")
out1 = jack.outports.register("out_0")
out2 = jack.outports.register("out_1")
2020-04-07 10:19:28 +00:00
transfer_buffer1: Any = None
transfer_buffer2: Any = None
2020-04-07 10:19:28 +00:00
def init_buffers() -> None:
global transfer_buffer1, transfer_buffer2
transfer_buffer1 = Jack.RingBuffer(jack.samplerate * 10)
transfer_buffer2 = Jack.RingBuffer(jack.samplerate * 10)
init_buffers()
@jack.set_process_callback # type: ignore
2020-04-07 10:19:28 +00:00
def process(frames: int) -> None:
buf1 = out1.get_buffer()
2020-04-12 20:45:29 +00:00
if transfer_buffer1.read_space == 0:
for i in range(len(buf1)):
buf1[i] = b'\x00'
else:
piece1 = transfer_buffer1.read(len(buf1))
buf1[: len(piece1)] = piece1
buf2 = out2.get_buffer()
2020-04-12 20:45:29 +00:00
if transfer_buffer2.read_space == 0:
for i in range(len(buf2)):
buf2[i] = b'\x00'
else:
piece2 = transfer_buffer2.read(len(buf2))
buf2[: len(piece2)] = piece2
active_sessions: Dict[str, "Session"] = {}
2020-04-12 14:49:50 +00:00
live_session: Optional["Session"] = None
async def notify_mattserver_about_sessions() -> None:
async with aiohttp.ClientSession() as session:
data: Dict[str, Dict[str, str]] = {}
for sid, sess in active_sessions.items():
data[sid] = sess.to_dict()
async with session.post(config.get("shittyserver", "notify_url"), json=data) as response:
2020-04-12 20:07:37 +00:00
print("Mattserver response", response)
class NotReadyException(BaseException):
pass
class Session(object):
2020-04-07 10:19:28 +00:00
websocket: Optional[websockets.WebSocketServerProtocol]
connection_state: Optional[str]
pc: Optional[Any]
connection_id: str
connected_at: datetime
2020-04-11 13:59:54 +00:00
lock: asyncio.Lock
2020-04-12 20:13:33 +00:00
running: bool
ended: bool
resampler: Optional[Any]
2020-04-07 10:19:28 +00:00
def __init__(self) -> None:
2020-04-02 18:26:58 +00:00
self.websocket = None
self.sender = None
self.pc = None
2020-04-12 20:19:13 +00:00
self.resampler = None
self.connection_state = None
self.connection_id = str(uuid.uuid4())
self.ended = False
2020-04-11 13:59:54 +00:00
self.lock = asyncio.Lock()
2020-04-12 20:13:33 +00:00
self.running = False
self.connected_at = datetime.now(timezone.utc)
def to_dict(self) -> Dict[str, str]:
return {
"connection_id": self.connection_id,
"connected_at": self.connected_at.strftime("%Y-%m-%dT%H:%M:%S%z")
}
async def activate(self) -> None:
print(self.connection_id, "Activating")
2020-04-12 20:13:33 +00:00
self.running = True
2020-04-13 11:08:28 +00:00
if self.websocket is not None:
await self.websocket.send(json.dumps({"kind": "ACTIVATED"}))
2020-04-02 18:26:58 +00:00
2020-04-12 21:21:38 +00:00
async def deactivate(self) -> None:
2020-04-12 21:22:12 +00:00
print(self.connection_id, "Deactivating")
2020-04-12 21:21:38 +00:00
self.running = False
2020-04-13 11:08:28 +00:00
if self.websocket is not None:
2020-04-14 09:28:43 +00:00
try:
await self.websocket.send(json.dumps({"kind": "DEACTIVATED"}))
2020-04-15 15:13:35 +00:00
except websockets.exceptions.ConnectionClosed:
2020-04-14 09:28:43 +00:00
print(self.connection_id, "not sending DEACTIVATED as it's already closed")
pass
2020-04-12 21:21:38 +00:00
2020-04-07 10:19:28 +00:00
async def end(self) -> None:
2020-04-12 15:38:43 +00:00
global active_sessions, live_session
2020-04-11 13:59:54 +00:00
async with self.lock:
if self.ended:
print(self.connection_id, "already over")
else:
print(self.connection_id, "going away")
2020-04-12 20:13:33 +00:00
self.ended = True
2020-04-12 21:21:38 +00:00
await self.deactivate()
2020-04-11 13:59:54 +00:00
if self.pc is not None:
await self.pc.close()
2020-04-12 14:49:50 +00:00
if (
self.websocket is not None
and self.websocket.state == websockets.protocol.State.OPEN
2020-04-12 14:49:50 +00:00
):
2020-04-13 14:18:13 +00:00
try:
await self.websocket.send(json.dumps({"kind": "DIED"}))
await self.websocket.close(1008)
2020-04-15 15:13:35 +00:00
except websockets.exceptions.ConnectionClosed:
2020-04-13 14:18:13 +00:00
print(self.connection_id, "socket already closed, no died message")
2020-04-11 13:59:54 +00:00
if self.connection_id in active_sessions:
2020-04-13 12:24:31 +00:00
print(self.connection_id, "removing from active_sessions")
2020-04-11 13:59:54 +00:00
del active_sessions[self.connection_id]
2020-04-13 12:24:31 +00:00
print("active_sessions now")
print(active_sessions)
2020-04-11 13:59:54 +00:00
if len(active_sessions) == 0:
write_ob_status(False)
else:
print(self.connection_id, "wasn't in active_sessions!")
2020-04-11 13:59:54 +00:00
if live_session == self:
live_session = None
await notify_mattserver_about_sessions()
print(self.connection_id, "bye bye")
2020-04-13 11:22:38 +00:00
self.ended = True
2020-04-07 10:19:28 +00:00
def create_peerconnection(self) -> None:
self.pc = RTCPeerConnection()
2020-04-07 10:19:28 +00:00
assert self.pc is not None
@self.pc.on("signalingstatechange") # type: ignore
2020-04-07 10:19:28 +00:00
async def on_signalingstatechange() -> None:
assert self.pc is not None
print(
self.connection_id,
"Signaling state is {}".format(self.pc.signalingState),
)
@self.pc.on("iceconnectionstatechange") # type: ignore
2020-04-07 10:19:28 +00:00
async def on_iceconnectionstatechange() -> None:
2020-04-07 11:23:10 +00:00
if self.pc is None:
print(
self.connection_id,
"ICE connection state change, but the PC is None!",
)
2020-04-07 11:23:10 +00:00
else:
print(
self.connection_id,
"ICE connection state is {}".format(self.pc.iceConnectionState),
)
if self.pc.iceConnectionState == "failed":
2020-04-16 11:07:25 +00:00
print(self.connection_id, "Ending due to ICE connection failure")
await self.end()
@self.pc.on("track") # type: ignore
2020-04-07 10:19:28 +00:00
async def on_track(track: MediaStreamTrack) -> None:
2020-04-12 20:13:33 +00:00
global live_session, transfer_buffer1, transfer_buffer2
2020-04-03 07:56:52 +00:00
print(self.connection_id, "Received track")
if track.kind == "audio":
2020-04-12 20:13:33 +00:00
print(self.connection_id, "It's audio.")
await notify_mattserver_about_sessions()
@track.on("ended") # type: ignore
2020-04-07 10:19:28 +00:00
async def on_ended() -> None:
2020-04-16 11:07:25 +00:00
print(self.connection_id, "Ending due to {} track end".format(track.kind))
await self.end()
2020-04-05 09:53:03 +00:00
write_ob_status(True)
2020-04-12 20:13:33 +00:00
while True:
try:
frame = await track.recv()
except MediaStreamError as e:
2020-04-16 11:07:25 +00:00
print(self.connection_id, "Ending due to MediaStreamError")
2020-04-13 11:25:00 +00:00
print(e)
await self.end()
2020-04-13 11:25:00 +00:00
break
2020-04-12 20:13:33 +00:00
if self.running:
# Right, depending on the format, we may need to do some fuckery.
# Jack expects all audio to be 32 bit floating point
# while PyAV may give us audio in any format
# (my testing has shown it to be signed 16-bit)
# We use PyAV to resample it into the right format
if self.resampler is None:
self.resampler = av.audio.resampler.AudioResampler(
format="fltp", layout="stereo", rate=jack.samplerate
)
frame.pts = None # DIRTY HACK
new_frame = self.resampler.resample(frame)
transfer_buffer1.write(new_frame.planes[0])
transfer_buffer2.write(new_frame.planes[1])
2020-04-07 10:19:28 +00:00
async def process_ice(self, message: Any) -> None:
if self.connection_state == "HELLO" and message["kind"] == "OFFER":
offer = RTCSessionDescription(sdp=message["sdp"], type=message["type"])
print(self.connection_id, "Received offer")
2020-04-07 10:19:28 +00:00
self.create_peerconnection()
2020-04-07 10:19:28 +00:00
assert self.pc is not None
await self.pc.setRemoteDescription(offer)
answer = await self.pc.createAnswer()
answer.sdp += "\na=fmtp:111 minptime=10;useinbandfec=0;maxaveragebitrate=262144;stereo=1;sprop-stereo=1;cbr=1"
await self.pc.setLocalDescription(answer)
2020-04-07 10:19:28 +00:00
assert self.websocket is not None
2020-04-07 09:45:00 +00:00
await self.websocket.send(
json.dumps(
{
"kind": "ANSWER",
"type": self.pc.localDescription.type,
"sdp": self.pc.localDescription.sdp,
}
)
)
self.connection_state = "ANSWER"
print(self.connection_id, "Sent answer")
else:
print(
self.connection_state,
"Incorrect kind {} for state {}".format(
message["kind"], self.connection_state
),
)
2020-04-07 10:19:28 +00:00
async def connect(self, websocket: websockets.WebSocketServerProtocol) -> None:
global active_sessions
active_sessions[self.connection_id] = self
self.websocket = websocket
self.connection_state = "HELLO"
print(self.connection_id, "Connected")
ice_config = get_turn_credentials()
print(self.connection_id, "Obtained ICE")
2021-04-25 08:48:37 +00:00
sentry_sdk.set_context("session", {"session_id": self.connection_id})
await websocket.send(
json.dumps({"kind": "HELLO", "connectionId": self.connection_id, "iceServers": ice_config})
)
try:
async for msg in websocket:
data = json.loads(msg)
if data["kind"] == "OFFER":
await self.process_ice(data)
elif data["kind"] == "TIME":
time = datetime.now().time()
2020-04-12 14:49:50 +00:00
await websocket.send(
json.dumps({"kind": "TIME", "time": str(time)})
)
else:
print(self.connection_id, "Unknown kind {}".format(data["kind"]))
await websocket.send(
json.dumps({"kind": "ERROR", "error": "unknown_kind"})
)
2020-04-15 15:13:35 +00:00
except websockets.exceptions.ConnectionClosed:
2020-04-16 11:07:25 +00:00
print(self.connection_id, "Ending due to dead WebSocket")
await self.end()
2020-04-07 10:19:28 +00:00
async def serve(websocket: websockets.WebSocketServerProtocol, path: str) -> None:
if path == "/stream":
session = Session()
await session.connect(websocket)
else:
pass
2020-04-12 14:49:50 +00:00
start_server = websockets.serve(
serve, host=None, port=int(config.get("shittyserver", "websocket_port"))
2020-04-12 14:49:50 +00:00
)
print("Shittyserver WS starting on port {}.".format(config.get("shittyserver", "websocket_port")))
async def telnet_server(
reader: asyncio.StreamReader, writer: asyncio.StreamWriter
) -> None:
2020-04-12 20:13:33 +00:00
global active_sessions, live_session
while True:
2020-04-12 21:04:52 +00:00
data = await reader.readline()
if not data:
break
2020-04-12 20:07:37 +00:00
try:
data_str = data.decode("utf-8")
except UnicodeDecodeError as e:
print(e)
continue
parts = data_str.rstrip().split(" ")
print(parts)
if parts[0] == "Q":
2020-04-11 13:59:54 +00:00
result: Dict[str, Dict[str, str]] = {}
for sid, sess in active_sessions.items():
2020-04-11 13:59:54 +00:00
result[sid] = sess.to_dict()
2020-04-12 14:49:50 +00:00
writer.write(
(
json.dumps(
{
"live": live_session.to_dict()
if live_session is not None
else None,
"active": result,
}
)
+ "\r\n"
2020-04-12 14:49:50 +00:00
).encode("utf-8")
)
elif parts[0] == "SEL":
sid = parts[1]
if sid == "NUL":
if live_session is not None:
2020-04-12 21:21:38 +00:00
await live_session.deactivate()
live_session = None
2020-04-12 21:32:44 +00:00
print("OKAY")
writer.write("OKAY\r\n".encode("utf-8"))
else:
2020-04-12 21:32:44 +00:00
print("WONT no_live_session")
writer.write("WONT no_live_session\r\n".encode("utf-8"))
else:
2020-04-12 17:48:05 +00:00
if sid not in active_sessions:
2020-04-12 21:32:44 +00:00
print("WONT no_such_session")
writer.write("WONT no_such_session\r\n".encode("utf-8"))
else:
2020-04-12 17:48:05 +00:00
session = active_sessions[sid]
if session is None:
2020-04-12 21:32:44 +00:00
print("OOPS no_such_session")
writer.write("OOPS no_such_session\r\n".encode("utf-8"))
2020-04-12 19:37:43 +00:00
elif live_session is not None and live_session.connection_id == sid:
2020-04-12 21:32:44 +00:00
print("WONT already_live")
2020-04-12 19:37:43 +00:00
writer.write("WONT already_live\r\n".encode("utf-8"))
2020-04-12 17:48:05 +00:00
else:
if live_session is not None:
2020-04-12 21:21:38 +00:00
await live_session.deactivate()
2020-04-12 20:13:33 +00:00
await session.activate()
2020-04-12 17:48:05 +00:00
live_session = session
2020-04-12 21:32:44 +00:00
print("OKAY")
2020-04-12 17:48:05 +00:00
writer.write("OKAY\r\n".encode("utf-8"))
else:
writer.write("WHAT\r\n".encode("utf-8"))
await writer.drain()
writer.close()
async def run_telnet_server() -> None:
server = await asyncio.start_server(
telnet_server, "localhost", int(config.get("shittyserver", "telnet_port"))
)
await server.serve_forever()
2020-04-03 07:56:52 +00:00
jack.activate()
2020-04-03 07:56:52 +00:00
print("Shittyserver TELNET starting on port {}".format(config.get("shittyserver", "telnet_port")))
asyncio.get_event_loop().run_until_complete(notify_mattserver_about_sessions())
2020-04-12 14:49:50 +00:00
asyncio.get_event_loop().run_until_complete(
asyncio.gather(start_server, run_telnet_server())
)
asyncio.get_event_loop().run_forever()