1
0
mirror of https://github.com/fcwu/docker-ubuntu-vnc-desktop synced 2026-08-05 16:12:41 +02:00

feat: support video mode

This commit is contained in:
Doro Wu
2018-03-15 15:22:19 +08:00
parent 44c1f26170
commit 12b6fe995c
267 changed files with 9305 additions and 37474 deletions
@@ -1,138 +0,0 @@
from flask import (Flask,
request,
abort,
)
import os
import json
from functools import wraps
import subprocess
import time
# Flask app
app = Flask('novnc2')
CONFIG = os.environ.get('CONFIG') or 'config.Development'
app.config.from_object('config.Default')
app.config.from_object(CONFIG)
FIRST = 'RESOLUTION' not in os.environ
# logging
import logging
from log.config import LoggingConfiguration
LoggingConfiguration.set(
logging.DEBUG if os.getenv('DEBUG') else logging.INFO,
'/var/log/web.log'
)
def exception_to_json(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
result = func(*args, **kwargs)
return result
except (BadRequest,
KeyError,
ValueError,
) as e:
result = {'error': {'code': 400,
'message': str(e)}}
except PermissionDenied as e:
result = {'error': {'code': 403,
'message': ', '.join(e.args)}}
except (NotImplementedError, RuntimeError, AttributeError) as e:
result = {'error': {'code': 500,
'message': ', '.join(e.args)}}
return json.dumps(result)
return wrapper
class PermissionDenied(Exception):
pass
class BadRequest(Exception):
pass
HTML_INDEX = '''<html><head>
<script type="text/javascript">
var w = window,
d = document,
e = d.documentElement,
g = d.getElementsByTagName('body')[0],
x = w.innerWidth || e.clientWidth || g.clientWidth,
y = w.innerHeight|| e.clientHeight|| g.clientHeight;
var url = "redirect.html?width=" + x + "&height=" + (parseInt(y));
window.location.href = url;
</script>
<title>Page Redirection</title>
</head><body></body></html>'''
HTML_REDIRECT = '''<html><head>
<script type="text/javascript">
var port = window.location.port;
if (!port)
port = window.location.protocol[4] == 's' ? 443 : 80;
window.location.href = "vnc.html?autoconnect=1&autoscale=0&quality=3";
</script>
<title>Page Redirection</title>
</head><body></body></html>'''
@app.route('/')
def index():
return HTML_INDEX
@app.route('/api/status')
def status():
global FIRST
return json.dumps({
'default_resolution': FIRST
})
@app.route('/redirect.html')
def redirectme():
global FIRST
if not FIRST:
return HTML_REDIRECT
env = {'width': 1024, 'height': 768}
if 'width' in request.args:
env['width'] = request.args['width']
if 'height' in request.args:
env['height'] = request.args['height']
# sed
cmd = (
'sed -i \'s#'
'^exec /usr/bin/Xvfb.*$'
'#'
'exec /usr/bin/Xvfb :1 -screen 0 {width}x{height}x16'
'#\' /usr/local/bin/xvfb.sh'
).format(**env),
subprocess.check_call(cmd, shell=True)
# supervisorctrl reload
subprocess.check_call(['supervisorctl', 'restart', 'x:'])
# check all running
for i in range(40):
output = subprocess.check_output(['supervisorctl', 'status'])
for line in output.strip().split('\n'):
if line.find('RUNNING') < 0:
break
else:
FIRST = False
return HTML_REDIRECT
time.sleep(1)
logging.info('wait services is ready...')
abort(500, 'service is not ready, please restart container')
if __name__ == '__main__':
app.run(host=app.config['ADDRESS'], port=app.config['PORT'])
+188
View File
@@ -0,0 +1,188 @@
from __future__ import (
absolute_import, division, print_function, with_statement
)
import re
from os import environ
from flask import (
Flask,
request,
Response,
jsonify,
)
from gevent import subprocess as gsp, spawn, sleep
from geventwebsocket.exceptions import WebSocketError
from .response import httperror
from .util import ignored
from .state import state
from .log import log
# Flask app
app = Flask('novnc2')
app.config.from_object('config.Default')
app.config.from_object(environ.get('CONFIG') or 'config.Development')
@app.route('/api/state')
@httperror
def apistate():
state.wait(int(request.args.get('id', -1)), 30)
state.switch_video(request.args.get('video', 'false') == 'true')
mystate = state.to_dict()
return jsonify({
'code': 200,
'data': mystate,
})
@app.route('/api/reset')
def reset():
if 'w' in request.args and 'h' in request.args:
args = {
'w': request.args.get('w'),
'h': request.args.get('h'),
}
state.set_size(args['w'], args['h'])
state.apply_and_restart()
# check all running
for i in range(40):
if state.health:
break
sleep(1)
log.info('wait services is ready...')
else:
return jsonify({
'code': 500,
'errorMessage': 'service is not ready, please restart container'
})
return jsonify({'code': 200})
@app.route('/api/live.flv')
@httperror
def liveflv():
def generate():
xenvs = {
'DISPLAY': ':1',
}
bufsize = 1024 * 1
framerate = 20
# sound
sound_cmd_input = []
sound_cmd_parameters = []
zero_latency_make_sound_not_good = [
'-tune', 'zerolatency',
]
xenvs['X_WIDTH'] = state.w
xenvs['X_HEIGHT'] = state.h
xenvs['X_WIDTH'] -= state.w % 2
xenvs['X_HEIGHT'] -= state.h % 2
pixels_count = xenvs['X_WIDTH'] * xenvs['X_HEIGHT']
# factor (720p)
# 383: 2400k
# 300: 3000k
# 230: 4000k
factor = 265
maxbitrate_cmd = [
'-maxrate', str(int(pixels_count / factor)) + 'k',
'-bufsize', str(int(pixels_count / factor / 3)) + 'k'
]
# TODO move to global
# get default source
sound_cmd_input = [
'-f', 'alsa',
'-i', 'hw:2,1',
]
sound_cmd_parameters = [
'-ar', '44100',
'-c:a', 'mp3',
]
# flv.js report error if enabling hw acceleration
# hwaccel_dev = ['-vaapi_device', '/dev/dri/renderD128']
# hwaccel_if = ['-vf', 'format=nv12,hwupload']
# vcodec = 'h264_vaapi'
hwaccel_dev = []
hwaccel_if = []
vcodec = 'libx264'
# zero_latency_make_sound_not_good = []
# sound_cmd_parameters = []
# sound_cmd_input = []
cmd = ['/usr/local/ffmpeg/ffmpeg'] + sound_cmd_input + hwaccel_dev + [
'-video_size', '{X_WIDTH}x{X_HEIGHT}'.format(**xenvs),
'-framerate', '{}'.format(framerate),
'-f', 'x11grab', '-draw_mouse', '1',
'-i', '{DISPLAY}'.format(**xenvs),
] + hwaccel_if + [
'-r', '{}'.format(framerate),
'-g', '{}'.format(framerate),
'-flags:v', '+global_header',
'-vcodec', vcodec,
'-preset', 'ultrafast',
'-b_strategy', '0',
'-pix_fmt', 'yuv420p',
'-bsf:v', 'dump_extra=freq=e',
] + maxbitrate_cmd \
+ sound_cmd_parameters + zero_latency_make_sound_not_good + [
'-f', 'flv', 'pipe:1',
]
log.info('command: ' + ' '.join(cmd))
pobj = gsp.Popen(
cmd,
stdout=gsp.PIPE,
stderr=gsp.PIPE,
env={k: str(v) for k, v in xenvs.iteritems()},
)
def readerr(f):
reobj = re.compile(r'bitrate=(\S+)')
global av_bitrate
try:
while True:
buf = f.read(bufsize)
if len(buf) == 0:
break
patterns = reobj.findall(buf.decode('utf-8', 'ignore'))
if len(patterns) > 0:
av_bitrate = patterns[-1]
# log.info(str(buf))
except Exception as e:
log.exception(e)
preaderr = None
try:
preaderr = spawn(readerr, pobj.stderr)
try:
while True:
buf = pobj.stdout.read(bufsize)
if len(buf) == 0:
break
# ws.send(buf)
yield buf
except WebSocketError:
pass
except Exception as e:
log.exception(e)
finally:
with ignored(Exception):
pobj.kill()
preaderr.join()
except Exception as e:
log.exception(e)
finally:
log.info('exiting')
with ignored(Exception):
pobj.kill()
with ignored(Exception):
preaderr.kill()
log.info('exited')
return Response(generate(), mimetype='video/x-flv')
if __name__ == '__main__':
app.run(host=app.config['ADDRESS'], port=app.config['PORT'])
@@ -0,0 +1,2 @@
import logging
log = logging.getLogger('novnc2')
@@ -0,0 +1,40 @@
from __future__ import (
absolute_import, division, print_function, with_statement
)
from functools import wraps
import logging
from flask import jsonify
log = logging.getLogger()
class PermissionDenied(Exception):
pass
class BadRequest(Exception):
pass
def httperror(f):
@wraps(f)
def func(*args, **kwargs):
result = {
'code': 400,
'errorMessage': '',
}
try:
return f(*args, **kwargs)
except PermissionDenied as e:
result['code'] = 403
result['errorMessage'] = str(e)
except BadRequest as e:
result['code'] = 400
result['errorMessage'] = str(e)
except Exception as e:
logging.exception(e)
result['code'] = 500
result['errorMessage'] = str(e)
return jsonify(result)
return func
@@ -0,0 +1,127 @@
from __future__ import (
absolute_import, division, print_function, with_statement
)
from os import environ
from gevent.event import Event
from gevent import subprocess as gsp
from re import search as research
from .log import log
class State(object):
def __init__(self):
self._eid = 0
self._event = Event()
self._w = self._h = self._health = None
self.size_changed_count = 0
def wait(self, eid, timeout=5):
if eid < self._eid:
return
self._event.clear()
self._event.wait(timeout)
return self._eid
def notify(self):
self._eid += 1
self._event.set()
def _update_health(self):
if self._health:
return
health = True
output = gsp.check_output(['supervisorctl', 'status'])
for line in output.strip().split('\n'):
if not line.startswith('web') and line.find('RUNNING') < 0:
health = False
break
if self._health != health:
self._health = health
self.notify()
return self._health
def to_dict(self):
self._update_health()
state = {
'id': self._eid,
'config': {
'fixedResolution': 'RESOLUTION' in environ,
'sizeChangedCount': self.size_changed_count
}
}
self._update_size()
state.update({
'width': self.w,
'height': self.h,
})
return state
def set_size(self, w, h):
gsp.check_call((
'sed -i \'s#'
'^exec /usr/bin/Xvfb.*$'
'#'
'exec /usr/bin/Xvfb :1 -screen 0 {}x{}x16'
'#\' /usr/local/bin/xvfb.sh'
).format(w, h), shell=True)
self.size_changed_count += 1
def apply_and_restart(self):
gsp.check_call(['supervisorctl', 'restart', 'x:'])
self._w = self._h = self._health = None
self.notify()
def switch_video(self, onoff):
xenvs = {
'DISPLAY': ':1',
}
try:
cmd = 'nofb' if onoff else 'fb'
gsp.check_output(['x11vnc', '-remote', cmd], env=xenvs)
except gsp.CalledProcessError as e:
log.warn('failed to set x11vnc fb: ' + str(e))
def _update_size(self):
if self._w is not None and self._h is not None:
return
xenvs = {
'DISPLAY': ':1',
}
try:
output = gsp.check_output([
'x11vnc', '-query', 'dpy_x,dpy_y'
], env=xenvs).decode('utf-8')
mobj = research(r'dpy_x:(\d+).*dpy_y:(\d+)', output)
if mobj is not None:
w, h = int(mobj.group(1)), int(mobj.group(2))
changed = False
if self._w != w:
changed = True
self._w = w
if self._h != h:
changed = True
self._h = h
if changed:
self.notify()
except gsp.CalledProcessError as e:
log.warn('failed to get dispaly size: ' + str(e))
@property
def w(self):
return self._w
@property
def h(self):
return self._h
@property
def health(self):
self._update_health()
return self._health
state = State()
@@ -0,0 +1,15 @@
from __future__ import (
absolute_import, division, print_function, with_statement
)
from contextlib import contextmanager
from gevent import GreenletExit
@contextmanager
def ignored(*exceptions):
try:
yield
except GreenletExit as e:
raise e
except exceptions:
pass