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:
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
View File

@ -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:

View File

@ -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')

View File

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

View File

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

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>
<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>

View File

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