From 6262ca4a516e0710281b46c1cc846d873f145021 Mon Sep 17 00:00:00 2001 From: noisymime Date: Mon, 14 Apr 2008 13:34:29 +0000 Subject: [PATCH] - Much cleaning up - Abstracted multimedia controllers --- interfaces/WheelMenu/WheelMenu.py | 2 +- modules/dvd_player/dvd_player.py | 2 +- modules/myth_tv_player/myth_tv_player.py | 3 +- modules/video_player/video_player.py | 4 +- .../AudioController.py | 0 multimedia/MediaController.py | 42 +++ multimedia/MediaOSD.py | 142 +++++++++ multimedia/VideoController.py | 273 ++++++++++++++++++ multimedia/__init__.py | 0 themeMgr.py | 2 +- themes/Mich/music.xml | 4 + themes/Mich/music/default_cover.png | Bin 0 -> 5555 bytes themes/default/music.xml | 4 + themes/default/music/default_cover.png | Bin 0 -> 5555 bytes 14 files changed, 472 insertions(+), 6 deletions(-) rename VideoController.py => multimedia/AudioController.py (100%) create mode 100644 multimedia/MediaController.py create mode 100644 multimedia/MediaOSD.py create mode 100644 multimedia/VideoController.py create mode 100644 multimedia/__init__.py create mode 100755 themes/Mich/music/default_cover.png create mode 100755 themes/default/music/default_cover.png diff --git a/interfaces/WheelMenu/WheelMenu.py b/interfaces/WheelMenu/WheelMenu.py index 4f4fab2..41492e8 100644 --- a/interfaces/WheelMenu/WheelMenu.py +++ b/interfaces/WheelMenu/WheelMenu.py @@ -4,7 +4,7 @@ import gtk import pango import time import math -from ReflectionTexture import Texture_Reflection +from ui_elements.ReflectionTexture import Texture_Reflection from interfaces.MenuItem import MenuItem from InputQueue import InputQueue diff --git a/modules/dvd_player/dvd_player.py b/modules/dvd_player/dvd_player.py index dc404f7..b5f742e 100644 --- a/modules/dvd_player/dvd_player.py +++ b/modules/dvd_player/dvd_player.py @@ -1,6 +1,6 @@ import clutter from clutter import cluttergst -from VideoController import VideoController +from multimedia.VideoController import VideoController class Module: title = "DVD" diff --git a/modules/myth_tv_player/myth_tv_player.py b/modules/myth_tv_player/myth_tv_player.py index 2ce5d9d..d144e5a 100644 --- a/modules/myth_tv_player/myth_tv_player.py +++ b/modules/myth_tv_player/myth_tv_player.py @@ -9,8 +9,7 @@ from clutter import cluttergst from modules.myth_tv_player.MythBackendConn import MythBackendConnection from modules.myth_tv_player.tv_db_controller import tv_db_controller from SplashScr import SplashScr -#from Menu import Menu -from VideoController import VideoController +from multimedia.VideoController import VideoController class Module: title = "TV" diff --git a/modules/video_player/video_player.py b/modules/video_player/video_player.py index 5d052fa..25b1f9f 100644 --- a/modules/video_player/video_player.py +++ b/modules/video_player/video_player.py @@ -5,7 +5,7 @@ import pango import clutter import os from clutter import cluttergst -from VideoController import VideoController +from multimedia.VideoController import VideoController from modules.video_player.elements.cover_viewer import coverViewer from modules.video_player.elements.video_details import video_details from modules.video_player.elements.folder_menu import folderMenu @@ -248,6 +248,7 @@ class Module(): #if len(self.currentViewer.textureLibrary) == 0: if self.currentViewer is None: self.glossMgr.display_msg("Error: No videos", "There are no videos available in the library. This maybe caused by an empty library or a failed connection to the server.") + self.currentViewer = None self.stop() return @@ -321,6 +322,7 @@ class Module(): timeline_stop.start() def destroyPlugin(self, data): + self.stage.remove(self.backdrop) self.stage.remove(self.currentViewer) self.stage.remove(self.folderLibrary[self.folder_level]) self.stage.remove(self.video_details) diff --git a/VideoController.py b/multimedia/AudioController.py similarity index 100% rename from VideoController.py rename to multimedia/AudioController.py diff --git a/multimedia/MediaController.py b/multimedia/MediaController.py new file mode 100644 index 0000000..6a5d1fb --- /dev/null +++ b/multimedia/MediaController.py @@ -0,0 +1,42 @@ +import clutter +import gobject +from multimedia.MediaOSD import osd + +class MediaController(gobject.GObject): + + #Setup signals + __gsignals__ = { + "playing": ( + gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, []), + "stopped": ( + gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, []) + } + + def __init__(self, glossMgr): + gobject.GObject.__init__(self) + self.stage = glossMgr.stage + #self.media_element = clutter.Media() + + self.use_osd = True + self.osd = osd(glossMgr) + + + #Skips the media forward the specified amount + def skip(self, amount): + if not self.media_element.get_can_seek(): + return + + #current_pos = self.video_texture.get_position() + current_pos = self.media_element.get_property("position") + new_pos = int(int(current_pos) + int(amount)) + + if new_pos >= self.media_element.get_duration(): + new_pos = self.media_element.get_duration()-1 + if new_pos <= 0: + new_pos = 1 + + # There's apparently a collision in the python bindings with the following method. Change this when its fixed in the bindings + #self.media_element.set_position(new_pos) + #Until then use: + self.media_element.set_property("position", int(new_pos)) + if self.use_osd: self.osd.shift_media(amount) \ No newline at end of file diff --git a/multimedia/MediaOSD.py b/multimedia/MediaOSD.py new file mode 100644 index 0000000..bd0b892 --- /dev/null +++ b/multimedia/MediaOSD.py @@ -0,0 +1,142 @@ +import clutter +import time + +class osd: + + def __init__(self, glossMgr): + self.glossMgr = glossMgr + self.stage = glossMgr.stage + self.timerRunning = False + self.setup_ui() + + self.bar_group = clutter.Group() + + + #self.background = clutter.Texture() + #self.background.set_pixbuf( gtk.gdk.pixbuf_new_from_file("ui/default/osd_bar3.png") ) + #self.background.set_opacity(255) + #self.background.set_width(stage.get_width()) + self.bar_group.add(self.background) + self.bar_group.show_all() + + def setup_ui(self): + self.background = self.glossMgr.themeMgr.get_texture("video_osd_bar", self.stage, None) + + def enter(self): + self.stage.add(self.bar_group) + self.bar_group.show() + + self.bar_group.set_position(0, self.stage.get_height()) + bar_position_y = int(self.stage.get_height() - self.background.get_height()) + + knots = (\ + (self.bar_group.get_x(), self.bar_group.get_y()),\ + (self.bar_group.get_x(), bar_position_y) \ + ) + + self.timeline = clutter.Timeline(25, 50) + self.alpha = clutter.Alpha(self.timeline, clutter.ramp_inc_func) + self.enter_behaviour_path = clutter.BehaviourPath(self.alpha, knots) + + self.enter_behaviour_path.apply(self.bar_group) + + self.timeline.start() + + self.timer = threading.Timer(3.0, self.exit) + self.timer.start() + + def exit(self): + + knots = (\ + (self.bar_group.get_x(), self.bar_group.get_y()),\ + (self.bar_group.get_x(), int(self.stage.get_height())) \ + ) + + self.timeline = clutter.Timeline(25, 50) + self.timeline.connect('completed', self.exit_end_event) + self.alpha = clutter.Alpha(self.timeline, clutter.ramp_inc_func) + self.exit_behaviour_path = clutter.BehaviourPath(self.alpha, knots) + + self.exit_behaviour_path.apply(self.bar_group) + self.timeline.start() + + def exit_end_event(self, data): + self.stage.remove(self.bar_group) + + #Is called when the media is skipped forwards or backwards + def shift_media(self, shift_amount): + #Firstly check whether the label is already there from last time + if self.timerRunning: + return + + shiftDistance = 100 + + self.shift_label = clutter.Label() + self.shift_label.set_font_name("Lucida Grande 60") + self.shift_label.set_opacity(0) + self.shift_label.set_color(clutter.color_parse('White')) + + #Set the string for the fast forward / rewind as well as the + if shift_amount > 0: + self.shift_label.set_text("+" + str(shift_amount) + "s >") + shift_label_x = int(self.stage.get_width() - self.shift_label.get_width() - shiftDistance) + direction = 1 + else: + self.shift_label.set_text("< " + str(shift_amount) + "s") + shift_label_x = int(0 + shiftDistance) + direction = -1 + + shift_label_y = int(self.stage.get_height() - self.shift_label.get_height()) + self.shift_label.set_position( shift_label_x, shift_label_y ) + incoming_label_knots = (\ + ( shift_label_x, shift_label_y ),\ + ( int(shift_label_x + (shiftDistance*direction)), shift_label_y )\ + ) + + self.incoming_text_timeline = clutter.Timeline(20, 60) + alpha = clutter.Alpha(self.incoming_text_timeline, clutter.ramp_inc_func) + self.behaviour1 = clutter.BehaviourPath(alpha, incoming_label_knots) + self.behaviour2 = clutter.BehaviourOpacity(opacity_start=0, opacity_end=120, alpha=alpha) + + self.behaviour1.apply(self.shift_label) + self.behaviour2.apply(self.shift_label) + self.stage.add(self.shift_label) + self.shift_label.show() + + #self.timer = threading.Timer(1.5, self.label_exit) + gobject.timeout_add(1500, self.label_exit) + self.timerRunning = True + #self.timer.start() + + self.incoming_text_timeline.start() + #print time.strftime("%H:%M:%S", time.gmtime(amount)) + + def label_exit(self): + self.timerRunning = False + #Check which way this label needs to go + if self.shift_label.get_text()[0] == "<": + end_x = int(self.shift_label.get_width() * -1) + else: + end_x = int(self.stage.get_width()) + + (starting_pos_x, starting_pos_y) = self.shift_label.get_abs_position() + outgoing_label_knots = (\ + ( starting_pos_x, starting_pos_y ),\ + ( end_x, starting_pos_y )\ + ) + + self.outgoing_text_timeline = clutter.Timeline(20, 60) + self.outgoing_text_timeline.connect('completed', self.removeLabel) + alpha = clutter.Alpha(self.outgoing_text_timeline, clutter.ramp_inc_func) + self.behaviour1 = clutter.BehaviourPath(alpha, outgoing_label_knots) + self.behaviour2 = clutter.BehaviourOpacity(opacity_start=self.shift_label.get_opacity() , opacity_end=0, alpha=alpha) + + self.behaviour1.apply(self.shift_label) + self.behaviour2.apply(self.shift_label) + + self.outgoing_text_timeline.start() + + return False + + def removeLabel(self, data): + self.stage.remove(self.shift_label) \ No newline at end of file diff --git a/multimedia/VideoController.py b/multimedia/VideoController.py new file mode 100644 index 0000000..ea74121 --- /dev/null +++ b/multimedia/VideoController.py @@ -0,0 +1,273 @@ +import sys, clutter, clutter.cluttergst, gst, pygst, gtk, pygtk, gobject +import threading +import os +from multimedia.MediaController import MediaController + +class VideoController(MediaController): + + def __init__(self, glossMgr): + MediaController.__init__(self, glossMgr) + self.overlay = None + self.blackdrop = None + + # Primary video texture & sink definition + self.video_texture = clutter.cluttergst.VideoTexture() + self.media_element = self.video_texture + self.video_sink = clutter.cluttergst.VideoSink(self.video_texture) + self.video_texture.connect('size-change', self.set_fullscreen) + self.video_texture.set_position(0,0) + + def on_key_press_event(self, event): + if event.keyval == clutter.keysyms.Left: + self.skip(-20) + if event.keyval == clutter.keysyms.Right: + self.skip(20) + + #self.osd.enter() + + def play_video(self, uri, player): + #self.customBin(uri) + #return + + self.player = player + self.video_texture.set_uri(uri) + + + #We need to connect to the message queue on the playbin to watch for any message (ie codec or file not found errors) + self.bin = self.video_texture.get_playbin() + #print "Queue: " + str(self.bin.get_property("queue_size")) + #print "Queue: " + str(self.bin.get_property("queue_threshold")) + #print "Queue: " + str(self.bin.get_property("queue_min_threshold")) + bus = self.video_texture.get_playbin().get_bus() + bus.add_signal_watch() + bus.connect('message', self.on_bus_message) + + #Now we can start the video + self.video_texture.set_playing(True) + #self.bin.set_state(gst.STATE_PAUSED) + self.bin.set_state(gst.STATE_PLAYING) + self.isPlaying = True + + #decodebin = self.bin.get_by_name("decodebin0") + #for element in decodebin.elements(): + # print "GST Element 1: " + str(element.get_name()) + #queue = decodebin.get_by_name("queue0") + #print queue.get_name() + #ypefind = decodebin.get_by_name("typefind") + + #decodebin.connect("pad-added", self.on_pad_added) + #vid = demuxer.get_by_name("video_00") + #self.queue1 = gst.element_factory_make("queue", "queue1") + #self.queue1.set_property("max-size-time", 50000) + #self.queue1.set_property("max-size-buffers", 0) + + #self.queue2 = gst.element_factory_make("queue", "queue2") + #self.bin.add(self.queue1) + #self.bin.add(self.queue2) + #decodebin.link(self.queue1) + #self.queue1.link(decodebin) + self.video_texture.set_opacity(255) + self.video_texture.set_position(0, 0) + self.video_texture.show() + #if self.video_texture.get_parent() is None: + self.stage.add(self.video_texture) + + self.emit("playing") + return self.video_texture + + #This handles any messages that are sent accross the playbin + #Currently this is checking two types of msgs: + # 1) A "codec not found" warning, at which stage playback is stopped + # 2) A Buffering msg. This pauses the video until the buffer is at 100% + def on_bus_message(self, bus, message): + t = message.type + #print "message type: " + str(t) + if t == gst.MESSAGE_ELEMENT: + #This occurs when an invalid codec is attempted to be played + #Need to insert some form of message to the user here + if self.player.glossMgr.debug: print "GStreamer Bus msg: " + message.structure.to_string() + struc = message.structure + if struc is None: + return + + if struc.get_name() == "missing-plugin": + print "GStreamer Error (missing-plugin): " + message.structure.to_string() + self.isPlaying = False + self.video_texture.set_playing(False) + self.player.stop_video() + elif t == gst.MESSAGE_BUFFERING: + percent = message.parse_buffering() + print "Buffer: " + str(percent) + if percent < 100: + self.bin.set_state(gst.STATE_PAUSED) + else: + if not self.bin.get_state() == gst.STATE_PLAYING: + self.bin.set_state(gst.STATE_PLAYING) + elif t == gst.MESSAGE_STATE_CHANGED: + prev, current, next = message.parse_state_changed() + #print "State Changed. Previous state: " + str(prev) + #print "State Changed. Current state: " + str(current) + elif t == gst.STREAM_ERROR: + #print "OHH NOES!" + print "GST Stream Error: " + message.structure.to_string() + else: + if not self.player is None: + if self.player.glossMgr.debug: print "GST Message: " + str(message) + + def stop_video(self): + if self.video_texture.get_playing(): + self.isPlaying = False + self.player.stop_video() + self.player = None + self.video_texture.set_playing(False) + + timeline = clutter.Timeline(15, 25) + timeline.connect('completed', self.end_video_event) + alpha = clutter.Alpha(timeline, clutter.ramp_inc_func) + self.behaviour = clutter.BehaviourOpacity(opacity_start=255, opacity_end=0, alpha=alpha) + self.behaviour.apply(self.video_texture) + if not (self.blackdrop is None): + self.behaviour.apply(self.blackdrop) + + timeline.start() + + def end_video_event(self, data): + self.stage.remove(self.video_texture) + if not (self.blackdrop is None): + self.stage.remove(self.blackdrop) + self.blackdrop = None + + def customBin(self, fd): + self.pipeline = gst.Pipeline("testPipeline") + #self.src = gst.element_factory_make("filesrc", "src") + #self.src.set_property("location", "test.mpg") + self.src = gst.element_factory_make("fdsrc", "src"); + self.src.set_property("fd", int(fd)) + self.demux = gst.element_factory_make("ffdemux_mpegts", "demux") + #self.demux = gst.element_factory_make("decodebin", "demux") + self.queue1 = gst.element_factory_make("queue", "queue1") + self.queue1.set_property("max-size-time", 500000) + self.queue1.set_property("max-size-buffers", 0) + self.queue2 = gst.element_factory_make("queue", "queue2") + #self.deinterlace = gst.element_factory_make("ffdeinterlace", "deinterlace") + self.vdecode = gst.element_factory_make("mpeg2dec", "vdecode") + self.adecode = gst.element_factory_make("mad", "adecode") + self.vsink = gst.element_factory_make("xvimagesink", "vsink") + #self.vsink = self.video_sink #cluttergst.VideoSink(self.video_texture) + self.asink = gst.element_factory_make("alsasink", "asink") + + bus = self.pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message', self.on_bus_message) + + # add elements to the pipeline + self.pipeline.add(self.src) + self.pipeline.add(self.demux) + self.pipeline.add(self.queue1) + self.pipeline.add(self.queue2) + self.pipeline.add(self.vdecode) + #self.pipeline.add(self.deinterlace) + self.pipeline.add(self.adecode) + self.pipeline.add(self.vsink) + self.pipeline.add(self.asink) + + bus = self.pipeline.get_bus() + gst.Bus.add_signal_watch (bus) + + # we can't link demux until the audio and video pads are added + # we need to listen for "pad-added" signals + self.demux.connect("pad-added", self.on_pad_added) + + # link all elements apart from demux + print "linking..." + gst.element_link_many(self.src, self.demux) + gst.element_link_many(self.queue1, self.vdecode, self.vsink) #self.deinterlace, self.vsink) + gst.element_link_many(self.queue2, self.adecode, self.asink) + + self.pipeline.set_state(gst.STATE_PLAYING) + + def on_pad_added(self, element, src_pad): + caps = src_pad.get_caps() + name = caps[0].get_name() + # link demux to vdecode when video/mpeg pad added to demux + if name == "video/mpeg": + sink_pad = self.queue1.get_pad("sink") + elif name == "audio/mpeg": + sink_pad = self.queue2.get_pad("sink") + else: + return + if not sink_pad.is_linked(): + src_pad.link(sink_pad) + + def set_fullscreen(self, texture, width, height): + texture.set_property("sync-size", False) + texture.set_position(0, 0) + ratio = float(self.stage.get_width()) / float(width) + xy_ratio = float(width) / float(height) + #print "Width: " + str(width) + #print "Height: " + str(height) + #print "XY Ratio: " + str(ratio) + + width = int(self.stage.get_width()) + height = int ((height * ratio)) + #print "New Width: " + str(width) + #print "New Height: " + str(height) + + if height < self.stage.get_height(): + #Create a black backdrop that the video can sit on + self.blackdrop = clutter.Rectangle() + self.blackdrop.set_color(clutter.color_parse('Black')) + self.blackdrop.set_size(self.stage.get_width(), self.stage.get_height()) + self.stage.remove(self.video_texture) + self.stage.add(self.blackdrop) + self.stage.add(self.video_texture) + self.blackdrop.show() + + #And move the video into the vertical center + pos_y = int((self.stage.get_height() - height) / 2) + self.video_texture.set_position(0, pos_y) + + texture.set_size(width, height) + + def pause_video(self, use_backdrop): + if use_backdrop: + #Use the overlay to go over show + if self.overlay == None: + self.overlay = clutter.Rectangle() + self.overlay.set_color(clutter.color_parse('Black')) + self.overlay.set_size(self.stage.get_width(), self.stage.get_height()) + self.stage.add(self.overlay) + self.overlay.set_opacity(0) + self.overlay.show() + + + #self.video_texture.lower_actor(self.overlay) + #self.overlay.raise_actor(self.video_texture) + #Fade the overlay in + timeline_overlay = clutter.Timeline(10,30) + alpha = clutter.Alpha(timeline_overlay, clutter.ramp_inc_func) + self.overlay_behaviour = clutter.BehaviourOpacity(opacity_start=0, opacity_end=200, alpha=alpha) + self.overlay_behaviour.apply(self.overlay) + #video_behaviour.apply(self.video_texture) + timeline_overlay.start() + + #Pause the video + self.video_texture.set_playing(False) + + def unpause_video(self): + if not self.overlay is None: + #Fade the backdrop in + timeline_unpause = clutter.Timeline(10,30) + alpha = clutter.Alpha(timeline_unpause, clutter.ramp_inc_func) + self.overlay_behaviour = clutter.BehaviourOpacity(opacity_start=200, opacity_end=0, alpha=alpha) + self.overlay_behaviour.apply(self.overlay) + #video_behaviour.apply(self.video_texture) + timeline_unpause.start() + + #Resume the video + self.video_texture.set_playing(True) + + + + + diff --git a/multimedia/__init__.py b/multimedia/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/themeMgr.py b/themeMgr.py index bddb2c1..999f3b7 100644 --- a/themeMgr.py +++ b/themeMgr.py @@ -10,7 +10,7 @@ class ThemeMgr: defaultTheme = "default" currentTheme = "default" currentTheme = "Pear" - #currentTheme = "Mich" + currentTheme = "Mich" #currentTheme = "Gloxygen" def __init__(self, glossMgr): diff --git a/themes/Mich/music.xml b/themes/Mich/music.xml index 6f844e6..23c13ab 100644 --- a/themes/Mich/music.xml +++ b/themes/Mich/music.xml @@ -10,4 +10,8 @@ 0 + + + music/default_cover.png + diff --git a/themes/Mich/music/default_cover.png b/themes/Mich/music/default_cover.png new file mode 100755 index 0000000000000000000000000000000000000000..1de9c14589378b20832bf61943cb32fdfb3421a2 GIT binary patch literal 5555 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 B0 + + + music/default_cover.png + diff --git a/themes/default/music/default_cover.png b/themes/default/music/default_cover.png new file mode 100755 index 0000000000000000000000000000000000000000..1de9c14589378b20832bf61943cb32fdfb3421a2 GIT binary patch literal 5555 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 B