Initial apiserver

master
informatic 2018-06-17 20:30:47 +02:00
parent f8f022f909
commit bd7a4f3ce8
11 changed files with 217 additions and 61 deletions

View File

@ -1 +1 @@
ui
frontend

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

View File

@ -28,31 +28,3 @@ RUN apt install -y --no-install-recommends netcat bc
USER snowmix
CMD snowmix
#RUN apt install -y --no-install-recommends git python3
# build gstreamer 1.0 from cerbero source
# the build commands are split so that docker can resume in case of errors
#RUN git clone --depth 1 git://anongit.freedesktop.org/gstreamer/cerbero
# hack: to pass "-y" argument to apt-get install launched by "cerbero bootstrap"
#RUN sed -i 's/apt-get install/apt-get install -y/g' cerbero/cerbero/bootstrap/linux.py
#RUN apt install -y --no-install-recommends python3-setuptools sudo
#RUN cd cerbero; ./cerbero-uninstalled bootstrap
#RUN git config --global user.email "you@example.com" && git config --global user.name "Your Name"
#RUN cd cerbero; ./cerbero-uninstalled build \
# glib bison gstreamer-1.0
#RUN apt install -y --no-install-recommends python3-dev python-dev
#RUN cd cerbero; perl -pi -e 's/[^[:ascii:]]//g' packages/gstreamer-1.0/license.txt
#RUN cd cerbero; ./cerbero-uninstalled package gstreamer-1.0
#RUN cd cerbero; ./cerbero-uninstalled build \
# gst-plugins-base-1.0 gst-plugins-good-1.0
#
#RUN cd cerbero; ./cerbero-uninstalled build \
# gst-plugins-bad-1.0 gst-plugins-ugly-1.0
#
#RUN cd cerbero; ./cerbero-uninstalled build \
# gst-libav-1.0

23
api/Dockerfile Normal file
View File

@ -0,0 +1,23 @@
FROM ubuntu:xenial
RUN useradd -d /app app
RUN apt-get update && \
apt-get --no-install-recommends -y install python3 python3-pip python3-setuptools netbase
# We use that to cache last requirements version
COPY requirements.txt /app/requirements.txt
RUN pip3 install -r /app/requirements.txt
COPY run.sh /app/run.sh
WORKDIR /app
EXPOSE 5000
USER app
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
ENV FLASK_APP apiserver.py
ENV FLASK_ENV production
CMD python3 -u apiserver.py

88
api/apiserver.py Normal file
View File

@ -0,0 +1,88 @@
import time
import logging
import eventlet
import flask
from flask import g, jsonify, request
from flask_cors import CORS
from flask_socketio import SocketIO
import snowmix
logging.basicConfig(level=logging.INFO)
eventlet.monkey_patch()
app = flask.Flask('snowmix-apiserver')
cors = CORS(app)
socketio = SocketIO(app)
app.snowmix = snowmix.SnowmixClient('snowmix')
app.snowmix.sem = eventlet.Semaphore()
state = {}
def start_updater(name, func, interval=1.0):
def updater():
global state
while True:
try:
data = func()
if state.get(name) != data:
state[name] = data
socketio.emit(name, data, broadcast=True)
except:
logging.exception('Oops?')
time.sleep(interval)
socketio.start_background_task(updater)
start_updater('scenes', (lambda: [app.snowmix.scene_info(s) for s in app.snowmix.scene_list()]), 0.3)
start_updater('feeds', (lambda: app.snowmix.feed_list()), 3.0)
start_updater('images', (lambda: app.snowmix.image_list()), 5.0)
@app.route('/api/1/scenes/')
def scenes():
return jsonify([
app.snowmix.scene_info(s)
for s in app.snowmix.scene_list()
])
@app.route('/api/1/scenes/<int:scene_id>')
def scene_info(scene_id):
return jsonify(app.snowmix.scene_info(scene_id))
@app.route('/api/1/scenes/<int:scene_id>', methods=['PATCH'])
def scene_patch(scene_id):
req = request.json
if 'alpha' in req:
app.snowmix.scene_alpha(scene_id, req['alpha'], -1)
if 'text_alpha' in req:
app.snowmix.scene_alpha(scene_id, req['text_alpha'], -2)
if 'background_alpha' in req:
app.snowmix.scene_alpha(scene_id, req['text_alpha'], -3)
return 'OK'
@app.route('/api/1/feeds/')
def feeds():
return jsonify(app.snowmix.feed_list())
@app.route('/api/1/images/')
def images():
return jsonify(app.snowmix.image_list())
@socketio.on('connect')
def on_connect():
for k, v in state.items():
socketio.emit(k, v)
@socketio.on('command')
def command(cmd):
pass
if __name__ == "__main__":
socketio.run(app, host='0.0.0.0')

