diff --git a/blueman/bluez/AVRemote.py b/blueman/bluez/AVRemote.py new file mode 100644 index 000000000..655476400 --- /dev/null +++ b/blueman/bluez/AVRemote.py @@ -0,0 +1,40 @@ +# coding=utf-8 +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import +from __future__ import unicode_literals + +from blueman.bluez.PropertiesBase import PropertiesBase +from gi.repository import GObject + + +class AVRemote(PropertiesBase): + __gsignals__ = { + str('status-changed'): (GObject.SignalFlags.NO_HOOKS, None, (GObject.TYPE_PYOBJECT,)) + } + + _interface_name = 'org.bluez.MediaPlayer1' + + def _init(self, obj_path): + super(AVRemote, self)._init(self._interface_name, obj_path) + + def play(self): + self._call('Play') + + def pause(self): + self._call('Pause') + + def stop(self): + self._call('Stop') + + def next(self): + self._call('Next') + + def previous(self): + self._call('Previous') + + def fast_forward(self): + self._call('FastForward') + + def rewind(self): + self._call('Rewind') diff --git a/blueman/bluez/Makefile.am b/blueman/bluez/Makefile.am index bade75583..733addb4c 100644 --- a/blueman/bluez/Makefile.am +++ b/blueman/bluez/Makefile.am @@ -6,6 +6,7 @@ blueman_PYTHON = \ Adapter.py \ Agent.py \ AgentManager.py \ + AVRemote.py \ Base.py \ Device.py \ errors.py \ diff --git a/blueman/plugins/applet/AVRemote.py b/blueman/plugins/applet/AVRemote.py new file mode 100644 index 000000000..f230d4223 --- /dev/null +++ b/blueman/plugins/applet/AVRemote.py @@ -0,0 +1,237 @@ +# coding=utf-8 +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import +from __future__ import unicode_literals + +from blueman.Functions import * +from blueman.Constants import * +from blueman.Sdp import uuid128_to_uuid16, AV_REMOTE_TARGET_SVCLASS_ID +from blueman.plugins.AppletPlugin import AppletPlugin +from blueman.bluez.AVRemote import AVRemote +from gi.repository import GLib +import blueman.bluez as Bluez + +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Pango, GLib +import gettext +import os +import time +from locale import bind_textdomain_codeset +import cgi + + +class PlayerWindow(Gtk.Window): + running = False + + def __init__(self): + if not PlayerWindow.running: + PlayerWindow.running = True + else: + return + + super(PlayerWindow, self).__init__(title="MediaPlayer") + + builder = Gtk.Builder() + builder.add_from_file(UI_PATH + "/media-player.ui") + builder.set_translation_domain("blueman") + bind_textdomain_codeset("blueman", "UTF-8") + + self.add(builder.get_object("media_player")) + + self._control_buttons = {} + self._control_buttons['play'] = builder.get_object("media_play") + self._control_buttons['pause'] = builder.get_object("media_pause") + self._control_buttons['stop'] = builder.get_object("media_stop") + self._control_buttons['next'] = builder.get_object("media_next") + self._control_buttons['prev'] = builder.get_object("media_previous") + + self._control_buttons['play'].connect("clicked", self._on_media_button_clicked, "play") + self._control_buttons['pause'].connect("clicked", self._on_media_button_clicked, "pause") + self._control_buttons['stop'].connect("clicked", self._on_media_button_clicked, "stop") + self._control_buttons['next'].connect("clicked", self._on_media_button_clicked, "next") + self._control_buttons['prev'].connect("clicked", self._on_media_button_clicked, "prev") + + self._artist = builder.get_object("media_artist") + self._track = builder.get_object("media_track") + self._time = builder.get_object("media_time") + + self.set_icon(get_icon("multimedia-player")) + self.show() + self.connect("delete-event", self._on_window_delete) + + cr_name_address = builder.get_object("name_address") + cr_name_address.props.ellipsize = Pango.EllipsizeMode.END + + cr_connected = builder.get_object("connected") + cr_connected.props.sensitive = False + cr_connected.props.style = Pango.Style.ITALIC + + self.liststore = Gtk.ListStore(str, str, object) + self.device_combo = builder.get_object("device_combo") + self.device_combo.set_model(self.liststore) + self.device_combo.connect("changed", self._on_device_combo_changed) + + self._manager = Bluez.Manager() + + self._remote_control_devices = {} + self.active_remote = None + + for adapter in self._manager.list_adapters(): + for device in adapter.list_devices(): + if self._has_remote_control(device['UUIDs']): + object_path = device.get_object_path() + player_path = os.path.join(object_path, 'player0') + remote_control = AVRemote(player_path) + sig = remote_control.connect_signal("property-changed", self._on_property_changed) + self._remote_control_devices[object_path] = (remote_control, sig) + + self.add_to_list(device) + dprint("Added device: ", player_path) + + GLib.timeout_add_seconds(1, self._update_time) + + def add_to_list(self, device): + dev_info = "%s\n%s" % (device['Alias'], device['Address']) + if device['Connected']: + conn_info = "Connected" + else: + conn_info = "Not Connected" + + titer = self.liststore.append([dev_info, conn_info, device.get_object_path()]) + + def _has_remote_control(self, uuids): + for uuid in uuids: + uuid16 = uuid128_to_uuid16(uuid) + if uuid16 == AV_REMOTE_TARGET_SVCLASS_ID: + return True + return False + + def update_track_info(self, remote): + self._update_artist() + self._update_time() + self._update_track() + + def _update_time(self): + if self.active_remote is None: + return True + + pos = self.active_remote['Position'] + track_lenght = float(self.active_remote['Track']['Duration']) + time_left = (track_lenght - pos) / 1000 + left_string = time.strftime('%M:%S', time.gmtime(time_left)) + self._time.set_markup("%s" % left_string) + + return True + + def _update_artist(self): + artist = self.active_remote['Track']['Artist'] + artist_markup = "%s" % cgi.escape(artist) + self._artist.set_markup(artist_markup) + + def _update_track(self): + track = self.active_remote['Track']['Title'] + self._track.set_markup(cgi.escape(track)) + + def _on_device_combo_changed(self, combo): + titer = combo.get_active_iter() + object_path = self.liststore[titer][2] + self.active_remote = self._remote_control_devices[object_path][0] + self.update_track_info(self.active_remote) + dprint(object_path) + + def _on_media_button_clicked(self, button, action): + dprint(action) + if self.active_remote is None: + return + + if action == "play": + self.active_remote.play() + elif action == "pause": + self.active_remote.pause() + elif action == "next": + self.active_remote.next() + elif action == "prev": + self.active_remote.previous() + elif action == "stop": + self.active_remote.stop() + + def _on_window_delete(self, win, event): + for path in list(self._remote_control_devices.keys()): + remote, sig = self._remote_control_devices.pop(path) + remote.disconnect_signal(sig) + + self.active_remote = None + self._remote_control_devices = {} + PlayerWindow.running = False + self.destroy() + + def _update_track_times(self, track_lenght, time_left): + position = self.active_remote['Position'] + self.time_label.set_markup("%s") + + def _on_device_created(self, object_path): + dprint(object_path) + + def _on_device_removed(self, object_path): + dprint(object_path) + + def _on_property_changed(self, remote, name, val, path): + if not path == self.active_remote.get_object_path(): + return + + dprint(name, val) + if name == "Track": + self._update_artist() + self._update_track() + elif name == "Position": + self._update_time() + elif name == "Status": + if val == "stopped": + self._control_buttons['play'].props.sensitive = True + self._control_buttons['stop'].props.sensitive = False + self._control_buttons['pause'].props.sensitive = False + elif val == "playing": + self._control_buttons['play'].props.sensitive = False + self._control_buttons['stop'].props.sensitive = True + self._control_buttons['pause'].props.sensitive = True + elif val == "paused": + self._control_buttons['play'].props.sensitive = True + self._control_buttons['stop'].props.sensitive = False + self._control_buttons['pause'].props.sensitive = False + + +class RemoteMediaPlayer(AppletPlugin): + __depends__ = ["Menu"] + __icon__ = "multimedia-player" + __description__ = _( + "Allows you to control remote A/V media devices.") + __author__ = "Sander Sweers (infirit)" + __autoload__ = False + + def on_load(self, applet): + item = create_menuitem(_("Media _Player"), get_icon("multimedia-player", 16)) + item.props.tooltip_text = _("Show a media player remote control") + item.connect("activate", self.activate_ui) + + self.Applet.Plugins.Menu.Register(self, item, 85, True) + + self.player_window = None + + def on_unload(self): + self.Applet.Plugins.Menu.Unregister(self) + if self.player_window is not None: + self.player_window.destroy() + + def activate_ui(self, item): + if not self.player_window: + self.player_window = PlayerWindow() + elif self.player_window.running: + self.player_window.present() + + def on_device_created(self, device): + self.player_window._on_device_created(device) + + def on_device_removed(self, device): + self.player_window._on_device_removed(device) diff --git a/blueman/plugins/applet/Makefile.am b/blueman/plugins/applet/Makefile.am index ef99f8edf..663a6dff9 100644 --- a/blueman/plugins/applet/Makefile.am +++ b/blueman/plugins/applet/Makefile.am @@ -3,6 +3,7 @@ bluemandir = $(pythondir)/blueman/plugins/applet blueman_PYTHON = \ __init__.py \ AuthAgent.py \ + AVRemote.py \ DBusService.py \ DhcpClient.py \ DiscvManager.py \ diff --git a/blueman/services/AVRemote.py b/blueman/services/AVRemote.py new file mode 100644 index 000000000..1c4b6137e --- /dev/null +++ b/blueman/services/AVRemote.py @@ -0,0 +1,16 @@ +# coding=utf-8 +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import +from __future__ import unicode_literals + +from blueman.Service import Service +from blueman.Sdp import AV_REMOTE_TARGET_SVCLASS_ID + + +class AudioSink(Service): + __group__ = 'audio' + __svclass_id__ = AV_REMOTE_TARGET_SVCLASS_ID + __description__ = _("Allows to control media player device") + __icon__ = "multimedia-player" + __priority__ = 90 diff --git a/blueman/services/Makefile.am b/blueman/services/Makefile.am index 86f6d0622..d86b70c23 100644 --- a/blueman/services/Makefile.am +++ b/blueman/services/Makefile.am @@ -5,6 +5,7 @@ blueman_PYTHON = \ __init__.py \ AudioSink.py \ AudioSource.py \ + AVRemote.py \ DialupNetwork.py \ Functions.py \ GroupNetwork.py \ diff --git a/data/ui/Makefile.am b/data/ui/Makefile.am index e41f6326c..e97839414 100644 --- a/data/ui/Makefile.am +++ b/data/ui/Makefile.am @@ -4,6 +4,7 @@ ui_DATA = \ adapters-tab.ui \ applet-passkey.ui \ manager-main.ui \ + media-player.ui \ device-list-widget.ui \ services-network.ui \ services-transfer.ui \ diff --git a/data/ui/media-player.ui b/data/ui/media-player.ui new file mode 100644 index 000000000..420d5d5e2 --- /dev/null +++ b/data/ui/media-player.ui @@ -0,0 +1,164 @@ + + + + + + True + False + media-skip-forward + + + True + False + media-playback-pause + + + True + False + media-playback-start + + + True + False + media-skip-backward + + + True + False + media-playback-stop + + + True + False + + + Pre_vious + True + True + True + media_previous_image + True + + + 0 + 3 + + + + + True + False + media_track_overwrite + + + 1 + 2 + 4 + + + + + True + False + media_artist_overwrite + + + 1 + 1 + 4 + + + + + _Next + True + True + True + media_next_image + True + + + 4 + 3 + + + + + _Stop + True + True + True + media_stop_image + True + + + 3 + 3 + + + + + P_ause + True + True + True + media_pause_image + True + + + 2 + 3 + + + + + _Play + True + True + True + media_play_image + True + + + 1 + 3 + + + + + True + False + + + + 0 + + + + + + 1 + + + + + 0 + 0 + 5 + + + + + True + False + <b>media_time</b> + True + + + 0 + 1 + 2 + + + +