-
Notifications
You must be signed in to change notification settings - Fork 113
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
197 additions
and
0 deletions.
There are no files selected for viewing
197 changes: 197 additions & 0 deletions
197
example-scripts/gstreamer/source-nostream-music-from-folder.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
#!/usr/bin/env python3 | ||
import os, sys, gi, signal, random | ||
import argparse, logging, pyinotify | ||
|
||
gi.require_version('Gst', '1.0') | ||
from gi.repository import Gst, GObject, GLib | ||
|
||
# init GObject & Co. before importing local classes | ||
GObject.threads_init() | ||
Gst.init([]) | ||
|
||
class Directory(object): | ||
def __init__(self, path): | ||
self.log = logging.getLogger('Directory') | ||
self.path = path | ||
self.scheduled = False | ||
self.rescan() | ||
|
||
self.log.debug('setting up inotify watch for %s', self.path) | ||
wm = pyinotify.WatchManager() | ||
notifier = pyinotify.Notifier(wm, | ||
timeout=10, | ||
default_proc_fun=self.inotify_callback) | ||
|
||
wm.add_watch( | ||
self.path, | ||
#pyinotify.ALL_EVENTS, | ||
pyinotify.IN_DELETE | pyinotify.IN_CREATE | pyinotify.IN_MODIFY, | ||
rec=True) | ||
|
||
GLib.io_add_watch( | ||
notifier._fd, | ||
GLib.IO_IN, | ||
self.io_callback, | ||
notifier) | ||
|
||
def inotify_callback(self, notifier): | ||
self.log.info('inotify callback %s: %s', notifier.maskname, notifier.pathname) | ||
if not self.scheduled: | ||
self.scheduled = True | ||
GLib.timeout_add(100, self.rescan) | ||
return True | ||
|
||
def io_callback(self, source, condition, notifier): | ||
notifier.process_events() | ||
while notifier.check_events(): | ||
notifier.read_events() | ||
notifier.process_events() | ||
|
||
return True | ||
|
||
def is_playable_file(self, filepath): | ||
root, ext = os.path.splitext(filepath) | ||
return ext in ['.mp3', '.ogg', '.oga', '.wav', '.m4a', '.flac', 'self.opus'] | ||
|
||
def rescan(self): | ||
self.log.info('scanning directory %s', self.path) | ||
self.scheduled = False | ||
|
||
all_files = [] | ||
|
||
for root, dirs, files in os.walk(self.path): | ||
files = filter(self.is_playable_file, files) | ||
files = map(lambda f: os.path.join(root, f), files) | ||
files = list(files) | ||
|
||
self.log.debug('found directory %s: %u playable file(s)', root, len(files)) | ||
all_files.extend(files) | ||
|
||
self.log.info('found %u playable files', len(all_files)) | ||
self.files = all_files | ||
|
||
def get_random_file(self): | ||
return random.choice(self.files) | ||
|
||
def get_random_uri(self): | ||
return 'file://'+self.get_random_file() | ||
|
||
|
||
class LoopSource(object): | ||
def __init__(self, directory): | ||
self.log = logging.getLogger('LoopSource') | ||
self.directory = directory | ||
|
||
pipeline = """ | ||
audioresample name=join ! | ||
audioconvert ! | ||
audio/x-raw,format=S16LE,channels=2,layout=interleaved,rate=48000 ! | ||
matroskamux ! | ||
tcpclientsink host=localhost port=18000 | ||
""" | ||
|
||
# Parsing Pipeline | ||
self.log.debug('creating pipeline\n%s', pipeline) | ||
self.pipeline = Gst.parse_launch(pipeline) | ||
|
||
# Selecting inital URI | ||
inital_uri = self.directory.get_random_uri() | ||
self.log.info('initial track %s', inital_uri) | ||
|
||
# Create decoder-element | ||
self.src = Gst.ElementFactory.make('uridecodebin', None) | ||
self.src.set_property('uri', inital_uri); | ||
self.src.connect('pad-added', self.on_pad_added) | ||
self.pipeline.add(self.src) | ||
|
||
# Save pad on the Join-Element | ||
self.joinpad = self.pipeline.get_by_name('join').get_static_pad('sink') | ||
|
||
# Binding End-of-Stream-Signal on Source-Pipeline | ||
self.pipeline.bus.add_signal_watch() | ||
self.pipeline.bus.connect("message::eos", self.on_eos) | ||
self.pipeline.bus.connect("message::error", self.on_error) | ||
|
||
self.log.debug('setting pipeline to playing') | ||
self.pipeline.set_state(Gst.State.PLAYING) | ||
|
||
def on_pad_added(self, src, pad): | ||
self.log.debug('new pad on decoder, setting pad-probe') | ||
pad.add_probe(Gst.PadProbeType.EVENT_DOWNSTREAM | Gst.PadProbeType.BLOCK, self.on_pad_event) | ||
if self.joinpad.is_linked(): | ||
self.log.debug('unlinking with joinpad') | ||
self.joinpad.unlink(self.joinpad.get_peer()) | ||
|
||
clock = self.pipeline.get_clock() | ||
if clock: | ||
runtime = clock.get_time() - self.pipeline.get_base_time() | ||
self.log.debug('setting pad offset to pipeline runtime: %sns', runtime) | ||
pad.set_offset(runtime) | ||
|
||
self.log.debug('linking with joinpad') | ||
pad.link(self.joinpad) | ||
|
||
def on_pad_event(self, pad, info): | ||
event = info.get_event() | ||
self.log.debug('event %s on pad %s', event.type, pad) | ||
|
||
if event.type == Gst.EventType.EOS: | ||
self.log.debug('scheduling next track and dropping EOS-Event') | ||
GObject.idle_add(self.next_track) | ||
return Gst.PadProbeReturn.DROP | ||
|
||
return Gst.PadProbeReturn.PASS | ||
|
||
def next_track(self): | ||
next_uri = self.directory.get_random_uri() | ||
self.log.info('next track %s', next_uri) | ||
|
||
self.src.set_state(Gst.State.READY) | ||
self.src.set_property('uri', next_uri); | ||
self.src.set_state(Gst.State.PLAYING) | ||
return False | ||
|
||
def on_eos(self, bus, message): | ||
self.log.info('received EOS-Event on bus, exiting') | ||
sys.exit(1) | ||
|
||
def on_error(self, bus, message): | ||
self.log.warning('received Error-Event on bus, exiting') | ||
(error, debug) = message.parse_error() | ||
self.log.warning('Error-Details: #%u: %s', error.code, debug) | ||
sys.exit(1) | ||
|
||
def main(): | ||
signal.signal(signal.SIGINT, signal.SIG_DFL) | ||
|
||
parser = argparse.ArgumentParser(description='Voctocore Music-Source') | ||
parser.add_argument('directory') | ||
|
||
parser.add_argument('-v|-vv', '--verbose', action='count', default=0, | ||
help="Also print INFO and DEBUG messages.") | ||
|
||
args = parser.parse_args() | ||
|
||
if args.verbose >= 2: | ||
level = logging.DEBUG | ||
elif args.verbose == 1: | ||
level = logging.INFO | ||
else: | ||
level = logging.WARNING | ||
|
||
logging.basicConfig( | ||
level=level, | ||
format='%(levelname)8s %(name)s: %(message)s') | ||
|
||
directory = Directory(args.directory) | ||
src = LoopSource(directory) | ||
|
||
mainloop = GObject.MainLoop() | ||
try: | ||
mainloop.run() | ||
except KeyboardInterrupt: | ||
print('Terminated via Ctrl-C') | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |