1
# Copyright (C) 2008 Valmantas Paliksa <walmis at balticum-tv dot lt>
2
# Copyright (C) 2008 Tadas Dailyda <tadas at dailyda dot com>
4
# Licensed under the GNU General Public License Version 3
6
# This program is free software: you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation, either version 3 of the License, or
9
# (at your option) any later version.
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License
17
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
from blueman.Functions import *
20
from blueman.plugins.AppletPlugin import AppletPlugin
21
from blueman.bluez.Device import Device as BluezDevice
22
from blueman.main.Device import Device
23
from blueman.gui.Notification import Notification
24
from blueman.main.PulseAudioUtils import PulseAudioUtils, EventType
25
from subprocess import Popen, PIPE
29
from blueman.main.SignalTracker import SignalTracker
31
class SourceRedirector:
33
def __init__(self, module_id, device_path, pa_utils):
34
if module_id in SourceRedirector.instances:
37
SourceRedirector.instances.append(module_id)
39
self.module_id = module_id
40
self.pa_utils = pa_utils
41
self.device = Device(device_path)
42
self.signals = SignalTracker()
43
self.bus = dbus.SystemBus()
44
self.signals.Handle("dbus", self.bus, self.on_source_prop_change, "PropertyChanged", "org.bluez.AudioSource", path=device_path)
48
self.loopback_id = None
50
dprint("Starting source redirector")
51
def sources_cb(sources):
52
for k, v in sources.iteritems():
54
if "bluetooth.protocol" in props:
55
if props["bluetooth.protocol"] == "a2dp_source":
56
if v["owner_module"] == self.module_id:
57
dprint("Found source", k)
58
self.start_redirect(k)
60
dprint("Source not found :(")
62
self.pa_utils.ListSources(sources_cb)
64
def start_redirect(self, source):
67
dprint("module-loopback load result", res)
69
self.parec = Popen(["parec", "-d", str(source)], stdout=PIPE)
70
self.pacat = Popen(["pacat", "--client-name=Blueman", "--stream-name=%s" % self.device.Address, "--property=application.icon_name=blueman"], stdin=self.parec.stdout)
72
self.loopback_id = res
74
self.pa_utils.LoadModule("module-loopback", "source=%d" % source, on_load)
76
def on_source_prop_change(self, key, value):
78
if value == "disconnected":
80
self.pacat.terminate()
82
self.parec.terminate()
84
self.pa_utils.UnloadModule(self.loopback_id, lambda x: dprint("Loopback module unload result", x))
86
self.signals.DisconnectAll()
88
SourceRedirector.instances.remove(self.module_id)
93
dprint("Destroying redirector")
95
class Module(gobject.GObject):
97
'loaded' : (gobject.SIGNAL_NO_HOOKS, gobject.TYPE_NONE, ()),
100
gobject.GObject.__init__(self)
106
pa = PulseAudioUtils()
108
pa.UnloadModule(self.id, lambda x: dprint("Unload %s result %s" % (id, x)))
115
dprint(self.id, self.refcount)
120
dprint(self.id, self.refcount)
122
if self.refcount <= 0 and self.id:
125
def load(self, args, cb):
140
PulseAudioUtils().LoadModule("module-bluetooth-device",
144
class PulseAudio(AppletPlugin):
145
__author__ = "Walmis"
146
__description__ = _("Automatically manages Pulseaudio Bluetooth sinks/sources.\n"
147
"<b>Note:</b> Requires pulseaudio 0.9.15 or higher")
148
__icon__ = "audio-card"
150
"checked" : {"type": bool, "default": False},
151
"make_default_sink": {"type":bool,
153
"name": _("Make default sink"),
154
"desc": _("Make the a2dp audio sink the default after connection")},
155
"move_streams": {"type": bool,
157
"name": _("Move streams"),
158
"desc": _("Move existing audio streams to bluetooth device")}
160
def on_load(self, applet):
161
self.signals = SignalTracker()
162
if not self.get_option("checked"):
163
self.set_option("checked", True)
164
if not have("pactl"):
165
applet.Plugins.SetConfig("PulseAudio", False)
168
self.bus = dbus.SystemBus()
170
self.connected_sources = []
171
self.connected_sinks = []
172
self.connected_hs = []
174
self.loaded_modules = {}
176
self.pulse_utils = PulseAudioUtils()
177
version = self.pulse_utils.GetVersion()
178
dprint("PulseAudio version:", version)
181
if tuple(version) < (0, 9, 15):
182
raise Exception("PulseAudio too old, required 0.9.15 or higher")
184
self.signals.Handle("dbus",
186
self.on_sink_prop_change,
188
"org.bluez.AudioSink",
189
path_keyword="device")
191
self.signals.Handle("dbus",
193
self.on_source_prop_change,
195
"org.bluez.AudioSource",
196
path_keyword="device")
198
self.signals.Handle("dbus",
200
self.on_hsp_prop_change,
203
path_keyword="device")
206
self.signals.Handle(self.pulse_utils, "event", self.on_pulse_event)
208
def on_pulse_event(self, pa_utils, event, idx):
209
if (EventType.CARD | EventType.CHANGE) == event:
213
m = self.loaded_modules[c["proplist"]["bluez.path"]]
214
if c["owner_module"] == m.id:
215
if c["active_profile"] == "a2dp_source":
216
SourceRedirector(m.id, c["proplist"]["bluez.path"], pa_utils)
218
pa_utils.GetCard(idx, card_cb)
222
self.signals.DisconnectAll()
224
def load_module(self, dev_path, args, cb=None):
225
if not dev_path in self.loaded_modules:
228
self.loaded_modules[dev_path] = m
230
self.loaded_modules[dev_path].ref()
232
def try_unload_module(self, dev_path):
234
m = self.loaded_modules[dev_path]
237
del self.loaded_modules[dev_path]
241
def on_source_prop_change(self, key, value, device):
245
if value == "connected":
246
if not device in self.connected_sources:
247
self.connected_sources.append(device)
249
self.load_module(device, "path=%s address=%s profile=a2dp_source source_properties=device.icon_name=blueman card_properties=device.icon_name=blueman" % (device, d.Address))
251
elif value == "disconnected":
252
self.try_unload_module(device)
253
if device in self.connected_sources:
254
self.connected_sources.remove(device)
256
elif value == "playing":
258
m = self.loaded_modules[device]
260
SourceRedirector(m.id, device, self.pulse_utils)
264
sig = m.connect("loaded", on_loaded)
266
SourceRedirector(m.id, device, self.pulse_utils)
272
def on_sink_prop_change(self, key, value, device):
273
if key == "Connected" and value:
274
if not device in self.connected_sinks:
275
self.connected_sinks.append(device)
276
gobject.timeout_add(500, self.setup_pa, device, "a2dp")
278
elif key == "Connected" and not value:
279
if device in self.connected_sinks:
280
self.connected_sinks.remove(device)
281
self.try_unload_module(device)
283
def on_hsp_prop_change(self, key, value, device):
284
if key == "Connected" and value:
285
if not device in self.connected_hs:
286
self.connected_hs.append(device)
287
self.setup_pa(device, "hsp")
289
elif key == "Connected" and not value:
290
self.try_unload_module(device)
291
if device in self.connected_hs:
292
self.connected_hs.remove(device)
294
def move_pa_streams(self, sink_id):
295
def inputs_cb(inputs):
296
for k, v in inputs.iteritems():
297
dprint("moving stream", v["name"], "to sink", sink_id)
298
self.pulse_utils.MoveSinkInput(k, sink_id, None)
300
self.pulse_utils.ListSinkInputs(inputs_cb)
302
def setup_pa_sinks(self, module_id):
303
dprint("module", module_id)
306
for k, v in sinks.iteritems():
307
if v["owner_module"] == module_id:
308
if self.get_option("make_default_sink"):
309
dprint("Making sink", v["name"], "the default")
310
self.pulse_utils.SetDefaultSink(v["name"], None)
311
if self.get_option("move_streams"):
312
self.move_pa_streams(k)
315
self.pulse_utils.ListSinks(sinks_cb)
317
def setup_pa(self, device_path, profile):
318
device = Device(device_path)
322
dprint("Load result", res)
325
Notification(_("Bluetooth Audio"),
326
_("Failed to initialize PulseAudio Bluetooth module. Bluetooth audio over PulseAudio will not work."),
327
pixbuf=get_notification_icon("gtk-dialog-error"),
328
status_icon=self.Applet.Plugins.StatusIcon)
330
Notification(_("Bluetooth Audio"),
331
_("Successfully connected to a Bluetooth audio device. This device will now be available in the PulseAudio mixer"),
332
pixbuf=get_notification_icon("audio-card"),
333
status_icon=self.Applet.Plugins.StatusIcon)
334
if profile == "a2dp":
335
self.setup_pa_sinks(res)
337
#connect to other services, so pulseaudio profile switcher could work
338
for s in ("headset", "audiosink", "audiosource"):
340
device.Services[s].Connect()
346
version = self.pulse_utils.GetVersion()
347
if version[0] == 1 or version[2] >= 18:
348
args = "address=%s profile=%s sink_properties=device.icon_name=blueman card_properties=device.icon_name=blueman"
350
args = "address=%s profile=%s"
352
self.load_module(device_path, args % (device.Address, profile), load_cb)