1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import errno
23 import os
24 import time
25 from datetime import datetime
26
27 import gobject
28 import gst
29 import time
30
31 from twisted.internet import reactor
32
33 from flumotion.component import feedcomponent
34 from flumotion.common import log, gstreamer, pygobject, messages, errors
35 from flumotion.common import common
36
37
38 from flumotion.component.component import moods
39 from flumotion.common.pygobject import gsignal
40
41 from flumotion.common.messages import N_
42 T_ = messages.gettexter('flumotion')
43
44 __all__ = ['Disker']
45
46
47 """
48 Disker has a property 'ical-schedule'. This allows an ical file to be
49 specified in the config and have recordings scheduled based on events.
50 This file will be monitored for changes and events reloaded if this
51 happens.
52
53 The filename of a recording started from an ical file will be produced
54 via passing the ical event summary through strftime, so that an archive
55 can encode the date and time that it was begun.
56
57 The time that will be given to strftime will be given in the timezone of
58 the ical event. In practice this will either be UTC or the local time of
59 the machine running the disker, as the ical scheduler does not
60 understand arbitrary timezones.
61 """
62
63 try:
64
65 from icalendar import Calendar
66 from dateutil import rrule
67 HAS_ICAL = True
68 except:
69 HAS_ICAL = False
70
90
91 -class Disker(feedcomponent.ParseLaunchComponent, log.Loggable):
92 componentMediumClass = DiskerMedium
93 checkOffset = True
94 pipe_template = 'multifdsink sync-method=1 name=fdsink mode=1 sync=false'
95 file = None
96 directory = None
97 location = None
98 caps = None
99
101 self.uiState.addKey('filename', None)
102 self.uiState.addKey('recording', False)
103 self.uiState.addKey('can-schedule', HAS_ICAL)
104
106 directory = properties['directory']
107
108 self.directory = directory
109
110 self.fixRenamedProperties(properties, [('rotateType', 'rotate-type')])
111
112 rotateType = properties.get('rotate-type', 'none')
113
114
115 if not rotateType in ['none', 'size', 'time']:
116 m = messages.Error(T_(N_(
117 "The configuration property 'rotate-type' should be set to "
118 "'size', time', or 'none', not '%s'. "
119 "Please fix the configuration."),
120 rotateType), id='rotate-type')
121 self.addMessage(m)
122 raise errors.ComponentSetupHandledError()
123
124
125 if rotateType in ['size', 'time']:
126 if rotateType not in properties.keys():
127 m = messages.Error(T_(N_(
128 "The configuration property '%s' should be set. "
129 "Please fix the configuration."),
130 rotateType), id='rotate-type')
131 self.addMessage(m)
132 raise errors.ComponentSetupHandledError()
133
134
135 if rotateType == 'size':
136 self.setSizeRotate(properties['size'])
137 elif rotateType == 'time':
138 self.setTimeRotate(properties['time'])
139
140
141 return self.pipe_template
142
148
154
160
170
172 if self.caps:
173 return self.caps.get_structure(0).get_name()
174
176 mime = self.get_mime()
177 if mime == 'multipart/x-mixed-replace':
178 mime += ";boundary=ThisRandomString"
179 return mime
180
182 """
183 @param filenameTemplate: strftime formatted string to decide filename
184 @param timeOrTuple: a valid time to pass to strftime, defaulting
185 to time.localtime(). A 9-tuple may be passed instead.
186 """
187 mime = self.get_mime()
188 if mime == 'application/ogg':
189 ext = 'ogg'
190 elif mime == 'multipart/x-mixed-replace':
191 ext = 'multipart'
192 elif mime == 'audio/mpeg':
193 ext = 'mp3'
194 elif mime == 'video/x-msvideo':
195 ext = 'avi'
196 elif mime == 'video/x-ms-asf':
197 ext = 'asf'
198 elif mime == 'audio/x-flac':
199 ext = 'flac'
200 elif mime == 'audio/x-wav':
201 ext = 'wav'
202 elif mime == 'video/x-matroska':
203 ext = 'mkv'
204 elif mime == 'video/x-dv':
205 ext = 'dv'
206 elif mime == 'video/x-flv':
207 ext = 'flv'
208 elif mime == 'video/mpegts':
209 ext = 'ts'
210 else:
211 ext = 'data'
212
213 self.stop_recording()
214
215 sink = self.get_element('fdsink')
216 if sink.get_state() == gst.STATE_NULL:
217 sink.set_state(gst.STATE_READY)
218
219 filename = ""
220 if not filenameTemplate:
221 filenameTemplate = self._defaultFilenameTemplate
222 filename = "%s.%s" % (common.strftime(filenameTemplate,
223 timeOrTuple or time.localtime()), ext)
224 self.location = os.path.join(self.directory, filename)
225 self.info("Changing filename to %s", self.location)
226 try:
227 self.file = open(self.location, 'a')
228 except IOError, e:
229 self.warning("Failed to open output file %s: %s",
230 self.location, log.getExceptionMessage(e))
231 m = messages.Error(T_(N_("Failed to open output file "
232 "%s. Check your permissions."
233 % (self.location,))))
234 self.addMessage(m)
235 return
236 self._plug_recording_started(self.file, self.location)
237 sink.emit('add', self.file.fileno())
238 self.uiState.set('filename', self.location)
239 self.uiState.set('recording', True)
240
241 if self.symlink_to_current_recording:
242 self.update_symlink(self.location,
243 self.symlink_to_current_recording)
244
246 if not dest.startswith('/'):
247 dest = os.path.join(self.directory, dest)
248 self.debug("updating symbolic link %s to point to %s", src, dest)
249 try:
250 try:
251 os.symlink(src, dest)
252 except OSError, e:
253 if e.errno == errno.EEXIST and os.path.islink(dest):
254 os.unlink(dest)
255 os.symlink(src, dest)
256 else:
257 raise
258 except Exception, e:
259 self.info("Failed to update link %s: %s", dest,
260 log.getExceptionMessage(e))
261 m = messages.Warning(T_(N_("Failed to update symbolic link "
262 "%s. Check your permissions."
263 % (dest,))),
264 debug=log.getExceptionMessage(e))
265 self.addMessage(m)
266
282
284 caps = pad.get_negotiated_caps()
285 if caps == None:
286 return
287
288 caps_str = gstreamer.caps_repr(caps)
289 self.debug('Got caps: %s' % caps_str)
290
291 new = True
292 if not self.caps == None:
293 self.warning('Already had caps: %s, replacing' % caps_str)
294 new = False
295
296 self.debug('Storing caps: %s' % caps_str)
297 self.caps = caps
298
299 if new and self._recordAtStart:
300 reactor.callLater(0, self.change_filename,
301 self._startFilenameTemplate)
302
303
304
309
321
371
374
377
379 if HAS_ICAL:
380 cal = Calendar.from_string(icsStr)
381 if self.icalScheduler:
382 events = self.icalScheduler.parseCalendar(cal)
383 if events:
384 self.icalScheduler.addEvents(events)
385 else:
386 self.warning("No events found in the ical string")
387 else:
388 self.warning("Cannot parse ICAL; neccesary modules not installed")
389
391 socket = 'flumotion.component.consumers.disker.disker_plug.DiskerPlug'
392
393 if socket not in self.plugs:
394 return
395 for plug in self.plugs[socket]:
396 self.debug('invoking recording_started on '
397 'plug %r on socket %s', plug, socket)
398 plug.recording_started(file, location)
399
401 socket = 'flumotion.component.consumers.disker.disker_plug.DiskerPlug'
402
403 if socket not in self.plugs:
404 return
405 for plug in self.plugs[socket]:
406 self.debug('invoking recording_stopped on '
407 'plug %r on socket %s', plug, socket)
408 plug.recording_stopped(file, location)
409
411 if event.type == gst.EVENT_CUSTOM_DOWNSTREAM:
412 evt_struct = event.get_structure()
413 if evt_struct.get_name() == 'FluStreamMark':
414 if evt_struct['action'] == 'start':
415 self._on_marker_start(evt_struct['prog_id'])
416 elif evt_struct['action'] == 'stop':
417 self._on_marker_stop()
418 return True
419
422
424 tmpl = self._defaultFilenameTemplate
425 if self._marker_prefix:
426 try:
427 tmpl = '%s%s' % (self._marker_prefix % data,
428 self._defaultFilenameTemplate)
429 except TypeError, err:
430 m = messages.Warning(T_(N_('Failed expanding filename prefix: '
431 '%r <-- %r.'),
432 self._marker_prefix, data),
433 id='expand-marker-prefix')
434 self.addMessage(m)
435 self.warning('Failed expanding filename prefix: '
436 '%r <-- %r; %r' %
437 (self._marker_prefix, data, err))
438 self.change_filename(tmpl)
439