diff --git a/requirements.txt b/requirements.txt index 232be3b..80ef696 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,14 +29,18 @@ parso==0.6.2 pluggy==0.13.1 pycparser==2.20 pyee==7.0.1 +PyJWT==1.7.1 pylibsrtp==0.6.6 pyls==0.1.6 pyls-mypy==0.1.8 +python-dateutil==2.8.1 python-jsonrpc-server==0.3.4 python-language-server==0.31.9 +pytz==2019.3 raygun4py==4.3.0 requests==2.23.0 six==1.14.0 +twilio==6.38.1 typed-ast==1.4.1 typing-extensions==3.7.4.2 ujson==1.35 diff --git a/serverconfig.ini.example b/serverconfig.ini.example index d3a2ce3..5abfed2 100644 --- a/serverconfig.ini.example +++ b/serverconfig.ini.example @@ -2,11 +2,19 @@ key = CHANGEME enable = False +[twilio] +account_sid = CHANGEME +auth_token = CHANGEME + +[time] +local_timezone = Europe/London + [shittyserver] notify_url = https://example.com websocket_port = 8079 telnet_port = 8078 +turn_provider = twilio [stateserver] myradio_key = CHANGEME -sustainer_autonews = True \ No newline at end of file +sustainer_autonews = True diff --git a/shittyserver.py b/shittyserver.py index 03acf6f..15834da 100644 --- a/shittyserver.py +++ b/shittyserver.py @@ -7,7 +7,7 @@ import sys import uuid from datetime import datetime, timezone from types import TracebackType -from typing import Optional, Any, Type, Dict +from typing import Optional, Any, Type, Dict, List import aiohttp import av # type: ignore @@ -21,18 +21,17 @@ config = configparser.RawConfigParser() config.read("serverconfig.ini") if config.get("raygun", "enable") == "True": - def handle_exception( - exc_type: Type[BaseException], - exc_value: BaseException, - exc_traceback: TracebackType, + exc_type: Type[BaseException], + exc_value: BaseException, + exc_traceback: TracebackType, ) -> None: sys.__excepthook__(exc_type, exc_value, exc_traceback) cl = raygunprovider.RaygunSender(config.get("raygun", "key")) cl.send_exception(exc_info=(exc_type, exc_value, exc_traceback)) - sys.excepthook = handle_exception + sys.excepthook = handle_exception file_contents_ex = re.compile(r"^ws=\d$") @@ -54,6 +53,42 @@ def write_ob_status(status: bool) -> None: 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": + return [ + { + "urls": ["stun:eu-turn4.xirsys.com"], + }, + { + "username": + "h42bRBHL2GtRTiQRoXN8GCG-PFYMl4Acel6EQ9xINBWdTpoZyBEGyCcJBCtT3iINAAAAAF5_NJptYXJrc3BvbGFrb3Zz", + "credential": "17e834fa-70e7-11ea-a66c-faa4ea02ad5c", + "urls": [ + "turn:eu-turn4.xirsys.com:80?transport=udp", + "turn:eu-turn4.xirsys.com:3478?transport=udp", + "turn:eu-turn4.xirsys.com:80?transport=tcp", + "turn:eu-turn4.xirsys.com:3478?transport=tcp", + "turns:eu-turn4.xirsys.com:443?transport=tcp", + "turns:eu-turn4.xirsys.com:5349?transport=tcp", + ], + }, + ] + else: + raise Exception("unknown provider " + provider) + + @Jack.set_error_function # type: ignore def error(msg: str) -> None: print("Error:", msg) @@ -177,8 +212,8 @@ class Session(object): await self.pc.close() if ( - self.websocket is not None - and self.websocket.state == websockets.protocol.State.OPEN + self.websocket is not None + and self.websocket.state == websockets.protocol.State.OPEN ): try: await self.websocket.send(json.dumps({"kind": "DIED"})) @@ -311,9 +346,11 @@ class Session(object): self.websocket = websocket self.connection_state = "HELLO" print(self.connection_id, "Connected") + ice_config = get_turn_credentials() + print(self.connection_id, "Obtained ICE") # TODO Raygun user ID await websocket.send( - json.dumps({"kind": "HELLO", "connectionId": self.connection_id}) + json.dumps({"kind": "HELLO", "connectionId": self.connection_id, "iceServers": ice_config}) ) try: @@ -353,7 +390,7 @@ print("Shittyserver WS starting on port {}.".format(config.get("shittyserver", " async def telnet_server( - reader: asyncio.StreamReader, writer: asyncio.StreamWriter + reader: asyncio.StreamReader, writer: asyncio.StreamWriter ) -> None: global active_sessions, live_session while True: @@ -374,15 +411,15 @@ async def telnet_server( result[sid] = sess.to_dict() writer.write( ( - json.dumps( - { - "live": live_session.to_dict() - if live_session is not None - else None, - "active": result, - } - ) - + "\r\n" + json.dumps( + { + "live": live_session.to_dict() + if live_session is not None + else None, + "active": result, + } + ) + + "\r\n" ).encode("utf-8") ) diff --git a/src/broadcast/rtc_streamer.ts b/src/broadcast/rtc_streamer.ts index 68b1c2b..2029169 100644 --- a/src/broadcast/rtc_streamer.ts +++ b/src/broadcast/rtc_streamer.ts @@ -29,46 +29,6 @@ export class WebRTCStreamer extends Streamer { async start(): Promise { console.log("RTCStreamer start"); - this.pc = new RTCPeerConnection({ - iceServers: [ - { - urls: [ - "stun:stun.l.google.com:19302", - "stun:stun1.l.google.com:19302", - "stun:stun2.l.google.com:19302", - "stun:stun3.l.google.com:19302", - "stun:stun4.l.google.com:19302", - ], - }, - { - urls: ["stun:eu-turn4.xirsys.com"], - }, - { - username: - "h42bRBHL2GtRTiQRoXN8GCG-PFYMl4Acel6EQ9xINBWdTpoZyBEGyCcJBCtT3iINAAAAAF5_NJptYXJrc3BvbGFrb3Zz", - credential: "17e834fa-70e7-11ea-a66c-faa4ea02ad5c", - urls: [ - "turn:eu-turn4.xirsys.com:80?transport=udp", - "turn:eu-turn4.xirsys.com:3478?transport=udp", - "turn:eu-turn4.xirsys.com:80?transport=tcp", - "turn:eu-turn4.xirsys.com:3478?transport=tcp", - "turns:eu-turn4.xirsys.com:443?transport=tcp", - "turns:eu-turn4.xirsys.com:5349?transport=tcp", - ], - }, - ], - }); - this.pc.oniceconnectionstatechange = (e) => { - if (!this.pc) { - throw new Error( - "Received ICEConnectionStateChange but PC was null?????" - ); - } - console.log("ICE Connection state change: " + this.pc.iceConnectionState); - this.onStateChange(this.mapStateToConnectionState()); - }; - this.stream.getAudioTracks().forEach((track) => this.pc!.addTrack(track)); - this.addConnectionStateListener((state) => { if (state === "CONNECTED") { this.newsInterval = later.setInterval( @@ -83,7 +43,6 @@ export class WebRTCStreamer extends Streamer { } }); - console.log("PC created"); this.ws = new WebSocket(process.env.REACT_APP_WS_URL!); this.ws.onopen = (e) => { console.log("WS open"); @@ -156,6 +115,9 @@ export class WebRTCStreamer extends Streamer { if (this.state !== "HELLO") { this.ws!.close(); } + + this.createPeerConnection(data.iceServers); + if (!this.pc) { throw new Error( "Tried to do websocket fuckery with a null PeerConnection!" @@ -225,6 +187,34 @@ export class WebRTCStreamer extends Streamer { } } + createPeerConnection(iceServers: RTCIceServer[]) { + this.pc = new RTCPeerConnection({ + iceServers: [ + { + urls: [ + "stun:stun.l.google.com:19302", + "stun:stun1.l.google.com:19302", + "stun:stun2.l.google.com:19302", + "stun:stun3.l.google.com:19302", + "stun:stun4.l.google.com:19302", + ], + }, + ...iceServers, + ], + }); + this.pc.oniceconnectionstatechange = (e) => { + if (!this.pc) { + throw new Error( + "Received ICEConnectionStateChange but PC was null?????" + ); + } + console.log("ICE Connection state change: " + this.pc.iceConnectionState); + this.onStateChange(this.mapStateToConnectionState()); + }; + this.stream.getAudioTracks().forEach((track) => this.pc!.addTrack(track)); + console.log("PC created"); + } + async getStatistics() { if (this.pc) { return await this.pc.getStats();