Made the stream selector a lightweight version of the VLC capture device selector. Still some work to do.

This commit is contained in:
Tomas Groth 2020-02-04 20:35:35 +00:00
parent d08762426c
commit d9b65d8873
24 changed files with 1189 additions and 143 deletions

View File

@ -15,7 +15,7 @@ environment:
install:
# Install dependencies from pypi
- "%PYTHON%\\python.exe -m pip install sqlalchemy alembic appdirs chardet beautifulsoup4 lxml Mako mysql-connector-python pytest mock pyodbc psycopg2 pypiwin32 websockets asyncio waitress six webob requests QtAwesome PyQt5 PyQtWebEngine pymediainfo PyMuPDF==1.16.7 QDarkStyle python-vlc Pyro4 zeroconf"
- "%PYTHON%\\python.exe -m pip install sqlalchemy alembic appdirs chardet beautifulsoup4 lxml Mako mysql-connector-python pytest mock pyodbc psycopg2 pypiwin32 websockets asyncio waitress six webob requests QtAwesome PyQt5 PyQtWebEngine pymediainfo PyMuPDF==1.16.7 QDarkStyle python-vlc Pyro4 zeroconf flask-cors"
build: off

View File

@ -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': '',
@ -268,10 +267,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',

View File

@ -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 = "<video loop autoplay muted><source src='" + theme.background_filename + "'></video>";
console.warn(backgroundHtml);

View File

@ -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
@ -567,7 +568,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

View File

@ -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'

View File

@ -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):

View File

@ -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):

View File

@ -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):
"""

View File

@ -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)

View File

@ -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())

View File

@ -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():

View File

@ -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.

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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():

View File

@ -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 <https://www.gnu.org/licenses/>. #
##########################################################################
#
# 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'))

View File

@ -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 <https://www.gnu.org/licenses/>. #
##########################################################################
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)

View File

@ -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)

View File

@ -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()

View File

@ -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(),

View File

@ -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)