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
|
USER snowmix
|
||||||
CMD 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 socket
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
from pyparsing import nestedExpr, originalTextFor
|
from pyparsing import nestedExpr, originalTextFor
|
||||||
|
|
||||||
tclparser = nestedExpr('{', '}')
|
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_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+)')
|
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+)$')
|
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):
|
def parse_tcl(data):
|
||||||
return tclparser.parseString('{%s}' % (data,))[0].asList()
|
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):
|
class SnowmixClient(object):
|
||||||
def __init__(self, host='127.0.0.1', port=9999):
|
def __init__(self, host='127.0.0.1', port=9999):
|
||||||
self.connect(host, port)
|
self.connect(host, port)
|
||||||
|
self.sem = threading.Semaphore()
|
||||||
|
|
||||||
def connect(self, host, port):
|
def connect(self, host, port):
|
||||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
@ -28,68 +40,81 @@ class SnowmixClient(object):
|
||||||
|
|
||||||
def flush_input(self):
|
def flush_input(self):
|
||||||
self.sock.setblocking(0)
|
self.sock.setblocking(0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.fd.read()
|
self.fd.read()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.sock.setblocking(1)
|
self.sock.setblocking(1)
|
||||||
self.sock.settimeout(1.0)
|
self.sock.settimeout(1.0)
|
||||||
|
|
||||||
def call(self, command, expect='MSG:'):
|
def call(self, command, expect='MSG:'):
|
||||||
self.flush_input()
|
with self.sem:
|
||||||
|
self.flush_input()
|
||||||
|
|
||||||
self.fd.write(command + '\r\n')
|
self.fd.write(command + '\r\n')
|
||||||
self.fd.flush()
|
self.fd.flush()
|
||||||
while True:
|
while True:
|
||||||
line = self.fd.readline()
|
line = self.fd.readline()
|
||||||
|
print('< ' + line.strip())
|
||||||
|
|
||||||
if expect and line.startswith(expect):
|
if expect and line.startswith(expect):
|
||||||
line = line[len(expect):].strip()
|
line = line[len(expect):].strip()
|
||||||
if not line:
|
if not line:
|
||||||
return
|
return
|
||||||
|
|
||||||
yield line
|
yield line
|
||||||
|
|
||||||
def tcl(self, code):
|
def tcl(self, code):
|
||||||
return parse_tcl(next(
|
return parse_tcl(next(
|
||||||
self.call('tcl eval snowmix message [%s]' % code), ''))
|
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):
|
def scene_info(self, scene_id):
|
||||||
lines = list(self.call('tcl eval SceneList %d' % scene_id))
|
lines = list(self.call('tcl eval SceneList %d' % scene_id))
|
||||||
|
|
||||||
s = {'frames': {}}
|
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()
|
scene_head_re.match(lines[0]).groups()
|
||||||
bg_type, bg_id, _, _, _, _ ,_ ,_ = \
|
bg_type, bg_id, _, _, _, _ ,_ ,_ = \
|
||||||
scene_back_re.match(lines[1]).groups()
|
scene_back_re.match(lines[1]).groups()
|
||||||
for l in lines[2:]:
|
for l in lines[2:]:
|
||||||
pos = {}
|
pos = {}
|
||||||
front = {}
|
front = {'source': [None, None]}
|
||||||
back = {}
|
back = {'source': [None, None]}
|
||||||
frame = {'position': pos, 'back': back, 'front': front}
|
frame = {'position': pos, 'back': back, 'front': front}
|
||||||
|
|
||||||
frame['id'], frame['active'], pos['width'], pos['height'], \
|
frame['id'], frame['active'], pos['width'], pos['height'], \
|
||||||
pos['x'], pos['y'], front['type'], back['type'], \
|
pos['x'], pos['y'], front['source'][0], back['source'][0], \
|
||||||
front['id'], back['id'], _, _, _, _ = \
|
front['source'][1], back['source'][1], _, _, _, _ = \
|
||||||
scene_frame_re.match(l).groups()
|
scene_frame_re.match(l).groups()
|
||||||
s['frames'][frame['id']] = frame
|
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:]:
|
for k, front_alpha, back_alpha in alpha[1:]:
|
||||||
s['frames'][k]['front']['alpha'] = front_alpha
|
s['frames'][k]['front']['alpha'] = float(front_alpha)
|
||||||
s['frames'][k]['back']['alpha'] = back_alpha
|
s['frames'][k]['back']['alpha'] = float(back_alpha)
|
||||||
|
|
||||||
alphalink = c.tcl('SceneAlphaLink %d' % scene_id)
|
alphalink = self.tcl('SceneAlphaLink %d' % scene_id)
|
||||||
s['alpha_background_link'] = alphalink[0][0]
|
s['alpha_background_link'] = alphalink[0][0] == '1'
|
||||||
s['alpha_text_link'] = alphalink[0][1]
|
s['alpha_text_link'] = alphalink[0][1] == '1'
|
||||||
for k, link in alphalink[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
|
return s
|
||||||
|
|
||||||
def image_list(self):
|
def image_list(self):
|
||||||
|
@ -99,6 +124,8 @@ class SnowmixClient(object):
|
||||||
img['id'], img['source'], img['width'], img['height'], \
|
img['id'], img['source'], img['width'], img['height'], \
|
||||||
img['bit'], img['type'], img['seqno'] = \
|
img['bit'], img['type'], img['seqno'] = \
|
||||||
image_list_re.match(l).groups()
|
image_list_re.match(l).groups()
|
||||||
|
cast(img, int, ['width', 'height', 'bit', 'seqno'])
|
||||||
|
|
||||||
images.append(img)
|
images.append(img)
|
||||||
return images
|
return images
|
||||||
|
|
||||||
|
@ -112,10 +139,22 @@ class SnowmixClient(object):
|
||||||
f['offsetx'], f['offsety'], f['fifo1'], f['fifo2'], \
|
f['offsetx'], f['offsety'], f['fifo1'], f['fifo2'], \
|
||||||
f['good'], f['missed'], f['dropped'], f['name'] = \
|
f['good'], f['missed'], f['dropped'], f['name'] = \
|
||||||
feed_list_re.match(l).groups()
|
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)
|
feeds.append(f)
|
||||||
|
|
||||||
return feeds
|
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__":
|
if __name__ == "__main__":
|
||||||
c = SnowmixClient('127.0.0.1')
|
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
|
; be used for gathering candidates, and enable or disable the
|
||||||
; internal libnice debugging, if needed.
|
; internal libnice debugging, if needed.
|
||||||
[nat]
|
[nat]
|
||||||
;stun_server = stun.voip.eutelia.it
|
stun_server = stun.voip.eutelia.it
|
||||||
;stun_port = 3478
|
stun_port = 3478
|
||||||
|
|
||||||
stun_server = stun.l.google.com
|
;stun_server = stun.l.google.com
|
||||||
stun_port = 19302
|
;stun_port = 19302
|
||||||
nice_debug = false
|
nice_debug = false
|
||||||
;ice_lite = true
|
;ice_lite = true
|
||||||
;ice_tcp = true
|
;ice_tcp = true
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
; to the plugins/streams folder.
|
; to the plugins/streams folder.
|
||||||
|
|
||||||
[general]
|
[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
|
; only if this key is provided in the request
|
||||||
|
|
||||||
[gstreamer-sample]
|
[gstreamer-sample]
|
||||||
|
@ -91,6 +91,7 @@ video = yes
|
||||||
videoport = 8004
|
videoport = 8004
|
||||||
videopt = 96
|
videopt = 96
|
||||||
videortpmap = H264/90000
|
videortpmap = H264/90000
|
||||||
|
videobufferkf = yes
|
||||||
videofmtp = profile-level-id=42e01f\;packetization-mode=1
|
videofmtp = profile-level-id=42e01f\;packetization-mode=1
|
||||||
#videofmtp = profile-level-id=64001f\;packetization-mode=1
|
#videofmtp = profile-level-id=64001f\;packetization-mode=1
|
||||||
|
|
||||||
|
|
|
@ -45,5 +45,14 @@ services:
|
||||||
- ./html:/usr/share/nginx/html:ro
|
- ./html:/usr/share/nginx/html:ro
|
||||||
ports:
|
ports:
|
||||||
- 8080:80
|
- 8080:80
|
||||||
|
|
||||||
|
api:
|
||||||
|
build: api
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 5000:5000
|
||||||
|
volumes:
|
||||||
|
- ./api:/app
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
sockets:
|
sockets:
|
||||||
|
|
Loading…
Reference in New Issue