Added post roll to timelapse
You can now define an amount of seconds that should be added to the rendered timelapse (so, since the current fps setting is 25 frames per seconds, 25 * the configured amount of post roll seconds images will need to be captured). Timed timelapses add this to the actual run time of the timelapse capturing process (so if you configure 1s post roll, a timed timelapse will run 25s longer than the print) Z-triggered timelapses just create one last capture from the webcam and use this image again and again (so 25 times for a post roll of 1s). Implements #384
This commit is contained in:
parent
27322a971f
commit
f580acaf70
7 changed files with 94 additions and 17 deletions
|
|
@ -23,12 +23,13 @@ from octoprint.server.api import api
|
|||
def getTimelapseData():
|
||||
timelapse = octoprint.timelapse.current
|
||||
|
||||
type = "off"
|
||||
config = {"type": "off"}
|
||||
if timelapse is not None and isinstance(timelapse, octoprint.timelapse.ZTimelapse):
|
||||
config["type"] = "zchange"
|
||||
config["postRoll"] = timelapse.postRoll()
|
||||
elif timelapse is not None and isinstance(timelapse, octoprint.timelapse.TimedTimelapse):
|
||||
config["type"] = "timed"
|
||||
config["postRoll"] = timelapse.postRoll()
|
||||
config.update({
|
||||
"interval": timelapse.interval()
|
||||
})
|
||||
|
|
@ -64,9 +65,17 @@ def setTimelapseConfig():
|
|||
if "type" in request.values:
|
||||
config = {
|
||||
"type": request.values["type"],
|
||||
"postRoll": 0,
|
||||
"options": {}
|
||||
}
|
||||
|
||||
|
||||
if "postRoll" in request.values:
|
||||
try:
|
||||
config["postRoll"] = int(request.values["postRoll"])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if "interval" in request.values:
|
||||
interval = 10
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ default_settings = {
|
|||
"flipV": False,
|
||||
"timelapse": {
|
||||
"type": "off",
|
||||
"options": {}
|
||||
"options": {},
|
||||
"postRoll": 0
|
||||
}
|
||||
},
|
||||
"gcodeViewer": {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -5,6 +5,7 @@ function TimelapseViewModel(loginStateViewModel) {
|
|||
|
||||
self.timelapseType = ko.observable(undefined);
|
||||
self.timelapseTimedInterval = ko.observable(undefined);
|
||||
self.timelapsePostRoll = ko.observable(undefined);
|
||||
|
||||
self.persist = ko.observable(false);
|
||||
self.isDirty = ko.observable(false);
|
||||
|
|
@ -17,6 +18,9 @@ function TimelapseViewModel(loginStateViewModel) {
|
|||
self.isReady = ko.observable(undefined);
|
||||
self.isLoading = ko.observable(undefined);
|
||||
|
||||
self.timelapseTypeSelected = ko.computed(function() {
|
||||
return ("off" != self.timelapseType());
|
||||
});
|
||||
self.intervalInputEnabled = ko.computed(function() {
|
||||
return ("timed" == self.timelapseType());
|
||||
});
|
||||
|
|
@ -122,6 +126,7 @@ function TimelapseViewModel(loginStateViewModel) {
|
|||
self.save = function(data, event) {
|
||||
var data = {
|
||||
"type": self.timelapseType(),
|
||||
"postRoll": self.timelapsePostRoll(),
|
||||
"save": self.persist()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -333,7 +333,8 @@ table {
|
|||
width: 50px;
|
||||
}
|
||||
|
||||
#temp_newTemp, #temp_newBedTemp, #speed_innerWall, #speed_outerWall, #speed_fill, #speed_support, #webcam_timelapse_interval {
|
||||
#temp_newTemp, #temp_newBedTemp, #speed_innerWall, #speed_outerWall, #speed_fill, #speed_support,
|
||||
#webcam_timelapse_interval, #webcam_timelapse_postRoll {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -533,7 +533,13 @@
|
|||
<option value="timed">Timed</option>
|
||||
</select>
|
||||
|
||||
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled()">
|
||||
<label for="webcam_timelapse_postRoll">Timelapse post roll (in rendered seconds)</label>
|
||||
<div class="input-append">
|
||||
<input type="text" class="input-mini" id="webcam_timelapse_postRoll" data-bind="value: timelapsePostRoll, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser() && timelapseTypeSelected()">
|
||||
<span class="add-on">sec</span>
|
||||
</div>
|
||||
|
||||
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled">
|
||||
<label for="webcam_timelapse_interval">Interval</label>
|
||||
<div class="input-append">
|
||||
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser()">
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import subprocess
|
|||
import fnmatch
|
||||
import datetime
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
import octoprint.util as util
|
||||
|
||||
|
|
@ -72,15 +73,16 @@ def configureTimelapse(config=None, persist=False):
|
|||
current.unload()
|
||||
|
||||
type = config["type"]
|
||||
postRoll = config["postRoll"]
|
||||
if type is None or "off" == type:
|
||||
current = None
|
||||
elif "zchange" == type:
|
||||
current = ZTimelapse()
|
||||
current = ZTimelapse(postRoll=postRoll)
|
||||
elif "timed" == type:
|
||||
interval = 10
|
||||
if "options" in config and "interval" in config["options"]:
|
||||
interval = config["options"]["interval"]
|
||||
current = TimedTimelapse(interval)
|
||||
current = TimedTimelapse(postRoll=postRoll, interval=interval)
|
||||
|
||||
notifyCallbacks(current)
|
||||
|
||||
|
|
@ -90,16 +92,22 @@ def configureTimelapse(config=None, persist=False):
|
|||
|
||||
|
||||
class Timelapse(object):
|
||||
def __init__(self):
|
||||
def __init__(self, postRoll=0):
|
||||
self._logger = logging.getLogger(__name__)
|
||||
self._imageNumber = None
|
||||
self._inTimelapse = False
|
||||
self._gcodeFile = None
|
||||
|
||||
self._postRoll = postRoll
|
||||
self._postRollStart = None
|
||||
self._onPostRollDone = None
|
||||
|
||||
self._captureDir = settings().getBaseFolder("timelapse_tmp")
|
||||
self._movieDir = settings().getBaseFolder("timelapse")
|
||||
self._snapshotUrl = settings().get(["webcam", "snapshot"])
|
||||
|
||||
self._fps = 25
|
||||
|
||||
self._renderThread = None
|
||||
self._captureMutex = threading.Lock()
|
||||
|
||||
|
|
@ -111,6 +119,9 @@ class Timelapse(object):
|
|||
for (event, callback) in self.eventSubscriptions():
|
||||
eventManager().subscribe(event, callback)
|
||||
|
||||
def postRoll(self):
|
||||
return self._postRoll
|
||||
|
||||
def unload(self):
|
||||
if self._inTimelapse:
|
||||
self.stopTimelapse(doCreateMovie=False)
|
||||
|
|
@ -175,13 +186,36 @@ class Timelapse(object):
|
|||
def stopTimelapse(self, doCreateMovie=True, success=True):
|
||||
self._logger.debug("Stopping timelapse")
|
||||
|
||||
if doCreateMovie:
|
||||
self._inTimelapse = False
|
||||
|
||||
def resetImageNumber():
|
||||
self._imageNumber = None
|
||||
|
||||
def createMovie():
|
||||
self._renderThread = threading.Thread(target=self._createMovie, kwargs={"success": success})
|
||||
self._renderThread.daemon = True
|
||||
self._renderThread.start()
|
||||
|
||||
self._imageNumber = None
|
||||
self._inTimelapse = False
|
||||
def resetAndCreate():
|
||||
resetImageNumber()
|
||||
createMovie()
|
||||
|
||||
if self._postRoll > 0:
|
||||
self._postRollStart = time.time()
|
||||
if doCreateMovie:
|
||||
self._onPostRollDone = resetAndCreate
|
||||
else:
|
||||
self._onPostRollDone = resetImageNumber
|
||||
self.processPostRoll()
|
||||
else:
|
||||
self._postRollStart = None
|
||||
if doCreateMovie:
|
||||
resetAndCreate()
|
||||
else:
|
||||
resetImageNumber()
|
||||
|
||||
def processPostRoll(self):
|
||||
pass
|
||||
|
||||
def captureImage(self):
|
||||
if self._captureDir is None:
|
||||
|
|
@ -195,6 +229,7 @@ class Timelapse(object):
|
|||
captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename})
|
||||
captureThread.daemon = True
|
||||
captureThread.start()
|
||||
return filename
|
||||
|
||||
def _captureWorker(self, filename):
|
||||
eventManager().fire(Events.CAPTURE_START, {"file": filename});
|
||||
|
|
@ -217,7 +252,7 @@ class Timelapse(object):
|
|||
|
||||
# prepare ffmpeg command
|
||||
command = [
|
||||
ffmpeg, '-i', input, '-vcodec', 'mpeg2video', '-pix_fmt', 'yuv420p', '-r', '25', '-y', '-b:v', bitrate,
|
||||
ffmpeg, '-i', input, '-vcodec', 'mpeg2video', '-pix_fmt', 'yuv420p', '-r', str(self._fps), '-y', '-b:v', bitrate,
|
||||
'-f', 'vob']
|
||||
|
||||
filters = []
|
||||
|
|
@ -275,8 +310,8 @@ class Timelapse(object):
|
|||
|
||||
|
||||
class ZTimelapse(Timelapse):
|
||||
def __init__(self):
|
||||
Timelapse.__init__(self)
|
||||
def __init__(self, postRoll=0):
|
||||
Timelapse.__init__(self, postRoll=postRoll)
|
||||
self._logger.debug("ZTimelapse initialized")
|
||||
|
||||
def eventSubscriptions(self):
|
||||
|
|
@ -289,13 +324,29 @@ class ZTimelapse(Timelapse):
|
|||
"type": "zchange"
|
||||
}
|
||||
|
||||
def processPostRoll(self):
|
||||
Timelapse.processPostRoll(self)
|
||||
|
||||
filename = os.path.join(self._captureDir, "tmp_%05d.jpg" % self._imageNumber)
|
||||
self._imageNumber += 1
|
||||
with self._captureMutex:
|
||||
self._captureWorker(filename)
|
||||
|
||||
for i in range(self._postRoll * self._fps):
|
||||
newFile = os.path.join(self._captureDir, "tmp_%05d.jpg" % (self._imageNumber))
|
||||
self._imageNumber += 1
|
||||
shutil.copyfile(filename, newFile)
|
||||
|
||||
if self._onPostRollDone is not None:
|
||||
self._onPostRollDone()
|
||||
|
||||
def _onZChange(self, event, payload):
|
||||
self.captureImage()
|
||||
|
||||
|
||||
class TimedTimelapse(Timelapse):
|
||||
def __init__(self, interval=1):
|
||||
Timelapse.__init__(self)
|
||||
def __init__(self, postRoll=0, interval=1):
|
||||
Timelapse.__init__(self, postRoll=postRoll)
|
||||
self._interval = interval
|
||||
if self._interval < 1:
|
||||
self._interval = 1 # force minimum interval of 1s
|
||||
|
|
@ -328,6 +379,10 @@ class TimedTimelapse(Timelapse):
|
|||
|
||||
def _timerWorker(self):
|
||||
self._logger.debug("Starting timer for interval based timelapse")
|
||||
while self._inTimelapse:
|
||||
while self._inTimelapse or (self._postRollStart and time.time() - self._postRollStart <= self._postRoll * self._fps):
|
||||
self.captureImage()
|
||||
time.sleep(self._interval)
|
||||
|
||||
if self._postRollStart is not None and self._onPostRollDone is not None:
|
||||
self._onPostRollDone()
|
||||
self._postRollStart = None
|
||||
|
|
|
|||
Loading…
Reference in a new issue