add janus, remove camera, and merge webInterface into parent process

This commit is contained in:
petrucci4prez 2017-05-21 14:29:17 -04:00
parent a93f116aae
commit 06a8a4443f
15 changed files with 2644 additions and 203 deletions

View File

@ -113,6 +113,7 @@ class KeypadListener(Thread):
except AttributeError: except AttributeError:
pass pass
# TODO: these are not threadsafe
# TODO: this code gets really confused if the pipe is deleted # TODO: this code gets really confused if the pipe is deleted
class PipeListener(Thread): class PipeListener(Thread):
def __init__(self, callback, path): def __init__(self, callback, path):

23
main.py
View File

@ -3,7 +3,7 @@
import sys, os, time, signal, traceback import sys, os, time, signal, traceback
import RPi.GPIO as GPIO import RPi.GPIO as GPIO
from queue import Queue from queue import Queue
from multiprocessing.managers import BaseManager, DictProxy from multiprocessing.managers import BaseManager
def clean(): def clean():
GPIO.cleanup() GPIO.cleanup()
@ -13,11 +13,6 @@ def clean():
except NameError: except NameError:
pass pass
try:
webInterface.stop() # Kill process 1
except NameError:
pass
try: try:
logger.info('Terminated root process - PID: %s', os.getpid()) logger.info('Terminated root process - PID: %s', os.getpid())
logger.stop() logger.stop()
@ -38,11 +33,7 @@ class ResourceManager(BaseManager):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
from camera import Camera
from microphone import Microphone
self.register('Camera', Camera)
self.register('Queue', Queue) self.register('Queue', Queue)
self.register('Dict', dict, DictProxy)
def __del__(self): def __del__(self):
self.shutdown() self.shutdown()
@ -58,22 +49,18 @@ if __name__ == '__main__':
manager.start() # Child process 1 manager.start() # Child process 1
loggerQueue = manager.Queue() # used to buffer logs 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 ttsQueue = manager.Queue() # used as buffer for TTS Engine
from sharedLogging import MasterLogger from sharedLogging import MasterLogger
logger = MasterLogger(__name__, 'DEBUG', loggerQueue) logger = MasterLogger(__name__, 'DEBUG', loggerQueue)
from notifier import criticalError from notifier import criticalError
from stateMachine import StateMachine from stateMachine import StateMachine
stateMachine = StateMachine(camera, ttsQueue, stateDict) stateMachine = StateMachine()
from webInterface import WebInterface
webInterface = WebInterface(camera, stateDict, ttsQueue, loggerQueue)
webInterface.start() # Child process 2
# TODO: segfaults are annoying :(
#~ signal.signal(signal.SIGSEGV, sig_handler)
signal.signal(signal.SIGTERM, sigtermHandler) signal.signal(signal.SIGTERM, sigtermHandler)
while 1: while 1:

View File

@ -7,7 +7,10 @@ def SlaveLogger(name, level, queue):
logger.addHandler(QueueHandler(queue)) logger.addHandler(QueueHandler(queue))
logger.propagate = False logger.propagate = False
return logger 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(): class MasterLogger():
def __init__(self, name, level, queue): def __init__(self, name, level, queue):
consoleFormat = logging.Formatter('[%(name)s] [%(levelname)s] %(message)s') consoleFormat = logging.Formatter('[%(name)s] [%(levelname)s] %(message)s')

View File

@ -3,6 +3,7 @@ from pygame import mixer
from subprocess import call from subprocess import call
from collections import OrderedDict from collections import OrderedDict
from auxilary import async from auxilary import async
from queue import Queue
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -66,7 +67,7 @@ class SoundLib:
_sentinel = None _sentinel = None
def __init__(self, ttsQueue): def __init__(self):
mixer.pre_init(frequency=44100, size=-16, channels=2, buffer=1024) mixer.pre_init(frequency=44100, size=-16, channels=2, buffer=1024)
mixer.init() mixer.init()
@ -88,7 +89,7 @@ class SoundLib:
self.volume = 100 self.volume = 100
self._applyVolumesToSounds(self.volume) self._applyVolumesToSounds(self.volume)
self._ttsQueue = ttsQueue self._ttsQueue = Queue()
self._stop = threading.Event() self._stop = threading.Event()
self._startMonitor() self._startMonitor()
@ -101,21 +102,7 @@ class SoundLib:
self._applyVolumesToSounds(0) self._applyVolumesToSounds(0)
def speak(self, text): def speak(self, text):
basename = hashlib.md5(text.encode()).hexdigest() self._ttsQueue.put_nowait(text)
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)
@async(daemon=False) @async(daemon=False)
def _fader(self, lowerVolume, totalDuration, fadeDuration=0.2, stepSize=5): def _fader(self, lowerVolume, totalDuration, fadeDuration=0.2, stepSize=5):
@ -156,6 +143,7 @@ class SoundLib:
for name, sound in s.items(): for name, sound in s.items():
sound.set_volume(v) sound.set_volume(v)
# TODO: maybe could simply now that we are not using MP for TTS
def _ttsMonitor(self): def _ttsMonitor(self):
q = self._ttsQueue q = self._ttsQueue
has_task_done = hasattr(q, 'task_done') has_task_done = hasattr(q, 'task_done')
@ -164,7 +152,7 @@ class SoundLib:
text = self._ttsQueue.get(True) text = self._ttsQueue.get(True)
if text is self._sentinel: if text is self._sentinel:
break break
self.speak(text) self._playSpeech(text)
if has_task_done: if has_task_done:
q.task_done() q.task_done()
except queue.Empty: except queue.Empty:
@ -175,12 +163,29 @@ class SoundLib:
text = self._ttsQueue.get(False) text = self._ttsQueue.get(False)
if text is self._sentinel: if text is self._sentinel:
break break
self.speak(text) self._playSpeech(text)
if has_task_done: if has_task_done:
q.task_done() q.task_done()
except queue.Empty: except queue.Empty:
break 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): def _startMonitor(self):
self._thread = t = threading.Thread(target=self._ttsMonitor, daemon=True) self._thread = t = threading.Thread(target=self._ttsMonitor, daemon=True)
t.start() t.start()

View File

@ -11,6 +11,7 @@ from notifier import intruderAlert
from listeners import KeypadListener, PipeListener from listeners import KeypadListener, PipeListener
from blinkenLights import Blinkenlights from blinkenLights import Blinkenlights
from soundLib import SoundLib from soundLib import SoundLib
from webInterface import initWebInterface
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -36,17 +37,17 @@ class State:
def entry(self): def entry(self):
logger.debug('entering ' + self.name) logger.debug('entering ' + self.name)
#~ if self.sound: if self.sound:
#~ self.sound.play() self.sound.play()
#~ self.stateMachine.LED.blink = self.blinkLED self.stateMachine.LED.blink = self.blinkLED
#~ self.stateMachine.keypadListener.resetBuffer() self.stateMachine.keypadListener.resetBuffer()
for c in self.entryCallbacks: for c in self.entryCallbacks:
c() c()
def exit(self): def exit(self):
logger.debug('exiting ' + self.name) logger.debug('exiting ' + self.name)
#~ if self.sound: if self.sound:
#~ self.sound.stop() self.sound.stop()
for c in self.exitCallbacks: for c in self.exitCallbacks:
c() c()
@ -64,9 +65,8 @@ class State:
return hash(self.name) return hash(self.name)
class StateMachine: class StateMachine:
def __init__(self, camera, ttsQueue, sharedState): def __init__(self):
self.sharedState = sharedState self.soundLib = SoundLib()
self.soundLib = SoundLib(ttsQueue)
self._cfg = ConfigFile('config.yaml') self._cfg = ConfigFile('config.yaml')
def startTimer(t, sound): def startTimer(t, sound):
@ -131,7 +131,7 @@ class StateMachine:
self._lock = Lock() self._lock = Lock()
#~ self.LED = Blinkenlights(17) self.LED = Blinkenlights(17)
def action(): def action():
if self.currentState == self.armed: if self.currentState == self.armed:
@ -142,17 +142,17 @@ class StateMachine:
self.selectState(SIGNALS.TRIGGER) self.selectState(SIGNALS.TRIGGER)
while GPIO.input(pin): while GPIO.input(pin):
path = '/mnt/glusterfs/pyledriver/images/%s.jpg' path = '/mnt/glusterfs/pyledriver/images/%s.jpg'
with open(path % datetime.now(), 'wb') as f: #~ with open(path % datetime.now(), 'wb') as f:
f.write(camera.getFrame()) #~ f.write(camera.getFrame())
time.sleep(0.2) time.sleep(0.2)
#~ setupMotionSensor(5, 'Nate\'s room', action) setupMotionSensor(5, 'Nate\'s room', action)
#~ setupMotionSensor(19, 'front door', action) setupMotionSensor(19, 'front door', action)
#~ setupMotionSensor(26, 'Laura\'s room', action) setupMotionSensor(26, 'Laura\'s room', action)
#~ setupMotionSensor(6, 'deck window', partial(actionVideo, 6)) setupMotionSensor(6, 'deck window', partial(actionVideo, 6))
#~ setupMotionSensor(13, 'kitchen bar', partial(actionVideo, 13)) setupMotionSensor(13, 'kitchen bar', partial(actionVideo, 13))
#~ setupDoorSensor(22, action, self.soundLib.soundEffects['door']) setupDoorSensor(22, action, self.soundLib.soundEffects['door'])
secretTable = { secretTable = {
"dynamoHum": partial(self.selectState, SIGNALS.DISARM), "dynamoHum": partial(self.selectState, SIGNALS.DISARM),
@ -160,35 +160,36 @@ class StateMachine:
"imTheSlime": partial(self.selectState, SIGNALS.INSTANT_ARM) "imTheSlime": partial(self.selectState, SIGNALS.INSTANT_ARM)
} }
#~ def secretCallback(secret, logger): def secretCallback(secret, logger):
#~ if secret in secretTable: if secret in secretTable:
#~ secretTable[secret]() secretTable[secret]()
#~ logger.debug('Secret pipe listener received: \"%s\"', secret) logger.debug('Secret pipe listener received: \"%s\"', secret)
#~ elif logger: elif logger:
#~ logger.error('Secret pipe listener received invalid secret') logger.error('Secret pipe listener received invalid secret')
#~ self.secretListener = PipeListener( self.secretListener = PipeListener(
#~ callback = secretCallback, callback = secretCallback,
#~ path = '/tmp/secret' path = '/tmp/secret'
#~ ) )
#~ def ttsCallback(text, logger): def ttsCallback(text, logger):
#~ self.soundLib.speak(text) self.soundLib.speak(text)
#~ logger.debug('TTS pipe listener received text: \"%s\"', text) logger.debug('TTS pipe listener received text: \"%s\"', text)
#~ self.ttsListener = PipeListener( self.ttsListener = PipeListener(
#~ callback = ttsCallback, callback = ttsCallback,
#~ path = '/tmp/tts' path = '/tmp/tts'
#~ ) )
#~ self.keypadListener = KeypadListener( self.keypadListener = KeypadListener(
#~ stateMachine = self, stateMachine = self,
#~ callbackDisarm = partial(self.selectState, 'disarm'), callbackDisarm = partial(self.selectState, 'disarm'),
#~ callbackArm = partial(self.selectState, 'arm'), callbackArm = partial(self.selectState, 'arm'),
#~ soundLib = self.soundLib, soundLib = self.soundLib,
#~ passwd = '5918462' passwd = '5918462'
#~ ) )
self.sharedState['name'] = self.currentState.name initWebInterface(self)
self.currentState.entry() self.currentState.entry()
def selectState(self, signal): def selectState(self, signal):
@ -201,7 +202,6 @@ class StateMachine:
self.currentState.entry() self.currentState.entry()
finally: finally:
self._lock.release() self._lock.release()
self.sharedState['name'] = self.currentState.name
self._cfg['state'] = self.currentState.name self._cfg['state'] = self.currentState.name
self._cfg.sync() self._cfg.sync()
@ -209,22 +209,14 @@ class StateMachine:
logger.info('state changed to %s', self.currentState) logger.info('state changed to %s', self.currentState)
def __del__(self): def __del__(self):
try: if hasattr(self, 'LED'):
self.LED.__del__() self.LED.__del__()
except AttributeError:
pass
try: if hasattr(self, 'soundLib'):
self.soundLib.__del__() self.soundLib.__del__()
except AttributeError:
pass
try: if hasattr(self, 'pipeListener'):
self.pipeListener.__del__() self.pipeListener.__del__()
except AttributeError:
pass
try: if hasattr(self, 'keypadListener'):
self.keypadListener.__del__() self.keypadListener.__del__()
except AttributeError:
pass

9
static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

25
static/css/pyledriver.css Normal file
View File

@ -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;
}

2156
static/janus.js Normal file

File diff suppressed because it is too large Load Diff

4
static/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

6
static/js/bootbox.min.js vendored Normal file

File diff suppressed because one or more lines are too long

9
static/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
static/js/spin.min.js vendored Normal file
View File

@ -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});

245
static/streamingtest.js Normal file
View File

@ -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();
}

View File

@ -1,35 +1,69 @@
<!DOCTYPE html>
<html> <html>
<link rel="stylesheet" href="{{ url_for('camPage.static', filename='index.css') }}"> <head>
<head> <meta charset="utf-8">
<title>Camera 1</title> <meta name="viewport" content="width=device-width, initial-scale=1">
</head> <title>Pyledriver</title>
<body>
<div class="titlebar"> <link rel="stylesheet" type="text/css" href="{{ url_for('siteRoot.static', filename='css/bootstrap.min.css') }}">
<b>Pyledriver Status: </b><span id='state'>{{ state }}</span> <link rel="stylesheet" type="text/css" href="{{ url_for('siteRoot.static', filename='css/pyledriver.css') }}">
</div>
<div class="camera_container"> <script type="text/javascript" src="{{ url_for('siteRoot.static', filename='jquery.min.js') }}"></script>
<div class="camera"> <script type="text/javascript" src="{{ url_for('siteRoot.static', filename='js/bootstrap.min.js') }}"></script>
<img id="bg" src="{{ url_for('camPage.videoFeed') }}"> <script type="text/javascript" src="{{ url_for('siteRoot.static', filename='js/bootbox.min.js') }}"></script>
</div> <script type="text/javascript" src="{{ url_for('siteRoot.static', filename='js/spin.min.js') }}"></script>
<div class="controls"> <script type="text/javascript" src="{{ url_for('siteRoot.static', filename='janus.js') }}"></script>
<form action="{{ url_for('camPage.index') }}" method='post' name='framerate'> <script type="text/javascript" src="{{ url_for('siteRoot.static', filename='streamingtest.js') }}"></script>
{{ camForm.hidden_tag() }} </head>
<p>Current Framerate: <span id='FPSvalue'>{{ fps }}</span></p>
<p>Set Framerate {{ camForm.fps }}</p> <body>
<p>{{ camForm.submitFPS }}</p> <div class="navbar navbar-inverse navbar-static-top">
</form> <div class="container-fluid">
<form action="{{ url_for('camPage.index') }}" method='post' name='text_to_speech'> <div class="navbar-header">
{{ ttsForm.hidden_tag() }} <span class="navbar-text"><b>Status: </b><span>{{ state }}</span></span>
<p>Speak your mind</p> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navRight">
<p>{{ ttsForm.tts }}</p> <span class="icon-bar"></span>
<p>{{ ttsForm.submitTTS }}</p> <span class="icon-bar"></span>
</form> <span class="icon-bar"></span>
<form action="{{ url_for('camPage.index') }}" method='post' name='text_to_speech'> </button>
{{ ttsForm.hidden_tag() }} </div>
<p>If something goes wrong...</p> <div class="collapse navbar-collapse" id="navRight">
<p>{{ resetForm.submitReset }}</p> <ul class="nav navbar-nav">
</form> <form action="{{ url_for('siteRoot.index') }}" method="post" name="text_to_speech" class="navbar-form" role="search">
</div> {{ ttsForm.hidden_tag() }}
</div> <div class="form-inline">
</body> <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> </html>

View File

@ -1,84 +1,48 @@
from multiprocessing import Process import logging
from sharedLogging import SlaveLogger from auxilary import async
from flask import Flask, render_template, Response, Blueprint, redirect, url_for from flask import Flask, render_template, Response, Blueprint, redirect, url_for
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms.fields import SelectField, StringField, SubmitField from wtforms.fields import StringField, SubmitField
from wtforms.validators import InputRequired from wtforms.validators import InputRequired
class CamForm(FlaskForm): logger = logging.getLogger(__name__)
fps = SelectField(choices=[(i, '%s fps' % i) for i in range(10, 31, 5)], coerce=int)
submitFPS = SubmitField('Set') # gag the flask logger unless it has something useful to say
werkzeug = logging.getLogger('werkzeug')
werkzeug.setLevel(logging.ERROR)
class TTSForm(FlaskForm): class TTSForm(FlaskForm):
tts = StringField(validators=[InputRequired()]) tts = StringField(validators=[InputRequired()])
submitTTS = SubmitField('Speak') submitTTS = SubmitField('Speak')
class ResetForm(FlaskForm):
submitReset = SubmitField('Reset')
# TODO: fix random connection fails (might be an nginx thing) # TODO: fix random connection fails (might be an nginx thing)
# TODO: show camera failed status here somewhere
class WebInterface(Process): @async(daemon=True)
def __init__(self, camera, stateDict, ttsQueue, loggerQueue): def initWebInterface(stateMachine):
self._moduleLogger = SlaveLogger(__name__, 'INFO', loggerQueue) siteRoot = Blueprint('siteRoot', __name__, static_folder='static', static_url_path='')
self._flaskLogger = SlaveLogger('werkzeug', 'ERROR', loggerQueue)
@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(): return render_template(
while 1: 'index.html',
yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + camera.getFrame() + b'\r\n') ttsForm=ttsForm,
state=stateMachine.currentState
@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'))
if resetForm.validate_on_submit() and resetForm.submitReset.data: app = Flask(__name__)
camera.reset() app.secret_key = '3276d68dac56985bea352325125641ff'
return redirect(url_for('camPage.index')) app.register_blueprint(siteRoot, url_prefix='/pyledriver')
return render_template( # TODO: not sure exactly how threaded=True works, intended to enable
'index.html', # multiple connections. May want to use something more robust w/ camera
camForm=camForm, # see here: https://blog.miguelgrinberg.com/post/video-streaming-with-flask
ttsForm=ttsForm, logger.info('Starting web interface')
resetForm=resetForm, app.run(debug=False, threaded=True)
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')