2013-01-03 20:38:46 +00:00
|
|
|
# coding=utf-8
|
2013-03-17 15:31:20 +00:00
|
|
|
import logging
|
|
|
|
|
2013-01-03 20:38:46 +00:00
|
|
|
__author__ = "Gina Häußge <osd@foosel.net>"
|
|
|
|
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
|
|
|
|
2013-01-18 22:23:50 +00:00
|
|
|
from octoprint.settings import settings
|
2013-03-06 22:27:16 +00:00
|
|
|
import octoprint.util as util
|
2013-01-03 20:38:46 +00:00
|
|
|
|
|
|
|
import os
|
|
|
|
import threading
|
|
|
|
import urllib
|
|
|
|
import time
|
2013-01-03 21:58:47 +00:00
|
|
|
import subprocess
|
2013-01-04 12:11:00 +00:00
|
|
|
import fnmatch
|
2013-03-06 22:27:16 +00:00
|
|
|
import datetime
|
2013-01-04 12:11:00 +00:00
|
|
|
|
2013-03-08 23:23:52 +00:00
|
|
|
import sys
|
|
|
|
|
2013-01-04 12:11:00 +00:00
|
|
|
def getFinishedTimelapses():
|
|
|
|
files = []
|
|
|
|
basedir = settings().getBaseFolder("timelapse")
|
|
|
|
for osFile in os.listdir(basedir):
|
|
|
|
if not fnmatch.fnmatch(osFile, "*.mpg"):
|
|
|
|
continue
|
2013-03-06 22:27:16 +00:00
|
|
|
statResult = os.stat(os.path.join(basedir, osFile))
|
2013-01-04 12:11:00 +00:00
|
|
|
files.append({
|
|
|
|
"name": osFile,
|
2013-03-06 22:27:16 +00:00
|
|
|
"size": util.getFormattedSize(statResult.st_size),
|
|
|
|
"bytes": statResult.st_size,
|
|
|
|
"date": util.getFormattedDateTime(datetime.datetime.fromtimestamp(statResult.st_ctime))
|
2013-01-04 12:11:00 +00:00
|
|
|
})
|
|
|
|
return files
|
2013-01-03 20:38:46 +00:00
|
|
|
|
|
|
|
class Timelapse(object):
|
|
|
|
def __init__(self):
|
2013-03-17 15:31:20 +00:00
|
|
|
self._logger = logging.getLogger(__name__)
|
|
|
|
|
2013-01-13 17:28:30 +00:00
|
|
|
self._imageNumber = None
|
|
|
|
self._inTimelapse = False
|
|
|
|
self._gcodeFile = None
|
2013-01-03 20:38:46 +00:00
|
|
|
|
2013-01-13 17:28:30 +00:00
|
|
|
self._captureDir = settings().getBaseFolder("timelapse_tmp")
|
|
|
|
self._movieDir = settings().getBaseFolder("timelapse")
|
2013-02-16 19:28:09 +00:00
|
|
|
self._snapshotUrl = settings().get(["webcam", "snapshot"])
|
2013-01-13 17:28:30 +00:00
|
|
|
|
|
|
|
self._renderThread = None
|
|
|
|
self._captureMutex = threading.Lock()
|
2013-01-03 20:38:46 +00:00
|
|
|
|
|
|
|
def onPrintjobStarted(self, gcodeFile):
|
|
|
|
self.startTimelapse(gcodeFile)
|
|
|
|
|
|
|
|
def onPrintjobStopped(self):
|
|
|
|
self.stopTimelapse()
|
|
|
|
|
|
|
|
def onZChange(self, oldZ, newZ):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def startTimelapse(self, gcodeFile):
|
2013-03-17 15:31:20 +00:00
|
|
|
self._logger.debug("Starting timelapse for %s" % gcodeFile)
|
2013-01-03 21:58:47 +00:00
|
|
|
self.cleanCaptureDir()
|
|
|
|
|
2013-01-13 17:28:30 +00:00
|
|
|
self._imageNumber = 0
|
|
|
|
self._inTimelapse = True
|
|
|
|
self._gcodeFile = os.path.basename(gcodeFile)
|
2013-01-03 20:38:46 +00:00
|
|
|
|
|
|
|
def stopTimelapse(self):
|
2013-03-17 15:31:20 +00:00
|
|
|
self._logger.debug("Stopping timelapse")
|
2013-01-13 17:28:30 +00:00
|
|
|
self._renderThread = threading.Thread(target=self._createMovie)
|
|
|
|
self._renderThread.daemon = True
|
|
|
|
self._renderThread.start()
|
2013-01-03 21:58:47 +00:00
|
|
|
|
2013-01-13 17:28:30 +00:00
|
|
|
self._imageNumber = None
|
|
|
|
self._inTimelapse = False
|
2013-01-03 20:38:46 +00:00
|
|
|
|
|
|
|
def captureImage(self):
|
2013-01-13 17:28:30 +00:00
|
|
|
if self._captureDir is None:
|
2013-03-17 15:31:20 +00:00
|
|
|
self._logger.warn("Cannot capture image, capture directory is unset")
|
2013-01-03 20:38:46 +00:00
|
|
|
return
|
|
|
|
|
2013-01-13 17:28:30 +00:00
|
|
|
with self._captureMutex:
|
|
|
|
filename = os.path.join(self._captureDir, "tmp_%05d.jpg" % (self._imageNumber))
|
2013-03-18 21:27:23 +00:00
|
|
|
self._imageNumber += 1
|
2013-03-17 15:31:20 +00:00
|
|
|
self._logger.debug("Capturing image to %s" % filename)
|
2013-01-03 20:38:46 +00:00
|
|
|
|
2013-01-13 17:28:30 +00:00
|
|
|
captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename})
|
|
|
|
captureThread.daemon = True
|
2013-01-03 20:38:46 +00:00
|
|
|
captureThread.start()
|
|
|
|
|
2013-01-13 17:28:30 +00:00
|
|
|
def _captureWorker(self, filename):
|
|
|
|
urllib.urlretrieve(self._snapshotUrl, filename)
|
2013-03-17 15:31:20 +00:00
|
|
|
self._logger.debug("Image %s captured from %s" % (filename, self._snapshotUrl))
|
2013-01-03 20:38:46 +00:00
|
|
|
|
2013-01-13 17:28:30 +00:00
|
|
|
def _createMovie(self):
|
2013-02-16 19:28:09 +00:00
|
|
|
ffmpeg = settings().get(["webcam", "ffmpeg"])
|
|
|
|
bitrate = settings().get(["webcam", "bitrate"])
|
2013-01-27 10:12:28 +00:00
|
|
|
if ffmpeg is None or bitrate is None:
|
2013-03-17 15:31:20 +00:00
|
|
|
self._logger.warn("Cannot create movie, path to ffmpeg is unset")
|
2013-01-03 21:58:47 +00:00
|
|
|
return
|
|
|
|
|
2013-01-13 17:28:30 +00:00
|
|
|
input = os.path.join(self._captureDir, "tmp_%05d.jpg")
|
|
|
|
output = os.path.join(self._movieDir, "%s_%s.mpg" % (os.path.splitext(self._gcodeFile)[0], time.strftime("%Y%m%d%H%M%S")))
|
2013-03-08 23:23:52 +00:00
|
|
|
|
|
|
|
# prepare ffmpeg command
|
|
|
|
command = [
|
|
|
|
ffmpeg, '-i', input, '-vcodec', 'mpeg2video', '-pix_fmt', 'yuv420p', '-r', '25', '-y', '-b:v', bitrate,
|
|
|
|
'-f', 'vob']
|
|
|
|
|
|
|
|
# add watermark if configured
|
|
|
|
if settings().getBoolean(["webcam", "watermark"]):
|
|
|
|
watermark = os.path.join(os.path.dirname(__file__), "static", "img", "watermark.png")
|
|
|
|
if sys.platform == "win32":
|
|
|
|
# Because ffmpeg hiccups on windows' drive letters and backslashes we have to give the watermark
|
|
|
|
# path a special treatment. Yeah, I couldn't believe it either...
|
|
|
|
watermark = watermark.replace("\\", "/").replace(":", "\\\\:")
|
2013-03-09 11:58:15 +00:00
|
|
|
command.extend(['-vf', 'movie=%s [wm]; [in][wm] overlay=10:main_h-overlay_h-10 [out]' % watermark])
|
2013-03-08 23:23:52 +00:00
|
|
|
|
|
|
|
# finalize command with output file
|
|
|
|
command.append(output)
|
|
|
|
subprocess.call(command)
|
2013-03-17 15:31:20 +00:00
|
|
|
self._logger.debug("Rendering movie to %s" % output)
|
2013-01-03 21:58:47 +00:00
|
|
|
|
|
|
|
def cleanCaptureDir(self):
|
2013-01-13 17:28:30 +00:00
|
|
|
if not os.path.isdir(self._captureDir):
|
2013-03-17 15:31:20 +00:00
|
|
|
self._logger.warn("Cannot clean capture directory, it is unset")
|
2013-01-03 21:58:47 +00:00
|
|
|
return
|
|
|
|
|
2013-01-13 17:28:30 +00:00
|
|
|
for filename in os.listdir(self._captureDir):
|
2013-01-04 12:11:00 +00:00
|
|
|
if not fnmatch.fnmatch(filename, "*.jpg"):
|
|
|
|
continue
|
2013-01-13 17:28:30 +00:00
|
|
|
os.remove(os.path.join(self._captureDir, filename))
|
2013-01-03 21:58:47 +00:00
|
|
|
|
2013-01-03 20:38:46 +00:00
|
|
|
class ZTimelapse(Timelapse):
|
|
|
|
def __init__(self):
|
|
|
|
Timelapse.__init__(self)
|
2013-03-17 15:31:20 +00:00
|
|
|
self._logger.debug("ZTimelapse initialized")
|
2013-01-03 20:38:46 +00:00
|
|
|
|
|
|
|
def onZChange(self, oldZ, newZ):
|
2013-03-17 15:31:20 +00:00
|
|
|
self._logger.debug("Z change detected, capturing image")
|
2013-01-03 20:38:46 +00:00
|
|
|
self.captureImage()
|
|
|
|
|
|
|
|
class TimedTimelapse(Timelapse):
|
|
|
|
def __init__(self, interval=1):
|
|
|
|
Timelapse.__init__(self)
|
|
|
|
|
2013-01-13 17:28:30 +00:00
|
|
|
self._interval = interval
|
|
|
|
if self._interval < 1:
|
|
|
|
self._interval = 1 # force minimum interval of 1s
|
2013-01-03 20:38:46 +00:00
|
|
|
|
2013-01-13 17:28:30 +00:00
|
|
|
self._timerThread = None
|
2013-01-03 20:38:46 +00:00
|
|
|
|
2013-03-17 15:31:20 +00:00
|
|
|
self._logger.debug("TimedTimelapse initialized")
|
|
|
|
|
2013-02-28 08:46:26 +00:00
|
|
|
def interval(self):
|
|
|
|
return self._interval
|
|
|
|
|
2013-01-04 10:08:19 +00:00
|
|
|
def onPrintjobStarted(self, filename):
|
|
|
|
Timelapse.onPrintjobStarted(self, filename)
|
2013-01-13 17:28:30 +00:00
|
|
|
if self._timerThread is not None:
|
2013-01-03 20:38:46 +00:00
|
|
|
return
|
|
|
|
|
2013-01-13 17:28:30 +00:00
|
|
|
self._timerThread = threading.Thread(target=self.timerWorker)
|
|
|
|
self._timerThread.daemon = True
|
|
|
|
self._timerThread.start()
|
2013-01-03 20:38:46 +00:00
|
|
|
|
|
|
|
def timerWorker(self):
|
2013-03-17 15:31:20 +00:00
|
|
|
self._logger.debug("Starting timer for interval based timelapse")
|
2013-01-13 17:28:30 +00:00
|
|
|
while self._inTimelapse:
|
2013-01-03 20:38:46 +00:00
|
|
|
self.captureImage()
|
2013-01-13 17:28:30 +00:00
|
|
|
time.sleep(self._interval)
|