#!/usr/bin/python
# -*- coding: utf-8 -*-
### BEGIN LICENSE
# Copyright (C) 2010 Rick Spencer <rick.spencer@canonical.com>
# This program is free software: you can redistribute it and/or modify it 
# under the terms of the GNU General Public License version 3, as published 
# by the Free Software Foundation.
# 
# This program is distributed in the hope that it will be useful, but 
# WITHOUT ANY WARRANTY; without even the implied warranties of 
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 
# PURPOSE.  See the GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License along 
# with this program.  If not, see <http://www.gnu.org/licenses/>.
### END LICENSE

"""A VBox that tries to play the media file as defined by it's URU property.
It works for video and sound files.

Using
#create the a MediaPlayerBox and set the URI, and start it playing
player = MediaPlayerBox()
player.uri = "file:///home/user/Videos/a_video.avi"
player.play()

#You can seek to a specific place in the media file
#This will jump to the 60th second in the file
player.position = 60

#you can get the position and duration, for example for display
position_string = str(player.position)
duration_string = str(player.duration)
label.set_text( position_string + "/" + duration_string )

#you can pause and stop the media as well
player.pause()
player.stop()

Configuring
#You can choose to create the MediaPlayerBox with or without
#visible controls. The controls are not visible by default.
#Set show_toolbar to True to create the controls with visible controls
player = MediaPlayerBox(True)

#You can change the visibile of the controls later by setting the 
#controls_visible property
player.controls_visible = False

#You can add Widgets to the MediaPlayerBox simply by packing them in
player.pack_start(my_widget, False, False)

#You can get a reference to the controls, which are a Gtk.Toolbar
mybutton = Gtk.ToolButton()
player.controls.insert(mybutton, 0)

#You can access the playbutton, slider, or time label directly as well
player.play_button.hide()#a Gtk.ToggleToolButton
player.slider.hide()#a Gtk.HScale
player.time_label.hide()#a Gtk.Label

#If you want access to all the gstreamer knobs and dials, you can just
#get a reference to the playbin (see gstreamer documentation for details.
player.playbin.set_property(property_name, value)

#you can send the playbin signals in this way, as well
player.playbin.emit(signal_name)

Extending
A WebCamBox is Gtk.VBox
A WebCamBox is a Gtk.VBox that contains a Gtk.DrawingArea for displaying
video output, and a thin wrapper around a playbin, which is a gstreamer
pipleine sublcass that provides all the media playing functionality.

To add GUI elements simple, create them and pack them into MediaPlayerBox, since
it's just a Gtk.VBox

Similarly, to add to or change the media player functionality, modify properties on
the playbin. You may also want to overide _on_message and/or _on_sync_message
to catch messages from the bus and add behavior.

"""


import sys
import os
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkX11
from gi.repository import GObject
from gi.repository import Gst
import datetime

import gettext
from gettext import gettext as _
gettext.textdomain('quickly-widgets')

class MediaPlayerBox(Gtk.VBox):
    """MediaPlayerBox - A VBox that tries to play the media file as defined by it's URU property.
       It works for video and sound files.

    """

    def __init__(self, show_controls = False):
        """Creates a MediaPlayerBox, Note that this does not start media.
        For that, set the uri property and then call play().
        
        This function has no argumentsf

        """
        Gtk.VBox.__init__(self, False, 5)
        self.video_window = Gtk.DrawingArea()
        self.video_window.connect("realize",self.__on_video_window_realized)
#        self.pack_start(self.video_window, True, True, 0)
        self.video_window.show()
        self.connect("destroy", self.on_destroy)

        self.playbin = Gst.ElementFactory.make("playbin2", "player")
        bus = self.playbin.get_bus()
        bus.add_signal_watch()
        bus.enable_sync_message_emission()
        bus.connect("message", self._on_message)
        bus.connect("sync-message::element", self._on_sync_message)
        self.__uri = ""
        self.realized = False

        self.controls = Gtk.Toolbar()
        if show_controls:
            self.controls.show()
        self.pack_start(self.controls, False, False, 0)
        self.pack_start(self.video_window, True, True, 0)

        self.play_button = Gtk.ToggleToolButton()
        self.play_button.set_stock_id(Gtk.STOCK_MEDIA_PLAY)
        self.play_button.show()
        self._play_button_toggled_handler = self.play_button.connect("toggled",self._play_button_toggled)
        self.controls.add(self.play_button)
        
        item = Gtk.ToolItem()
        item.show()
        self.slider = Gtk.HScale()
        self.slider_changed_handler = None
        self.slider.set_draw_value(False)
        self.slider.set_increments(10,60)
        self.slider.set_restrict_to_fill_level(False)
        self.slider.show()
        self.slider.set_size_request(300,-1)
        item.add(self.slider)
        self.controls.insert(item, -1)

        item2 = Gtk.ToolItem()
        item2.show()
        self.time_label = Gtk.Label("")
        self.time_label.show()
        item2.add(self.time_label)
        self.controls.insert(item2, -1)

        self._slider_changed_handler = None
        self._duration_time_str = ""

    def play(self):
        """play - Start the media paying. It is necessary to start the 
        media playing before using most other functions.
    
        This function has no arguments
        
        """

        if not self.realized:
            self._set_video_window_id()
        if not self.realized:
            print _("""WARNING: MediaPlayerBox window not realized.
                    Will try to play anyway, but bad things could happen
                    if there is video to play""")
        if self.__uri != "":
            self.playbin.set_property("uri", self.__uri)
        self.playbin.set_state(Gst.State.PLAYING)
        self.slider.set_sensitive(True)
        self._reformat_slider()
        self._start_slider_updates()
        self.play_button.set_stock_id(Gtk.STOCK_MEDIA_PAUSE)
        self._set_play_button_active(True)

    def pause(self):
        """pause - Pause the media. If the media is a video, it will cause
        the image to "freeze". Use play() to start the media again. Note that
        calling pause before play may cause errors.
    
        This function has no arguments
        
        """

        self.playbin.set_state(Gst.State.PAUSED)
        self.slider.set_sensitive(True)
        self._reformat_slider()
        self.slider.set_sensitive(True)
        self.play_button.set_stock_id(Gtk.STOCK_MEDIA_PLAY)
        self._set_play_button_active(False)

    def stop(self):
        """stop - Stop the media playing. If the media contains video
        calling stop() will blank the video.
    
        This function has no arguments
        
        """

        self.playbin.set_state(Gst.State.NULL)
        self.slider.set_sensitive(False)
        self.play_button.set_stock_id(Gtk.STOCK_MEDIA_PLAY)
        self._set_play_button_active(False)
        self.slider.set_value(0)

    @property
    def controls_visible(self):
        """controls_visible - A boolean representing whether
        the MediaBoxPlayers default controls are visible or
        not. 

        This propery is read/write

        """

        return self.controls.get_property("visible")

    @controls_visible.setter
    def controls_visible(self, visibility):
        self.controls.set_property("visible",visibility)

    @property
    def uri(self):
        """uri - a string of a well formed uri pointing to the
        media to play. If the media is local, it will typicall start in the
        form: file:///home/username/etc.

        Defaults to "", which MediaPlayerBox will not try to play. When set,
        the MediaPlayerBox will try to change to the new video, but maintain it's
        state of being paused, playing, or stopped.

        This property is read/write.

        """
        return self.__uri

    @uri.setter
    def uri(self, uri):
        state = self.playbin.get_state(0)
        if state[0] == Gst.StateChangeReturn.SUCCESS:
            return_to_state = state[1]
        else:
            return_to_state = Gst.State.NULL
        self.stop()
        self.__uri = uri
        self.playbin.set_property("uri", self.__uri)
        self.playbin.set_state(return_to_state)
        if return_to_state != Gst.State.NULL:
            self._reformat_slider()
            self._start_slider_updates()
        
    @property
    def position(self):
        """position - the current position in the media in seconds.
        Trying to set the position past the duration of the media
        may cause errors.

        If the current position cannot be read, -1 will be returned.

        This property is read/write.

        """

        try:
            return self.playbin.query_position(Gst.Format.TIME,None)[0] / Gst.SECOND
        except Exception, inst:
            return -1

    @position.setter
    def position(self, seconds):
        self.playbin.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
                                 seconds * Gst.SECOND)
    @property
    def duration(self):
        """duration - the total duration of the media in seconds.

        If the duration cannot be read, -1 will be returned.

        This property is read only.

        """

        try:
            return self.playbin.query_duration(Gst.Format.TIME,None)[0] / Gst.SECOND
        except Exception, inst:
            return -1

    def _set_play_button_active(self, active):
        if self._play_button_toggled_handler is not None:
            self.play_button.disconnect(self._play_button_toggled_handler)
        self.play_button.set_active(active)
        self._play_button_toggled_handler = self.play_button.connect("toggled",self._play_button_toggled)

    def _play_button_toggled(self, widget, data=None):
        if widget.get_active():
            self.play()
        else:
            self.pause()

    def _reformat_slider(self):
        if self.playbin.get_state(0)[1] == Gst.State.NULL:
            self.slider.set_range(0, 0)
        else:
            GObject.idle_add(self._set_slider_range)

    def _set_slider_range(self):
        dur = self.duration
        if dur < 0:
            return True
        else:
            self._duration_time_str = self._formatted_time(dur)
            Gdk.threads_enter()
            self.slider.set_range(0, dur)
            Gdk.threads_leave()
            return False

    def _start_slider_updates(self):
        if self.playbin.get_state(0)[1] == Gst.State.NULL:
            self.slide.set_value(0)
        else:
            GObject.timeout_add(1000, self._set_slider_position)

    def _set_slider_position(self):
        if self._slider_changed_handler is not None:
            self.slider.disconnect(self._slider_changed_handler)
        self.slider.set_value(self.position)
        self._slider_changed_handler = self.slider.connect("value-changed",self._seek)
        try:
            if self.position < 0 or self.duration < 0:
                self.time_label.set_text("")
            else:
                dur = self._duration_time_str
                pos = self._formatted_time(self.position, len(dur) > 5)
                self.time_label.set_text(pos + "/" + dur)
        except:
            self.time_label.set_text("")
        return self.playbin.get_state(0)[1] == Gst.State.PLAYING

    def _formatted_time(self, pos, always_include_hours = False):
        seconds = pos % 60
        minutes = (pos % (60 * 60)) / 60
        hours = pos / (60 * 60)
        if hours > 23:
            hours = hours % 24
        t = datetime.time(hours, minutes, seconds)
        if t.hour > 0 or always_include_hours:
            format = "%H:%M:%S"
        else:
            format = "%M:%S"
        return t.strftime(format)


    def _seek(self, widget, data=None):
        self.position = widget.get_value()

    def _on_message(self, bus, message):
        """ _on_message - internal signal handler for bus messages.
        May be useful to extend in a base class to handle messages
        produced from custom behaviors.


        arguments -
        bus: the bus from which the message was sent, typically self.bux
        message: the message sent

        """

        if message is None:
            return

        t = message.type
        if t == Gst.MessageType.EOS:
            self.playbin.set_state(Gst.State.NULL)
            self.emit("end-of-file", self.uri)
        elif t == Gst.MessageType.ERROR:
            err, debug = message.parse_error()
            print "Error: %s" % err, debug

    def _on_sync_message(self, bus, message):
        """ _on_sync_message - internal signal handler for bus messages.
        May be useful to extend in a base class to handle messages
        produced from custom behaviors.


        arguments -
        bus: the bus from which the message was sent, typically self.bux
        message: the message sent

        """

        if message.structure is None:
            return
        message_name = message.structure.get_name()
        if message_name == "prepare-xwindow-id":
            imagesink = message.src
            imagesink.set_property("force-aspect-ratio", True)
            imagesink.set_xwindow_id(self.video_window.get_window().get_xid())

    def __on_video_window_realized(self, widget, data=None):
        """__on_video_window_realized - internal signal handler, used
        to set up the xid for the drawing area in thread safe manner.
        Do not call directly.

        """

        self._set_video_window_id()

    def _set_video_window_id(self):
        if not self.realized and self.video_window.get_window() is not None:
            x = self.video_window.get_window().get_xid()
            self.realized = True

    def on_destroy(self, widget, data=None):
        #clean up the camera before exiting
        self.playbin.set_state(Gst.State.NULL)

    __gsignals__ = {'end-of-file' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
		(GObject.TYPE_PYOBJECT,)),
		} 

def __seek_func(sender, mb):
    """Function for testing purposes, do not use"""

    mb.position = 10

def __seek_time(data):
    """Function for testing purposes, do not use"""

    mb, label = data
    label.set_text(str(mb.position) + "/" + str(mb.duration))
    return True

def __set_uri(sender, data):
    """Function for testing purposes, do not use"""

    mb, entry = data
    mb.uri = entry.get_text()

def __controls_func(sender, mb):
    mb.controls_visible = sender.get_active()

def __on_media_ended(widget, uri):
    print uri

if __name__ == "__main__":
    """creates a test WebCamBox"""

    Gst.init (sys.argv)

    #create and show a test window
    win = Gtk.Window()
    win.set_title("WebCam Test Window")
    win.connect("destroy",Gtk.main_quit)
    win.show()

    #create a top level container
    vbox = Gtk.VBox(False, 10)
    vbox.show()
    win.add(vbox)

    mb = MediaPlayerBox()

    mb.set_size_request(600,600)
    vbox.add(mb)
    mb.show()

    uri_entry = Gtk.Entry()

    play_butt = Gtk.Button("Play")
    pause_butt = Gtk.Button("Pause")
    stop_butt = Gtk.Button("Stop")
    seek_butt = Gtk.Button("Seek")
    controls_butt = Gtk.ToggleButton("Controls")
    time_label = Gtk.Label("")

    play_butt.connect("clicked", lambda x:mb.play())
    play_butt.show()
    mb.pack_end(play_butt, False, False, 0)

    uri_entry.connect("activate", __set_uri, (mb, uri_entry))
    uri_entry.set_text("file:///home/rick/Videos/VID00110.AVI")
    uri_entry.show()
    mb.pack_end(uri_entry, False, False, 0)

    pause_butt.connect("clicked", lambda x:mb.pause())
    pause_butt.show()
    mb.pack_end(pause_butt, False, False, 0)

    stop_butt.connect("clicked", lambda x:mb.stop())
    stop_butt.show()
    mb.pack_end(stop_butt, False, False, 0)

    seek_butt.connect("clicked", __seek_func, mb)
    seek_butt.show()
    mb.pack_end(seek_butt, False, False, 0)

    controls_butt.connect("clicked", __controls_func, mb)
    controls_butt.show()
    mb.pack_end(controls_butt, False, False, 0)

    mb.connect("end-of-file", __on_media_ended)

    time_label.show()
    mb.pack_end(time_label, False, False, 0)

    GObject.timeout_add(1000, __seek_time, (mb, time_label))

    Gtk.main()


