diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index b178b20b2..0bd394373 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -234,7 +234,6 @@ class Settings(QtCore.QSettings): 'core/click live slide to unblank': False, 'core/blank warning': False, 'core/ccli number': '', - 'advanced/experimental': False, 'core/has run wizard': False, 'core/language': '[en]', 'core/last version test': '', @@ -269,10 +268,7 @@ class Settings(QtCore.QSettings): 'media/media files': [], 'media/last directory': None, 'media/media auto start': QtCore.Qt.Unchecked, - 'media/stream command': '', 'media/vlc arguments': '', - 'media/video': '', - 'media/audio': '', 'media/live volume': 50, 'media/preview volume': 0, 'remotes/download version': '0.0', diff --git a/openlp/core/display/html/display.js b/openlp/core/display/html/display.js index 86e0a288a..fd10d4e55 100644 --- a/openlp/core/display/html/display.js +++ b/openlp/core/display/html/display.js @@ -1001,6 +1001,7 @@ var Display = { console.warn(backgroundStyle["background-image"]); break; case BackgroundType.Video: + // never actually used since background type is overridden from video to transparent in window.py backgroundStyle["background-color"] = theme.background_border_color; backgroundHtml = ""; console.warn(backgroundHtml); diff --git a/openlp/core/display/render.py b/openlp/core/display/render.py index 4c107f439..3b4fe564b 100644 --- a/openlp/core/display/render.py +++ b/openlp/core/display/render.py @@ -32,6 +32,7 @@ import time from PyQt5 import QtWidgets, QtGui from openlp.core.common import ThemeLevel +from openlp.core.common.enum import ServiceItemType from openlp.core.common.i18n import translate from openlp.core.common.mixins import LogMixin from openlp.core.common.registry import Registry, RegistryBase @@ -566,7 +567,7 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow): # save value for use in format_slide self.force_page = force_page if not self.force_page: - self.set_theme(theme_data, is_sync=True) + self.set_theme(theme_data, is_sync=True, service_item_type=ServiceItemType.Text) slides = self.format_slide(VERSE, None) verses = dict() verses['title'] = TITLE diff --git a/openlp/core/display/window.py b/openlp/core/display/window.py index 02fcfdcbc..fc64f475c 100644 --- a/openlp/core/display/window.py +++ b/openlp/core/display/window.py @@ -384,11 +384,16 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties): if theme.background_type == 'video' or theme.background_type == 'stream': theme_copy.background_type = 'transparent' else: - # If review Display for media so we need to display black box. - if theme.background_type == 'stream': - theme_copy.background_type = 'transparent' - elif service_item_type == ServiceItemType.Command or theme.background_type == 'video' or \ - theme.background_type == 'live': + # If preview Display with media background we just show the background color, no media + if theme.background_type == 'stream' or theme.background_type == 'video': + theme_copy.background_type = 'solid' + theme_copy.background_start_color = theme.background_border_color + theme_copy.background_end_color = theme.background_border_color + theme_copy.background_main_color = theme.background_border_color + theme_copy.background_footer_color = theme.background_border_color + theme_copy.background_color = theme.background_border_color + # If preview Display for media so we need to display black box. + elif service_item_type == ServiceItemType.Command or theme.background_type == 'live': theme_copy.background_type = 'solid' theme_copy.background_start_color = '#590909' theme_copy.background_end_color = '#590909' diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 2bac98530..892680409 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -163,6 +163,9 @@ class ItemCapabilities(object): ``HasBackgroundVideo`` That a video file is present with the text + + ``HasBackgroundStream`` + That a video stream is present with the text """ CanPreview = 1 CanEdit = 2 @@ -188,6 +191,7 @@ class ItemCapabilities(object): HasMetaData = 22 CanStream = 23 HasBackgroundVideo = 24 + HasBackgroundStream = 25 def get_text_file_string(text_file_path): diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index 66f516bbb..840d2da46 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -414,9 +414,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties): for index in range(self.list_view.count()): list_item = self.list_view.item(index) file_path = list_item.data(QtCore.Qt.UserRole) - # This is added as start of OpenLP each time - if file_path != UiStrings().LiveStream: - file_paths.append(file_path) + file_paths.append(file_path) return file_paths def load_list(self, load_list, target_group): diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index 042d7394a..42389be24 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -130,12 +130,17 @@ class ServiceItem(RegistryProperties): theme = theme_manager.get_theme_data(theme) # Clean up capabilities and reload from the theme. if self.is_text(): + # Cleanup capabilities if self.is_capable(ItemCapabilities.CanStream): self.remove_capability(ItemCapabilities.CanStream) if self.is_capable(ItemCapabilities.HasBackgroundVideo): self.remove_capability(ItemCapabilities.HasBackgroundVideo) + if self.is_capable(ItemCapabilities.HasBackgroundStream): + self.remove_capability(ItemCapabilities.HasBackgroundStream) + # Reload capabilities if theme.background_type == BackgroundType.to_string(BackgroundType.Stream): - self.add_capability(ItemCapabilities.CanStream) + self.add_capability(ItemCapabilities.HasBackgroundStream) + self.stream_mrl = theme.background_filename if theme.background_type == BackgroundType.to_string(BackgroundType.Video): self.video_file_name = theme.background_filename self.add_capability(ItemCapabilities.HasBackgroundVideo) @@ -663,7 +668,7 @@ class ServiceItem(RegistryProperties): def requires_media(self): return self.is_capable(ItemCapabilities.HasBackgroundAudio) or \ self.is_capable(ItemCapabilities.HasBackgroundVideo) or \ - self.is_capable(ItemCapabilities.CanStream) + self.is_capable(ItemCapabilities.HasBackgroundStream) def missing_frames(self): """ diff --git a/openlp/core/pages/background.py b/openlp/core/pages/background.py index a0b8b8ed6..42647f4a3 100644 --- a/openlp/core/pages/background.py +++ b/openlp/core/pages/background.py @@ -26,11 +26,17 @@ from PyQt5 import QtWidgets from openlp.core.common import get_images_filter from openlp.core.common.i18n import UiStrings, translate from openlp.core.lib.theme import BackgroundGradientType, BackgroundType +from openlp.core.lib.ui import critical_error_message_box from openlp.core.pages import GridLayoutPage +from openlp.core.ui.icons import UiIcons from openlp.core.ui.media import VIDEO_EXT from openlp.core.widgets.buttons import ColorButton from openlp.core.widgets.edits import PathEdit from openlp.core.widgets.labels import FormLabel +from openlp.core.ui.media.vlcplayer import get_vlc + +if get_vlc() is not None: + from openlp.plugins.media.forms.streamselectorform import StreamSelectorForm class BackgroundPage(GridLayoutPage): @@ -41,6 +47,7 @@ class BackgroundPage(GridLayoutPage): Gradient = 'gradient' Image = 'image' Video = 'video' + Stream = 'stream' def setup_ui(self): """ @@ -114,11 +121,36 @@ class BackgroundPage(GridLayoutPage): self.video_color_button.setObjectName('video_color_button') self.layout.addWidget(self.video_color_button, 7, 1) self.video_widgets = [self.video_label, self.video_path_edit, self.video_color_label, self.video_color_button] + # streams + self.stream_label = FormLabel(self) + self.stream_label.setObjectName('stream_label') + self.layout.addWidget(self.stream_label, 6, 0) + self.stream_layout = QtWidgets.QHBoxLayout() + self.stream_lineedit = QtWidgets.QLineEdit(self) + self.stream_lineedit.setObjectName('stream_lineedit') + self.stream_lineedit.setReadOnly(True) + self.stream_layout.addWidget(self.stream_lineedit) + # button to open select stream forms + self.stream_select_button = QtWidgets.QToolButton(self) + self.stream_select_button.setObjectName('stream_select_button') + self.stream_select_button.setIcon(UiIcons().device_stream) + self.stream_layout.addWidget(self.stream_select_button) + self.layout.addLayout(self.stream_layout, 6, 1, 1, 3) + self.stream_color_label = FormLabel(self) + self.stream_color_label.setObjectName('stream_color_label') + self.layout.addWidget(self.stream_color_label, 7, 0) + self.stream_color_button = ColorButton(self) + self.stream_color_button.color = '#000000' + self.stream_color_button.setObjectName('stream_color_button') + self.layout.addWidget(self.stream_color_button, 7, 1) + self.stream_widgets = [self.stream_label, self.stream_lineedit, self.stream_select_button, + self.stream_color_label, self.stream_color_button] # Force everything up self.layout_spacer = QtWidgets.QSpacerItem(1, 1) self.layout.addItem(self.layout_spacer, 8, 0, 1, 4) # Connect slots self.background_combo_box.currentIndexChanged.connect(self._on_background_type_index_changed) + self.stream_select_button.clicked.connect(self._on_stream_select_button_triggered) # Force the first set of widgets to show self._on_background_type_index_changed(0) @@ -134,7 +166,7 @@ class BackgroundPage(GridLayoutPage): self.background_combo_box.setItemText(BackgroundType.Transparent, translate('OpenLP.ThemeWizard', 'Transparent')) self.background_combo_box.setItemText(BackgroundType.Stream, - translate('OpenLP.ThemeWizard', 'Live Stream')) + translate('OpenLP.ThemeWizard', 'Live stream')) self.color_label.setText(translate('OpenLP.ThemeWizard', 'Color:')) self.gradient_start_label.setText(translate('OpenLP.ThemeWizard', 'Starting color:')) self.gradient_end_label.setText(translate('OpenLP.ThemeWizard', 'Ending color:')) @@ -153,6 +185,8 @@ class BackgroundPage(GridLayoutPage): self.image_label.setText('{text}:'.format(text=UiStrings().Image)) self.video_color_label.setText(translate('OpenLP.ThemeWizard', 'Background color:')) self.video_label.setText('{text}:'.format(text=UiStrings().Video)) + self.stream_color_label.setText(translate('OpenLP.ThemeWizard', 'Background color:')) + self.stream_label.setText('{text}:'.format(text=UiStrings().LiveStream)) self.image_path_edit.filters = \ '{name};;{text} (*)'.format(name=get_images_filter(), text=UiStrings().AllFiles) visible_formats = '(*.{name})'.format(name='; *.'.join(VIDEO_EXT)) @@ -165,7 +199,8 @@ class BackgroundPage(GridLayoutPage): """ Hide and show widgets based on index """ - widget_sets = [self.color_widgets, self.gradient_widgets, self.image_widgets, [], self.video_widgets] + widget_sets = [self.color_widgets, self.gradient_widgets, self.image_widgets, [], self.video_widgets, + self.stream_widgets] for widgets in widget_sets: for widget in widgets: widget.hide() @@ -173,6 +208,26 @@ class BackgroundPage(GridLayoutPage): for widget in widget_sets[index]: widget.show() + def _on_stream_select_button_triggered(self): + """ + Open the Stream selection form. + """ + if get_vlc(): + stream_selector_form = StreamSelectorForm(self, self.set_stream, True) + if self.stream_lineedit.text(): + stream_selector_form.set_mrl(self.stream_lineedit.text()) + stream_selector_form.exec() + del stream_selector_form + else: + critical_error_message_box(translate('MediaPlugin.MediaItem', 'VLC is not available'), + translate('MediaPlugin.MediaItem', 'Device streaming support requires VLC.')) + + def set_stream(self, stream_str): + """ + callback method used to get the stream mrl and options + """ + self.stream_lineedit.setText(stream_str) + @property def background_type(self): return BackgroundType.to_string(self.background_combo_box.currentIndex()) @@ -254,3 +309,19 @@ class BackgroundPage(GridLayoutPage): @video_path.setter def video_path(self, value): self.video_path_edit.path = value + + @property + def stream_color(self): + return self.stream_color_button.color + + @stream_color.setter + def stream_color(self, value): + self.stream_color_button.color = value + + @property + def stream_mrl(self): + return self.stream_lineedit.text() + + @stream_mrl.setter + def stream_mrl(self, value): + self.stream_lineedit.setText(value) diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index 96b272140..bf6abf71f 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -116,9 +116,6 @@ class AdvancedTab(SettingsTab): self.enable_auto_close_check_box = QtWidgets.QCheckBox(self.ui_group_box) self.enable_auto_close_check_box.setObjectName('enable_auto_close_check_box') self.ui_layout.addRow(self.enable_auto_close_check_box) - self.experimental_check_box = QtWidgets.QCheckBox(self.ui_group_box) - self.experimental_check_box.setObjectName('experimental_check_box') - self.ui_layout.addRow(self.experimental_check_box) self.left_layout.addWidget(self.ui_group_box) if HAS_DARK_STYLE: self.use_dark_style_checkbox = QtWidgets.QCheckBox(self.ui_group_box) @@ -293,8 +290,6 @@ class AdvancedTab(SettingsTab): 'Auto-scroll the next slide to bottom')) self.enable_auto_close_check_box.setText(translate('OpenLP.AdvancedTab', 'Enable application exit confirmation')) - self.experimental_check_box.setText(translate('OpenLP.GeneralTab', - 'Experimental features (use at your own risk)')) if HAS_DARK_STYLE: self.use_dark_style_checkbox.setText(translate('OpenLP.AdvancedTab', 'Use dark style (needs restart)')) self.service_name_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Default Service Name')) @@ -364,7 +359,6 @@ class AdvancedTab(SettingsTab): if self.autoscroll_map[i] == autoscroll_value and i < self.autoscroll_combo_box.count(): self.autoscroll_combo_box.setCurrentIndex(i) self.enable_auto_close_check_box.setChecked(settings.value('enable exit confirmation')) - self.experimental_check_box.setChecked(settings.value('experimental')) if HAS_DARK_STYLE: self.use_dark_style_checkbox.setChecked(settings.value('use_dark_style')) self.hide_mouse_check_box.setChecked(settings.value('hide mouse')) @@ -428,7 +422,6 @@ class AdvancedTab(SettingsTab): slide_max_height_value = self.slide_max_height_combo_box.itemData(slide_max_height_index) settings.setValue('slide max height', slide_max_height_value) settings.setValue('autoscrolling', self.autoscroll_map[self.autoscroll_combo_box.currentIndex()]) - settings.setValue('experimental', self.experimental_check_box.isChecked()) settings.setValue('enable exit confirmation', self.enable_auto_close_check_box.isChecked()) settings.setValue('hide mouse', self.hide_mouse_check_box.isChecked()) settings.setValue('alternate rows', self.alternate_rows_check_box.isChecked()) diff --git a/openlp/core/ui/icons.py b/openlp/core/ui/icons.py index 918ccf416..89939be78 100644 --- a/openlp/core/ui/icons.py +++ b/openlp/core/ui/icons.py @@ -64,7 +64,7 @@ class UiIcons(metaclass=Singleton): 'authentication': {'icon': 'fa.exclamation-triangle', 'attr': 'red'}, 'address': {'icon': 'fa.book'}, 'back': {'icon': 'fa.step-backward'}, - 'backspace': {'icon': 'mdi.backspace-outline'}, + 'backspace': {'icon': 'fa-times-circle'}, 'bible': {'icon': 'fa.book'}, 'blank': {'icon': 'fa.times-circle'}, 'blank_theme': {'icon': 'fa.file-image-o'}, @@ -82,6 +82,7 @@ class UiIcons(metaclass=Singleton): 'default': {'icon': 'fa.info-circle'}, 'desktop': {'icon': 'fa.desktop'}, 'delete': {'icon': 'fa.trash'}, + 'device_stream': {'icon': 'fa.video-camera'}, 'download': {'icon': 'fa.download'}, 'edit': {'icon': 'fa.edit'}, 'email': {'icon': 'fa.envelope'}, @@ -102,7 +103,7 @@ class UiIcons(metaclass=Singleton): 'new_group': {'icon': 'fa.folder'}, 'notes': {'icon': 'fa.sticky-note'}, 'open': {'icon': 'fa.folder-open'}, - 'optical': {'icon': 'fa.file-video-o'}, + 'optical': {'icon': 'fa.circle-thin'}, 'pause': {'icon': 'fa.pause'}, 'planning_center': {'icon': 'fa.cloud-download'}, 'play': {'icon': 'fa.play'}, @@ -181,9 +182,9 @@ class UiIcons(metaclass=Singleton): except Exception: import sys log.error('Unexpected error: %s' % sys.exc_info()) - setattr(self, key, qta.icon('fa.plus-circle', color='red')) + setattr(self, key, qta.icon('fa.exclamation-circle', color='red')) except Exception: - setattr(self, key, qta.icon('fa.plus-circle', color='red')) + setattr(self, key, qta.icon('fa.exclamation-circle', color='red')) @staticmethod def _print_icons(): diff --git a/openlp/core/ui/media/__init__.py b/openlp/core/ui/media/__init__.py index 297c2efba..795cf67ee 100644 --- a/openlp/core/ui/media/__init__.py +++ b/openlp/core/ui/media/__init__.py @@ -104,6 +104,22 @@ def parse_optical_path(input_string): return filename, title, audio_track, subtitle_track, start, end, clip_name +def parse_devicestream_path(input_string): + """ + Split the device stream path info. + + :param input_string: The string to parse + :return: The elements extracted from the string: streamname, MRL, VLC-options + """ + log.debug('parse_devicestream_path, about to parse: "{text}"'.format(text=input_string)) + # skip the header: 'devicestream:', split at '&&' + stream_info = input_string[len('devicestream:'):].split('&&') + name = stream_info[0] + mrl = stream_info[1] + options = stream_info[2] + return name, mrl, options + + def format_milliseconds(milliseconds): """ Format milliseconds into a human readable time string. diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 47d0ca11e..305e1ea21 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -41,7 +41,8 @@ from openlp.core.common.registry import Registry, RegistryBase from openlp.core.lib.serviceitem import ItemCapabilities from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui import DisplayControllerType -from openlp.core.ui.media import MediaState, ItemMediaInfo, MediaType, parse_optical_path, VIDEO_EXT, AUDIO_EXT +from openlp.core.ui.media import MediaState, ItemMediaInfo, MediaType, parse_optical_path, parse_devicestream_path, \ + VIDEO_EXT, AUDIO_EXT from openlp.core.ui.media.remote import register_views from openlp.core.ui.media.vlcplayer import VlcPlayer, get_vlc @@ -229,46 +230,55 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): if service_item.is_capable(ItemCapabilities.HasBackgroundAudio): controller.media_info.file_info = service_item.background_audio else: - if service_item.is_capable(ItemCapabilities.HasBackgroundVideo): + if service_item.is_capable(ItemCapabilities.HasBackgroundStream): + (name, mrl, options) = parse_devicestream_path(service_item.stream_mrl) + controller.media_info.file_info = (mrl, options) + controller.media_info.is_background = True + controller.media_info.media_type = MediaType.Stream + elif service_item.is_capable(ItemCapabilities.HasBackgroundVideo): controller.media_info.file_info = [service_item.video_file_name] service_item.media_length = self.media_length(path_to_str(service_item.video_file_name)) controller.media_info.is_looping_playback = True controller.media_info.is_background = True - elif service_item.is_capable(ItemCapabilities.CanStream): - controller.media_info.file_info = [] - controller.media_info.is_background = True else: controller.media_info.file_info = [service_item.get_frame_path()] display = self._define_display(controller) if controller.is_live: # if this is an optical device use special handling - if service_item.is_capable(ItemCapabilities.CanStream): - is_valid = self._check_file_type(controller, display, True) - controller.media_info.media_type = MediaType.Stream - elif service_item.is_capable(ItemCapabilities.IsOptical): + 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) + elif service_item.is_capable(ItemCapabilities.CanStream): + log.debug('video is stream and live') + path = service_item.get_frames()[0]['path'] + controller.media_info.media_type = MediaType.Stream + (name, mrl, options) = parse_devicestream_path(path) + controller.media_info.file_info = (mrl, options) + is_valid = self._check_file_type(controller, display) else: - log.debug('video is not optical and live') + log.debug('video is not optical or stream, but live') controller.media_info.length = service_item.media_length is_valid = self._check_file_type(controller, display) controller.media_info.start_time = service_item.start_time controller.media_info.end_time = service_item.end_time elif controller.preview_display: - if service_item.is_capable(ItemCapabilities.CanStream): - controller.media_info.media_type = MediaType.Stream - is_valid = self._check_file_type(controller, display, True) - elif service_item.is_capable(ItemCapabilities.IsOptical): + 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) + elif service_item.is_capable(ItemCapabilities.CanStream): + path = service_item.get_frames()[0]['path'] + controller.media_info.media_type = MediaType.Stream + (name, mrl, options) = parse_devicestream_path(path) + controller.media_info.file_info = (mrl, options) + is_valid = self._check_file_type(controller, display) else: - log.debug('video is not optical and preview') + log.debug('video is not optical or stream, but preview') controller.media_info.length = service_item.media_length is_valid = self._check_file_type(controller, display) if not is_valid: @@ -356,21 +366,19 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): controller.media_info.media_type = MediaType.DVD return True - def _check_file_type(self, controller, display, stream=False): + def _check_file_type(self, controller, display): """ Select the correct media Player type from the prioritized Player list :param controller: First element is the controller which should be used :param display: Which display to use - :param stream: Are we streaming or not """ - if stream: + if controller.media_info.media_type == MediaType.Stream: self.resize(controller, self.vlc_player) - controller.media_info.media_type = MediaType.Stream - if self.vlc_player.load(controller, display, None): + if self.vlc_player.load(controller, display, controller.media_info.file_info): self.current_media_players[controller.controller_type] = self.vlc_player return True - return True + return False for file in controller.media_info.file_info: if file.is_file: suffix = '*%s' % file.suffix.lower() diff --git a/openlp/core/ui/media/mediatab.py b/openlp/core/ui/media/mediatab.py index 7c6198523..32f194538 100644 --- a/openlp/core/ui/media/mediatab.py +++ b/openlp/core/ui/media/mediatab.py @@ -24,17 +24,15 @@ The :mod:`~openlp.core.ui.media.mediatab` module holds the configuration tab for import logging from PyQt5 import QtWidgets -from PyQt5.QtMultimedia import QCameraInfo, QAudioDeviceInfo, QAudio -from openlp.core.common import is_linux, is_win from openlp.core.common.i18n import translate from openlp.core.common.settings import Settings from openlp.core.lib.settingstab import SettingsTab +from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui.icons import UiIcons -LINUX_STREAM = 'v4l2://{video}:v4l2-standard= :input-slave=alsa://{audio} :live-caching=300' -WIN_STREAM = 'dshow://:dshow-vdev={video} :dshow-adev={audio} :live-caching=300' -OSX_STREAM = 'avcapture://{video}:qtsound://{audio} :live-caching=300' +VLC_ARGUMENT_BLACKLIST = [' -h ', ' --help ', ' -H ', '--full-help ', ' --longhelp ', ' --help-verbose ', ' -l ', + ' --list ', ' --list-verbose '] log = logging.getLogger(__name__) @@ -65,36 +63,25 @@ class MediaTab(SettingsTab): self.auto_start_check_box.setObjectName('auto_start_check_box') self.media_layout.addWidget(self.auto_start_check_box) self.left_layout.addWidget(self.live_media_group_box) - self.stream_media_group_box = QtWidgets.QGroupBox(self.left_column) - self.stream_media_group_box.setObjectName('stream_media_group_box') - self.stream_media_layout = QtWidgets.QFormLayout(self.stream_media_group_box) - self.stream_media_layout.setObjectName('stream_media_layout') - self.stream_media_layout.setContentsMargins(0, 0, 0, 0) - self.video_edit = QtWidgets.QLineEdit(self) - self.stream_media_layout.addRow(translate('MediaPlugin.MediaTab', 'Video:'), self.video_edit) - self.audio_edit = QtWidgets.QLineEdit(self) - self.stream_media_layout.addRow(translate('MediaPlugin.MediaTab', 'Audio:'), self.audio_edit) - self.stream_cmd = QtWidgets.QLabel(self) - self.stream_media_layout.addWidget(self.stream_cmd) - self.left_layout.addWidget(self.stream_media_group_box) self.vlc_arguments_group_box = QtWidgets.QGroupBox(self.left_column) self.vlc_arguments_group_box.setObjectName('vlc_arguments_group_box') self.vlc_arguments_layout = QtWidgets.QHBoxLayout(self.vlc_arguments_group_box) self.vlc_arguments_layout.setObjectName('vlc_arguments_layout') self.vlc_arguments_layout.setContentsMargins(0, 0, 0, 0) - self.vlc_arguments_edit = QtWidgets.QPlainTextEdit(self) + self.vlc_arguments_edit = QtWidgets.QLineEdit(self) self.vlc_arguments_layout.addWidget(self.vlc_arguments_edit) self.left_layout.addWidget(self.vlc_arguments_group_box) self.left_layout.addStretch() self.right_layout.addStretch() + # Connect vlc_arguments_edit content validator + self.vlc_arguments_edit.editingFinished.connect(self.on_vlc_arguments_edit_finished) def retranslate_ui(self): """ Translate the UI on the fly """ self.live_media_group_box.setTitle(translate('MediaPlugin.MediaTab', 'Live Media')) - self.stream_media_group_box.setTitle(translate('MediaPlugin.MediaTab', 'Stream Media Command')) - self.vlc_arguments_group_box.setTitle(translate('MediaPlugin.MediaTab', 'VLC arguments')) + self.vlc_arguments_group_box.setTitle(translate('MediaPlugin.MediaTab', 'VLC arguments (requires restart)')) self.auto_start_check_box.setText(translate('MediaPlugin.MediaTab', 'Start Live items automatically')) def load(self): @@ -102,27 +89,7 @@ class MediaTab(SettingsTab): Load the settings """ self.auto_start_check_box.setChecked(Settings().value(self.settings_section + '/media auto start')) - self.stream_cmd.setText(Settings().value(self.settings_section + '/stream command')) - self.audio_edit.setText(Settings().value(self.settings_section + '/audio')) - self.video_edit.setText(Settings().value(self.settings_section + '/video')) - if not self.stream_cmd.text(): - self.set_base_stream() - self.vlc_arguments_edit.setPlainText(Settings().value(self.settings_section + '/vlc arguments')) - if Settings().value('advanced/experimental'): - # vlc.MediaPlayer().audio_output_device_enum() - for cam in QCameraInfo.availableCameras(): - log.debug(cam.deviceName()) - log.debug(cam.description()) - for au in QAudioDeviceInfo.availableDevices(QAudio.AudioInput): - log.debug(au.deviceName()) - - def set_base_stream(self): - if is_linux: - self.stream_cmd.setText(LINUX_STREAM) - elif is_win: - self.stream_cmd.setText(WIN_STREAM) - else: - self.stream_cmd.setText(OSX_STREAM) + self.vlc_arguments_edit.setText(Settings().value(self.settings_section + '/vlc arguments')) def save(self): """ @@ -131,12 +98,7 @@ class MediaTab(SettingsTab): setting_key = self.settings_section + '/media auto start' if Settings().value(setting_key) != self.auto_start_check_box.checkState(): Settings().setValue(setting_key, self.auto_start_check_box.checkState()) - Settings().setValue(self.settings_section + '/stream command', self.stream_cmd.text()) - Settings().setValue(self.settings_section + '/vlc arguments', self.vlc_arguments_edit.toPlainText()) - Settings().setValue(self.settings_section + '/video', self.video_edit.text()) - Settings().setValue(self.settings_section + '/audio', self.audio_edit.text()) - self.stream_cmd.setText(self.stream_cmd.text().format(video=self.video_edit.text(), - audio=self.audio_edit.text())) + Settings().setValue(self.settings_section + '/vlc arguments', self.vlc_arguments_edit.text()) def post_set_up(self, post_update=False): """ @@ -148,3 +110,24 @@ class MediaTab(SettingsTab): def on_revert(self): pass + + def on_vlc_arguments_edit_finished(self): + """ + Verify that there is no blacklisted arguments in entered that could cause issues, like shutting down OpenLP. + """ + # This weird modified checking and setting is needed to prevent an infinite loop due to setting the focus + # back to vlc_arguments_edit triggers the editingFinished signal. + if not self.vlc_arguments_edit.isModified(): + self.vlc_arguments_edit.setModified(True) + return + self.vlc_arguments_edit.setModified(False) + # Check for blacklisted arguments + arguments = ' ' + self.vlc_arguments_edit.text() + ' ' + self.vlc_arguments_edit.setModified(False) + for blacklisted in VLC_ARGUMENT_BLACKLIST: + if blacklisted in arguments: + critical_error_message_box(message=translate('MediaPlugin.MediaTab', + 'The argument {arg} must not be used for VLC!'.format( + arg=blacklisted.strip())), parent=self) + self.vlc_arguments_edit.setFocus() + return diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index 4e9cfb37a..227136ceb 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -32,7 +32,9 @@ from time import sleep from PyQt5 import QtCore, QtWidgets from openlp.core.common import is_linux, is_macosx, is_win +from openlp.core.common.i18n import translate from openlp.core.display.screens import ScreenList +from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui.media import MediaState, MediaType from openlp.core.ui.media.mediaplayer import MediaPlayer @@ -116,8 +118,20 @@ class VlcPlayer(MediaPlayer): if self.settings.value('advanced/hide mouse') and live_display: command_line_options += '--mouse-hide-timeout=0 ' if self.settings.value('media/vlc arguments'): - command_line_options += self.settings.value('media/vlc arguments') - controller.vlc_instance = vlc.Instance(command_line_options) + options = command_line_options + ' ' + self.settings.value('media/vlc arguments') + controller.vlc_instance = vlc.Instance(options) + # if the instance is None, it is likely that the comamndline options were invalid, so try again without + if not controller.vlc_instance: + controller.vlc_instance = vlc.Instance(command_line_options) + if controller.vlc_instance: + critical_error_message_box(message=translate('MediaPlugin.VlcPlayer', + 'The VLC arguments are invalid.')) + else: + return + else: + controller.vlc_instance = vlc.Instance(command_line_options) + if not controller.vlc_instance: + return # creating an empty vlc media player controller.vlc_media_player = controller.vlc_instance.media_player_new() controller.vlc_widget.resize(controller.size()) @@ -146,19 +160,21 @@ class VlcPlayer(MediaPlayer): """ return get_vlc() is not None - def load(self, controller, output_display, file=None): + def load(self, controller, output_display, file): """ Load a video into VLC :param controller: The controller where the media is :param output_display: The display where the media is - :param file: file to be played or None for live streaming + :param file: file/stream to be played :return: """ + if not controller.vlc_instance: + return False vlc = get_vlc() log.debug('load vid in Vlc Controller') path = None - if file: + if file and not controller.media_info.media_type == MediaType.Stream: path = os.path.normcase(file) # create the media if controller.media_info.media_type == MediaType.CD: @@ -175,8 +191,8 @@ class VlcPlayer(MediaPlayer): return False controller.vlc_media_player = audio_cd_tracks.item_at_index(controller.media_info.title_track) elif controller.media_info.media_type == MediaType.Stream: - stream_cmd = self.settings.value('media/stream command') - controller.vlc_media = controller.vlc_instance.media_new_location(stream_cmd) + controller.vlc_media = controller.vlc_instance.media_new_location(file[0]) + controller.vlc_media.add_options(file[1]) else: controller.vlc_media = controller.vlc_instance.media_new_path(path) # put the media in the media player diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 628ec62fe..13eb9ebb7 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -1482,7 +1482,8 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties): if self.is_live and self.hide_mode() == HideMode.Theme: self.media_controller.load_video(self.controller_type, item, HideMode.Blank) self.on_blank_display(True) - else: + elif self.is_live or item.is_media(): + # avoid loading the video if this is preview and the media is background self.media_controller.load_video(self.controller_type, item, self.hide_mode()) if not self.is_live: self.preview_display.show() diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index b7e4424b2..1712bdd7a 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -26,6 +26,7 @@ import logging from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import is_not_image_file +from openlp.core.common.enum import ServiceItemType from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.mixins import RegistryProperties from openlp.core.common.registry import Registry @@ -124,16 +125,31 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): """ Validate the current page """ - background_image = BackgroundType.to_string(BackgroundType.Image) - if self.page(self.currentId()) == self.background_page and \ - self.background_page.background_type == background_image and \ - is_not_image_file(self.background_page.image_path): - QtWidgets.QMessageBox.critical(self, translate('OpenLP.ThemeWizard', 'Background Image Empty'), - translate('OpenLP.ThemeWizard', 'You have not selected a ' - 'background image. Please select one before continuing.')) - return False - else: - return True + if self.page(self.currentId()) == self.background_page: + background_image = BackgroundType.to_string(BackgroundType.Image) + background_video = BackgroundType.to_string(BackgroundType.Video) + background_stream = BackgroundType.to_string(BackgroundType.Stream) + if self.background_page.background_type == background_image and \ + is_not_image_file(self.background_page.image_path): + QtWidgets.QMessageBox.critical(self, translate('OpenLP.ThemeWizard', 'Background Image Empty'), + translate('OpenLP.ThemeWizard', 'You have not selected a ' + 'background image. Please select one before continuing.')) + return False + elif self.background_page.background_type == background_video and \ + not self.background_page.video_path: + QtWidgets.QMessageBox.critical(self, translate('OpenLP.ThemeWizard', 'Background Video Empty'), + translate('OpenLP.ThemeWizard', 'You have not selected a ' + 'background video. Please select one before continuing.')) + return False + elif self.background_page.background_type == background_stream and \ + not self.background_page.stream_mrl.strip(): + QtWidgets.QMessageBox.critical(self, translate('OpenLP.ThemeWizard', 'Background Stream Empty'), + translate('OpenLP.ThemeWizard', 'You have not selected a ' + 'background stream. Please select one before continuing.')) + return False + else: + return True + return True def on_current_id_changed(self, page_id): """ @@ -144,7 +160,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): self.setOption(QtWidgets.QWizard.HaveCustomButton1, enabled) if self.page(page_id) == self.preview_page: self.update_theme() - self.preview_box.set_theme(self.theme) + self.preview_box.set_theme(self.theme, service_item_type=ServiceItemType.Text) self.preview_box.clear_slides() self.preview_box.set_scale(float(self.preview_box.width()) / self.renderer.width()) try: @@ -253,6 +269,9 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): self.background_page.video_path = self.theme.background_source else: self.background_page.video_path = self.theme.background_filename + elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Stream): + self.background_page.stream_color = self.theme.background_border_color + self.background_page.stream_mrl = self.theme.background_source def set_main_area_page_values(self): """ @@ -312,7 +331,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): Handle the display and state of the Preview page. """ self.theme_name_edit.setText(self.theme.theme_name) - self.preview_box.set_theme(self.theme) + self.preview_box.set_theme(self.theme, service_item_type=ServiceItemType.Text) def update_theme(self): """ @@ -338,6 +357,10 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): self.theme.background_border_color = self.background_page.video_color self.theme.background_source = self.background_page.video_path self.theme.background_filename = self.background_page.video_path + elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Stream): + self.theme.background_border_color = self.background_page.stream_color + self.theme.background_source = self.background_page.stream_mrl + self.theme.background_filename = self.background_page.stream_mrl # main page self.theme.font_main_name = self.main_area_page.font_name self.theme.font_main_size = self.main_area_page.font_size @@ -397,6 +420,8 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties): self.theme.background_type == BackgroundType.to_string(BackgroundType.Video): file_name = self.theme.background_filename.name destination_path = self.path / self.theme.theme_name / file_name + if self.theme.background_type == BackgroundType.to_string(BackgroundType.Stream): + destination_path = self.theme.background_source if not self.edit_mode and not self.theme_manager.check_if_theme_exists(self.theme.theme_name): return # Set the theme background to the cache location diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index d56be2ec2..92d0e9ca5 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -686,7 +686,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R theme_path.write_text(theme_pretty) except OSError: self.log_exception('Saving theme to file failed') - if theme.background_source and theme.background_filename: + if theme.background_source and theme.background_filename and theme.background_type != 'stream': background_file = background_override # Use theme source image if override doesn't exist if not background_file or not background_file.exists(): diff --git a/openlp/plugins/media/forms/streamselectordialog.py b/openlp/plugins/media/forms/streamselectordialog.py new file mode 100644 index 000000000..b012af83e --- /dev/null +++ b/openlp/plugins/media/forms/streamselectordialog.py @@ -0,0 +1,775 @@ +# -*- coding: utf-8 -*- + +########################################################################## +# OpenLP - Open Source Lyrics Projection # +# ---------------------------------------------------------------------- # +# Copyright (c) 2008-2020 OpenLP Developers # +# ---------------------------------------------------------------------- # +# 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, either version 3 of the License, or # +# (at your option) any later version. # +# # +# 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, see . # +########################################################################## + +# +# Most of this file is heavily inspired by (and in some cases copied from) +# the VLC open capture GUIs, available in the VLC source tree at: +# * modules/gui/qt/dialogs/open/open_panels.cpp (Linux/Windows) +# * modules/gui/macosx/windows/VLCOpenWindowController.m (Mac) +# Both are licensed under GPL2 or later. +# + +import glob +import re +from PyQt5 import QtCore, QtWidgets +from PyQt5.QtMultimedia import QCameraInfo, QAudioDeviceInfo, QAudio + +from openlp.core.common import is_linux, is_macosx, is_win +from openlp.core.common.i18n import translate + +# Copied from VLC source code: modules/access/v4l2/v4l2.c +VIDEO_STANDARDS_VLC = [ + '', 'ALL', + # Pseudo standards + 'PAL', 'PAL_BG', 'PAL_DK', + 'NTSC', + 'SECAM', 'SECAM_DK', + 'MTS', '525_60', '625_50', + 'ATSC', + # Chroma-agnostic ITU standards (PAL/NTSC or PAL/SECAM) + 'B', 'G', 'H', 'L', + 'GH', 'DK', 'BG', 'MN', + # Individual standards + 'PAL_B', 'PAL_B1', 'PAL_G', 'PAL_H', + 'PAL_I', 'PAL_D', 'PAL_D1', 'PAL_K', + 'PAL_M', 'PAL_N', 'PAL_Nc', 'PAL_60', + 'NTSC_M', 'NTSC_M_JP', 'NTSC_443', 'NTSC_M_KR', + 'SECAM_B', 'SECAM_D', 'SECAM_G', 'SECAM_H', + 'SECAM_K', 'SECAM_K1', 'SECAM_L', 'SECAM_LC', + 'ATSC_8_VSB', 'ATSC_16_VSB', +] +VIDEO_STANDARDS_USER = [ + 'Undefined', 'All', + 'PAL', 'PAL B/G', 'PAL D/K', + 'NTSC', + 'SECAM', 'SECAM D/K', + 'Multichannel television sound (MTS)', + '525 lines / 60 Hz', '625 lines / 50 Hz', + 'ATSC', + 'PAL/SECAM B', 'PAL/SECAM G', 'PAL/SECAM H', 'PAL/SECAM L', + 'PAL/SECAM G/H', 'PAL/SECAM D/K', 'PAL/SECAM B/G', 'PAL/NTSC M/N', + 'PAL B', 'PAL B1', 'PAL G', 'PAL H', + 'PAL I', 'PAL D', 'PAL D1', 'PAL K', + 'PAL M', 'PAL N', 'PAL N Argentina', 'PAL 60', + 'NTSC M', 'NTSC M Japan', 'NTSC 443', 'NTSC M South Korea', + 'SECAM B', 'SECAM D', 'SECAM G', 'SECAM H', + 'SECAM K', 'SECAM K1', 'SECAM L', 'SECAM L/C', + 'ATSC 8-VSB', 'ATSC 16-VSB', +] +# Copied from VLC source code: modules/gui/qt/dialogs/open/open_panels.cpp +DIGITAL_TV_STANDARDS = [('DVB-T', 'dvb-t'), ('DVB-T2', 'dvb-t2'), ('DVB-C', 'dvb-c'), ('DVB-S', 'dvb-s'), + ('DVB-S2', 'dvb-s2'), ('ATSC', 'atsc'), ('Clear QAM', 'cqam')] +DIGITAL_TV_BANDWIDTH = [('Automatic', '0'), ('10 MHz', '10'), ('8 MHz', '8'), ('7 MHz', '7'), ('6 MHz', '6'), + ('5 MHz', '5'), ('1.712 MHz', '2')] +DIGITAL_TV_QAM = [('Automatic', 'QAM'), ('256-QAM', '256QAM'), ('128-QAM', '128QAM'), ('64-QAM', '64QAM'), + ('32-QAM', '32QAM'), ('16-QAM', '16QAM')] +DIGITAL_TV_PSK = [('QPSK', 'QPSK'), ('DQPSK', 'DQPSK'), ('8-PSK', '8PSK'), ('16-APSK', '16APSK'), ('32-APSK', '32APSK')] + + +class CaptureModeWidget(QtWidgets.QWidget): + """ + Simple widget containing a groupbox to hold devices and a groupbox for options + """ + def __init__(self, parent=None, disable_audio=False): + super().__init__(parent) + self.disable_audio = disable_audio + self.setup_ui() + + def setup_ui(self): + self.setObjectName('capture_mode_widget') + self.capture_mode_widget_layout = QtWidgets.QVBoxLayout(self) + self.capture_mode_widget_layout.setObjectName('capture_mode_widget_layout') + self.device_group = QtWidgets.QGroupBox(self) + self.device_group.setObjectName('device_group') + self.device_group_layout = QtWidgets.QFormLayout(self.device_group) + self.device_group_layout.setObjectName('device_group_layout') + self.capture_mode_widget_layout.addWidget(self.device_group) + self.options_group = QtWidgets.QGroupBox(self) + self.options_group.setObjectName('options_group') + self.options_group_layout = QtWidgets.QFormLayout(self.options_group) + self.options_group_layout.setObjectName('options_group_layout') + self.capture_mode_widget_layout.addWidget(self.options_group) + + def retranslate_ui(self): + self.device_group.setTitle(translate('MediaPlugin.StreamSelector', 'Device Selection')) + self.options_group.setTitle(translate('MediaPlugin.StreamSelector', 'Options')) + + def find_devices(self): + pass + + def update_mrl(self): + pass + + def colon_escape(self, s): + return s.replace(':', '\\:') + + def set_callback(self, callback): + self.callback = callback + + def has_support_for_mrl(self, mrl, options): + return False + + def set_mrl(self, main, options): + pass + + +class CaptureVideoWidget(CaptureModeWidget): + """ + Widget inherits groupboxes from CaptureModeWidget and inserts comboboxes for audio and video devices + """ + def __init__(self, parent=None, disable_audio=False): + super().__init__(parent, disable_audio) + + def setup_ui(self): + super().setup_ui() + # Video devices + self.video_devices_label = QtWidgets.QLabel(self) + self.video_devices_label.setObjectName('video_devices_label') + self.video_devices_combo_box = QtWidgets.QComboBox(self) + self.video_devices_combo_box.addItems(['']) + self.video_devices_combo_box.setObjectName('video_devices_combo_box') + if is_linux(): + self.video_devices_combo_box.setEditable(True) + self.device_group_layout.addRow(self.video_devices_label, self.video_devices_combo_box) + # Audio devices + self.audio_devices_label = QtWidgets.QLabel(self) + self.audio_devices_label.setObjectName('audio_devices_label') + self.audio_devices_combo_box = QtWidgets.QComboBox(self) + self.audio_devices_combo_box.addItems(['']) + self.audio_devices_combo_box.setObjectName('audio_devices_combo_box') + if is_linux(): + self.audio_devices_combo_box.setEditable(True) + if self.disable_audio: + self.audio_devices_combo_box.hide() + self.audio_devices_label.hide() + self.device_group_layout.addRow(self.audio_devices_label, self.audio_devices_combo_box) + # connect + self.video_devices_combo_box.currentIndexChanged.connect(self.update_mrl) + self.audio_devices_combo_box.currentIndexChanged.connect(self.update_mrl) + + def retranslate_ui(self): + super().retranslate_ui() + self.video_devices_label.setText(translate('MediaPlugin.StreamSelector', 'Video device name')) + self.audio_devices_label.setText(translate('MediaPlugin.StreamSelector', 'Audio device name')) + + +class CaptureVideoLinuxWidget(CaptureVideoWidget): + """ + Widget inherits groupboxes from CaptureVideoWidget and inserts widgets for linux + """ + def __init__(self, parent=None, disable_audio=False): + super().__init__(parent, disable_audio) + + def setup_ui(self): + super().setup_ui() + # Options + self.video_std_label = QtWidgets.QLabel(self) + self.video_std_label.setObjectName('video_std_label') + self.video_std_combobox = QtWidgets.QComboBox(self) + self.video_std_combobox.setObjectName('video_std_combobox') + self.video_std_combobox.addItems(VIDEO_STANDARDS_USER) + self.options_group_layout.addRow(self.video_std_label, self.video_std_combobox) + # connect + self.video_std_combobox.currentIndexChanged.connect(self.update_mrl) + + def retranslate_ui(self): + super().retranslate_ui() + self.video_std_label.setText(translate('MediaPlugin.StreamSelector', 'Video standard')) + + def find_devices(self): + """ + Insert devices for V4L2 + """ + video_devs = glob.glob('/dev/video*') + self.video_devices_combo_box.addItems(video_devs) + audio_devs = glob.glob('/dev/snd/pcmC*D*c') + vlc_audio_devs = [] + for dev in audio_devs: + vlc_dev = dev.replace('/dev/snd/pcmC', 'hw:') + vlc_dev = re.sub(r'c$', '', vlc_dev).replace('D', ',') + vlc_audio_devs.append(vlc_dev) + self.audio_devices_combo_box.addItems(vlc_audio_devs) + + def update_mrl(self): + vdev = self.video_devices_combo_box.currentText().strip() + adev = self.audio_devices_combo_box.currentText().strip() + vstd = VIDEO_STANDARDS_VLC[self.video_std_combobox.currentIndex()] + main_file = 'v4l2://{vdev}'.format(vdev=vdev) + options = ':v4l2-standard={vstd} '.format(vstd=vstd) + if adev: + options += ' :input-slave={adev}'.format(adev=adev) + self.callback(main_file, options) + + def has_support_for_mrl(self, mrl, options): + return mrl.startswith('v4l2://') and 'v4l2-tuner-frequency' not in options + + def set_mrl(self, main, options): + # find video dev + vdev = re.search(r'v4l2://([\w/-]+)', main) + if vdev: + for i in range(self.video_devices_combo_box.count()): + if self.video_devices_combo_box.itemText(i) == vdev.group(1): + self.video_devices_combo_box.setCurrentIndex(i) + break + # find audio dev + adev = re.search(r'input-slave=([\w/-:]+)', options) + if adev: + for i in range(self.audio_devices_combo_box.count()): + if self.audio_devices_combo_box.itemText(i) == adev.group(1): + self.audio_devices_combo_box.setCurrentIndex(i) + break + # find video std + vstd = re.search(r'v4l2-standard=(\w+)', options) + if vstd and vstd.group(1) in VIDEO_STANDARDS_VLC: + idx = VIDEO_STANDARDS_VLC.index(vstd.group(1)) + self.video_std_combobox.setCurrentIndex(idx) + + +class CaptureAnalogTVWidget(CaptureVideoLinuxWidget): + """ + """ + def __init__(self, parent=None, disable_audio=False): + super().__init__(parent, disable_audio) + + def setup_ui(self): + super().setup_ui() + # frequency + self.freq_label = QtWidgets.QLabel(self) + self.freq_label.setObjectName('freq_label') + self.freq = QtWidgets.QSpinBox(self) + self.freq.setAlignment(QtCore.Qt.AlignRight) + self.freq.setSuffix(' kHz') + self.freq.setSingleStep(1) + self.freq.setMaximum(2147483647) # Max value + self.options_group_layout.addRow(self.freq_label, self.freq) + # connect + self.freq.valueChanged.connect(self.update_mrl) + + def retranslate_ui(self): + super().retranslate_ui() + self.video_std_label.setText(translate('MediaPlugin.StreamSelector', 'Video standard')) + self.freq_label.setText(translate('MediaPlugin.StreamSelector', 'Frequency')) + + def update_mrl(self): + vdev = self.video_devices_combo_box.currentText().strip() + adev = self.audio_devices_combo_box.currentText().strip() + freq = self.freq.value() + vstd = VIDEO_STANDARDS_VLC[self.video_std_combobox.currentIndex()] + main_file = 'v4l2://{vdev}'.format(vdev=vdev) + options = ':v4l2-standard={vstd} '.format(vstd=vstd) + if freq: + options += ':v4l2-tuner-frequency={freq}'.format(freq=freq) + if adev: + options += ' :input-slave={adev}'.format(adev=adev) + self.callback(main_file, options) + + def has_support_for_mrl(self, mrl, options): + return mrl.startswith('v4l2://') and 'v4l2-tuner-frequency' in options + + def set_mrl(self, main, options): + # let super class handle most + super().set_mrl(main, options) + # find tuner freq + freq = re.search(r'v4l2-tuner-frequency=(\d+)', options) + if freq: + self.freq.setValue(int(freq.group(1))) + + +class CaptureDigitalTVWidget(CaptureModeWidget): + """ + Widget inherits groupboxes from CaptureModeWidget and inserts widgets for digital TV + """ + def __init__(self, parent=None, disable_audio=False): + super().__init__(parent, disable_audio) + + def setup_ui(self): + super().setup_ui() + # Tuner card + self.tuner_card_label = QtWidgets.QLabel(self) + self.tuner_card_label.setObjectName('tuner_card_label') + self.tuner_card = QtWidgets.QSpinBox(self) + self.tuner_card.setObjectName('tuner_card') + self.tuner_card.setAlignment(QtCore.Qt.AlignRight) + if is_linux(): + self.tuner_card.setPrefix('/dev/dvb/adapter') + self.device_group_layout.addRow(self.tuner_card_label, self.tuner_card) + # Delivery system + self.delivery_system_label = QtWidgets.QLabel(self) + self.delivery_system_label.setObjectName('delivery_system_label') + self.delivery_system_combo_box = QtWidgets.QComboBox(self) + for std in DIGITAL_TV_STANDARDS: + self.delivery_system_combo_box.addItem(*std) + self.delivery_system_combo_box.setObjectName('delivery_system_combo_box') + self.device_group_layout.addRow(self.delivery_system_label, self.delivery_system_combo_box) + # Options + # DVB frequency + self.dvb_freq_label = QtWidgets.QLabel(self) + self.dvb_freq_label.setObjectName('dvb_freq_label') + self.dvb_freq = QtWidgets.QSpinBox(self) + self.dvb_freq.setAlignment(QtCore.Qt.AlignRight) + self.dvb_freq.setSuffix(' kHz') + self.dvb_freq.setSingleStep(1000) + self.dvb_freq.setMaximum(2147483647) # Max value + self.options_group_layout.addRow(self.dvb_freq_label, self.dvb_freq) + # Bandwidth + self.dvb_bandwidth_label = QtWidgets.QLabel(self) + self.dvb_bandwidth_label.setObjectName('dvb_bandwidth_label') + self.dvb_bandwidth_combo_box = QtWidgets.QComboBox(self) + for bandwidth in DIGITAL_TV_BANDWIDTH: + self.dvb_bandwidth_combo_box.addItem(*bandwidth) + self.dvb_bandwidth_combo_box.setObjectName('dvb_bandwidth_combo_box') + self.options_group_layout.addRow(self.dvb_bandwidth_label, self.dvb_bandwidth_combo_box) + # QAM + self.qam_label = QtWidgets.QLabel(self) + self.qam_label.setObjectName('qam_label') + self.qam_combo_box = QtWidgets.QComboBox(self) + for qam in DIGITAL_TV_QAM: + self.qam_combo_box.addItem(*qam) + self.qam_combo_box.setObjectName('dvb_bandwidth_combo_box') + self.options_group_layout.addRow(self.qam_label, self.qam_combo_box) + # PSK + self.psk_label = QtWidgets.QLabel(self) + self.psk_label.setObjectName('psk_label') + self.psk_combo_box = QtWidgets.QComboBox(self) + for psk in DIGITAL_TV_PSK: + self.psk_combo_box.addItem(*psk) + self.psk_combo_box.setObjectName('dvb_bandwidth_combo_box') + self.options_group_layout.addRow(self.psk_label, self.psk_combo_box) + # DVB-S baud rate + self.dvbs_rate_label = QtWidgets.QLabel(self) + self.dvbs_rate_label.setObjectName('dvbs_rate_label') + self.dvbs_rate = QtWidgets.QSpinBox(self) + self.dvbs_rate.setObjectName('dvbs_rate') + self.dvbs_rate.setAlignment(QtCore.Qt.AlignRight) + self.dvbs_rate.setSuffix(' bauds') + self.options_group_layout.addRow(self.dvbs_rate_label, self.dvbs_rate) + # connect + self.delivery_system_combo_box.currentIndexChanged.connect(self.update_dvb_widget) + self.delivery_system_combo_box.currentIndexChanged.connect(self.update_mrl) + self.tuner_card.valueChanged.connect(self.update_mrl) + self.dvb_freq.valueChanged.connect(self.update_mrl) + self.dvb_bandwidth_combo_box.currentIndexChanged.connect(self.update_mrl) + self.qam_combo_box.currentIndexChanged.connect(self.update_mrl) + self.psk_combo_box.currentIndexChanged.connect(self.update_mrl) + self.dvbs_rate.valueChanged.connect(self.update_mrl) + # Arrange the widget + self.update_dvb_widget() + + def set_mrl(self, main, options): + card = re.search(r'dvb-adapter=(\d+)', options) + if card: + self.tuner_card.setValue(int(card.group(1))) + system = re.search(r'([\w-]+)://', main) + if system: + for i in range(len(DIGITAL_TV_STANDARDS)): + if DIGITAL_TV_STANDARDS[i][1] == system.group(1): + self.delivery_system_combo_box.setCurrentIndex(i) + break + freq = re.search(r'frequency=(\d+)000', main) + if freq: + self.freq.setValue(int(freq.group(1))) + modulation = re.search(r'modulation=([\w-]+)', main) + if modulation and system: + if system.group(1) in ['dvb-c', 'cqam']: + for i in range(len(DIGITAL_TV_QAM)): + if DIGITAL_TV_QAM[i][1] == modulation.group(1): + self.qam_combo_box.setCurrentIndex(i) + break + if system.group(1) == 'dvb-s2': + for i in range(len(DIGITAL_TV_QAM)): + if DIGITAL_TV_PSK[i][1] == modulation.group(1): + self.psk_combo_box.setCurrentIndex(i) + break + bandwidth = re.search(r'bandwidth=(\d+)', main) + if bandwidth: + for i in range(len(DIGITAL_TV_BANDWIDTH)): + if DIGITAL_TV_BANDWIDTH[i][1] == bandwidth.group(1): + self.dvb_bandwidth_combo_box.setCurrentIndex(i) + break + srate = re.search(r'srate=(\d+)', main) + if srate: + self.dvbs_rate.setValue(int(srate.group())) + + def update_mrl(self): + card = self.tuner_card.value() + system = self.delivery_system_combo_box.currentData() + freq = self.dvb_freq.value() + qam = self.qam_combo_box.currentData() + psk = self.psk_combo_box.currentData() + dvbs_rate = self.dvbs_rate.value() + dvb_bandwidth = self.dvb_bandwidth_combo_box.currentData() + main_file = '{system}://frequency={freq}000'.format(system=system, freq=freq) + if system in ['dvb-c', 'cqam']: + main_file += ':modulation={qam}'.format(qam=qam) + if system == 'dvb-s2': + main_file += ':modulation={psk}'.format(psk=psk) + if system in ['dvb-c', 'dvb-s', 'dvb-s2']: + main_file += ':srate={rate}'.format(rate=dvbs_rate) + if system in ['dvb-t', 'dvb-t2']: + main_file += ':bandwidth={bandwidth}'.format(bandwidth=dvb_bandwidth) + options = ' :dvb-adapter={card}'.format(card=card) + self.callback(main_file, options) + + def update_dvb_widget(self): + """ + Show and hides widgets if they are needed with the current selected system + """ + system = self.delivery_system_combo_box.currentText() + # Bandwidth + if system in ['DVB-T', 'DVB-T2']: + self.dvb_bandwidth_label.show() + self.dvb_bandwidth_combo_box.show() + else: + self.dvb_bandwidth_label.hide() + self.dvb_bandwidth_combo_box.hide() + # QAM + if system == 'DVB-C': + self.qam_label.show() + self.qam_combo_box.show() + else: + self.qam_label.hide() + self.qam_combo_box.hide() + # PSK + if system == 'DVB-S2': + self.psk_label.show() + self.psk_combo_box.show() + else: + self.psk_label.hide() + self.psk_combo_box.hide() + # Baud rate + if system in ['DVB-C', 'DVB-S', 'DVB-S2']: + self.dvbs_rate_label.show() + self.dvbs_rate.show() + else: + self.dvbs_rate_label.hide() + self.dvbs_rate.hide() + + def retranslate_ui(self): + super().retranslate_ui() + self.tuner_card_label.setText(translate('MediaPlugin.StreamSelector', 'Tuner card')) + self.delivery_system_label.setText(translate('MediaPlugin.StreamSelector', 'Delivery system')) + self.dvb_freq_label.setText(translate('MediaPlugin.StreamSelector', 'Transponder/multiplexer frequency')) + self.dvb_bandwidth_label.setText(translate('MediaPlugin.StreamSelector', 'Bandwidth')) + self.qam_label.setText(translate('MediaPlugin.StreamSelector', 'Modulation / Constellation')) + self.psk_label.setText(translate('MediaPlugin.StreamSelector', 'Modulation / Constellation')) + self.dvbs_rate_label.setText(translate('MediaPlugin.StreamSelector', 'Transponder symbol rate')) + + def has_support_for_mrl(self, mrl, options): + return '//frequency=' in mrl + + +class JackAudioKitWidget(CaptureModeWidget): + """ + Widget for JACK Audio Connection Kit + """ + def __init__(self, parent=None, disable_audio=False): + super().__init__(parent, disable_audio) + + def setup_ui(self): + super().setup_ui() + # Selected ports + self.ports_label = QtWidgets.QLabel(self) + self.ports_label.setObjectName('ports_label') + self.ports = QtWidgets.QLineEdit(self) + self.ports.setText('.*') + self.ports.setObjectName('ports') + self.ports.setAlignment(QtCore.Qt.AlignRight) + self.device_group_layout.addRow(self.ports_label, self.ports) + # channels + self.channels_label = QtWidgets.QLabel(self) + self.channels_label.setObjectName('channels_label') + self.channels = QtWidgets.QSpinBox(self) + self.channels.setObjectName('channels') + self.channels.setMaximum(255) + self.channels.setValue(2) + self.channels.setAlignment(QtCore.Qt.AlignRight) + self.device_group_layout.addRow(self.channels_label, self.channels) + # Options + self.jack_pace = QtWidgets.QCheckBox(translate('MediaPlugin.StreamSelector', 'Use VLC pace')) + self.jack_connect = QtWidgets.QCheckBox(translate('MediaPlugin.StreamSelector', 'Auto connection')) + self.options_group_layout.addRow(self.jack_pace, self.jack_connect) + # connect + self.ports.editingFinished.connect(self.update_mrl) + self.channels.valueChanged.connect(self.update_mrl) + self.jack_pace.stateChanged.connect(self.update_mrl) + self.jack_connect.stateChanged.connect(self.update_mrl) + + def retranslate_ui(self): + super().retranslate_ui() + self.ports_label.setText(translate('MediaPlugin.StreamSelector', 'Selected ports')) + self.channels_label.setText(translate('MediaPlugin.StreamSelector', 'Channels')) + + def update_mrl(self): + ports = self.ports.text().strip() + channels = self.channels.value() + main_file = 'jack://channels={channel}:ports={ports}'.format(channel=channels, ports=ports) + options = '' + if self.jack_pace.isChecked(): + options += ' :jack-input-use-vlc-pace' + if self.jack_connect.isChecked(): + options += ' :jack-input-auto-connect' + self.callback(main_file, options) + + def has_support_for_mrl(self, mrl): + return mrl.startswith('jack') + + def set_mrl(self, main, options): + channels = re.search(r'channels=(\d+)', main) + if channels: + self.channels.setValue(int(channels.group(1))) + ports = re.search(r'ports=([\w\.\*-]+)', main) + if ports: + self.ports.setText(ports.group(1)) + if 'jack-input-use-vlc-pace' in options: + self.jack_pace.setChecked(True) + if 'jack-input-auto-connect' in options: + self.jack_connect.setChecked(True) + + +class CaptureVideoQtDetectWidget(CaptureVideoWidget): + """ + Widget inherits groupboxes from CaptureVideoWidget and detects device using Qt + """ + def __init__(self, parent=None, disable_audio=False): + super().__init__(parent, disable_audio) + + def find_devices(self): + """ + Insert devices detected by Qt + """ + for cam in QCameraInfo.availableCameras(): + self.video_devices_combo_box.addItem(cam.description(), cam.deviceName()) + for au in QAudioDeviceInfo.availableDevices(QAudio.AudioInput): + self.audio_devices_combo_box.addItem(au.deviceName()) + + +class MacInputWidget(CaptureVideoQtDetectWidget): + """ + Widget for macOS +https://github.com/videolan/vlc/blob/13e18f3182e2a7b425411ce70ed83161108c3d1f/modules/gui/macosx/windows/VLCOpenWindowController.m#L472 + """ + def __init__(self, parent=None, disable_audio=False): + super().__init__(parent, disable_audio) + + def setup_ui(self): + super().setup_ui() + # There are no options available on Mac + self.options_group.hide() + + def update_mrl(self): + vdev = self.video_devices_combo_box.currentData() + # Audio is not supported on Mac since we currently don't have a way to + # extract the needed HW ids. + # adev = self.audio_devices_combo_box.currentText() + main_file = 'avcapture://{vdev}'.format(vdev=vdev) + # options = 'input-slave=qtsound://{adev}'.format(adev=adev) + self.callback(main_file, '') + + def has_support_for_mrl(self, mrl): + return mrl.startswith('avcapture') + + def set_mrl(self, main, options): + vdev = re.search(r'avcapture=(\w+)', main) + if vdev: + for i in range(self.video_devices_combo_box.count()): + if self.video_devices_combo_box.itemData(i) == vdev.group(1): + self.video_devices_combo_box.setCurrentIndex(i) + break + + +class CaptureVideoDirectShowWidget(CaptureVideoQtDetectWidget): + """ + Widget for directshow input + """ + def __init__(self, parent=None, disable_audio=False): + super().__init__(parent, disable_audio) + + def setup_ui(self): + super().setup_ui() + # Options + self.video_size_label = QtWidgets.QLabel(self) + self.video_size_label.setObjectName('video_size_label') + self.video_size_lineedit = QtWidgets.QLineEdit(self) + self.video_size_lineedit.setObjectName('video_size_lineedit') + self.options_group_layout.addRow(self.video_size_label, self.video_size_lineedit) + # connect + self.video_size_lineedit.editingFinished.connect(self.update_mrl) + + def retranslate_ui(self): + super().retranslate_ui() + self.video_size_label.setText(translate('MediaPlugin.StreamSelector', 'Video size')) + + def update_mrl(self): + vdev = self.video_devices_combo_box.currentText().strip() + adev = self.audio_devices_combo_box.currentText().strip() + vsize = self.video_size_lineedit.text().strip() + main_file = 'dshow://' + options = '' + if vdev: + options = ':"dshow-vdev={vdev}" '.format(vdev=self.colon_escape(vdev)) + if adev: + options += ':"dshow-adev={adev}" '.format(adev=self.colon_escape(adev)) + if vsize: + options += ':dshow-size={vsize}'.format(vsize) + self.callback(main_file, options) + + def has_support_for_mrl(self, mrl): + return mrl.startswith('dshow') + + def set_mrl(self, main, options): + vdev = re.search(r'"dshow-vdev=(.+)"', main) + if vdev: + for i in range(self.video_devices_combo_box.count()): + if self.video_devices_combo_box.itemText(i) == vdev.group(1): + self.video_devices_combo_box.setCurrentIndex(i) + break + adev = re.search(r'"dshow-adev=(.+)"', main) + if adev: + for i in range(self.audio_devices_combo_box.count()): + if self.audio_devices_combo_box.itemText(i) == adev.group(1): + self.audio_devices_combo_box.setCurrentIndex(i) + break + + +class Ui_StreamSelector(object): + def setup_ui(self, stream_selector): + stream_selector.setObjectName('stream_selector') + self.combobox_size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, + QtWidgets.QSizePolicy.Fixed) + stream_selector.setSizePolicy( + QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding)) + self.main_layout = QtWidgets.QVBoxLayout(stream_selector) + self.main_layout.setObjectName('main_layout') + + self.top_widget = QtWidgets.QWidget(stream_selector) + self.top_widget.setObjectName('top_widget') + self.top_layout = QtWidgets.QFormLayout(self.top_widget) + self.top_layout.setObjectName('top_layout') + # Stream name + if not self.theme_stream: + self.stream_name_label = QtWidgets.QLabel(self.top_widget) + self.stream_name_label.setObjectName('stream_name_label') + self.stream_name_edit = QtWidgets.QLineEdit(self.top_widget) + self.stream_name_edit.setObjectName('stream_name_edit') + self.top_layout.addRow(self.stream_name_label, self.stream_name_edit) + # Mode combobox + self.capture_mode_label = QtWidgets.QLabel(self.top_widget) + self.capture_mode_label.setObjectName('capture_mode_label') + self.capture_mode_combo_box = QtWidgets.QComboBox(self.top_widget) + self.capture_mode_combo_box.setObjectName('capture_mode_combo_box') + self.top_layout.addRow(self.capture_mode_label, self.capture_mode_combo_box) + self.main_layout.addWidget(self.top_widget) + # Stacked Layout for capture modes + self.stacked_modes = QtWidgets.QWidget(stream_selector) + self.stacked_modes.setObjectName('stacked_modes') + self.stacked_modes_layout = QtWidgets.QStackedLayout(self.stacked_modes) + self.stacked_modes_layout.setObjectName('stacked_modes_layout') + # Widget for DirectShow - Windows only + if is_win(): + self.direct_show_widget = CaptureVideoDirectShowWidget(stream_selector, self.theme_stream) + self.stacked_modes_layout.addWidget(self.direct_show_widget) + self.capture_mode_combo_box.addItem(translate('MediaPlugin.StreamSelector', 'DirectShow')) + elif is_linux(): + # Widget for V4L2 - Linux only + self.v4l2_widget = CaptureVideoLinuxWidget(stream_selector, self.theme_stream) + self.stacked_modes_layout.addWidget(self.v4l2_widget) + self.capture_mode_combo_box.addItem(translate('MediaPlugin.StreamSelector', 'Video Camera')) + # Widget for analog TV - Linux only + self.analog_tv_widget = CaptureAnalogTVWidget(stream_selector, self.theme_stream) + self.stacked_modes_layout.addWidget(self.analog_tv_widget) + self.capture_mode_combo_box.addItem(translate('MediaPlugin.StreamSelector', 'TV - analog')) + # Do not allow audio streams for themes + if not self.theme_stream: + # Widget for JACK - Linux only + self.jack_widget = JackAudioKitWidget(stream_selector, self.theme_stream) + self.stacked_modes_layout.addWidget(self.jack_widget) + self.capture_mode_combo_box.addItem(translate('MediaPlugin.StreamSelector', + 'JACK Audio Connection Kit')) + # Digital TV - both linux and windows + if is_win() or is_linux(): + self.digital_tv_widget = CaptureDigitalTVWidget(stream_selector, self.theme_stream) + self.stacked_modes_layout.addWidget(self.digital_tv_widget) + self.capture_mode_combo_box.addItem(translate('MediaPlugin.StreamSelector', 'TV - digital')) + # for macs + if is_macosx(): + self.mac_input_widget = MacInputWidget(stream_selector, self.theme_stream) + self.stacked_modes_layout.addWidget(self.mac_input_widget) + self.capture_mode_combo_box.addItem(translate('MediaPlugin.StreamSelector', 'Input devices')) + # Setup the stacked widgets + self.main_layout.addWidget(self.stacked_modes) + self.stacked_modes_layout.setCurrentIndex(0) + for i in range(self.stacked_modes_layout.count()): + self.stacked_modes_layout.widget(i).find_devices() + self.stacked_modes_layout.widget(i).retranslate_ui() + # Groupbox for more options + self.more_options_group = QtWidgets.QGroupBox(self) + self.more_options_group.setObjectName('more_options_group') + self.more_options_group_layout = QtWidgets.QFormLayout(self.more_options_group) + self.more_options_group_layout.setObjectName('more_options_group_layout') + # Caching spinbox + self.caching_label = QtWidgets.QLabel(self) + self.caching_label.setObjectName('caching_label') + self.caching = QtWidgets.QSpinBox(self) + self.caching.setAlignment(QtCore.Qt.AlignRight) + self.caching.setSuffix(' ms') + self.caching.setSingleStep(100) + self.caching.setMaximum(65535) + self.caching.setValue(300) + self.more_options_group_layout.addRow(self.caching_label, self.caching) + # MRL + self.mrl_label = QtWidgets.QLabel(self) + self.mrl_label.setObjectName('mrl_label') + self.mrl_lineedit = QtWidgets.QLineEdit(self) + self.mrl_lineedit.setObjectName('mrl_lineedit') + self.more_options_group_layout.addRow(self.mrl_label, self.mrl_lineedit) + # VLC options + self.vlc_options_label = QtWidgets.QLabel(self) + self.vlc_options_label.setObjectName('vlc_options_label') + self.vlc_options_lineedit = QtWidgets.QLineEdit(self) + self.vlc_options_lineedit.setObjectName('vlc_options_lineedit') + self.more_options_group_layout.addRow(self.vlc_options_label, self.vlc_options_lineedit) + # Add groupbox for more options to main layout + self.main_layout.addWidget(self.more_options_group) + # Save and close buttons + self.button_box = QtWidgets.QDialogButtonBox(stream_selector) + self.button_box.addButton(QtWidgets.QDialogButtonBox.Save) + self.button_box.addButton(QtWidgets.QDialogButtonBox.Close) + self.close_button = self.button_box.button(QtWidgets.QDialogButtonBox.Close) + self.save_button = self.button_box.button(QtWidgets.QDialogButtonBox.Save) + self.main_layout.addWidget(self.button_box) + + # translate + self.retranslate_ui(stream_selector) + # connect + self.capture_mode_combo_box.currentIndexChanged.connect(stream_selector.on_capture_mode_combo_box) + self.caching.valueChanged.connect(stream_selector.on_capture_mode_combo_box) + self.button_box.accepted.connect(stream_selector.accept) + self.button_box.rejected.connect(stream_selector.reject) + + def retranslate_ui(self, stream_selector): + stream_selector.setWindowTitle(translate('MediaPlugin.StreamSelector', 'Select Input Stream')) + if not self.theme_stream: + self.stream_name_label.setText(translate('MediaPlugin.StreamSelector', 'Stream name')) + self.capture_mode_label.setText(translate('MediaPlugin.StreamSelector', 'Capture Mode')) + self.more_options_group.setTitle(translate('MediaPlugin.StreamSelector', 'More options')) + self.caching_label.setText(translate('MediaPlugin.StreamSelector', 'Caching')) + self.mrl_label.setText(translate('MediaPlugin.StreamSelector', 'MRL')) + self.vlc_options_label.setText(translate('MediaPlugin.StreamSelector', 'VLC options')) diff --git a/openlp/plugins/media/forms/streamselectorform.py b/openlp/plugins/media/forms/streamselectorform.py new file mode 100644 index 000000000..471dc3cea --- /dev/null +++ b/openlp/plugins/media/forms/streamselectorform.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- + +########################################################################## +# OpenLP - Open Source Lyrics Projection # +# ---------------------------------------------------------------------- # +# Copyright (c) 2008-2020 OpenLP Developers # +# ---------------------------------------------------------------------- # +# 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, either version 3 of the License, or # +# (at your option) any later version. # +# # +# 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, see . # +########################################################################## + +import logging + +from PyQt5 import QtCore, QtWidgets + +from openlp.plugins.media.forms.streamselectordialog import Ui_StreamSelector +from openlp.core.ui.media import parse_devicestream_path +from openlp.core.lib.ui import critical_error_message_box +from openlp.core.common.i18n import translate + +log = logging.getLogger(__name__) + + +class StreamSelectorForm(QtWidgets.QDialog, Ui_StreamSelector): + """ + Class to manage the clip selection + """ + log.info('{name} StreamSelectorForm loaded'.format(name=__name__)) + + def __init__(self, parent, callback, theme_stream=False): + """ + Constructor + """ + super(StreamSelectorForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | + QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint) + self.callback = callback + self.theme_stream = theme_stream + self.setup_ui(self) + # setup callbacks + for i in range(self.stacked_modes_layout.count()): + self.stacked_modes_layout.widget(i).set_callback(self.update_mrl_options) + self.stacked_modes_layout.currentWidget().update_mrl() + + def exec(self): + """ + Start dialog + """ + return QtWidgets.QDialog.exec(self) + + def accept(self): + """ + Saves the current stream as a clip to the mediamanager + """ + log.debug('in StreamSelectorForm.accept') + if not self.theme_stream: + # Verify that a stream name exists + if not self.stream_name_edit.text().strip(): + critical_error_message_box(message=translate('MediaPlugin.StreamSelector', 'A Stream name is needed.')) + return + stream_name = self.stream_name_edit.text().strip() + else: + stream_name = ' ' + # Verify that a MRL exists + if not self.mrl_lineedit.text().strip(): + critical_error_message_box(message=translate('MediaPlugin.StreamSelector', 'A MRL is needed.'), parent=self) + return + stream_string = 'devicestream:{name}&&{mrl}&&{options}'.format(name=stream_name, + mrl=self.mrl_lineedit.text().strip(), + options=self.vlc_options_lineedit.text().strip()) + self.callback(stream_string) + return QtWidgets.QDialog.accept(self) + + def update_mrl_options(self, mrl, options): + """ + Callback method used to fill the MRL and Options text fields + """ + options += ' :live-caching={cache}'.format(cache=self.caching.value()) + self.mrl_lineedit.setText(mrl) + self.vlc_options_lineedit.setText(options) + + def on_capture_mode_combo_box(self): + self.stacked_modes_layout.setCurrentIndex(self.capture_mode_combo_box.currentIndex()) + self.stacked_modes_layout.currentWidget().update_mrl() + + def set_mrl(self, device_stream_str): + """ + Setup the stream widgets based on the saved devicestream. This is best effort as the string is + editable for the user. + """ + (name, mrl, options) = parse_devicestream_path(device_stream_str) + for i in range(self.stacked_modes_layout.count()): + if self.stacked_modes_layout.widget(i).has_support_for_mrl(mrl, options): + self.stacked_modes_layout.setCurrentIndex(i) + self.stacked_modes_layout.widget(i).set_mrl(mrl, options) + break + + self.mrl_lineedit.setText(mrl) + self.vlc_options_lineedit.setText(options) diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 530cf6d06..f6b510a73 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -28,19 +28,20 @@ from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings, get_natural_key, translate from openlp.core.common.mixins import RegistryProperties from openlp.core.common.path import create_paths, path_to_str -from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings +from openlp.core.common.registry import Registry from openlp.core.lib import MediaType, ServiceItemContext, check_item_selected from openlp.core.lib.mediamanageritem import MediaManagerItem from openlp.core.lib.serviceitem import ItemCapabilities from openlp.core.lib.ui import critical_error_message_box from openlp.core.state import State from openlp.core.ui.icons import UiIcons -from openlp.core.ui.media import parse_optical_path, format_milliseconds, AUDIO_EXT, VIDEO_EXT +from openlp.core.ui.media import parse_optical_path, parse_devicestream_path, format_milliseconds, AUDIO_EXT, VIDEO_EXT from openlp.core.ui.media.vlcplayer import get_vlc if get_vlc() is not None: from openlp.plugins.media.forms.mediaclipselectorform import MediaClipSelectorForm + from openlp.plugins.media.forms.streamselectorform import StreamSelectorForm log = logging.getLogger(__name__) @@ -125,6 +126,12 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): text=optical_button_text, tooltip=optical_button_tooltip, triggers=self.on_load_optical) + device_stream_button_text = translate('MediaPlugin.MediaItem', 'Open device stream') + device_stream_button_tooltip = translate('MediaPlugin.MediaItem', 'Open device stream') + self.open_stream = self.toolbar.add_toolbar_action('open_device_stream', icon=UiIcons().device_stream, + text=device_stream_button_text, + tooltip=device_stream_button_tooltip, + triggers=self.on_open_device_stream) def generate_slide_data(self, service_item, *, item=None, remote=False, context=ServiceItemContext.Service, **kwargs): @@ -143,11 +150,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): return False filename = str(item.data(QtCore.Qt.UserRole)) # Special handling if the filename is a optical clip - if filename == UiStrings().LiveStream: - service_item.processor = 'vlc' - service_item.title = filename - service_item.add_capability(ItemCapabilities.CanStream) - elif filename.startswith('optical:'): + 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: @@ -165,6 +168,13 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): service_item.start_time = start service_item.end_time = end service_item.add_capability(ItemCapabilities.IsOptical) + elif filename.startswith('devicestream:'): + # Special handling if the filename is a devicestream + (name, mrl, options) = parse_devicestream_path(filename) + service_item.processor = 'vlc' + service_item.add_from_command(filename, name, CLAPPERBOARD) + service_item.title = name + service_item.add_capability(ItemCapabilities.CanStream) else: if not os.path.exists(filename): if not remote: @@ -229,20 +239,11 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): :param target_group: """ media.sort(key=lambda file_path: get_natural_key(os.path.split(str(file_path))[1])) - file_name = translate('MediaPlugin.MediaItem', 'Live Stream') - item_name = QtWidgets.QListWidgetItem(file_name) - item_name.setIcon(UiIcons().video) - item_name.setData(QtCore.Qt.UserRole, UiStrings().LiveStream) - item_name.setToolTip(translate('MediaPlugin.MediaItem', 'Show Live Stream')) - self.list_view.addItem(item_name) for track in media: track_str = str(track) track_info = QtCore.QFileInfo(track_str) item_name = None - # Dont add the live stream in when reloading the UI. - if track_str == UiStrings().LiveStream: - continue - elif track_str.startswith('optical:'): + if track_str.startswith('optical:'): # Handle optical based item (file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track_str) item_name = QtWidgets.QListWidgetItem(clip_name) @@ -251,6 +252,12 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): item_name.setToolTip('{name}@{start}-{end}'.format(name=file_name, start=format_milliseconds(start), end=format_milliseconds(end))) + elif track_str.startswith('devicestream:'): + (name, mrl, options) = parse_devicestream_path(track_str) + item_name = QtWidgets.QListWidgetItem(name) + item_name.setIcon(UiIcons().device_stream) + item_name.setData(QtCore.Qt.UserRole, track) + item_name.setToolTip(mrl) elif not os.path.exists(track): # File doesn't exist, mark as error. file_name = os.path.split(track_str)[1] @@ -309,13 +316,13 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): """ When the load optical button is clicked, open the clip selector window. """ - # self.media_clip_selector_form.exec() if get_vlc(): media_clip_selector_form = MediaClipSelectorForm(self, self.main_window, None) media_clip_selector_form.exec() del media_clip_selector_form else: - QtWidgets.QMessageBox.critical(self, 'VLC is not available', 'VLC is not available') + critical_error_message_box(translate('MediaPlugin.MediaItem', 'VLC is not available'), + translate('MediaPlugin.MediaItem', 'Optical device support requires VLC.')) def add_optical_clip(self, optical): """ @@ -333,3 +340,32 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): file_paths.append(optical) self.load_list([str(optical)]) Settings().setValue(self.settings_section + '/media files', file_paths) + + def on_open_device_stream(self): + """ + When the open device stream button is clicked, open the stream selector window. + """ + if get_vlc(): + stream_selector_form = StreamSelectorForm(self.main_window, self.add_device_stream) + stream_selector_form.exec() + del stream_selector_form + else: + critical_error_message_box(translate('MediaPlugin.MediaItem', 'VLC is not available'), + translate('MediaPlugin.MediaItem', 'Device streaming support requires VLC.')) + + def add_device_stream(self, stream): + """ + Add a device stream based clip to the mediamanager, called from stream_selector_form. + + :param stream: The clip to add. + """ + file_paths = self.get_file_list() + # If the clip already is in the media list it isn't added and an error message is displayed. + if stream in file_paths: + critical_error_message_box(translate('MediaPlugin.MediaItem', 'Stream already saved'), + translate('MediaPlugin.MediaItem', 'This stream has already been saved')) + return + # Append the device stream string to the media list + file_paths.append(stream) + self.load_list([str(stream)]) + Settings().setValue(self.settings_section + '/media files', file_paths) diff --git a/openlp/plugins/songs/lib/songstab.py b/openlp/plugins/songs/lib/songstab.py index cca5d9397..fddfd0cb6 100644 --- a/openlp/plugins/songs/lib/songstab.py +++ b/openlp/plugins/songs/lib/songstab.py @@ -210,7 +210,7 @@ class SongsTab(SettingsTab): self.chord_notation = 'neo-latin' def on_footer_reset_button_clicked(self): - self.footer_edit_box.setPlainText(self.settings.get_default_value('songs/footer template')) + self.footer_edit_box.setPlainText(self.settings.get_default_value('footer template')) def load(self): self.settings.beginGroup(self.settings_section) @@ -247,7 +247,7 @@ class SongsTab(SettingsTab): self.settings.setValue('disable chords import', self.disable_chords_import) self.settings.setValue('chord notation', self.chord_notation) # Only save footer template if it has been changed. This allows future updates - if self.footer_edit_box.toPlainText() != self.settings.get_default_value('songs/footer template'): + if self.footer_edit_box.toPlainText() != self.settings.get_default_value('footer template'): self.settings.setValue('footer template', self.footer_edit_box.toPlainText()) self.settings.setValue('add songbook slide', self.songbook_slide) self.settings.endGroup() diff --git a/tests/interfaces/openlp_plugins/planningcenter/forms/test_selectplanform.py b/tests/interfaces/openlp_plugins/planningcenter/forms/test_selectplanform.py index 5441641a5..1035bedd9 100644 --- a/tests/interfaces/openlp_plugins/planningcenter/forms/test_selectplanform.py +++ b/tests/interfaces/openlp_plugins/planningcenter/forms/test_selectplanform.py @@ -109,9 +109,10 @@ class TestSelectPlanForm(TestCase, TestMixin): # The first service type is selected self.assertEqual(self.form.service_type_combo_box.currentText(), 'gbf', 'The service_type_combo_box defaults to "gbf"') - # the selected plan is today (the mocked date is a Sunday) - self.assertEqual(self.form.plan_selection_combo_box.currentText(), - date.strftime(mock_date.today.return_value, '%B %d, %Y'), + # the selected plan is today (the mocked date is a Sunday). Set to lowercase beacuse in some locales + # months is not capitalized. + self.assertEqual(self.form.plan_selection_combo_box.currentText().lower(), + date.strftime(mock_date.today.return_value, '%B %d, %Y').lower(), 'Incorrect default date selected for Plan Date') # count the number of themes listed and make sure it matches expected value self.assertEqual(self.form.song_theme_selection_combo_box.count(), diff --git a/tests/openlp_core/ui/test_themeform.py b/tests/openlp_core/ui/test_themeform.py index 6d101e4ef..0080397e3 100644 --- a/tests/openlp_core/ui/test_themeform.py +++ b/tests/openlp_core/ui/test_themeform.py @@ -25,6 +25,7 @@ from pathlib import Path from unittest import TestCase from unittest.mock import patch, MagicMock +from openlp.core.common.enum import ServiceItemType from openlp.core.common.registry import Registry from openlp.core.lib.theme import BackgroundType from openlp.core.ui.themeform import ThemeForm @@ -270,7 +271,8 @@ class TestThemeForm(TestCase, TestMixin): # THEN: The right options should have been set theme_form.update_theme.assert_called_once() - theme_form.preview_box.set_theme.assert_called_once_with('my fake theme') + theme_form.preview_box.set_theme.assert_called_once_with('my fake theme', + service_item_type=ServiceItemType.Text) theme_form.preview_box.clear_slides.assert_called_once() theme_form.preview_box.set_scale.assert_called_once_with(float(300 / 1920)) theme_form.preview_area_layout.set_aspect_ratio(16 / 9)