diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index 17d11ef63..ecd6ca5bd 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -111,6 +111,9 @@ class ItemCapabilities(object): ``CanEditTitle`` The capability to edit the title of the item + ``IsOptical`` + .Determines is the service_item is based on an optical device + """ CanPreview = 1 CanEdit = 2 @@ -129,6 +132,7 @@ class ItemCapabilities(object): HasBackgroundAudio = 15 CanAutoStartForLive = 16 CanEditTitle = 17 + IsOptical = 18 class ServiceItem(RegistryProperties): @@ -416,7 +420,10 @@ class ServiceItem(RegistryProperties): for text_image in service_item['serviceitem']['data']: if not self.title: self.title = text_image['title'] - if path: + if self.is_capable(ItemCapabilities.IsOptical): + self.has_original_files = False + self.add_from_command(text_image['path'], text_image['title'], text_image['image']) + elif path: self.has_original_files = False self.add_from_command(path, text_image['title'], text_image['image']) else: @@ -427,7 +434,8 @@ class ServiceItem(RegistryProperties): """ Returns the title of the service item. """ - if self.is_text() or ItemCapabilities.CanEditTitle in self.capabilities: + if self.is_text() or self.is_capable(ItemCapabilities.IsOptical) \ + or self.is_capable(ItemCapabilities.CanEditTitle): return self.title else: if len(self._raw_frames) > 1: @@ -495,7 +503,8 @@ class ServiceItem(RegistryProperties): """ Confirms if the ServiceItem uses a file """ - return self.service_item_type == ServiceItemType.Image or self.service_item_type == ServiceItemType.Command + return self.service_item_type == ServiceItemType.Image or \ + (self.service_item_type == ServiceItemType.Command and not self.is_capable(ItemCapabilities.IsOptical)) def is_text(self): """ @@ -553,7 +562,7 @@ class ServiceItem(RegistryProperties): frame = self._raw_frames[row] except IndexError: return '' - if self.is_image(): + if self.is_image() or self.is_capable(ItemCapabilities.IsOptical): path_from = frame['path'] else: path_from = os.path.join(frame['path'], frame['title']) @@ -623,12 +632,17 @@ class ServiceItem(RegistryProperties): self.is_valid = False break elif self.is_command(): - file_name = os.path.join(frame['path'], frame['title']) - if not os.path.exists(file_name): - self.is_valid = False - break - if suffix_list and not self.is_text(): - file_suffix = frame['title'].split('.')[-1] - if file_suffix.lower() not in suffix_list: + if self.is_capable(ItemCapabilities.IsOptical): + if not os.path.exists(frame['title']): self.is_valid = False break + else: + file_name = os.path.join(frame['path'], frame['title']) + if not os.path.exists(file_name): + self.is_valid = False + break + if suffix_list and not self.is_text(): + file_suffix = frame['title'].split('.')[-1] + if file_suffix.lower() not in suffix_list: + self.is_valid = False + break diff --git a/openlp/core/ui/media/__init__.py b/openlp/core/ui/media/__init__.py index b2b6ab0b8..a44604d2f 100644 --- a/openlp/core/ui/media/__init__.py +++ b/openlp/core/ui/media/__init__.py @@ -72,6 +72,9 @@ class MediaInfo(object): length = 0 start_time = 0 end_time = 0 + title_track = 0 + audio_track = 0 + subtitle_track = 0 media_type = MediaType() @@ -107,6 +110,40 @@ def set_media_players(players_list, overridden_player='auto'): players = players.replace(overridden_player, '[%s]' % overridden_player) Settings().setValue('media/players', players) + +def parse_optical_path(input): + """ + Split the optical path info. + + :param input: The string to parse + :return: The elements extracted from the string: filename, title, audio_track, subtitle_track, start, end + """ + log.debug('parse_optical_path, about to parse: "%s"' % input) + clip_info = input.split(sep=':') + title = int(clip_info[1]) + audio_track = int(clip_info[2]) + subtitle_track = int(clip_info[3]) + start = float(clip_info[4]) + end = float(clip_info[5]) + clip_name = clip_info[6] + filename = clip_info[7] + # Windows path usually contains a colon after the drive letter + if len(clip_info) > 8: + filename += ':' + clip_info[8] + return filename, title, audio_track, subtitle_track, start, end, clip_name + + +def format_milliseconds(milliseconds): + """ + Format milliseconds into a human readable time string. + :param milliseconds: Milliseconds to format + :return: Time string in format: hh.mm.ss,ttt + """ + seconds, millis = divmod(milliseconds, 1000) + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + return "%02d:%02d:%02d,%03d" % (hours, minutes, seconds, millis) + from .mediacontroller import MediaController from .playertab import PlayerTab diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index f086b80b2..4a2d475c1 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -36,9 +36,10 @@ import datetime from PyQt4 import QtCore, QtGui from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, translate -from openlp.core.lib import OpenLPToolbar +from openlp.core.lib import OpenLPToolbar, ItemCapabilities from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players +from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players,\ + parse_optical_path from openlp.core.ui.media.mediaplayer import MediaPlayer from openlp.core.common import AppLocation from openlp.core.ui import DisplayControllerType @@ -368,7 +369,16 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): controller.media_info.file_info = QtCore.QFileInfo(service_item.get_frame_path()) display = self._define_display(controller) if controller.is_live: - is_valid = self._check_file_type(controller, display, service_item) + # if this is an optical device use special handling + if service_item.is_capable(ItemCapabilities.IsOptical): + log.debug('video is optical and live') + path = service_item.get_frame_path() + (name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(path) + is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display, + controller) + else: + log.debug('video is not optical and live') + is_valid = self._check_file_type(controller, display, service_item) display.override['theme'] = '' display.override['video'] = True if controller.media_info.is_background: @@ -379,12 +389,21 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): controller.media_info.start_time = service_item.start_time controller.media_info.end_time = service_item.end_time elif controller.preview_display: - is_valid = self._check_file_type(controller, display, service_item) + if service_item.is_capable(ItemCapabilities.IsOptical): + log.debug('video is optical and preview') + path = service_item.get_frame_path() + (name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(path) + is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display, + controller) + else: + log.debug('video is not optical and preview') + is_valid = self._check_file_type(controller, display, service_item) if not is_valid: # Media could not be loaded correctly critical_error_message_box(translate('MediaPlugin.MediaItem', 'Unsupported File'), translate('MediaPlugin.MediaItem', 'Unsupported File')) return False + log.debug('video mediatype: ' + str(controller.media_info.media_type)) # dont care about actual theme, set a black background if controller.is_live and not controller.media_info.is_background: display.frame.evaluateJavaScript('show_video( "setBackBoard", null, null, null,"visible");') @@ -436,6 +455,62 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): log.debug('use %s controller' % self.current_media_players[controller.controller_type]) return True + def media_setup_optical(self, filename, title, audio_track, subtitle_track, start, end, display, controller): + """ + Setup playback of optical media + + :param filename: Path of the optical device/drive. + :param title: The main/title track to play. + :param audio_track: The audio track to play. + :param subtitle_track: The subtitle track to play. + :param start: Start position in miliseconds. + :param end: End position in miliseconds. + :param display: The display to play the media. + :param controller: The media contraoller. + :return: True if setup succeded else False. + """ + log.debug('media_setup_optical') + if controller is None: + controller = self.display_controllers[DisplayControllerType.Plugin] + # stop running videos + self.media_reset(controller) + # Setup media info + controller.media_info = MediaInfo() + controller.media_info.file_info = QtCore.QFileInfo(filename) + if audio_track == -1 and subtitle_track == -1: + controller.media_info.media_type = MediaType.CD + else: + controller.media_info.media_type = MediaType.DVD + controller.media_info.start_time = start/1000 + controller.media_info.end_time = end/1000 + controller.media_info.length = (end - start)/1000 + controller.media_info.title_track = title + controller.media_info.audio_track = audio_track + controller.media_info.subtitle_track = subtitle_track + # When called from mediaitem display is None + if display is None: + display = controller.preview_display + # Find vlc player + used_players = get_media_players()[0] + vlc_player = None + for title in used_players: + player = self.media_players[title] + if player.name == 'vlc': + vlc_player = player + if vlc_player is None: + critical_error_message_box(translate('MediaPlugin.MediaItem', 'VLC player required'), + translate('MediaPlugin.MediaItem', + 'VLC player required for playback of optical devices')) + return False + vlc_player.load(display) + self.resize(display, vlc_player) + self.current_media_players[controller.controller_type] = vlc_player + if audio_track == -1 and subtitle_track == -1: + controller.media_info.media_type = MediaType.CD + else: + controller.media_info.media_type = MediaType.DVD + return True + def _check_file_type(self, controller, display, service_item): """ Select the correct media Player type from the prioritized Player list diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index 4394126c0..7e8acc318 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -40,7 +40,7 @@ from PyQt4 import QtGui from openlp.core.common import Settings, is_win, is_macosx from openlp.core.lib import translate -from openlp.core.ui.media import MediaState +from openlp.core.ui.media import MediaState, MediaType from openlp.core.ui.media.mediaplayer import MediaPlayer log = logging.getLogger(__name__) @@ -166,7 +166,19 @@ class VlcPlayer(MediaPlayer): file_path = str(controller.media_info.file_info.absoluteFilePath()) path = os.path.normcase(file_path) # create the media - display.vlc_media = display.vlc_instance.media_new_path(path) + if controller.media_info.media_type == MediaType.CD: + display.vlc_media = display.vlc_instance.media_new_location('cdda://' + path) + display.vlc_media_player.set_media(display.vlc_media) + display.vlc_media_player.play() + # Wait for media to start playing. In this case VLC actually returns an error. + self.media_state_wait(display, vlc.State.Playing) + # If subitems exists, this is a CD + audio_cd_tracks = display.vlc_media.subitems() + if not audio_cd_tracks or audio_cd_tracks.count() < 1: + return False + display.vlc_media = audio_cd_tracks.item_at_index(controller.media_info.title_track) + else: + display.vlc_media = display.vlc_instance.media_new_path(path) # put the media in the media player display.vlc_media_player.set_media(display.vlc_media) # parse the metadata of the file @@ -206,15 +218,40 @@ class VlcPlayer(MediaPlayer): """ controller = display.controller start_time = 0 + log.debug('vlc play') if self.state != MediaState.Paused and controller.media_info.start_time > 0: start_time = controller.media_info.start_time threading.Thread(target=display.vlc_media_player.play).start() if not self.media_state_wait(display, vlc.State.Playing): return False + if self.state != MediaState.Paused and controller.media_info.start_time > 0: + log.debug('vlc play, starttime set') + start_time = controller.media_info.start_time + log.debug('mediatype: ' + str(controller.media_info.media_type)) + # Set tracks for the optical device + if controller.media_info.media_type == MediaType.DVD: + log.debug('vlc play, playing started') + if controller.media_info.title_track > 0: + log.debug('vlc play, title_track set: ' + str(controller.media_info.title_track)) + display.vlc_media_player.set_title(controller.media_info.title_track) + display.vlc_media_player.play() + if not self.media_state_wait(display, vlc.State.Playing): + return False + if controller.media_info.audio_track > 0: + display.vlc_media_player.audio_set_track(controller.media_info.audio_track) + log.debug('vlc play, audio_track set: ' + str(controller.media_info.audio_track)) + if controller.media_info.subtitle_track > 0: + display.vlc_media_player.video_set_spu(controller.media_info.subtitle_track) + log.debug('vlc play, subtitle_track set: ' + str(controller.media_info.subtitle_track)) + if controller.media_info.start_time > 0: + log.debug('vlc play, starttime set: ' + str(controller.media_info.start_time)) + start_time = controller.media_info.start_time + controller.media_info.length = controller.media_info.end_time - controller.media_info.start_time + else: + controller.media_info.length = int(display.vlc_media_player.get_media().get_duration() / 1000) self.volume(display, controller.media_info.volume) - if start_time > 0: - self.seek(display, controller.media_info.start_time * 1000) - controller.media_info.length = int(display.vlc_media_player.get_media().get_duration() / 1000) + if start_time > 0 and display.vlc_media_player.is_seekable(): + display.vlc_media_player.set_time(int(start_time * 1000)) controller.seek_slider.setMaximum(controller.media_info.length * 1000) self.state = MediaState.Playing display.vlc_widget.raise_() @@ -248,6 +285,9 @@ class VlcPlayer(MediaPlayer): """ Go to a particular position """ + if display.controller.media_info.media_type == MediaType.CD \ + or display.controller.media_info.media_type == MediaType.DVD: + seek_value += int(display.controller.media_info.start_time * 1000) if display.vlc_media_player.is_seekable(): display.vlc_media_player.set_time(seek_value) @@ -280,7 +320,12 @@ class VlcPlayer(MediaPlayer): self.set_visible(display, False) if not controller.seek_slider.isSliderDown(): controller.seek_slider.blockSignals(True) - controller.seek_slider.setSliderPosition(display.vlc_media_player.get_time()) + if display.controller.media_info.media_type == MediaType.CD \ + or display.controller.media_info.media_type == MediaType.DVD: + controller.seek_slider.setSliderPosition(display.vlc_media_player.get_time() - + int(display.controller.media_info.start_time * 1000)) + else: + controller.seek_slider.setSliderPosition(display.vlc_media_player.get_time()) controller.seek_slider.blockSignals(False) def get_info(self): diff --git a/openlp/plugins/media/forms/__init__.py b/openlp/plugins/media/forms/__init__.py new file mode 100644 index 000000000..6b241e7fc --- /dev/null +++ b/openlp/plugins/media/forms/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### diff --git a/openlp/plugins/media/forms/mediaclipselectordialog.py b/openlp/plugins/media/forms/mediaclipselectordialog.py new file mode 100644 index 000000000..ca091693e --- /dev/null +++ b/openlp/plugins/media/forms/mediaclipselectordialog.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### + + +from PyQt4 import QtCore, QtGui +from openlp.core.common import translate +from openlp.core.lib import build_icon + + +class Ui_MediaClipSelector(object): + def setupUi(self, media_clip_selector): + media_clip_selector.setObjectName('media_clip_selector') + media_clip_selector.resize(554, 654) + self.combobox_size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Fixed) + media_clip_selector.setSizePolicy( + QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding)) + self.main_layout = QtGui.QVBoxLayout(media_clip_selector) + self.main_layout.setContentsMargins(8, 8, 8, 8) + self.main_layout.setObjectName('main_layout') + # Source groupbox + self.source_groupbox = QtGui.QGroupBox(media_clip_selector) + self.source_groupbox.setObjectName('source_groupbox') + self.source_layout = QtGui.QHBoxLayout() + self.source_layout.setContentsMargins(8, 8, 8, 8) + self.source_layout.setObjectName('source_layout') + self.source_groupbox.setLayout(self.source_layout) + # Media path label + self.media_path_label = QtGui.QLabel(self.source_groupbox) + self.media_path_label.setObjectName('media_path_label') + self.source_layout.addWidget(self.media_path_label) + # Media path combobox + self.media_path_combobox = QtGui.QComboBox(self.source_groupbox) + # Make the combobox expand + self.media_path_combobox.setSizePolicy(self.combobox_size_policy) + self.media_path_combobox.setEditable(True) + self.media_path_combobox.setObjectName('media_path_combobox') + self.source_layout.addWidget(self.media_path_combobox) + # Load disc button + self.load_disc_button = QtGui.QPushButton(media_clip_selector) + self.load_disc_button.setEnabled(True) + self.load_disc_button.setObjectName('load_disc_button') + self.source_layout.addWidget(self.load_disc_button) + self.main_layout.addWidget(self.source_groupbox) + # Track details group box + self.track_groupbox = QtGui.QGroupBox(media_clip_selector) + self.track_groupbox.setObjectName('track_groupbox') + self.track_layout = QtGui.QFormLayout() + self.track_layout.setContentsMargins(8, 8, 8, 8) + self.track_layout.setObjectName('track_layout') + self.label_alignment = self.track_layout.labelAlignment() + self.track_groupbox.setLayout(self.track_layout) + # Title track + self.title_label = QtGui.QLabel(self.track_groupbox) + self.title_label.setObjectName('title_label') + self.titles_combo_box = QtGui.QComboBox(self.track_groupbox) + self.titles_combo_box.setSizePolicy(self.combobox_size_policy) + self.titles_combo_box.setEditText('') + self.titles_combo_box.setObjectName('titles_combo_box') + self.track_layout.addRow(self.title_label, self.titles_combo_box) + # Audio track + self.audio_track_label = QtGui.QLabel(self.track_groupbox) + self.audio_track_label.setObjectName('audio_track_label') + self.audio_tracks_combobox = QtGui.QComboBox(self.track_groupbox) + self.audio_tracks_combobox.setSizePolicy(self.combobox_size_policy) + self.audio_tracks_combobox.setObjectName('audio_tracks_combobox') + self.track_layout.addRow(self.audio_track_label, self.audio_tracks_combobox) + self.main_layout.addWidget(self.track_groupbox) + # Subtitle track + self.subtitle_track_label = QtGui.QLabel(self.track_groupbox) + self.subtitle_track_label.setObjectName('subtitle_track_label') + self.subtitle_tracks_combobox = QtGui.QComboBox(self.track_groupbox) + self.subtitle_tracks_combobox.setSizePolicy(self.combobox_size_policy) + self.subtitle_tracks_combobox.setObjectName('subtitle_tracks_combobox') + self.track_layout.addRow(self.subtitle_track_label, self.subtitle_tracks_combobox) + # Preview frame + self.preview_frame = QtGui.QFrame(media_clip_selector) + self.preview_frame.setMinimumSize(QtCore.QSize(320, 240)) + self.preview_frame.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, + QtGui.QSizePolicy.MinimumExpanding)) + self.preview_frame.setStyleSheet('background-color:black;') + self.preview_frame.setFrameShape(QtGui.QFrame.NoFrame) + self.preview_frame.setObjectName('preview_frame') + self.main_layout.addWidget(self.preview_frame) + # player controls + self.controls_layout = QtGui.QHBoxLayout() + self.controls_layout.setObjectName('controls_layout') + self.play_button = QtGui.QToolButton(media_clip_selector) + self.play_button.setIcon(build_icon(':/slides/media_playback_start.png')) + self.play_button.setObjectName('play_button') + self.controls_layout.addWidget(self.play_button) + self.position_slider = QtGui.QSlider(media_clip_selector) + self.position_slider.setTracking(False) + self.position_slider.setOrientation(QtCore.Qt.Horizontal) + self.position_slider.setObjectName('position_slider') + self.controls_layout.addWidget(self.position_slider) + self.position_timeedit = QtGui.QTimeEdit(media_clip_selector) + self.position_timeedit.setReadOnly(True) + self.position_timeedit.setObjectName('position_timeedit') + self.controls_layout.addWidget(self.position_timeedit) + self.main_layout.addLayout(self.controls_layout) + # Range + self.range_groupbox = QtGui.QGroupBox(media_clip_selector) + self.range_groupbox.setObjectName('range_groupbox') + self.range_layout = QtGui.QGridLayout() + self.range_layout.setContentsMargins(8, 8, 8, 8) + self.range_layout.setObjectName('range_layout') + self.range_groupbox.setLayout(self.range_layout) + # Start position + self.start_position_label = QtGui.QLabel(self.range_groupbox) + self.start_position_label.setObjectName('start_position_label') + self.range_layout.addWidget(self.start_position_label, 0, 0, self.label_alignment) + self.start_position_edit = QtGui.QTimeEdit(self.range_groupbox) + self.start_position_edit.setObjectName('start_position_edit') + self.range_layout.addWidget(self.start_position_edit, 0, 1) + self.set_start_button = QtGui.QPushButton(self.range_groupbox) + self.set_start_button.setObjectName('set_start_button') + self.range_layout.addWidget(self.set_start_button, 0, 2) + self.jump_start_button = QtGui.QPushButton(self.range_groupbox) + self.jump_start_button.setObjectName('jump_start_button') + self.range_layout.addWidget(self.jump_start_button, 0, 3) + # End position + self.end_position_label = QtGui.QLabel(self.range_groupbox) + self.end_position_label.setObjectName('end_position_label') + self.range_layout.addWidget(self.end_position_label, 1, 0, self.label_alignment) + self.end_timeedit = QtGui.QTimeEdit(self.range_groupbox) + self.end_timeedit.setObjectName('end_timeedit') + self.range_layout.addWidget(self.end_timeedit, 1, 1) + self.set_end_button = QtGui.QPushButton(self.range_groupbox) + self.set_end_button.setObjectName('set_end_button') + self.range_layout.addWidget(self.set_end_button, 1, 2) + self.jump_end_button = QtGui.QPushButton(self.range_groupbox) + self.jump_end_button.setObjectName('jump_end_button') + self.range_layout.addWidget(self.jump_end_button, 1, 3) + self.main_layout.addWidget(self.range_groupbox) + # Save and close buttons + self.button_box = QtGui.QDialogButtonBox(media_clip_selector) + self.button_box.addButton(QtGui.QDialogButtonBox.Save) + self.button_box.addButton(QtGui.QDialogButtonBox.Close) + self.close_button = self.button_box.button(QtGui.QDialogButtonBox.Close) + self.save_button = self.button_box.button(QtGui.QDialogButtonBox.Save) + self.main_layout.addWidget(self.button_box) + + self.retranslateUi(media_clip_selector) + self.button_box.accepted.connect(media_clip_selector.accept) + self.button_box.rejected.connect(media_clip_selector.reject) + QtCore.QMetaObject.connectSlotsByName(media_clip_selector) + media_clip_selector.setTabOrder(self.media_path_combobox, self.load_disc_button) + media_clip_selector.setTabOrder(self.load_disc_button, self.titles_combo_box) + media_clip_selector.setTabOrder(self.titles_combo_box, self.audio_tracks_combobox) + media_clip_selector.setTabOrder(self.audio_tracks_combobox, self.subtitle_tracks_combobox) + media_clip_selector.setTabOrder(self.subtitle_tracks_combobox, self.play_button) + media_clip_selector.setTabOrder(self.play_button, self.position_slider) + media_clip_selector.setTabOrder(self.position_slider, self.position_timeedit) + media_clip_selector.setTabOrder(self.position_timeedit, self.start_position_edit) + media_clip_selector.setTabOrder(self.start_position_edit, self.set_start_button) + media_clip_selector.setTabOrder(self.set_start_button, self.jump_start_button) + media_clip_selector.setTabOrder(self.jump_start_button, self.end_timeedit) + media_clip_selector.setTabOrder(self.end_timeedit, self.set_end_button) + media_clip_selector.setTabOrder(self.set_end_button, self.jump_end_button) + media_clip_selector.setTabOrder(self.jump_end_button, self.save_button) + media_clip_selector.setTabOrder(self.save_button, self.close_button) + + def retranslateUi(self, media_clip_selector): + media_clip_selector.setWindowTitle(translate('MediaPlugin.MediaClipSelector', 'Select Media Clip')) + self.source_groupbox.setTitle(translate('MediaPlugin.MediaClipSelector', 'Source')) + self.media_path_label.setText(translate('MediaPlugin.MediaClipSelector', 'Media path:')) + self.media_path_combobox.lineEdit().setPlaceholderText(translate('MediaPlugin.MediaClipSelector', + 'Select drive from list')) + self.load_disc_button.setText(translate('MediaPlugin.MediaClipSelector', 'Load disc')) + self.track_groupbox.setTitle(translate('MediaPlugin.MediaClipSelector', 'Track Details')) + self.title_label.setText(translate('MediaPlugin.MediaClipSelector', 'Title:')) + self.audio_track_label.setText(translate('MediaPlugin.MediaClipSelector', 'Audio track:')) + self.subtitle_track_label.setText(translate('MediaPlugin.MediaClipSelector', 'Subtitle track:')) + self.position_timeedit.setDisplayFormat(translate('MediaPlugin.MediaClipSelector', 'HH:mm:ss.z')) + self.range_groupbox.setTitle(translate('MediaPlugin.MediaClipSelector', 'Clip Range')) + self.start_position_label.setText(translate('MediaPlugin.MediaClipSelector', 'Start point:')) + self.start_position_edit.setDisplayFormat(translate('MediaPlugin.MediaClipSelector', 'HH:mm:ss.z')) + self.set_start_button.setText(translate('MediaPlugin.MediaClipSelector', 'Set start point')) + self.jump_start_button.setText(translate('MediaPlugin.MediaClipSelector', 'Jump to start point')) + self.end_position_label.setText(translate('MediaPlugin.MediaClipSelector', 'End point:')) + self.end_timeedit.setDisplayFormat(translate('MediaPlugin.MediaClipSelector', 'HH:mm:ss.z')) + self.set_end_button.setText(translate('MediaPlugin.MediaClipSelector', 'Set end point')) + self.jump_end_button.setText(translate('MediaPlugin.MediaClipSelector', 'Jump to end point')) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py new file mode 100644 index 000000000..28d37f32e --- /dev/null +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -0,0 +1,665 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### + +import os +if os.name == 'nt': + from win32com.client import Dispatch + import string +import sys + +if sys.platform.startswith('linux'): + import dbus +import logging +import re +from time import sleep +from datetime import datetime + + +from PyQt4 import QtCore, QtGui + +from openlp.core.common import translate +from openlp.plugins.media.forms.mediaclipselectordialog import Ui_MediaClipSelector +from openlp.core.lib.ui import critical_error_message_box +from openlp.core.ui.media import format_milliseconds +try: + from openlp.core.ui.media.vendor import vlc +except (ImportError, NameError, NotImplementedError): + pass +except OSError as e: + if sys.platform.startswith('win'): + if not isinstance(e, WindowsError) and e.winerror != 126: + raise + else: + raise + +log = logging.getLogger(__name__) + + +class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): + """ + Class to manage the clip selection + """ + log.info('%s MediaClipSelectorForm loaded', __name__) + + def __init__(self, media_item, parent, manager): + """ + Constructor + """ + super(MediaClipSelectorForm, self).__init__(parent) + self.vlc_instance = None + self.vlc_media_player = None + self.vlc_media = None + self.timer = None + self.audio_cd_tracks = None + self.audio_cd = False + self.playback_length = 0 + self.media_item = media_item + self.setupUi(self) + # setup play/pause icon + self.play_icon = QtGui.QIcon() + self.play_icon.addPixmap(QtGui.QPixmap(":/slides/media_playback_start.png"), QtGui.QIcon.Normal, + QtGui.QIcon.Off) + self.pause_icon = QtGui.QIcon() + self.pause_icon.addPixmap(QtGui.QPixmap(":/slides/media_playback_pause.png"), QtGui.QIcon.Normal, + QtGui.QIcon.Off) + + def reject(self): + """ + Exit Dialog and do not save + """ + log.debug('MediaClipSelectorForm.reject') + # Tear down vlc + if self.vlc_media_player: + self.vlc_media_player.stop() + self.vlc_media_player.release() + self.vlc_media_player = None + if self.vlc_instance: + self.vlc_instance.release() + self.vlc_instance = None + if self.vlc_media: + self.vlc_media.release() + self.vlc_media = None + return QtGui.QDialog.reject(self) + + def exec_(self): + """ + Start dialog + """ + self.reset_ui() + self.setup_vlc() + return QtGui.QDialog.exec_(self) + + def reset_ui(self): + """ + Reset the UI to default values + """ + self.playback_length = 0 + self.position_slider.setMinimum(0) + self.disable_all() + self.toggle_disable_load_media(False) + self.subtitle_tracks_combobox.clear() + self.audio_tracks_combobox.clear() + self.titles_combo_box.clear() + time = QtCore.QTime() + self.start_position_edit.setTime(time) + self.end_timeedit.setTime(time) + self.position_timeedit.setTime(time) + + def setup_vlc(self): + """ + Setup VLC instance and mediaplayer + """ + self.vlc_instance = vlc.Instance() + # creating an empty vlc media player + self.vlc_media_player = self.vlc_instance.media_player_new() + # The media player has to be 'connected' to the QFrame. + # (otherwise a video would be displayed in it's own window) + # This is platform specific! + # You have to give the id of the QFrame (or similar object) + # to vlc, different platforms have different functions for this. + win_id = int(self.preview_frame.winId()) + if sys.platform == "win32": + self.vlc_media_player.set_hwnd(win_id) + elif sys.platform == "darwin": + # We have to use 'set_nsobject' since Qt4 on OSX uses Cocoa + # framework and not the old Carbon. + self.vlc_media_player.set_nsobject(win_id) + else: + # for Linux using the X Server + self.vlc_media_player.set_xwindow(win_id) + self.vlc_media = None + # Setup timer every 100 ms to update position + self.timer = QtCore.QTimer(self) + self.timer.timeout.connect(self.update_position) + self.timer.start(100) + self.find_optical_devices() + self.audio_cd = False + self.audio_cd_tracks = None + + def detect_audio_cd(self, path): + """ + Detects is the given path is an audio CD + + :param path: Path to the device to be tested. + :return: True if it was an audio CD else False. + """ + # Detect by trying to play it as a CD + self.vlc_media = self.vlc_instance.media_new_location('cdda://' + path) + self.vlc_media_player.set_media(self.vlc_media) + self.vlc_media_player.play() + # Wait for media to start playing. In this case VLC actually returns an error. + self.media_state_wait(vlc.State.Playing) + self.vlc_media_player.set_pause(1) + # If subitems exists, this is a CD + self.audio_cd_tracks = self.vlc_media.subitems() + if not self.audio_cd_tracks or self.audio_cd_tracks.count() < 1: + return False + # Insert into titles_combo_box + self.titles_combo_box.clear() + for i in range(self.audio_cd_tracks.count()): + item = self.audio_cd_tracks.item_at_index(i) + item_title = item.get_meta(vlc.Meta.Title) + self.titles_combo_box.addItem(item_title, i) + self.vlc_media_player.set_media(self.audio_cd_tracks.item_at_index(0)) + self.audio_cd = True + self.titles_combo_box.setDisabled(False) + self.titles_combo_box.setCurrentIndex(0) + self.on_title_combo_box_currentIndexChanged(0) + + return True + + @QtCore.pyqtSlot(bool) + def on_load_disc_button_clicked(self, clicked): + """ + Load the media when the load-button has been clicked + + :param clicked: Given from signal, not used. + """ + log.debug('on_load_disc_button_clicked') + self.disable_all() + path = self.media_path_combobox.currentText() + # Check if given path is non-empty and exists before starting VLC + if not path: + log.debug('no given path') + critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', 'No path was given')) + self.toggle_disable_load_media(False) + return + if not os.path.exists(path): + log.debug('Given path does not exists') + critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', + 'Given path does not exists')) + self.toggle_disable_load_media(False) + return + # VLC behaves a bit differently on windows and linux when loading, which creates problems when trying to + # detect if we're dealing with a DVD or CD, so we use different loading approaches depending on the OS. + if os.name == 'nt': + # If the given path is in the format "D:\" or "D:", prefix it with "/" to make VLC happy + pattern = re.compile('^\w:\\\\*$') + if pattern.match(path): + path = '/' + path + self.vlc_media = self.vlc_instance.media_new_location('dvd://' + path) + else: + self.vlc_media = self.vlc_instance.media_new_path(path) + if not self.vlc_media: + log.debug('vlc media player is none') + critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', + 'An error happened during initialization of VLC player')) + self.toggle_disable_load_media(False) + return + # put the media in the media player + self.vlc_media_player.set_media(self.vlc_media) + self.vlc_media_player.audio_set_mute(True) + # start playback to get vlc to parse the media + if self.vlc_media_player.play() < 0: + log.debug('vlc play returned error') + critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', + 'VLC player failed playing the media')) + self.toggle_disable_load_media(False) + return + self.vlc_media_player.audio_set_mute(True) + if not self.media_state_wait(vlc.State.Playing): + # Tests if this is an audio CD + if not self.detect_audio_cd(path): + critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', + 'VLC player failed playing the media')) + self.toggle_disable_load_media(False) + return + self.vlc_media_player.audio_set_mute(True) + if not self.audio_cd: + # Get titles, insert in combobox + titles = self.vlc_media_player.video_get_title_description() + self.titles_combo_box.clear() + for title in titles: + self.titles_combo_box.addItem(title[1].decode(), title[0]) + # Main title is usually title #1 + if len(titles) > 1: + self.titles_combo_box.setCurrentIndex(1) + else: + self.titles_combo_box.setCurrentIndex(0) + # Enable audio track combobox if anything is in it + if len(titles) > 0: + self.titles_combo_box.setDisabled(False) + self.toggle_disable_load_media(False) + log.debug('load_disc_button end - vlc_media_player state: %s' % self.vlc_media_player.get_state()) + + @QtCore.pyqtSlot(bool) + def on_play_button_clicked(self, clicked): + """ + Toggle the playback + + :param clicked: Given from signal, not used. + """ + if self.vlc_media_player.get_state() == vlc.State.Playing: + self.vlc_media_player.pause() + self.play_button.setIcon(self.play_icon) + else: + self.vlc_media_player.play() + self.media_state_wait(vlc.State.Playing) + self.play_button.setIcon(self.pause_icon) + + @QtCore.pyqtSlot(bool) + def on_set_start_button_clicked(self, clicked): + """ + Copy the current player position to start_position_edit + + :param clicked: Given from signal, not used. + """ + vlc_ms_pos = self.vlc_media_player.get_time() + time = QtCore.QTime() + new_pos_time = time.addMSecs(vlc_ms_pos) + self.start_position_edit.setTime(new_pos_time) + # If start time is after end time, update end time. + end_time = self.end_timeedit.time() + if end_time < new_pos_time: + self.end_timeedit.setTime(new_pos_time) + + @QtCore.pyqtSlot(bool) + def on_set_end_button_clicked(self, clicked): + """ + Copy the current player position to end_timeedit + + :param clicked: Given from signal, not used. + """ + vlc_ms_pos = self.vlc_media_player.get_time() + time = QtCore.QTime() + new_pos_time = time.addMSecs(vlc_ms_pos) + self.end_timeedit.setTime(new_pos_time) + # If start time is after end time, update start time. + start_time = self.start_position_edit.time() + if start_time > new_pos_time: + self.start_position_edit.setTime(new_pos_time) + + @QtCore.pyqtSlot(QtCore.QTime) + def on_start_timeedit_timeChanged(self, new_time): + """ + Called when start_position_edit is changed manually + + :param new_time: The new time + """ + # If start time is after end time, update end time. + end_time = self.end_timeedit.time() + if end_time < new_time: + self.end_timeedit.setTime(new_time) + + @QtCore.pyqtSlot(QtCore.QTime) + def on_end_timeedit_timeChanged(self, new_time): + """ + Called when end_timeedit is changed manually + + :param new_time: The new time + """ + # If start time is after end time, update start time. + start_time = self.start_position_edit.time() + if start_time > new_time: + self.start_position_edit.setTime(new_time) + + @QtCore.pyqtSlot(bool) + def on_jump_end_button_clicked(self, clicked): + """ + Set the player position to the position stored in end_timeedit + + :param clicked: Given from signal, not used. + """ + end_time = self.end_timeedit.time() + end_time_ms = end_time.hour() * 60 * 60 * 1000 + \ + end_time.minute() * 60 * 1000 + \ + end_time.second() * 1000 + \ + end_time.msec() + self.vlc_media_player.set_time(end_time_ms) + + @QtCore.pyqtSlot(bool) + def on_jump_start_button_clicked(self, clicked): + """ + Set the player position to the position stored in start_position_edit + + :param clicked: Given from signal, not used. + """ + start_time = self.start_position_edit.time() + start_time_ms = start_time.hour() * 60 * 60 * 1000 + \ + start_time.minute() * 60 * 1000 + \ + start_time.second() * 1000 + \ + start_time.msec() + self.vlc_media_player.set_time(start_time_ms) + + @QtCore.pyqtSlot(int) + def on_titles_combo_box_currentIndexChanged(self, index): + """ + When a new title is chosen, it is loaded by VLC and info about audio and subtitle tracks is reloaded + + :param index: The index of the newly chosen title track. + """ + log.debug('in on_titles_combo_box_changed, index: %d', index) + if not self.vlc_media_player: + log.error('vlc_media_player was None') + return + if self.audio_cd: + self.vlc_media = self.audio_cd_tracks.item_at_index(index) + self.vlc_media_player.set_media(self.vlc_media) + self.vlc_media_player.set_time(0) + self.vlc_media_player.play() + if not self.media_state_wait(vlc.State.Playing): + log.error('Could not start playing audio cd, needed to get track info') + return + self.vlc_media_player.audio_set_mute(True) + # Sleep 1 second to make sure VLC has the needed metadata + sleep(1) + # pause + self.vlc_media_player.set_time(0) + self.vlc_media_player.set_pause(1) + self.vlc_media_player.audio_set_mute(False) + self.toggle_disable_player(False) + else: + self.vlc_media_player.set_title(index) + self.vlc_media_player.set_time(0) + self.vlc_media_player.play() + if not self.media_state_wait(vlc.State.Playing): + log.error('Could not start playing dvd, needed to get track info') + return + self.vlc_media_player.audio_set_mute(True) + # Sleep 1 second to make sure VLC has the needed metadata + sleep(1) + self.vlc_media_player.set_time(0) + # Get audio tracks, insert in combobox + audio_tracks = self.vlc_media_player.audio_get_track_description() + self.audio_tracks_combobox.clear() + for audio_track in audio_tracks: + self.audio_tracks_combobox.addItem(audio_track[1].decode(), audio_track[0]) + # Enable audio track combobox if anything is in it + if len(audio_tracks) > 0: + self.audio_tracks_combobox.setDisabled(False) + # First track is "deactivated", so set to next if it exists + if len(audio_tracks) > 1: + self.audio_tracks_combobox.setCurrentIndex(1) + # Get subtitle tracks, insert in combobox + subtitles_tracks = self.vlc_media_player.video_get_spu_description() + self.subtitle_tracks_combobox.clear() + for subtitle_track in subtitles_tracks: + self.subtitle_tracks_combobox.addItem(subtitle_track[1].decode(), subtitle_track[0]) + # Enable subtitle track combobox is anything in it + if len(subtitles_tracks) > 0: + self.subtitle_tracks_combobox.setDisabled(False) + self.vlc_media_player.audio_set_mute(False) + self.vlc_media_player.set_pause(1) + # If a title or audio track is available the player is enabled + if self.titles_combo_box.count() > 0 or len(audio_tracks) > 0: + self.toggle_disable_player(False) + # Set media length info + self.playback_length = self.vlc_media_player.get_length() + log.debug('playback_length: %d ms' % self.playback_length) + self.position_slider.setMaximum(self.playback_length) + # setup start and end time + rounded_vlc_ms_length = int(round(self.playback_length / 100.0) * 100.0) + time = QtCore.QTime() + playback_length_time = time.addMSecs(rounded_vlc_ms_length) + self.start_position_edit.setMaximumTime(playback_length_time) + self.end_timeedit.setMaximumTime(playback_length_time) + self.end_timeedit.setTime(playback_length_time) + # Pause once again, just to make sure + loop_count = 0 + while self.vlc_media_player.get_state() == vlc.State.Playing and loop_count < 20: + sleep(0.1) + self.vlc_media_player.set_pause(1) + loop_count += 1 + log.debug('titles_combo_box end - vlc_media_player state: %s' % self.vlc_media_player.get_state()) + + @QtCore.pyqtSlot(int) + def on_audio_tracks_combobox_currentIndexChanged(self, index): + """ + When a new audio track is chosen update audio track bing played by VLC + + :param index: The index of the newly chosen audio track. + """ + if not self.vlc_media_player: + return + audio_track = self.audio_tracks_combobox.itemData(index) + log.debug('in on_audio_tracks_combobox_currentIndexChanged, index: %d audio_track: %s' % (index, audio_track)) + if audio_track and int(audio_track) > 0: + self.vlc_media_player.audio_set_track(int(audio_track)) + + @QtCore.pyqtSlot(int) + def on_subtitle_tracks_combobox_currentIndexChanged(self, index): + """ + When a new subtitle track is chosen update subtitle track bing played by VLC + + :param index: The index of the newly chosen subtitle. + """ + if not self.vlc_media_player: + return + subtitle_track = self.subtitle_tracks_combobox.itemData(index) + if subtitle_track: + self.vlc_media_player.video_set_spu(int(subtitle_track)) + + def on_position_slider_sliderMoved(self, position): + """ + Set player position according to new slider position. + + :param position: Position to seek to. + """ + self.vlc_media_player.set_time(position) + + def update_position(self): + """ + Update slider position and displayed time according to VLC player position. + """ + if self.vlc_media_player: + vlc_ms_pos = self.vlc_media_player.get_time() + rounded_vlc_ms_pos = int(round(vlc_ms_pos / 100.0) * 100.0) + time = QtCore.QTime() + new_pos_time = time.addMSecs(rounded_vlc_ms_pos) + self.position_timeedit.setTime(new_pos_time) + self.position_slider.setSliderPosition(vlc_ms_pos) + + def disable_all(self): + """ + Disable all elements in the dialog + """ + self.toggle_disable_load_media(True) + self.titles_combo_box.setDisabled(True) + self.audio_tracks_combobox.setDisabled(True) + self.subtitle_tracks_combobox.setDisabled(True) + self.toggle_disable_player(True) + + def toggle_disable_load_media(self, action): + """ + Enable/disable load media combobox and button. + + :param action: If True elements are disabled, if False they are enabled. + """ + self.media_path_combobox.setDisabled(action) + self.load_disc_button.setDisabled(action) + + def toggle_disable_player(self, action): + """ + Enable/disable player elements. + + :param action: If True elements are disabled, if False they are enabled. + """ + self.play_button.setDisabled(action) + self.position_slider.setDisabled(action) + self.position_timeedit.setDisabled(action) + self.start_position_edit.setDisabled(action) + self.set_start_button.setDisabled(action) + self.jump_start_button.setDisabled(action) + self.end_timeedit.setDisabled(action) + self.set_end_button.setDisabled(action) + self.jump_end_button.setDisabled(action) + self.save_button.setDisabled(action) + + def accept(self): + """ + Saves the current media and trackinfo as a clip to the mediamanager + """ + log.debug('in on_save_button_clicked') + start_time = self.start_position_edit.time() + start_time_ms = start_time.hour() * 60 * 60 * 1000 + \ + start_time.minute() * 60 * 1000 + \ + start_time.second() * 1000 + \ + start_time.msec() + end_time = self.end_timeedit.time() + end_time_ms = end_time.hour() * 60 * 60 * 1000 + \ + end_time.minute() * 60 * 1000 + \ + end_time.second() * 1000 + \ + end_time.msec() + title = self.titles_combo_box.itemData(self.titles_combo_box.currentIndex()) + path = self.media_path_combobox.currentText() + optical = '' + if self.audio_cd: + optical = 'optical:%d:-1:-1:%d:%d:' % (title, start_time_ms, end_time_ms) + else: + audio_track = self.audio_tracks_combobox.itemData(self.audio_tracks_combobox.currentIndex()) + subtitle_track = self.subtitle_tracks_combobox.itemData(self.subtitle_tracks_combobox.currentIndex()) + optical = 'optical:%d:%d:%d:%d:%d:' % (title, audio_track, subtitle_track, start_time_ms, end_time_ms) + # Ask for an alternative name for the mediaclip + while True: + new_optical_name, ok = QtGui.QInputDialog.getText(self, translate('MediaPlugin.MediaClipSelectorForm', + 'Set name of mediaclip'), + translate('MediaPlugin.MediaClipSelectorForm', + 'Name of mediaclip:'), + QtGui.QLineEdit.Normal) + # User pressed cancel, don't save the clip + if not ok: + return + # User pressed ok, but the input text is blank + if not new_optical_name: + critical_error_message_box(translate('MediaPlugin.MediaClipSelectorForm', + 'Enter a valid name or cancel'), + translate('MediaPlugin.MediaClipSelectorForm', + 'Enter a valid name or cancel')) + # The entered new name contains a colon, which we don't allow because colons is used to seperate clip info + elif new_optical_name.find(':') >= 0: + critical_error_message_box(translate('MediaPlugin.MediaClipSelectorForm', 'Invalid character'), + translate('MediaPlugin.MediaClipSelectorForm', + 'The name of the mediaclip must not contain the character ":"')) + # New name entered and we use it + else: + break + # Append the new name to the optical string and the path + optical += new_optical_name + ':' + path + self.media_item.add_optical_clip(optical) + + def media_state_wait(self, media_state): + """ + Wait for the video to change its state + Wait no longer than 15 seconds. (loading an optical disc takes some time) + + :param media_state: VLC media state to wait for. + :return: True if state was reached within 15 seconds, False if not or error occurred. + """ + start = datetime.now() + while media_state != self.vlc_media_player.get_state(): + if self.vlc_media_player.get_state() == vlc.State.Error: + return False + if (datetime.now() - start).seconds > 30: + return False + return True + + def find_optical_devices(self): + """ + Attempt to autodetect optical devices on the computer, and add them to the media-dropdown + :return: + """ + # Clear list first + self.media_path_combobox.clear() + if os.name == 'nt': + # use win api to find optical drives + fso = Dispatch('scripting.filesystemobject') + for drive in fso.Drives: + log.debug('Drive %s has type %d' % (drive.DriveLetter, drive.DriveType)) + # if type is 4, it is a cd-rom drive + if drive.DriveType == 4: + self.media_path_combobox.addItem('%s:\\' % drive.DriveLetter) + elif sys.platform.startswith('linux'): + # Get disc devices from dbus and find the ones that are optical + bus = dbus.SystemBus() + try: + udev_manager_obj = bus.get_object('org.freedesktop.UDisks', '/org/freedesktop/UDisks') + udev_manager = dbus.Interface(udev_manager_obj, 'org.freedesktop.UDisks') + for dev in udev_manager.EnumerateDevices(): + device_obj = bus.get_object("org.freedesktop.UDisks", dev) + device_props = dbus.Interface(device_obj, dbus.PROPERTIES_IFACE) + if device_props.Get('org.freedesktop.UDisks.Device', 'DeviceIsDrive'): + drive_props = device_props.Get('org.freedesktop.UDisks.Device', 'DriveMediaCompatibility') + if any('optical' in prop for prop in drive_props): + self.media_path_combobox.addItem(device_props.Get('org.freedesktop.UDisks.Device', + 'DeviceFile')) + return + except dbus.exceptions.DBusException: + log.debug('could not use udisks, will try udisks2') + udev_manager_obj = bus.get_object('org.freedesktop.UDisks2', '/org/freedesktop/UDisks2') + udev_manager = dbus.Interface(udev_manager_obj, 'org.freedesktop.DBus.ObjectManager') + for k, v in udev_manager.GetManagedObjects().items(): + drive_info = v.get('org.freedesktop.UDisks2.Drive', {}) + drive_props = drive_info.get('MediaCompatibility') + if drive_props and any('optical' in prop for prop in drive_props): + for device in udev_manager.GetManagedObjects().values(): + if dbus.String('org.freedesktop.UDisks2.Block') in device: + if device[dbus.String('org.freedesktop.UDisks2.Block')][dbus.String('Drive')] == k: + block_file = '' + for c in device[dbus.String('org.freedesktop.UDisks2.Block')][ + dbus.String('PreferredDevice')]: + if chr(c) != '\x00': + block_file += chr(c) + self.media_path_combobox.addItem(block_file) + elif sys.platform.startswith('darwin'): + # Look for DVD folders in devices to find optical devices + volumes = os.listdir('/Volumes') + candidates = list() + for volume in volumes: + if volume.startswith('.'): + continue + dirs = os.listdir('/Volumes/' + volume) + # Detect DVD + if 'VIDEO_TS' in dirs: + self.media_path_combobox.addItem('/Volumes/' + volume) + # Detect audio cd + files = [f for f in dirs if os.path.isfile(f)] + for file in files: + if file.endswith('aiff'): + self.media_path_combobox.addItem('/Volumes/' + volume) + break diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 0780b175d..679180c15 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -29,6 +29,7 @@ import logging import os +from datetime import time from PyQt4 import QtCore, QtGui @@ -38,17 +39,21 @@ from openlp.core.lib import ItemCapabilities, MediaManagerItem, MediaType, Servi build_icon, check_item_selected from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box from openlp.core.ui import DisplayController, Display, DisplayControllerType -from openlp.core.ui.media import get_media_players, set_media_players +from openlp.core.ui.media import get_media_players, set_media_players, parse_optical_path, format_milliseconds from openlp.core.utils import get_locale_key +from openlp.core.ui.media.vlcplayer import VLC_AVAILABLE +if VLC_AVAILABLE: + from openlp.plugins.media.forms.mediaclipselectorform import MediaClipSelectorForm log = logging.getLogger(__name__) CLAPPERBOARD = ':/media/slidecontroller_multimedia.png' +OPTICAL = ':/media/media_optical.png' VIDEO_ICON = build_icon(':/media/media_video.png') AUDIO_ICON = build_icon(':/media/media_audio.png') -DVD_ICON = build_icon(':/media/media_video.png') +OPTICAL_ICON = build_icon(OPTICAL) ERROR_ICON = build_icon(':/general/general_delete.png') @@ -88,6 +93,10 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): self.list_view.activateDnD() def retranslateUi(self): + """ + This method is called automatically to provide OpenLP with the opportunity to translate the ``MediaManagerItem`` + to another language. + """ self.on_new_prompt = translate('MediaPlugin.MediaItem', 'Select Media') self.replace_action.setText(UiStrings().ReplaceBG) self.replace_action.setToolTip(UiStrings().ReplaceLiveBG) @@ -106,10 +115,35 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): self.has_edit_icon = False def add_list_view_to_toolbar(self): + """ + Creates the main widget for listing items. + """ MediaManagerItem.add_list_view_to_toolbar(self) self.list_view.addAction(self.replace_action) + def add_start_header_bar(self): + """ + Adds buttons to the start of the header bar. + """ + if 'vlc' in get_media_players()[0]: + diable_optical_button_text = False + optical_button_text = translate('MediaPlugin.MediaItem', 'Load CD/DVD') + optical_button_tooltip = translate('MediaPlugin.MediaItem', 'Load CD/DVD') + else: + diable_optical_button_text = True + optical_button_text = translate('MediaPlugin.MediaItem', 'Load CD/DVD') + optical_button_tooltip = translate('MediaPlugin.MediaItem', + 'Load CD/DVD - only supported when VLC is installed and enabled') + self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=OPTICAL_ICON, text=optical_button_text, + tooltip=optical_button_tooltip, + triggers=self.on_load_optical) + if diable_optical_button_text: + self.load_optical.setDisabled(True) + def add_end_header_bar(self): + """ + Adds buttons to the end of the header bar. + """ # Replace backgrounds do not work at present so remove functionality. self.replace_action = self.toolbar.add_toolbar_action('replace_action', icon=':/slides/slide_blank.png', triggers=self.on_replace_click) @@ -198,22 +232,42 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): if item is None: return False filename = item.data(QtCore.Qt.UserRole) - if not os.path.exists(filename): - if not remote: - # File is no longer present - critical_error_message_box( - translate('MediaPlugin.MediaItem', 'Missing Media File'), - translate('MediaPlugin.MediaItem', 'The file %s no longer exists.') % filename) - return False - (path, name) = os.path.split(filename) - service_item.title = name - service_item.processor = self.display_type_combo_box.currentText() - service_item.add_from_command(path, name, CLAPPERBOARD) - # Only get start and end times if going to a service - if context == ServiceItemContext.Service: - # Start media and obtain the length - if not self.media_controller.media_length(service_item): + # Special handling if the filename is a optical clip + if filename.startswith('optical:'): + (name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(filename) + if not os.path.exists(name): + if not remote: + # Optical disc is no longer present + critical_error_message_box( + translate('MediaPlugin.MediaItem', 'Missing Media File'), + translate('MediaPlugin.MediaItem', 'The optical disc %s is no longer available.') % name) return False + service_item.processor = self.display_type_combo_box.currentText() + service_item.add_from_command(filename, name, CLAPPERBOARD) + service_item.title = clip_name + # Set the length + self.media_controller.media_setup_optical(name, title, audio_track, subtitle_track, start, end, None, None) + service_item.set_media_length((end - start) / 1000) + service_item.start_time = start / 1000 + service_item.end_time = end / 1000 + service_item.add_capability(ItemCapabilities.IsOptical) + else: + if not os.path.exists(filename): + if not remote: + # File is no longer present + critical_error_message_box( + translate('MediaPlugin.MediaItem', 'Missing Media File'), + translate('MediaPlugin.MediaItem', 'The file %s no longer exists.') % filename) + return False + (path, name) = os.path.split(filename) + service_item.title = name + service_item.processor = self.display_type_combo_box.currentText() + service_item.add_from_command(path, name, CLAPPERBOARD) + # Only get start and end times if going to a service + if context == ServiceItemContext.Service: + # Start media and obtain the length + if not self.media_controller.media_length(service_item): + return False service_item.add_capability(ItemCapabilities.CanAutoStartForLive) service_item.add_capability(ItemCapabilities.CanEditTitle) service_item.add_capability(ItemCapabilities.RequiresMedia) @@ -224,6 +278,9 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): return True def initialise(self): + """ + Initialize media item. + """ self.list_view.clear() self.list_view.setIconSize(QtCore.QSize(88, 50)) self.service_path = os.path.join(AppLocation.get_section_data_path(self.settings_section), 'thumbnails') @@ -241,6 +298,9 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): ' '.join(self.media_controller.audio_extensions_list), UiStrings().AllFiles) def display_setup(self): + """ + Setup media controller display. + """ self.media_controller.setup_display(self.display_controller.preview_display, False) def populate_display_types(self): @@ -280,7 +340,6 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): Settings().setValue(self.settings_section + '/media files', self.get_file_list()) def load_list(self, media, target_group=None): - # Sort the media by its filename considering language specific characters. """ Load the media list @@ -290,12 +349,22 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): media.sort(key=lambda file_name: get_locale_key(os.path.split(str(file_name))[1])) for track in media: track_info = QtCore.QFileInfo(track) - if not os.path.exists(track): + if track.startswith('optical:'): + # Handle optical based item + (file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track) + item_name = QtGui.QListWidgetItem(clip_name) + item_name.setIcon(OPTICAL_ICON) + item_name.setData(QtCore.Qt.UserRole, track) + item_name.setToolTip('%s@%s-%s' % (file_name, format_milliseconds(start), format_milliseconds(end))) + elif not os.path.exists(track): + # File doesn't exist, mark as error. file_name = os.path.split(str(track))[1] item_name = QtGui.QListWidgetItem(file_name) item_name.setIcon(ERROR_ICON) item_name.setData(QtCore.Qt.UserRole, track) + item_name.setToolTip(track) elif track_info.isFile(): + # Normal media file handling. file_name = os.path.split(str(track))[1] item_name = QtGui.QListWidgetItem(file_name) if '*.%s' % (file_name.split('.')[-1].lower()) in self.media_controller.audio_extensions_list: @@ -303,15 +372,16 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): else: item_name.setIcon(VIDEO_ICON) item_name.setData(QtCore.Qt.UserRole, track) - else: - file_name = os.path.split(str(track))[1] - item_name = QtGui.QListWidgetItem(file_name) - item_name.setIcon(build_icon(DVD_ICON)) - item_name.setData(QtCore.Qt.UserRole, track) - item_name.setToolTip(track) + item_name.setToolTip(track) self.list_view.addItem(item_name) def get_list(self, type=MediaType.Audio): + """ + Get the list of media, optional select media type. + + :param type: Type to get, defaults to audio. + :return: The media list + """ media = Settings().value(self.settings_section + '/media files') media.sort(key=lambda filename: get_locale_key(os.path.split(str(filename))[1])) if type == MediaType.Audio: @@ -323,6 +393,13 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): return media def search(self, string, show_error): + """ + Performs a search for items containing ``string`` + + :param string: String to be displayed + :param show_error: Should the error be shown (True) + :return: The search result. + """ files = Settings().value(self.settings_section + '/media files') results = [] string = string.lower() @@ -331,3 +408,32 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): if filename.lower().find(string) > -1: results.append([file, filename]) return results + + def on_load_optical(self): + """ + When the load optical button is clicked, open the clip selector window. + """ + # self.media_clip_selector_form.exec_() + if VLC_AVAILABLE: + media_clip_selector_form = MediaClipSelectorForm(self, self.main_window, None) + media_clip_selector_form.exec_() + del media_clip_selector_form + else: + QtGui.QMessageBox.critical(self, 'VLC is not available', 'VLC is not available') + + def add_optical_clip(self, optical): + """ + Add a optical based clip to the mediamanager, called from media_clip_selector_form. + + :param optical: The clip to add. + """ + full_list = self.get_file_list() + # If the clip already is in the media list it isn't added and an error message is displayed. + if optical in full_list: + critical_error_message_box(translate('MediaPlugin.MediaItem', 'Mediaclip already saved'), + translate('MediaPlugin.MediaItem', 'This mediaclip has already been saved')) + return + # Append the optical string to the media list + full_list.append(optical) + self.load_list([optical]) + Settings().setValue(self.settings_section + '/media files', self.get_file_list()) diff --git a/openlp/plugins/songs/forms/songexportform.py b/openlp/plugins/songs/forms/songexportform.py index 589da4d33..412ec0369 100644 --- a/openlp/plugins/songs/forms/songexportform.py +++ b/openlp/plugins/songs/forms/songexportform.py @@ -124,7 +124,7 @@ class SongExportForm(OpenLPWizard): self.export_song_layout = QtGui.QHBoxLayout(self.export_song_page) self.export_song_layout.setObjectName('export_song_layout') self.grid_layout = QtGui.QGridLayout() - self.grid_layout.setObjectName('grid_layout') + self.grid_layout.setObjectName('range_layout') self.selected_list_widget = QtGui.QListWidget(self.export_song_page) self.selected_list_widget.setObjectName('selected_list_widget') self.grid_layout.addWidget(self.selected_list_widget, 1, 0, 1, 1) diff --git a/resources/forms/mediaclipselector.ui b/resources/forms/mediaclipselector.ui new file mode 100644 index 000000000..b3f22e678 --- /dev/null +++ b/resources/forms/mediaclipselector.ui @@ -0,0 +1,336 @@ + + + MediaClipSelector + + + + 0 + 0 + 683 + 739 + + + + + 0 + 0 + + + + + 683 + 686 + + + + Qt::NoFocus + + + Select media clip + + + false + + + Qt::ImhNone + + + + + 0 + 0 + + + + + + + true + + + + 0 + 0 + + + + true + + + + + + + true + + + HH:mm:ss.z + + + + + + + true + + + HH:mm:ss.z + + + + + + + true + + + Set current position as start point + + + + + + + true + + + Load disc + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 40 + + + + + + + + true + + + + + + + ../images/media_playback_start.png../images/media_playback_start.png + + + + + + + true + + + End point + + + + + + + true + + + + + + + true + + + Title + + + + + + + true + + + + + + + true + + + Set current position as end point + + + + + + + true + + + Save current clip + + + + + + + true + + + Close + + + + + + + true + + + Start point + + + + + + + true + + + Jump to start point + + + + + + + true + + + Audio track + + + + + + + true + + + true + + + HH:mm:ss.z + + + + + + + + 665 + 375 + + + + background-color:black; + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + true + + + Subtitle track + + + + + + + true + + + Jump to end point + + + + + + + true + + + Media path + + + + + + + true + + + + + + + + + + true + + + false + + + Qt::Horizontal + + + false + + + + + + + + media_path_combobox + load_disc_pushbutton + title_combo_box + audio_tracks_combobox + subtitle_tracks_combobox + play_pushbutton + position_horizontalslider + media_position_timeedit + start_timeedit + set_start_pushbutton + jump_start_pushbutton + end_timeedit + set_end_pushbutton + jump_end_pushbutton + save_pushbutton + close_pushbutton + + + + diff --git a/resources/images/media_optical.png b/resources/images/media_optical.png new file mode 100644 index 000000000..033b98472 Binary files /dev/null and b/resources/images/media_optical.png differ diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index 79036f08f..d61166acc 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -139,6 +139,7 @@ media_stop.png media_audio.png media_video.png + media_optical.png slidecontroller_multimedia.png auto-start_active.png auto-start_inactive.png diff --git a/tests/functional/openlp_core_lib/test_serviceitem.py b/tests/functional/openlp_core_lib/test_serviceitem.py index d75e94040..4fd6f6b83 100644 --- a/tests/functional/openlp_core_lib/test_serviceitem.py +++ b/tests/functional/openlp_core_lib/test_serviceitem.py @@ -206,3 +206,24 @@ class TestServiceItem(TestCase): 'This service item should be able to be run in a can be made to Loop') self.assertTrue(service_item.is_capable(ItemCapabilities.CanAppend), 'This service item should be able to have new items added to it') + + def service_item_load_optical_media_from_service_test(self): + """ + Test the Service Item - load an optical media item + """ + # GIVEN: A new service item and a mocked add icon function + service_item = ServiceItem(None) + service_item.add_icon = MagicMock() + + # WHEN: We load a serviceitem with optical media + line = convert_file_service_item(TEST_PATH, 'serviceitem-dvd.osj') + with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists: + mocked_exists.return_value = True + service_item.set_from_service(line) + + # THEN: We should get back a valid service item with optical media info + self.assertTrue(service_item.is_valid, 'The service item should be valid') + self.assertTrue(service_item.is_capable(ItemCapabilities.IsOptical), 'The item should be Optical') + self.assertEqual(service_item.start_time, 654.375, 'Start time should be 654.375') + self.assertEqual(service_item.end_time, 672.069, 'End time should be 672.069') + self.assertEqual(service_item.media_length, 17.694, 'Media length should be 17.694') diff --git a/tests/functional/openlp_core_ui/test_media.py b/tests/functional/openlp_core_ui/test_media.py index 4c6fa7f86..fbb5787c0 100644 --- a/tests/functional/openlp_core_ui/test_media.py +++ b/tests/functional/openlp_core_ui/test_media.py @@ -32,7 +32,7 @@ Package to test the openlp.core.ui package. from PyQt4 import QtCore from unittest import TestCase -from openlp.core.ui.media import get_media_players +from openlp.core.ui.media import get_media_players, parse_optical_path from tests.functional import MagicMock, patch from tests.helpers.testmixin import TestMixin @@ -126,3 +126,59 @@ class TestMedia(TestCase, TestMixin): # THEN: the used_players should be an empty list, and the overridden player should be an empty string self.assertEqual(['vlc', 'webkit', 'phonon'], used_players, 'Used players should be correct') self.assertEqual('vlc,webkit,phonon', overridden_player, 'Overridden player should be a string of players') + + def test_parse_optical_path_linux(self): + """ + Test that test_parse_optical_path() parses a optical path with linux device path correctly + """ + + # GIVEN: An optical formatted path + org_title_track = 1 + org_audio_track = 2 + org_subtitle_track = -1 + org_start = 1234 + org_end = 4321 + org_name = 'test name' + org_device_path = '/dev/dvd' + path = 'optical:%d:%d:%d:%d:%d:%s:%s' % (org_title_track, org_audio_track, org_subtitle_track, + org_start, org_end, org_name, org_device_path) + + # WHEN: parsing the path + (device_path, title_track, audio_track, subtitle_track, start, end, name) = parse_optical_path(path) + + # THEN: The return values should match the original values + self.assertEqual(org_title_track, title_track, 'Returned title_track should match the original') + self.assertEqual(org_audio_track, audio_track, 'Returned audio_track should match the original') + self.assertEqual(org_subtitle_track, subtitle_track, 'Returned subtitle_track should match the original') + self.assertEqual(org_start, start, 'Returned start should match the original') + self.assertEqual(org_end, end, 'Returned end should match the original') + self.assertEqual(org_name, name, 'Returned end should match the original') + self.assertEqual(org_device_path, device_path, 'Returned device_path should match the original') + + def test_parse_optical_path_win(self): + """ + Test that test_parse_optical_path() parses a optical path with windows device path correctly + """ + + # GIVEN: An optical formatted path + org_title_track = 1 + org_audio_track = 2 + org_subtitle_track = -1 + org_start = 1234 + org_end = 4321 + org_name = 'test name' + org_device_path = 'D:' + path = 'optical:%d:%d:%d:%d:%d:%s:%s' % (org_title_track, org_audio_track, org_subtitle_track, + org_start, org_end, org_name, org_device_path) + + # WHEN: parsing the path + (device_path, title_track, audio_track, subtitle_track, start, end, name) = parse_optical_path(path) + + # THEN: The return values should match the original values + self.assertEqual(org_title_track, title_track, 'Returned title_track should match the original') + self.assertEqual(org_audio_track, audio_track, 'Returned audio_track should match the original') + self.assertEqual(org_subtitle_track, subtitle_track, 'Returned subtitle_track should match the original') + self.assertEqual(org_start, start, 'Returned start should match the original') + self.assertEqual(org_end, end, 'Returned end should match the original') + self.assertEqual(org_name, name, 'Returned end should match the original') + self.assertEqual(org_device_path, device_path, 'Returned device_path should match the original') diff --git a/tests/interfaces/openlp_plugins/media/__init__.py b/tests/interfaces/openlp_plugins/media/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py new file mode 100644 index 000000000..e97a06238 --- /dev/null +++ b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Module to test the MediaClipSelectorForm. +""" + +import os +from unittest import TestCase, SkipTest +from openlp.core.ui.media.vlcplayer import VLC_AVAILABLE + +if os.name == 'nt' and not VLC_AVAILABLE: + raise SkipTest('Windows without VLC, skipping this test since it cannot run without vlc') + +from PyQt4 import QtGui, QtTest, QtCore + +from openlp.core.common import Registry +from openlp.plugins.media.forms.mediaclipselectorform import MediaClipSelectorForm +from tests.interfaces import MagicMock, patch +from tests.helpers.testmixin import TestMixin + + +class TestMediaClipSelectorForm(TestCase, TestMixin): + """ + Test the EditCustomSlideForm. + """ + def setUp(self): + """ + Create the UI + """ + Registry.create() + self.get_application() + self.main_window = QtGui.QMainWindow() + Registry().register('main_window', self.main_window) + # Mock VLC so we don't actually use it + self.vlc_patcher = patch('openlp.plugins.media.forms.mediaclipselectorform.vlc') + self.vlc_patcher.start() + # Mock the media item + self.mock_media_item = MagicMock() + # create form to test + self.form = MediaClipSelectorForm(self.mock_media_item, self.main_window, None) + mock_media_state_wait = MagicMock() + mock_media_state_wait.return_value = True + self.form.media_state_wait = mock_media_state_wait + + def tearDown(self): + """ + Delete all the C++ objects at the end so that we don't have a segfault + """ + self.vlc_patcher.stop() + del self.form + del self.main_window + + def basic_test(self): + """ + Test if the dialog is correctly set up. + """ + # GIVEN: A mocked QDialog.exec_() method + with patch('PyQt4.QtGui.QDialog.exec_') as mocked_exec: + # WHEN: Show the dialog. + self.form.exec_() + + # THEN: The media path should be empty. + assert self.form.media_path_combobox.currentText() == '', 'There should not be any text in the media path.' + + def click_load_button_test(self): + """ + Test that the correct function is called when load is clicked, and that it behaves as expected. + """ + # GIVEN: Mocked methods. + with patch('openlp.plugins.media.forms.mediaclipselectorform.critical_error_message_box') as \ + mocked_critical_error_message_box,\ + patch('openlp.plugins.media.forms.mediaclipselectorform.os.path.exists') as mocked_os_path_exists,\ + patch('PyQt4.QtGui.QDialog.exec_') as mocked_exec: + self.form.exec_() + + # WHEN: The load button is clicked with no path set + QtTest.QTest.mouseClick(self.form.load_disc_button, QtCore.Qt.LeftButton) + + # THEN: we should get an error + mocked_critical_error_message_box.assert_called_with(message='No path was given') + + # WHEN: The load button is clicked with a non-existing path + mocked_os_path_exists.return_value = False + self.form.media_path_combobox.insertItem(0, '/non-existing/test-path.test') + self.form.media_path_combobox.setCurrentIndex(0) + QtTest.QTest.mouseClick(self.form.load_disc_button, QtCore.Qt.LeftButton) + + # THEN: we should get an error + assert self.form.media_path_combobox.currentText() == '/non-existing/test-path.test',\ + 'The media path should be the given one.' + mocked_critical_error_message_box.assert_called_with(message='Given path does not exists') + + # WHEN: The load button is clicked with a mocked existing path + mocked_os_path_exists.return_value = True + self.form.vlc_media_player = MagicMock() + self.form.vlc_media_player.play.return_value = -1 + self.form.media_path_combobox.insertItem(0, '/existing/test-path.test') + self.form.media_path_combobox.setCurrentIndex(0) + QtTest.QTest.mouseClick(self.form.load_disc_button, QtCore.Qt.LeftButton) + + # THEN: we should get an error + assert self.form.media_path_combobox.currentText() == '/existing/test-path.test',\ + 'The media path should be the given one.' + mocked_critical_error_message_box.assert_called_with(message='VLC player failed playing the media') + + def title_combobox_test(self): + """ + Test the behavior when the title combobox is updated + """ + # GIVEN: Mocked methods and some entries in the title combobox. + with patch('PyQt4.QtGui.QDialog.exec_') as mocked_exec: + self.form.exec_() + self.form.vlc_media_player.get_length.return_value = 1000 + self.form.audio_tracks_combobox.itemData = MagicMock() + self.form.subtitle_tracks_combobox.itemData = MagicMock() + self.form.audio_tracks_combobox.itemData.return_value = None + self.form.subtitle_tracks_combobox.itemData.return_value = None + self.form.titles_combo_box.insertItem(0, 'Test Title 0') + self.form.titles_combo_box.insertItem(1, 'Test Title 1') + + # WHEN: There exists audio and subtitle tracks and the index is updated. + self.form.vlc_media_player.audio_get_track_description.return_value = [(-1, b'Disabled'), + (0, b'Audio Track 1')] + self.form.vlc_media_player.video_get_spu_description.return_value = [(-1, b'Disabled'), + (0, b'Subtitle Track 1')] + self.form.titles_combo_box.setCurrentIndex(1) + + # THEN: The subtitle and audio track comboboxes should be updated and get signals and call itemData. + self.form.audio_tracks_combobox.itemData.assert_any_call(0) + self.form.audio_tracks_combobox.itemData.assert_any_call(1) + self.form.subtitle_tracks_combobox.itemData.assert_any_call(0) diff --git a/tests/resources/serviceitem-dvd.osj b/tests/resources/serviceitem-dvd.osj new file mode 100644 index 000000000..997dbd539 --- /dev/null +++ b/tests/resources/serviceitem-dvd.osj @@ -0,0 +1 @@ +[{"serviceitem": {"header": {"auto_play_slides_once": false, "data": "", "processor": "Automatic", "theme": -1, "theme_overwritten": false, "end_time": 672.069, "start_time": 654.375, "capabilities": [12, 18, 16, 4], "media_length": 17.694, "audit": "", "xml_version": null, "title": "First DVD Clip", "auto_play_slides_loop": false, "notes": "", "icon": ":/plugins/plugin_media.png", "type": 3, "background_audio": [], "plugin": "media", "from_plugin": false, "search": "", "will_auto_start": false, "name": "media", "footer": [], "timed_slide_interval": 0}, "data": [{"image": ":/media/slidecontroller_multimedia.png", "path": "optical:1:5:3:654375:672069:First DVD Clip:/dev/sr0", "title": "/dev/sr0"}]}}]