1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import gst
23 from gst.extend import discoverer
24
25 import time
26 import calendar
27 from StringIO import StringIO
28
29 from xml.dom import Node
30
31 from twisted.internet import reactor
32
33 from flumotion.common import log, fxml
34
36 - def __init__(self, id, timestamp, uri, offset, duration):
37 self.id = id
38 self.timestamp = timestamp
39 self.uri = uri
40 self.offset = offset
41 self.duration = duration
42
43 self.hasAudio = True
44 self.hasVideo = True
45
46 self.next = None
47 self.prev = None
48
50 logCategory = 'playlist-list'
51
53 """
54 Create an initially empty playlist
55 """
56 self.items = None
57 self._itemsById = {}
58
59 self.producer = producer
60
62
63 cur = self.items
64 while cur:
65 if cur.timestamp < position and \
66 cur.timestamp + cur.duration > position:
67 return cur
68 if cur.timestamp > position:
69 return None
70 cur = cur.next
71 return None
72
79
96
97 - def addItem(self, id, timestamp, uri, offset, duration, hasAudio, hasVideo):
98 """
99 Add an item to the playlist.
100
101 This may remove overlapping entries, or adjust timestamps/durations of
102 entries to make the new one fit.
103 """
104 current = self._getCurrentItem()
105 if current and timestamp < current.timestamp + current.duration:
106 self.warning("New object at uri %s starts during current object, "
107 "cannot add")
108 return None
109
110 if current:
111 self.items = current
112
113 newitem = PlaylistItem(id, timestamp, uri, offset, duration)
114 newitem.hasAudio = hasAudio
115 newitem.hasVideo = hasVideo
116
117 if id in self._itemsById:
118 self._itemsById[id].append(newitem)
119 else:
120 self._itemsById[id] = [newitem]
121
122
123
124 prev = next = None
125 item = self.items
126 while item:
127 if item.timestamp < newitem.timestamp:
128 prev = item
129 else:
130 break
131 item = item.next
132
133 if prev:
134 item = prev.next
135 while item:
136 if (item.timestamp > newitem.timestamp and
137 item.timestamp + item.duration >
138 newitem.timestamp + newitem.duration):
139 next = item
140 break
141 item = item.next
142
143 if prev:
144
145
146 cur = prev.next
147 while cur != next:
148 self._itemsById[cur.id].remove(cur)
149 if not self._itemsById[cur.id]:
150 del self._itemsById[cur.id]
151 self.producer.unscheduleItem(cur)
152 cur = cur.next
153
154
155 if prev:
156 prev.next = newitem
157 newitem.prev = prev
158 else:
159 self.items = newitem
160
161 if next:
162 newitem.next = next
163 next.prev = newitem
164
165
166 if prev and prev.timestamp + prev.duration > newitem.timestamp:
167 self.debug("Changing duration of previous item from %d to %d",
168 prev.duration, newitem.timestamp - prev.timestamp)
169 prev.duration = newitem.timestamp - prev.timestamp
170 self.producer.adjustItemScheduling(prev)
171
172 if next and newitem.timestamp + newitem.duration > next.timestamp:
173 self.debug("Changing timestamp of next item from %d to %d to fit",
174 newitem.timestamp, newitem.timestamp + newitem.duration)
175 ts = newitem.timestamp + newitem.duration
176 duration = next.duration - (ts - next.timestamp)
177 next.duration = duration
178 next.timestamp = ts
179 self.producer.adjustItemScheduling(next)
180
181
182 if not self.producer.scheduleItem(newitem):
183 self.debug("Failed to schedule item, unlinking")
184
185 self.unlinkItem(newitem)
186 return None
187
188 return newitem
189
191 if item.prev:
192 item.prev.next = item.next
193 else:
194 self.items = item.next
195
196 if item.next:
197 item.next.prev = item.prev
198
200 logCategory = 'playlist-parse'
201
203 self.playlist = playlist
204
205 self._pending_items = []
206 self._discovering = False
207 self._discovering_blocked = 0
208
209 self._baseDirectory = None
210
212 if not baseDir.endswith('/'):
213 baseDir = baseDir + '/'
214 self._baseDirectory = baseDir
215
217 """
218 Prevent playlist parser from running discoverer on any pending
219 playlist entries. Multiple subsequent invocations will require
220 the same corresponding number of calls to L{unblockDiscovery}
221 to resume discovery.
222 """
223 self._discovering_blocked += 1
224 self.debug(' blocking discovery: %d' % self._discovering_blocked)
225
227 """
228 Resume discovering of any pending playlist entries. If
229 L{blockDiscovery} was called multiple times multiple
230 invocations of unblockDiscovery will be required to unblock
231 the discoverer.
232 """
233 if self._discovering_blocked > 0:
234 self._discovering_blocked -= 1
235 self.debug('unblocking discovery: %d' % self._discovering_blocked)
236 if self._discovering_blocked < 1:
237 self.startDiscovery()
238
240 """
241 Initiate discovery of any pending playlist entries.
242
243 @param doSort: should the pending entries be ordered
244 chronologically before initiating discovery
245 @type doSort: bool
246 """
247 self.log('startDiscovery: discovering: %s, block: %d, pending: %d' %
248 (self._discovering, self._discovering_blocked,
249 len(self._pending_items)))
250 if not self._discovering and self._discovering_blocked < 1 \
251 and self._pending_items:
252 if doSort:
253 self._sortPending()
254 self._discoverPending()
255
257 self.debug('sort pending: %d' % len(self._pending_items))
258 if not self._pending_items:
259 return
260 sortlist = [(elt[1], elt) for elt in self._pending_items]
261 sortlist.sort()
262 self._pending_items = [elt for (ts, elt) in sortlist]
263
265 def _discovered(disc, is_media):
266 self.debug("Discovered!")
267 reactor.callFromThread(_discoverer_done, disc, is_media)
268
269 def _discoverer_done(disc, is_media):
270 if is_media:
271 self.debug("Discovery complete, media found")
272 uri = "file://" + item[0]
273 timestamp = item[1]
274 duration = item[2]
275 offset = item[3]
276 id = item[4]
277
278 hasA = disc.is_audio
279 hasV = disc.is_video
280 durationDiscovered = min(disc.audiolength,
281 disc.videolength)
282 if not duration or duration > durationDiscovered:
283 duration = durationDiscovered
284
285 if duration + offset > durationDiscovered:
286 offset = 0
287
288 if duration > 0:
289 self.playlist.addItem(id, timestamp, uri, offset, duration,
290 hasA, hasV)
291 else:
292 self.warning("Duration of item is zero, not adding")
293 else:
294 self.warning("Discover failed to find media in %s", item[0])
295
296
297
298 self.debug("Continuing on to next file in one second")
299 reactor.callLater(1, self._discoverPending)
300
301 if not self._pending_items:
302 self.debug("No more files to discover")
303 self._discovering = False
304 return
305
306 if self._discovering_blocked > 0:
307 self.debug("Discovering blocked: %d" % self._discovering_blocked)
308 self._discovering = False
309 return
310
311 self._discovering = True
312
313 item = self._pending_items.pop(0)
314
315 self.debug("Discovering file %s", item[0])
316 disc = discoverer.Discoverer(item[0])
317
318 disc.connect('discovered', _discovered)
319 disc.discover()
320
337
339 logCategory = 'playlist-xml'
340
347
351
353 """
354 Parse a playlist file. Adds the contents of the file to the existing
355 playlist, overwriting any existing entries for the same time period.
356 """
357 parser = fxml.Parser()
358
359 root = parser.getRoot(file)
360
361 node = root.documentElement
362 self.debug("Parsing playlist from file %s", file)
363 if node.nodeName != 'playlist':
364 raise fxml.ParserError("Root node is not 'playlist'")
365
366 self.blockDiscovery()
367 try:
368 for child in node.childNodes:
369 if child.nodeType == Node.ELEMENT_NODE and \
370 child.nodeName == 'entry':
371 self.debug("Parsing entry")
372 self._parsePlaylistEntry(parser, child, id)
373 finally:
374 self.unblockDiscovery()
375
376
377
379 out = []
380 for k in required:
381 if node.hasAttribute(k):
382 out.append(node.getAttribute(k))
383 else:
384 raise fxml.ParserError("Missing required attribute %s" % k)
385
386 for k in optional:
387 if node.hasAttribute(k):
388 out.append(node.getAttribute(k))
389 else:
390 out.append(None)
391 return out
392
393 - def _parsePlaylistEntry(self, parser, entry, id):
394 mandatory = ['filename', 'time']
395 optional = ['duration', 'offset']
396
397 (filename, timestamp, duration, offset) = self._parseAttributes(
398 entry, mandatory, optional)
399
400 if duration is not None:
401 duration = int(float(duration) * gst.SECOND)
402 if offset is None:
403 offset = 0
404 offset = int(offset) * gst.SECOND
405
406 timestamp = self._parseTimestamp(timestamp)
407
408
409 filename = filename.encode("UTF-8")
410
411 self.addItemToPlaylist(filename, timestamp, duration, offset, id)
412
414
415
416
417
418
419 tsmain, trailing = ts[:-4], ts[-4:]
420 if trailing[0] != '.' or trailing[3] != 'Z' or \
421 not trailing[1].isdigit() or not trailing[2].isdigit():
422 raise fxml.ParserError("Invalid timestamp %s" % ts)
423 format = "%Y-%m-%dT%H:%M:%S"
424
425 try:
426 timestruct = time.strptime(tsmain, format)
427 return int(calendar.timegm(timestruct) * gst.SECOND)
428 except ValueError:
429 raise fxml.ParserError("Invalid timestamp %s" % ts)
430