From 127ee05ac3a09c1256f8eab820e89c2223f1adf3 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sun, 8 Mar 2020 22:05:09 +0000 Subject: [PATCH] Add support for network streams. --- openlp/core/pages/background.py | 37 ++++- openlp/core/ui/icons.py | 1 + openlp/core/ui/media/__init__.py | 10 +- openlp/core/ui/media/mediacontroller.py | 8 +- openlp/plugins/media/forms/__init__.py | 146 ++++++++++++++++++ .../media/forms/networkstreamselectorform.py | 81 ++++++++++ .../media/forms/streamselectordialog.py | 55 +------ .../plugins/media/forms/streamselectorform.py | 63 ++------ openlp/plugins/media/lib/mediaitem.py | 52 ++++++- 9 files changed, 329 insertions(+), 124 deletions(-) create mode 100644 openlp/plugins/media/forms/networkstreamselectorform.py diff --git a/openlp/core/pages/background.py b/openlp/core/pages/background.py index 42647f4a3..314e125f2 100644 --- a/openlp/core/pages/background.py +++ b/openlp/core/pages/background.py @@ -37,6 +37,7 @@ from openlp.core.ui.media.vlcplayer import get_vlc if get_vlc() is not None: from openlp.plugins.media.forms.streamselectorform import StreamSelectorForm + from openlp.plugins.media.forms.networkstreamselectorform import NetworkStreamSelectorForm class BackgroundPage(GridLayoutPage): @@ -130,11 +131,16 @@ class BackgroundPage(GridLayoutPage): 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) + # button to open select device stream form + self.device_stream_select_button = QtWidgets.QToolButton(self) + self.device_stream_select_button.setObjectName('device_stream_select_button') + self.device_stream_select_button.setIcon(UiIcons().device_stream) + self.stream_layout.addWidget(self.device_stream_select_button) + # button to open select network stream form + self.network_stream_select_button = QtWidgets.QToolButton(self) + self.network_stream_select_button.setObjectName('network_stream_select_button') + self.network_stream_select_button.setIcon(UiIcons().network_stream) + self.stream_layout.addWidget(self.network_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') @@ -143,14 +149,15 @@ class BackgroundPage(GridLayoutPage): 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_widgets = [self.stream_label, self.stream_lineedit, self.device_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) + self.device_stream_select_button.clicked.connect(self._on_device_stream_select_button_triggered) + self.network_stream_select_button.clicked.connect(self._on_network_stream_select_button_triggered) # Force the first set of widgets to show self._on_background_type_index_changed(0) @@ -208,7 +215,7 @@ class BackgroundPage(GridLayoutPage): for widget in widget_sets[index]: widget.show() - def _on_stream_select_button_triggered(self): + def _on_device_stream_select_button_triggered(self): """ Open the Stream selection form. """ @@ -222,6 +229,20 @@ class BackgroundPage(GridLayoutPage): critical_error_message_box(translate('MediaPlugin.MediaItem', 'VLC is not available'), translate('MediaPlugin.MediaItem', 'Device streaming support requires VLC.')) + def _on_network_stream_select_button_triggered(self): + """ + Open the Stream selection form. + """ + if get_vlc(): + stream_selector_form = NetworkStreamSelectorForm(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', 'Network streaming support requires VLC.')) + def set_stream(self, stream_str): """ callback method used to get the stream mrl and options diff --git a/openlp/core/ui/icons.py b/openlp/core/ui/icons.py index c74109ca4..49ee464be 100644 --- a/openlp/core/ui/icons.py +++ b/openlp/core/ui/icons.py @@ -99,6 +99,7 @@ class UiIcons(metaclass=Singleton): 'media': {'icon': 'fa.fax'}, 'minus': {'icon': 'fa.minus'}, 'music': {'icon': 'fa.music'}, + 'network_stream': {'icon': 'fa.link'}, 'new': {'icon': 'fa.file'}, 'new_group': {'icon': 'fa.folder'}, 'notes': {'icon': 'fa.sticky-note'}, diff --git a/openlp/core/ui/media/__init__.py b/openlp/core/ui/media/__init__.py index 795cf67ee..c70913f82 100644 --- a/openlp/core/ui/media/__init__.py +++ b/openlp/core/ui/media/__init__.py @@ -104,16 +104,18 @@ def parse_optical_path(input_string): return filename, title, audio_track, subtitle_track, start, end, clip_name -def parse_devicestream_path(input_string): +def parse_stream_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('&&') + log.debug('parse_stream_path, about to parse: "{text}"'.format(text=input_string)) + # skip the header: 'devicestream:' or 'networkstream:' + header, data = input_string.split(':', 1) + # split at '&&' + stream_info = data.split('&&') name = stream_info[0] mrl = stream_info[1] options = stream_info[2] diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 3383f8c5c..7c4003edb 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -41,7 +41,7 @@ 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, HideMode -from openlp.core.ui.media import MediaState, ItemMediaInfo, MediaType, parse_optical_path, parse_devicestream_path, \ +from openlp.core.ui.media import MediaState, ItemMediaInfo, MediaType, parse_optical_path, parse_stream_path, \ VIDEO_EXT, AUDIO_EXT from openlp.core.ui.media.remote import register_views from openlp.core.ui.media.vlcplayer import VlcPlayer, get_vlc @@ -231,7 +231,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): controller.media_info.file_info = service_item.background_audio else: if service_item.is_capable(ItemCapabilities.HasBackgroundStream): - (name, mrl, options) = parse_devicestream_path(service_item.stream_mrl) + (name, mrl, options) = parse_stream_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 @@ -255,7 +255,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): 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) + (name, mrl, options) = parse_stream_path(path) controller.media_info.file_info = (mrl, options) is_valid = self._check_file_type(controller, display) else: @@ -274,7 +274,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): 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) + (name, mrl, options) = parse_stream_path(path) controller.media_info.file_info = (mrl, options) is_valid = self._check_file_type(controller, display) else: diff --git a/openlp/plugins/media/forms/__init__.py b/openlp/plugins/media/forms/__init__.py index fa1ec5512..08121bf41 100644 --- a/openlp/plugins/media/forms/__init__.py +++ b/openlp/plugins/media/forms/__init__.py @@ -18,3 +18,149 @@ # You should have received a copy of the GNU General Public License # # along with this program. If not, see . # ########################################################################## + +import re + +from PyQt5 import QtCore, QtWidgets + +from openlp.core.common.i18n import translate +from openlp.core.lib.ui import critical_error_message_box +from openlp.core.ui.media import parse_stream_path + + +class StreamSelectorFormBase(QtWidgets.QDialog): + """ + Class to manage the clip selection + """ + + def __init__(self, parent, callback, theme_stream=False): + """ + Constructor + """ + super(StreamSelectorFormBase, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | + QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint) + self.callback = callback + self.theme_stream = theme_stream + self.setup_base_ui() + self.type = '' + + def setup_base_ui(self): + self.setObjectName('stream_selector') + self.combobox_size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, + QtWidgets.QSizePolicy.Fixed) + self.setSizePolicy( + QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding)) + self.main_layout = QtWidgets.QVBoxLayout(self) + self.main_layout.setObjectName('main_layout') + + self.top_widget = QtWidgets.QWidget(self) + 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) + + def exec(self): + """ + Start dialog + """ + return QtWidgets.QDialog.exec(self) + + def accept(self): + """ + Saves the current stream as a clip to the mediamanager + """ + 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.more_options_group.mrl_lineedit.text().strip(): + critical_error_message_box(message=translate('MediaPlugin.StreamSelector', 'A MRL is needed.'), parent=self) + return + stream_string = '{type}:{name}&&{mrl}&&{options}'.format( + type=self.type, name=stream_name, mrl=self.more_options_group.mrl_lineedit.text().strip(), + options=self.more_options_group.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.more_options_group.caching.value()) + self.more_options_group.mrl_lineedit.setText(mrl) + self.more_options_group.vlc_options_lineedit.setText(options) + + def set_mrl(self, stream_str): + """ + Setup the stream widgets based on the saved stream string. This is best effort as the string is + editable for the user. + """ + (name, mrl, options) = parse_stream_path(stream_str) + cache = re.search(r'live-caching=(\d+)', options) + if cache: + self.more_options_group.caching.setValue(int(cache.group(1))) + + self.more_options_group.mrl_lineedit.setText(mrl) + self.more_options_group.vlc_options_lineedit.setText(options) + + +class VLCOptionsWidget(QtWidgets.QGroupBox): + """ + Groupbox widget for VLC options: caching, mrl and VLC options + """ + def __init__(self, parent=None): + """ + Initialise the widget. + + :param QtWidgets.QWidget | None parent: The widgets parent + """ + super().__init__(parent) + self.setup_ui() + self.retranslate_ui() + + def setup_ui(self): + """ + Create the widget layout and sub widgets + """ + # Groupbox for VLC options + self.vlc_options_group_layout = QtWidgets.QFormLayout(self) + self.vlc_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.vlc_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.vlc_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.vlc_options_group_layout.addRow(self.vlc_options_label, self.vlc_options_lineedit) + + def retranslate_ui(self): + self.setTitle(translate('MediaPlugin.StreamSelector', 'More options')) + self.caching_label.setText(translate('MediaPlugin.StreamSelector', 'Caching')) + self.mrl_label.setText(translate('MediaPlugin.StreamSelector', 'MRL')) + self.vlc_options_label.setText(translate('MediaPlugin.StreamSelector', 'VLC options')) diff --git a/openlp/plugins/media/forms/networkstreamselectorform.py b/openlp/plugins/media/forms/networkstreamselectorform.py new file mode 100644 index 000000000..c9a04c77b --- /dev/null +++ b/openlp/plugins/media/forms/networkstreamselectorform.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- + +########################################################################## +# OpenLP - Open Source Lyrics Projection # +# ---------------------------------------------------------------------- # +# Copyright (c) 2008-2020 OpenLP Developers # +# ---------------------------------------------------------------------- # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +########################################################################## + +import logging + +from PyQt5 import QtWidgets + +from openlp.core.common.i18n import translate +from openlp.plugins.media.forms import StreamSelectorFormBase, VLCOptionsWidget + +log = logging.getLogger(__name__) + + +class NetworkStreamSelectorForm(StreamSelectorFormBase): + """ + Class to manage the network stream selection + """ + log.info('{name} NetworkStreamSelectorForm loaded'.format(name=__name__)) + + def __init__(self, parent, callback, theme_stream=False): + """ + Constructor + """ + super(NetworkStreamSelectorForm, self).__init__(parent, callback, theme_stream) + self.type = 'networkstream' + self.setup_ui() + + def setup_ui(self): + self.net_mrl_label = QtWidgets.QLabel(self) + self.net_mrl_label.setObjectName('net_mrl_label') + self.net_mrl_lineedit = QtWidgets.QLineEdit(self) + self.net_mrl_lineedit.setObjectName('net_mrl_lineedit') + self.top_layout.addRow(self.net_mrl_label, self.net_mrl_lineedit) + self.main_layout.addWidget(self.top_widget) + + # Add groupbox for VLC options + self.more_options_group = VLCOptionsWidget(self) + # 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(self) + 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() + # connect + self.net_mrl_lineedit.editingFinished.connect(self.on_updates) + self.more_options_group.caching.valueChanged.connect(self.on_updates) + self.button_box.accepted.connect(self.accept) + self.button_box.rejected.connect(self.reject) + + def retranslate_ui(self): + self.setWindowTitle(translate('MediaPlugin.StreamSelector', 'Insert Input Stream')) + if not self.theme_stream: + self.stream_name_label.setText(translate('MediaPlugin.StreamSelector', 'Stream name')) + self.net_mrl_label.setText(translate('MediaPlugin.StreamSelector', 'Network URL')) + + def on_updates(self): + self.update_mrl_options(self.net_mrl_lineedit.text(), '') diff --git a/openlp/plugins/media/forms/streamselectordialog.py b/openlp/plugins/media/forms/streamselectordialog.py index b012af83e..d17c1e7f1 100644 --- a/openlp/plugins/media/forms/streamselectordialog.py +++ b/openlp/plugins/media/forms/streamselectordialog.py @@ -34,6 +34,7 @@ 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 +from openlp.plugins.media.forms import VLCOptionsWidget # Copied from VLC source code: modules/access/v4l2/v4l2.c VIDEO_STANDARDS_VLC = [ @@ -651,25 +652,6 @@ class CaptureVideoDirectShowWidget(CaptureVideoQtDetectWidget): 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') @@ -719,33 +701,8 @@ class Ui_StreamSelector(object): 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 VLC options + self.more_options_group = VLCOptionsWidget(self) # Add groupbox for more options to main layout self.main_layout.addWidget(self.more_options_group) # Save and close buttons @@ -760,7 +717,7 @@ class Ui_StreamSelector(object): 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.more_options_group.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) @@ -769,7 +726,3 @@ class Ui_StreamSelector(object): if not self.theme_stream: self.stream_name_label.setText(translate('MediaPlugin.StreamSelector', 'Stream name')) self.capture_mode_label.setText(translate('MediaPlugin.StreamSelector', 'Capture Mode')) - self.more_options_group.setTitle(translate('MediaPlugin.StreamSelector', 'More options')) - self.caching_label.setText(translate('MediaPlugin.StreamSelector', 'Caching')) - self.mrl_label.setText(translate('MediaPlugin.StreamSelector', 'MRL')) - self.vlc_options_label.setText(translate('MediaPlugin.StreamSelector', 'VLC options')) diff --git a/openlp/plugins/media/forms/streamselectorform.py b/openlp/plugins/media/forms/streamselectorform.py index 471dc3cea..e6f5b5c2c 100644 --- a/openlp/plugins/media/forms/streamselectorform.py +++ b/openlp/plugins/media/forms/streamselectorform.py @@ -20,18 +20,16 @@ ########################################################################## import logging - -from PyQt5 import QtCore, QtWidgets +import re 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 +from openlp.core.ui.media import parse_stream_path +from openlp.plugins.media.forms import StreamSelectorFormBase log = logging.getLogger(__name__) -class StreamSelectorForm(QtWidgets.QDialog, Ui_StreamSelector): +class StreamSelectorForm(StreamSelectorFormBase, Ui_StreamSelector): """ Class to manage the clip selection """ @@ -41,53 +39,14 @@ class StreamSelectorForm(QtWidgets.QDialog, Ui_StreamSelector): """ Constructor """ - super(StreamSelectorForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | - QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint) - self.callback = callback - self.theme_stream = theme_stream + super(StreamSelectorForm, self).__init__(parent, callback, theme_stream) + self.type = 'devicestream' 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() @@ -97,12 +56,14 @@ class StreamSelectorForm(QtWidgets.QDialog, Ui_StreamSelector): 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) + (name, mrl, options) = parse_stream_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) + cache = re.search(r'live-caching=(\d+)', options) + if cache: + self.more_options_group.caching.setValue(int(cache.group(1))) + self.more_options_group.mrl_lineedit.setText(mrl) + self.more_options_group.vlc_options_lineedit.setText(options) diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 4bd987b58..f0ffee8a6 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -35,12 +35,13 @@ 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, parse_devicestream_path, format_milliseconds, AUDIO_EXT, VIDEO_EXT +from openlp.core.ui.media import parse_optical_path, parse_stream_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 + from openlp.plugins.media.forms.networkstreamselectorform import NetworkStreamSelectorForm log = logging.getLogger(__name__) @@ -129,6 +130,13 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): text=device_stream_button_text, tooltip=device_stream_button_tooltip, triggers=self.on_open_device_stream) + network_stream_button_text = translate('MediaPlugin.MediaItem', 'Open network stream') + network_stream_button_tooltip = translate('MediaPlugin.MediaItem', 'Open network stream') + self.open_network_stream = self.toolbar.add_toolbar_action('open_network_stream', + icon=UiIcons().network_stream, + text=network_stream_button_text, + tooltip=network_stream_button_tooltip, + triggers=self.on_open_network_stream) def generate_slide_data(self, service_item, *, item=None, remote=False, context=ServiceItemContext.Service, **kwargs): @@ -165,9 +173,9 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): service_item.start_time = start service_item.end_time = end service_item.add_capability(ItemCapabilities.IsOptical) - elif filename.startswith('devicestream:'): + elif filename.startswith('devicestream:') or filename.startswith('networkstream:'): # Special handling if the filename is a devicestream - (name, mrl, options) = parse_devicestream_path(filename) + (name, mrl, options) = parse_stream_path(filename) service_item.processor = 'vlc' service_item.add_from_command(filename, name, self.clapperboard) service_item.title = name @@ -249,10 +257,13 @@ 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) + elif track_str.startswith('devicestream:') or track_str.startswith('networkstream:'): + (name, mrl, options) = parse_stream_path(track_str) item_name = QtWidgets.QListWidgetItem(name) - item_name.setIcon(UiIcons().device_stream) + if track_str.startswith('devicestream:'): + item_name.setIcon(UiIcons().device_stream) + else: + item_name.setIcon(UiIcons().network_stream) item_name.setData(QtCore.Qt.UserRole, track) item_name.setToolTip(mrl) elif not os.path.exists(track): @@ -366,3 +377,32 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): file_paths.append(stream) self.load_list([str(stream)]) self.settings.setValue(self.settings_section + '/media files', file_paths) + + def on_open_network_stream(self): + """ + When the open network stream button is clicked, open the stream selector window. + """ + if get_vlc(): + stream_selector_form = NetworkStreamSelectorForm(self.main_window, self.add_network_stream) + stream_selector_form.exec() + del stream_selector_form + else: + critical_error_message_box(translate('MediaPlugin.MediaItem', 'VLC is not available'), + translate('MediaPlugin.MediaItem', 'Network streaming support requires VLC.')) + + def add_network_stream(self, stream): + """ + Add a network 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)]) + self.settings.setValue(self.settings_section + '/media files', file_paths)