add filedump functionality
This commit is contained in:
parent
4c2dd985a4
commit
a4a1671650
|
@ -1,6 +1,6 @@
|
||||||
gmail:
|
gmail:
|
||||||
username: natedwarshuis@gmail.com
|
|
||||||
passwd: bwsasfxqjbookmed
|
passwd: bwsasfxqjbookmed
|
||||||
recipientList:
|
recipientList:
|
||||||
- natedwarshuis@gmail.com
|
- natedwarshuis@gmail.com
|
||||||
|
username: natedwarshuis@gmail.com
|
||||||
state: disarmed
|
state: disarmed
|
||||||
|
|
|
@ -40,7 +40,7 @@ class GlusterFSHandler(TimedRotatingFileHandler):
|
||||||
if not os.path.exists(mountpoint):
|
if not os.path.exists(mountpoint):
|
||||||
raise FileNotFoundError
|
raise FileNotFoundError
|
||||||
|
|
||||||
self._mountpoint = mountpoint
|
self.mountpoint = mountpoint
|
||||||
self._server = server
|
self._server = server
|
||||||
self._volume = volume
|
self._volume = volume
|
||||||
self._options = options
|
self._options = options
|
||||||
|
@ -56,18 +56,20 @@ class GlusterFSHandler(TimedRotatingFileHandler):
|
||||||
self.setFormatter(fmt)
|
self.setFormatter(fmt)
|
||||||
|
|
||||||
def _mount(self):
|
def _mount(self):
|
||||||
if os.path.ismount(self._mountpoint):
|
if os.path.ismount(self.mountpoint):
|
||||||
# this assumes that the already-mounted device is the one intended
|
# this assumes that the already-mounted device is the one intended
|
||||||
logger.warning('Device already mounted at {}'.format(self._mountpoint))
|
logger.warning('Device already mounted at {}'.format(self.mountpoint))
|
||||||
else:
|
else:
|
||||||
dst = self._server + ':/' + self._volume
|
dst = self._server + ':/' + self._volume
|
||||||
cmd = ['mount', '-t', 'glusterfs', dst, self._mountpoint]
|
cmd = ['mount', '-t', 'glusterfs', dst, self.mountpoint]
|
||||||
if self._options:
|
if self._options:
|
||||||
cmd[1:1] = ['-o', self._options]
|
cmd[1:1] = ['-o', self._options]
|
||||||
self._run(cmd)
|
self._run(cmd)
|
||||||
|
self.isMounted = True
|
||||||
|
|
||||||
def _unmount(self):
|
def _unmount(self):
|
||||||
self._run(['umount', self._mountpoint])
|
self._run(['umount', self.mountpoint])
|
||||||
|
self.isMounted = False
|
||||||
|
|
||||||
def _run(self, cmd):
|
def _run(self, cmd):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import RPi.GPIO as GPIO
|
import RPi.GPIO as GPIO
|
||||||
import time, logging
|
import time, logging
|
||||||
from datetime import datetime
|
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
@ -12,7 +11,7 @@ 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
|
from webInterface import initWebInterface
|
||||||
from stream import initCamera
|
from stream import initCamera, FileDump
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -142,15 +141,16 @@ class StateMachine:
|
||||||
if self.currentState == self.states.armed:
|
if self.currentState == self.states.armed:
|
||||||
self.selectState(SIGNALS.TRIGGER)
|
self.selectState(SIGNALS.TRIGGER)
|
||||||
|
|
||||||
|
fileDump = FileDump()
|
||||||
|
sensitiveStates = (self.states.armed, self.states.armedCountdown, self.states.triggered)
|
||||||
|
|
||||||
def actionVideo(pin):
|
def actionVideo(pin):
|
||||||
if self.currentState in (self.states.armed, self.states.armedCountdown, self.states.triggered):
|
if self.currentState in sensitiveStates:
|
||||||
self.selectState(SIGNALS.TRIGGER)
|
self.selectState(SIGNALS.TRIGGER)
|
||||||
while GPIO.input(pin):
|
fileDump.addInitiator(pin)
|
||||||
# TODO: check that this path exists
|
while GPIO.input(pin) and self.currentState in sensitiveStates:
|
||||||
path = '/mnt/glusterfs/pyledriver/images/%s.jpg'
|
time.sleep(0.1)
|
||||||
#~ with open(path % datetime.now(), 'wb') as f:
|
fileDump.removeInitiator(pin)
|
||||||
#~ f.write(camera.getFrame())
|
|
||||||
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)
|
||||||
|
|
107
stream.py
107
stream.py
|
@ -12,10 +12,12 @@ From a logging an error handling standpoint, all 'errors' here are logged as
|
||||||
'critical' which will shut down the entire program and send an email.
|
'critical' which will shut down the entire program and send an email.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from auxilary import async, waitForPath
|
import gi, time, os, logging
|
||||||
from threading import Thread
|
from datetime import datetime
|
||||||
|
from threading import Thread, Lock
|
||||||
|
|
||||||
import gi, time, logging
|
from auxilary import async, waitForPath, mkdirSafe
|
||||||
|
from sharedLogging import gluster
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
|
@ -72,9 +74,10 @@ def eventLoop(pipeline, block=True, doProgress=False, targetState=Gst.State.PLAY
|
||||||
pName = pipeline.get_name()
|
pName = pipeline.get_name()
|
||||||
bus = pipeline.get_bus()
|
bus = pipeline.get_bus()
|
||||||
|
|
||||||
|
# TODO: add killswitch to exit this thread?
|
||||||
while 1:
|
while 1:
|
||||||
msg = bus.timed_pop(1e18 if block else 0)
|
msg = bus.timed_pop(1e18 if block else 0)
|
||||||
|
|
||||||
if not msg:
|
if not msg:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -87,9 +90,8 @@ def eventLoop(pipeline, block=True, doProgress=False, targetState=Gst.State.PLAY
|
||||||
# messages that involve manipulating the pipeline
|
# messages that involve manipulating the pipeline
|
||||||
if msgType == Gst.MessageType.REQUEST_STATE:
|
if msgType == Gst.MessageType.REQUEST_STATE:
|
||||||
state = msg.parse_request_state()
|
state = msg.parse_request_state()
|
||||||
|
|
||||||
logger.info('Setting state to %s as requested by %s',
|
logger.info('Setting state to %s as requested by %s', state.value_name, msgSrcName)
|
||||||
Gst.Element.get_state_name(state), msgSrcName)
|
|
||||||
|
|
||||||
pipeline.set_state(state)
|
pipeline.set_state(state)
|
||||||
|
|
||||||
|
@ -218,7 +220,10 @@ def eventLoop(pipeline, block=True, doProgress=False, targetState=Gst.State.PLAY
|
||||||
elif msgType == Gst.MessageType.UNKNOWN:
|
elif msgType == Gst.MessageType.UNKNOWN:
|
||||||
gstPrintMsg(pName, 'Unknown message', sName=msgSrcname)
|
gstPrintMsg(pName, 'Unknown message', sName=msgSrcname)
|
||||||
|
|
||||||
@async(daemon=True)
|
@async(daemon=True)
|
||||||
|
def mainLoop(pipeline):
|
||||||
|
eventLoop(pipeline, block=True, doProgress=False, targetState=Gst.State.PLAYING)
|
||||||
|
|
||||||
def startPipeline(pipeline, play=True):
|
def startPipeline(pipeline, play=True):
|
||||||
pName = pipeline.get_name()
|
pName = pipeline.get_name()
|
||||||
stateChange = pipeline.set_state(Gst.State.PAUSED)
|
stateChange = pipeline.set_state(Gst.State.PAUSED)
|
||||||
|
@ -259,7 +264,7 @@ def startPipeline(pipeline, play=True):
|
||||||
|
|
||||||
# ...we end up here and loop until Tool releases their next album
|
# ...we end up here and loop until Tool releases their next album
|
||||||
try:
|
try:
|
||||||
eventLoop(pipeline, block=True, doProgress=False, targetState=Gst.State.PLAYING)
|
mainLoop(pipeline)
|
||||||
except:
|
except:
|
||||||
raise GstException
|
raise GstException
|
||||||
|
|
||||||
|
@ -319,47 +324,99 @@ def initCamera(video=True, audio=True):
|
||||||
startPipeline(pipeline)
|
startPipeline(pipeline)
|
||||||
|
|
||||||
class FileDump:
|
class FileDump:
|
||||||
|
'''
|
||||||
|
Pipeline that takes audio and input from two udp ports, muxes them, and
|
||||||
|
dumps the result to a file. Intended to work with the Camera above. Will
|
||||||
|
init to a paused state, then will transition to a playing state (which will
|
||||||
|
dump the file) when at least one initiator registers with the class.
|
||||||
|
|
||||||
|
Initiators are represented by unique identifiers held in a list. The current
|
||||||
|
use case is that each identifier is for the pin of the IR sensor that
|
||||||
|
detects motion, and thus adding a pin number to the list signifies that
|
||||||
|
video/audio should be recorded
|
||||||
|
'''
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self._initiators = []
|
||||||
|
self._lock = Lock()
|
||||||
|
|
||||||
|
if not gluster.isMounted:
|
||||||
|
logger.error('Attempting to init FileDump without gluster mounted. Aborting')
|
||||||
|
raise SystemExit
|
||||||
|
|
||||||
|
self._savePath = os.path.join(gluster.mountpoint + '/video')
|
||||||
|
|
||||||
|
mkdirSafe(self._savePath, logger)
|
||||||
|
|
||||||
self.pipeline = Gst.Pipeline.new('filedump')
|
self.pipeline = Gst.Pipeline.new('filedump')
|
||||||
|
|
||||||
aSource = Gst.ElementFactory.make('udpsrc', 'audioSource')
|
aSource = Gst.ElementFactory.make('udpsrc', 'audioSource')
|
||||||
aParse = Gst.ElementFactory.make('opusparse', 'audioParse')
|
aJitBuf = Gst.ElementFactory.make('rtpjitterbuffer', 'audioJitterBuffer')
|
||||||
|
aDepay = Gst.ElementFactory.make('rtpopusdepay', 'audioDepay')
|
||||||
aQueue = Gst.ElementFactory.make('queue', 'audioQueue')
|
aQueue = Gst.ElementFactory.make('queue', 'audioQueue')
|
||||||
|
|
||||||
|
aCaps = Gst.Caps.from_string('application/x-rtp,encoding-name=OPUS,payload=96')
|
||||||
|
|
||||||
vSource = Gst.ElementFactory.make('udpsrc', 'videoSource')
|
vSource = Gst.ElementFactory.make('udpsrc', 'videoSource')
|
||||||
|
vJitBuf = Gst.ElementFactory.make('rtpjitterbuffer', 'videoJitterBuffer')
|
||||||
|
vDepay = Gst.ElementFactory.make('rtph264depay', 'videoDepay')
|
||||||
vParse = Gst.ElementFactory.make('h264parse', 'videoParse')
|
vParse = Gst.ElementFactory.make('h264parse', 'videoParse')
|
||||||
vQueue = Gst.ElementFactory.make('queue', 'videoQueue')
|
vQueue = Gst.ElementFactory.make('queue', 'videoQueue')
|
||||||
|
|
||||||
|
vCaps = Gst.Caps.from_string('application/x-rtp,encoding-name=H264,payload=96')
|
||||||
|
|
||||||
mux = Gst.ElementFactory.make('matroskamux', 'mux')
|
mux = Gst.ElementFactory.make('matroskamux', 'mux')
|
||||||
|
|
||||||
self.sink = Gst.ElementFactory.make('filesink', 'sink')
|
self.sink = Gst.ElementFactory.make('filesink', 'sink')
|
||||||
self.sink.set_property('location', '/dev/null')
|
self.sink.set_property('location', '/dev/null')
|
||||||
|
|
||||||
aSource.set_property('port', 8000)
|
aSource.set_property('port', 8002)
|
||||||
vSource.set_property('port', 9000)
|
vSource.set_property('port', 9002)
|
||||||
|
|
||||||
self.pipeline.add(aSource, aParse, aQueue, vSource, vParse, vQueue, mux, self.sink)
|
self.pipeline.add(aSource, aJitBuf, aDepay, aQueue,
|
||||||
|
vSource, vJitBuf, vDepay, vParse, vQueue, mux, self.sink)
|
||||||
|
|
||||||
linkElements(vSource, vParse)
|
|
||||||
|
linkElements(aSource, aJitBuf, aCaps)
|
||||||
|
linkElements(aJitBuf, aDepay)
|
||||||
|
linkElements(aDepay, aQueue)
|
||||||
|
linkElements(aQueue, mux)
|
||||||
|
|
||||||
|
linkElements(vSource, vJitBuf, vCaps)
|
||||||
|
linkElements(vJitBuf, vDepay)
|
||||||
|
linkElements(vDepay, vParse)
|
||||||
linkElements(vParse, vQueue)
|
linkElements(vParse, vQueue)
|
||||||
linkElements(vQueue, mux)
|
linkElements(vQueue, mux)
|
||||||
|
|
||||||
linkElements(aSource, aParse)
|
|
||||||
linkElements(aParse, aQueue)
|
|
||||||
linkElements(aQueue, mux)
|
|
||||||
|
|
||||||
linkElements(mux, self.sink)
|
linkElements(mux, self.sink)
|
||||||
|
|
||||||
startPipeline(self.pipeline, play=False)
|
# TODO: there is probably a better way to init than starting up to PAUSE
|
||||||
|
# and then dropping back down to NULL
|
||||||
|
startPipeline(self.pipeline, play=False)
|
||||||
|
|
||||||
def setPath(self, path):
|
self.pipeline.post_message(Gst.Message.new_request_state(self.pipeline, Gst.State.NULL))
|
||||||
self.sink.set_property('location', path)
|
|
||||||
|
|
||||||
def play(self):
|
def addInitiator(self, identifier):
|
||||||
self.pipeline.set_state(Gst.State.PLAYING)
|
with self._lock:
|
||||||
|
if identifier in self._initiators:
|
||||||
|
logger.warn('Identifier \'%s\' already in FileDump initiator list', identifier)
|
||||||
|
else:
|
||||||
|
self._initiators.append(identifier)
|
||||||
|
|
||||||
|
if self.pipeline.get_state(Gst.CLOCK_TIME_NONE).state == Gst.State.NULL:
|
||||||
|
filePath = os.path.join(self._savePath, '{}.mkv'.format(datetime.now()))
|
||||||
|
self.sink.set_property('location', filePath)
|
||||||
|
# TODO: cannot post messages from null to change state, is this bad?
|
||||||
|
self.pipeline.set_state(Gst.State.PLAYING)
|
||||||
|
|
||||||
def pause(self):
|
def removeInitiator(self, identifier):
|
||||||
self.pipeline.set_state(Gst.State.PAUSED)
|
with self._lock:
|
||||||
|
try:
|
||||||
|
self._initiators.remove(identifier)
|
||||||
|
except ValueError:
|
||||||
|
logger.warn('Attempted to remove nonexistant identifier \'%s\'', identifier)
|
||||||
|
|
||||||
|
if len(self._initiators) == 0:
|
||||||
|
self.pipeline.set_state(Gst.State.NULL)
|
||||||
|
|
||||||
Gst.init(None)
|
Gst.init(None)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue