mmp

The mini music player, an alternative to MPD

git clone https://git.8pit.net/mmp.git

  1# -*- coding: utf-8 -*-
  2# This file is part of beets.
  3# Copyright 2016, Adrian Sampson.
  4#
  5# Permission is hereby granted, free of charge, to any person obtaining
  6# a copy of this software and associated documentation files (the
  7# "Software"), to deal in the Software without restriction, including
  8# without limitation the rights to use, copy, modify, merge, publish,
  9# distribute, sublicense, and/or sell copies of the Software, and to
 10# permit persons to whom the Software is furnished to do so, subject to
 11# the following conditions:
 12#
 13# The above copyright notice and this permission notice shall be
 14# included in all copies or substantial portions of the Software.
 15
 16"""A wrapper for the GStreamer Python bindings that exposes a simple
 17music player.
 18"""
 19
 20import urllib.parse
 21import _thread
 22import time
 23from threading import BoundedSemaphore, Event, Lock
 24
 25import gi
 26gi.require_version('Gst', '1.0')
 27from gi.repository import GLib, Gst
 28
 29Gst.init(None)
 30
 31class QueryError(Exception):
 32    pass
 33
 34class GstPlayer(object):
 35    """A music player abstracting GStreamer's Playbin element.
 36
 37    Create a player object, then call run() to start a thread with a
 38    runloop. Then call play_file to play music. Use player.playing
 39    to check whether music is currently playing.
 40
 41    A basic play queue is also implemented (just a Python list,
 42    player.queue, whose last element is next to play). To use it,
 43    just call enqueue() and then play(). When a track finishes and
 44    another is available on the queue, it is played automatically.
 45    """
 46
 47    def __init__(self):
 48        """Initialize a player.
 49
 50        Once the player has been created, call run() to begin the main
 51        runloop in a separate thread.
 52        """
 53
 54        def _create_element(name):
 55            elem = Gst.ElementFactory.make(name)
 56            if elem is None:
 57                raise RuntimeError("Could not create element {}".format(elem))
 58            return elem
 59
 60        self.player = _create_element("playbin")
 61        fakesink = _create_element("fakesink")
 62
 63        self.player.set_property("video-sink", fakesink)
 64        bus = self.player.get_bus()
 65        bus.add_signal_watch()
 66        bus.connect("message", self._handle_message)
 67
 68        self._playing_event = Event()
 69        self._paused_event  = Event()
 70
 71        self._finisked_callback = None
 72        self._callback_lock = Lock()
 73        self._finished = Event() # set if playback finished
 74        self.cached_time = None
 75
 76    def clear_callback(self):
 77        self.set_callback(None)
 78
 79    def set_callback(self, fn):
 80        """Sets a callback function invoked when EOS (end of stream) is
 81        reached by GStreamer. The callback should return true if it has
 82        changed the player state and false otherwise. If false is
 83        returned the GstPlayer state is reset."""
 84        self._callback_lock.acquire()
 85        self._finisked_callback = fn
 86        self._callback_lock.release()
 87
 88    def _get_state(self):
 89        """Returns the current state flag of the playbin."""
 90        _, state, _ = self.player.get_state(Gst.CLOCK_TIME_NONE)
 91        return state
 92
 93    def _handle_message(self, bus, message):
 94        """Callback for status updates from GStreamer."""
 95        if message.type == Gst.MessageType.EOS:
 96            # file finished playing
 97            self.cached_time = None
 98            self._finished.set()
 99
100            self._callback_lock.acquire()
101            if self._finisked_callback:
102                if not self._finisked_callback():
103                    self.player.set_state(Gst.State.NULL)
104            self._callback_lock.release()
105
106        elif message.type == Gst.MessageType.ERROR:
107            # error
108            self.player.set_state(Gst.State.NULL)
109            self._finished.set()
110            err, _ = message.parse_error()
111            raise RuntimeError("GStreamer Error: {}".format(err))
112
113        elif message.type == Gst.MessageType.STATE_CHANGED:
114            if isinstance(message.src, Gst.Pipeline):
115                old_state, new_state, _ = message.parse_state_changed()
116                if new_state == Gst.State.PLAYING:
117                    self._playing_event.set()
118                elif new_state == Gst.State.PAUSED:
119                    self._paused_event.set()
120
121    def state(self):
122        """Return current player state as a string."""
123        state = self._get_state()
124        if state == Gst.State.VOID_PENDING:
125            return "pending"
126        elif state == Gst.State.NULL:
127            return "stop"
128        elif state == Gst.State.READY:
129            return "ready"
130        elif state == Gst.State.PAUSED:
131            return "pause"
132        elif state == Gst.State.PLAYING:
133            return "play"
134
135        raise RuntimeError("invalid player state")
136
137    def play_file(self, path):
138        """Immediately begin playing the audio file at the given
139        path.
140        """
141        self.player.set_state(Gst.State.NULL)
142
143        uri = 'file://' + urllib.parse.quote(path)
144        self.player.set_property("uri", uri)
145        self.player.set_state(Gst.State.PLAYING)
146        self._finished.clear()
147        self._playing_event.wait()
148
149    def play(self):
150        """If paused, resume playback."""
151        if self._get_state() == Gst.State.PAUSED:
152            self.player.set_state(Gst.State.PLAYING)
153            self._finished.clear()
154            self._playing_event.wait()
155
156    def pause(self):
157        """Pause playback."""
158        if self._get_state() == Gst.State.PLAYING:
159            self.player.set_state(Gst.State.PAUSED)
160            self._finished.set()
161            self._paused_event.wait()
162
163    def stop(self):
164        """Halt playback."""
165        if self._get_state() == Gst.State.PLAYING:
166            self.player.set_state(Gst.State.NULL)
167            self._finished.set()
168            self.cached_time = None
169            self._paused_event.wait()
170
171    def run(self):
172        """Start a new thread for the player.
173
174        Call this function before trying to play any music with
175        play_file() or play().
176        """
177
178        # If we don't use the MainLoop, messages are never sent.
179
180        def start():
181            loop = GLib.MainLoop()
182            loop.run()
183
184        _thread.start_new_thread(start, ())
185
186    def time(self):
187        """Returns a tuple containing (position, length) where both
188        values are integers in seconds. If no stream is available,
189        returns (0, 0).
190        """
191        fmt = Gst.Format(Gst.Format.TIME)
192        try:
193            posq = self.player.query_position(fmt)
194            if not posq[0]:
195                raise QueryError("query_position failed")
196            pos = posq[1] / (10 ** 9)
197
198            lengthq = self.player.query_duration(fmt)
199            if not lengthq[0]:
200                raise QueryError("query_duration failed")
201            length = lengthq[1] / (10 ** 9)
202
203            self.cached_time = (pos, length)
204            return (pos, length)
205
206        except QueryError:
207            # Stream not ready. For small gaps of time, for instance
208            # after seeking, the time values are unavailable. For this
209            # reason, we cache recent.
210            if (not self._finished.is_set()) and self.cached_time:
211                return self.cached_time
212            else:
213                return (0, 0)
214
215    def seek(self, position):
216        """Seeks to position (in seconds)."""
217        cur_pos, cur_len = self.time()
218        if position > cur_len:
219            self.stop()
220            return
221
222        fmt = Gst.Format(Gst.Format.TIME)
223        ns = position * 10 ** 9  # convert to nanoseconds
224        self.player.seek_simple(fmt, Gst.SeekFlags.FLUSH, ns)
225
226        # save new cached time
227        self.cached_time = (position, cur_len)
228
229    def block(self):
230        """Block until playing finishes."""
231        self._finished.wait()