diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index f974d949b..312afcd21 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -210,6 +210,8 @@ class Settings(QtCore.QSettings): 'media/media auto start': QtCore.Qt.Unchecked, 'media/stream command': '', 'media/vlc arguments': '', + 'media/video': '', + 'media/audio': '', 'remotes/download version': '0.0', 'players/background color': '#000000', 'servicemanager/last directory': None, diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 75973f389..13b7c848f 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -173,6 +173,7 @@ class ItemCapabilities(object): HasNotes = 20 HasThumbnails = 21 HasMetaData = 22 + CanStream = 23 def get_text_file_string(text_file_path): diff --git a/openlp/core/ui/media/__init__.py b/openlp/core/ui/media/__init__.py index 3c079b7d0..d63d0e9ba 100644 --- a/openlp/core/ui/media/__init__.py +++ b/openlp/core/ui/media/__init__.py @@ -26,6 +26,19 @@ import logging log = logging.getLogger(__name__ + '.__init__') +# Audio and video extensions copied from 'include/vlc_interface.h' from vlc 2.2.0 source +AUDIO_EXT = ['*.3ga', '*.669', '*.a52', '*.aac', '*.ac3', '*.adt', '*.adts', '*.aif', '*.aifc', '*.aiff', '*.amr', + '*.aob', '*.ape', '*.awb', '*.caf', '*.dts', '*.flac', '*.it', '*.kar', '*.m4a', '*.m4b', '*.m4p', '*.m5p', + '*.mid', '*.mka', '*.mlp', '*.mod', '*.mpa', '*.mp1', '*.mp2', '*.mp3', '*.mpc', '*.mpga', '*.mus', + '*.oga', '*.ogg', '*.oma', '*.opus', '*.qcp', '*.ra', '*.rmi', '*.s3m', '*.sid', '*.spx', '*.thd', '*.tta', + '*.voc', '*.vqf', '*.w64', '*.wav', '*.wma', '*.wv', '*.xa', '*.xm'] +VIDEO_EXT = ['*.3g2', '*.3gp', '*.3gp2', '*.3gpp', '*.amv', '*.asf', '*.avi', '*.bik', '*.divx', '*.drc', '*.dv', + '*.f4v', '*.flv', '*.gvi', '*.gxf', '*.iso', '*.m1v', '*.m2v', '*.m2t', '*.m2ts', '*.m4v', '*.mkv', + '*.mov', '*.mp2', '*.mp2v', '*.mp4', '*.mp4v', '*.mpe', '*.mpeg', '*.mpeg1', '*.mpeg2', '*.mpeg4', '*.mpg', + '*.mpv2', '*.mts', '*.mtv', '*.mxf', '*.mxg', '*.nsv', '*.nuv', '*.ogg', '*.ogm', '*.ogv', '*.ogx', '*.ps', + '*.rec', '*.rm', '*.rmvb', '*.rpl', '*.thp', '*.tod', '*.ts', '*.tts', '*.txd', '*.vob', '*.vro', '*.webm', + '*.wm', '*.wmv', '*.wtv', '*.xesc', '*.nut', '*.rv', '*.xvid'] + class MediaState(object): """ diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 4938c5f39..588bf636e 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -42,9 +42,9 @@ from openlp.core.common.settings import Settings 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 +from openlp.core.ui.media import MediaState, ItemMediaInfo, MediaType, parse_optical_path, VIDEO_EXT, AUDIO_EXT from openlp.core.ui.media.endpoint import media_endpoint -from openlp.core.ui.media.vlcplayer import AUDIO_EXT, VIDEO_EXT, VlcPlayer, get_vlc +from openlp.core.ui.media.vlcplayer import VlcPlayer, get_vlc log = logging.getLogger(__name__) @@ -184,7 +184,8 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): display.has_audio = False self.vlc_player.setup(display, preview) - def set_controls_visible(self, controller, value): + @staticmethod + def set_controls_visible(controller, value): """ After a new display is configured, all media related widget will be created too @@ -229,7 +230,10 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): display = self._define_display(controller) if controller.is_live: # if this is an optical device use special handling - if service_item.is_capable(ItemCapabilities.IsOptical): + 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): 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) @@ -249,7 +253,10 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): 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.IsOptical): + 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): 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) @@ -270,6 +277,8 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): # display.frame.runJavaScript('show_video("setBackBoard", null, null,"visible");') # now start playing - Preview is autoplay! autoplay = False + if service_item.is_capable(ItemCapabilities.CanStream): + autoplay = True # Preview requested if not controller.is_live: autoplay = True @@ -346,13 +355,21 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): controller.media_info.media_type = MediaType.DVD return True - def _check_file_type(self, controller, display): + def _check_file_type(self, controller, display, stream=False): """ 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: + self.resize(display, self.vlc_player) + display.media_info.media_type = MediaType.Stream + if self.vlc_player.load(display, None): + self.current_media_players[controller.controller_type] = self.vlc_player + return True + return True for file in controller.media_info.file_info: if file.is_file: suffix = '*%s' % file.suffix.lower() diff --git a/openlp/core/ui/media/mediatab.py b/openlp/core/ui/media/mediatab.py index ac4e66f42..c844129ef 100644 --- a/openlp/core/ui/media/mediatab.py +++ b/openlp/core/ui/media/mediatab.py @@ -33,9 +33,9 @@ from openlp.core.common.settings import Settings from openlp.core.lib.settingstab import SettingsTab from openlp.core.ui.icons import UiIcons -LINUX_STREAM = 'v4l2://{video} :v4l2-standard= :input-slave={audio} :live-caching=300' +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' +OSX_STREAM = 'avcapture://{video}:qtsound://{audio} :live-caching=300' log = logging.getLogger(__name__) @@ -68,11 +68,15 @@ class MediaTab(SettingsTab): 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.QHBoxLayout(self.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.stream_edit = QtWidgets.QLabel(self) - self.stream_media_layout.addWidget(self.stream_edit) + 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') @@ -84,7 +88,6 @@ class MediaTab(SettingsTab): self.left_layout.addWidget(self.vlc_arguments_group_box) self.left_layout.addStretch() self.right_layout.addStretch() - # # Signals and slots def retranslate_ui(self): """ @@ -100,22 +103,28 @@ class MediaTab(SettingsTab): Load the settings """ self.auto_start_check_box.setChecked(Settings().value(self.settings_section + '/media auto start')) - self.stream_edit.setText(Settings().value(self.settings_section + '/stream command')) - if not self.stream_edit.text(): - if is_linux: - self.stream_edit.setText(LINUX_STREAM) - elif is_win: - self.stream_edit.setText(WIN_STREAM) - else: - self.stream_edit.setText(OSX_STREAM) + 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) + def save(self): """ Save the settings @@ -123,8 +132,12 @@ 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_edit.text()) + 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())) def post_set_up(self, post_update=False): """ diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index 055d599aa..eb0f3dceb 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -40,18 +40,6 @@ from openlp.core.ui.media.mediaplayer import MediaPlayer log = logging.getLogger(__name__) # Audio and video extensions copied from 'include/vlc_interface.h' from vlc 2.2.0 source -AUDIO_EXT = ('3ga', '669', 'a52', 'aac', 'ac3', 'adt', 'adts', 'aif', 'aifc', 'aiff', 'amr', 'aob', 'ape', 'awb', 'caf', - 'dts', 'flac', 'it', 'kar', 'm4a', 'm4b', 'm4p', 'm5p', 'mid', 'mka', 'mlp', 'mod', 'mpa', 'mp1', 'mp2', - 'mp3', 'mpc', 'mpga', 'mus', 'oga', 'ogg', 'oma', 'opus', 'qcp', 'ra', 'rmi', 's3m', 'sid', 'spx', 'thd', - 'tta', 'voc', 'vqf', 'w64', 'wav', 'wma', 'wv', 'xa', 'xm') - -VIDEO_EXT = ('3g2', '3gp', '3gp2', '3gpp', 'amv', 'asf', 'avi', 'bik', 'divx', 'drc', 'dv', 'f4v', 'flv', 'gvi', 'gxf', - 'iso', 'm1v', 'm2v', 'm2t', 'm2ts', 'm4v', 'mkv', 'mov', 'mp2', 'mp2v', 'mp4', 'mp4v', 'mpe', 'mpeg', - 'mpeg1', 'mpeg2', 'mpeg4', 'mpg', 'mpv2', 'mts', 'mtv', 'mxf', 'mxg', 'nsv', 'nuv', 'ogg', 'ogm', 'ogv', - 'ogx', 'ps', 'rec', 'rm', 'rmvb', 'rpl', 'thp', 'tod', 'ts', 'tts', 'txd', 'vob', 'vro', 'webm', 'wm', - 'wmv', 'wtv', 'xesc', - # These extensions was not in the official list, added manually. - 'nut', 'rv', 'xvid') def get_vlc(): @@ -159,16 +147,15 @@ class VlcPlayer(MediaPlayer): Load a video into VLC :param output_display: The display where the media is - :param file: file to be played + :param file: file to be played or None for live streaming :return: """ vlc = get_vlc() log.debug('load vid in Vlc Controller') - controller = output_display - volume = controller.media_info.volume - path = os.path.normcase(file) + if file: + path = os.path.normcase(file) # create the media - if controller.media_info.media_type == MediaType.CD: + if output_display.media_info.media_type == MediaType.CD: if is_win(): path = '/' + path output_display.vlc_media = output_display.vlc_instance.media_new_location('cdda://' + path) @@ -180,8 +167,8 @@ class VlcPlayer(MediaPlayer): audio_cd_tracks = output_display.vlc_media.subitems() if not audio_cd_tracks or audio_cd_tracks.count() < 1: return False - output_display.vlc_media = audio_cd_tracks.item_at_index(controller.media_info.title_track) - elif controller.media_info.media_type == MediaType.Stream: + output_display.vlc_media = audio_cd_tracks.item_at_index(output_display.media_info.title_track) + elif output_display.media_info.media_type == MediaType.Stream: stream_cmd = Settings().value('media/stream command') output_display.vlc_media = output_display.vlc_instance.media_new_location(stream_cmd) else: @@ -190,7 +177,7 @@ class VlcPlayer(MediaPlayer): output_display.vlc_media_player.set_media(output_display.vlc_media) # parse the metadata of the file output_display.vlc_media.parse() - self.volume(output_display, volume) + self.volume(output_display, output_display.media_info.volume) return True def media_state_wait(self, output_display, media_state): diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 77c50e43c..92b34badd 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -48,7 +48,7 @@ from openlp.core.lib.plugin import PluginStatus from openlp.core.lib.serviceitem import ItemCapabilities, ServiceItem from openlp.core.lib.ui import create_widget_action, critical_error_message_box, find_and_set_in_combo_box from openlp.core.ui.icons import UiIcons -from openlp.core.ui.media.vlcplayer import AUDIO_EXT, VIDEO_EXT +from openlp.core.ui.media import AUDIO_EXT, VIDEO_EXT from openlp.core.ui.serviceitemeditform import ServiceItemEditForm from openlp.core.ui.servicenoteform import ServiceNoteForm from openlp.core.ui.starttimeform import StartTimeForm diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index 183993500..82a71175f 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -32,7 +32,7 @@ from openlp.core.common.mixins import RegistryProperties from openlp.core.common.registry import Registry from openlp.core.lib.theme import BackgroundGradientType, BackgroundType from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.media.vlcplayer import VIDEO_EXT +from openlp.core.ui.media import VIDEO_EXT from openlp.core.ui.themelayoutform import ThemeLayoutForm from openlp.core.ui.themewizard import Ui_ThemeWizard diff --git a/openlp/core/widgets/widgets.py b/openlp/core/widgets/widgets.py index fff21666e..0415d73a5 100644 --- a/openlp/core/widgets/widgets.py +++ b/openlp/core/widgets/widgets.py @@ -103,8 +103,8 @@ class ProxyWidget(QtWidgets.QGroupBox): :param QtWidgets.QRadioButton button: The button that has toggled :param bool checked: The buttons new state """ - id = self.radio_group.id(button) # The work around (see above comment) - enable_manual_edits = id == ProxyMode.MANUAL_PROXY and checked + group_id = self.radio_group.id(button) # The work around (see above comment) + enable_manual_edits = group_id == ProxyMode.MANUAL_PROXY and checked self.http_edit.setEnabled(enable_manual_edits) self.https_edit.setEnabled(enable_manual_edits) self.username_edit.setEnabled(enable_manual_edits) diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 30962967a..4ccc55d36 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -37,8 +37,8 @@ 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.ui.icons import UiIcons -from openlp.core.ui.media import parse_optical_path, format_milliseconds -from openlp.core.ui.media.vlcplayer import AUDIO_EXT, VIDEO_EXT, get_vlc +from openlp.core.ui.media import parse_optical_path, format_milliseconds, AUDIO_EXT, VIDEO_EXT +from openlp.core.ui.media.vlcplayer import get_vlc if get_vlc() is not None: @@ -175,7 +175,11 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): return False filename = str(item.data(QtCore.Qt.UserRole)) # Special handling if the filename is a optical clip - if filename.startswith('optical:'): + if filename == 'live': + service_item.processor = 'vlc' + service_item.title = filename + service_item.add_capability(ItemCapabilities.CanStream) + elif 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: @@ -232,9 +236,9 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): """ # self.populate_display_types() self.on_new_file_masks = translate('MediaPlugin.MediaItem', - 'Videos (*.{video});;Audio (*.{audio});;{files} ' - '(*)').format(video=' *.'.join(VIDEO_EXT), - audio=' *.'.join(AUDIO_EXT), + 'Videos ({video});;Audio ({audio});;{files} ' + '(*)').format(video=' '.join(VIDEO_EXT), + audio=' '.join(AUDIO_EXT), files=UiStrings().AllFiles) def on_delete_click(self): @@ -258,6 +262,12 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): """ # TODO needs to be fixed as no idea why this fails # media.sort(key=lambda file_path: get_natural_key(file_path.name)) + file_name = translate('MediaPlugin.MediaItem', 'Live Stream') + item_name = QtWidgets.QListWidgetItem(file_name) + item_name.setIcon(UiIcons().video) + item_name.setData(QtCore.Qt.UserRole, 'live') + 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) diff --git a/tests/functional/openlp_core/ui/media/test_mediacontroller.py b/tests/functional/openlp_core/ui/media/test_mediacontroller.py index b0e41c19c..3c8eb17f1 100644 --- a/tests/functional/openlp_core/ui/media/test_mediacontroller.py +++ b/tests/functional/openlp_core/ui/media/test_mediacontroller.py @@ -27,6 +27,7 @@ from unittest.mock import MagicMock, patch from openlp.core.common.registry import Registry from openlp.core.ui.media.mediacontroller import MediaController +from openlp.core.ui.media import ItemMediaInfo from tests.helpers.testmixin import TestMixin from tests.utils.constants import RESOURCE_PATH @@ -57,7 +58,7 @@ class TestMediaController(TestCase, TestMixin): # THEN: The player's resize method should be called correctly mocked_player.resize.assert_called_with(mocked_display) - def test_check_file_type(self): + def test_check_file_type_null(self): """ Test that we don't try to play media when no players available """ @@ -71,7 +72,47 @@ class TestMediaController(TestCase, TestMixin): ret = media_controller._check_file_type(mocked_controller, mocked_display) # THEN: it should return False - assert ret is False, '_check_file_type should return False when no mediaplayers are available.' + assert ret is False, '_check_file_type should return False when no media file matches.' + + def test_check_file_video(self): + """ + Test that we process a file that is valid + """ + # GIVEN: A mocked UiStrings, get_used_players, controller, display and service_item + media_controller = MediaController() + mocked_controller = MagicMock() + mocked_display = MagicMock() + media_controller.media_players = MagicMock() + mocked_controller.media_info = ItemMediaInfo() + mocked_controller.media_info.file_info = [TEST_PATH / 'mp3_file.mp3'] + media_controller.current_media_players = {} + media_controller.vlc_player = MagicMock() + + # WHEN: calling _check_file_type when no players exists + ret = media_controller._check_file_type(mocked_controller, mocked_display) + + # THEN: it should return False + assert ret is True, '_check_file_type should return True when audio file is present and matches.' + + def test_check_file_audio(self): + """ + Test that we process a file that is valid + """ + # GIVEN: A mocked UiStrings, get_used_players, controller, display and service_item + media_controller = MediaController() + mocked_controller = MagicMock() + mocked_display = MagicMock() + media_controller.media_players = MagicMock() + mocked_controller.media_info = ItemMediaInfo() + mocked_controller.media_info.file_info = [TEST_PATH / 'mp4_file.mp4'] + media_controller.current_media_players = {} + media_controller.vlc_player = MagicMock() + + # WHEN: calling _check_file_type when no players exists + ret = media_controller._check_file_type(mocked_controller, mocked_display) + + # THEN: it should return False + assert ret is True, '_check_file_type should return True when media file is present and matches.' def test_media_play_msg(self): """