import logging import threading from typing import Optional import gi gi.require_version('Gst', '1.0') gi.require_version('GstApp', '1.0') # flake8: noqa from gi.repository import GLib, Gst, GstApp Gst.init(None) class Pipeline: """ A GStreamer pipeline. """ def __init__(self): self.logger = logging.getLogger('gst-pipeline') self.pipeline = Gst.Pipeline() self.pipeline.set_state(Gst.State.NULL) self.loop = Loop() self.source = None self.sink = None self.bus = self.pipeline.get_bus() self.bus.add_signal_watch() self.bus.connect('message::eos', self.on_eos) self.bus.connect('message::error', self.on_error) self.data_ready = threading.Event() self.data = None def add(self, element_name: str, *args, **props): el = Gst.ElementFactory.make(element_name, *args) for k, v in props.items(): if k == 'caps': v = Gst.caps_from_string(v) el.set_property(k, v) self.pipeline.add(el) return el def add_source(self, element_name: str, *args, **props): assert not self.source, 'A source element is already set for this pipeline' source = self.add(element_name, *args, **props) self.source = source return source def add_sink(self, element_name: str, *args, **props): assert not self.sink, 'A sink element is already set for this pipeline' sink = self.add(element_name, *args, **props) sink.connect('new-sample', self.on_buffer) sink.set_property('emit-signals', True) self.sink = sink return sink @staticmethod def link(*elements): for i, el in enumerate(elements): if i == len(elements) - 1: break el.link(elements[i + 1]) def emit(self, signal, *args, **kwargs): return self.pipeline.emit(signal, *args, **kwargs) def play(self): self.pipeline.set_state(Gst.State.PLAYING) assert self.loop, 'No GLib loop is running' self.loop.start() def pause(self): state = self.get_state() if state == Gst.State.PAUSED: self.pipeline.set_state(Gst.State.PLAYING) else: self.pipeline.set_state(Gst.State.PAUSED) def stop(self): self.pipeline.set_state(Gst.State.NULL) if self.loop: self.loop.stop() self.loop = None def get_volume(self) -> float: assert self.source, 'No source initialized' return self.source.get_property('volume') or 0 def set_volume(self, volume: float): assert self.source, 'No source initialized' self.source.set_property('volume', volume) def on_buffer(self, sink): sample = GstApp.AppSink.pull_sample(sink) buffer = sample.get_buffer() size, offset, _ = buffer.get_sizes() self.data = buffer.extract_dup(offset, size) self.data_ready.set() return False def on_eos(self, *_, **__): self.logger.info('End of stream event received') self.stop() def on_error(self, _, msg): self.logger.warning('GStreamer pipeline error: %s', msg.parse_error()) self.stop() def get_source(self): return self.source def get_sink(self): return self.sink def get_state(self) -> Gst.State: if not (self.source and self.source.current_state): self.logger.warning('Unable to get pipeline state') return Gst.State.NULL return self.source.current_state def is_playing(self) -> bool: return self.get_state() == Gst.State.PLAYING def is_paused(self) -> bool: return self.get_state() == Gst.State.PAUSED def get_position(self) -> Optional[float]: if not self.source: self.logger.warning('Unable to get pipeline state') return Gst.State.NULL pos = self.source.query_position(Gst.Format(Gst.Format.TIME)) if not pos[0]: return None return pos[1] / 1e9 def get_duration(self) -> Optional[float]: assert self.source, 'No active source found' pos = self.source.query_duration(Gst.Format(Gst.Format.TIME)) if not pos[0]: return None return pos[1] / 1e9 def is_muted(self) -> bool: if not self.source: return False return self.source.get_property('mute') def set_mute(self, mute: bool): assert self.source, 'No source specified' self.source.set_property('mute', mute) def mute(self): self.set_mute(True) def unmute(self): self.set_mute(False) def seek(self, position: float): assert self.source, 'No source specified' position = max(0, position) duration = self.get_duration() if duration and position > duration: position = duration seek_ns = int(position * 1e9) self.source.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH, seek_ns) class Loop(threading.Thread): """ Wraps the execution of a GLib main loop into its own thread. """ def __init__(self): super().__init__() self._loop = GLib.MainLoop() def run(self): assert self._loop, 'No GLib loop is running' self._loop.run() def is_running(self) -> bool: return self.is_alive() or self._loop is not None def stop(self): if not self.is_running(): return if self._loop: self._loop.quit() if threading.get_ident() != self.ident: self.join() self._loop = None # vim:sw=4:ts=4:et: