add janus, remove camera, and merge webInterface into parent process
This commit is contained in:
parent
a93f116aae
commit
06a8a4443f
|
@ -113,6 +113,7 @@ class KeypadListener(Thread):
|
|||
except AttributeError:
|
||||
pass
|
||||
|
||||
# TODO: these are not threadsafe
|
||||
# TODO: this code gets really confused if the pipe is deleted
|
||||
class PipeListener(Thread):
|
||||
def __init__(self, callback, path):
|
||||
|
|
23
main.py
23
main.py
|
@ -3,7 +3,7 @@
|
|||
import sys, os, time, signal, traceback
|
||||
import RPi.GPIO as GPIO
|
||||
from queue import Queue
|
||||
from multiprocessing.managers import BaseManager, DictProxy
|
||||
from multiprocessing.managers import BaseManager
|
||||
|
||||
def clean():
|
||||
GPIO.cleanup()
|
||||
|
@ -13,11 +13,6 @@ def clean():
|
|||
except NameError:
|
||||
pass
|
||||
|
||||
try:
|
||||
webInterface.stop() # Kill process 1
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
try:
|
||||
logger.info('Terminated root process - PID: %s', os.getpid())
|
||||
logger.stop()
|
||||
|
@ -38,11 +33,7 @@ class ResourceManager(BaseManager):
|
|||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
from camera import Camera
|
||||
from microphone import Microphone
|
||||
self.register('Camera', Camera)
|
||||
self.register('Queue', Queue)
|
||||
self.register('Dict', dict, DictProxy)
|
||||
|
||||
def __del__(self):
|
||||
self.shutdown()
|
||||
|
@ -58,22 +49,18 @@ if __name__ == '__main__':
|
|||
manager.start() # Child process 1
|
||||
|
||||
loggerQueue = manager.Queue() # used to buffer logs
|
||||
camera = manager.Camera(loggerQueue)
|
||||
stateDict = manager.Dict() # used to hold state info
|
||||
ttsQueue = manager.Queue() # used as buffer for TTS Engine
|
||||
|
||||
from sharedLogging import MasterLogger
|
||||
logger = MasterLogger(__name__, 'DEBUG', loggerQueue)
|
||||
|
||||
from notifier import criticalError
|
||||
|
||||
|
||||
from stateMachine import StateMachine
|
||||
stateMachine = StateMachine(camera, ttsQueue, stateDict)
|
||||
|
||||
from webInterface import WebInterface
|
||||
webInterface = WebInterface(camera, stateDict, ttsQueue, loggerQueue)
|
||||
webInterface.start() # Child process 2
|
||||
stateMachine = StateMachine()
|
||||
|
||||
# TODO: segfaults are annoying :(
|
||||
#~ signal.signal(signal.SIGSEGV, sig_handler)
|
||||
signal.signal(signal.SIGTERM, sigtermHandler)
|
||||
|
||||
while 1:
|
||||
|
|
|
@ -7,7 +7,10 @@ def SlaveLogger(name, level, queue):
|
|||
logger.addHandler(QueueHandler(queue))
|
||||
logger.propagate = False
|
||||
return logger
|
||||
|
||||
|
||||
#TODO: need to add mounting code here for gluster. since this app is the only
|
||||
# gluster user, (un)mounting should be handled here instead of by systemd
|
||||
|
||||
class MasterLogger():
|
||||
def __init__(self, name, level, queue):
|
||||
consoleFormat = logging.Formatter('[%(name)s] [%(levelname)s] %(message)s')
|
||||
|
|
45
soundLib.py
45
soundLib.py
|
@ -3,6 +3,7 @@ from pygame import mixer
|
|||
from subprocess import call
|
||||
from collections import OrderedDict
|
||||
from auxilary import async
|
||||
from queue import Queue
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -66,7 +67,7 @@ class SoundLib:
|
|||
|
||||
_sentinel = None
|
||||
|
||||
def __init__(self, ttsQueue):
|
||||
def __init__(self):
|
||||
mixer.pre_init(frequency=44100, size=-16, channels=2, buffer=1024)
|
||||
mixer.init()
|
||||
|
||||
|
@ -88,7 +89,7 @@ class SoundLib:
|
|||
self.volume = 100
|
||||
self._applyVolumesToSounds(self.volume)
|
||||
|
||||
self._ttsQueue = ttsQueue
|
||||
self._ttsQueue = Queue()
|
||||
self._stop = threading.Event()
|
||||
self._startMonitor()
|
||||
|
||||
|
@ -101,21 +102,7 @@ class SoundLib:
|
|||
self._applyVolumesToSounds(0)
|
||||
|
||||
def speak(self, text):
|
||||
basename = hashlib.md5(text.encode()).hexdigest()
|
||||
|
||||
if basename in self._ttsSounds:
|
||||
self._ttsSounds.move_to_end(basename)
|
||||
else:
|
||||
path = '/tmp/' + basename
|
||||
call(['espeak', '-a180', '-g8', '-p75', '-w', path, text])
|
||||
self._ttsSounds[basename] = TTSSound(path)
|
||||
|
||||
self._fader(
|
||||
lowerVolume=0.1,
|
||||
totalDuration=self._ttsSounds[basename].get_length()
|
||||
)
|
||||
self._ttsSounds[basename].play()
|
||||
logger.debug('TTS engine received "%s"', text)
|
||||
self._ttsQueue.put_nowait(text)
|
||||
|
||||
@async(daemon=False)
|
||||
def _fader(self, lowerVolume, totalDuration, fadeDuration=0.2, stepSize=5):
|
||||
|
@ -156,6 +143,7 @@ class SoundLib:
|
|||
for name, sound in s.items():
|
||||
sound.set_volume(v)
|
||||
|
||||
# TODO: maybe could simply now that we are not using MP for TTS
|
||||
def _ttsMonitor(self):
|
||||
q = self._ttsQueue
|
||||
has_task_done = hasattr(q, 'task_done')
|
||||
|
@ -164,7 +152,7 @@ class SoundLib:
|
|||
text = self._ttsQueue.get(True)
|
||||
if text is self._sentinel:
|
||||
break
|
||||
self.speak(text)
|
||||
self._playSpeech(text)
|
||||
if has_task_done:
|
||||
q.task_done()
|
||||
except queue.Empty:
|
||||
|
@ -175,12 +163,29 @@ class SoundLib:
|
|||
text = self._ttsQueue.get(False)
|
||||
if text is self._sentinel:
|
||||
break
|
||||
self.speak(text)
|
||||
self._playSpeech(text)
|
||||
if has_task_done:
|
||||
q.task_done()
|
||||
except queue.Empty:
|
||||
break
|
||||
|
||||
|
||||
def _playSpeech(self, text):
|
||||
basename = hashlib.md5(text.encode()).hexdigest()
|
||||
|
||||
if basename in self._ttsSounds:
|
||||
self._ttsSounds.move_to_end(basename)
|
||||
else:
|
||||
path = '/tmp/' + basename
|
||||
call(['espeak', '-a180', '-g8', '-p75', '-w', path, text])
|
||||
self._ttsSounds[basename] = TTSSound(path)
|
||||
|
||||
self._fader(
|
||||
lowerVolume=0.1,
|
||||
totalDuration=self._ttsSounds[basename].get_length()
|
||||
)
|
||||
self._ttsSounds[basename].play()
|
||||
logger.debug('TTS engine received "%s"', text)
|
||||
|
||||
def _startMonitor(self):
|
||||
self._thread = t = threading.Thread(target=self._ttsMonitor, daemon=True)
|
||||
t.start()
|
||||
|
|
114
stateMachine.py
114
stateMachine.py
|
@ -11,6 +11,7 @@ from notifier import intruderAlert
|
|||
from listeners import KeypadListener, PipeListener
|
||||
from blinkenLights import Blinkenlights
|
||||
from soundLib import SoundLib
|
||||
from webInterface import initWebInterface
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -36,17 +37,17 @@ class State:
|
|||
|
||||
def entry(self):
|
||||
logger.debug('entering ' + self.name)
|
||||
#~ if self.sound:
|
||||
#~ self.sound.play()
|
||||
#~ self.stateMachine.LED.blink = self.blinkLED
|
||||
#~ self.stateMachine.keypadListener.resetBuffer()
|
||||
if self.sound:
|
||||
self.sound.play()
|
||||
self.stateMachine.LED.blink = self.blinkLED
|
||||
self.stateMachine.keypadListener.resetBuffer()
|
||||
for c in self.entryCallbacks:
|
||||
c()
|
||||
|
||||
def exit(self):
|
||||
logger.debug('exiting ' + self.name)
|
||||
#~ if self.sound:
|
||||
#~ self.sound.stop()
|
||||
if self.sound:
|
||||
self.sound.stop()
|
||||
for c in self.exitCallbacks:
|
||||
c()
|
||||
|
||||
|
@ -64,9 +65,8 @@ class State:
|
|||
return hash(self.name)
|
||||
|
||||
class StateMachine:
|
||||
def __init__(self, camera, ttsQueue, sharedState):
|
||||
self.sharedState = sharedState
|
||||
self.soundLib = SoundLib(ttsQueue)
|
||||
def __init__(self):
|
||||
self.soundLib = SoundLib()
|
||||
self._cfg = ConfigFile('config.yaml')
|
||||
|
||||
def startTimer(t, sound):
|
||||
|
@ -131,7 +131,7 @@ class StateMachine:
|
|||
|
||||
self._lock = Lock()
|
||||
|
||||
#~ self.LED = Blinkenlights(17)
|
||||
self.LED = Blinkenlights(17)
|
||||
|
||||
def action():
|
||||
if self.currentState == self.armed:
|
||||
|
@ -142,17 +142,17 @@ class StateMachine:
|
|||
self.selectState(SIGNALS.TRIGGER)
|
||||
while GPIO.input(pin):
|
||||
path = '/mnt/glusterfs/pyledriver/images/%s.jpg'
|
||||
with open(path % datetime.now(), 'wb') as f:
|
||||
f.write(camera.getFrame())
|
||||
#~ with open(path % datetime.now(), 'wb') as f:
|
||||
#~ f.write(camera.getFrame())
|
||||
time.sleep(0.2)
|
||||
|
||||
#~ setupMotionSensor(5, 'Nate\'s room', action)
|
||||
#~ setupMotionSensor(19, 'front door', action)
|
||||
#~ setupMotionSensor(26, 'Laura\'s room', action)
|
||||
#~ setupMotionSensor(6, 'deck window', partial(actionVideo, 6))
|
||||
#~ setupMotionSensor(13, 'kitchen bar', partial(actionVideo, 13))
|
||||
setupMotionSensor(5, 'Nate\'s room', action)
|
||||
setupMotionSensor(19, 'front door', action)
|
||||
setupMotionSensor(26, 'Laura\'s room', action)
|
||||
setupMotionSensor(6, 'deck window', partial(actionVideo, 6))
|
||||
setupMotionSensor(13, 'kitchen bar', partial(actionVideo, 13))
|
||||
|
||||
#~ setupDoorSensor(22, action, self.soundLib.soundEffects['door'])
|
||||
setupDoorSensor(22, action, self.soundLib.soundEffects['door'])
|
||||
|
||||
secretTable = {
|
||||
"dynamoHum": partial(self.selectState, SIGNALS.DISARM),
|
||||
|
@ -160,35 +160,36 @@ class StateMachine:
|
|||
"imTheSlime": partial(self.selectState, SIGNALS.INSTANT_ARM)
|
||||
}
|
||||
|
||||
#~ def secretCallback(secret, logger):
|
||||
#~ if secret in secretTable:
|
||||
#~ secretTable[secret]()
|
||||
#~ logger.debug('Secret pipe listener received: \"%s\"', secret)
|
||||
#~ elif logger:
|
||||
#~ logger.error('Secret pipe listener received invalid secret')
|
||||
|
||||
#~ self.secretListener = PipeListener(
|
||||
#~ callback = secretCallback,
|
||||
#~ path = '/tmp/secret'
|
||||
#~ )
|
||||
|
||||
#~ def ttsCallback(text, logger):
|
||||
#~ self.soundLib.speak(text)
|
||||
#~ logger.debug('TTS pipe listener received text: \"%s\"', text)
|
||||
|
||||
#~ self.ttsListener = PipeListener(
|
||||
#~ callback = ttsCallback,
|
||||
#~ path = '/tmp/tts'
|
||||
#~ )
|
||||
#~ self.keypadListener = KeypadListener(
|
||||
#~ stateMachine = self,
|
||||
#~ callbackDisarm = partial(self.selectState, 'disarm'),
|
||||
#~ callbackArm = partial(self.selectState, 'arm'),
|
||||
#~ soundLib = self.soundLib,
|
||||
#~ passwd = '5918462'
|
||||
#~ )
|
||||
|
||||
self.sharedState['name'] = self.currentState.name
|
||||
def secretCallback(secret, logger):
|
||||
if secret in secretTable:
|
||||
secretTable[secret]()
|
||||
logger.debug('Secret pipe listener received: \"%s\"', secret)
|
||||
elif logger:
|
||||
logger.error('Secret pipe listener received invalid secret')
|
||||
|
||||
self.secretListener = PipeListener(
|
||||
callback = secretCallback,
|
||||
path = '/tmp/secret'
|
||||
)
|
||||
|
||||
def ttsCallback(text, logger):
|
||||
self.soundLib.speak(text)
|
||||
logger.debug('TTS pipe listener received text: \"%s\"', text)
|
||||
|
||||
self.ttsListener = PipeListener(
|
||||
callback = ttsCallback,
|
||||
path = '/tmp/tts'
|
||||
)
|
||||
self.keypadListener = KeypadListener(
|
||||
stateMachine = self,
|
||||
callbackDisarm = partial(self.selectState, 'disarm'),
|
||||
callbackArm = partial(self.selectState, 'arm'),
|
||||
soundLib = self.soundLib,
|
||||
passwd = '5918462'
|
||||
)
|
||||
|
||||
initWebInterface(self)
|
||||
|
||||
self.currentState.entry()
|
||||
|
||||
def selectState(self, signal):
|
||||
|
@ -201,7 +202,6 @@ class StateMachine:
|
|||
self.currentState.entry()
|
||||
finally:
|
||||
self._lock.release()
|
||||
self.sharedState['name'] = self.currentState.name
|
||||
|
||||
self._cfg['state'] = self.currentState.name
|
||||
self._cfg.sync()
|
||||
|
@ -209,22 +209,14 @@ class StateMachine:
|
|||
logger.info('state changed to %s', self.currentState)
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
self.LED.__del__()
|
||||
except AttributeError:
|
||||
pass
|
||||
if hasattr(self, 'LED'):
|
||||
self.LED.__del__()
|
||||
|
||||
try:
|
||||
if hasattr(self, 'soundLib'):
|
||||
self.soundLib.__del__()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if hasattr(self, 'pipeListener'):
|
||||
self.pipeListener.__del__()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if hasattr(self, 'keypadListener'):
|
||||
self.keypadListener.__del__()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,25 @@
|
|||
@media (max-width: 767px) {
|
||||
.navbar-text {
|
||||
margin-left: 10px !important;
|
||||
}
|
||||
.form-inline .form-group {
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.form-inline .form-control {
|
||||
display: inline-block;
|
||||
}
|
||||
.nav > li {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-form {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.navbar-collapse .navbar-nav.navbar-right:last-child {
|
||||
margin-right: 0;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
|||
(function(t,e){if(typeof exports=="object")module.exports=e();else if(typeof define=="function"&&define.amd)define(e);else t.Spinner=e()})(this,function(){"use strict";var t=["webkit","Moz","ms","O"],e={},i;function o(t,e){var i=document.createElement(t||"div"),o;for(o in e)i[o]=e[o];return i}function n(t){for(var e=1,i=arguments.length;e<i;e++)t.appendChild(arguments[e]);return t}var r=function(){var t=o("style",{type:"text/css"});n(document.getElementsByTagName("head")[0],t);return t.sheet||t.styleSheet}();function s(t,o,n,s){var a=["opacity",o,~~(t*100),n,s].join("-"),f=.01+n/s*100,l=Math.max(1-(1-t)/o*(100-f),t),u=i.substring(0,i.indexOf("Animation")).toLowerCase(),d=u&&"-"+u+"-"||"";if(!e[a]){r.insertRule("@"+d+"keyframes "+a+"{"+"0%{opacity:"+l+"}"+f+"%{opacity:"+t+"}"+(f+.01)+"%{opacity:1}"+(f+o)%100+"%{opacity:"+t+"}"+"100%{opacity:"+l+"}"+"}",r.cssRules.length);e[a]=1}return a}function a(e,i){var o=e.style,n,r;i=i.charAt(0).toUpperCase()+i.slice(1);for(r=0;r<t.length;r++){n=t[r]+i;if(o[n]!==undefined)return n}if(o[i]!==undefined)return i}function f(t,e){for(var i in e)t.style[a(t,i)||i]=e[i];return t}function l(t){for(var e=1;e<arguments.length;e++){var i=arguments[e];for(var o in i)if(t[o]===undefined)t[o]=i[o]}return t}function u(t){var e={x:t.offsetLeft,y:t.offsetTop};while(t=t.offsetParent)e.x+=t.offsetLeft,e.y+=t.offsetTop;return e}function d(t,e){return typeof t=="string"?t:t[e%t.length]}var p={lines:12,length:7,width:5,radius:10,rotate:0,corners:1,color:"#000",direction:1,speed:1,trail:100,opacity:1/4,fps:20,zIndex:2e9,className:"spinner",top:"auto",left:"auto",position:"relative"};function c(t){if(typeof this=="undefined")return new c(t);this.opts=l(t||{},c.defaults,p)}c.defaults={};l(c.prototype,{spin:function(t){this.stop();var e=this,n=e.opts,r=e.el=f(o(0,{className:n.className}),{position:n.position,width:0,zIndex:n.zIndex}),s=n.radius+n.length+n.width,a,l;if(t){t.insertBefore(r,t.firstChild||null);l=u(t);a=u(r);f(r,{left:(n.left=="auto"?l.x-a.x+(t.offsetWidth>>1):parseInt(n.left,10)+s)+"px",top:(n.top=="auto"?l.y-a.y+(t.offsetHeight>>1):parseInt(n.top,10)+s)+"px"})}r.setAttribute("role","progressbar");e.lines(r,e.opts);if(!i){var d=0,p=(n.lines-1)*(1-n.direction)/2,c,h=n.fps,m=h/n.speed,y=(1-n.opacity)/(m*n.trail/100),g=m/n.lines;(function v(){d++;for(var t=0;t<n.lines;t++){c=Math.max(1-(d+(n.lines-t)*g)%m*y,n.opacity);e.opacity(r,t*n.direction+p,c,n)}e.timeout=e.el&&setTimeout(v,~~(1e3/h))})()}return e},stop:function(){var t=this.el;if(t){clearTimeout(this.timeout);if(t.parentNode)t.parentNode.removeChild(t);this.el=undefined}return this},lines:function(t,e){var r=0,a=(e.lines-1)*(1-e.direction)/2,l;function u(t,i){return f(o(),{position:"absolute",width:e.length+e.width+"px",height:e.width+"px",background:t,boxShadow:i,transformOrigin:"left",transform:"rotate("+~~(360/e.lines*r+e.rotate)+"deg) translate("+e.radius+"px"+",0)",borderRadius:(e.corners*e.width>>1)+"px"})}for(;r<e.lines;r++){l=f(o(),{position:"absolute",top:1+~(e.width/2)+"px",transform:e.hwaccel?"translate3d(0,0,0)":"",opacity:e.opacity,animation:i&&s(e.opacity,e.trail,a+r*e.direction,e.lines)+" "+1/e.speed+"s linear infinite"});if(e.shadow)n(l,f(u("#000","0 0 4px "+"#000"),{top:2+"px"}));n(t,n(l,u(d(e.color,r),"0 0 1px rgba(0,0,0,.1)")))}return t},opacity:function(t,e,i){if(e<t.childNodes.length)t.childNodes[e].style.opacity=i}});function h(){function t(t,e){return o("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">',e)}r.addRule(".spin-vml","behavior:url(#default#VML)");c.prototype.lines=function(e,i){var o=i.length+i.width,r=2*o;function s(){return f(t("group",{coordsize:r+" "+r,coordorigin:-o+" "+-o}),{width:r,height:r})}var a=-(i.width+i.length)*2+"px",l=f(s(),{position:"absolute",top:a,left:a}),u;function p(e,r,a){n(l,n(f(s(),{rotation:360/i.lines*e+"deg",left:~~r}),n(f(t("roundrect",{arcsize:i.corners}),{width:o,height:i.width,left:i.radius,top:-i.width>>1,filter:a}),t("fill",{color:d(i.color,e),opacity:i.opacity}),t("stroke",{opacity:0}))))}if(i.shadow)for(u=1;u<=i.lines;u++)p(u,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(u=1;u<=i.lines;u++)p(u);return n(e,l)};c.prototype.opacity=function(t,e,i,o){var n=t.firstChild;o=o.shadow&&o.lines||0;if(n&&e+o<n.childNodes.length){n=n.childNodes[e+o];n=n&&n.firstChild;n=n&&n.firstChild;if(n)n.opacity=i}}}var m=f(o("group"),{behavior:"url(#default#VML)"});if(!a(m,"transform")&&m.adj)h();else i=a(m,"animation");return c});
|
|
@ -0,0 +1,245 @@
|
|||
// We make use of this 'server' variable to provide the address of the
|
||||
// REST Janus API. By default, in this example we assume that Janus is
|
||||
// co-located with the web server hosting the HTML pages but listening
|
||||
// on a different port (8088, the default for HTTP in Janus), which is
|
||||
// why we make use of the 'window.location.hostname' base address. Since
|
||||
// Janus can also do HTTPS, and considering we don't really want to make
|
||||
// use of HTTP for Janus if your demos are served on HTTPS, we also rely
|
||||
// on the 'window.location.protocol' prefix to build the variable, in
|
||||
// particular to also change the port used to contact Janus (8088 for
|
||||
// HTTP and 8089 for HTTPS, if enabled).
|
||||
// In case you place Janus behind an Apache frontend (as we did on the
|
||||
// online demos at http://janus.conf.meetecho.com) you can just use a
|
||||
// relative path for the variable, e.g.:
|
||||
//
|
||||
// var server = "/janus";
|
||||
//
|
||||
// which will take care of this on its own.
|
||||
//
|
||||
//
|
||||
// If you want to use the WebSockets frontend to Janus, instead, you'll
|
||||
// have to pass a different kind of address, e.g.:
|
||||
//
|
||||
// var server = "ws://" + window.location.hostname + ":8188";
|
||||
//
|
||||
// Of course this assumes that support for WebSockets has been built in
|
||||
// when compiling the gateway. WebSockets support has not been tested
|
||||
// as much as the REST API, so handle with care!
|
||||
//
|
||||
//
|
||||
// If you have multiple options available, and want to let the library
|
||||
// autodetect the best way to contact your gateway (or pool of gateways),
|
||||
// you can also pass an array of servers, e.g., to provide alternative
|
||||
// means of access (e.g., try WebSockets first and, if that fails, fall
|
||||
// back to plain HTTP) or just have failover servers:
|
||||
//
|
||||
// var server = [
|
||||
// "ws://" + window.location.hostname + ":8188",
|
||||
// "/janus"
|
||||
// ];
|
||||
//
|
||||
// This will tell the library to try connecting to each of the servers
|
||||
// in the presented order. The first working server will be used for
|
||||
// the whole session.
|
||||
//
|
||||
//~ var server = null;
|
||||
//~ if(window.location.protocol === 'http:')
|
||||
//~ server = "http://" + window.location.hostname + ":8088/janus";
|
||||
//~ else
|
||||
//~ server = "https://" + window.location.hostname + ":8089/janus";
|
||||
|
||||
var server = "/janus";
|
||||
|
||||
var janus = null;
|
||||
var streaming = null;
|
||||
var started = false;
|
||||
var spinner = null;
|
||||
|
||||
var selectedStream = null;
|
||||
|
||||
|
||||
$(document).ready(function() {
|
||||
// Initialize the library (all console debuggers enabled)
|
||||
Janus.init({debug: "all", callback: function() {
|
||||
// Use a button to start the demo
|
||||
//~ $('#start').click(function() {
|
||||
if(started)
|
||||
return;
|
||||
started = true;
|
||||
$(this).attr('disabled', true).unbind('click');
|
||||
// Make sure the browser supports WebRTC
|
||||
if(!Janus.isWebrtcSupported()) {
|
||||
bootbox.alert("No WebRTC support... ");
|
||||
return;
|
||||
}
|
||||
// Create session
|
||||
janus = new Janus(
|
||||
{
|
||||
server: server,
|
||||
success: function() {
|
||||
// Attach to streaming plugin
|
||||
janus.attach(
|
||||
{
|
||||
plugin: "janus.plugin.streaming",
|
||||
success: function(pluginHandle) {
|
||||
//~ $('#details').remove();
|
||||
streaming = pluginHandle;
|
||||
Janus.log("Plugin attached! (" + streaming.getPlugin() + ", id=" + streaming.getId() + ")");
|
||||
// Setup streaming session
|
||||
$('#update-streams').click(updateStreamsList);
|
||||
updateStreamsList();
|
||||
//~ $('#start').removeAttr('disabled').html("Stop")
|
||||
//~ .click(function() {
|
||||
//~ $(this).attr('disabled', true);
|
||||
//~ janus.destroy();
|
||||
//~ $('#streamslist').attr('disabled', true);
|
||||
//~ $('#watch').attr('disabled', true).unbind('click');
|
||||
//~ $('#start').attr('disabled', true).html("Bye").unbind('click');
|
||||
//~ });
|
||||
},
|
||||
error: function(error) {
|
||||
Janus.error(" -- Error attaching plugin... ", error);
|
||||
bootbox.alert("Error attaching plugin... " + error);
|
||||
},
|
||||
onmessage: function(msg, jsep) {
|
||||
Janus.debug(" ::: Got a message :::");
|
||||
Janus.debug(JSON.stringify(msg));
|
||||
var result = msg["result"];
|
||||
if(result !== null && result !== undefined) {
|
||||
if(result["status"] !== undefined && result["status"] !== null) {
|
||||
var status = result["status"];
|
||||
if(status === 'starting')
|
||||
$('#status').removeClass('hide').text("Starting, please wait...").show();
|
||||
else if(status === 'started')
|
||||
$('#status').removeClass('hide').text("Started").show();
|
||||
else if(status === 'stopped')
|
||||
stopStream();
|
||||
}
|
||||
} else if(msg["error"] !== undefined && msg["error"] !== null) {
|
||||
bootbox.alert(msg["error"]);
|
||||
stopStream();
|
||||
return;
|
||||
}
|
||||
if(jsep !== undefined && jsep !== null) {
|
||||
Janus.debug("Handling SDP as well...");
|
||||
Janus.debug(jsep);
|
||||
// Answer
|
||||
streaming.createAnswer(
|
||||
{
|
||||
jsep: jsep,
|
||||
media: { audioSend: false, videoSend: false }, // We want recvonly audio/video
|
||||
success: function(jsep) {
|
||||
Janus.debug("Got SDP!");
|
||||
Janus.debug(jsep);
|
||||
var body = { "request": "start" };
|
||||
streaming.send({"message": body, "jsep": jsep});
|
||||
$('#watch').html("Stop").removeAttr('disabled').click(stopStream);
|
||||
},
|
||||
error: function(error) {
|
||||
Janus.error("WebRTC error:", error);
|
||||
bootbox.alert("WebRTC error... " + JSON.stringify(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onremotestream: function(stream) {
|
||||
Janus.debug(" ::: Got a remote stream :::");
|
||||
Janus.debug(JSON.stringify(stream));
|
||||
if($('#remotevideo').length === 0)
|
||||
$('#stream').append('<video class="centered hide" id="remotevideo" width=640 height=480 autoplay/>');
|
||||
// Show the stream and hide the spinner when we get a playing event
|
||||
$("#remotevideo").bind("playing", function () {
|
||||
$('#waitingvideo').remove();
|
||||
$('#remotevideo').removeClass('hide');
|
||||
if(spinner !== null && spinner !== undefined)
|
||||
spinner.stop();
|
||||
spinner = null;
|
||||
});
|
||||
Janus.attachMediaStream($('#remotevideo').get(0), stream);
|
||||
},
|
||||
oncleanup: function() {
|
||||
Janus.log(" ::: Got a cleanup notification :::");
|
||||
$('#waitingvideo').remove();
|
||||
$('#remotevideo').remove();
|
||||
}
|
||||
});
|
||||
},
|
||||
error: function(error) {
|
||||
Janus.error(error);
|
||||
bootbox.alert(error, function() {
|
||||
window.location.reload();
|
||||
});
|
||||
},
|
||||
destroyed: function() {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
//~ });
|
||||
}});
|
||||
});
|
||||
|
||||
function updateStreamsList() {
|
||||
$('#update-streams').unbind('click').addClass('fa-spin');
|
||||
var body = { "request": "list" };
|
||||
Janus.debug("Sending message (" + JSON.stringify(body) + ")");
|
||||
streaming.send({"message": body, success: function(result) {
|
||||
setTimeout(function() {
|
||||
$('#update-streams').removeClass('fa-spin').click(updateStreamsList);
|
||||
}, 500);
|
||||
if(result === null || result === undefined) {
|
||||
bootbox.alert("Got no response to our query for available streams");
|
||||
return;
|
||||
}
|
||||
if(result["list"] !== undefined && result["list"] !== null) {
|
||||
//~ $('#streams').removeClass('hide').show();
|
||||
$('#streamslist').empty();
|
||||
$('#watch').attr('disabled', true).unbind('click');
|
||||
var list = result["list"];
|
||||
Janus.log("Got a list of available streams");
|
||||
Janus.debug(list);
|
||||
for(var mp in list) {
|
||||
Janus.debug(" >> [" + list[mp]["id"] + "] " + list[mp]["description"] + " (" + list[mp]["type"] + ")");
|
||||
$('#streamslist').append("<li><a href='#' id='" + list[mp]["id"] + "'>" + list[mp]["description"] + " (" + list[mp]["type"] + ")" + "</a></li>");
|
||||
}
|
||||
$('#streamslist a').unbind('click').click(function() {
|
||||
selectedStream = $(this).attr("id");
|
||||
$('#streamset').html($(this).html()).parent().removeClass('open');
|
||||
return false;
|
||||
|
||||
});
|
||||
$('#watch').removeAttr('disabled').click(startStream);
|
||||
}
|
||||
}});
|
||||
}
|
||||
|
||||
function startStream() {
|
||||
Janus.log("Selected video id #" + selectedStream);
|
||||
if(selectedStream === undefined || selectedStream === null) {
|
||||
bootbox.alert("Select a stream from the list");
|
||||
return;
|
||||
}
|
||||
$('#streamset').attr('disabled', true);
|
||||
$('#streamslist').attr('disabled', true);
|
||||
$('#watch').attr('disabled', true).unbind('click');
|
||||
var body = { "request": "watch", id: parseInt(selectedStream) };
|
||||
streaming.send({"message": body});
|
||||
// No remote video yet
|
||||
$('#stream').append('<video class="centered" id="waitingvideo" width=640 height=480 />');
|
||||
if(spinner == null) {
|
||||
var target = document.getElementById('stream');
|
||||
spinner = new Spinner({top:100}).spin(target);
|
||||
} else {
|
||||
spinner.spin();
|
||||
}
|
||||
}
|
||||
|
||||
function stopStream() {
|
||||
$('#watch').attr('disabled', true).unbind('click');
|
||||
var body = { "request": "stop" };
|
||||
streaming.send({"message": body});
|
||||
streaming.hangup();
|
||||
$('#streamset').removeAttr('disabled');
|
||||
$('#streamslist').removeAttr('disabled');
|
||||
$('#watch').html("Start").removeAttr('disabled').click(startStream);
|
||||
$('#status').empty().hide();
|
||||
}
|
|
@ -1,35 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<link rel="stylesheet" href="{{ url_for('camPage.static', filename='index.css') }}">
|
||||
<head>
|
||||
<title>Camera 1</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="titlebar">
|
||||
<b>Pyledriver Status: </b><span id='state'>{{ state }}</span>
|
||||
</div>
|
||||
<div class="camera_container">
|
||||
<div class="camera">
|
||||
<img id="bg" src="{{ url_for('camPage.videoFeed') }}">
|
||||
</div>
|
||||
<div class="controls">
|
||||
<form action="{{ url_for('camPage.index') }}" method='post' name='framerate'>
|
||||
{{ camForm.hidden_tag() }}
|
||||
<p>Current Framerate: <span id='FPSvalue'>{{ fps }}</span></p>
|
||||
<p>Set Framerate {{ camForm.fps }}</p>
|
||||
<p>{{ camForm.submitFPS }}</p>
|
||||
</form>
|
||||
<form action="{{ url_for('camPage.index') }}" method='post' name='text_to_speech'>
|
||||
{{ ttsForm.hidden_tag() }}
|
||||
<p>Speak your mind</p>
|
||||
<p>{{ ttsForm.tts }}</p>
|
||||
<p>{{ ttsForm.submitTTS }}</p>
|
||||
</form>
|
||||
<form action="{{ url_for('camPage.index') }}" method='post' name='text_to_speech'>
|
||||
{{ ttsForm.hidden_tag() }}
|
||||
<p>If something goes wrong...</p>
|
||||
<p>{{ resetForm.submitReset }}</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Pyledriver</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('siteRoot.static', filename='css/bootstrap.min.css') }}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('siteRoot.static', filename='css/pyledriver.css') }}">
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('siteRoot.static', filename='jquery.min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('siteRoot.static', filename='js/bootstrap.min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('siteRoot.static', filename='js/bootbox.min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('siteRoot.static', filename='js/spin.min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('siteRoot.static', filename='janus.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('siteRoot.static', filename='streamingtest.js') }}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="navbar navbar-inverse navbar-static-top">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<span class="navbar-text"><b>Status: </b><span>{{ state }}</span></span>
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navRight">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="navRight">
|
||||
<ul class="nav navbar-nav">
|
||||
<form action="{{ url_for('siteRoot.index') }}" method="post" name="text_to_speech" class="navbar-form" role="search">
|
||||
{{ ttsForm.hidden_tag() }}
|
||||
<div class="form-inline">
|
||||
<div class="form-group">{{ ttsForm.tts(class_="form-control") }}</div>
|
||||
{{ ttsForm.submitTTS(class_="btn btn-default") }}
|
||||
</div>
|
||||
</form>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a id="streamset" class="dropdown-toggle" data-toggle="dropdown">
|
||||
Pick Stream<span class="caret"></span>
|
||||
</a>
|
||||
<ul id="streamslist" class="dropdown-menu" role="menu"></ul>
|
||||
</li>
|
||||
<button class="btn btn-default navbar-btn" autocomplete="off" id="watch">Start</button>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid body-content">
|
||||
<div class="col-md-6">
|
||||
<!--
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Stream <span class="label label-info hide" id="status"></h3>
|
||||
</div>
|
||||
-->
|
||||
<div id="stream"></div>
|
||||
<p class="hide" id="status">
|
||||
<!--
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
104
webInterface.py
104
webInterface.py
|
@ -1,84 +1,48 @@
|
|||
from multiprocessing import Process
|
||||
from sharedLogging import SlaveLogger
|
||||
import logging
|
||||
from auxilary import async
|
||||
from flask import Flask, render_template, Response, Blueprint, redirect, url_for
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms.fields import SelectField, StringField, SubmitField
|
||||
from wtforms.fields import StringField, SubmitField
|
||||
from wtforms.validators import InputRequired
|
||||
|
||||
class CamForm(FlaskForm):
|
||||
fps = SelectField(choices=[(i, '%s fps' % i) for i in range(10, 31, 5)], coerce=int)
|
||||
submitFPS = SubmitField('Set')
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# gag the flask logger unless it has something useful to say
|
||||
werkzeug = logging.getLogger('werkzeug')
|
||||
werkzeug.setLevel(logging.ERROR)
|
||||
|
||||
class TTSForm(FlaskForm):
|
||||
tts = StringField(validators=[InputRequired()])
|
||||
submitTTS = SubmitField('Speak')
|
||||
|
||||
class ResetForm(FlaskForm):
|
||||
submitReset = SubmitField('Reset')
|
||||
|
||||
# TODO: fix random connection fails (might be an nginx thing)
|
||||
# TODO: show camera failed status here somewhere
|
||||
|
||||
class WebInterface(Process):
|
||||
def __init__(self, camera, stateDict, ttsQueue, loggerQueue):
|
||||
self._moduleLogger = SlaveLogger(__name__, 'INFO', loggerQueue)
|
||||
self._flaskLogger = SlaveLogger('werkzeug', 'ERROR', loggerQueue)
|
||||
@async(daemon=True)
|
||||
def initWebInterface(stateMachine):
|
||||
siteRoot = Blueprint('siteRoot', __name__, static_folder='static', static_url_path='')
|
||||
|
||||
@siteRoot.route('/', methods=['GET', 'POST'])
|
||||
@siteRoot.route('/index', methods=['GET', 'POST'])
|
||||
def index():
|
||||
ttsForm = TTSForm()
|
||||
|
||||
camPage = Blueprint('camPage', __name__, static_folder='static', template_folder='templates')
|
||||
if ttsForm.validate_on_submit() and ttsForm.submitTTS.data:
|
||||
stateMachine.soundLib.speak(ttsForm.tts.data)
|
||||
#~ ttsQueue.put_nowait(ttsForm.tts.data)
|
||||
return redirect(url_for('siteRoot.index'))
|
||||
|
||||
def generateFrame():
|
||||
while 1:
|
||||
yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + camera.getFrame() + b'\r\n')
|
||||
|
||||
@camPage.route('/', methods=['GET', 'POST'])
|
||||
@camPage.route('/index', methods=['GET', 'POST'])
|
||||
def index():
|
||||
props = camera.getProps('FPS')
|
||||
fps = int(props['FPS'])
|
||||
camForm = CamForm(fps = props['FPS'])
|
||||
ttsForm = TTSForm()
|
||||
resetForm = ResetForm()
|
||||
|
||||
if camForm.validate_on_submit() and camForm.submitFPS.data:
|
||||
camera.setProps(FPS=camForm.fps.data)
|
||||
return redirect(url_for('camPage.index'))
|
||||
|
||||
if ttsForm.validate_on_submit() and ttsForm.submitTTS.data:
|
||||
ttsQueue.put_nowait(ttsForm.tts.data)
|
||||
return redirect(url_for('camPage.index'))
|
||||
return render_template(
|
||||
'index.html',
|
||||
ttsForm=ttsForm,
|
||||
state=stateMachine.currentState
|
||||
)
|
||||
|
||||
if resetForm.validate_on_submit() and resetForm.submitReset.data:
|
||||
camera.reset()
|
||||
return redirect(url_for('camPage.index'))
|
||||
app = Flask(__name__)
|
||||
app.secret_key = '3276d68dac56985bea352325125641ff'
|
||||
app.register_blueprint(siteRoot, url_prefix='/pyledriver')
|
||||
|
||||
return render_template(
|
||||
'index.html',
|
||||
camForm=camForm,
|
||||
ttsForm=ttsForm,
|
||||
resetForm=resetForm,
|
||||
fps=fps,
|
||||
state=stateDict['name']
|
||||
)
|
||||
|
||||
@camPage.route('/videoFeed')
|
||||
def videoFeed():
|
||||
return Response(generateFrame(), mimetype='multipart/x-mixed-replace; boundary=frame')
|
||||
|
||||
self._app = Flask(__name__)
|
||||
self._app.secret_key = '3276d68dac56985bea352325125641ff'
|
||||
self._app.register_blueprint(camPage, url_prefix='/pyledriver')
|
||||
|
||||
super().__init__(daemon=True)
|
||||
|
||||
def run(self):
|
||||
# TODO: not sure exactly how threaded=True works, intended to enable
|
||||
# multiple connections. May want to use something more robust w/ camera
|
||||
# see here: https://blog.miguelgrinberg.com/post/video-streaming-with-flask
|
||||
|
||||
self._moduleLogger.info('Started web interface')
|
||||
self._app.run(debug=False, threaded=True)
|
||||
|
||||
def stop(self):
|
||||
self.terminate()
|
||||
self.join()
|
||||
self._moduleLogger.info('Terminated web interface')
|
||||
# TODO: not sure exactly how threaded=True works, intended to enable
|
||||
# multiple connections. May want to use something more robust w/ camera
|
||||
# see here: https://blog.miguelgrinberg.com/post/video-streaming-with-flask
|
||||
logger.info('Starting web interface')
|
||||
app.run(debug=False, threaded=True)
|
||||
|
|
Loading…
Reference in New Issue