forked from openlp/openlp
Made the stream selector a lightweight version of the VLC capture device selector. Still some work to do.
This commit is contained in:
parent
d08762426c
commit
d9b65d8873
|
@ -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
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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'))
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue