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 obtaining6# a copy of this software and associated documentation files (the7# "Software"), to deal in the Software without restriction, including8# without limitation the rights to use, copy, modify, merge, publish,9# distribute, sublicense, and/or sell copies of the Software, and to10# permit persons to whom the Software is furnished to do so, subject to11# the following conditions:12#13# The above copyright notice and this permission notice shall be14# included in all copies or substantial portions of the Software.1516"""A wrapper for the GStreamer Python bindings that exposes a simple17music player.18"""1920import urllib.parse21import _thread22import time23from threading import BoundedSemaphore, Event, Lock2425import gi26gi.require_version('Gst', '1.0')27from gi.repository import GLib, Gst2829Gst.init(None)3031class QueryError(Exception):32 pass3334class GstPlayer(object):35 """A music player abstracting GStreamer's Playbin element.3637 Create a player object, then call run() to start a thread with a38 runloop. Then call play_file to play music. Use player.playing39 to check whether music is currently playing.4041 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 and44 another is available on the queue, it is played automatically.45 """4647 def __init__(self):48 """Initialize a player.4950 Once the player has been created, call run() to begin the main51 runloop in a separate thread.52 """5354 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 elem5960 self.player = _create_element("playbin")61 fakesink = _create_element("fakesink")6263 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)6768 self._playing_event = Event()69 self._paused_event = Event()7071 self._finisked_callback = None72 self._callback_lock = Lock()73 self._finished = Event() # set if playback finished74 self.cached_time = None7576 def clear_callback(self):77 self.set_callback(None)7879 def set_callback(self, fn):80 """Sets a callback function invoked when EOS (end of stream) is81 reached by GStreamer. The callback should return true if it has82 changed the player state and false otherwise. If false is83 returned the GstPlayer state is reset."""84 self._callback_lock.acquire()85 self._finisked_callback = fn86 self._callback_lock.release()8788 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 state9293 def _handle_message(self, bus, message):94 """Callback for status updates from GStreamer."""95 if message.type == Gst.MessageType.EOS:96 # file finished playing97 self.cached_time = None98 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()