aboutsummaryrefslogtreecommitdiff
path: root/example-scripts/gstreamer/source-nostream-music-from-folder.py
blob: a176a7d976470455ef0a013eb696bae419a2978e (plain)
  1. #!/usr/bin/env python3
  2. import os, sys, gi, signal, random
  3. import argparse, logging, pyinotify
  4. gi.require_version('Gst', '1.0')
  5. from gi.repository import Gst, GObject, GLib
  6. # init GObject & Co. before importing local classes
  7. GObject.threads_init()
  8. Gst.init([])
  9. class Directory(object):
  10. def __init__(self, path):
  11. self.log = logging.getLogger('Directory')
  12. self.path = path
  13. self.scheduled = False
  14. self.rescan()
  15. self.log.debug('setting up inotify watch for %s', self.path)
  16. wm = pyinotify.WatchManager()
  17. notifier = pyinotify.Notifier(wm,
  18. timeout=10,
  19. default_proc_fun=self.inotify_callback)
  20. wm.add_watch(
  21. self.path,
  22. #pyinotify.ALL_EVENTS,
  23. pyinotify.IN_DELETE | pyinotify.IN_CREATE | pyinotify.IN_MODIFY,
  24. rec=True)
  25. GLib.io_add_watch(
  26. notifier._fd,
  27. GLib.IO_IN,
  28. self.io_callback,
  29. notifier)
  30. def inotify_callback(self, notifier):
  31. self.log.info('inotify callback %s: %s', notifier.maskname, notifier.pathname)
  32. if not self.scheduled:
  33. self.scheduled = True
  34. GLib.timeout_add(100, self.rescan)
  35. return True
  36. def io_callback(self, source, condition, notifier):
  37. notifier.process_events()
  38. while notifier.check_events():
  39. notifier.read_events()
  40. notifier.process_events()
  41. return True
  42. def is_playable_file(self, filepath):
  43. root, ext = os.path.splitext(filepath)
  44. return ext in ['.mp3', '.ogg', '.oga', '.wav', '.m4a', '.flac', 'self.opus']
  45. def rescan(self):
  46. self.log.info('scanning directory %s', self.path)
  47. self.scheduled = False
  48. all_files = []
  49. for root, dirs, files in os.walk(self.path):
  50. files = filter(self.is_playable_file, files)
  51. files = map(lambda f: os.path.join(root, f), files)
  52. files = list(files)
  53. self.log.debug('found directory %s: %u playable file(s)', root, len(files))
  54. all_files.extend(files)
  55. self.log.info('found %u playable files', len(all_files))
  56. self.files = all_files
  57. def get_random_file(self):
  58. return random.choice(self.files)
  59. def get_random_uri(self):
  60. return 'file://'+self.get_random_file()
  61. class LoopSource(object):
  62. def __init__(self, directory):
  63. self.log = logging.getLogger('LoopSource')
  64. self.directory = directory
  65. pipeline = """
  66. audioresample name=join !
  67. audioconvert !
  68. audio/x-raw,format=S16LE,channels=2,layout=interleaved,rate=48000 !
  69. matroskamux !
  70. tcpclientsink host=localhost port=18000
  71. """
  72. # Parsing Pipeline
  73. self.log.debug('creating pipeline\n%s', pipeline)
  74. self.pipeline = Gst.parse_launch(pipeline)
  75. # Selecting inital URI
  76. inital_uri = self.directory.get_random_uri()
  77. self.log.info('initial track %s', inital_uri)
  78. # Create decoder-element
  79. self.src = Gst.ElementFactory.make('uridecodebin', None)
  80. self.src.set_property('uri', inital_uri);
  81. self.src.connect('pad-added', self.on_pad_added)
  82. self.pipeline.add(self.src)
  83. # Save pad on the Join-Element
  84. self.joinpad = self.pipeline.get_by_name('join').get_static_pad('sink')
  85. # Binding End-of-Stream-Signal on Source-Pipeline
  86. self.pipeline.bus.add_signal_watch()
  87. self.pipeline.bus.connect("message::eos", self.on_eos)
  88. self.pipeline.bus.connect("message::error", self.on_error)
  89. self.log.debug('setting pipeline to playing')
  90. self.pipeline.set_state(Gst.State.PLAYING)
  91. def on_pad_added(self, src, pad):
  92. self.log.debug('new pad on decoder, setting pad-probe')
  93. pad.add_probe(Gst.PadProbeType.EVENT_DOWNSTREAM | Gst.PadProbeType.BLOCK, self.on_pad_event)
  94. if self.joinpad.is_linked():
  95. self.log.debug('unlinking with joinpad')
  96. self.joinpad.unlink(self.joinpad.get_peer())
  97. clock = self.pipeline.get_clock()
  98. if clock:
  99. runtime = clock.get_time() - self.pipeline.get_base_time()
  100. self.log.debug('setting pad offset to pipeline runtime: %sns', runtime)
  101. pad.set_offset(runtime)
  102. self.log.debug('linking with joinpad')
  103. pad.link(self.joinpad)
  104. def on_pad_event(self, pad, info):
  105. event = info.get_event()
  106. self.log.debug('event %s on pad %s', event.type, pad)
  107. if event.type == Gst.EventType.EOS:
  108. self.log.debug('scheduling next track and dropping EOS-Event')
  109. GObject.idle_add(self.next_track)
  110. return Gst.PadProbeReturn.DROP
  111. return Gst.PadProbeReturn.PASS
  112. def next_track(self):
  113. next_uri = self.directory.get_random_uri()
  114. self.log.info('next track %s', next_uri)
  115. self.src.set_state(Gst.State.READY)
  116. self.src.set_property('uri', next_uri);
  117. self.src.set_state(Gst.State.PLAYING)
  118. return False
  119. def on_eos(self, bus, message):
  120. self.log.info('received EOS-Event on bus, exiting')
  121. sys.exit(1)
  122. def on_error(self, bus, message):
  123. self.log.warning('received Error-Event on bus, exiting')
  124. (error, debug) = message.parse_error()
  125. self.log.warning('Error-Details: #%u: %s', error.code, debug)
  126. sys.exit(1)
  127. def main():
  128. signal.signal(signal.SIGINT, signal.SIG_DFL)
  129. parser = argparse.ArgumentParser(description='Voctocore Music-Source')
  130. parser.add_argument('directory')
  131. parser.add_argument('-v|-vv', '--verbose', action='count', default=0,
  132. help="Also print INFO and DEBUG messages.")
  133. args = parser.parse_args()
  134. if args.verbose >= 2:
  135. level = logging.DEBUG
  136. elif args.verbose == 1:
  137. level = logging.INFO
  138. else:
  139. level = logging.WARNING
  140. logging.basicConfig(
  141. level=level,
  142. format='%(levelname)8s %(name)s: %(message)s')
  143. directory = Directory(args.directory)
  144. src = LoopSource(directory)
  145. mainloop = GObject.MainLoop()
  146. try:
  147. mainloop.run()
  148. except KeyboardInterrupt:
  149. print('Terminated via Ctrl-C')
  150. if __name__ == '__main__':
  151. main()