gstlal 1.13.0
Loading...
Searching...
No Matches
stream.py
1# Copyright (C) 2020 Patrick Godwin
2#
3# This program is free software; you can redistribute it and/or modify it
4# under the terms of the GNU General Public License as published by the
5# Free Software Foundation; either version 2 of the License, or (at your
6# option) any later version.
7#
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11# Public License for more details.
12#
13# You should have received a copy of the GNU General Public License along
14# with this program; if not, write to the Free Software Foundation, Inc.,
15# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16
17"""High-level tools to build GStreamer pipelines.
18
19"""
20import os
21import uuid
22from collections import namedtuple
23from collections.abc import Mapping
24from typing import Callable, Iterable, Optional, Tuple, Union
25from typing import Mapping as MappingType
26
27import gi
28
29gi.require_version('Gst', '1.0')
30gi.require_version('GstAudio', '1.0')
31gi.require_version('GLib', '2.0')
32from gi.repository import Gst, GLib
33
34from lal import LIGOTimeGPS
35
36from gstlal import datasource
37from gstlal import pipeparts
38from gstlal import pipeio
39from gstlal import simplehandler
40from gstlal.utilities.element_registry import ElementRegistry
41
42
43SourceElem = namedtuple("SourceElem", "datasource is_live gps_range state_vector dq_vector idq_series")
44Buffer = namedtuple("Buffer", "name t0 duration data caps is_gap")
45
46MessageType = Gst.MessageType
47
48
49class Stream(ElementRegistry):
50 """Class for building a GStreamer-based pipeline.
51 """
52 _gst_init = False
53 _has_elements = False
54 _caps_buffer_map = None
55
57 self,
58 name: Optional[str] = None,
59 mainloop: Optional[GLib.MainLoop] = None,
60 pipeline: Optional[Gst.Pipeline] = None,
61 handler: Optional["StreamHandler"] = None,
62 source: Optional["SourceElem"] = None,
63 head: Union[MappingType[str, Gst.Element], Gst.Element, None] = None,
64 ) -> None:
65 """Create a Stream that can be used to build a GStreamer-based pipeline.
66
67 Args:
68 name:
69 str, a name for the GStreamer pipeline (optional).
70 If not set, generates a unique name.
71 mainloop:
72 GLib.MainLoop, the GLib event loop to drive the GStreamer pipeline.
73 If not set, one will be created.
74 pipeline:
75 Gst.Pipeline, the GStreamer pipeline object that contains the pipeline graph.
76 If not set, one will be created.
77 handler:
78 StreamHandler, a handler which registers callbacks upon new bus messages and
79 stops the event loop upon EOS. If not set, one will be created.
80 source:
81 SourceElem, an object that stores source information as well as state/DQ vector
82 elements. If not set, one will be created.
83 head:
84 Union[MappingType[str, Gst.Element], Gst.Element], a pointer to the current
85 element in the pipeline. If not set, the Stream will not have any elements
86 attached to the pipeline upon instantiation.
87
88 """
89 # initialize GStreamer if needed
90 if not self._gst_init:
91 Gst.init(None)
92 self._gst_init = True
93
94 # register caps to buffer mapping
95 if self._caps_buffer_map is None:
97
98 # set up gstreamer pipeline
99 self.name = name if name else str(uuid.uuid1())
100 self.mainloop = mainloop if mainloop else GLib.MainLoop()
101 self.pipeline = pipeline if pipeline else Gst.Pipeline(self.name)
102 self.handler = handler if handler else StreamHandler(self.mainloop, self.pipeline)
103 self.head = head
104
105 # set up source elem properties
106 self.source = source if source else None
107
108 def start(self) -> None:
109 """Start the main event loop for this stream.
110
111 """
112 if self.source.is_live:
114 self.set_state(Gst.State.READY)
115 if not self.source.is_live:
116 self._seek_gps()
117 self.set_state(Gst.State.PLAYING)
118
119
120 if os.environ.get("GST_DEBUG_DUMP_DOT_DIR", False):
121 name = self.pipeline.get_name()
122 pipeparts.write_dump_dot(self.pipeline, f"{name}_PLAYING", verbose=True)
123
124
126 class SigHandler(simplehandler.OneTimeSignalHandler):
127 def do_on_call(self, signum, frame):
128 pipeparts.write_dump_dot(self.pipeline, f"{name}_SIGINT", verbose=True)
129
130 sighandler = SigHandler(self.pipeline)
131
132 self.mainloop.run()
133
134 @classmethod
136 cls,
137 data_source_info: datasource.DataSourceInfo,
138 ifos: Union[str, Iterable[str]],
139 name: Optional[str] = None,
140 verbose: bool = False,
141 state_vector: bool = False,
142 dq_vector: bool = False,
143 idq_series: bool = False
144 ) -> "Stream":
145 """Construct a Stream from a datasource.DataSourceInfo object.
146
147 Args:
148 data_source_info:
149 DataSourceInfo, the object to construct this stream with.
150 ifos:
151 Union[str, Iterable[str]], the detectors read timeseries data for.
152 name:
153 str, a name for the GStreamer pipeline (optional).
154 If not set, generates a unique name.
155 verbose:
156 bool, default False, whether to display logging/progress information.
157 state_vector:
158 bool, default False, whether to attach state vector information to this Stream
159 dq_vector:
160 bool, default False, whether to attach data quality vector information to this Stream
161 idq_series:
162 bool, default False, whether to fetch idq data information with this Stream
163
164 Returns:
165 Stream, the newly created stream.
166
167 """
168 is_live = data_source_info.data_source in datasource.KNOWN_LIVE_DATASOURCES
169 if isinstance(ifos, str):
170 ifos = [ifos]
171 keyed = False
172 else:
173 keyed = True
174
175 stream = cls(name=name, head={})
176 state_vectors = {}
177 dq_vectors = {}
178 idq_series_data = {}
179 for ifo in ifos:
180 src, state_vectors[ifo], dq_vectors[ifo], idq_series_data[ifo] = datasource.mkbasicsrc(
181 stream.pipeline,
182 data_source_info,
183 ifo,
184 verbose=verbose
185 )
186 stream[ifo] = cls(
187 name=stream.name,
188 mainloop=stream.mainloop,
189 pipeline=stream.pipeline,
190 handler=stream.handler,
191 head=src,
192 )
193
194 stream.source = SourceElem(
195 datasource=data_source_info.data_source,
196 is_live=is_live,
197 gps_range=data_source_info.seg,
198 state_vector=state_vectors if state_vector else None,
199 dq_vector=dq_vectors if dq_vector else None,
200 idq_series = idq_series_data if idq_series else None,
201 )
202
203 if keyed:
204 return stream
205 else:
206 return stream[ifos[0]]
207
208 def connect(self, *args, **kwargs) -> None:
209 """Attach a callback to one of this element's signals.
210
211 """
212 self.head.connect(*args, **kwargs)
213
214 def bufsink(self,
215 func: Callable[[Buffer], None],
216 caps: Optional[Gst.Caps] = None
217 ) -> None:
218 """Terminate this stream with an appsink element and process new buffers with a callback.
219
220 Args:
221 func:
222 Callable[[Buffer], None], a callback that gets invoked when a new buffer is available
223 caps:
224 Gst.Caps, how to interpret the contents of the raw buffers.
225 If not set, defaults to raw audio buffers (audio/x-raw).
226
227 """
228
229 def sample_handler(elem: Gst.Element):
230 buf = self._pull_buffer(elem, caps=caps)
231 func(buf)
232 return Gst.FlowReturn.OK
233
234 if isinstance(self.head, Mapping):
235 self._appsync = pipeparts.AppSync(appsink_new_buffer=sample_handler)
236 for key in self.keys():
237 self._appsync.add_sink(self.pipeline, self.head[key], name=key)
238 else:
239 sink = pipeparts.mkappsink(self.pipeline, self.head, max_buffers=1, sync=False)
240 sink.connect("new-sample", sample_handler)
241 sink.connect("new-preroll", self._preroll_handler)
242
243 def add_callback(self, msg_type: Gst.MessageType, *args) -> None:
244 """Attach a callback which get invoked when new bus messages are available.
245
246 Args:
247 msg_type:
248 Gst.MessageType, the type of message to invoke a callback for.
249 *args:
250 extra arguments
251
252 """
253 self.handler.add_callback(msg_type, *args)
254
255 def set_state(self, state: Gst.State) -> None:
256 """Set pipeline state, checking for errors.
257
258 Args:
259 state:
260 Gst.State: The state to set this stream's pipeline to.
261
262 Raises:
263 RuntimeError:
264 If the pipeline failed to transition to the state specified.
265
266 """
267 if self.pipeline.set_state(state) == Gst.StateChangeReturn.FAILURE:
268 raise RuntimeError(f"pipeline failed to enter {state.value_name}")
269
270 def get_element_by_name(self, name: str) -> Gst.Element:
271 """Retrieve an element from the stream's pipeline by name.
272
273 Args:
274 name:
275 str, the name of the element to retrieve
276
277 Returns:
278 Gst.Element, the element associated with the name given.
279
280 """
281 return self.pipeline.get_by_name(name)
282
283 def post_message(self, msg_name: None, timestamp: Optional[int] = None) -> None:
284 """Post a new application message to this stream's bus.
285
286 Args:
287 msg_name:
288 str, the name of the application message to send.
289 timestamp:
290 (int, optional), the timestamp to attach to this message.
291
292 """
293 s = Gst.Structure.new_empty(msg_name)
294 message = Gst.Message.new_application(self.pipeline, s)
295 if timestamp:
296 message.timestamp = timestamp
297 self.pipeline.get_bus().post(message)
298
299 def __getitem__(self, key: str) -> "Stream":
300 """Retrieves a new Stream with specified key.
301
302 """
303 return self.__class__(
304 name=self.name,
305 mainloop=self.mainloop,
306 pipeline=self.pipeline,
307 handler=self.handler,
308 source=self.source,
309 head=self.head.setdefault(key, {}),
310 )
311
312 def __setitem__(self, key: str, value: "Stream") -> None:
313 """Attach a new Stream with specified key/value pair.
314
315 """
316 if self.pipeline:
317 assert self.name == value.name
318 assert self.mainloop is value.mainloop
319 assert self.pipeline is value.pipeline
320 assert self.handler is value.handler
321 assert self.source is value.source
322 else:
323 self.name = value.name
324 self.mainloop = value.mainloop
325 self.pipeline = value.pipeline
326 self.handler = value.handler
327 self.source = value.source
328
329 self.head[key] = value.head
330
331 def keys(self) -> Iterable[str]:
332 yield from self.head.keys()
333
334 def values(self) -> Iterable["Stream"]:
335 for key in self.keys():
336 yield self[key]
337
338 def items(self) -> Iterable[Tuple[str, "Stream"]]:
339 for key in self.keys():
340 yield key, self[key]
341
342 def clear(self) -> "Stream":
343 """Return a new stream with all pointers to elements cleared out.
344
345 """
346 return self.__class__(
347 name=self.name,
348 mainloop=self.mainloop,
349 pipeline=self.pipeline,
350 handler=self.handler,
351 source=self.source,
352 head={},
353 )
354
355 def _seek_gps(self) -> None:
356 """Seek pipeline to the given gps start/end times.
357
358 """
359 start, end = self.source.gps_range
360 datasource.pipeline_seek_for_gps(self.pipeline, start, end)
361
362 @classmethod
363 def _pull_buffer(cls, elem: Gst.Element, caps: Optional[Gst.Caps] = None):
364 # get buffer
365 sample = elem.emit("pull-sample")
366 buf = sample.get_buffer()
367 buftime = LIGOTimeGPS(0, buf.pts)
368 is_gap = bool(buf.mini_object.flags & Gst.BufferFlags.GAP)
369
370 if is_gap:
371 data = None
372 else:
373 # read from buffer
374 if caps:
375 data = []
376 for i in range(buf.n_memory()):
377 memory = buf.peek_memory(i)
378 success, mapinfo = memory.map(Gst.MapFlags.READ)
379 assert success
380 if mapinfo.data:
381 # FIXME: gst-python 1.18 returns a memoryview
382 # instead of a read-only bytes-like object, so
383 # cast to bytes. this is likely inefficient but
384 # a proper solution will require .from_buffer()
385 # to leverage the buffer protocol instead
386 rows = cls._caps_buffer_map[caps.to_string()](bytes(mapinfo.data))
387 data.extend(rows)
388
389 memory.unmap(mapinfo)
390
391 else:
392 data = pipeio.array_from_audio_sample(sample)
393
394 return Buffer(
395 name=elem.name,
396 t0=buftime,
397 duration=buf.duration,
398 data=data,
399 caps=sample.get_caps(),
400 is_gap=is_gap,
401 )
402
403 @classmethod
404 def _load_caps_buffer_map(cls) -> None:
405 bufmap = {}
406 # load table definitions if available
407 # FIXME: this is really ugly, revisit this with importlib or similar
408 try:
409 from gstlal.snglinspiraltable import GSTLALSnglInspiral
410 except ImportError:
411 pass
412 else:
413 bufmap["application/x-lal-snglinspiral"] = GSTLALSnglInspiral.from_buffer
414
415 try:
416 from gstlal.snglbursttable import GSTLALSnglBurst
417 except ImportError:
418 pass
419 else:
420 bufmap["application/x-lal-snglburst"] = GSTLALSnglBurst.from_buffer
421
422 try:
423 from gstlal.sngltriggertable import GSTLALSnglTrigger
424 except ImportError:
425 pass
426 else:
427 bufmap["application/gstlal-sngltrigger"] = GSTLALSnglTrigger.from_buffer
428
429 cls._caps_buffer_map = bufmap
430
431 @staticmethod
432 def _preroll_handler(elem: Gst.Element) -> Gst.FlowReturn:
433 buf = elem.emit("pull-preroll")
434 del buf
435 return Gst.FlowReturn.OK
436
437
439 def __init__(self, *args, **kwargs):
440 super().__init__(*args, **kwargs)
441
442 # set up callbacks
443 self.callbacks = {
444 Gst.MessageType.ELEMENT: {},
445 Gst.MessageType.APPLICATION: {},
446 Gst.MessageType.EOS: {},
447 }
448
449 def add_callback(self, msg_type: Gst.MessageType, *args) -> None:
450 """Attach a callback which get invoked when new bus messages are available.
451
452 Args:
453 msg_type:
454 Gst.MessageType, the type of message to invoke a callback for.
455 *args:
456 extra arguments
457
458 """
459 # FIXME: would be better to rearrange the method signature so
460 # this extra step to determine args doesn't need to be done
461 if len(args) == 1:
462 msg_name = None
463 callback = args[0]
464 else:
465 msg_name, callback = args
466 if msg_name in self.callbacks[msg_type]:
467 raise ValueError("callback already registered for message type/name")
468 self.callbacks[msg_type][msg_name] = callback
469
470 def do_on_message(self, bus: Gst.Bus, message: Gst.Message):
471 """Invoke registered callbacks when new bus messages are received.
472
473 Args:
474 bus:
475 Gst.Bus, the GStreamer bus.
476 message:
477 Gst.Message, the message received.
478
479 Returns:
480 bool, whether further message handling is performed by the parent class
481 with default cases for EOS, INFO, WARNING and ERROR messages.
482
483 """
484 if message.type in self.callbacks:
485 if message.type == Gst.MessageType.EOS:
486 # EOS messages don't have specific subtypes so we don't
487 # parse the message's structure to determine how to proceed
488 message_name = None
489 elif message.get_structure():
490 message_name = message.get_structure().get_name()
491 else:
492 return False
493 if message_name in self.callbacks[message.type]:
494 self.callbacks[message.type][message_name](message)
495 return False
do_on_message(self, Gst.Bus bus, Gst.Message message)
Definition stream.py:470
None add_callback(self, Gst.MessageType msg_type, *args)
Definition stream.py:449
None __setitem__(self, str key, "Stream" value)
Definition stream.py:312
"Stream" __getitem__(self, str key)
Definition stream.py:299
None bufsink(self, Callable[[Buffer], None] func, Optional[Gst.Caps] caps=None)
Definition stream.py:217
None start(self)
Definition stream.py:108
Gst.Element get_element_by_name(self, str name)
Definition stream.py:270
None _load_caps_buffer_map(cls)
Definition stream.py:404
"Stream" from_datasource(cls, datasource.DataSourceInfo data_source_info, Union[str, Iterable[str]] ifos, Optional[str] name=None, bool verbose=False, bool state_vector=False, bool dq_vector=False, bool idq_series=False)
Definition stream.py:144
None connect(self, *args, **kwargs)
Definition stream.py:208
None __init__(self, Optional[str] name=None, Optional[GLib.MainLoop] mainloop=None, Optional[Gst.Pipeline] pipeline=None, Optional["StreamHandler"] handler=None, Optional["SourceElem"] source=None, Union[MappingType[str, Gst.Element], Gst.Element, None] head=None)
Definition stream.py:64
_pull_buffer(cls, Gst.Element elem, Optional[Gst.Caps] caps=None)
Definition stream.py:363
None post_message(self, None msg_name, Optional[int] timestamp=None)
Definition stream.py:283
pipeline
Setup a signal handler to intercept SIGINT in order to write the pipeline graph at ctrl+C before clea...
Definition stream.py:101
None set_state(self, Gst.State state)
Definition stream.py:255
Iterable[str] keys(self)
Definition stream.py:331
None _seek_gps(self)
Definition stream.py:355
"Stream" clear(self)
Definition stream.py:342
None add_callback(self, Gst.MessageType msg_type, *args)
Definition stream.py:243