Add serverside type checking
This commit is contained in:
parent
7b5315d470
commit
d18550e247
5 changed files with 94 additions and 33 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -24,3 +24,6 @@ yarn-error.log*
|
||||||
|
|
||||||
.env.local
|
.env.local
|
||||||
.yarn/
|
.yarn/
|
||||||
|
|
||||||
|
.mypy_cache/
|
||||||
|
env/
|
31
Jenkinsfile
vendored
31
Jenkinsfile
vendored
|
@ -7,11 +7,37 @@ pipeline {
|
||||||
|
|
||||||
stages {
|
stages {
|
||||||
stage('Install dependencies') {
|
stage('Install dependencies') {
|
||||||
steps {
|
parallel {
|
||||||
sh 'CI=true npm_config_python=/usr/local/bin/python2.7 yarn --no-progress --non-interactive --skip-integrity-check --frozen-lockfile install'
|
stage('JavaScript') {
|
||||||
|
steps {
|
||||||
|
sh 'CI=true npm_config_python=/usr/local/bin/python2.7 yarn --no-progress --non-interactive --skip-integrity-check --frozen-lockfile install'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Python') {
|
||||||
|
steps {
|
||||||
|
sh '/usr/local/bin/python3.7 -m venv env'
|
||||||
|
sh 'source env/bin/activate'
|
||||||
|
sh 'pip install -r requirements.txt'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stage('Type checks') {
|
||||||
|
parallel {
|
||||||
|
stage('TypeScript') {
|
||||||
|
steps {
|
||||||
|
sh 'node_modules/.bin/tsc -p tsconfig.json --noEmit --extendedDiagnostics'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('MyPy') {
|
||||||
|
steps {
|
||||||
|
sh 'mypy server.py'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stage('Build and deploy to dev instance') {
|
stage('Build and deploy to dev instance') {
|
||||||
steps {
|
steps {
|
||||||
sh 'sed -i -e \'s/ury.org.uk\\/webstudio/ury.org.uk\\/webstudio-dev/\' package.json'
|
sh 'sed -i -e \'s/ury.org.uk\\/webstudio/ury.org.uk\\/webstudio-dev/\' package.json'
|
||||||
|
@ -21,6 +47,7 @@ pipeline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Build and deploy for production') {
|
stage('Build and deploy for production') {
|
||||||
when {
|
when {
|
||||||
branch 'production'
|
branch 'production'
|
||||||
|
|
2
mypy.ini
Normal file
2
mypy.ini
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[mypy]
|
||||||
|
strict = True
|
|
@ -5,9 +5,21 @@ cffi==1.14.0
|
||||||
crc32c==2.0
|
crc32c==2.0
|
||||||
cryptography==2.8
|
cryptography==2.8
|
||||||
JACK-Client==0.5.2
|
JACK-Client==0.5.2
|
||||||
|
jedi==0.15.2
|
||||||
|
mypy==0.770
|
||||||
|
mypy-extensions==0.4.3
|
||||||
netifaces==0.10.9
|
netifaces==0.10.9
|
||||||
|
parso==0.6.2
|
||||||
|
pluggy==0.13.1
|
||||||
pycparser==2.20
|
pycparser==2.20
|
||||||
pyee==7.0.1
|
pyee==7.0.1
|
||||||
pylibsrtp==0.6.6
|
pylibsrtp==0.6.6
|
||||||
|
pyls==0.1.6
|
||||||
|
pyls-mypy==0.1.8
|
||||||
|
python-jsonrpc-server==0.3.4
|
||||||
|
python-language-server==0.31.9
|
||||||
six==1.14.0
|
six==1.14.0
|
||||||
|
typed-ast==1.4.1
|
||||||
|
typing-extensions==3.7.4.2
|
||||||
|
ujson==1.35
|
||||||
websockets==8.1
|
websockets==8.1
|
||||||
|
|
79
server.py
79
server.py
|
@ -2,20 +2,21 @@ import asyncio
|
||||||
import websockets
|
import websockets
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import av
|
import av # type: ignore
|
||||||
import struct
|
import struct
|
||||||
from aiortc import MediaStreamTrack, RTCPeerConnection, RTCSessionDescription
|
from aiortc import MediaStreamTrack, RTCPeerConnection, RTCSessionDescription # type: ignore
|
||||||
from aiortc.contrib.media import MediaBlackhole, MediaPlayer
|
from aiortc.contrib.media import MediaBlackhole, MediaPlayer # type: ignore
|
||||||
import jack as Jack
|
import jack as Jack # type: ignore
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Optional, Any
|
||||||
|
|
||||||
|
|
||||||
file_contents_ex = re.compile(r"^ws=\d$")
|
file_contents_ex = re.compile(r"^ws=\d$")
|
||||||
|
|
||||||
|
|
||||||
def write_ob_status(status):
|
def write_ob_status(status: bool) -> None:
|
||||||
if not os.path.exists("/music/ob_state.conf"):
|
if not os.path.exists("/music/ob_state.conf"):
|
||||||
print("OB State file does not exist. Bailing.")
|
print("OB State file does not exist. Bailing.")
|
||||||
return
|
return
|
||||||
|
@ -26,19 +27,19 @@ def write_ob_status(status):
|
||||||
else:
|
else:
|
||||||
if content[len(content) - 1] != "\n":
|
if content[len(content) - 1] != "\n":
|
||||||
content += "\n"
|
content += "\n"
|
||||||
content += "ws=" + (1 if status else 0) + "\n"
|
content += "ws=" + str(1 if status else 0) + "\n"
|
||||||
fd.seek(0)
|
fd.seek(0)
|
||||||
fd.write(content)
|
fd.write(content)
|
||||||
fd.truncate()
|
fd.truncate()
|
||||||
|
|
||||||
|
|
||||||
@Jack.set_error_function
|
@Jack.set_error_function # type: ignore
|
||||||
def error(msg):
|
def error(msg: str) -> None:
|
||||||
print("Error:", msg)
|
print("Error:", msg)
|
||||||
|
|
||||||
|
|
||||||
@Jack.set_info_function
|
@Jack.set_info_function # type: ignore
|
||||||
def info(msg):
|
def info(msg: str) -> None:
|
||||||
print("Info:", msg)
|
print("Info:", msg)
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,8 +47,10 @@ jack = Jack.Client("webstudio")
|
||||||
out1 = jack.outports.register("out_0")
|
out1 = jack.outports.register("out_0")
|
||||||
out2 = jack.outports.register("out_1")
|
out2 = jack.outports.register("out_1")
|
||||||
|
|
||||||
|
transfer_buffer1: Any = None
|
||||||
|
transfer_buffer2: Any = None
|
||||||
|
|
||||||
def init_buffers():
|
def init_buffers() -> None:
|
||||||
global transfer_buffer1, transfer_buffer2
|
global transfer_buffer1, transfer_buffer2
|
||||||
transfer_buffer1 = Jack.RingBuffer(jack.samplerate * 10)
|
transfer_buffer1 = Jack.RingBuffer(jack.samplerate * 10)
|
||||||
transfer_buffer2 = Jack.RingBuffer(jack.samplerate * 10)
|
transfer_buffer2 = Jack.RingBuffer(jack.samplerate * 10)
|
||||||
|
@ -56,8 +59,8 @@ def init_buffers():
|
||||||
init_buffers()
|
init_buffers()
|
||||||
|
|
||||||
|
|
||||||
@jack.set_process_callback
|
@jack.set_process_callback # type: ignore
|
||||||
def process(frames):
|
def process(frames: int) -> None:
|
||||||
buf1 = out1.get_buffer()
|
buf1 = out1.get_buffer()
|
||||||
piece1 = transfer_buffer1.read(len(buf1))
|
piece1 = transfer_buffer1.read(len(buf1))
|
||||||
buf1[: len(piece1)] = piece1
|
buf1[: len(piece1)] = piece1
|
||||||
|
@ -67,15 +70,16 @@ def process(frames):
|
||||||
|
|
||||||
|
|
||||||
class JackSender(object):
|
class JackSender(object):
|
||||||
def __init__(self, track):
|
resampler: Any
|
||||||
|
def __init__(self, track: MediaStreamTrack) -> None:
|
||||||
self.track = track
|
self.track = track
|
||||||
self.resampler = None
|
self.resampler = None
|
||||||
self.ended = False
|
self.ended = False
|
||||||
|
|
||||||
def end():
|
def end(self) -> None:
|
||||||
self.ended = True
|
self.ended = True
|
||||||
|
|
||||||
async def process(self):
|
async def process(self) -> None:
|
||||||
while True:
|
while True:
|
||||||
if self.ended:
|
if self.ended:
|
||||||
break
|
break
|
||||||
|
@ -99,13 +103,18 @@ current_session = None
|
||||||
|
|
||||||
|
|
||||||
class Session(object):
|
class Session(object):
|
||||||
def __init__(self):
|
websocket: Optional[websockets.WebSocketServerProtocol]
|
||||||
|
sender: Optional[JackSender]
|
||||||
|
connection_state: Optional[str]
|
||||||
|
pc: Optional[Any]
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
self.websocket = None
|
self.websocket = None
|
||||||
self.sender = None
|
self.sender = None
|
||||||
self.pc = None
|
self.pc = None
|
||||||
self.connection_state = None
|
self.connection_state = None
|
||||||
|
|
||||||
async def end():
|
async def end(self) -> None:
|
||||||
print(self.connection_id, "going away")
|
print(self.connection_id, "going away")
|
||||||
if self.sender is not None:
|
if self.sender is not None:
|
||||||
self.sender.end()
|
self.sender.end()
|
||||||
|
@ -116,18 +125,21 @@ class Session(object):
|
||||||
if self.websocket is not None:
|
if self.websocket is not None:
|
||||||
await self.websocket.send(json.dumps({"kind": "REPLACED"}))
|
await self.websocket.send(json.dumps({"kind": "REPLACED"}))
|
||||||
|
|
||||||
def create_peerconnection(self):
|
def create_peerconnection(self) -> None:
|
||||||
self.pc = RTCPeerConnection()
|
self.pc = RTCPeerConnection()
|
||||||
|
assert self.pc is not None
|
||||||
|
|
||||||
@self.pc.on("signalingstatechange")
|
@self.pc.on("signalingstatechange") # type: ignore
|
||||||
async def on_signalingstatechange():
|
async def on_signalingstatechange() -> None:
|
||||||
|
assert self.pc is not None
|
||||||
print(
|
print(
|
||||||
self.connection_id,
|
self.connection_id,
|
||||||
"Signaling state is {}".format(self.pc.signalingState),
|
"Signaling state is {}".format(self.pc.signalingState),
|
||||||
)
|
)
|
||||||
|
|
||||||
@self.pc.on("iceconnectionstatechange")
|
@self.pc.on("iceconnectionstatechange") # type: ignore
|
||||||
async def on_iceconnectionstatechange():
|
async def on_iceconnectionstatechange() -> None:
|
||||||
|
assert self.pc is not None
|
||||||
print(
|
print(
|
||||||
self.connection_id,
|
self.connection_id,
|
||||||
"ICE connection state is {}".format(self.pc.iceConnectionState),
|
"ICE connection state is {}".format(self.pc.iceConnectionState),
|
||||||
|
@ -135,18 +147,19 @@ class Session(object):
|
||||||
if self.pc.iceConnectionState == "failed":
|
if self.pc.iceConnectionState == "failed":
|
||||||
await self.pc.close()
|
await self.pc.close()
|
||||||
self.pc = None
|
self.pc = None
|
||||||
await self.websocket.close(1008)
|
if self.websocket is not None:
|
||||||
|
await self.websocket.close(1008)
|
||||||
return
|
return
|
||||||
|
|
||||||
@self.pc.on("track")
|
@self.pc.on("track") # type: ignore
|
||||||
async def on_track(track):
|
async def on_track(track: MediaStreamTrack) -> None:
|
||||||
global current_session
|
global current_session
|
||||||
print(self.connection_id, "Received track")
|
print(self.connection_id, "Received track")
|
||||||
if track.kind == "audio":
|
if track.kind == "audio":
|
||||||
print(self.connection_id, "Adding to Jack.")
|
print(self.connection_id, "Adding to Jack.")
|
||||||
|
|
||||||
@track.on("ended")
|
@track.on("ended") # type: ignore
|
||||||
async def on_ended():
|
async def on_ended() -> None:
|
||||||
print(self.connection_id, "Track {} ended".format(track.kind))
|
print(self.connection_id, "Track {} ended".format(track.kind))
|
||||||
# TODO: this doesn't exactly handle reconnecting gracefully
|
# TODO: this doesn't exactly handle reconnecting gracefully
|
||||||
self.end()
|
self.end()
|
||||||
|
@ -158,16 +171,20 @@ class Session(object):
|
||||||
write_ob_status(True)
|
write_ob_status(True)
|
||||||
await self.sender.process()
|
await self.sender.process()
|
||||||
|
|
||||||
async def process_ice(self, message):
|
async def process_ice(self, message: Any) -> None:
|
||||||
if self.connection_state == "HELLO" and message["kind"] == "OFFER":
|
if self.connection_state == "HELLO" and message["kind"] == "OFFER":
|
||||||
offer = RTCSessionDescription(sdp=message["sdp"], type=message["type"])
|
offer = RTCSessionDescription(sdp=message["sdp"], type=message["type"])
|
||||||
print(self.connection_id, "Received offer")
|
print(self.connection_id, "Received offer")
|
||||||
|
|
||||||
self.create_peerconnection()
|
self.create_peerconnection()
|
||||||
|
assert self.pc is not None
|
||||||
|
|
||||||
await self.pc.setRemoteDescription(offer)
|
await self.pc.setRemoteDescription(offer)
|
||||||
|
|
||||||
answer = await self.pc.createAnswer()
|
answer = await self.pc.createAnswer()
|
||||||
await self.pc.setLocalDescription(answer)
|
await self.pc.setLocalDescription(answer)
|
||||||
|
|
||||||
|
assert self.websocket is not None
|
||||||
await self.websocket.send(
|
await self.websocket.send(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
|
@ -187,7 +204,7 @@ class Session(object):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def connect(self, websocket):
|
async def connect(self, websocket: websockets.WebSocketServerProtocol) -> None:
|
||||||
self.websocket = websocket
|
self.websocket = websocket
|
||||||
self.connection_id = uuid.uuid4()
|
self.connection_id = uuid.uuid4()
|
||||||
self.connection_state = "HELLO"
|
self.connection_state = "HELLO"
|
||||||
|
@ -210,7 +227,7 @@ class Session(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def serve(websocket, path):
|
async def serve(websocket: websockets.WebSocketServerProtocol, path: str) -> None:
|
||||||
if path == "/stream":
|
if path == "/stream":
|
||||||
session = Session()
|
session = Session()
|
||||||
await session.connect(websocket)
|
await session.connect(websocket)
|
||||||
|
|
Loading…
Reference in a new issue