Ticket #3714: 0001-Modularization-of-classes.patch

File 0001-Modularization-of-classes.patch, 91.2 KB (added by humitos, 11 years ago)
  • deleted file ControlToolbar.py

    From a9d92381d222f8c5fff29c2b6d90f78820c27ea2 Mon Sep 17 00:00:00 2001
    From: Manuel Kaufmann <humitos@gmail.com>
    Date: Mon, 14 Jan 2013 12:42:35 -0300
    Subject: [PATCH Jukebox 1/2] Modularization of classes
    
     - Put each class in a different .py file
     - Rename jukeboxactivity.py to activity.py to make this more standard
    
    Signed-off-by: Manuel Kaufmann <humitos@gmail.com>
    ---
     ControlToolbar.py      | 169 ----------
     activity.py            | 678 +++++++++++++++++++++++++++++++++++++++
     activity/activity.info |   2 +-
     controls.py            | 134 ++++++++
     jukeboxactivity.py     | 841 -------------------------------------------------
     player.py              | 182 +++++++++++
     playlist.py            | 137 ++++++++
     viewtoolbar.py         | 171 ++++++++++
     widgets.py             | 137 --------
     9 files changed, 1303 insertions(+), 1148 deletions(-)
     delete mode 100644 ControlToolbar.py
     create mode 100644 activity.py
     create mode 100644 controls.py
     delete mode 100644 jukeboxactivity.py
     create mode 100644 player.py
     create mode 100644 playlist.py
     create mode 100644 viewtoolbar.py
     delete mode 100644 widgets.py
    
    diff --git a/ControlToolbar.py b/ControlToolbar.py
    deleted file mode 100644
    index 2205bde..0000000
    + -  
    1 # Copyright (C) 2007 Andy Wingo <wingo@pobox.com>
    2 # Copyright (C) 2007 Red Hat, Inc.
    3 # Copyright (C) 2008 Kushal Das <kushal@fedoraproject.org>
    4 # This program is free software; you can redistribute it and/or modify
    5 # it under the terms of the GNU General Public License as published by
    6 # the Free Software Foundation; either version 2 of the License, or
    7 # (at your option) any later version.
    8 #
    9 # This program is distributed in the hope that it will be useful,
    10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
    11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12 # GNU General Public License for more details.
    13 #
    14 # You should have received a copy of the GNU General Public License
    15 # along with this program; if not, write to the Free Software
    16 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
    17 
    18 import logging
    19 
    20 from gettext import gettext as _
    21 
    22 from gi.repository import GObject
    23 from gi.repository import Gtk
    24 
    25 from sugar3.graphics.toolbutton import ToolButton
    26 from sugar3.graphics.toggletoolbutton import ToggleToolButton
    27 
    28 
    29 class ViewToolbar(Gtk.Toolbar):
    30     __gtype_name__ = 'ViewToolbar'
    31 
    32     __gsignals__ = {
    33         'go-fullscreen': (GObject.SignalFlags.RUN_FIRST,
    34                           None,
    35                          ([])),
    36         'toggle-playlist': (GObject.SignalFlags.RUN_FIRST,
    37                             None,
    38                             ([]))
    39     }
    40 
    41     def __init__(self):
    42         GObject.GObject.__init__(self)
    43 
    44         self._show_playlist = ToggleToolButton('view-list')
    45         self._show_playlist.set_active(True)
    46         self._show_playlist.set_tooltip(_('Show Playlist'))
    47         self._show_playlist.connect('toggled', self._playlist_toggled_cb)
    48         self.insert(self._show_playlist, -1)
    49         self._show_playlist.show()
    50 
    51         self._fullscreen = ToolButton('view-fullscreen')
    52         self._fullscreen.set_tooltip(_('Fullscreen'))
    53         self._fullscreen.connect('clicked', self._fullscreen_cb)
    54         self.insert(self._fullscreen, -1)
    55         self._fullscreen.show()
    56 
    57     def _fullscreen_cb(self, button):
    58         self.emit('go-fullscreen')
    59 
    60     def _playlist_toggled_cb(self, button):
    61         self.emit('toggle-playlist')
    62 
    63 
    64 class Control(GObject.GObject):
    65     """Class to create the Control (play) toolbar"""
    66 
    67     def __init__(self, toolbar, jukebox):
    68         GObject.GObject.__init__(self)
    69 
    70         self.toolbar = toolbar
    71         self.jukebox = jukebox
    72 
    73         self.open_button = ToolButton('list-add')
    74         self.open_button.set_tooltip(_('Add track'))
    75         self.open_button.show()
    76         self.open_button.connect('clicked', jukebox.open_button_clicked_cb)
    77         self.toolbar.insert(self.open_button, -1)
    78 
    79         erase_playlist_entry_btn = ToolButton(icon_name='list-remove')
    80         erase_playlist_entry_btn.set_tooltip(_('Remove track'))
    81         erase_playlist_entry_btn.connect('clicked',
    82                  jukebox._erase_playlist_entry_clicked_cb)
    83         self.toolbar.insert(erase_playlist_entry_btn, -1)
    84 
    85         spacer = Gtk.SeparatorToolItem()
    86         self.toolbar.insert(spacer, -1)
    87         spacer.show()
    88 
    89         self.prev_button = ToolButton('player_rew')
    90         self.prev_button.set_tooltip(_('Previous'))
    91         self.prev_button.show()
    92         self.prev_button.connect('clicked', self.prev_button_clicked_cb)
    93         self.toolbar.insert(self.prev_button, -1)
    94 
    95         self.pause_image = Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_PAUSE,
    96                                                     Gtk.IconSize.BUTTON)
    97         self.pause_image.show()
    98         self.play_image = Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_PLAY,
    99                                                    Gtk.IconSize.BUTTON)
    100         self.play_image.show()
    101 
    102         self.button = Gtk.ToolButton()
    103         self.button.set_icon_widget(self.play_image)
    104         self.button.set_property('can-default', True)
    105         self.button.show()
    106         self.button.connect('clicked', self._button_clicked_cb)
    107 
    108         self.toolbar.insert(self.button, -1)
    109 
    110         self.next_button = ToolButton('player_fwd')
    111         self.next_button.set_tooltip(_('Next'))
    112         self.next_button.show()
    113         self.next_button.connect('clicked', self.next_button_clicked_cb)
    114         self.toolbar.insert(self.next_button, -1)
    115 
    116         current_time = Gtk.ToolItem()
    117         self.current_time_label = Gtk.Label(label='')
    118         current_time.add(self.current_time_label)
    119         current_time.show()
    120         toolbar.insert(current_time, -1)
    121 
    122         self.adjustment = Gtk.Adjustment(0.0, 0.00, 100.0, 0.1, 1.0, 1.0)
    123         self.hscale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL,
    124                                 adjustment=self.adjustment)
    125         self.hscale.set_draw_value(False)
    126         # FIXME: this seems to be deprecated
    127         # self.hscale.set_update_policy(Gtk.UPDATE_CONTINUOUS)
    128         logging.debug("FIXME: AttributeError: 'Scale' object has no "
    129                       "attribute 'set_update_policy'")
    130         self.hscale.connect('button-press-event',
    131                 jukebox.scale_button_press_cb)
    132         self.hscale.connect('button-release-event',
    133                 jukebox.scale_button_release_cb)
    134 
    135         self.scale_item = Gtk.ToolItem()
    136         self.scale_item.set_expand(True)
    137         self.scale_item.add(self.hscale)
    138         self.toolbar.insert(self.scale_item, -1)
    139 
    140         total_time = Gtk.ToolItem()
    141         self.total_time_label = Gtk.Label(label='')
    142         total_time.add(self.total_time_label)
    143         total_time.show()
    144         toolbar.insert(total_time, -1)
    145 
    146     def prev_button_clicked_cb(self, widget):
    147         self.jukebox.songchange('prev')
    148 
    149     def next_button_clicked_cb(self, widget):
    150         self.jukebox.songchange('next')
    151 
    152     def _button_clicked_cb(self, widget):
    153         self.jukebox.play_toggled()
    154 
    155     def set_button_play(self):
    156         self.button.set_icon_widget(self.play_image)
    157 
    158     def set_button_pause(self):
    159         self.button.set_icon_widget(self.pause_image)
    160 
    161     def set_disabled(self):
    162         self.button.set_sensitive(False)
    163         self.scale_item.set_sensitive(False)
    164         self.hscale.set_sensitive(False)
    165 
    166     def set_enabled(self):
    167         self.button.set_sensitive(True)
    168         self.scale_item.set_sensitive(True)
    169         self.hscale.set_sensitive(True)
  • new file activity.py

    diff --git a/activity.py b/activity.py
    new file mode 100644
    index 0000000..14fcbe9
    - +  
     1# This program is free software; you can redistribute it and/or
     2# modify it under the terms of the GNU Lesser General Public
     3# License as published by the Free Software Foundation; either
     4# version 2.1 of the License, or (at your option) any later version.
     5#
     6# This library is distributed in the hope that it will be useful,
     7# but WITHOUT ANY WARRANTY; without even the implied warranty of
     8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     9# Lesser General Public License for more details.
     10#
     11# You should have received a copy of the GNU Lesser General Public
     12# License along with this library; if not, write to the Free Software
     13# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
     14# USA
     15
     16# Activity that plays media.
     17# Copyright (C) 2007 Andy Wingo <wingo@pobox.com>
     18# Copyright (C) 2007 Red Hat, Inc.
     19# Copyright (C) 2008-2010 Kushal Das <kushal@fedoraproject.org>
     20# Copyright (C) 2013 Manuel Kaufmann <humitos@gmail.com>
     21
     22import sys
     23import logging
     24import tempfile
     25from gettext import gettext as _
     26import os
     27
     28from sugar3.activity import activity
     29from sugar3.graphics.objectchooser import ObjectChooser
     30from sugar3 import mime
     31from sugar3.datastore import datastore
     32
     33from sugar3.graphics.toolbarbox import ToolbarBox
     34from sugar3.graphics.toolbarbox import ToolbarButton
     35from sugar3.activity.widgets import StopButton
     36from sugar3.activity.widgets import ActivityToolbarButton
     37from sugar3.graphics.alert import ErrorAlert
     38from sugar3.graphics.alert import Alert
     39
     40import gi
     41gi.require_version('Gtk', '3.0')
     42gi.require_version('Gst', '1.0')
     43
     44from gi.repository import GObject
     45from gi.repository import Gdk
     46from gi.repository import Gtk
     47from gi.repository import Gst
     48from gi.repository import Gio
     49
     50import urllib
     51from viewtoolbar import ViewToolbar
     52from controls import Controls
     53from player import GstPlayer
     54
     55from playlist import PlayList
     56
     57PLAYLIST_WIDTH_PROP = 1.0 / 3
     58
     59
     60class JukeboxActivity(activity.Activity):
     61    UPDATE_INTERVAL = 500
     62
     63    def __init__(self, handle):
     64        activity.Activity.__init__(self, handle)
     65        self._object_id = handle.object_id
     66        self.set_title(_('Jukebox Activity'))
     67        self.player = None
     68        self.max_participants = 1
     69        self._playlist_jobject = None
     70
     71        toolbar_box = ToolbarBox()
     72        activity_button = ActivityToolbarButton(self)
     73        activity_toolbar = activity_button.page
     74        toolbar_box.toolbar.insert(activity_button, 0)
     75        self.title_entry = activity_toolbar.title
     76
     77        # FIXME: I don't know what is the mission of this line
     78        # activity_toolbar.stop.hide()
     79
     80        self.volume_monitor = Gio.VolumeMonitor.get()
     81        self.volume_monitor.connect('mount-added', self._mount_added_cb)
     82        self.volume_monitor.connect('mount-removed', self._mount_removed_cb)
     83
     84        _view_toolbar = ViewToolbar()
     85        _view_toolbar.connect('go-fullscreen',
     86                              self.__go_fullscreen_cb)
     87        _view_toolbar.connect('toggle-playlist',
     88                              self.__toggle_playlist_cb)
     89        view_toolbar_button = ToolbarButton(
     90            page=_view_toolbar,
     91            icon_name='toolbar-view')
     92        _view_toolbar.show()
     93        toolbar_box.toolbar.insert(view_toolbar_button, -1)
     94        view_toolbar_button.show()
     95
     96        self.control = Controls(toolbar_box.toolbar, self)
     97
     98        toolbar_box.toolbar.insert(StopButton(self), -1)
     99
     100        self.set_toolbar_box(toolbar_box)
     101        toolbar_box.show_all()
     102
     103        self.connect("key_press_event", self._key_press_event_cb)
     104
     105        # We want to be notified when the activity gets the focus or
     106        # loses it.  When it is not active, we don't need to keep
     107        # reproducing the video
     108        self.connect("notify::active", self._notify_active_cb)
     109
     110        # FIXME: this is related with shared activity and it doesn't work
     111        # if handle.uri:
     112        #     pass
     113        # elif self._shared_activity:
     114        #     if self.get_shared():
     115        #         pass
     116        #     else:
     117        #         # Wait for a successful join before trying to get the document
     118        #         self.connect("joined", self._joined_cb)
     119
     120        self.update_id = -1
     121        self.changed_id = -1
     122        self.seek_timeout_id = -1
     123        self.player = None
     124        self.uri = None
     125
     126        # {'url': 'file://.../media.ogg', 'title': 'My song', object_id: '..'}
     127        self.playlist = []
     128
     129        self.jobjectlist = []
     130        self.playpath = None
     131        self.currentplaying = None
     132        self.playflag = False
     133        self._not_found_files = 0
     134
     135        # README: I changed this because I was getting an error when I
     136        # tried to modify self.bin with something different than
     137        # Gtk.Bin
     138
     139        # self.bin = Gtk.HBox()
     140        # self.bin.show()
     141
     142        self.canvas = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
     143        self._alert = None
     144
     145        self.playlist_widget = PlayList(self.play)
     146        self.playlist_widget.update(self.playlist)
     147        self.playlist_widget.show()
     148        self.canvas.pack_start(self.playlist_widget, False, True, 0)
     149        self._empty_widget = Gtk.Label(label="")
     150        self._empty_widget.show()
     151        self.videowidget = VideoWidget()
     152        self.set_canvas(self.canvas)
     153        self._init_view_area()
     154        self.show_all()
     155        self.canvas.connect('size-allocate', self.__size_allocate_cb)
     156
     157        #From ImageViewer Activity
     158        self._want_document = True
     159        if self._object_id is None:
     160            self._show_object_picker = GObject.timeout_add(1000, \
     161            self._show_picker_cb)
     162
     163        if handle.uri:
     164            self.uri = handle.uri
     165            GObject.idle_add(self._start, self.uri, handle.title)
     166
     167        # Create the player just once
     168        logging.debug('Instantiating GstPlayer')
     169        self.player = GstPlayer(self.videowidget)
     170        self.player.connect("eos", self._player_eos_cb)
     171        self.player.connect("error", self._player_error_cb)
     172        self.p_position = Gst.CLOCK_TIME_NONE
     173        self.p_duration = Gst.CLOCK_TIME_NONE
     174
     175    def _notify_active_cb(self, widget, event):
     176        """Sugar notify us that the activity is becoming active or inactive.
     177        When we are inactive, we stop the player if it is reproducing
     178        a video.
     179        """
     180        if self.player.player.props.uri is not None:
     181            if not self.player.is_playing() and self.props.active:
     182                self.player.play()
     183            if self.player.is_playing() and not self.props.active:
     184                self.player.pause()
     185
     186    def _init_view_area(self):
     187        """
     188        Use a notebook with two pages, one empty an another
     189        with the videowidget
     190        """
     191        self.view_area = Gtk.Notebook()
     192        self.view_area.set_show_tabs(False)
     193        self.view_area.append_page(self._empty_widget, None)
     194        self.view_area.append_page(self.videowidget, None)
     195        self.canvas.pack_end(self.view_area, expand=True,
     196                             fill=True, padding=0)
     197
     198    def _switch_canvas(self, show_video):
     199        """Show or hide the video visualization in the canvas.
     200
     201        When hidden, the canvas is filled with an empty widget to
     202        ensure redrawing.
     203
     204        """
     205        if show_video:
     206            self.view_area.set_current_page(1)
     207        else:
     208            self.view_area.set_current_page(0)
     209        self.canvas.queue_draw()
     210
     211    def __size_allocate_cb(self, widget, allocation):
     212        canvas_size = self.canvas.get_allocation()
     213        playlist_width = int(canvas_size.width * PLAYLIST_WIDTH_PROP)
     214        self.playlist_widget.set_size_request(playlist_width, 0)
     215
     216    def open_button_clicked_cb(self, widget):
     217        """ To open the dialog to select a new file"""
     218        #self.player.seek(0L)
     219        #self.player.stop()
     220        #self.playlist = []
     221        #self.playpath = None
     222        #self.currentplaying = None
     223        #self.playflag = False
     224        self._want_document = True
     225        self._show_object_picker = GObject.timeout_add(1, self._show_picker_cb)
     226
     227    def _key_press_event_cb(self, widget, event):
     228        keyname = Gdk.keyval_name(event.keyval)
     229        logging.info("Keyname Press: %s, time: %s", keyname, event.time)
     230        if self.title_entry.has_focus():
     231            return False
     232
     233        if keyname == "space":
     234            self.play_toggled()
     235            return True
     236
     237    def check_if_next_prev(self):
     238        if self.currentplaying == 0:
     239            self.control.prev_button.set_sensitive(False)
     240        else:
     241            self.control.prev_button.set_sensitive(True)
     242        if self.currentplaying == len(self.playlist) - 1:
     243            self.control.next_button.set_sensitive(False)
     244        else:
     245            self.control.next_button.set_sensitive(True)
     246
     247    def songchange(self, direction):
     248        #if self.playflag:
     249        #    self.playflag = False
     250        #    return
     251        self.player.seek(0L)
     252        if direction == "prev" and self.currentplaying > 0:
     253            self.play(self.currentplaying - 1)
     254            logging.info("prev: " + self.playlist[self.currentplaying]['url'])
     255            #self.playflag = True
     256        elif direction == "next" and \
     257                self.currentplaying < len(self.playlist) - 1:
     258            self.play(self.currentplaying + 1)
     259            logging.info("next: " + self.playlist[self.currentplaying]['url'])
     260            #self.playflag = True
     261        else:
     262            self.play_toggled()
     263            self.player.stop()
     264            self._switch_canvas(show_video=False)
     265            self.player.set_uri(None)
     266            self.check_if_next_prev()
     267
     268    def play(self, media_index):
     269        self._switch_canvas(show_video=True)
     270        self.currentplaying = media_index
     271        url = self.playlist[self.currentplaying]['url']
     272        error = None
     273        if url.startswith('journal://'):
     274            try:
     275                jobject = datastore.get(url[len("journal://"):])
     276                url = 'file://' + jobject.file_path
     277            except:
     278                path = url[len("journal://"):]
     279                error = _('The file %s was not found') % path
     280
     281        self.check_if_next_prev()
     282
     283        if error is None:
     284            self.player.set_uri(url)
     285            self.player.play()
     286        else:
     287            self.control.set_disabled()
     288            self._show_error_alert(error)
     289
     290        self.playlist_widget.set_cursor(self.currentplaying)
     291
     292    def _player_eos_cb(self, widget):
     293        self.songchange('next')
     294
     295    def _show_error_alert(self, title, msg=None):
     296        self._alert = ErrorAlert()
     297        self._alert.props.title = title
     298        if msg is not None:
     299            self._alert.props.msg = msg
     300        self.add_alert(self._alert)
     301        self._alert.connect('response', self._alert_cancel_cb)
     302        self._alert.show()
     303
     304    def _mount_added_cb(self, volume_monitor, device):
     305        self.view_area.set_current_page(0)
     306        self.remove_alert(self._alert)
     307        self.playlist_widget.update(self.playlist)
     308
     309    def _mount_removed_cb(self, volume_monitor, device):
     310        self.view_area.set_current_page(0)
     311        self.remove_alert(self._alert)
     312        self.playlist_widget.update(self.playlist)
     313
     314    def _show_missing_tracks_alert(self, nro):
     315        self._alert = Alert()
     316        title = _('%s tracks not found.') % nro
     317        self._alert.props.title = title
     318        self._alert.add_button(Gtk.ResponseType.APPLY, _('Details'))
     319        self.add_alert(self._alert)
     320        self._alert.connect('response',
     321                self.__missing_tracks_alert_response_cb)
     322
     323    def __missing_tracks_alert_response_cb(self, alert, response_id):
     324        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
     325        vbox.props.valign = Gtk.Align.CENTER
     326        label = Gtk.Label(label='')
     327        label.set_markup(_('<b>Missing tracks</b>'))
     328        vbox.pack_start(label, False, False, 15)
     329
     330        for track in self.playlist_widget.get_missing_tracks():
     331            path = track['url'].replace('journal://', '')\
     332                .replace('file://', '')
     333            label = Gtk.Label(label=path)
     334            vbox.add(label)
     335
     336        _missing_tracks = Gtk.ScrolledWindow()
     337        _missing_tracks.add_with_viewport(vbox)
     338        _missing_tracks.show_all()
     339
     340        self.view_area.append_page(_missing_tracks, None)
     341
     342        self.view_area.set_current_page(2)
     343        self.remove_alert(alert)
     344
     345    def _alert_cancel_cb(self, alert, response_id):
     346        self.remove_alert(alert)
     347
     348    def _player_error_cb(self, widget, message, detail):
     349        self.player.stop()
     350        self.player.set_uri(None)
     351        self.control.set_disabled()
     352
     353        file_path = self.playlist[self.currentplaying]['url']\
     354            .replace('journal://', 'file://')
     355        mimetype = mime.get_for_file(file_path)
     356
     357        title = _('Error')
     358        msg = _('This "%s" file can\'t be played') % mimetype
     359        self._switch_canvas(False)
     360        self._show_error_alert(title, msg)
     361
     362    def _joined_cb(self, activity):
     363        logging.debug("someone joined")
     364        pass
     365
     366    def _shared_cb(self, activity):
     367        logging.debug("shared start")
     368        pass
     369
     370    def _show_picker_cb(self):
     371        #From ImageViewer Activity
     372        if not self._want_document:
     373            return
     374
     375        # README: some arguments are deprecated so I avoid them
     376
     377        # chooser = ObjectChooser(_('Choose document'), self,
     378        #     Gtk.DialogFlags.MODAL |
     379        #     Gtk.DialogFlags.DESTROY_WITH_PARENT,
     380        #     what_filter=mime.GENERIC_TYPE_AUDIO)
     381
     382        chooser = ObjectChooser(self, what_filter=mime.GENERIC_TYPE_AUDIO)
     383
     384        try:
     385            result = chooser.run()
     386            if result == Gtk.ResponseType.ACCEPT:
     387                jobject = chooser.get_selected_object()
     388                if jobject and jobject.file_path:
     389                    logging.error('Adding %s', jobject.file_path)
     390                    title = jobject.metadata.get('title', None)
     391                    self._load_file(jobject.file_path, title,
     392                            jobject.object_id)
     393        finally:
     394            #chooser.destroy()
     395            #del chooser
     396            pass
     397
     398    def read_file(self, file_path):
     399        """Load a file from the datastore on activity start."""
     400        logging.debug('JukeBoxAtivity.read_file: %s', file_path)
     401        title = self.metadata.get('title', None)
     402        self._load_file(file_path, title, self._object_id)
     403
     404    def _load_file(self, file_path, title, object_id):
     405        self.uri = os.path.abspath(file_path)
     406        if os.path.islink(self.uri):
     407            self.uri = os.path.realpath(self.uri)
     408        mimetype = mime.get_for_file('file://' + file_path)
     409        logging.error('read_file mime %s', mimetype)
     410        if mimetype == 'audio/x-mpegurl':
     411            # is a M3U playlist:
     412            for uri in self._read_m3u_playlist(file_path):
     413                if not self.playlist_widget.check_available_media(uri['url']):
     414                    self._not_found_files += 1
     415
     416                GObject.idle_add(self._start, uri['url'], uri['title'],
     417                        uri['object_id'])
     418        else:
     419            # is another media file:
     420            GObject.idle_add(self._start, self.uri, title, object_id)
     421
     422        if self._not_found_files > 0:
     423            self._show_missing_tracks_alert(self._not_found_files)
     424
     425    def _create_playlist_jobject(self):
     426        """Create an object in the Journal to store the playlist.
     427
     428        This is needed if the activity was not started from a playlist
     429        or from scratch.
     430
     431        """
     432        jobject = datastore.create()
     433        jobject.metadata['mime_type'] = "audio/x-mpegurl"
     434        jobject.metadata['title'] = _('Jukebox playlist')
     435
     436        temp_path = os.path.join(activity.get_activity_root(),
     437                                 'instance')
     438        if not os.path.exists(temp_path):
     439            os.makedirs(temp_path)
     440
     441        jobject.file_path = tempfile.mkstemp(dir=temp_path)[1]
     442        self._playlist_jobject = jobject
     443
     444    def write_file(self, file_path):
     445
     446        def write_playlist_to_file(file_path):
     447            """Open the file at file_path and write the playlist.
     448
     449            It is saved in audio/x-mpegurl format.
     450
     451            """
     452            list_file = open(file_path, 'w')
     453            for uri in self.playlist:
     454                list_file.write('#EXTINF: %s\n' % uri['title'])
     455                list_file.write('%s\n' % uri['url'])
     456            list_file.close()
     457
     458        if not self.metadata['mime_type']:
     459            self.metadata['mime_type'] = 'audio/x-mpegurl'
     460
     461        if self.metadata['mime_type'] == 'audio/x-mpegurl':
     462            write_playlist_to_file(file_path)
     463
     464        else:
     465            if self._playlist_jobject is None:
     466                self._create_playlist_jobject()
     467
     468            # Add the playlist to the playlist jobject description.
     469            # This is only done if the activity was not started from a
     470            # playlist or from scratch:
     471            description = ''
     472            for uri in self.playlist:
     473                description += '%s\n' % uri['title']
     474            self._playlist_jobject.metadata['description'] = description
     475
     476            write_playlist_to_file(self._playlist_jobject.file_path)
     477            datastore.write(self._playlist_jobject)
     478
     479    def _read_m3u_playlist(self, file_path):
     480        urls = []
     481        title = ''
     482        for line in open(file_path).readlines():
     483            line = line.strip()
     484            if line != '':
     485                if line.startswith('#EXTINF:'):
     486                    # line with data
     487                    #EXTINF: title
     488                    title = line[len('#EXTINF:'):]
     489                else:
     490                    uri = {}
     491                    uri['url'] = line.strip()
     492                    uri['title'] = title
     493                    if uri['url'].startswith('journal://'):
     494                        uri['object_id'] = uri['url'][len('journal://'):]
     495                    else:
     496                        uri['object_id'] = None
     497                    urls.append(uri)
     498                    title = ''
     499        return urls
     500
     501    def _start(self, uri=None, title=None, object_id=None):
     502        self._want_document = False
     503        self.playpath = os.path.dirname(uri)
     504        if not uri:
     505            return False
     506
     507        if title is not None:
     508            title = title.strip()
     509        if object_id is not None:
     510            self.playlist.append({'url': 'journal://' + object_id,
     511                    'title': title})
     512        else:
     513            if uri.startswith("file://"):
     514                self.playlist.append({'url': uri, 'title': title})
     515            else:
     516                uri = "file://" + urllib.quote(os.path.abspath(uri))
     517                self.playlist.append({'url': uri, 'title': title})
     518
     519        self.playlist_widget.update(self.playlist)
     520
     521        try:
     522            if self.currentplaying is None:
     523                logging.info("Playing: " + self.playlist[0]['url'])
     524                url = self.playlist[0]['url']
     525                if url.startswith('journal://'):
     526                    jobject = datastore.get(url[len("journal://"):])
     527                    url = 'file://' + jobject.file_path
     528
     529                self.player.set_uri(url)
     530                self.player.play()
     531                self.currentplaying = 0
     532                self.play_toggled()
     533                self.show_all()
     534            else:
     535                pass
     536                #self.player.seek(0L)
     537                #self.player.stop()
     538                #self.currentplaying += 1
     539                #self.player.set_uri(self.playlist[self.currentplaying])
     540                #self.play_toggled()
     541        except:
     542            pass
     543        self.check_if_next_prev()
     544        return False
     545
     546    def play_toggled(self):
     547        self.control.set_enabled()
     548
     549        if self.player.is_playing():
     550            self.player.pause()
     551            self.control.set_button_play()
     552        else:
     553            if self.player.error:
     554                self.control.set_disabled()
     555            else:
     556                self.player.play()
     557                if self.update_id == -1:
     558                    self.update_id = GObject.timeout_add(self.UPDATE_INTERVAL,
     559                                                         self.update_scale_cb)
     560                self.control.set_button_pause()
     561
     562    def volume_changed_cb(self, widget, value):
     563        if self.player:
     564            self.player.player.set_property('volume', value)
     565
     566    def scale_button_press_cb(self, widget, event):
     567        self.control.button.set_sensitive(False)
     568        self.was_playing = self.player.is_playing()
     569        if self.was_playing:
     570            self.player.pause()
     571
     572        # don't timeout-update position during seek
     573        if self.update_id != -1:
     574            GObject.source_remove(self.update_id)
     575            self.update_id = -1
     576
     577        # make sure we get changed notifies
     578        if self.changed_id == -1:
     579            self.changed_id = self.control.hscale.connect('value-changed',
     580                self.scale_value_changed_cb)
     581
     582    def scale_value_changed_cb(self, scale):
     583        # see seek.c:seek_cb
     584        real = long(scale.get_value() * self.p_duration / 100)  # in ns
     585        self.player.seek(real)
     586        # allow for a preroll
     587        self.player.get_state(timeout=50 * Gst.MSECOND)  # 50 ms
     588
     589    def scale_button_release_cb(self, widget, event):
     590        # see seek.cstop_seek
     591        widget.disconnect(self.changed_id)
     592        self.changed_id = -1
     593
     594        self.control.button.set_sensitive(True)
     595        if self.seek_timeout_id != -1:
     596            GObject.source_remove(self.seek_timeout_id)
     597            self.seek_timeout_id = -1
     598        else:
     599            if self.was_playing:
     600                self.player.play()
     601
     602        if self.update_id != -1:
     603            self.error('Had a previous update timeout id')
     604        else:
     605            self.update_id = GObject.timeout_add(self.UPDATE_INTERVAL,
     606                self.update_scale_cb)
     607
     608    def update_scale_cb(self):
     609        success, self.p_position, self.p_duration = \
     610            self.player.query_position()
     611
     612        if not success:
     613            return True
     614
     615        if self.p_position != Gst.CLOCK_TIME_NONE:
     616            value = self.p_position * 100.0 / self.p_duration
     617            self.control.adjustment.set_value(value)
     618
     619            # Update the current time
     620            seconds = self.p_position * 10 ** -9
     621            time = '%2d:%02d' % (int(seconds / 60), int(seconds % 60))
     622            self.control.current_time_label.set_text(time)
     623
     624        # FIXME: this should be updated just once when the file starts
     625        # the first time
     626        if self.p_duration != Gst.CLOCK_TIME_NONE:
     627            seconds = self.p_duration * 10 ** -9
     628            time = '%2d:%02d' % (int(seconds / 60), int(seconds % 60))
     629            self.control.total_time_label.set_text(time)
     630
     631        return True
     632
     633    def _erase_playlist_entry_clicked_cb(self, widget):
     634        self.playlist_widget.delete_selected_items()
     635
     636    def __go_fullscreen_cb(self, toolbar):
     637        self.fullscreen()
     638
     639    def __toggle_playlist_cb(self, toolbar):
     640        if self.playlist_widget.get_visible():
     641            self.playlist_widget.hide()
     642        else:
     643            self.playlist_widget.show_all()
     644        self.canvas.queue_draw()
     645
     646
     647class VideoWidget(Gtk.DrawingArea):
     648    def __init__(self):
     649        GObject.GObject.__init__(self)
     650        self.set_events(Gdk.EventMask.POINTER_MOTION_MASK |
     651                        Gdk.EventMask.POINTER_MOTION_HINT_MASK |
     652                        Gdk.EventMask.EXPOSURE_MASK |
     653                        Gdk.EventMask.KEY_PRESS_MASK |
     654                        Gdk.EventMask.KEY_RELEASE_MASK)
     655
     656        self.set_app_paintable(True)
     657        self.set_double_buffered(False)
     658
     659
     660if __name__ == '__main__':
     661    window = Gtk.Window()
     662    view = VideoWidget()
     663
     664    #player.connect("eos", self._player_eos_cb)
     665    #player.connect("error", self._player_error_cb)
     666    view.show()
     667    window.add(view)
     668
     669    def map_cb(widget):
     670        player = GstPlayer(view)
     671        player.set_uri(sys.argv[1])
     672        player.play()
     673
     674    window.connect('map', map_cb)
     675    window.maximize()
     676    window.show_all()
     677    window.connect("destroy", Gtk.main_quit)
     678    Gtk.main()
  • activity/activity.info

    diff --git a/activity/activity.info b/activity/activity.info
    index 76cfd1b..f66c422 100644
    a b name = Jukebox 
    33bundle_id = org.laptop.sugar.Jukebox
    44license = GPLv2+
    55icon = activity-jukebox
    6 exec = sugar-activity jukeboxactivity.JukeboxActivity
     6exec = sugar-activity activity.JukeboxActivity
    77show_launcher = yes
    88activity_version = 29
    99mime_types = video/x-theora;audio/x-vorbis;audio/x-flac;audio/x-speex;application/x-ogm-video;application/x-ogm-audio;video/x-mng;audio/x-aiff;audio/x-wav;audio/x-m4a;video/mpeg4;video/mpeg-stream;video/mpeg;application/ogg;video/mpegts;video/mpeg2;video/mpeg1;audio/mpeg;audio/x-ac3;video/x-cdxa;audio/x-au;audio/mpegurl;audio/x-mpegurl;audio/x-vorbis+ogg;audio/x-scpls;audio/ogg;video/ogg;audio/x-flac+ogg;audio/x-speex+ogg;video/x-theora+ogg;video/x-ogm+ogg;video/x-flv;video/mp4;video/x-matroska;video/x-msvideo;video/quicktime, video/x-quicktime, image/mov, audio/aiff, audio/x-midi, video/avi
  • new file controls.py

    diff --git a/controls.py b/controls.py
    new file mode 100644
    index 0000000..b84a55a
    - +  
     1# This program is free software; you can redistribute it and/or
     2# modify it under the terms of the GNU Lesser General Public
     3# License as published by the Free Software Foundation; either
     4# version 2.1 of the License, or (at your option) any later version.
     5#
     6# This library is distributed in the hope that it will be useful,
     7# but WITHOUT ANY WARRANTY; without even the implied warranty of
     8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     9# Lesser General Public License for more details.
     10#
     11# You should have received a copy of the GNU Lesser General Public
     12# License along with this library; if not, write to the Free Software
     13# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
     14# USA
     15
     16# Copyright (C) 2013 Manuel Kaufmann <humitos@gmail.com>
     17
     18import logging
     19
     20from gi.repository import Gtk
     21from gi.repository import GObject
     22
     23from gettext import gettext as _
     24
     25from sugar3.graphics.toolbutton import ToolButton
     26
     27
     28class Controls(GObject.GObject):
     29    """Class to create the Control (play, back, forward,
     30    add, remove, etc) toolbar"""
     31
     32    def __init__(self, toolbar, jukebox):
     33        GObject.GObject.__init__(self)
     34
     35        self.toolbar = toolbar
     36        self.jukebox = jukebox
     37
     38        self.open_button = ToolButton('list-add')
     39        self.open_button.set_tooltip(_('Add track'))
     40        self.open_button.show()
     41        self.open_button.connect('clicked', jukebox.open_button_clicked_cb)
     42        self.toolbar.insert(self.open_button, -1)
     43
     44        erase_playlist_entry_btn = ToolButton(icon_name='list-remove')
     45        erase_playlist_entry_btn.set_tooltip(_('Remove track'))
     46        erase_playlist_entry_btn.connect('clicked',
     47                 jukebox._erase_playlist_entry_clicked_cb)
     48        self.toolbar.insert(erase_playlist_entry_btn, -1)
     49
     50        spacer = Gtk.SeparatorToolItem()
     51        self.toolbar.insert(spacer, -1)
     52        spacer.show()
     53
     54        self.prev_button = ToolButton('player_rew')
     55        self.prev_button.set_tooltip(_('Previous'))
     56        self.prev_button.show()
     57        self.prev_button.connect('clicked', self.prev_button_clicked_cb)
     58        self.toolbar.insert(self.prev_button, -1)
     59
     60        self.pause_image = Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_PAUSE,
     61                                                    Gtk.IconSize.BUTTON)
     62        self.pause_image.show()
     63        self.play_image = Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_PLAY,
     64                                                   Gtk.IconSize.BUTTON)
     65        self.play_image.show()
     66
     67        self.button = Gtk.ToolButton()
     68        self.button.set_icon_widget(self.play_image)
     69        self.button.set_property('can-default', True)
     70        self.button.show()
     71        self.button.connect('clicked', self._button_clicked_cb)
     72
     73        self.toolbar.insert(self.button, -1)
     74
     75        self.next_button = ToolButton('player_fwd')
     76        self.next_button.set_tooltip(_('Next'))
     77        self.next_button.show()
     78        self.next_button.connect('clicked', self.next_button_clicked_cb)
     79        self.toolbar.insert(self.next_button, -1)
     80
     81        current_time = Gtk.ToolItem()
     82        self.current_time_label = Gtk.Label(label='')
     83        current_time.add(self.current_time_label)
     84        current_time.show()
     85        toolbar.insert(current_time, -1)
     86
     87        self.adjustment = Gtk.Adjustment(0.0, 0.00, 100.0, 0.1, 1.0, 1.0)
     88        self.hscale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL,
     89                                adjustment=self.adjustment)
     90        self.hscale.set_draw_value(False)
     91        # FIXME: this seems to be deprecated
     92        # self.hscale.set_update_policy(Gtk.UPDATE_CONTINUOUS)
     93        logging.debug("FIXME: AttributeError: 'Scale' object has no "
     94                      "attribute 'set_update_policy'")
     95        self.hscale.connect('button-press-event',
     96                jukebox.scale_button_press_cb)
     97        self.hscale.connect('button-release-event',
     98                jukebox.scale_button_release_cb)
     99
     100        self.scale_item = Gtk.ToolItem()
     101        self.scale_item.set_expand(True)
     102        self.scale_item.add(self.hscale)
     103        self.toolbar.insert(self.scale_item, -1)
     104
     105        total_time = Gtk.ToolItem()
     106        self.total_time_label = Gtk.Label(label='')
     107        total_time.add(self.total_time_label)
     108        total_time.show()
     109        toolbar.insert(total_time, -1)
     110
     111    def prev_button_clicked_cb(self, widget):
     112        self.jukebox.songchange('prev')
     113
     114    def next_button_clicked_cb(self, widget):
     115        self.jukebox.songchange('next')
     116
     117    def _button_clicked_cb(self, widget):
     118        self.jukebox.play_toggled()
     119
     120    def set_button_play(self):
     121        self.button.set_icon_widget(self.play_image)
     122
     123    def set_button_pause(self):
     124        self.button.set_icon_widget(self.pause_image)
     125
     126    def set_disabled(self):
     127        self.button.set_sensitive(False)
     128        self.scale_item.set_sensitive(False)
     129        self.hscale.set_sensitive(False)
     130
     131    def set_enabled(self):
     132        self.button.set_sensitive(True)
     133        self.scale_item.set_sensitive(True)
     134        self.hscale.set_sensitive(True)
  • deleted file jukeboxactivity.py

    diff --git a/jukeboxactivity.py b/jukeboxactivity.py
    deleted file mode 100644
    index 1bc4d4a..0000000
    + -  
    1 """
    2  jukeboxactivity.py
    3  Activity that plays media.
    4  Copyright (C) 2007 Andy Wingo <wingo@pobox.com>
    5  Copyright (C) 2007 Red Hat, Inc.
    6  Copyright (C) 2008-2010 Kushal Das <kushal@fedoraproject.org>
    7 """
    8 
    9 # This program is free software; you can redistribute it and/or
    10 # modify it under the terms of the GNU Lesser General Public
    11 # License as published by the Free Software Foundation; either
    12 # version 2.1 of the License, or (at your option) any later version.
    13 #
    14 # This library is distributed in the hope that it will be useful,
    15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
    16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    17 # Lesser General Public License for more details.
    18 #
    19 # You should have received a copy of the GNU Lesser General Public
    20 # License along with this library; if not, write to the Free Software
    21 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
    22 # USA
    23 
    24 import sys
    25 import logging
    26 import tempfile
    27 from gettext import gettext as _
    28 import os
    29 
    30 from sugar3.activity import activity
    31 from sugar3.graphics.objectchooser import ObjectChooser
    32 from sugar3 import mime
    33 from sugar3.datastore import datastore
    34 
    35 from sugar3.graphics.toolbarbox import ToolbarBox
    36 from sugar3.graphics.toolbarbox import ToolbarButton
    37 from sugar3.activity.widgets import StopButton
    38 from sugar3.activity.widgets import ActivityToolbarButton
    39 from sugar3.graphics.alert import ErrorAlert
    40 from sugar3.graphics.alert import Alert
    41 
    42 import gi
    43 gi.require_version('Gtk', '3.0')
    44 gi.require_version('Gst', '1.0')
    45 
    46 from gi.repository import GObject
    47 from gi.repository import Gdk
    48 from gi.repository import Gtk
    49 from gi.repository import Gst
    50 from gi.repository import Gio
    51 
    52 # Needed for window.get_xid(), xvimagesink.set_window_handle(),
    53 # respectively:
    54 from gi.repository import GdkX11, GstVideo
    55 
    56 # Avoid "Fatal Python error: GC object already tracked"
    57 # http://stackoverflow.com/questions/7496629/gstreamer-appsrc-causes-random-crashes
    58 GObject.threads_init()
    59 
    60 # Initialize GStreamer
    61 Gst.init(None)
    62 
    63 import urllib
    64 from ControlToolbar import Control, ViewToolbar
    65 from ConfigParser import ConfigParser
    66 cf = ConfigParser()
    67 
    68 from widgets import PlayListWidget
    69 
    70 PLAYLIST_WIDTH_PROP = 1.0 / 3
    71 
    72 
    73 class JukeboxActivity(activity.Activity):
    74     UPDATE_INTERVAL = 500
    75 
    76     def __init__(self, handle):
    77         activity.Activity.__init__(self, handle)
    78         self._object_id = handle.object_id
    79         self.set_title(_('Jukebox Activity'))
    80         self.player = None
    81         self.max_participants = 1
    82         self._playlist_jobject = None
    83 
    84         toolbar_box = ToolbarBox()
    85         activity_button = ActivityToolbarButton(self)
    86         activity_toolbar = activity_button.page
    87         toolbar_box.toolbar.insert(activity_button, 0)
    88         self.title_entry = activity_toolbar.title
    89 
    90         # FIXME: I don't know what is the mission of this line
    91         # activity_toolbar.stop.hide()
    92 
    93         self.volume_monitor = Gio.VolumeMonitor.get()
    94         self.volume_monitor.connect('mount-added', self._mount_added_cb)
    95         self.volume_monitor.connect('mount-removed', self._mount_removed_cb)
    96 
    97         _view_toolbar = ViewToolbar()
    98         _view_toolbar.connect('go-fullscreen',
    99                               self.__go_fullscreen_cb)
    100         _view_toolbar.connect('toggle-playlist',
    101                               self.__toggle_playlist_cb)
    102         view_toolbar_button = ToolbarButton(
    103             page=_view_toolbar,
    104             icon_name='toolbar-view')
    105         _view_toolbar.show()
    106         toolbar_box.toolbar.insert(view_toolbar_button, -1)
    107         view_toolbar_button.show()
    108 
    109         self.control = Control(toolbar_box.toolbar, self)
    110 
    111         toolbar_box.toolbar.insert(StopButton(self), -1)
    112 
    113         self.set_toolbar_box(toolbar_box)
    114         toolbar_box.show_all()
    115 
    116         self.connect("key_press_event", self._key_press_event_cb)
    117 
    118         # We want to be notified when the activity gets the focus or
    119         # loses it.  When it is not active, we don't need to keep
    120         # reproducing the video
    121         self.connect("notify::active", self._notify_active_cb)
    122 
    123         # FIXME: this is related with shared activity and it doesn't work
    124         # if handle.uri:
    125         #     pass
    126         # elif self._shared_activity:
    127         #     if self.get_shared():
    128         #         pass
    129         #     else:
    130         #         # Wait for a successful join before trying to get the document
    131         #         self.connect("joined", self._joined_cb)
    132 
    133         self.update_id = -1
    134         self.changed_id = -1
    135         self.seek_timeout_id = -1
    136         self.player = None
    137         self.uri = None
    138 
    139         # {'url': 'file://.../media.ogg', 'title': 'My song', object_id: '..'}
    140         self.playlist = []
    141 
    142         self.jobjectlist = []
    143         self.playpath = None
    144         self.currentplaying = None
    145         self.playflag = False
    146         self._not_found_files = 0
    147 
    148         # README: I changed this because I was getting an error when I
    149         # tried to modify self.bin with something different than
    150         # Gtk.Bin
    151 
    152         # self.bin = Gtk.HBox()
    153         # self.bin.show()
    154 
    155         self.canvas = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
    156         self._alert = None
    157 
    158         self.playlist_widget = PlayListWidget(self.play)
    159         self.playlist_widget.update(self.playlist)
    160         self.playlist_widget.show()
    161         self.canvas.pack_start(self.playlist_widget, False, True, 0)
    162         self._empty_widget = Gtk.Label(label="")
    163         self._empty_widget.show()
    164         self.videowidget = VideoWidget()
    165         self.set_canvas(self.canvas)
    166         self._init_view_area()
    167         self.show_all()
    168         self.canvas.connect('size-allocate', self.__size_allocate_cb)
    169 
    170         #From ImageViewer Activity
    171         self._want_document = True
    172         if self._object_id is None:
    173             self._show_object_picker = GObject.timeout_add(1000, \
    174             self._show_picker_cb)
    175 
    176         if handle.uri:
    177             self.uri = handle.uri
    178             GObject.idle_add(self._start, self.uri, handle.title)
    179 
    180         # Create the player just once
    181         logging.debug('Instantiating GstPlayer')
    182         self.player = GstPlayer(self.videowidget)
    183         self.player.connect("eos", self._player_eos_cb)
    184         self.player.connect("error", self._player_error_cb)
    185         self.p_position = Gst.CLOCK_TIME_NONE
    186         self.p_duration = Gst.CLOCK_TIME_NONE
    187 
    188     def _notify_active_cb(self, widget, event):
    189         """Sugar notify us that the activity is becoming active or inactive.
    190         When we are inactive, we stop the player if it is reproducing
    191         a video.
    192         """
    193         if self.player.player.props.uri is not None:
    194             if not self.player.is_playing() and self.props.active:
    195                 self.player.play()
    196             if self.player.is_playing() and not self.props.active:
    197                 self.player.pause()
    198 
    199     def _init_view_area(self):
    200         """
    201         Use a notebook with two pages, one empty an another
    202         with the videowidget
    203         """
    204         self.view_area = Gtk.Notebook()
    205         self.view_area.set_show_tabs(False)
    206         self.view_area.append_page(self._empty_widget, None)
    207         self.view_area.append_page(self.videowidget, None)
    208         self.canvas.pack_end(self.view_area, expand=True,
    209                              fill=True, padding=0)
    210 
    211     def _switch_canvas(self, show_video):
    212         """Show or hide the video visualization in the canvas.
    213 
    214         When hidden, the canvas is filled with an empty widget to
    215         ensure redrawing.
    216 
    217         """
    218         if show_video:
    219             self.view_area.set_current_page(1)
    220         else:
    221             self.view_area.set_current_page(0)
    222         self.canvas.queue_draw()
    223 
    224     def __size_allocate_cb(self, widget, allocation):
    225         canvas_size = self.canvas.get_allocation()
    226         playlist_width = int(canvas_size.width * PLAYLIST_WIDTH_PROP)
    227         self.playlist_widget.set_size_request(playlist_width, 0)
    228 
    229     def open_button_clicked_cb(self, widget):
    230         """ To open the dialog to select a new file"""
    231         #self.player.seek(0L)
    232         #self.player.stop()
    233         #self.playlist = []
    234         #self.playpath = None
    235         #self.currentplaying = None
    236         #self.playflag = False
    237         self._want_document = True
    238         self._show_object_picker = GObject.timeout_add(1, self._show_picker_cb)
    239 
    240     def _key_press_event_cb(self, widget, event):
    241         keyname = Gdk.keyval_name(event.keyval)
    242         logging.info("Keyname Press: %s, time: %s", keyname, event.time)
    243         if self.title_entry.has_focus():
    244             return False
    245 
    246         if keyname == "space":
    247             self.play_toggled()
    248             return True
    249 
    250     def check_if_next_prev(self):
    251         if self.currentplaying == 0:
    252             self.control.prev_button.set_sensitive(False)
    253         else:
    254             self.control.prev_button.set_sensitive(True)
    255         if self.currentplaying == len(self.playlist) - 1:
    256             self.control.next_button.set_sensitive(False)
    257         else:
    258             self.control.next_button.set_sensitive(True)
    259 
    260     def songchange(self, direction):
    261         #if self.playflag:
    262         #    self.playflag = False
    263         #    return
    264         self.player.seek(0L)
    265         if direction == "prev" and self.currentplaying > 0:
    266             self.play(self.currentplaying - 1)
    267             logging.info("prev: " + self.playlist[self.currentplaying]['url'])
    268             #self.playflag = True
    269         elif direction == "next" and \
    270                 self.currentplaying < len(self.playlist) - 1:
    271             self.play(self.currentplaying + 1)
    272             logging.info("next: " + self.playlist[self.currentplaying]['url'])
    273             #self.playflag = True
    274         else:
    275             self.play_toggled()
    276             self.player.stop()
    277             self._switch_canvas(show_video=False)
    278             self.player.set_uri(None)
    279             self.check_if_next_prev()
    280 
    281     def play(self, media_index):
    282         self._switch_canvas(show_video=True)
    283         self.currentplaying = media_index
    284         url = self.playlist[self.currentplaying]['url']
    285         error = None
    286         if url.startswith('journal://'):
    287             try:
    288                 jobject = datastore.get(url[len("journal://"):])
    289                 url = 'file://' + jobject.file_path
    290             except:
    291                 path = url[len("journal://"):]
    292                 error = _('The file %s was not found') % path
    293 
    294         self.check_if_next_prev()
    295 
    296         if error is None:
    297             self.player.set_uri(url)
    298             self.player.play()
    299         else:
    300             self.control.set_disabled()
    301             self._show_error_alert(error)
    302 
    303         self.playlist_widget.set_cursor(self.currentplaying)
    304 
    305     def _player_eos_cb(self, widget):
    306         self.songchange('next')
    307 
    308     def _show_error_alert(self, title, msg=None):
    309         self._alert = ErrorAlert()
    310         self._alert.props.title = title
    311         if msg is not None:
    312             self._alert.props.msg = msg
    313         self.add_alert(self._alert)
    314         self._alert.connect('response', self._alert_cancel_cb)
    315         self._alert.show()
    316 
    317     def _mount_added_cb(self, volume_monitor, device):
    318         self.view_area.set_current_page(0)
    319         self.remove_alert(self._alert)
    320         self.playlist_widget.update(self.playlist)
    321 
    322     def _mount_removed_cb(self, volume_monitor, device):
    323         self.view_area.set_current_page(0)
    324         self.remove_alert(self._alert)
    325         self.playlist_widget.update(self.playlist)
    326 
    327     def _show_missing_tracks_alert(self, nro):
    328         self._alert = Alert()
    329         title = _('%s tracks not found.') % nro
    330         self._alert.props.title = title
    331         self._alert.add_button(Gtk.ResponseType.APPLY, _('Details'))
    332         self.add_alert(self._alert)
    333         self._alert.connect('response',
    334                 self.__missing_tracks_alert_response_cb)
    335 
    336     def __missing_tracks_alert_response_cb(self, alert, response_id):
    337         vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    338         vbox.props.valign = Gtk.Align.CENTER
    339         label = Gtk.Label(label='')
    340         label.set_markup(_('<b>Missing tracks</b>'))
    341         vbox.pack_start(label, False, False, 15)
    342 
    343         for track in self.playlist_widget.get_missing_tracks():
    344             path = track['url'].replace('journal://', '')\
    345                 .replace('file://', '')
    346             label = Gtk.Label(label=path)
    347             vbox.add(label)
    348 
    349         _missing_tracks = Gtk.ScrolledWindow()
    350         _missing_tracks.add_with_viewport(vbox)
    351         _missing_tracks.show_all()
    352 
    353         self.view_area.append_page(_missing_tracks, None)
    354 
    355         self.view_area.set_current_page(2)
    356         self.remove_alert(alert)
    357 
    358     def _alert_cancel_cb(self, alert, response_id):
    359         self.remove_alert(alert)
    360 
    361     def _player_error_cb(self, widget, message, detail):
    362         self.player.stop()
    363         self.player.set_uri(None)
    364         self.control.set_disabled()
    365 
    366         file_path = self.playlist[self.currentplaying]['url']\
    367             .replace('journal://', 'file://')
    368         mimetype = mime.get_for_file(file_path)
    369 
    370         title = _('Error')
    371         msg = _('This "%s" file can\'t be played') % mimetype
    372         self._switch_canvas(False)
    373         self._show_error_alert(title, msg)
    374 
    375     def _joined_cb(self, activity):
    376         logging.debug("someone joined")
    377         pass
    378 
    379     def _shared_cb(self, activity):
    380         logging.debug("shared start")
    381         pass
    382 
    383     def _show_picker_cb(self):
    384         #From ImageViewer Activity
    385         if not self._want_document:
    386             return
    387 
    388         # README: some arguments are deprecated so I avoid them
    389 
    390         # chooser = ObjectChooser(_('Choose document'), self,
    391         #     Gtk.DialogFlags.MODAL |
    392         #     Gtk.DialogFlags.DESTROY_WITH_PARENT,
    393         #     what_filter=mime.GENERIC_TYPE_AUDIO)
    394 
    395         chooser = ObjectChooser(self, what_filter=mime.GENERIC_TYPE_AUDIO)
    396 
    397         try:
    398             result = chooser.run()
    399             if result == Gtk.ResponseType.ACCEPT:
    400                 jobject = chooser.get_selected_object()
    401                 if jobject and jobject.file_path:
    402                     logging.error('Adding %s', jobject.file_path)
    403                     title = jobject.metadata.get('title', None)
    404                     self._load_file(jobject.file_path, title,
    405                             jobject.object_id)
    406         finally:
    407             #chooser.destroy()
    408             #del chooser
    409             pass
    410 
    411     def read_file(self, file_path):
    412         """Load a file from the datastore on activity start."""
    413         logging.debug('JukeBoxAtivity.read_file: %s', file_path)
    414         title = self.metadata.get('title', None)
    415         self._load_file(file_path, title, self._object_id)
    416 
    417     def _load_file(self, file_path, title, object_id):
    418         self.uri = os.path.abspath(file_path)
    419         if os.path.islink(self.uri):
    420             self.uri = os.path.realpath(self.uri)
    421         mimetype = mime.get_for_file('file://' + file_path)
    422         logging.error('read_file mime %s', mimetype)
    423         if mimetype == 'audio/x-mpegurl':
    424             # is a M3U playlist:
    425             for uri in self._read_m3u_playlist(file_path):
    426                 if not self.playlist_widget.check_available_media(uri['url']):
    427                     self._not_found_files += 1
    428 
    429                 GObject.idle_add(self._start, uri['url'], uri['title'],
    430                         uri['object_id'])
    431         else:
    432             # is another media file:
    433             GObject.idle_add(self._start, self.uri, title, object_id)
    434 
    435         if self._not_found_files > 0:
    436             self._show_missing_tracks_alert(self._not_found_files)
    437 
    438     def _create_playlist_jobject(self):
    439         """Create an object in the Journal to store the playlist.
    440 
    441         This is needed if the activity was not started from a playlist
    442         or from scratch.
    443 
    444         """
    445         jobject = datastore.create()
    446         jobject.metadata['mime_type'] = "audio/x-mpegurl"
    447         jobject.metadata['title'] = _('Jukebox playlist')
    448 
    449         temp_path = os.path.join(activity.get_activity_root(),
    450                                  'instance')
    451         if not os.path.exists(temp_path):
    452             os.makedirs(temp_path)
    453 
    454         jobject.file_path = tempfile.mkstemp(dir=temp_path)[1]
    455         self._playlist_jobject = jobject
    456 
    457     def write_file(self, file_path):
    458 
    459         def write_playlist_to_file(file_path):
    460             """Open the file at file_path and write the playlist.
    461 
    462             It is saved in audio/x-mpegurl format.
    463 
    464             """
    465             list_file = open(file_path, 'w')
    466             for uri in self.playlist:
    467                 list_file.write('#EXTINF: %s\n' % uri['title'])
    468                 list_file.write('%s\n' % uri['url'])
    469             list_file.close()
    470 
    471         if not self.metadata['mime_type']:
    472             self.metadata['mime_type'] = 'audio/x-mpegurl'
    473 
    474         if self.metadata['mime_type'] == 'audio/x-mpegurl':
    475             write_playlist_to_file(file_path)
    476 
    477         else:
    478             if self._playlist_jobject is None:
    479                 self._create_playlist_jobject()
    480 
    481             # Add the playlist to the playlist jobject description.
    482             # This is only done if the activity was not started from a
    483             # playlist or from scratch:
    484             description = ''
    485             for uri in self.playlist:
    486                 description += '%s\n' % uri['title']
    487             self._playlist_jobject.metadata['description'] = description
    488 
    489             write_playlist_to_file(self._playlist_jobject.file_path)
    490             datastore.write(self._playlist_jobject)
    491 
    492     def _read_m3u_playlist(self, file_path):
    493         urls = []
    494         title = ''
    495         for line in open(file_path).readlines():
    496             line = line.strip()
    497             if line != '':
    498                 if line.startswith('#EXTINF:'):
    499                     # line with data
    500                     #EXTINF: title
    501                     title = line[len('#EXTINF:'):]
    502                 else:
    503                     uri = {}
    504                     uri['url'] = line.strip()
    505                     uri['title'] = title
    506                     if uri['url'].startswith('journal://'):
    507                         uri['object_id'] = uri['url'][len('journal://'):]
    508                     else:
    509                         uri['object_id'] = None
    510                     urls.append(uri)
    511                     title = ''
    512         return urls
    513 
    514     def _start(self, uri=None, title=None, object_id=None):
    515         self._want_document = False
    516         self.playpath = os.path.dirname(uri)
    517         if not uri:
    518             return False
    519 
    520         if title is not None:
    521             title = title.strip()
    522         if object_id is not None:
    523             self.playlist.append({'url': 'journal://' + object_id,
    524                     'title': title})
    525         else:
    526             if uri.startswith("file://"):
    527                 self.playlist.append({'url': uri, 'title': title})
    528             else:
    529                 uri = "file://" + urllib.quote(os.path.abspath(uri))
    530                 self.playlist.append({'url': uri, 'title': title})
    531 
    532         self.playlist_widget.update(self.playlist)
    533 
    534         try:
    535             if self.currentplaying is None:
    536                 logging.info("Playing: " + self.playlist[0]['url'])
    537                 url = self.playlist[0]['url']
    538                 if url.startswith('journal://'):
    539                     jobject = datastore.get(url[len("journal://"):])
    540                     url = 'file://' + jobject.file_path
    541 
    542                 self.player.set_uri(url)
    543                 self.player.play()
    544                 self.currentplaying = 0
    545                 self.play_toggled()
    546                 self.show_all()
    547             else:
    548                 pass
    549                 #self.player.seek(0L)
    550                 #self.player.stop()
    551                 #self.currentplaying += 1
    552                 #self.player.set_uri(self.playlist[self.currentplaying])
    553                 #self.play_toggled()
    554         except:
    555             pass
    556         self.check_if_next_prev()
    557         return False
    558 
    559     def play_toggled(self):
    560         self.control.set_enabled()
    561 
    562         if self.player.is_playing():
    563             self.player.pause()
    564             self.control.set_button_play()
    565         else:
    566             if self.player.error:
    567                 self.control.set_disabled()
    568             else:
    569                 self.player.play()
    570                 if self.update_id == -1:
    571                     self.update_id = GObject.timeout_add(self.UPDATE_INTERVAL,
    572                                                          self.update_scale_cb)
    573                 self.control.set_button_pause()
    574 
    575     def volume_changed_cb(self, widget, value):
    576         if self.player:
    577             self.player.player.set_property('volume', value)
    578 
    579     def scale_button_press_cb(self, widget, event):
    580         self.control.button.set_sensitive(False)
    581         self.was_playing = self.player.is_playing()
    582         if self.was_playing:
    583             self.player.pause()
    584 
    585         # don't timeout-update position during seek
    586         if self.update_id != -1:
    587             GObject.source_remove(self.update_id)
    588             self.update_id = -1
    589 
    590         # make sure we get changed notifies
    591         if self.changed_id == -1:
    592             self.changed_id = self.control.hscale.connect('value-changed',
    593                 self.scale_value_changed_cb)
    594 
    595     def scale_value_changed_cb(self, scale):
    596         # see seek.c:seek_cb
    597         real = long(scale.get_value() * self.p_duration / 100)  # in ns
    598         self.player.seek(real)
    599         # allow for a preroll
    600         self.player.get_state(timeout=50 * Gst.MSECOND)  # 50 ms
    601 
    602     def scale_button_release_cb(self, widget, event):
    603         # see seek.cstop_seek
    604         widget.disconnect(self.changed_id)
    605         self.changed_id = -1
    606 
    607         self.control.button.set_sensitive(True)
    608         if self.seek_timeout_id != -1:
    609             GObject.source_remove(self.seek_timeout_id)
    610             self.seek_timeout_id = -1
    611         else:
    612             if self.was_playing:
    613                 self.player.play()
    614 
    615         if self.update_id != -1:
    616             self.error('Had a previous update timeout id')
    617         else:
    618             self.update_id = GObject.timeout_add(self.UPDATE_INTERVAL,
    619                 self.update_scale_cb)
    620 
    621     def update_scale_cb(self):
    622         success, self.p_position, self.p_duration = \
    623             self.player.query_position()
    624 
    625         if not success:
    626             return True
    627 
    628         if self.p_position != Gst.CLOCK_TIME_NONE:
    629             value = self.p_position * 100.0 / self.p_duration
    630             self.control.adjustment.set_value(value)
    631 
    632             # Update the current time
    633             seconds = self.p_position * 10 ** -9
    634             time = '%2d:%02d' % (int(seconds / 60), int(seconds % 60))
    635             self.control.current_time_label.set_text(time)
    636 
    637         # FIXME: this should be updated just once when the file starts
    638         # the first time
    639         if self.p_duration != Gst.CLOCK_TIME_NONE:
    640             seconds = self.p_duration * 10 ** -9
    641             time = '%2d:%02d' % (int(seconds / 60), int(seconds % 60))
    642             self.control.total_time_label.set_text(time)
    643 
    644         return True
    645 
    646     def _erase_playlist_entry_clicked_cb(self, widget):
    647         self.playlist_widget.delete_selected_items()
    648 
    649     def __go_fullscreen_cb(self, toolbar):
    650         self.fullscreen()
    651 
    652     def __toggle_playlist_cb(self, toolbar):
    653         if self.playlist_widget.get_visible():
    654             self.playlist_widget.hide()
    655         else:
    656             self.playlist_widget.show_all()
    657         self.canvas.queue_draw()
    658 
    659 
    660 class GstPlayer(GObject.GObject):
    661 
    662     __gsignals__ = {
    663         'error': (GObject.SignalFlags.RUN_FIRST, None, [str, str]),
    664         'eos': (GObject.SignalFlags.RUN_FIRST, None, []),
    665     }
    666 
    667     def __init__(self, videowidget):
    668         GObject.GObject.__init__(self)
    669 
    670         self.playing = False
    671         self.error = False
    672 
    673         # Create GStreamer pipeline
    674         self.pipeline = Gst.Pipeline()
    675         # Create bus to get events from GStreamer pipeline
    676         self.bus = self.pipeline.get_bus()
    677         self.bus.add_signal_watch()
    678 
    679         self.bus.connect('message::eos', self.__on_eos_message)
    680         self.bus.connect('message::error', self.__on_error_message)
    681 
    682         # This is needed to make the video output in our DrawingArea
    683         self.bus.enable_sync_message_emission()
    684         self.bus.connect('sync-message::element', self.__on_sync_message)
    685 
    686         # Create GStreamer elements
    687         self.player = Gst.ElementFactory.make('playbin', None)
    688         self.pipeline.add(self.player)
    689 
    690         # Set the proper flags to render the vis-plugin
    691         GST_PLAY_FLAG_VIS = 1 << 3
    692         GST_PLAY_FLAG_TEXT = 1 << 2
    693         self.player.props.flags |= GST_PLAY_FLAG_VIS
    694         self.player.props.flags |= GST_PLAY_FLAG_TEXT
    695 
    696         r = Gst.Registry.get()
    697         l = [x for x in r.get_feature_list(Gst.ElementFactory)
    698              if (x.get_metadata('klass') == "Visualization")]
    699         if len(l):
    700             e = l.pop()  # take latest plugin in the list
    701             vis_plug = Gst.ElementFactory.make(e.get_name(), e.get_name())
    702             self.player.set_property('vis-plugin', vis_plug)
    703 
    704         self.overlay = None
    705         videowidget.realize()
    706         self.videowidget = videowidget
    707         self.videowidget_xid = videowidget.get_window().get_xid()
    708         self._init_video_sink()
    709 
    710     def __on_error_message(self, bus, msg):
    711         self.stop()
    712         self.playing = False
    713         self.error = True
    714         err, debug = msg.parse_error()
    715         self.emit('error', err, debug)
    716 
    717     def __on_eos_message(self, bus, msg):
    718         logging.debug('SIGNAL: eos')
    719         self.playing = False
    720         self.emit('eos')
    721 
    722     def __on_sync_message(self, bus, msg):
    723         if msg.get_structure().get_name() == 'prepare-window-handle':
    724             msg.src.set_window_handle(self.videowidget_xid)
    725 
    726     def set_uri(self, uri):
    727         self.pipeline.set_state(Gst.State.READY)
    728         logging.debug('### Setting URI: %s', uri)
    729         self.player.set_property('uri', uri)
    730 
    731     def _init_video_sink(self):
    732         self.bin = Gst.Bin()
    733         videoscale = Gst.ElementFactory.make('videoscale', 'videoscale')
    734         self.bin.add(videoscale)
    735         pad = videoscale.get_static_pad("sink")
    736         ghostpad = Gst.GhostPad.new("sink", pad)
    737         self.bin.add_pad(ghostpad)
    738         videoscale.set_property("method", 0)
    739 
    740         textoverlay = Gst.ElementFactory.make('textoverlay', 'textoverlay')
    741         self.overlay = textoverlay
    742         self.bin.add(textoverlay)
    743         conv = Gst.ElementFactory.make("videoconvert", "conv")
    744         self.bin.add(conv)
    745         videosink = Gst.ElementFactory.make('autovideosink', 'autovideosink')
    746         self.bin.add(videosink)
    747 
    748         videoscale.link(textoverlay)
    749         textoverlay.link(conv)
    750         conv.link(videosink)
    751 
    752         self.player.set_property("video-sink", self.bin)
    753 
    754     def set_overlay(self, title, artist, album):
    755         text = "%s\n%s" % (title, artist)
    756         if album and len(album):
    757             text += "\n%s" % album
    758         self.overlay.set_property("text", text)
    759         self.overlay.set_property("font-desc", "sans bold 14")
    760         self.overlay.set_property("halignment", "right")
    761         self.overlay.set_property("valignment", "bottom")
    762         try:
    763             # Only in OLPC versions of gstreamer-plugins-base for now
    764             self.overlay.set_property("line-align", "left")
    765         except:
    766             pass
    767 
    768     def query_position(self):
    769         "Returns a (position, duration) tuple"
    770 
    771         p_success, position = self.player.query_position(Gst.Format.TIME)
    772         d_success, duration = self.player.query_duration(Gst.Format.TIME)
    773 
    774         return (p_success and d_success, position, duration)
    775 
    776     def seek(self, location):
    777         """
    778         @param location: time to seek to, in nanoseconds
    779         """
    780 
    781         logging.debug('Seek: %s ns', location)
    782 
    783         self.pipeline.seek_simple(Gst.Format.TIME,
    784                                   Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
    785                                   location)
    786 
    787     def pause(self):
    788         logging.debug("pausing player")
    789         self.pipeline.set_state(Gst.State.PAUSED)
    790         self.playing = False
    791 
    792     def play(self):
    793         logging.debug("playing player")
    794         self.pipeline.set_state(Gst.State.PLAYING)
    795         self.playing = True
    796         self.error = False
    797 
    798     def stop(self):
    799         self.playing = False
    800         self.pipeline.set_state(Gst.State.NULL)
    801         logging.debug("stopped player")
    802 
    803     def get_state(self, timeout=1):
    804         return self.player.get_state(timeout=timeout)
    805 
    806     def is_playing(self):
    807         return self.playing
    808 
    809 
    810 class VideoWidget(Gtk.DrawingArea):
    811     def __init__(self):
    812         GObject.GObject.__init__(self)
    813         self.set_events(Gdk.EventMask.POINTER_MOTION_MASK |
    814                         Gdk.EventMask.POINTER_MOTION_HINT_MASK |
    815                         Gdk.EventMask.EXPOSURE_MASK |
    816                         Gdk.EventMask.KEY_PRESS_MASK |
    817                         Gdk.EventMask.KEY_RELEASE_MASK)
    818 
    819         self.set_app_paintable(True)
    820         self.set_double_buffered(False)
    821 
    822 
    823 if __name__ == '__main__':
    824     window = Gtk.Window()
    825     view = VideoWidget()
    826 
    827     #player.connect("eos", self._player_eos_cb)
    828     #player.connect("error", self._player_error_cb)
    829     view.show()
    830     window.add(view)
    831 
    832     def map_cb(widget):
    833         player = GstPlayer(view)
    834         player.set_uri(sys.argv[1])
    835         player.play()
    836 
    837     window.connect('map', map_cb)
    838     window.maximize()
    839     window.show_all()
    840     window.connect("destroy", Gtk.main_quit)
    841     Gtk.main()
  • new file player.py

    diff --git a/player.py b/player.py
    new file mode 100644
    index 0000000..b5b03da
    - +  
     1# This program is free software; you can redistribute it and/or
     2# modify it under the terms of the GNU Lesser General Public
     3# License as published by the Free Software Foundation; either
     4# version 2.1 of the License, or (at your option) any later version.
     5#
     6# This library is distributed in the hope that it will be useful,
     7# but WITHOUT ANY WARRANTY; without even the implied warranty of
     8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     9# Lesser General Public License for more details.
     10#
     11# You should have received a copy of the GNU Lesser General Public
     12# License along with this library; if not, write to the Free Software
     13# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
     14# USA
     15
     16# Copyright (C) 2013 Manuel Kaufmann <humitos@gmail.com>
     17
     18import logging
     19
     20from gi.repository import Gst
     21from gi.repository import GObject
     22
     23# Needed for window.get_xid(), xvimagesink.set_window_handle(),
     24# respectively:
     25from gi.repository import GdkX11, GstVideo
     26
     27# Avoid "Fatal Python error: GC object already tracked"
     28# http://stackoverflow.com/questions/7496629/gstreamer-appsrc-causes-random-crashes
     29# GObject.threads_init()
     30
     31# Initialize GStreamer
     32Gst.init(None)
     33
     34
     35class GstPlayer(GObject.GObject):
     36
     37    __gsignals__ = {
     38        'error': (GObject.SignalFlags.RUN_FIRST, None, [str, str]),
     39        'eos': (GObject.SignalFlags.RUN_FIRST, None, []),
     40    }
     41
     42    def __init__(self, videowidget):
     43        GObject.GObject.__init__(self)
     44
     45        self.playing = False
     46        self.error = False
     47
     48        # Create GStreamer pipeline
     49        self.pipeline = Gst.Pipeline()
     50        # Create bus to get events from GStreamer pipeline
     51        self.bus = self.pipeline.get_bus()
     52        self.bus.add_signal_watch()
     53
     54        self.bus.connect('message::eos', self.__on_eos_message)
     55        self.bus.connect('message::error', self.__on_error_message)
     56
     57        # This is needed to make the video output in our DrawingArea
     58        self.bus.enable_sync_message_emission()
     59        self.bus.connect('sync-message::element', self.__on_sync_message)
     60
     61        # Create GStreamer elements
     62        self.player = Gst.ElementFactory.make('playbin', None)
     63        self.pipeline.add(self.player)
     64
     65        # Set the proper flags to render the vis-plugin
     66        GST_PLAY_FLAG_VIS = 1 << 3
     67        GST_PLAY_FLAG_TEXT = 1 << 2
     68        self.player.props.flags |= GST_PLAY_FLAG_VIS
     69        self.player.props.flags |= GST_PLAY_FLAG_TEXT
     70
     71        r = Gst.Registry.get()
     72        l = [x for x in r.get_feature_list(Gst.ElementFactory)
     73             if (x.get_metadata('klass') == "Visualization")]
     74        if len(l):
     75            e = l.pop()  # take latest plugin in the list
     76            vis_plug = Gst.ElementFactory.make(e.get_name(), e.get_name())
     77            self.player.set_property('vis-plugin', vis_plug)
     78
     79        self.overlay = None
     80        videowidget.realize()
     81        self.videowidget = videowidget
     82        self.videowidget_xid = videowidget.get_window().get_xid()
     83        self._init_video_sink()
     84
     85    def __on_error_message(self, bus, msg):
     86        self.stop()
     87        self.playing = False
     88        self.error = True
     89        err, debug = msg.parse_error()
     90        self.emit('error', err, debug)
     91
     92    def __on_eos_message(self, bus, msg):
     93        logging.debug('SIGNAL: eos')
     94        self.playing = False
     95        self.emit('eos')
     96
     97    def __on_sync_message(self, bus, msg):
     98        if msg.get_structure().get_name() == 'prepare-window-handle':
     99            msg.src.set_window_handle(self.videowidget_xid)
     100
     101    def set_uri(self, uri):
     102        self.pipeline.set_state(Gst.State.READY)
     103        logging.debug('### Setting URI: %s', uri)
     104        self.player.set_property('uri', uri)
     105
     106    def _init_video_sink(self):
     107        self.bin = Gst.Bin()
     108        videoscale = Gst.ElementFactory.make('videoscale', 'videoscale')
     109        self.bin.add(videoscale)
     110        pad = videoscale.get_static_pad("sink")
     111        ghostpad = Gst.GhostPad.new("sink", pad)
     112        self.bin.add_pad(ghostpad)
     113        videoscale.set_property("method", 0)
     114
     115        textoverlay = Gst.ElementFactory.make('textoverlay', 'textoverlay')
     116        self.overlay = textoverlay
     117        self.bin.add(textoverlay)
     118        conv = Gst.ElementFactory.make("videoconvert", "conv")
     119        self.bin.add(conv)
     120        videosink = Gst.ElementFactory.make('autovideosink', 'autovideosink')
     121        self.bin.add(videosink)
     122
     123        videoscale.link(textoverlay)
     124        textoverlay.link(conv)
     125        conv.link(videosink)
     126
     127        self.player.set_property("video-sink", self.bin)
     128
     129    def set_overlay(self, title, artist, album):
     130        text = "%s\n%s" % (title, artist)
     131        if album and len(album):
     132            text += "\n%s" % album
     133        self.overlay.set_property("text", text)
     134        self.overlay.set_property("font-desc", "sans bold 14")
     135        self.overlay.set_property("halignment", "right")
     136        self.overlay.set_property("valignment", "bottom")
     137        try:
     138            # Only in OLPC versions of gstreamer-plugins-base for now
     139            self.overlay.set_property("line-align", "left")
     140        except:
     141            pass
     142
     143    def query_position(self):
     144        "Returns a (position, duration) tuple"
     145
     146        p_success, position = self.player.query_position(Gst.Format.TIME)
     147        d_success, duration = self.player.query_duration(Gst.Format.TIME)
     148
     149        return (p_success and d_success, position, duration)
     150
     151    def seek(self, location):
     152        """
     153        @param location: time to seek to, in nanoseconds
     154        """
     155
     156        logging.debug('Seek: %s ns', location)
     157
     158        self.pipeline.seek_simple(Gst.Format.TIME,
     159                                  Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
     160                                  location)
     161
     162    def pause(self):
     163        logging.debug("pausing player")
     164        self.pipeline.set_state(Gst.State.PAUSED)
     165        self.playing = False
     166
     167    def play(self):
     168        logging.debug("playing player")
     169        self.pipeline.set_state(Gst.State.PLAYING)
     170        self.playing = True
     171        self.error = False
     172
     173    def stop(self):
     174        self.playing = False
     175        self.pipeline.set_state(Gst.State.NULL)
     176        logging.debug("stopped player")
     177
     178    def get_state(self, timeout=1):
     179        return self.player.get_state(timeout=timeout)
     180
     181    def is_playing(self):
     182        return self.playing
  • new file playlist.py

    diff --git a/playlist.py b/playlist.py
    new file mode 100644
    index 0000000..4563d47
    - +  
     1# This program is free software; you can redistribute it and/or
     2# modify it under the terms of the GNU Lesser General Public
     3# License as published by the Free Software Foundation; either
     4# version 2.1 of the License, or (at your option) any later version.
     5#
     6# This library is distributed in the hope that it will be useful,
     7# but WITHOUT ANY WARRANTY; without even the implied warranty of
     8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     9# Lesser General Public License for more details.
     10#
     11# You should have received a copy of the GNU Lesser General Public
     12# License along with this library; if not, write to the Free Software
     13# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
     14# USA
     15
     16import logging
     17import os
     18from gettext import gettext as _
     19
     20import gi
     21gi.require_version('Gtk', '3.0')
     22
     23from gi.repository import GObject
     24from gi.repository import Gtk
     25from gi.repository import Pango
     26
     27from sugar3.graphics.icon import CellRendererIcon
     28
     29
     30COLUMNS_NAME = ('index', 'media', 'available')
     31COLUMNS = dict((name, i) for i, name in enumerate(COLUMNS_NAME))
     32
     33
     34class PlayList(Gtk.ScrolledWindow):
     35    def __init__(self, play_callback):
     36        self._playlist = None
     37        self._play_callback = play_callback
     38
     39        GObject.GObject.__init__(self, hadjustment=None,
     40                                    vadjustment=None)
     41        self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
     42        self.listview = Gtk.TreeView()
     43        self.treemodel = Gtk.ListStore(int, object, bool)
     44        self.listview.set_model(self.treemodel)
     45        selection = self.listview.get_selection()
     46        selection.set_mode(Gtk.SelectionMode.SINGLE)
     47
     48        renderer_icon = CellRendererIcon(self.listview)
     49        renderer_icon.props.icon_name = 'emblem-notification'
     50        renderer_icon.props.width = 20
     51        renderer_icon.props.height = 20
     52        renderer_icon.props.size = 20
     53        treecol_icon = Gtk.TreeViewColumn()
     54        treecol_icon.pack_start(renderer_icon, False)
     55        treecol_icon.set_cell_data_func(renderer_icon, self._set_icon)
     56        self.listview.append_column(treecol_icon)
     57
     58        renderer_idx = Gtk.CellRendererText()
     59        treecol_idx = Gtk.TreeViewColumn(_('No.'))
     60        treecol_idx.pack_start(renderer_idx, True)
     61        treecol_idx.set_cell_data_func(renderer_idx, self._set_number)
     62        self.listview.append_column(treecol_idx)
     63
     64        renderer_title = Gtk.CellRendererText()
     65        renderer_title.set_property('ellipsize', Pango.EllipsizeMode.END)
     66        treecol_title = Gtk.TreeViewColumn(_('Play List'))
     67        treecol_title.pack_start(renderer_title, True)
     68        treecol_title.set_cell_data_func(renderer_title, self._set_title)
     69        self.listview.append_column(treecol_title)
     70
     71        # we don't support search in the playlist for the moment:
     72        self.listview.set_enable_search(False)
     73
     74        self.listview.connect('row-activated', self.__on_row_activated)
     75
     76        self.add(self.listview)
     77
     78    def __on_row_activated(self, treeview, path, col):
     79        model = treeview.get_model()
     80
     81        treeiter = model.get_iter(path)
     82        media_idx = model.get_value(treeiter, COLUMNS['index'])
     83        self._play_callback(media_idx)
     84
     85    def _set_number(self, column, cell, model, it, data):
     86        idx = model.get_value(it, COLUMNS['index'])
     87        cell.set_property('text', idx + 1)
     88
     89    def _set_title(self, column, cell, model, it, data):
     90        playlist_item = model.get_value(it, COLUMNS['media'])
     91        available = model.get_value(it, COLUMNS['available'])
     92
     93        cell.set_property('text', playlist_item['title'])
     94        sensitive = True
     95        if not available:
     96            sensitive = False
     97        cell.set_property('sensitive', sensitive)
     98
     99    def _set_icon(self, column, cell, model, it, data):
     100        available = model.get_value(it, COLUMNS['available'])
     101        cell.set_property('visible', not available)
     102
     103    def update(self, playlist):
     104        self.treemodel.clear()
     105        self._playlist = playlist
     106        pl = list(enumerate(playlist))
     107        for i, media in pl:
     108            available = self.check_available_media(media['url'])
     109            media['available'] = available
     110            self.treemodel.append((i, media, available))
     111        #self.set_cursor(0)
     112
     113    def set_cursor(self, index):
     114        self.listview.set_cursor((index,))
     115
     116    def delete_selected_items(self):
     117        selection = self.listview.get_selection()
     118        sel_model, sel_rows = self.listview.get_selection().get_selected_rows()
     119        for row in sel_rows:
     120            index = sel_model.get_value(sel_model.get_iter(row), 0)
     121            self._playlist.pop(index)
     122            self.treemodel.remove(self.treemodel.get_iter(row))
     123        self.update(self._playlist)
     124
     125    def check_available_media(self, uri):
     126        path = uri.replace('journal://', '').replace('file://', '')
     127        if os.path.exists(path):
     128            return True
     129        else:
     130            return False
     131
     132    def get_missing_tracks(self):
     133        missing_tracks = []
     134        for track in self._playlist:
     135            if not track['available']:
     136                missing_tracks.append(track)
     137        return missing_tracks
  • new file viewtoolbar.py

    diff --git a/viewtoolbar.py b/viewtoolbar.py
    new file mode 100644
    index 0000000..f2d99d2
    - +  
     1# This program is free software; you can redistribute it and/or modify
     2# it under the terms of the GNU General Public License as published by
     3# the Free Software Foundation; either version 2 of the License, or
     4# (at your option) any later version.
     5#
     6# This program is distributed in the hope that it will be useful,
     7# but WITHOUT ANY WARRANTY; without even the implied warranty of
     8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     9# GNU General Public License for more details.
     10#
     11# You should have received a copy of the GNU General Public License
     12# along with this program; if not, write to the Free Software
     13# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
     14
     15# Copyright (C) 2007 Andy Wingo <wingo@pobox.com>
     16# Copyright (C) 2007 Red Hat, Inc.
     17# Copyright (C) 2008 Kushal Das <kushal@fedoraproject.org>
     18# Copyright (C) 2013 Manuel Kaufmann <humitos@gmail.com>
     19
     20import logging
     21
     22from gettext import gettext as _
     23
     24from gi.repository import GObject
     25from gi.repository import Gtk
     26
     27from sugar3.graphics.toolbutton import ToolButton
     28from sugar3.graphics.toggletoolbutton import ToggleToolButton
     29
     30
     31class ViewToolbar(Gtk.Toolbar):
     32    __gtype_name__ = 'ViewToolbar'
     33
     34    __gsignals__ = {
     35        'go-fullscreen': (GObject.SignalFlags.RUN_FIRST,
     36                          None,
     37                         ([])),
     38        'toggle-playlist': (GObject.SignalFlags.RUN_FIRST,
     39                            None,
     40                            ([]))
     41    }
     42
     43    def __init__(self):
     44        GObject.GObject.__init__(self)
     45
     46        self._show_playlist = ToggleToolButton('view-list')
     47        self._show_playlist.set_active(True)
     48        self._show_playlist.set_tooltip(_('Show Playlist'))
     49        self._show_playlist.connect('toggled', self._playlist_toggled_cb)
     50        self.insert(self._show_playlist, -1)
     51        self._show_playlist.show()
     52
     53        self._fullscreen = ToolButton('view-fullscreen')
     54        self._fullscreen.set_tooltip(_('Fullscreen'))
     55        self._fullscreen.connect('clicked', self._fullscreen_cb)
     56        self.insert(self._fullscreen, -1)
     57        self._fullscreen.show()
     58
     59    def _fullscreen_cb(self, button):
     60        self.emit('go-fullscreen')
     61
     62    def _playlist_toggled_cb(self, button):
     63        self.emit('toggle-playlist')
     64
     65
     66class Control(GObject.GObject):
     67    """Class to create the Control (play) toolbar"""
     68
     69    def __init__(self, toolbar, jukebox):
     70        GObject.GObject.__init__(self)
     71
     72        self.toolbar = toolbar
     73        self.jukebox = jukebox
     74
     75        self.open_button = ToolButton('list-add')
     76        self.open_button.set_tooltip(_('Add track'))
     77        self.open_button.show()
     78        self.open_button.connect('clicked', jukebox.open_button_clicked_cb)
     79        self.toolbar.insert(self.open_button, -1)
     80
     81        erase_playlist_entry_btn = ToolButton(icon_name='list-remove')
     82        erase_playlist_entry_btn.set_tooltip(_('Remove track'))
     83        erase_playlist_entry_btn.connect('clicked',
     84                 jukebox._erase_playlist_entry_clicked_cb)
     85        self.toolbar.insert(erase_playlist_entry_btn, -1)
     86
     87        spacer = Gtk.SeparatorToolItem()
     88        self.toolbar.insert(spacer, -1)
     89        spacer.show()
     90
     91        self.prev_button = ToolButton('player_rew')
     92        self.prev_button.set_tooltip(_('Previous'))
     93        self.prev_button.show()
     94        self.prev_button.connect('clicked', self.prev_button_clicked_cb)
     95        self.toolbar.insert(self.prev_button, -1)
     96
     97        self.pause_image = Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_PAUSE,
     98                                                    Gtk.IconSize.BUTTON)
     99        self.pause_image.show()
     100        self.play_image = Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_PLAY,
     101                                                   Gtk.IconSize.BUTTON)
     102        self.play_image.show()
     103
     104        self.button = Gtk.ToolButton()
     105        self.button.set_icon_widget(self.play_image)
     106        self.button.set_property('can-default', True)
     107        self.button.show()
     108        self.button.connect('clicked', self._button_clicked_cb)
     109
     110        self.toolbar.insert(self.button, -1)
     111
     112        self.next_button = ToolButton('player_fwd')
     113        self.next_button.set_tooltip(_('Next'))
     114        self.next_button.show()
     115        self.next_button.connect('clicked', self.next_button_clicked_cb)
     116        self.toolbar.insert(self.next_button, -1)
     117
     118        current_time = Gtk.ToolItem()
     119        self.current_time_label = Gtk.Label(label='')
     120        current_time.add(self.current_time_label)
     121        current_time.show()
     122        toolbar.insert(current_time, -1)
     123
     124        self.adjustment = Gtk.Adjustment(0.0, 0.00, 100.0, 0.1, 1.0, 1.0)
     125        self.hscale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL,
     126                                adjustment=self.adjustment)
     127        self.hscale.set_draw_value(False)
     128        # FIXME: this seems to be deprecated
     129        # self.hscale.set_update_policy(Gtk.UPDATE_CONTINUOUS)
     130        logging.debug("FIXME: AttributeError: 'Scale' object has no "
     131                      "attribute 'set_update_policy'")
     132        self.hscale.connect('button-press-event',
     133                jukebox.scale_button_press_cb)
     134        self.hscale.connect('button-release-event',
     135                jukebox.scale_button_release_cb)
     136
     137        self.scale_item = Gtk.ToolItem()
     138        self.scale_item.set_expand(True)
     139        self.scale_item.add(self.hscale)
     140        self.toolbar.insert(self.scale_item, -1)
     141
     142        total_time = Gtk.ToolItem()
     143        self.total_time_label = Gtk.Label(label='')
     144        total_time.add(self.total_time_label)
     145        total_time.show()
     146        toolbar.insert(total_time, -1)
     147
     148    def prev_button_clicked_cb(self, widget):
     149        self.jukebox.songchange('prev')
     150
     151    def next_button_clicked_cb(self, widget):
     152        self.jukebox.songchange('next')
     153
     154    def _button_clicked_cb(self, widget):
     155        self.jukebox.play_toggled()
     156
     157    def set_button_play(self):
     158        self.button.set_icon_widget(self.play_image)
     159
     160    def set_button_pause(self):
     161        self.button.set_icon_widget(self.pause_image)
     162
     163    def set_disabled(self):
     164        self.button.set_sensitive(False)
     165        self.scale_item.set_sensitive(False)
     166        self.hscale.set_sensitive(False)
     167
     168    def set_enabled(self):
     169        self.button.set_sensitive(True)
     170        self.scale_item.set_sensitive(True)
     171        self.hscale.set_sensitive(True)
  • deleted file widgets.py

    diff --git a/widgets.py b/widgets.py
    deleted file mode 100644
    index fbbd9c5..0000000
    + -  
    1 # This program is free software; you can redistribute it and/or
    2 # modify it under the terms of the GNU Lesser General Public
    3 # License as published by the Free Software Foundation; either
    4 # version 2.1 of the License, or (at your option) any later version.
    5 #
    6 # This library is distributed in the hope that it will be useful,
    7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
    8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    9 # Lesser General Public License for more details.
    10 #
    11 # You should have received a copy of the GNU Lesser General Public
    12 # License along with this library; if not, write to the Free Software
    13 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
    14 # USA
    15 
    16 import logging
    17 import os
    18 from gettext import gettext as _
    19 
    20 import gi
    21 gi.require_version('Gtk', '3.0')
    22 
    23 from gi.repository import GObject
    24 from gi.repository import Gtk
    25 from gi.repository import Pango
    26 
    27 from sugar3.graphics.icon import CellRendererIcon
    28 
    29 
    30 COLUMNS_NAME = ('index', 'media', 'available')
    31 COLUMNS = dict((name, i) for i, name in enumerate(COLUMNS_NAME))
    32 
    33 
    34 class PlayListWidget(Gtk.ScrolledWindow):
    35     def __init__(self, play_callback):
    36         self._playlist = None
    37         self._play_callback = play_callback
    38 
    39         GObject.GObject.__init__(self, hadjustment=None,
    40                                     vadjustment=None)
    41         self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
    42         self.listview = Gtk.TreeView()
    43         self.treemodel = Gtk.ListStore(int, object, bool)
    44         self.listview.set_model(self.treemodel)
    45         selection = self.listview.get_selection()
    46         selection.set_mode(Gtk.SelectionMode.SINGLE)
    47 
    48         renderer_icon = CellRendererIcon(self.listview)
    49         renderer_icon.props.icon_name = 'emblem-notification'
    50         renderer_icon.props.width = 20
    51         renderer_icon.props.height = 20
    52         renderer_icon.props.size = 20
    53         treecol_icon = Gtk.TreeViewColumn()
    54         treecol_icon.pack_start(renderer_icon, False)
    55         treecol_icon.set_cell_data_func(renderer_icon, self._set_icon)
    56         self.listview.append_column(treecol_icon)
    57 
    58         renderer_idx = Gtk.CellRendererText()
    59         treecol_idx = Gtk.TreeViewColumn(_('No.'))
    60         treecol_idx.pack_start(renderer_idx, True)
    61         treecol_idx.set_cell_data_func(renderer_idx, self._set_number)
    62         self.listview.append_column(treecol_idx)
    63 
    64         renderer_title = Gtk.CellRendererText()
    65         renderer_title.set_property('ellipsize', Pango.EllipsizeMode.END)
    66         treecol_title = Gtk.TreeViewColumn(_('Play List'))
    67         treecol_title.pack_start(renderer_title, True)
    68         treecol_title.set_cell_data_func(renderer_title, self._set_title)
    69         self.listview.append_column(treecol_title)
    70 
    71         # we don't support search in the playlist for the moment:
    72         self.listview.set_enable_search(False)
    73 
    74         self.listview.connect('row-activated', self.__on_row_activated)
    75 
    76         self.add(self.listview)
    77 
    78     def __on_row_activated(self, treeview, path, col):
    79         model = treeview.get_model()
    80 
    81         treeiter = model.get_iter(path)
    82         media_idx = model.get_value(treeiter, COLUMNS['index'])
    83         self._play_callback(media_idx)
    84 
    85     def _set_number(self, column, cell, model, it, data):
    86         idx = model.get_value(it, COLUMNS['index'])
    87         cell.set_property('text', idx + 1)
    88 
    89     def _set_title(self, column, cell, model, it, data):
    90         playlist_item = model.get_value(it, COLUMNS['media'])
    91         available = model.get_value(it, COLUMNS['available'])
    92 
    93         cell.set_property('text', playlist_item['title'])
    94         sensitive = True
    95         if not available:
    96             sensitive = False
    97         cell.set_property('sensitive', sensitive)
    98 
    99     def _set_icon(self, column, cell, model, it, data):
    100         available = model.get_value(it, COLUMNS['available'])
    101         cell.set_property('visible', not available)
    102 
    103     def update(self, playlist):
    104         self.treemodel.clear()
    105         self._playlist = playlist
    106         pl = list(enumerate(playlist))
    107         for i, media in pl:
    108             available = self.check_available_media(media['url'])
    109             media['available'] = available
    110             self.treemodel.append((i, media, available))
    111         #self.set_cursor(0)
    112 
    113     def set_cursor(self, index):
    114         self.listview.set_cursor((index,))
    115 
    116     def delete_selected_items(self):
    117         selection = self.listview.get_selection()
    118         sel_model, sel_rows = self.listview.get_selection().get_selected_rows()
    119         for row in sel_rows:
    120             index = sel_model.get_value(sel_model.get_iter(row), 0)
    121             self._playlist.pop(index)
    122             self.treemodel.remove(self.treemodel.get_iter(row))
    123         self.update(self._playlist)
    124 
    125     def check_available_media(self, uri):
    126         path = uri.replace('journal://', '').replace('file://', '')
    127         if os.path.exists(path):
    128             return True
    129         else:
    130             return False
    131 
    132     def get_missing_tracks(self):
    133         missing_tracks = []
    134         for track in self._playlist:
    135             if not track['available']:
    136                 missing_tracks.append(track)
    137         return missing_tracks