def __init__(self, link_name, redis_host): """ Set up a new LinkConfig instance - needs to know the link name and configuration host. """ self.int_properties = [ 'port', 'jitter_buffer', 'opus_framesize', 'opus_complexity', 'bitrate', 'opus_loss_expectation' ] self.bool_properties = ['opus_dtx', 'opus_fec', 'multicast'] self.link_name = link_name self.redis_host = redis_host self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('link.%s.config' % self.link_name) self.logger.info("Connecting to configuration host %s" % self.redis_host) self.redis = None while True: try: self.redis = redis.StrictRedis(host=self.redis_host, charset="utf-8", decode_responses=True) break except Exception as e: self.logger.error( "Unable to connect to configuration host! Retrying. (%s)" % e) time.sleep(0.1)
def __init__(self, node_name, interface_name='default'): self.interface_name = interface_name self.node_name = node_name self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger( 'node.%s.audio_interface.%s' % (self.node_name, self.interface_name)) self.config = dict()
def __init__(self, node_name, link_config, audio_interface): """Sets up a new RTP receiver""" self.link_config = link_config self.audio_interface = audio_interface self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('node.%s.link.%s.%s' % (node_name, self.link_config.name, self.audio_interface.mode)) self.logger.info('Creating reception pipeline') self.build_pipeline()
class LinkConfig(object): """ The LinkConfig class encapsulates link configuration. It's genderless; a TX node should be able to set up a new link and an RX node should be able (once the TX node has specified the port caps) to configure itself to receive the stream using the data and methods in this config. """ def __init__(self, link_name, redis_host): """ Set up a new LinkConfig instance - needs to know the link name and configuration host. """ self.link_name = link_name self.redis_host = redis_host self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('link.%s.config' % self.link_name) self.logger.info("Connecting to configuration host %s" % self.redis_host) self.redis = None while True: try: self.redis = redis.StrictRedis(self.redis_host) break except Exception, e: self.logger.error( "Unable to connect to configuration host! Retrying. (%s)" % e) time.sleep(0.1)
def __init__(self, node_name, interface_name='default'): self.interface_name = interface_name self.node_name = node_name self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('node.%s.audio_interface.%s' % (self.node_name, self.interface_name)) self.config = dict()
class LinkConfig(object): """ The LinkConfig class encapsulates link configuration. It's genderless; a TX node should be able to set up a new link and an RX node should be able (once the TX node has specified the port caps) to configure itself to receive the stream using the data and methods in this config. """ def __init__(self, link_name, redis_host): """ Set up a new LinkConfig instance - needs to know the link name and configuration host. """ self.link_name = link_name self.redis_host = redis_host self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('link.%s.config' % self.link_name) self.logger.info("Connecting to configuration host %s" % self.redis_host) self.redis = None while True: try: self.redis = redis.StrictRedis(self.redis_host) break except Exception, e: self.logger.error( "Unable to connect to configuration host! Retrying. (%s)" % e ) time.sleep(0.1)
def __init__(self, node_name, link_config, audio_interface): """Sets up a new RTP transmitter""" self.link_config = link_config self.audio_interface = audio_interface self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('node.%s.link.%s.%s' % (node_name, self.link_config.name, self.audio_interface.mode)) self.logger.info('Creating transmission pipeline') self.build_pipeline()
def __init__(self, link_name, redis_host): """ Set up a new LinkConfig instance - needs to know the link name and configuration host. """ self.link_name = link_name self.redis_host = redis_host self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('link.%s.config' % self.link_name) self.logger.info("Connecting to configuration host %s" % self.redis_host) self.redis = None while True: try: self.redis = redis.StrictRedis(self.redis_host) break except Exception, e: self.logger.error( "Unable to connect to configuration host! Retrying. (%s)" % e) time.sleep(0.1)
class AudioInterface(object): """ The AudioInterface class describes an audio interface on a Node. The configuration is not shared across the network. The type property of an AudioInterface should define the mode of link operation. """ def __init__(self, node_name, interface_name='default'): self.interface_name = interface_name self.node_name = node_name self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('node.%s.audio_interface.%s' % (self.node_name, self.interface_name)) self.config = dict() def set(self, key, value): """Set a config value""" self.logger.debug("Set %s to %s" % (key, value)) self.config[key] = value def get(self, key): """Get a config value""" value = self.config[key] self.logger.debug("Fetched %s, got %s" % (key, value)) return value def __getattr__(self, key): """Convenience method to access get""" return self.get(key) def set_from_argparse(self, opts): """Set up the audio interface from argparse options""" self.set("mode", opts.mode) if opts.mode == "tx": self.set("type", opts.audio_input) self.set("samplerate", opts.samplerate) elif opts.mode == "rx": self.set("type", opts.audio_output) if self.get("type") == "alsa": self.set("alsa_device", opts.alsa_device) elif self.get("type") == "jack": if opts.jack_auto is not False: self.set("jack_auto", opts.jack_auto) if opts.jack_name is not None: self.set("jack_name", opts.jack_name) else: self.set("jack_name", "openob")
class AudioInterface(object): """ The AudioInterface class describes an audio interface on a Node. The configuration is not shared across the network. The type property of an AudioInterface should define the mode of link operation. """ def __init__(self, node_name, interface_name='default'): self.interface_name = interface_name self.node_name = node_name self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger( 'node.%s.audio_interface.%s' % (self.node_name, self.interface_name)) self.config = dict() def set(self, key, value): """Set a config value""" self.logger.debug("Set %s to %s" % (key, value)) self.config[key] = value def get(self, key): """Get a config value""" value = self.config[key] self.logger.debug("Fetched %s, got %s" % (key, value)) return value def __getattr__(self, key): """Convenience method to access get""" return self.get(key) def set_from_argparse(self, opts): """Set up the audio interface from argparse options""" self.set("mode", opts.mode) if opts.mode == "tx": self.set("type", opts.audio_input) self.set("samplerate", opts.samplerate) elif opts.mode == "rx": self.set("type", opts.audio_output) if self.get("type") == "alsa": self.set("alsa_device", opts.alsa_device) elif self.get("type") == "jack": self.set("jack_auto", opts.jack_auto) if opts.jack_name is not None: self.set("jack_name", opts.jack_name) else: self.set("jack_name", "openob") if opts.jack_port_pattern is not None: self.set("jack_port_pattern", opts.jack_port_pattern)
def __init__(self, link_name, redis_host): """ Set up a new LinkConfig instance - needs to know the link name and configuration host. """ self.link_name = link_name self.redis_host = redis_host self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('link.%s.config' % self.link_name) self.logger.info("Connecting to configuration host %s" % self.redis_host) self.redis = None while True: try: self.redis = redis.StrictRedis(self.redis_host) break except Exception, e: self.logger.error( "Unable to connect to configuration host! Retrying. (%s)" % e ) time.sleep(0.1)
def __init__(self, link_name, redis_host): """ Set up a new LinkConfig instance - needs to know the link name and configuration host. """ self.int_properties = ['port', 'jitter_buffer', 'opus_framesize', 'opus_complexity', 'bitrate', 'opus_loss_expectation'] self.bool_properties = ['opus_dtx', 'opus_fec', 'multicast'] self.link_name = link_name self.redis_host = redis_host self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('link.%s.config' % self.link_name) self.logger.info("Connecting to configuration host %s" % self.redis_host) self.redis = None while True: try: self.redis = redis.StrictRedis(self.redis_host) break except Exception as e: self.logger.error( "Unable to connect to configuration host! Retrying. (%s)" % e ) time.sleep(0.1)
class RTPTransmitter(object): def __init__(self, node_name, link_config, audio_interface): """Sets up a new RTP transmitter""" self.started = False self.caps = 'None' self.pipeline = gst.Pipeline("tx") self.bus = self.pipeline.get_bus() self.bus.connect("message", self.on_message) self.link_config = link_config self.audio_interface = audio_interface self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger( 'node.%s.link.%s.%s' % (node_name, self.link_config.name, self.audio_interface.mode)) self.logger.info("Creating RTP transmission pipeline") # Audio input if self.audio_interface.type == 'auto': self.source = gst.element_factory_make('autoaudiosrc') elif self.audio_interface.type == 'alsa': self.source = gst.element_factory_make('alsasrc') self.source.set_property('device', self.audio_interface.alsa_device) elif self.audio_interface.type == 'jack': self.source = gst.element_factory_make("jackaudiosrc") if self.audio_interface.jack_auto: self.source.set_property('connect', 'auto') else: self.source.set_property('connect', 'none') self.source.set_property('buffer-time', 50000) self.source.set_property('name', self.audio_interface.jack_name) self.source.set_property('client-name', self.audio_interface.jack_name) # Audio resampling and conversion self.audioresample = gst.element_factory_make("audioresample") self.audioconvert = gst.element_factory_make("audioconvert") self.audioresample.set_property('quality', 9) # SRC # Encoding and payloading if self.link_config.encoding == 'opus': self.encoder = gst.element_factory_make("opusenc", "encoder") self.encoder.set_property('bitrate', int(self.link_config.bitrate) * 1000) self.encoder.set_property('tolerance', 80000000) self.encoder.set_property('frame-size', self.link_config.opus_framesize) self.encoder.set_property('complexity', int(self.link_config.opus_complexity)) self.encoder.set_property('inband-fec', self.link_config.opus_fec) self.encoder.set_property( 'packet-loss-percentage', int(self.link_config.opus_loss_expectation)) self.encoder.set_property('dtx', self.link_config.opus_dtx) print(self.encoder.get_properties('bitrate', 'dtx', 'inband-fec')) self.payloader = gst.element_factory_make("rtpopuspay", "payloader") elif self.link_config.encoding == 'pcm': # we have no encoder for PCM operation self.payloader = gst.element_factory_make("rtpL16pay", "payloader") else: self.logger.critical("Unknown encoding type %s" % self.link_config.encoding) # TODO: Add a tee here, and sort out creating multiple UDP sinks for multipath # Now the RTP bits # We'll send audio out on this self.udpsink_rtpout = gst.element_factory_make("udpsink", "udpsink_rtp") self.udpsink_rtpout.set_property('host', self.link_config.receiver_host) self.udpsink_rtpout.set_property('port', self.link_config.port) self.logger.info( 'Set receiver to %s:%i' % (self.link_config.receiver_host, self.link_config.port)) if self.link_config.multicast: self.udpsink_rtpout.set_property('auto_multicast', True) self.logger.info('Multicast mode enabled') # Our RTP manager self.rtpbin = gst.element_factory_make("gstrtpbin", "gstrtpbin") self.rtpbin.set_property('latency', 0) # Our level monitor self.level = gst.element_factory_make("level") self.level.set_property('message', True) self.level.set_property('interval', 1000000000) # Add a capsfilter to allow specification of input sample rate self.capsfilter = gst.element_factory_make("capsfilter") # Add to the pipeline self.pipeline.add(self.source, self.capsfilter, self.audioresample, self.audioconvert, self.payloader, self.udpsink_rtpout, self.rtpbin, self.level) if self.link_config.encoding != 'pcm': # Only add the encoder if we're not in PCM mode self.pipeline.add(self.encoder) # Decide which format to apply to the capsfilter (Jack uses float) if self.audio_interface.type == 'jack': data_type = 'audio/x-raw-float' else: data_type = 'audio/x-raw-int' # if audio_rate has been specified, then add that to the capsfilter if self.audio_interface.samplerate != 0: self.capsfilter.set_property( "caps", gst.Caps('%s, channels=2, rate=%d' % (data_type, self.audio_interface.samplerate))) else: self.capsfilter.set_property( "caps", gst.Caps('%s, channels=2' % data_type)) # Then continue linking the pipeline together gst.element_link_many(self.source, self.capsfilter, self.level, self.audioresample, self.audioconvert) # Now we get to link this up to our encoder/payloader if self.link_config.encoding != 'pcm': gst.element_link_many(self.audioconvert, self.encoder, self.payloader) else: gst.element_link_many(self.audioconvert, self.payloader) # And now the RTP bits self.payloader.link_pads('src', self.rtpbin, 'send_rtp_sink_0') self.rtpbin.link_pads('send_rtp_src_0', self.udpsink_rtpout, 'sink') # self.udpsrc_rtcpin.link_pads('src', self.rtpbin, 'recv_rtcp_sink_0') # # RTCP SRs # self.rtpbin.link_pads('send_rtcp_src_0', self.udpsink_rtcpout, 'sink') # Connect our bus up self.bus.add_signal_watch() self.bus.connect('message', self.on_message) def run(self): self.pipeline.set_state(gst.STATE_PLAYING) while self.caps == 'None': self.logger.debug(self.udpsink_rtpout.get_state()) self.caps = str( self.udpsink_rtpout.get_pad('sink').get_property('caps')) # Fix for gstreamer bug in rtpopuspay fixed in GST-plugins-bad # 50140388d2b62d32dd9d0c071e3051ebc5b4083b, bug 686547 if self.link_config.encoding == 'opus': self.caps = re.sub(r'(caps=.+ )', '', self.caps) if self.caps == 'None': self.logger.warn("Waiting for audio interface/caps") time.sleep(0.1) def loop(self): try: self.loop = gobject.MainLoop() self.loop.run() except Exception as e: self.logger.exception( "Encountered a problem in the MainLoop, tearing down the pipeline: %s" % e) self.pipeline.set_state(gst.STATE_NULL) def on_message(self, bus, message): if message.type == gst.MESSAGE_ELEMENT: if message.structure.get_name() == 'level': if self.started is False: self.started = True #gst.DEBUG_BIN_TO_DOT_FILE(self.pipeline, gst.DEBUG_GRAPH_SHOW_ALL, 'tx-graph') #self.logger.debug(self.source.get_property('actual-buffer-time')) if len(message.structure['peak']) == 1: self.logger.info("Started mono audio transmission") else: self.logger.info("Started stereo audio transmission") return True def get_caps(self): return self.caps
class LinkConfig(object): """ The LinkConfig class encapsulates link configuration. It's genderless; a TX node should be able to set up a new link and an RX node should be able (once the TX node has specified the port caps) to configure itself to receive the stream using the data and methods in this config. """ def __init__(self, link_name, redis_host): """ Set up a new LinkConfig instance - needs to know the link name and configuration host. """ self.int_properties = ['port', 'jitter_buffer', 'opus_framesize', 'opus_complexity', 'bitrate', 'opus_loss_expectation'] self.bool_properties = ['opus_dtx', 'opus_fec', 'multicast'] self.link_name = link_name self.redis_host = redis_host self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('link.%s.config' % self.link_name) self.logger.info("Connecting to configuration host %s" % self.redis_host) self.redis = None while True: try: self.redis = redis.StrictRedis(self.redis_host) break except Exception as e: self.logger.error( "Unable to connect to configuration host! Retrying. (%s)" % e ) time.sleep(0.1) def blocking_get(self, key): """Get a value, blocking until it's not None if needed""" while True: value = self.get(key) if value is not None: self.logger.debug("Fetched (blocking) %s, got %s" % (key, value)) return value time.sleep(0.1) def set(self, key, value): """Set a value in the config store""" scoped_key = self.scoped_key(key) self.redis.set(scoped_key, value) self.logger.debug("Set %s to %s" % (scoped_key, value)) return value def get(self, key): """Get a value from the config store""" scoped_key = self.scoped_key(key) value = self.redis.get(scoped_key) # Do some typecasting if key in self.int_properties: value = int(value) if key in self.bool_properties: value = (value == 'True') self.logger.debug("Fetched %s, got %s" % (scoped_key, value)) return value def unset(self, key): scoped_key = self.scoped_key(key) self.redis.delete(scoped_key) self.logger.debug("Unset %s" % scoped_key) def __getattr__(self, key): """Convenience method to access get""" return self.get(key) def scoped_key(self, key): """Return an appropriate key name scoped to a link""" return ("openob:%s:%s" % (self.link_name, key)) def set_from_argparse(self, opts): """Given an optparse object from bin/openob, configure this link""" self.set("name", opts.link_name) if opts.mode == "tx": self.set("port", opts.port) self.set("jitter_buffer", opts.jitter_buffer) self.set("encoding", opts.encoding) self.set("bitrate", opts.bitrate) self.set("multicast", opts.multicast) self.set("input_samplerate", opts.samplerate) self.set("receiver_host", opts.receiver_host) self.set("opus_framesize", opts.framesize) self.set("opus_complexity", opts.complexity) self.set("opus_fec", opts.fec) self.set("opus_loss_expectation", opts.loss) self.set("opus_dtx", opts.dtx) def commit_changes(self, restart=False): """ To be called after calls to set() on a running link to signal a reconfiguration event for that link. If restart is True, the link should simply terminate itself so it can be restarted with the new parameters. If restart is False, the link should set all parameters it can which do not involve a restart. """ raise(NotImplementedError, "Link reconfiguration is not yet implemented")
def __init__(self, node_name, link_config, audio_interface): """Sets up a new RTP transmitter""" self.started = False self.caps = 'None' self.pipeline = gst.Pipeline("tx") self.bus = self.pipeline.get_bus() self.bus.connect("message", self.on_message) self.link_config = link_config self.audio_interface = audio_interface self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('node.%s.link.%s.%s' % (node_name, self.link_config.name, self.audio_interface.mode)) self.logger.info("Creating RTP transmission pipeline") # Audio input if self.audio_interface.type == 'auto': self.source = gst.element_factory_make('autoaudiosrc') elif self.audio_interface.type == 'alsa': self.source = gst.element_factory_make('alsasrc') self.source.set_property('device', self.audio_interface.alsa_device) elif self.audio_interface.type == 'jack': self.source = gst.element_factory_make("jackaudiosrc") if self.audio_interface.jack_auto: self.source.set_property('connect', 'auto') else: self.source.set_property('connect', 'none') self.source.set_property('buffer-time', 50000) self.source.set_property('name', self.audio_interface.jack_name) self.source.set_property('client-name', self.audio_interface.jack_name) # Audio resampling and conversion self.audioresample = gst.element_factory_make("audioresample") self.audioconvert = gst.element_factory_make("audioconvert") self.audioresample.set_property('quality', 9) # SRC # Encoding and payloading if self.link_config.encoding == 'opus': self.encoder = gst.element_factory_make("opusenc", "encoder") self.encoder.set_property('bitrate', int(self.link_config.bitrate) * 1000) self.encoder.set_property('tolerance', 80000000) self.encoder.set_property('frame-size', self.link_config.opus_framesize) self.encoder.set_property('complexity', int(self.link_config.opus_complexity)) self.encoder.set_property('inband-fec', self.link_config.opus_fec) self.encoder.set_property('packet-loss-percentage', int(self.link_config.opus_loss_expectation)) self.encoder.set_property('dtx', self.link_config.opus_dtx) print(self.encoder.get_properties('bitrate', 'dtx', 'inband-fec')) self.payloader = gst.element_factory_make("rtpopuspay", "payloader") elif self.link_config.encoding == 'pcm': # we have no encoder for PCM operation self.payloader = gst.element_factory_make("rtpL16pay", "payloader") else: self.logger.critical("Unknown encoding type %s" % self.link_config.encoding) # TODO: Add a tee here, and sort out creating multiple UDP sinks for multipath # Now the RTP bits # We'll send audio out on this self.udpsink_rtpout = gst.element_factory_make("udpsink", "udpsink_rtp") self.udpsink_rtpout.set_property('host', self.link_config.receiver_host) self.udpsink_rtpout.set_property('port', self.link_config.port) self.logger.info('Set receiver to %s:%i' % (self.link_config.receiver_host, self.link_config.port)) if self.link_config.multicast: self.udpsink_rtpout.set_property('auto_multicast', True) self.logger.info('Multicast mode enabled') # Our RTP manager self.rtpbin = gst.element_factory_make("gstrtpbin", "gstrtpbin") self.rtpbin.set_property('latency', 0) # Our level monitor self.level = gst.element_factory_make("level") self.level.set_property('message', True) self.level.set_property('interval', 1000000000) # Add a capsfilter to allow specification of input sample rate self.capsfilter = gst.element_factory_make("capsfilter") # Add to the pipeline self.pipeline.add( self.source, self.capsfilter, self.audioresample, self.audioconvert, self.payloader, self.udpsink_rtpout, self.rtpbin, self.level) if self.link_config.encoding != 'pcm': # Only add the encoder if we're not in PCM mode self.pipeline.add(self.encoder) # Decide which format to apply to the capsfilter (Jack uses float) if self.audio_interface.type == 'jack': data_type = 'audio/x-raw-float' else: data_type = 'audio/x-raw-int' # if audio_rate has been specified, then add that to the capsfilter if self.audio_interface.samplerate != 0: self.capsfilter.set_property( "caps", gst.Caps('%s, channels=2, rate=%d' % (data_type, self.audio_interface.samplerate))) else: self.capsfilter.set_property( "caps", gst.Caps('%s, channels=2' % data_type)) # Then continue linking the pipeline together gst.element_link_many( self.source, self.capsfilter, self.level, self.audioresample, self.audioconvert) # Now we get to link this up to our encoder/payloader if self.link_config.encoding != 'pcm': gst.element_link_many( self.audioconvert, self.encoder, self.payloader) else: gst.element_link_many(self.audioconvert, self.payloader) # And now the RTP bits self.payloader.link_pads('src', self.rtpbin, 'send_rtp_sink_0') self.rtpbin.link_pads('send_rtp_src_0', self.udpsink_rtpout, 'sink') # self.udpsrc_rtcpin.link_pads('src', self.rtpbin, 'recv_rtcp_sink_0') # # RTCP SRs # self.rtpbin.link_pads('send_rtcp_src_0', self.udpsink_rtcpout, 'sink') # Connect our bus up self.bus.add_signal_watch() self.bus.connect('message', self.on_message)
class RTPTransmitter(object): def __init__(self, node_name, link_config, audio_interface): """Sets up a new RTP transmitter""" self.link_config = link_config self.audio_interface = audio_interface self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('node.%s.link.%s.%s' % (node_name, self.link_config.name, self.audio_interface.mode)) self.logger.info('Creating transmission pipeline') self.build_pipeline() def run(self): self.pipeline.set_state(Gst.State.PLAYING) # Gst.debug_bin_to_dot_file(self.pipeline, Gst.DebugGraphDetails.ALL, 'tx-graph') while self.caps == None: caps = self.transport.get_by_name('udpsink').get_static_pad('sink').get_property('caps') if caps == None: self.logger.warn('Waiting for audio interface/caps') time.sleep(0.1) else: self.caps = caps.to_string() def loop(self): try: loop = GLib.MainLoop() loop.run() except Exception as e: self.logger.exception('Encountered a problem in the MainLoop, tearing down the pipeline: %s' % e) self.pipeline.set_state(Gst.State.NULL) def build_pipeline(self): self.pipeline = Gst.Pipeline.new('tx') self.started = False self.caps = None bus = self.pipeline.get_bus() self.source = self.build_audio_interface() self.encoder = self.build_encoder() self.transport = self.build_transport() self.pipeline.add(self.source) self.pipeline.add(self.encoder) self.pipeline.add(self.transport) self.source.link(self.encoder) self.encoder.link(self.transport) # Connect our bus up bus.add_signal_watch() bus.connect('message', self.on_message) def build_audio_interface(self): self.logger.debug('Building audio input bin') bin = Gst.Bin.new('audio') # Audio input if self.audio_interface.type == 'auto': source = Gst.ElementFactory.make('autoaudiosrc') elif self.audio_interface.type == 'alsa': source = Gst.ElementFactory.make('alsasrc') source.set_property('device', self.audio_interface.alsa_device) elif self.audio_interface.type == 'jack': source = Gst.ElementFactory.make('jackaudiosrc') if self.audio_interface.jack_auto: source.set_property('connect', 'auto') else: source.set_property('connect', 'none') source.set_property('buffer-time', 50000) source.set_property('name', self.audio_interface.jack_name) source.set_property('client-name', self.audio_interface.jack_name) if self.audio_interface.jack_port_pattern: source.set_property('port-pattern', self.audio_interface.jack_port_pattern) elif self.audio_interface.type == 'test': source = Gst.ElementFactory.make('audiotestsrc') bin.add(source) # Our level monitor level = Gst.ElementFactory.make('level') level.set_property('message', True) level.set_property('interval', 1000000000) bin.add(level) # Audio resampling and conversion resample = Gst.ElementFactory.make('audioresample') resample.set_property('quality', 9) # SRC bin.add(resample) convert = Gst.ElementFactory.make('audioconvert') bin.add(convert) # Add a capsfilter to allow specification of input sample rate capsfilter = Gst.ElementFactory.make('capsfilter') caps = Gst.Caps.new_empty_simple('audio/x-raw') # if audio_rate has been specified, then add that to the capsfilter if self.audio_interface.samplerate != 0: caps.set_value('rate', self.audio_interface.samplerate) self.logger.debug(caps.to_string()) capsfilter.set_property('caps', caps) bin.add(capsfilter) source.link(level) level.link(resample) resample.link(convert) convert.link(capsfilter) bin.add_pad(Gst.GhostPad.new('src', capsfilter.get_static_pad('src'))) return bin def build_encoder(self): self.logger.debug('Building encoder bin') bin = Gst.Bin.new('encoder') # Encoding and payloading if self.link_config.encoding == 'opus': encoder = Gst.ElementFactory.make('opusenc', 'encoder') encoder.set_property('bitrate', int(self.link_config.bitrate) * 1000) encoder.set_property('tolerance', 80000000) encoder.set_property('frame-size', self.link_config.opus_framesize) encoder.set_property('complexity', int(self.link_config.opus_complexity)) encoder.set_property('inband-fec', self.link_config.opus_fec) encoder.set_property('packet-loss-percentage', int(self.link_config.opus_loss_expectation)) encoder.set_property('dtx', self.link_config.opus_dtx) payloader = Gst.ElementFactory.make('rtpopuspay', 'payloader') elif self.link_config.encoding == 'pcm': # we have no encoder for PCM operation payloader = Gst.ElementFactory.make('rtpL16pay', 'payloader') else: self.logger.critical('Unknown encoding type %s' % self.link_config.encoding) bin.add(payloader) if 'encoder' in locals(): bin.add(encoder) encoder.link(payloader) bin.add_pad(Gst.GhostPad.new('sink', encoder.get_static_pad('sink'))) else: bin.add_pad(Gst.GhostPad.new('sink', payloader.get_static_pad('sink'))) bin.add_pad(Gst.GhostPad.new('src', payloader.get_static_pad('src'))) return bin def build_transport(self): self.logger.debug('Building RTP transport bin') bin = Gst.Bin.new('transport') # Our RTP manager rtpbin = Gst.ElementFactory.make('rtpbin', 'rtpbin') rtpbin.set_property('latency', 0) bin.add(rtpbin) # TODO: Add a tee here, and sort out creating multiple UDP sinks for multipath udpsink = Gst.ElementFactory.make('udpsink', 'udpsink') udpsink.set_property('host', self.link_config.receiver_host) udpsink.set_property('port', self.link_config.port) self.logger.info('Set receiver to %s:%i' % (self.link_config.receiver_host, self.link_config.port)) if self.link_config.multicast: udpsink.set_property('auto_multicast', True) self.logger.info('Multicast mode enabled') bin.add(udpsink) bin.add_pad(Gst.GhostPad.new('sink', rtpbin.get_request_pad('send_rtp_sink_0'))) rtpbin.link_pads('send_rtp_src_0', udpsink, 'sink') return bin def on_message(self, bus, message): if message.type == Gst.MessageType.ELEMENT: struct = message.get_structure() if struct != None: if struct.get_name() == 'level': if self.started is False: self.started = True if len(struct.get_value('peak')) == 1: self.logger.info('Started mono audio transmission') else: self.logger.info('Started stereo audio transmission') else: if len(struct.get_value('peak')) == 1: self.logger.debug('Level: %.2f', struct.get_value('peak')[0]) else: self.logger.debug('Levels: L %.2f R %.2f' % (struct.get_value('peak')[0], struct.get_value('peak')[1])) return True def get_caps(self): return self.caps
def __init__(self, node_name): """Set up a new node.""" self.node_name = node_name self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('node.%s' % self.node_name)
class RTPReceiver(object): def __init__(self, node_name, link_config, audio_interface): """Sets up a new RTP receiver""" self.started = False self.pipeline = gst.Pipeline("rx") self.bus = self.pipeline.get_bus() self.bus.connect("message", self.on_message) self.link_config = link_config self.audio_interface = audio_interface self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger( 'node.%s.link.%s.%s' % (node_name, self.link_config.name, self.audio_interface.mode)) caps = self.link_config.get("caps") # Audio output if self.audio_interface.type == 'auto': self.sink = gst.element_factory_make("autoaudiosink") elif self.audio_interface.type == 'alsa': self.sink = gst.element_factory_make("alsasink") self.sink.set_property('device', self.audio_interface.alsa_device) elif self.audio_interface.type == 'jack': self.sink = gst.element_factory_make("jackaudiosink") if self.audio_interface.jack_auto: self.sink.set_property('connect', 'auto') else: self.sink.set_property('connect', 'none') self.sink.set_property('name', self.audio_interface.jack_name) # Audio conversion and resampling self.audioconvert = gst.element_factory_make("audioconvert") self.audioresample = gst.element_factory_make("audioresample") self.audioresample.set_property('quality', 9) self.audiorate = gst.element_factory_make("audiorate") # Decoding and depayloading if self.link_config.encoding == 'opus': self.decoder = gst.element_factory_make("opusdec", "decoder") self.decoder.set_property('use-inband-fec', True) # FEC self.decoder.set_property('plc', True) # Packet loss concealment self.depayloader = gst.element_factory_make( "rtpopusdepay", "depayloader") elif self.link_config.encoding == 'pcm': self.depayloader = gst.element_factory_make( "rtpL16depay", "depayloader") # RTP stuff self.rtpbin = gst.element_factory_make('gstrtpbin') self.rtpbin.set_property('latency', self.link_config.jitter_buffer) self.rtpbin.set_property('autoremove', True) self.rtpbin.set_property('do-lost', True) # Where audio comes in self.udpsrc_rtpin = gst.element_factory_make('udpsrc') self.udpsrc_rtpin.set_property('port', self.link_config.port) if self.link_config.multicast: self.udpsrc_rtpin.set_property('auto_multicast', True) self.udpsrc_rtpin.set_property('multicast_group', self.link_config.receiver_host) caps = caps.replace('\\', '') # Fix for gstreamer bug in rtpopuspay fixed in GST-plugins-bad # 50140388d2b62d32dd9d0c071e3051ebc5b4083b, bug 686547 if self.link_config.encoding == 'opus': caps = re.sub(r'(caps=.+ )', '', caps) udpsrc_caps = gst.caps_from_string(caps) self.udpsrc_rtpin.set_property('caps', udpsrc_caps) self.udpsrc_rtpin.set_property('timeout', 3000000) # Our level monitor, also used for continuous audio self.level = gst.element_factory_make("level") self.level.set_property('message', True) self.level.set_property('interval', 1000000000) # And now we've got it all set up we need to add the elements self.pipeline.add(self.audiorate, self.audioresample, self.audioconvert, self.sink, self.level, self.depayloader, self.rtpbin, self.udpsrc_rtpin) if self.link_config.encoding != 'pcm': self.pipeline.add(self.decoder) gst.element_link_many(self.depayloader, self.decoder, self.audioconvert) else: gst.element_link_many(self.depayloader, self.audioconvert) gst.element_link_many(self.audioconvert, self.audioresample, self.audiorate, self.level, self.sink) # Now the RTP pads self.udpsrc_rtpin.link_pads('src', self.rtpbin, 'recv_rtp_sink_0') # Attach callbacks for dynamic pads (RTP output) and busses self.rtpbin.connect('pad-added', self.rtpbin_pad_added) self.bus.add_signal_watch() # Our RTPbin won't give us an audio pad till it receives, so we need to # attach it here def rtpbin_pad_added(self, obj, pad): # Unlink first. self.rtpbin.unlink(self.depayloader) # Relink self.rtpbin.link(self.depayloader) def on_message(self, bus, message): if message.type == gst.MESSAGE_ELEMENT: if message.structure.get_name() == 'level': if self.started is False: self.started = True if len(message.structure['peak']) == 1: self.logger.info("Receiving mono audio transmission") else: self.logger.info("Receiving stereo audio transmission") if message.structure.get_name() == 'GstUDPSrcTimeout': # Only UDP source configured to emit timeouts is the audio # input self.logger.critical("No data received for 3 seconds!") if self.started: self.logger.critical("Shutting down receiver for restart") self.pipeline.set_state(gst.STATE_NULL) self.loop.quit() return True def run(self): self.pipeline.set_state(gst.STATE_PLAYING) def loop(self): self.loop = gobject.MainLoop() self.loop.run()
class LinkConfig(object): """ The LinkConfig class encapsulates link configuration. It's genderless; a TX node should be able to set up a new link and an RX node should be able (once the TX node has specified the port caps) to configure itself to receive the stream using the data and methods in this config. """ def __init__(self, link_name, redis_host): """ Set up a new LinkConfig instance - needs to know the link name and configuration host. """ self.int_properties = [ 'port', 'jitter_buffer', 'opus_framesize', 'opus_complexity', 'bitrate', 'opus_loss_expectation' ] self.bool_properties = ['opus_dtx', 'opus_fec', 'multicast'] self.link_name = link_name self.redis_host = redis_host self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('link.%s.config' % self.link_name) self.logger.info("Connecting to configuration host %s" % self.redis_host) self.redis = None while True: try: self.redis = redis.StrictRedis(host=self.redis_host, charset="utf-8", decode_responses=True) break except Exception as e: self.logger.error( "Unable to connect to configuration host! Retrying. (%s)" % e) time.sleep(0.1) def blocking_get(self, key): """Get a value, blocking until it's not None if needed""" while True: value = self.get(key) if value is not None: self.logger.debug("Fetched (blocking) %s, got %s" % (key, value)) return value time.sleep(0.1) def set(self, key, value): """Set a value in the config store""" scoped_key = self.scoped_key(key) if key in self.bool_properties: value = int(value) self.redis.set(scoped_key, value) self.logger.debug("Set %s to %s" % (scoped_key, value)) return value def get(self, key): """Get a value from the config store""" scoped_key = self.scoped_key(key) value = self.redis.get(scoped_key) # Do some typecasting if key in self.int_properties: value = int(value) if key in self.bool_properties: value = (value == 'True') self.logger.debug("Fetched %s, got %s" % (scoped_key, value)) return value def unset(self, key): scoped_key = self.scoped_key(key) self.redis.delete(scoped_key) self.logger.debug("Unset %s" % scoped_key) def __getattr__(self, key): """Convenience method to access get""" return self.get(key) def scoped_key(self, key): """Return an appropriate key name scoped to a link""" return ("openob:%s:%s" % (self.link_name, key)) def set_from_argparse(self, opts): """Given an optparse object from bin/openob, configure this link""" self.set("name", opts.link_name) if opts.mode == "tx": self.set("port", opts.port) self.set("jitter_buffer", opts.jitter_buffer) self.set("encoding", opts.encoding) self.set("bitrate", opts.bitrate) self.set("multicast", opts.multicast) self.set("input_samplerate", opts.samplerate) self.set("receiver_host", opts.receiver_host) self.set("opus_framesize", opts.framesize) self.set("opus_complexity", opts.complexity) self.set("opus_fec", opts.fec) self.set("opus_loss_expectation", opts.loss) self.set("opus_dtx", opts.dtx) def commit_changes(self, restart=False): """ To be called after calls to set() on a running link to signal a reconfiguration event for that link. If restart is True, the link should simply terminate itself so it can be restarted with the new parameters. If restart is False, the link should set all parameters it can which do not involve a restart. """ raise (NotImplementedError, "Link reconfiguration is not yet implemented")
def __init__(self, node_name, link_config, audio_interface): """Sets up a new RTP receiver""" self.started = False self.pipeline = gst.Pipeline("rx") self.bus = self.pipeline.get_bus() self.bus.connect("message", self.on_message) self.link_config = link_config self.audio_interface = audio_interface self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger( 'node.%s.link.%s.%s' % (node_name, self.link_config.name, self.audio_interface.mode)) caps = self.link_config.get("caps") # Audio output if self.audio_interface.type == 'auto': self.sink = gst.element_factory_make("autoaudiosink") elif self.audio_interface.type == 'alsa': self.sink = gst.element_factory_make("alsasink") self.sink.set_property('device', self.audio_interface.alsa_device) elif self.audio_interface.type == 'jack': self.sink = gst.element_factory_make("jackaudiosink") if self.audio_interface.jack_auto: self.sink.set_property('connect', 'auto') else: self.sink.set_property('connect', 'none') self.sink.set_property('name', self.audio_interface.jack_name) # Audio conversion and resampling self.audioconvert = gst.element_factory_make("audioconvert") self.audioresample = gst.element_factory_make("audioresample") self.audioresample.set_property('quality', 9) self.audiorate = gst.element_factory_make("audiorate") # Decoding and depayloading if self.link_config.encoding == 'opus': self.decoder = gst.element_factory_make("opusdec", "decoder") self.decoder.set_property('use-inband-fec', True) # FEC self.decoder.set_property('plc', True) # Packet loss concealment self.depayloader = gst.element_factory_make( "rtpopusdepay", "depayloader") elif self.link_config.encoding == 'pcm': self.depayloader = gst.element_factory_make( "rtpL16depay", "depayloader") # RTP stuff self.rtpbin = gst.element_factory_make('gstrtpbin') self.rtpbin.set_property('latency', self.link_config.jitter_buffer) self.rtpbin.set_property('autoremove', True) self.rtpbin.set_property('do-lost', True) # Where audio comes in self.udpsrc_rtpin = gst.element_factory_make('udpsrc') self.udpsrc_rtpin.set_property('port', self.link_config.port) if self.link_config.multicast: self.udpsrc_rtpin.set_property('auto_multicast', True) self.udpsrc_rtpin.set_property('multicast_group', self.link_config.receiver_host) caps = caps.replace('\\', '') # Fix for gstreamer bug in rtpopuspay fixed in GST-plugins-bad # 50140388d2b62d32dd9d0c071e3051ebc5b4083b, bug 686547 if self.link_config.encoding == 'opus': caps = re.sub(r'(caps=.+ )', '', caps) udpsrc_caps = gst.caps_from_string(caps) self.udpsrc_rtpin.set_property('caps', udpsrc_caps) self.udpsrc_rtpin.set_property('timeout', 3000000) # Our level monitor, also used for continuous audio self.level = gst.element_factory_make("level") self.level.set_property('message', True) self.level.set_property('interval', 1000000000) # And now we've got it all set up we need to add the elements self.pipeline.add(self.audiorate, self.audioresample, self.audioconvert, self.sink, self.level, self.depayloader, self.rtpbin, self.udpsrc_rtpin) if self.link_config.encoding != 'pcm': self.pipeline.add(self.decoder) gst.element_link_many(self.depayloader, self.decoder, self.audioconvert) else: gst.element_link_many(self.depayloader, self.audioconvert) gst.element_link_many(self.audioconvert, self.audioresample, self.audiorate, self.level, self.sink) # Now the RTP pads self.udpsrc_rtpin.link_pads('src', self.rtpbin, 'recv_rtp_sink_0') # Attach callbacks for dynamic pads (RTP output) and busses self.rtpbin.connect('pad-added', self.rtpbin_pad_added) self.bus.add_signal_watch()
def __init__(self, node_name, link_config, audio_interface): """Sets up a new RTP receiver""" self.started = False self.pipeline = gst.Pipeline("rx") self.bus = self.pipeline.get_bus() self.bus.connect("message", self.on_message) self.link_config = link_config self.audio_interface = audio_interface self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('node.%s.link.%s.%s' % (node_name, self.link_config.name, self.audio_interface.mode)) caps = self.link_config.get("caps") # Audio output if self.audio_interface.type == 'auto': self.sink = gst.element_factory_make("autoaudiosink") elif self.audio_interface.type == 'alsa': self.sink = gst.element_factory_make("alsasink") self.sink.set_property('device', self.audio_interface.alsa_device) elif self.audio_interface.type == 'jack': self.sink = gst.element_factory_make("jackaudiosink") if self.audio_interface.jack_auto: self.sink.set_property('connect', 'auto') else: self.sink.set_property('connect', 'none') self.sink.set_property('name', self.audio_interface.jack_name) # Audio conversion and resampling self.audioconvert = gst.element_factory_make("audioconvert") self.audioresample = gst.element_factory_make("audioresample") self.audioresample.set_property('quality', 9) self.audiorate = gst.element_factory_make("audiorate") # Decoding and depayloading if self.link_config.encoding == 'opus': self.decoder = gst.element_factory_make("opusdec", "decoder") self.decoder.set_property('use-inband-fec', True) # FEC self.decoder.set_property('plc', True) # Packet loss concealment self.depayloader = gst.element_factory_make( "rtpopusdepay", "depayloader") elif self.link_config.encoding == 'pcm': self.depayloader = gst.element_factory_make( "rtpL16depay", "depayloader") # RTP stuff self.rtpbin = gst.element_factory_make('gstrtpbin') self.rtpbin.set_property('latency', self.link_config.jitter_buffer) self.rtpbin.set_property('autoremove', True) self.rtpbin.set_property('do-lost', True) # Where audio comes in self.udpsrc_rtpin = gst.element_factory_make('udpsrc') self.udpsrc_rtpin.set_property('port', self.link_config.port) if self.link_config.multicast: self.udpsrc_rtpin.set_property('auto_multicast', True) self.udpsrc_rtpin.set_property('multicast_group', self.link_config.receiver_host) caps = caps.replace('\\', '') # Fix for gstreamer bug in rtpopuspay fixed in GST-plugins-bad # 50140388d2b62d32dd9d0c071e3051ebc5b4083b, bug 686547 if self.link_config.encoding == 'opus': caps = re.sub(r'(caps=.+ )', '', caps) udpsrc_caps = gst.caps_from_string(caps) self.udpsrc_rtpin.set_property('caps', udpsrc_caps) self.udpsrc_rtpin.set_property('timeout', 3000000) # Our level monitor, also used for continuous audio self.level = gst.element_factory_make("level") self.level.set_property('message', True) self.level.set_property('interval', 1000000000) # And now we've got it all set up we need to add the elements self.pipeline.add( self.audiorate, self.audioresample, self.audioconvert, self.sink, self.level, self.depayloader, self.rtpbin, self.udpsrc_rtpin) if self.link_config.encoding != 'pcm': self.pipeline.add(self.decoder) gst.element_link_many( self.depayloader, self.decoder, self.audioconvert) else: gst.element_link_many(self.depayloader, self.audioconvert) gst.element_link_many( self.audioconvert, self.audioresample, self.audiorate, self.level, self.sink) # Now the RTP pads self.udpsrc_rtpin.link_pads('src', self.rtpbin, 'recv_rtp_sink_0') # Attach callbacks for dynamic pads (RTP output) and busses self.rtpbin.connect('pad-added', self.rtpbin_pad_added) self.bus.add_signal_watch()
def __init__(self, node_name, link_config, audio_interface): """Sets up a new RTP transmitter""" self.started = False self.caps = 'None' self.pipeline = gst.Pipeline("tx") self.bus = self.pipeline.get_bus() self.bus.connect("message", self.on_message) self.link_config = link_config self.audio_interface = audio_interface self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger( 'node.%s.link.%s.%s' % (node_name, self.link_config.name, self.audio_interface.mode)) self.logger.info("Starting up RTP transmitter") # Audio input if self.audio_interface.type == 'auto': self.source = gst.element_factory_make('autoaudiosrc') elif self.audio_interface.type == 'alsa': self.source = gst.element_factory_make('alsasrc') self.source.set_property('device', self.audio_interface.alsa_device) elif self.audio_interface.type == 'jack': self.source = gst.element_factory_make("jackaudiosrc") if self.audio_interface.jack_auto: self.source.set_property('connect', 'auto') else: self.source.set_property('connect', 'none') self.source.set_property('name', self.audio_interface.jack_name) # Audio conversion and resampling self.audioconvert = gst.element_factory_make("audioconvert") self.audioresample = gst.element_factory_make("audioresample") self.audioresample.set_property('quality', 9) # SRC self.audiorate = gst.element_factory_make("audiorate") # Encoding and payloading if self.link_config.encoding == 'opus': self.encoder = gst.element_factory_make("opusenc", "encoder") self.encoder.set_property('bitrate', int(self.link_config.bitrate) * 1000) self.encoder.set_property('frame-size', self.link_config.opus_framesize) self.encoder.set_property('complexity', int(self.link_config.opus_complexity)) self.encoder.set_property('inband-fec', self.link_config.opus_fec) self.encoder.set_property( 'packet-loss-percentage', int(self.link_config.opus_loss_expectation)) self.encoder.set_property('dtx', self.link_config.opus_dtx) self.payloader = gst.element_factory_make("rtpopuspay", "payloader") elif self.link_config.encoding == 'pcm': # we have no encoder for PCM operation self.payloader = gst.element_factory_make("rtpL16pay", "payloader") else: self.logger.critical("Unknown encoding type %s" % self.link_config.encoding) # TODO: Add a tee here, and sort out creating multiple UDP sinks for multipath # Now the RTP bits # We'll send audio out on this self.udpsink_rtpout = gst.element_factory_make("udpsink", "udpsink_rtp") self.udpsink_rtpout.set_property('host', self.link_config.receiver_host) self.udpsink_rtpout.set_property('port', self.link_config.port) if self.link_config.multicast: self.udpsink_rtpout.set_property('auto_multicast', True) # Our RTP manager self.rtpbin = gst.element_factory_make("gstrtpbin", "gstrtpbin") # Our level monitor self.level = gst.element_factory_make("level") self.level.set_property('message', True) self.level.set_property('interval', 1000000000) # Add a capsfilter to allow specification of input sample rate self.capsfilter = gst.element_factory_make("capsfilter") # Add to the pipeline self.pipeline.add(self.source, self.capsfilter, self.audioconvert, self.audioresample, self.audiorate, self.payloader, self.udpsink_rtpout, self.rtpbin, self.level) if self.link_config.encoding != 'pcm': # Only add the encoder if we're not in PCM mode self.pipeline.add(self.encoder) # Decide which format to apply to the capsfilter (Jack uses float) if self.audio_interface.type == 'jack': type = 'audio/x-raw-float' else: type = 'audio/x-raw-int' # if audio_rate has been specified, then add that to the capsfilter if self.audio_interface.samplerate != 0: self.capsfilter.set_property( "caps", gst.Caps('%s, channels=2, rate=%d' % (type, self.audio_interface.samplerate))) else: self.capsfilter.set_property("caps", gst.Caps('%s, channels=2' % type)) # Then continue linking the pipeline together gst.element_link_many(self.source, self.capsfilter, self.level, self.audioresample, self.audiorate, self.audioconvert) # Now we get to link this up to our encoder/payloader if self.link_config.encoding != 'pcm': gst.element_link_many(self.audioconvert, self.encoder, self.payloader) else: gst.element_link_many(self.audioconvert, self.payloader) # And now the RTP bits self.payloader.link_pads('src', self.rtpbin, 'send_rtp_sink_0') self.rtpbin.link_pads('send_rtp_src_0', self.udpsink_rtpout, 'sink') # self.udpsrc_rtcpin.link_pads('src', self.rtpbin, 'recv_rtcp_sink_0') # # RTCP SRs # self.rtpbin.link_pads('send_rtcp_src_0', self.udpsink_rtcpout, 'sink') # Connect our bus up self.bus.add_signal_watch() self.bus.connect('message', self.on_message)
argv = sys.argv sys.argv = [] from openob.logger import LoggerFactory from openob.node import Node from openob.link_config import LinkConfig from openob.audio_interface import AudioInterface sys.argv = argv Config = ConfigParser.ConfigParser() Config.read("/home/pi/openob-gui/outstreamer.ini") if len(sys.argv) > 1 and sys.argv[1] == 'autostart': if Config.get("outstreamer", "Boot") != '1': print "Autostart off" sys.exit() Log_Level = logging.INFO #INFO / DEBUG logger_factory = LoggerFactory(level=Log_Level) link_config = LinkConfig("transmission", Config.get("outstreamer", "Encoder_IP")) link_config.set("jitter_buffer", Config.get("outstreamer", "Jitter_Buffer")) audio_interface = AudioInterface("recepteur") link_config.set("port", Config.get("outstreamer", "Listen_Port")) audio_interface.set("mode", "rx") audio_interface.set("type", "alsa") audio_interface.set("alsa_device", "hw:" + Config.get("outstreamer", "Soundcard_ID")) node = Node("recepteur") node.run_link(link_config, audio_interface)
class Node(object): """ OpenOB node instance. Nodes run links. Each Node looks after its end of a link, ensuring that it remains running and tries to recover from failures, as well as responding to configuration changes. Nodes have a name; everything else is link specific. For instance, a node might be the 'studio' node, which would run a 'tx' end for the 'stl' link. Nodes have a config host which is where they store their inter-Node data and communicate with other Nodes. """ def __init__(self, node_name): """Set up a new node.""" self.node_name = node_name self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('node.%s' % self.node_name) def run_link(self, link_config, audio_interface): """ Run a new TX or RX node. """ # We're now entering the realm where we should desperately try and # maintain a link under all circumstances forever. self.logger.info("Link %s initial setup start on %s" % (link_config.name, self.node_name)) link_logger = self.logger_factory.getLogger( 'node.%s.link.%s' % (self.node_name, link_config.name)) while True: try: if audio_interface.mode == 'tx': try: link_logger.info("Starting up transmitter") transmitter = RTPTransmitter(self.node_name, link_config, audio_interface) transmitter.run() caps = transmitter.get_caps() link_logger.debug( "Got caps from transmitter, setting config") link_config.set("caps", caps) transmitter.loop() except ElementNotFoundError as e: link_logger.critical( "GStreamer element missing: %s - will now exit" % e) sys.exit(1) except Exception as e: link_logger.exception( "Transmitter crashed for some reason! Restarting..." ) time.sleep(0.5) elif audio_interface.mode == 'rx': link_logger.info("Waiting for transmitter capabilities...") caps = link_config.blocking_get("caps") link_logger.info("Got caps from transmitter") try: link_logger.info("Starting up receiver") receiver = RTPReceiver(self.node_name, link_config, audio_interface) receiver.run() receiver.loop() except ElementNotFoundError as e: link_logger.critical( "GStreamer element missing: %s - will now exit" % e) sys.exit(1) except Exception as e: link_logger.exception( "Receiver crashed for some reason! Restarting...") time.sleep(0.1) else: link_logger.critical("Unknown audio interface mode (%s)!" % audio_interface.mode) sys.exit(1) except Exception as e: link_logger.exception( "Unknown exception thrown - please report this as a bug! %s" % e) raise
class RTPTransmitter(object): def __init__(self, node_name, link_config, audio_interface): """Sets up a new RTP transmitter""" self.link_config = link_config self.audio_interface = audio_interface self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger( 'node.%s.link.%s.%s' % (node_name, self.link_config.name, self.audio_interface.mode)) self.logger.info('Creating transmission pipeline') self.build_pipeline() def run(self): self.pipeline.set_state(Gst.State.PLAYING) # Gst.debug_bin_to_dot_file(self.pipeline, Gst.DebugGraphDetails.ALL, 'tx-graph') while self.caps == None: caps = self.transport.get_by_name('udpsink').get_static_pad( 'sink').get_property('caps') if caps == None: self.logger.warn('Waiting for audio interface/caps') time.sleep(0.1) else: self.caps = caps.to_string() def loop(self): try: loop = GLib.MainLoop() loop.run() except Exception as e: self.logger.exception( 'Encountered a problem in the MainLoop, tearing down the pipeline: %s' % e) self.pipeline.set_state(Gst.State.NULL) def build_pipeline(self): self.pipeline = Gst.Pipeline.new('tx') self.started = False self.caps = None bus = self.pipeline.get_bus() self.source = self.build_audio_interface() self.encoder = self.build_encoder() self.transport = self.build_transport() self.pipeline.add(self.source) self.pipeline.add(self.encoder) self.pipeline.add(self.transport) self.source.link(self.encoder) self.encoder.link(self.transport) # Connect our bus up bus.add_signal_watch() bus.connect('message', self.on_message) def build_audio_interface(self): self.logger.debug('Building audio input bin') bin = Gst.Bin.new('audio') # Audio input if self.audio_interface.type == 'auto': source = Gst.ElementFactory.make('autoaudiosrc') elif self.audio_interface.type == 'alsa': source = Gst.ElementFactory.make('alsasrc') source.set_property('device', self.audio_interface.alsa_device) elif self.audio_interface.type == 'jack': source = Gst.ElementFactory.make('jackaudiosrc') if self.audio_interface.jack_auto: source.set_property('connect', 'auto') else: source.set_property('connect', 'none') source.set_property('buffer-time', 50000) source.set_property('name', self.audio_interface.jack_name) source.set_property('client-name', self.audio_interface.jack_name) if self.audio_interface.jack_port_pattern: source.set_property('port-pattern', self.audio_interface.jack_port_pattern) elif self.audio_interface.type == 'test': source = Gst.ElementFactory.make('audiotestsrc') bin.add(source) # Our level monitor level = Gst.ElementFactory.make('level') level.set_property('message', True) level.set_property('interval', 1000000000) bin.add(level) # Audio resampling and conversion resample = Gst.ElementFactory.make('audioresample') resample.set_property('quality', 9) # SRC bin.add(resample) convert = Gst.ElementFactory.make('audioconvert') bin.add(convert) # Add a capsfilter to allow specification of input sample rate capsfilter = Gst.ElementFactory.make('capsfilter') caps = Gst.Caps.new_empty_simple('audio/x-raw') # if audio_rate has been specified, then add that to the capsfilter if self.audio_interface.samplerate != 0: caps.set_value('rate', self.audio_interface.samplerate) self.logger.debug(caps.to_string()) capsfilter.set_property('caps', caps) bin.add(capsfilter) source.link(level) level.link(resample) resample.link(convert) convert.link(capsfilter) bin.add_pad(Gst.GhostPad.new('src', capsfilter.get_static_pad('src'))) return bin def build_encoder(self): self.logger.debug('Building encoder bin') bin = Gst.Bin.new('encoder') # Encoding and payloading if self.link_config.encoding == 'opus': encoder = Gst.ElementFactory.make('opusenc', 'encoder') encoder.set_property('bitrate', int(self.link_config.bitrate) * 1000) encoder.set_property('tolerance', 80000000) encoder.set_property('frame-size', self.link_config.opus_framesize) encoder.set_property('complexity', int(self.link_config.opus_complexity)) encoder.set_property('inband-fec', self.link_config.opus_fec) encoder.set_property('packet-loss-percentage', int(self.link_config.opus_loss_expectation)) encoder.set_property('dtx', self.link_config.opus_dtx) payloader = Gst.ElementFactory.make('rtpopuspay', 'payloader') elif self.link_config.encoding == 'pcm': # we have no encoder for PCM operation payloader = Gst.ElementFactory.make('rtpL16pay', 'payloader') else: self.logger.critical('Unknown encoding type %s' % self.link_config.encoding) bin.add(payloader) if 'encoder' in locals(): bin.add(encoder) encoder.link(payloader) bin.add_pad( Gst.GhostPad.new('sink', encoder.get_static_pad('sink'))) else: bin.add_pad( Gst.GhostPad.new('sink', payloader.get_static_pad('sink'))) bin.add_pad(Gst.GhostPad.new('src', payloader.get_static_pad('src'))) return bin def build_transport(self): self.logger.debug('Building RTP transport bin') bin = Gst.Bin.new('transport') # Our RTP manager rtpbin = Gst.ElementFactory.make('rtpbin', 'rtpbin') rtpbin.set_property('latency', 0) bin.add(rtpbin) # TODO: Add a tee here, and sort out creating multiple UDP sinks for multipath udpsink = Gst.ElementFactory.make('udpsink', 'udpsink') udpsink.set_property('host', self.link_config.receiver_host) udpsink.set_property('port', self.link_config.port) self.logger.info( 'Set receiver to %s:%i' % (self.link_config.receiver_host, self.link_config.port)) if self.link_config.multicast: udpsink.set_property('auto_multicast', True) self.logger.info('Multicast mode enabled') bin.add(udpsink) bin.add_pad( Gst.GhostPad.new('sink', rtpbin.get_request_pad('send_rtp_sink_0'))) rtpbin.link_pads('send_rtp_src_0', udpsink, 'sink') return bin def on_message(self, bus, message): if message.type == Gst.MessageType.ELEMENT: struct = message.get_structure() if struct != None: if struct.get_name() == 'level': if self.started is False: self.started = True if len(struct.get_value('peak')) == 1: self.logger.info('Started mono audio transmission') else: self.logger.info( 'Started stereo audio transmission') else: if len(struct.get_value('peak')) == 1: self.logger.debug('Level: %.2f', struct.get_value('peak')[0]) else: self.logger.debug('Levels: L %.2f R %.2f' % (struct.get_value('peak')[0], struct.get_value('peak')[1])) return True def get_caps(self): return self.caps
def __init__(self, node_name, link_config, audio_interface): """Sets up a new RTP receiver""" self.started = False self.pipeline = gst.Pipeline("rx") self.bus = self.pipeline.get_bus() self.bus.connect("message", self.on_message) self.link_config = link_config self.audio_interface = audio_interface self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger( "node.%s.link.%s.%s" % (node_name, self.link_config.name, self.audio_interface.mode) ) self.logger.info("Creating RTP reception pipeline") caps = self.link_config.get("caps") # Audio output if self.audio_interface.type == "auto": self.sink = gst.element_factory_make("autoaudiosink") elif self.audio_interface.type == "alsa": self.sink = gst.element_factory_make("alsasink") self.sink.set_property("device", self.audio_interface.alsa_device) elif self.audio_interface.type == "jack": self.sink = gst.element_factory_make("jackaudiosink") if self.audio_interface.jack_auto: self.sink.set_property("connect", "auto") else: self.sink.set_property("connect", "none") self.sink.set_property("name", self.audio_interface.jack_name) self.sink.set_property("client-name", self.audio_interface.jack_name) # Audio resampling and conversion self.audioresample = gst.element_factory_make("audioresample") self.audioconvert = gst.element_factory_make("audioconvert") self.audioresample.set_property("quality", 6) # Decoding and depayloading if self.link_config.encoding == "opus": self.decoder = gst.element_factory_make("opusdec", "decoder") self.decoder.set_property("use-inband-fec", True) # FEC self.decoder.set_property("plc", True) # Packet loss concealment self.depayloader = gst.element_factory_make("rtpopusdepay", "depayloader") elif self.link_config.encoding == "pcm": self.depayloader = gst.element_factory_make("rtpL16depay", "depayloader") # RTP stuff self.rtpbin = gst.element_factory_make("gstrtpbin") self.rtpbin.set_property("latency", self.link_config.jitter_buffer) self.rtpbin.set_property("autoremove", True) self.rtpbin.set_property("do-lost", True) # self.rtpbin.set_property('buffer-mode', 1) # Where audio comes in self.udpsrc_rtpin = gst.element_factory_make("udpsrc") self.udpsrc_rtpin.set_property("port", self.link_config.port) if self.link_config.multicast: self.udpsrc_rtpin.set_property("auto_multicast", True) self.udpsrc_rtpin.set_property("multicast_group", self.link_config.receiver_host) self.logger.info("Multicast mode enabled") caps = caps.replace("\\", "") # Fix for gstreamer bug in rtpopuspay fixed in GST-plugins-bad # 50140388d2b62d32dd9d0c071e3051ebc5b4083b, bug 686547 if self.link_config.encoding == "opus": caps = re.sub(r"(caps=.+ )", "", caps) udpsrc_caps = gst.caps_from_string(caps) self.udpsrc_rtpin.set_property("caps", udpsrc_caps) self.udpsrc_rtpin.set_property("timeout", 3000000) # Our level monitor, also used for continuous audio self.level = gst.element_factory_make("level") self.level.set_property("message", True) self.level.set_property("interval", 1000000000) # And now we've got it all set up we need to add the elements self.pipeline.add( self.audioconvert, self.audioresample, self.sink, self.level, self.depayloader, self.rtpbin, self.udpsrc_rtpin, ) if self.link_config.encoding != "pcm": self.pipeline.add(self.decoder) gst.element_link_many(self.depayloader, self.decoder, self.audioconvert) else: gst.element_link_many(self.depayloader, self.audioconvert) gst.element_link_many(self.audioconvert, self.audioresample, self.level, self.sink) self.logger.debug(self.sink) # Now the RTP pads self.udpsrc_rtpin.link_pads("src", self.rtpbin, "recv_rtp_sink_0") # Attach callbacks for dynamic pads (RTP output) and busses self.rtpbin.connect("pad-added", self.rtpbin_pad_added) self.bus.add_signal_watch()
class RTPTransmitter(object): def __init__(self, node_name, link_config, audio_interface): """Sets up a new RTP transmitter""" self.started = False self.caps = 'None' self.pipeline = gst.Pipeline("tx") self.bus = self.pipeline.get_bus() self.bus.connect("message", self.on_message) self.link_config = link_config self.audio_interface = audio_interface self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('node.%s.link.%s.%s' % (node_name, self.link_config.name, self.audio_interface.mode)) self.logger.info("Starting up RTP transmitter") # Audio input if self.audio_interface.type == 'auto': self.source = gst.element_factory_make('autoaudiosrc') elif self.audio_interface.type == 'alsa': self.source = gst.element_factory_make('alsasrc') self.source.set_property('device', self.audio_interface.alsa_device) elif self.audio_interface.type == 'jack': self.source = gst.element_factory_make("jackaudiosrc") if self.audio_interface.jack_auto: self.source.set_property('connect', 'auto') else: self.source.set_property('connect', 'none') self.source.set_property('name', self.audio_interface.jack_name) # Audio conversion and resampling self.audioconvert = gst.element_factory_make("audioconvert") self.audioresample = gst.element_factory_make("audioresample") self.audioresample.set_property('quality', 9) # SRC self.audiorate = gst.element_factory_make("audiorate") # Encoding and payloading if self.link_config.encoding == 'opus': self.encoder = gst.element_factory_make("opusenc", "encoder") self.encoder.set_property('bitrate', int(self.link_config.bitrate) * 1000) self.encoder.set_property('frame-size', self.link_config.opus_framesize) self.encoder.set_property('complexity', int(self.link_config.opus_complexity)) self.encoder.set_property('inband-fec', self.link_config.opus_fec) self.encoder.set_property('packet-loss-percentage', int(self.link_config.opus_loss_expectation)) self.encoder.set_property('dtx', self.link_config.opus_dtx) self.payloader = gst.element_factory_make("rtpopuspay", "payloader") elif self.link_config.encoding == 'pcm': # we have no encoder for PCM operation self.payloader = gst.element_factory_make("rtpL16pay", "payloader") else: self.logger.critical("Unknown encoding type %s" % self.link_config.encoding) # TODO: Add a tee here, and sort out creating multiple UDP sinks for multipath # Now the RTP bits # We'll send audio out on this self.udpsink_rtpout = gst.element_factory_make( "udpsink", "udpsink_rtp") self.udpsink_rtpout.set_property('host', self.link_config.receiver_host) self.udpsink_rtpout.set_property('port', self.link_config.port) if self.link_config.multicast: self.udpsink_rtpout.set_property('auto_multicast', True) # Our RTP manager self.rtpbin = gst.element_factory_make("gstrtpbin", "gstrtpbin") # Our level monitor self.level = gst.element_factory_make("level") self.level.set_property('message', True) self.level.set_property('interval', 1000000000) # Add a capsfilter to allow specification of input sample rate self.capsfilter = gst.element_factory_make("capsfilter") # Add to the pipeline self.pipeline.add( self.source, self.capsfilter, self.audioconvert, self.audioresample, self.audiorate, self.payloader, self.udpsink_rtpout, self.rtpbin, self.level) if self.link_config.encoding != 'pcm': # Only add the encoder if we're not in PCM mode self.pipeline.add(self.encoder) # Decide which format to apply to the capsfilter (Jack uses float) if self.audio_interface.type == 'jack': type = 'audio/x-raw-float' else: type = 'audio/x-raw-int' # if audio_rate has been specified, then add that to the capsfilter if self.audio_interface.samplerate != 0: self.capsfilter.set_property( "caps", gst.Caps('%s, channels=2, rate=%d' % (type, self.audio_interface.samplerate))) else: self.capsfilter.set_property( "caps", gst.Caps('%s, channels=2' % type)) # Then continue linking the pipeline together gst.element_link_many( self.source, self.capsfilter, self.level, self.audioresample, self.audiorate, self.audioconvert) # Now we get to link this up to our encoder/payloader if self.link_config.encoding != 'pcm': gst.element_link_many( self.audioconvert, self.encoder, self.payloader) else: gst.element_link_many(self.audioconvert, self.payloader) # And now the RTP bits self.payloader.link_pads('src', self.rtpbin, 'send_rtp_sink_0') self.rtpbin.link_pads('send_rtp_src_0', self.udpsink_rtpout, 'sink') # self.udpsrc_rtcpin.link_pads('src', self.rtpbin, 'recv_rtcp_sink_0') # # RTCP SRs # self.rtpbin.link_pads('send_rtcp_src_0', self.udpsink_rtcpout, 'sink') # Connect our bus up self.bus.add_signal_watch() self.bus.connect('message', self.on_message) def run(self): self.pipeline.set_state(gst.STATE_PLAYING) while self.caps == 'None': self.logger.warn("Waiting for audio interface/caps") self.logger.debug(self.udpsink_rtpout.get_state()) self.caps = str( self.udpsink_rtpout.get_pad('sink').get_property('caps')) # Fix for gstreamer bug in rtpopuspay fixed in GST-plugins-bad # 50140388d2b62d32dd9d0c071e3051ebc5b4083b, bug 686547 if self.link_config.encoding == 'opus': self.caps = re.sub(r'(caps=.+ )', '', self.caps) time.sleep(0.1) def loop(self): try: self.loop = gobject.MainLoop() self.loop.run() except Exception, e: self.logger.exception("Encountered a problem in the MainLoop, tearing down the pipeline") self.pipeline.set_state(gst.STATE_NULL)
class RTPReceiver(object): def __init__(self, node_name, link_config, audio_interface): """Sets up a new RTP receiver""" self.started = False self.pipeline = gst.Pipeline("rx") self.bus = self.pipeline.get_bus() self.bus.connect("message", self.on_message) self.link_config = link_config self.audio_interface = audio_interface self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger( "node.%s.link.%s.%s" % (node_name, self.link_config.name, self.audio_interface.mode) ) self.logger.info("Creating RTP reception pipeline") caps = self.link_config.get("caps") # Audio output if self.audio_interface.type == "auto": self.sink = gst.element_factory_make("autoaudiosink") elif self.audio_interface.type == "alsa": self.sink = gst.element_factory_make("alsasink") self.sink.set_property("device", self.audio_interface.alsa_device) elif self.audio_interface.type == "jack": self.sink = gst.element_factory_make("jackaudiosink") if self.audio_interface.jack_auto: self.sink.set_property("connect", "auto") else: self.sink.set_property("connect", "none") self.sink.set_property("name", self.audio_interface.jack_name) self.sink.set_property("client-name", self.audio_interface.jack_name) # Audio resampling and conversion self.audioresample = gst.element_factory_make("audioresample") self.audioconvert = gst.element_factory_make("audioconvert") self.audioresample.set_property("quality", 6) # Decoding and depayloading if self.link_config.encoding == "opus": self.decoder = gst.element_factory_make("opusdec", "decoder") self.decoder.set_property("use-inband-fec", True) # FEC self.decoder.set_property("plc", True) # Packet loss concealment self.depayloader = gst.element_factory_make("rtpopusdepay", "depayloader") elif self.link_config.encoding == "pcm": self.depayloader = gst.element_factory_make("rtpL16depay", "depayloader") # RTP stuff self.rtpbin = gst.element_factory_make("gstrtpbin") self.rtpbin.set_property("latency", self.link_config.jitter_buffer) self.rtpbin.set_property("autoremove", True) self.rtpbin.set_property("do-lost", True) # self.rtpbin.set_property('buffer-mode', 1) # Where audio comes in self.udpsrc_rtpin = gst.element_factory_make("udpsrc") self.udpsrc_rtpin.set_property("port", self.link_config.port) if self.link_config.multicast: self.udpsrc_rtpin.set_property("auto_multicast", True) self.udpsrc_rtpin.set_property("multicast_group", self.link_config.receiver_host) self.logger.info("Multicast mode enabled") caps = caps.replace("\\", "") # Fix for gstreamer bug in rtpopuspay fixed in GST-plugins-bad # 50140388d2b62d32dd9d0c071e3051ebc5b4083b, bug 686547 if self.link_config.encoding == "opus": caps = re.sub(r"(caps=.+ )", "", caps) udpsrc_caps = gst.caps_from_string(caps) self.udpsrc_rtpin.set_property("caps", udpsrc_caps) self.udpsrc_rtpin.set_property("timeout", 3000000) # Our level monitor, also used for continuous audio self.level = gst.element_factory_make("level") self.level.set_property("message", True) self.level.set_property("interval", 1000000000) # And now we've got it all set up we need to add the elements self.pipeline.add( self.audioconvert, self.audioresample, self.sink, self.level, self.depayloader, self.rtpbin, self.udpsrc_rtpin, ) if self.link_config.encoding != "pcm": self.pipeline.add(self.decoder) gst.element_link_many(self.depayloader, self.decoder, self.audioconvert) else: gst.element_link_many(self.depayloader, self.audioconvert) gst.element_link_many(self.audioconvert, self.audioresample, self.level, self.sink) self.logger.debug(self.sink) # Now the RTP pads self.udpsrc_rtpin.link_pads("src", self.rtpbin, "recv_rtp_sink_0") # Attach callbacks for dynamic pads (RTP output) and busses self.rtpbin.connect("pad-added", self.rtpbin_pad_added) self.bus.add_signal_watch() # Our RTPbin won't give us an audio pad till it receives, so we need to # attach it here def rtpbin_pad_added(self, obj, pad): # Unlink first. self.rtpbin.unlink(self.depayloader) # Relink self.rtpbin.link(self.depayloader) def on_message(self, bus, message): if message.type == gst.MESSAGE_ELEMENT: if message.structure.get_name() == "level": if self.started is False: self.started = True # gst.DEBUG_BIN_TO_DOT_FILE(self.pipeline, gst.DEBUG_GRAPH_SHOW_ALL, 'rx-graph') if len(message.structure["peak"]) == 1: self.logger.info("Receiving mono audio transmission") else: self.logger.info("Receiving stereo audio transmission") if message.structure.get_name() == "GstUDPSrcTimeout": # Only UDP source configured to emit timeouts is the audio # input self.logger.critical("No data received for 3 seconds!") if self.started: self.logger.critical("Shutting down receiver for restart") self.pipeline.set_state(gst.STATE_NULL) self.loop.quit() return True def run(self): self.pipeline.set_state(gst.STATE_PLAYING) self.logger.info("Listening for stream on %s:%i" % (self.link_config.receiver_host, self.link_config.port)) def loop(self): self.loop = gobject.MainLoop() self.loop.run()
class Node(object): """ OpenOB node instance. Nodes run links. Each Node looks after its end of a link, ensuring that it remains running and tries to recover from failures, as well as responding to configuration changes. Nodes have a name; everything else is link specific. For instance, a node might be the 'studio' node, which would run a 'tx' end for the 'stl' link. Nodes have a config host which is where they store their inter-Node data and communicate with other Nodes. """ def __init__(self, node_name): """Set up a new node.""" self.node_name = node_name self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('node.%s' % self.node_name) def run_link(self, link_config, audio_interface): """ Run a new TX or RX node. """ # We're now entering the realm where we should desperately try and # maintain a link under all circumstances forever. self.logger.info("Link %s initial setup start on %s" % (link_config.name, self.node_name)) link_logger = self.logger_factory.getLogger('node.%s.link.%s' % (self.node_name, link_config.name)) while True: try: if audio_interface.mode == 'tx': try: link_logger.info("Starting up transmitter") transmitter = RTPTransmitter(self.node_name, link_config, audio_interface) transmitter.run() caps = transmitter.get_caps() link_logger.debug("Got caps from transmitter, setting config") link_config.set("caps", caps) transmitter.loop() except ElementNotFoundError as e: link_logger.critical("GStreamer element missing: %s - will now exit" % e) sys.exit(1) except Exception as e: link_logger.exception("Transmitter crashed for some reason! Restarting...") time.sleep(0.5) elif audio_interface.mode == 'rx': link_logger.info("Waiting for transmitter capabilities...") caps = link_config.blocking_get("caps") link_logger.info("Got caps from transmitter") try: link_logger.info("Starting up receiver") receiver = RTPReceiver(self.node_name, link_config, audio_interface) receiver.run() receiver.loop() except ElementNotFoundError as e: link_logger.critical("GStreamer element missing: %s - will now exit" % e) sys.exit(1) except Exception as e: link_logger.exception("Receiver crashed for some reason! Restarting...") time.sleep(0.1) else: link_logger.critical("Unknown audio interface mode (%s)!" % audio_interface.mode) sys.exit(1) except Exception as e: link_logger.exception("Unknown exception thrown - please report this as a bug! %s" % e) raise
sys.argv = [] from openob.logger import LoggerFactory from openob.node import Node from openob.link_config import LinkConfig from openob.audio_interface import AudioInterface sys.argv = argv Config = configparser.ConfigParser() Config.read("/home/pi/openob-gui/instreamer.ini") if len(sys.argv) > 1 and sys.argv[1] == 'autostart': if Config.get("instreamer", "Boot") != '1': print("Autostart off") sys.exit() logger_factory = LoggerFactory(level=logging.INFO) link_config = LinkConfig("transmission", Config.get("instreamer", "Encoder_IP")) audio_interface = AudioInterface("emetteur") link_config.set("port", Config.get("instreamer", "Listen_Port")) link_config.set("bitrate", int(Config.get("instreamer", "Bitrate"))) link_config.set("encoding", Config.get("instreamer", "Encoding")) link_config.set("opus_framesize", "20") link_config.set("opus_complexity", "9") link_config.set("opus_loss_expectation", "0") link_config.set("jitter_buffer", "40") link_config.set("receiver_host", Config.get("instreamer", "Receiver_IP")) audio_interface.set("mode", "tx") audio_interface.set("samplerate", int(Config.get("instreamer", "Samplerate"))) audio_interface.set("type", "alsa")
class RTPReceiver(object): def __init__(self, node_name, link_config, audio_interface): """Sets up a new RTP receiver""" self.started = False self.pipeline = gst.Pipeline("rx") self.bus = self.pipeline.get_bus() self.bus.connect("message", self.on_message) self.link_config = link_config self.audio_interface = audio_interface self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('node.%s.link.%s.%s' % (node_name, self.link_config.name, self.audio_interface.mode)) caps = self.link_config.get("caps") # Audio output if self.audio_interface.type == 'auto': self.sink = gst.element_factory_make("autoaudiosink") elif self.audio_interface.type == 'alsa': self.sink = gst.element_factory_make("alsasink") self.sink.set_property('device', self.audio_interface.alsa_device) elif self.audio_interface.type == 'jack': self.sink = gst.element_factory_make("jackaudiosink") if self.audio_interface.jack_auto: self.sink.set_property('connect', 'auto') else: self.sink.set_property('connect', 'none') self.sink.set_property('name', self.audio_interface.jack_name) # Audio conversion and resampling self.audioconvert = gst.element_factory_make("audioconvert") self.audioresample = gst.element_factory_make("audioresample") self.audioresample.set_property('quality', 9) self.audiorate = gst.element_factory_make("audiorate") # Decoding and depayloading if self.link_config.encoding == 'opus': self.decoder = gst.element_factory_make("opusdec", "decoder") self.decoder.set_property('use-inband-fec', True) # FEC self.decoder.set_property('plc', True) # Packet loss concealment self.depayloader = gst.element_factory_make( "rtpopusdepay", "depayloader") elif self.link_config.encoding == 'pcm': self.depayloader = gst.element_factory_make( "rtpL16depay", "depayloader") # RTP stuff self.rtpbin = gst.element_factory_make('gstrtpbin') self.rtpbin.set_property('latency', self.link_config.jitter_buffer) self.rtpbin.set_property('autoremove', True) self.rtpbin.set_property('do-lost', True) # Where audio comes in self.udpsrc_rtpin = gst.element_factory_make('udpsrc') self.udpsrc_rtpin.set_property('port', self.link_config.port) if self.link_config.multicast: self.udpsrc_rtpin.set_property('auto_multicast', True) self.udpsrc_rtpin.set_property('multicast_group', self.link_config.receiver_host) caps = caps.replace('\\', '') # Fix for gstreamer bug in rtpopuspay fixed in GST-plugins-bad # 50140388d2b62d32dd9d0c071e3051ebc5b4083b, bug 686547 if self.link_config.encoding == 'opus': caps = re.sub(r'(caps=.+ )', '', caps) udpsrc_caps = gst.caps_from_string(caps) self.udpsrc_rtpin.set_property('caps', udpsrc_caps) self.udpsrc_rtpin.set_property('timeout', 3000000) # Our level monitor, also used for continuous audio self.level = gst.element_factory_make("level") self.level.set_property('message', True) self.level.set_property('interval', 1000000000) # And now we've got it all set up we need to add the elements self.pipeline.add( self.audiorate, self.audioresample, self.audioconvert, self.sink, self.level, self.depayloader, self.rtpbin, self.udpsrc_rtpin) if self.link_config.encoding != 'pcm': self.pipeline.add(self.decoder) gst.element_link_many( self.depayloader, self.decoder, self.audioconvert) else: gst.element_link_many(self.depayloader, self.audioconvert) gst.element_link_many( self.audioconvert, self.audioresample, self.audiorate, self.level, self.sink) # Now the RTP pads self.udpsrc_rtpin.link_pads('src', self.rtpbin, 'recv_rtp_sink_0') # Attach callbacks for dynamic pads (RTP output) and busses self.rtpbin.connect('pad-added', self.rtpbin_pad_added) self.bus.add_signal_watch() # Our RTPbin won't give us an audio pad till it receives, so we need to # attach it here def rtpbin_pad_added(self, obj, pad): # Unlink first. self.rtpbin.unlink(self.depayloader) # Relink self.rtpbin.link(self.depayloader) def on_message(self, bus, message): if message.type == gst.MESSAGE_ELEMENT: if message.structure.get_name() == 'level': if self.started is False: self.started = True if len(message.structure['peak']) == 1: self.logger.info("Receiving mono audio transmission") else: self.logger.info("Receiving stereo audio transmission") if message.structure.get_name() == 'GstUDPSrcTimeout': # Only UDP source configured to emit timeouts is the audio # input self.logger.critical("No data received for 3 seconds!") if self.started: self.logger.critical("Shutting down receiver for restart") self.pipeline.set_state(gst.STATE_NULL) self.loop.quit() return True def run(self): self.pipeline.set_state(gst.STATE_PLAYING) def loop(self): self.loop = gobject.MainLoop() self.loop.run()
class RTPReceiver(object): def __init__(self, node_name, link_config, audio_interface): """Sets up a new RTP receiver""" self.link_config = link_config self.audio_interface = audio_interface self.logger_factory = LoggerFactory() self.logger = self.logger_factory.getLogger('node.%s.link.%s.%s' % (node_name, self.link_config.name, self.audio_interface.mode)) self.logger.info('Creating reception pipeline') self.build_pipeline() def run(self): self.pipeline.set_state(Gst.State.PLAYING) self.logger.info('Listening for stream on %s:%i' % (self.link_config.receiver_host, self.link_config.port)) def loop(self): try: self.main_loop = GLib.MainLoop() self.main_loop.run() except Exception as e: self.logger.exception('Encountered a problem in the MainLoop, tearing down the pipeline: %s' % e) self.pipeline.set_state(Gst.State.NULL) def build_pipeline(self): self.pipeline = Gst.Pipeline.new('rx') self.started = False bus = self.pipeline.get_bus() self.transport = self.build_transport() self.decoder = self.build_decoder() self.output = self.build_audio_interface() self.pipeline.add(self.transport) self.pipeline.add(self.decoder) self.pipeline.add(self.output) self.transport.link(self.decoder) self.decoder.link(self.output) bus.add_signal_watch() bus.connect('message', self.on_message) def build_audio_interface(self): self.logger.debug('Building audio output bin') bin = Gst.Bin.new('audio') # Audio output if self.audio_interface.type == 'auto': sink = Gst.ElementFactory.make('autoaudiosink') elif self.audio_interface.type == 'alsa': sink = Gst.ElementFactory.make('alsasink') sink.set_property('device', self.audio_interface.alsa_device) elif self.audio_interface.type == 'jack': sink = Gst.ElementFactory.make('jackaudiosink') if self.audio_interface.jack_auto: sink.set_property('connect', 'auto') else: sink.set_property('connect', 'none') sink.set_property('name', self.audio_interface.jack_name) sink.set_property('client-name', self.audio_interface.jack_name) if self.audio_interface.jack_port_pattern: sink.set_property('port-pattern', self.audio_interface.jack_port_pattern) elif self.audio_interface.type == 'test': sink = Gst.ElementFactory.make('fakesink') bin.add(sink) # Audio resampling and conversion resample = Gst.ElementFactory.make('audioresample') resample.set_property('quality', 9) bin.add(resample) convert = Gst.ElementFactory.make('audioconvert') bin.add(convert) # Our level monitor, also used for continuous audio level = Gst.ElementFactory.make('level') level.set_property('message', True) level.set_property('interval', 1000000000) bin.add(level) resample.link(convert) convert.link(level) level.link(sink) bin.add_pad(Gst.GhostPad.new('sink', resample.get_static_pad('sink'))) return bin def build_decoder(self): self.logger.debug('Building decoder bin') bin = Gst.Bin.new('decoder') # Decoding and depayloading if self.link_config.encoding == 'opus': decoder = Gst.ElementFactory.make('opusdec', 'decoder') decoder.set_property('use-inband-fec', True) # FEC decoder.set_property('plc', True) # Packet loss concealment depayloader = Gst.ElementFactory.make( 'rtpopusdepay', 'depayloader') elif self.link_config.encoding == 'pcm': depayloader = Gst.ElementFactory.make( 'rtpL16depay', 'depayloader') else: self.logger.critical('Unknown encoding type %s' % self.link_config.encoding) bin.add(depayloader) bin.add_pad(Gst.GhostPad.new('sink', depayloader.get_static_pad('sink'))) if 'decoder' in locals(): bin.add(decoder) depayloader.link(decoder) bin.add_pad(Gst.GhostPad.new('src', decoder.get_static_pad('src'))) else: bin.add_pad(Gst.GhostPad.new('src', depayloader.get_static_pad('src'))) return bin def build_transport(self): self.logger.debug('Building RTP transport bin') bin = Gst.Bin.new('transport') caps = self.link_config.get('caps').replace('\\', '') udpsrc_caps = Gst.Caps.from_string(caps) # Where audio comes in udpsrc = Gst.ElementFactory.make('udpsrc', 'udpsrc') udpsrc.set_property('port', self.link_config.port) udpsrc.set_property('caps', udpsrc_caps) udpsrc.set_property('timeout', 3000000000) if self.link_config.multicast: udpsrc.set_property('auto_multicast', True) udpsrc.set_property('multicast_group', self.link_config.receiver_host) self.logger.info('Multicast mode enabled') bin.add(udpsrc) rtpbin = Gst.ElementFactory.make('rtpbin', 'rtpbin') rtpbin.set_property('latency', self.link_config.jitter_buffer) rtpbin.set_property('autoremove', True) rtpbin.set_property('do-lost', True) bin.add(rtpbin) udpsrc.link_pads('src', rtpbin, 'recv_rtp_sink_0') valve = Gst.ElementFactory.make('valve', 'valve') bin.add(valve) bin.add_pad(Gst.GhostPad.new('src', valve.get_static_pad('src'))) # Attach callbacks for dynamic pads (RTP output) and busses rtpbin.connect('pad-added', self.rtpbin_pad_added) return bin # Our RTPbin won't give us an audio pad till it receives, so we need to # attach it here def rtpbin_pad_added(self, obj, pad): valve = self.transport.get_by_name('valve') rtpbin = self.transport.get_by_name('rtpbin') # Unlink first. rtpbin.unlink(valve) # Relink rtpbin.link(valve) def on_message(self, bus, message): if message.type == Gst.MessageType.ELEMENT: struct = message.get_structure() if struct != None: if struct.get_name() == 'level': if self.started is False: self.started = True if len(struct.get_value('peak')) == 1: self.logger.info('Receiving mono audio transmission') else: self.logger.info('Receiving stereo audio transmission') else: if len(struct.get_value('peak')) == 1: self.logger.debug('Level: %.2f', struct.get_value('peak')[0]) else: self.logger.debug('Levels: L %.2f R %.2f' % (struct.get_value('peak')[0], struct.get_value('peak')[1])) if struct.get_name() == 'GstUDPSrcTimeout': # Gst.debug_bin_to_dot_file(self.pipeline, Gst.DebugGraphDetails.ALL, 'rx-graph') # Only UDP source configured to emit timeouts is the audio input self.logger.critical('No data received for 3 seconds!') if self.started: self.logger.critical('Shutting down receiver for restart') self.pipeline.set_state(Gst.State.NULL) self.main_loop.quit() return True