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)