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() 99100 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()105106 elif message.type == Gst.MessageType.ERROR:107 # error108 self.player.set_state(Gst.State.NULL)109 self._finished.set()110 err, _ = message.parse_error()111 raise RuntimeError("GStreamer Error: {}".format(err))112113 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()120121 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"134135 raise RuntimeError("invalid player state")136137 def play_file(self, path):138 """Immediately begin playing the audio file at the given139 path.140 """141 self.player.set_state(Gst.State.NULL)142143 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()148149 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()155156 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()162163 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 = None169 self._paused_event.wait()170171 def run(self):172 """Start a new thread for the player.173174 Call this function before trying to play any music with175 play_file() or play().176 """177178 # If we don't use the MainLoop, messages are never sent.179180 def start():181 loop = GLib.MainLoop()182 loop.run()183184 _thread.start_new_thread(start, ())185186 def time(self):187 """Returns a tuple containing (position, length) where both188 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)197198 lengthq = self.player.query_duration(fmt)199 if not lengthq[0]:200 raise QueryError("query_duration failed")201 length = lengthq[1] / (10 ** 9)202203 self.cached_time = (pos, length)204 return (pos, length)205206 except QueryError:207 # Stream not ready. For small gaps of time, for instance208 # after seeking, the time values are unavailable. For this209 # reason, we cache recent.210 if (not self._finished.is_set()) and self.cached_time:211 return self.cached_time212 else:213 return (0, 0)214215 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 return221222 fmt = Gst.Format(Gst.Format.TIME)223 ns = position * 10 ** 9 # convert to nanoseconds224 self.player.seek_simple(fmt, Gst.SeekFlags.FLUSH, ns)225226 # save new cached time227 self.cached_time = (position, cur_len)228229 def block(self):230 """Block until playing finishes."""231 self._finished.wait()