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