Lines 4-17
Link Here
|
4 |
# it under the terms of the GNU General Public License version 2 as |
4 |
# it under the terms of the GNU General Public License version 2 as |
5 |
# published by the Free Software Foundation |
5 |
# published by the Free Software Foundation |
6 |
# |
6 |
# |
7 |
# $Id: gstbe.py e8210497a0bb 2009/06/22 05:20:40 $ |
7 |
# $Id$ |
8 |
|
8 |
|
9 |
import gobject |
9 |
import gobject |
|
|
10 |
import glib |
10 |
|
11 |
|
11 |
import pygst |
12 |
import pygst |
12 |
pygst.require("0.10") |
13 |
pygst.require("0.10") |
13 |
|
14 |
|
14 |
import gst |
15 |
import gst |
|
|
16 |
if gst.pygst_version >= (0, 10, 10): |
17 |
import gst.pbutils |
15 |
|
18 |
|
16 |
from quodlibet import config |
19 |
from quodlibet import config |
17 |
from quodlibet import const |
20 |
from quodlibet import const |
Lines 24-36
Link Here
|
24 |
"""Try to create a GStreamer pipeline: |
27 |
"""Try to create a GStreamer pipeline: |
25 |
* Try making the pipeline (defaulting to gconfaudiosink). |
28 |
* Try making the pipeline (defaulting to gconfaudiosink). |
26 |
* If it fails, fall back to autoaudiosink. |
29 |
* If it fails, fall back to autoaudiosink. |
27 |
* If that fails, complain loudly.""" |
30 |
* If that fails, complain loudly. |
|
|
31 |
|
32 |
Returns the pipeline's description and a list of disconnected elements.""" |
28 |
|
33 |
|
29 |
if pipeline == "gconf": pipeline = "gconfaudiosink profile=music" |
34 |
if pipeline == "gconf": pipeline = "gconfaudiosink profile=music" |
30 |
try: pipe = gst.parse_launch(pipeline) |
35 |
try: pipe = [gst.parse_launch(element) for element in pipeline.split('!')] |
31 |
except gobject.GError, err: |
36 |
except gobject.GError, err: |
32 |
if pipeline != "autoaudiosink": |
37 |
if pipeline != "autoaudiosink": |
33 |
try: pipe = gst.parse_launch("autoaudiosink") |
38 |
try: pipe = [gst.parse_launch("autoaudiosink")] |
34 |
except gobject.GError: pipe = None |
39 |
except gobject.GError: pipe = None |
35 |
else: pipeline = "autoaudiosink" |
40 |
else: pipeline = "autoaudiosink" |
36 |
else: pipe = None |
41 |
else: pipe = None |
Lines 47-68
Link Here
|
47 |
|
52 |
|
48 |
def __init__(self, sinkname, librarian=None): |
53 |
def __init__(self, sinkname, librarian=None): |
49 |
super(GStreamerPlayer, self).__init__() |
54 |
super(GStreamerPlayer, self).__init__() |
50 |
device, sinkname = GStreamerSink(sinkname) |
|
|
51 |
self.name = sinkname |
52 |
self.version_info = "GStreamer: %s / PyGSt: %s" % ( |
55 |
self.version_info = "GStreamer: %s / PyGSt: %s" % ( |
53 |
fver(gst.version()), fver(gst.pygst_version)) |
56 |
fver(gst.version()), fver(gst.pygst_version)) |
54 |
self.bin = gst.element_factory_make('playbin') |
57 |
self.librarian = librarian |
|
|
58 |
self.name = sinkname |
59 |
self.bin = None |
60 |
self.connect('destroy', lambda s: self.__destroy_pipeline()) |
61 |
self._in_gapless_transition = False |
62 |
self.paused = True |
63 |
|
64 |
def __init_pipeline(self): |
65 |
if self.bin: return |
66 |
pipeline, self.name = GStreamerSink(self.name) |
67 |
if gst.version() >= (0, 10, 24): |
68 |
self.bin = gst.element_factory_make('playbin2') |
69 |
id = self.bin.connect('about-to-finish', self.__about_to_finish) |
70 |
self.__atf_id = id |
71 |
# The output buffer is necessary to run the song-ended and |
72 |
# song-started events through QL's signal handlers before the |
73 |
# playbin2 hits EOF inside a gapless transition. |
74 |
bufbin = gst.Bin() |
75 |
queue = gst.element_factory_make('queue') |
76 |
queue.set_property('max-size-time', 500 * gst.MSECOND) |
77 |
self._vol_element = vol = gst.element_factory_make('volume') |
78 |
pipeline = [queue, vol] + pipeline |
79 |
for idx, elem in enumerate(pipeline): |
80 |
bufbin.add(elem) |
81 |
if idx > 0: |
82 |
pipeline[idx-1].link(elem) |
83 |
gpad = gst.GhostPad('sink', queue.get_pad('sink')) |
84 |
bufbin.add_pad(gpad) |
85 |
self.bin.set_property('audio-sink', bufbin) |
86 |
else: |
87 |
self.bin = gst.element_factory_make('playbin') |
88 |
self.bin.set_property('audio-sink', pipeline[-1]) |
89 |
self._vol_element = self.bin |
90 |
self.__atf_id = None |
55 |
self.bin.set_property('video-sink', None) |
91 |
self.bin.set_property('video-sink', None) |
56 |
self.bin.set_property('audio-sink', device) |
92 |
# ReplayGain information gets lost when destroying |
|
|
93 |
self.volume = self.volume |
57 |
bus = self.bin.get_bus() |
94 |
bus = self.bin.get_bus() |
58 |
bus.add_signal_watch() |
95 |
bus.add_signal_watch() |
59 |
bus.connect('message', self.__message, librarian) |
96 |
self.__bus_id = bus.connect('message', self.__message, self.librarian) |
60 |
self.connect_object('destroy', self.bin.set_state, gst.STATE_NULL) |
97 |
if gst.pygst_version >= (0, 10, 10): |
61 |
self.paused = True |
98 |
self.__elem_id = bus.connect('message::element', |
|
|
99 |
self.__message_elem) |
100 |
|
101 |
def __destroy_pipeline(self): |
102 |
if self.bin is None: return |
103 |
self.bin.set_state(gst.STATE_NULL) |
104 |
bus = self.bin.get_bus() |
105 |
bus.disconnect(self.__bus_id) |
106 |
if gst.pygst_version >= (0, 10, 10): |
107 |
bus.disconnect(self.__elem_id) |
108 |
bus.remove_signal_watch() |
109 |
if self.__atf_id is not None: |
110 |
self.bin.disconnect(self.__atf_id) |
111 |
del self.bin |
112 |
del self._vol_element |
113 |
self.bin = None |
114 |
return True |
62 |
|
115 |
|
63 |
def __message(self, bus, message, librarian): |
116 |
def __message(self, bus, message, librarian): |
64 |
if message.type == gst.MESSAGE_EOS: |
117 |
if message.type == gst.MESSAGE_EOS: |
65 |
self._source.next_ended() |
118 |
if not self._in_gapless_transition: |
|
|
119 |
self._source.next_ended() |
66 |
self._end(False) |
120 |
self._end(False) |
67 |
elif message.type == gst.MESSAGE_TAG: |
121 |
elif message.type == gst.MESSAGE_TAG: |
68 |
self.__tag(message.parse_tag(), librarian) |
122 |
self.__tag(message.parse_tag(), librarian) |
Lines 72-95
Link Here
|
72 |
self.error(err, True) |
126 |
self.error(err, True) |
73 |
return True |
127 |
return True |
74 |
|
128 |
|
|
|
129 |
def __message_elem(self, bus, message): |
130 |
if not message.structure.get_name().startswith('missing-'): |
131 |
return True |
132 |
d = gst.pbutils.missing_plugin_message_get_installer_detail(message) |
133 |
ctx = gst.pbutils.InstallPluginsContext() |
134 |
gobject.idle_add(self.stop) |
135 |
gst.pbutils.install_plugins_async([d], ctx, self.__message_elem_cb) |
136 |
return True |
137 |
|
138 |
def __message_elem_cb(self, result): |
139 |
gst.update_registry() |
140 |
|
141 |
def __about_to_finish(self, pipeline): |
142 |
self._in_gapless_transition = True |
143 |
self._source.next_ended() |
144 |
if self._source.current: |
145 |
self.bin.set_property('uri', self._source.current("~uri")) |
146 |
glib.timeout_add(0, self._end, False, True, |
147 |
priority = glib.PRIORITY_HIGH) |
148 |
|
75 |
def stop(self): |
149 |
def stop(self): |
76 |
# On GStreamer, we can release the device when stopped. |
150 |
# On GStreamer, we can release the device when stopped. |
77 |
if not self.paused: |
151 |
if not self.paused: |
78 |
self._paused = True |
152 |
self._paused = True |
79 |
if self.song: |
153 |
if self.song: |
80 |
self.emit('paused') |
154 |
self.emit('paused') |
81 |
self.bin.set_state(gst.STATE_NULL) |
155 |
self.__destroy_pipeline() |
82 |
self.seek(0) |
|
|
83 |
|
156 |
|
84 |
def do_set_property(self, property, v): |
157 |
def do_set_property(self, property, v): |
85 |
if property.name == 'volume': |
158 |
if property.name == 'volume': |
|
|
159 |
if self._in_gapless_transition: |
160 |
return |
86 |
self._volume = v |
161 |
self._volume = v |
87 |
if self.song is None: |
162 |
if self.song and config.getboolean("player", "replaygain"): |
88 |
self.bin.set_property('volume', v) |
163 |
profiles = filter(None, self.replaygain_profiles)[0] |
89 |
else: |
164 |
v = max(0.0, min(4.0, v * self.song.replay_gain(profiles))) |
90 |
if config.getboolean("player", "replaygain"): |
165 |
if self.bin: |
91 |
profiles = filter(None, self.replaygain_profiles)[0] |
|
|
92 |
v = max(0.0, min(4.0, v * self.song.replay_gain(profiles))) |
93 |
self.bin.set_property('volume', v) |
166 |
self.bin.set_property('volume', v) |
94 |
else: |
167 |
else: |
95 |
raise AttributeError |
168 |
raise AttributeError |
Lines 97-118
Link Here
|
97 |
def get_position(self): |
170 |
def get_position(self): |
98 |
"""Return the current playback position in milliseconds, |
171 |
"""Return the current playback position in milliseconds, |
99 |
or 0 if no song is playing.""" |
172 |
or 0 if no song is playing.""" |
100 |
if self.bin.get_property('uri'): |
173 |
if self.song and self.bin: |
101 |
try: p = self.bin.query_position(gst.FORMAT_TIME)[0] |
174 |
try: p = self.bin.query_position(gst.FORMAT_TIME)[0] |
102 |
except gst.QueryError: p = 0 |
175 |
except gst.QueryError: p = 0 |
103 |
p //= gst.MSECOND |
176 |
p //= gst.MSECOND |
104 |
return p |
177 |
return p |
105 |
else: return 0 |
178 |
else: |
106 |
|
179 |
return 0 |
|
|
180 |
|
107 |
def _set_paused(self, paused): |
181 |
def _set_paused(self, paused): |
108 |
if paused != self._paused: |
182 |
if paused != self._paused: |
109 |
self._paused = paused |
183 |
self._paused = paused |
110 |
if self.song: |
184 |
if self.song: |
111 |
self.emit((paused and 'paused') or 'unpaused') |
185 |
self.emit((paused and 'paused') or 'unpaused') |
|
|
186 |
if not self.bin: |
187 |
self.__init_pipeline() |
112 |
if self._paused: |
188 |
if self._paused: |
113 |
if not self.song.is_file: |
189 |
self.bin.set_state(gst.STATE_PAUSED) |
114 |
self.bin.set_state(gst.STATE_NULL) |
|
|
115 |
else: self.bin.set_state(gst.STATE_PAUSED) |
116 |
else: self.bin.set_state(gst.STATE_PLAYING) |
190 |
else: self.bin.set_state(gst.STATE_PLAYING) |
117 |
elif paused is True: |
191 |
elif paused is True: |
118 |
# Something wants us to pause between songs, or when |
192 |
# Something wants us to pause between songs, or when |
Lines 122-136
Link Here
|
122 |
paused = property(_get_paused, _set_paused) |
196 |
paused = property(_get_paused, _set_paused) |
123 |
|
197 |
|
124 |
def error(self, message, lock): |
198 |
def error(self, message, lock): |
125 |
self.bin.set_property('uri', '') |
199 |
print_w(message) |
126 |
self.bin.set_state(gst.STATE_NULL) |
|
|
127 |
self.emit('error', self.song, message, lock) |
200 |
self.emit('error', self.song, message, lock) |
128 |
if not self.paused: |
201 |
if not self.paused: |
129 |
self.next() |
202 |
self.next() |
130 |
|
203 |
|
131 |
def seek(self, pos): |
204 |
def seek(self, pos): |
132 |
"""Seek to a position in the song, in milliseconds.""" |
205 |
"""Seek to a position in the song, in milliseconds.""" |
133 |
if self.bin.get_property('uri'): |
206 |
if self.song is not None: |
134 |
pos = max(0, int(pos)) |
207 |
pos = max(0, int(pos)) |
135 |
if pos >= self._length: |
208 |
if pos >= self._length: |
136 |
self.paused = True |
209 |
self.paused = True |
Lines 143-149
Link Here
|
143 |
if self.bin.send_event(event): |
216 |
if self.bin.send_event(event): |
144 |
self.emit('seek', self.song, pos) |
217 |
self.emit('seek', self.song, pos) |
145 |
|
218 |
|
146 |
def _end(self, stopped): |
219 |
def _end(self, stopped, gapless = False): |
147 |
# We need to set self.song to None before calling our signal |
220 |
# We need to set self.song to None before calling our signal |
148 |
# handlers. Otherwise, if they try to end the song they're given |
221 |
# handlers. Otherwise, if they try to end the song they're given |
149 |
# (e.g. by removing it), then we get in an infinite loop. |
222 |
# (e.g. by removing it), then we get in an infinite loop. |
Lines 152-172
Link Here
|
152 |
self.emit('song-ended', song, stopped) |
225 |
self.emit('song-ended', song, stopped) |
153 |
|
226 |
|
154 |
# Then, set up the next song. |
227 |
# Then, set up the next song. |
|
|
228 |
self._in_gapless_transition = False |
155 |
self.song = self.info = self._source.current |
229 |
self.song = self.info = self._source.current |
156 |
self.emit('song-started', self.song) |
230 |
self.emit('song-started', self.song) |
157 |
|
231 |
|
158 |
if self.song is not None: |
232 |
if self.song is not None: |
159 |
# Changing the URI in a playbin requires "resetting" it. |
233 |
self._length = self.info["~#length"] * 1000 |
160 |
if not self.bin.set_state(gst.STATE_NULL): return |
234 |
if not gapless: |
161 |
self.bin.set_property('uri', self.song("~uri")) |
235 |
# Due to extensive problems with playbin2, we destroy the |
162 |
self._length = self.song["~#length"] * 1000 |
236 |
# entire pipeline and recreate it each time we're not in |
163 |
if self._paused: self.bin.set_state(gst.STATE_PAUSED) |
237 |
# a gapless transition. |
164 |
else: self.bin.set_state(gst.STATE_PLAYING) |
238 |
self.__destroy_pipeline() |
|
|
239 |
self.__init_pipeline() |
240 |
self.bin.set_property('uri', self.song("~uri")) |
241 |
if self.bin: |
242 |
if self._paused: |
243 |
self.bin.set_state(gst.STATE_PAUSED) |
244 |
else: |
245 |
self.bin.set_state(gst.STATE_PLAYING) |
165 |
else: |
246 |
else: |
166 |
|
|
|
167 |
self.paused = True |
247 |
self.paused = True |
168 |
self.bin.set_state(gst.STATE_NULL) |
248 |
self.__destroy_pipeline() |
169 |
self.bin.set_property('uri', '') |
|
|
170 |
|
249 |
|
171 |
def __tag(self, tags, librarian): |
250 |
def __tag(self, tags, librarian): |
172 |
if self.song and self.song.multisong: |
251 |
if self.song and self.song.multisong: |
Lines 224-230
Link Here
|
224 |
|
303 |
|
225 |
def init(librarian): |
304 |
def init(librarian): |
226 |
pipeline = (config.get("player", "gst_pipeline") or |
305 |
pipeline = (config.get("player", "gst_pipeline") or |
227 |
"gconfaudiosink profile = music") |
306 |
"gconfaudiosink profile=music") |
228 |
gst.debug_set_default_threshold(gst.LEVEL_ERROR) |
307 |
gst.debug_set_default_threshold(gst.LEVEL_ERROR) |
229 |
if gst.element_make_from_uri( |
308 |
if gst.element_make_from_uri( |
230 |
gst.URI_SRC, |
309 |
gst.URI_SRC, |
Lines 234-237
Link Here
|
234 |
raise PlayerError( |
313 |
raise PlayerError( |
235 |
_("Unable to open input files"), |
314 |
_("Unable to open input files"), |
236 |
_("GStreamer has no element to handle reading files. Check " |
315 |
_("GStreamer has no element to handle reading files. Check " |
237 |
"your GStreamer installation settings.")) |
316 |
"your GStreamer installation settings.")) |