14
api/requirements.txt Normal file
View File

@ -0,0 +1,14 @@
click==6.7
eventlet==0.23.0
Flask==1.0.2
Flask-Cors==3.0.6
Flask-SocketIO==3.0.1
greenlet==0.4.13
itsdangerous==0.24
Jinja2==2.10
MarkupSafe==1.0
pyparsing==2.2.0
python-engineio==2.1.1
python-socketio==1.9.0
six==1.11.0
Werkzeug==0.14.1

9
api/run.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
set -e
#exec uwsgi --http-socket :5000 --processes 4 --master --plugin python \
# --lazy-apps --pythonpath /app --module apiserver:app \
# --touch-reload '/app/apiserver.py' --gevent 1000 --http-websockets
exec flask run --host 0.0.0.0 --port 5000 --debugger

View File

@ -1,12 +1,13 @@
import socket
import re
import logging
import threading
from pyparsing import nestedExpr, originalTextFor
tclparser = nestedExpr('{', '}')
scene_head_re = re.compile(r'Scene (\d+) active ([01]) WxH (\d+)x(\d+) at (\d+),(\d+) name (.*)')
scene_head_re = re.compile(r'Scene (\d+) active ([01])"? WxH (\d+)x(\d+) at (\d+),(\d+) name (.*)')
scene_back_re = re.compile(r'- back : (\w+) (\d+) WxH (\d+)x(\d+) at ([\d.]+),([\d.]+) shape (\d+) place (\d+)')
scene_frame_re = re.compile(r'- frame (\d+) active (\d+) : (\d+)x(\d+) at (\d+),(\d+) source ([\w-]+),([\w-]+) id ([\d-]+),([\d-]+) shape (\d+),(\d+) place (\d+),(\d+)')
image_list_re = re.compile('^image load (\d+) <(.*)> (\d+)x(\d+) bit depth (\d+) type (.*) seqno (\d+)$')
@ -15,9 +16,20 @@ feed_list_re = re.compile(r'^feed (\d+) : (.*) (.*) (.*) (\d+)x(\d+) (\d+),(\d+)
def parse_tcl(data):
return tclparser.parseString('{%s}' % (data,))[0].asList()
def cast(l, t, kl, default=None):
for k in kl:
try:
l[k] = t(l[k])
except:
logging.exception('Cast to %r failed on %r -> %r', t, k, l)
l[k] = default
return l
class SnowmixClient(object):
def __init__(self, host='127.0.0.1', port=9999):
self.connect(host, port)
self.sem = threading.Semaphore()
def connect(self, host, port):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@ -28,68 +40,81 @@ class SnowmixClient(object):
def flush_input(self):
self.sock.setblocking(0)
try:
self.fd.read()
except:
pass
self.sock.setblocking(1)
self.sock.settimeout(1.0)
def call(self, command, expect='MSG:'):
self.flush_input()
with self.sem:
self.flush_input()
self.fd.write(command + '\r\n')
self.fd.flush()
while True:
line = self.fd.readline()
self.fd.write(command + '\r\n')
self.fd.flush()
while True:
line = self.fd.readline()
print('< ' + line.strip())
if expect and line.startswith(expect):
line = line[len(expect):].strip()
if not line:
return
if expect and line.startswith(expect):
line = line[len(expect):].strip()
if not line:
return
yield line
yield line
def tcl(self, code):
return parse_tcl(next(
self.call('tcl eval snowmix message [%s]' % code), ''))
def scene_list(self):
return [int(s) for s in next(self.call('tcl eval ScenesList')).split(' ')[2:]]
def scene_info(self, scene_id):
lines = list(self.call('tcl eval SceneList %d' % scene_id))
s = {'frames': {}}
_, s['active'], s['width'], s['height'], s['x'], s['y'], s['name'] = \
s['id'], s['active'], s['width'], s['height'], s['x'], s['y'], s['name'] = \
scene_head_re.match(lines[0]).groups()
bg_type, bg_id, _, _, _, _ ,_ ,_ = \
scene_back_re.match(lines[1]).groups()
for l in lines[2:]:
pos = {}
front = {}
back = {}
front = {'source': [None, None]}
back = {'source': [None, None]}
frame = {'position': pos, 'back': back, 'front': front}
frame['id'], frame['active'], pos['width'], pos['height'], \
pos['x'], pos['y'], front['type'], back['type'], \
front['id'], back['id'], _, _, _, _ = \
pos['x'], pos['y'], front['source'][0], back['source'][0], \
front['source'][1], back['source'][1], _, _, _, _ = \
scene_frame_re.match(l).groups()
s['frames'][frame['id']] = frame
frame['active'] = frame['active'] == '1'
cast(pos, int, ['width', 'height', 'x', 'y'])
back['source'] = ':'.join(back['source'])
front['source'] = ':'.join(front['source'])
alpha = self.tcl('SceneAlpha %d' % scene_id)
s['alpha'] = float(alpha[0][0])
s['background_alpha'] = float(alpha[0][1])
s['text_alpha'] = float(alpha[0][2])
alpha = c.tcl('SceneAlpha %d' % scene_id)
s['alpha'] = alpha[0][0]
s['background_alpha'] = alpha[0][1]
s['text_alpha'] = alpha[0][2]
print(alpha)
for k, front_alpha, back_alpha in alpha[1:]:
s['frames'][k]['front']['alpha'] = front_alpha
s['frames'][k]['back']['alpha'] = back_alpha
s['frames'][k]['front']['alpha'] = float(front_alpha)
s['frames'][k]['back']['alpha'] = float(back_alpha)
alphalink = c.tcl('SceneAlphaLink %d' % scene_id)
s['alpha_background_link'] = alphalink[0][0]
s['alpha_text_link'] = alphalink[0][1]
alphalink = self.tcl('SceneAlphaLink %d' % scene_id)
s['alpha_background_link'] = alphalink[0][0] == '1'
s['alpha_text_link'] = alphalink[0][1] == '1'
for k, link in alphalink[1:]:
s['frames'][k]['alpha_link'] = link
s['frames'][k]['alpha_link'] = link == '1'
cast(s, int, ['active', 'width', 'height', 'x', 'y',
'alpha_background_link', 'alpha_text_link'])
return s
def image_list(self):
@ -99,6 +124,8 @@ class SnowmixClient(object):
img['id'], img['source'], img['width'], img['height'], \
img['bit'], img['type'], img['seqno'] = \
image_list_re.match(l).groups()
cast(img, int, ['width', 'height', 'bit', 'seqno'])
images.append(img)
return images
@ -112,10 +139,22 @@ class SnowmixClient(object):
f['offsetx'], f['offsety'], f['fifo1'], f['fifo2'], \
f['good'], f['missed'], f['dropped'], f['name'] = \
feed_list_re.match(l).groups()
cast(f, int, ['width', 'height', 'cutstartx', 'cutstarty',
'cutw', 'cuth', 'offsetx', 'offsety',
'fifo1', 'fifo2', 'good', 'missed', 'dropped'])
feeds.append(f)
return feeds
def scene_cut(self, scene_id):
self.tcl('SceneSetState %d 1 0' % scene_id)
def scene_fade(self, scene_id):
self.tcl('SceneSetState %d 1 1' % scene_id)
def scene_alpha(self, scene_id, alpha, frame_id=-1):
self.tcl('SceneAlpha %d %d %f' % (scene_id, frame_id, alpha))
if __name__ == "__main__":
c = SnowmixClient('127.0.0.1')

View File

@ -76,11 +76,11 @@ cert_key = /opt/janus/share/janus/certs/mycert.key
; be used for gathering candidates, and enable or disable the
; internal libnice debugging, if needed.
[nat]
;stun_server = stun.voip.eutelia.it
;stun_port = 3478
stun_server = stun.voip.eutelia.it
stun_port = 3478
stun_server = stun.l.google.com
stun_port = 19302
;stun_server = stun.l.google.com
;stun_port = 19302
nice_debug = false
;ice_lite = true
;ice_tcp = true

View File

@ -38,7 +38,7 @@
; to the plugins/streams folder.
[general]
;admin_key = supersecret ; If set, mountpoints can be created via API
admin_key = alkjsdnfljaLIUDHljfndlkjsa ; If set, mountpoints can be created via API
; only if this key is provided in the request
[gstreamer-sample]
@ -91,6 +91,7 @@ video = yes
videoport = 8004
videopt = 96
videortpmap = H264/90000
videobufferkf = yes
videofmtp = profile-level-id=42e01f\;packetization-mode=1
#videofmtp = profile-level-id=64001f\;packetization-mode=1

View File

@ -45,5 +45,14 @@ services:
- ./html:/usr/share/nginx/html:ro
ports:
- 8080:80
api:
build: api
restart: unless-stopped
ports:
- 5000:5000
volumes:
- ./api:/app
volumes:
sockets: