Initial apiserver
parent
f8f022f909
commit
bd7a4f3ce8
|
@ -1 +1 @@
|
|||
ui
|
||||
frontend
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
node_modules
|
28
Dockerfile
28
Dockerfile
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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')
|
|
@ -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
|
|
@ -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
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue