Initial commit
This commit is contained in:
commit
5617c368b7
|
@ -0,0 +1,10 @@
|
||||||
|
Catpic
|
||||||
|
|
||||||
|
1) Create ./images/ directory
|
||||||
|
2) Install the python modules mentioned in requirements.txt
|
||||||
|
3) Install "imagesnap" (available from Homebrew) and Imagemagick (we need the "convert" command for jpegs)
|
||||||
|
4) Run `python3 catpic.py`
|
||||||
|
5) Browse to http://127.0.0.1:3000/
|
||||||
|
|
||||||
|
The application uses a websocket to push new frames to clients. The initial frame is loaded from /latest.jpg.
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from threading import Thread
|
||||||
|
import traceback
|
||||||
|
import os
|
||||||
|
import cherrypy
|
||||||
|
import subprocess
|
||||||
|
from base64 import b64encode
|
||||||
|
from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
|
||||||
|
from ws4py.websocket import EchoWebSocket
|
||||||
|
from time import sleep, time
|
||||||
|
|
||||||
|
|
||||||
|
INTERVAL = 2
|
||||||
|
cherrypy.config.update({'server.socket_port': 3000,
|
||||||
|
'server.socket_host': "0.0.0.0"})
|
||||||
|
WebSocketPlugin(cherrypy.engine).subscribe()
|
||||||
|
cherrypy.tools.websocket = WebSocketTool()
|
||||||
|
|
||||||
|
|
||||||
|
class CatHandler(Thread):
|
||||||
|
def __init__(self, master):
|
||||||
|
"""
|
||||||
|
Run the image creator subprocess. Every X seconds, delete all images but the newest one
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
self.master = master
|
||||||
|
self.proc = None
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
compressed = ""
|
||||||
|
self.startproc()
|
||||||
|
while True:
|
||||||
|
if time() - self.proctime > 15 * 60: # restart proc every 15 minutes
|
||||||
|
self.stopproc()
|
||||||
|
self.startproc()
|
||||||
|
start = time()
|
||||||
|
images = sorted([i.name for i in os.scandir("images")]) # newer images towards end
|
||||||
|
if images:
|
||||||
|
for imname in images[0:-1]:
|
||||||
|
os.unlink(os.path.join("images", imname))
|
||||||
|
newest = os.path.join("images", images[-1])
|
||||||
|
if newest != compressed:
|
||||||
|
subprocess.check_call(["convert", "-strip", "-interlace", "Plane", "-gaussian-blur", "0.05",
|
||||||
|
"-quality", "50%", newest, newest])
|
||||||
|
compressed = newest
|
||||||
|
with open(newest, "rb") as f:
|
||||||
|
self.master.latest = f.read()
|
||||||
|
cherrypy.engine.publish('websocket-broadcast',
|
||||||
|
"data:image/jpeg;base64,{}".format(b64encode(self.master.latest).decode("ascii")))
|
||||||
|
spent = time() - start
|
||||||
|
sleep(max(0.2, INTERVAL - spent))
|
||||||
|
|
||||||
|
def stopproc(self):
|
||||||
|
if not self.proc:
|
||||||
|
raise Exception("No proc to kill")
|
||||||
|
self.proc.kill()
|
||||||
|
self.proc.wait()
|
||||||
|
self.proc = None
|
||||||
|
|
||||||
|
def startproc(self):
|
||||||
|
if self.proc:
|
||||||
|
raise Exception("Proc already running!")
|
||||||
|
# initial cleanout
|
||||||
|
for f in os.scandir("images"):
|
||||||
|
os.unlink(f)
|
||||||
|
self.proc = subprocess.Popen(["imagesnap", "-t", str(INTERVAL)],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
cwd=os.path.join(os.path.dirname(__file__), "images"))
|
||||||
|
self.proctime = time()
|
||||||
|
|
||||||
|
|
||||||
|
class Root(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.latest = None
|
||||||
|
self.cathandler = CatHandler(self)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def index(self):
|
||||||
|
with open("page.htm") as f:
|
||||||
|
yield f.read()
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def latest_jpg(self):
|
||||||
|
if self.latest:
|
||||||
|
cherrypy.response.headers['Content-Type'] = 'image/jpeg'
|
||||||
|
yield self.latest
|
||||||
|
else:
|
||||||
|
raise cherrypy.HTTPError(404)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def ws(self):
|
||||||
|
# you can access the class instance through
|
||||||
|
handler = cherrypy.request.ws_handler
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
cherrypy.quickstart(Root(), '/', config={'/ws': {'tools.websocket.on': True,
|
||||||
|
'tools.websocket.handler_cls': EchoWebSocket}})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>CAT</title>
|
||||||
|
<script type='application/javascript' src='https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js'></script>
|
||||||
|
<script type='application/javascript'>
|
||||||
|
var last_update = Date.now() // milisecond timestamp
|
||||||
|
$(document).ready(function() {
|
||||||
|
var html = $("html");
|
||||||
|
var indicator = $(".indicator");
|
||||||
|
websocket = (window.location.href + "ws").replace("http", "ws"); // Will break if http appears later in the url
|
||||||
|
if (window.WebSocket) {
|
||||||
|
ws = new WebSocket(websocket);
|
||||||
|
}
|
||||||
|
else if (window.MozWebSocket) {
|
||||||
|
ws = MozWebSocket(websocket);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('WebSocket Not Supported');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.onbeforeunload = function(e) {
|
||||||
|
ws.close(1000);
|
||||||
|
};
|
||||||
|
ws.onmessage = function (evt) {
|
||||||
|
// console.log(evt.data)
|
||||||
|
html.css("background-image", "url(" + evt.data + ")")
|
||||||
|
console.log("Got message length " + evt.data.length)
|
||||||
|
last_update = Date.now()
|
||||||
|
};
|
||||||
|
ws.onopen = function() {
|
||||||
|
indicator.css("background-color", "#090")
|
||||||
|
console.log("connected")
|
||||||
|
};
|
||||||
|
ws.onclose = function(evt) {
|
||||||
|
console.log("disconnected")
|
||||||
|
indicator.css("background-color", "#F00")
|
||||||
|
};
|
||||||
|
setInterval(function(){
|
||||||
|
indicator.html((Date.now() - last_update)/1000)
|
||||||
|
}, 200)
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
background-attachment: fixed;
|
||||||
|
background-clip: border-box;
|
||||||
|
background-image: url(/latest.jpg);
|
||||||
|
background-position-x: 50%;
|
||||||
|
background-position-y: 50%;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
.indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 100%;
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 10px;
|
||||||
|
background-color: #FF0;
|
||||||
|
padding: 15px 0px;
|
||||||
|
line-height: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="indicator">0</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,9 @@
|
||||||
|
cheroot==6.0.0
|
||||||
|
CherryPy==13.1.0
|
||||||
|
more-itertools==4.0.1
|
||||||
|
portend==2.2
|
||||||
|
pytz==2017.3
|
||||||
|
six==1.11.0
|
||||||
|
tempora==1.10
|
||||||
|
wheel==0.24.0
|
||||||
|
ws4py==0.4.3
|
Loading…
Reference in New Issue