Merge branch 'vlc_5' into 'master'

VLC - Cleanup

See merge request openlp/openlp!607
This commit is contained in:
Raoul Snyman 2023-05-16 16:50:13 +00:00
commit 2427ae10a8
10 changed files with 433 additions and 130 deletions

View File

@ -456,6 +456,7 @@ class UiStrings(metaclass=Singleton):
self.View = translate('OpenLP.Ui', 'View')
self.ViewMode = translate('OpenLP.Ui', 'View Mode')
self.Video = translate('OpenLP.Ui', 'Video')
self.Warning = translate('OpenLP.Ui', 'Warning')
self.WebDownloadText = translate('OpenLP.Ui', 'Web Interface, Download and Install Latest Version')
self.WholeVerseContinuous = translate('OpenLP.Ui', 'Continuous (whole verses)')
book_chapter = translate('OpenLP.Ui', 'Book Chapter')

View File

@ -127,6 +127,22 @@ def critical_error_message_box(title=None, message=None, parent=None, question=F
QtWidgets.QMessageBox.critical(parent, title, message)
def warning_message_box(title=None, message=None, parent=None, question=False):
"""
Provides a standard critical message box for errors that OpenLP displays to users.
:param title: The title for the message box.
:param message: The message to display to the user.
:param parent: The parent UI element to attach the dialog to.
:param question: Should this message box question the user.
"""
if question:
return QtWidgets.QMessageBox.warning(parent, UiStrings().Warning, message,
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
QtWidgets.QMessageBox.No))
QtWidgets.QMessageBox.warning(parent, title, message)
def create_horizontal_adjusting_combo_box(parent, name):
"""
Creates a QComboBox with adapting width for media items.

View File

@ -91,6 +91,7 @@ class ItemMediaInfo(object):
end_time = 0
title_track = 0
is_playing = False
old_timer = -1
timer = 1000
audio_track = 0
subtitle_track = 0

View File

@ -23,6 +23,7 @@ The :mod:`~openlp.core.ui.media.mediacontroller` module is the control module fo
"""
import logging
from pathlib import Path
from typing import Type, Union
try:
from pymediainfo import MediaInfo, __version__ as pymediainfo_version
@ -38,10 +39,12 @@ from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.path import path_to_str
from openlp.core.common.platform import is_linux, is_macosx
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.display.window import DisplayWindow
from openlp.core.lib.serviceitem import ItemCapabilities
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.lib.ui import critical_error_message_box, warning_message_box
from openlp.core.state import State
from openlp.core.ui import DisplayControllerType, HideMode
from openlp.core.ui.slidecontroller import SlideController
from openlp.core.ui.media import MediaState, ItemMediaInfo, MediaType, parse_optical_path, parse_stream_path, \
get_volume, toggle_looping_playback, is_looping_playback, save_volume
from openlp.core.ui.media.remote import register_views
@ -149,17 +152,18 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
'OpenLP.MediaController', 'No Displays have been configured, so Live Media has been disabled'))
self.setup_display(self.preview_controller, True)
def _display_controllers(self, controller_type):
def _display_controllers(self, controller_type: DisplayControllerType) -> Type[SlideController]:
"""
Decides which controller to use.
:param controller_type: The controller type where a player will be placed
:return the correct Controller
"""
if controller_type == DisplayControllerType.Live:
return self.live_controller
return self.preview_controller
def _media_state_live(self):
def _media_state_live(self) -> None:
"""
Check if there is a running Live media Player and do updating stuff (e.g. update the UI)
"""
@ -173,7 +177,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
self.live_timer.stop()
self.media_stop(self.live_controller)
def _media_state_preview(self):
def _media_state_preview(self) -> None:
"""
Check if there is a running Preview media Player and do updating stuff (e.g. update the UI)
"""
@ -187,7 +191,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
self.preview_timer.stop()
self.media_stop(self.preview_controller)
def setup_display(self, controller, preview):
def setup_display(self, controller: Type[SlideController], preview: bool) -> None:
"""
After a new display is configured, all media related widgets will be created too
@ -201,7 +205,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
self.vlc_player.setup(controller, self._define_display(controller))
@staticmethod
def set_controls_visible(controller, value):
def set_controls_visible(controller: Type[SlideController], value: int) -> None:
"""
After a new display is configured, all media related widget will be created too
@ -212,7 +216,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
controller.mediabar.setVisible(value)
@staticmethod
def resize(controller, player):
def resize(controller: Type[SlideController], player) -> None:
"""
After Mainwindow changes or Splitter moved all related media widgets have to be resized
@ -221,13 +225,14 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
"""
player.resize(controller)
def load_video(self, source, service_item, hidden=False, is_theme_background=False):
def load_video(self, source, service_item, hidden: bool = False, is_theme_background: bool = False) -> None:
"""
Loads and starts a video to run and sets the stored sound value.
:param source: Where the call originated form
:param service_item: The player which is doing the playing
:param hidden: The player which is doing the playing
:param is_theme_background: Is the theme providing a background
"""
self.is_theme_background = is_theme_background
is_valid = True
@ -256,48 +261,28 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
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.IsOptical):
self.log_debug('video is optical and live')
path_string = path_to_str(service_item.get_frame_path())
(name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(path_string)
is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display,
controller)
elif service_item.is_capable(ItemCapabilities.CanStream):
self.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_stream_path(path)
controller.media_info.file_info = (mrl, options)
is_valid = self._check_file_type(controller, display)
else:
self.log_debug('video is not optical or stream, but live')
controller.media_info.length = service_item.media_length
controller.media_info.start_time = service_item.start_time
controller.media_info.timer = service_item.start_time
controller.media_info.end_time = service_item.end_time
is_valid = self._check_file_type(controller, display)
elif controller.preview_display:
if service_item.is_capable(ItemCapabilities.IsOptical):
self.log_debug('video is optical and preview')
path_string = path_to_str(service_item.get_frame_path())
(name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(path_string)
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_stream_path(path)
controller.media_info.file_info = (mrl, options)
is_valid = self._check_file_type(controller, display)
else:
self.log_debug('video is not optical or stream, but preview')
controller.media_info.length = service_item.media_length
controller.media_info.start_time = service_item.start_time
controller.media_info.timer = service_item.start_time
controller.media_info.end_time = service_item.end_time
is_valid = self._check_file_type(controller, display)
# if this is an optical device use special handling
if service_item.is_capable(ItemCapabilities.IsOptical):
self.log_debug(f'video is optical live={controller.is_live}')
path_string = path_to_str(service_item.get_frame_path())
(name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(path_string)
is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display,
controller)
elif service_item.is_capable(ItemCapabilities.CanStream):
self.log_debug(f'video is stream live={controller.is_live}')
path = service_item.get_frames()[0]['path']
controller.media_info.media_type = MediaType.Stream
(name, mrl, options) = parse_stream_path(path)
controller.media_info.file_info = (mrl, options)
is_valid = self._check_file_type(controller, display)
else:
self.log_debug(f'standard media, is not optical or stream, live={controller.is_live}')
controller.media_info.length = service_item.media_length
controller.media_info.start_time = service_item.start_time
controller.media_info.timer = service_item.start_time
controller.media_info.end_time = service_item.end_time
is_valid = self._check_file_type(controller, display)
if not is_valid:
# Media could not be loaded correctly
critical_error_message_box(translate('MediaPlugin.MediaItem', 'Unsupported File'),
@ -309,26 +294,20 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
if controller.is_live:
if self.preview_controller.media_info.media_type == MediaType.Stream:
self.log_warning('stream can only be displayed in one instance, killing preview stream')
warning_message_box(translate('MediaPlugin.MediaItem', 'Unable to Preview Stream'),
translate('MediaPlugin.MediaItem',
'Closing Preview to allow Live Stream'))
self.preview_controller.on_media_close()
else:
if self.live_controller.media_info.media_type == MediaType.Stream:
self.log_warning('stream cannot be previewed while also streaming live')
warning_message_box(translate('MediaPlugin.MediaItem', 'Unable to Preview Stream '),
translate('MediaPlugin.MediaItem',
'Unable to preview when live is currently streaming'))
return
self.is_autoplay = False
if service_item.requires_media() and hidden == HideMode.Theme:
self.is_autoplay = True
# Preview requested
elif not controller.is_live:
self.is_autoplay = True
# Visible or background requested or Service Item wants to autostart
elif not hidden and service_item.will_auto_start:
self.is_autoplay = True
# Unblank on load set
elif self.settings.value('core/auto unblank'):
self.is_autoplay = True
if self.is_theme_background:
self.is_autoplay = True
if self.is_autoplay:
if self.decide_autoplay(service_item, controller, hidden):
start_hidden = self.is_theme_background and controller.is_live and \
(controller.current_hide_mode == HideMode.Blank or controller.current_hide_mode == HideMode.Screen)
if not self.media_play(controller, start_hidden):
@ -341,8 +320,33 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
format(nm=self.current_media_players[controller.controller_type].display_name))
return True
def decide_autoplay(self, service_item, controller, hidden: bool) -> bool:
"""
Function to decide if we can / want to autoplay a media item
:param service_item: The Media Service item
:param controller: The controller on which the item is to be played
:param hidden: is the display hidden at present?
:return: Can we autoplay the media.
"""
if not controller.is_live:
return True
is_autoplay = False
# Visible or background requested or Service Item wants background media
if service_item.requires_media() and hidden == HideMode.Theme:
is_autoplay = True
elif not hidden and (service_item.will_auto_start or
self.settings.value('media/media auto start') == QtCore.Qt.CheckState.Checked):
is_autoplay = True
# Unblank on load set
elif self.settings.value('core/auto unblank'):
is_autoplay = True
if self.is_theme_background:
is_autoplay = True
return is_autoplay
@staticmethod
def media_length(media_path):
def media_length(media_path: Union[str, Path]):
"""
Uses Media Info to obtain the media length
@ -362,7 +366,8 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
return media_data.tracks[0].duration or 0
return 0
def media_setup_optical(self, filename, title, audio_track, subtitle_track, start, end, display, controller):
def media_setup_optical(self, filename, title, audio_track, subtitle_track, start, end,
display: Type[DisplayWindow], controller: Type[SlideController]):
"""
Setup playback of optical media
@ -400,7 +405,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
self.current_media_players[controller.controller_type] = self.vlc_player
return True
def _check_file_type(self, controller, display):
def _check_file_type(self, controller: Type[SlideController], display: Type[DisplayWindow]):
"""
Select the correct media Player type from the prioritized Player list
@ -423,7 +428,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
return True
return False
def media_play_msg(self, msg):
def media_play_msg(self, msg: list):
"""
Responds to the request to play a loaded video
@ -438,7 +443,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
"""
return self.media_play(self.live_controller)
def media_play(self, controller, start_hidden=False):
def media_play(self, controller: Type[SlideController], start_hidden=False):
"""
Responds to the request to play a loaded video
@ -456,15 +461,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
self.media_volume(controller, get_volume(controller))
if not start_hidden:
self._media_set_visibility(controller, True)
controller.mediabar.actions['playbackPlay'].setVisible(False)
controller.mediabar.actions['playbackPause'].setVisible(True)
controller.mediabar.actions['playbackStop'].setDisabled(False)
controller.mediabar.actions['playbackLoop'].setChecked(is_looping_playback(controller))
controller.mediabar.actions['playbackStop'].setVisible(not controller.media_info.is_background or
controller.media_info.media_type is MediaType.Audio)
controller.mediabar.actions['playbackLoop'].setVisible((not controller.media_info.is_background and
controller.media_info.media_type is not MediaType.Stream)
or controller.media_info.media_type is MediaType.Audio)
self._media_bar(controller, "play")
# Start Timer for ui updates
if controller.is_live:
if not self.live_timer.isActive():
@ -495,13 +492,14 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
start_again = False
stopped = False
if controller.media_info.is_playing and controller.media_info.length > 0:
controller.media_info.timer += TICK_TIME
if controller.media_info.timer >= controller.media_info.start_time + controller.media_info.length:
controller.media_info.timer = controller.vlc_media_player.get_time()
if controller.media_info.old_timer > controller.media_info.timer:
if is_looping_playback(controller) or self.is_theme_background:
start_again = True
else:
self.media_stop(controller)
stopped = True
controller.media_info.old_timer = controller.media_info.timer
self._update_seek_ui(controller)
else:
stopped = True
@ -511,7 +509,42 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
self._update_seek_ui(controller)
return not stopped
def _update_seek_ui(self, controller):
def _media_bar(self, controller: Type[SlideController], mode: str) -> None:
"""
Set the media bar state depending on the function called.
:param controller: The controller being updated
:param mode: The mode the code is being called from
:return: None
"""
loop_set = self.is_theme_background or is_looping_playback(controller)
loop_disabled = controller.media_info.media_type is MediaType.Stream or self.is_theme_background
controller.mediabar.blockSignals(True)
if mode == "load":
controller.mediabar.actions['playbackPlay'].setDisabled(False)
controller.mediabar.actions['playbackPause'].setDisabled(True)
controller.mediabar.actions['playbackStop'].setDisabled(True)
controller.mediabar.actions['playbackLoop'].setChecked(loop_set)
controller.mediabar.actions['playbackLoop'].setDisabled(loop_disabled)
if mode == "play":
controller.mediabar.actions['playbackPlay'].setDisabled(True)
controller.mediabar.actions['playbackPause'].setDisabled(False)
controller.mediabar.actions['playbackStop'].setDisabled(False)
controller.mediabar.actions['playbackLoop'].setChecked(loop_set)
controller.mediabar.actions['playbackLoop'].setDisabled(loop_disabled)
if mode == "pause" or mode == "stop" or mode == "reset":
controller.mediabar.actions['playbackPlay'].setDisabled(False)
controller.mediabar.actions['playbackPause'].setDisabled(True)
controller.mediabar.actions['playbackStop'].setDisabled(False)
if mode == "stop" or mode == "reset":
controller.mediabar.actions['playbackLoop'].setChecked(loop_set)
controller.mediabar.actions['playbackLoop'].setDisabled(loop_disabled)
controller.mediabar.blockSignals(False)
@staticmethod
def _update_seek_ui(controller):
if controller.media_info.timer > controller.media_info.end_time:
controller.media_info.timer = controller.media_info.end_time
seconds = controller.media_info.timer // 1000
minutes = seconds // 60
seconds %= 60
@ -524,7 +557,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
controller.mediabar.position_label.setText(' %02d:%02d / %02d:%02d' %
(minutes, seconds, end_minutes, end_seconds))
def media_pause_msg(self, msg):
def media_pause_msg(self, msg: list):
"""
Responds to the request to pause a loaded video
@ -538,7 +571,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
"""
return self.media_pause(self.live_controller)
def media_pause(self, controller):
def media_pause(self, controller: Type[SlideController]):
"""
Responds to the request to pause a loaded video
@ -547,17 +580,16 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
self.log_debug(f'media_stop is_live:{controller.is_live}')
if controller.controller_type in self.current_media_players:
self.current_media_players[controller.controller_type].pause(controller)
controller.mediabar.actions['playbackPlay'].setVisible(True)
controller.mediabar.actions['playbackPause'].setVisible(False)
self._media_bar(controller, "pause")
controller.media_info.is_playing = False
# Add a tick to the timer to prevent it finishing the video before it can loop back or stop
# If the clip finishes, we hit a bug where we cannot start the video
controller.media_info.timer += TICK_TIME
controller.media_info.timer = controller.vlc_media_player.get_time()
controller.output_has_changed()
return True
return False
def media_loop_msg(self, msg):
def media_loop_msg(self, msg: list):
"""
Responds to the request to loop a loaded video
@ -566,7 +598,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
self.media_loop(msg[0])
@staticmethod
def media_loop(controller):
def media_loop(controller: Type[SlideController]):
"""
Responds to the request to loop a loaded video
@ -575,7 +607,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
toggle_looping_playback(controller)
controller.mediabar.actions['playbackLoop'].setChecked(is_looping_playback(controller))
def media_stop_msg(self, msg):
def media_stop_msg(self, msg: list):
"""
Responds to the request to stop a loaded video
@ -589,7 +621,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
"""
return self.media_stop(self.live_controller)
def media_stop(self, controller):
def media_stop(self, controller: Type[SlideController]):
"""
Responds to the request to stop a loaded video
@ -608,9 +640,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
controller.set_hide_mode(display.hide_mode or HideMode.Blank)
else:
self._media_set_visibility(controller, False)
controller.mediabar.actions['playbackPlay'].setVisible(True)
controller.mediabar.actions['playbackStop'].setDisabled(True)
controller.mediabar.actions['playbackPause'].setVisible(False)
self._media_bar(controller, "stop")
controller.media_info.is_playing = False
controller.media_info.timer = controller.media_info.start_time
controller.mediabar.seek_slider.setSliderPosition(controller.media_info.start_time)
@ -619,7 +649,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
return True
return False
def media_volume_msg(self, msg):
def media_volume_msg(self, msg: list):
"""
Changes the volume of a running video
@ -629,7 +659,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
vol = msg[1][0]
self.media_volume(controller, vol)
def media_volume(self, controller, volume):
def media_volume(self, controller: Type[SlideController], volume: int):
"""
Changes the volume of a running video
@ -641,7 +671,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
self.current_media_players[controller.controller_type].volume(controller, volume)
controller.mediabar.volume_slider.setValue(volume)
def media_seek_msg(self, msg):
def media_seek_msg(self, msg: list):
"""
Responds to the request to change the seek Slider of a loaded video via a message
@ -652,7 +682,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
seek_value = msg[1][0]
self.media_seek(controller, seek_value)
def media_seek(self, controller, seek_value):
def media_seek(self, controller: Type[SlideController], seek_value):
"""
Responds to the request to change the seek Slider of a loaded video
@ -665,7 +695,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
controller.media_info.timer = seek_value
self._update_seek_ui(controller)
def media_reset(self, controller, delayed=False):
def media_reset(self, controller: Type[SlideController], delayed: bool = False) -> None:
"""
Responds to the request to reset a loaded video
:param controller: The controller to use.
@ -683,11 +713,8 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
else:
self._media_set_visibility(controller, False)
del self.current_media_players[controller.controller_type]
controller.mediabar.actions['playbackPlay'].setVisible(True)
controller.mediabar.actions['playbackStop'].setDisabled(True)
controller.mediabar.actions['playbackPause'].setVisible(False)
def media_hide_msg(self, msg):
def media_hide_msg(self, msg: list):
"""
Hide the related video Widget
@ -722,7 +749,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
self._media_set_visibility(self.live_controller, False)
del self.current_media_players[self.live_controller.controller_type]
def _media_set_visibility(self, controller, visible):
def _media_set_visibility(self, controller: Type[SlideController], visible):
"""
Set the live video Widget visibility
"""
@ -734,7 +761,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
display = self._define_display(controller)
display.raise_()
def media_blank(self, msg):
def media_blank(self, msg: list):
"""
Blank the related video Widget
@ -765,7 +792,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
self.media_pause(self.live_controller)
self._media_set_visibility(self.live_controller, False)
def media_unblank(self, msg):
def media_unblank(self, msg: list):
"""
Unblank the related video Widget
@ -799,11 +826,12 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
self.media_reset(self._display_controllers(DisplayControllerType.Preview))
@staticmethod
def _define_display(controller):
def _define_display(controller: SlideController) -> Type[DisplayWindow]:
"""
Extract the correct display for a given controller
:param controller: Controller to be used
:return correct display window
"""
if controller.is_live:
return controller.display

View File

@ -28,6 +28,7 @@ import sys
import threading
from datetime import datetime
from time import sleep
from typing import Type
from PyQt5 import QtCore, QtWidgets
@ -35,7 +36,9 @@ from openlp.core.common.i18n import translate
from openlp.core.common.mixins import LogMixin
from openlp.core.common.platform import is_linux, is_macosx, is_win
from openlp.core.display.screens import ScreenList
from openlp.core.display.window import DisplayWindow
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.ui.slidecontroller import SlideController
from openlp.core.ui.media import MediaState, MediaType, VlCState, get_volume
from openlp.core.ui.media.mediaplayer import MediaPlayer
@ -99,7 +102,7 @@ class VlcPlayer(MediaPlayer, LogMixin):
self.parent = parent
self.can_folder = True
def setup(self, controller, display):
def setup(self, controller: SlideController, display: Type[DisplayWindow]) -> None:
"""
Set up the media player
@ -162,14 +165,14 @@ class VlcPlayer(MediaPlayer, LogMixin):
"""
return get_vlc() is not None
def load(self, controller, output_display, file):
def load(self, controller: SlideController, output_display: Type[DisplayWindow], file: str) -> bool:
"""
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/stream to be played
:return:
:return: Success or Failure
"""
if not controller.vlc_instance:
return False
@ -232,7 +235,7 @@ class VlcPlayer(MediaPlayer, LogMixin):
self.volume(controller, get_volume(controller))
return True
def media_state_wait(self, controller, media_state):
def media_state_wait(self, controller: Type[SlideController], media_state: VlCState) -> bool:
"""
Wait for the video to change its state
Wait no longer than 60 seconds. (loading an iso file needs a long time)
@ -251,7 +254,7 @@ class VlcPlayer(MediaPlayer, LogMixin):
return False
return True
def resize(self, controller):
def resize(self, controller: Type[SlideController]) -> None:
"""
Resize the player
@ -263,7 +266,7 @@ class VlcPlayer(MediaPlayer, LogMixin):
else:
controller.vlc_widget.resize(controller.preview_display.size())
def play(self, controller, output_display):
def play(self, controller: Type[SlideController], output_display: Type[DisplayWindow]) -> bool:
"""
Play the current item
@ -279,7 +282,7 @@ class VlcPlayer(MediaPlayer, LogMixin):
self.set_state(MediaState.Playing, controller)
return True
def pause(self, controller):
def pause(self, controller: Type[SlideController]) -> None:
"""
Pause the current item
@ -292,7 +295,7 @@ class VlcPlayer(MediaPlayer, LogMixin):
if self.media_state_wait(controller, VlCState.Paused):
self.set_state(MediaState.Paused, controller)
def stop(self, controller):
def stop(self, controller: Type[SlideController]) -> None:
"""
Stop the current item
@ -302,7 +305,7 @@ class VlcPlayer(MediaPlayer, LogMixin):
threading.Thread(target=controller.vlc_media_player.stop).start()
self.set_state(MediaState.Stopped, controller)
def volume(self, controller, vol):
def volume(self, controller: Type[SlideController], vol: int) -> None:
"""
Set the volume
@ -312,7 +315,7 @@ class VlcPlayer(MediaPlayer, LogMixin):
"""
controller.vlc_media_player.audio_set_volume(vol)
def seek(self, controller, seek_value):
def seek(self, controller: Type[SlideController], seek_value: int) -> None:
"""
Go to a particular position
@ -322,7 +325,7 @@ class VlcPlayer(MediaPlayer, LogMixin):
if controller.vlc_media_player.is_seekable():
controller.vlc_media_player.set_time(seek_value)
def reset(self, controller):
def reset(self, controller: Type[SlideController]) -> None:
"""
Reset the player
@ -331,7 +334,7 @@ class VlcPlayer(MediaPlayer, LogMixin):
controller.vlc_media_player.stop()
self.set_state(MediaState.Off, controller)
def set_visible(self, controller, status):
def set_visible(self, controller: Type[SlideController], status: bool) -> None:
"""
Set the visibility
@ -340,7 +343,7 @@ class VlcPlayer(MediaPlayer, LogMixin):
"""
controller.vlc_widget.setVisible(status)
def update_ui(self, controller, output_display):
def update_ui(self, controller: Type[SlideController], output_display: Type[DisplayWindow]) -> None:
"""
Update the UI

View File

@ -114,6 +114,8 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
self.controller_type = None
self.displays = []
self.screens = ScreenList()
self.vlc_instance = None
self.media_info = None
Registry().set_flag('has doubleclick added item to service', True)
Registry().set_flag('replace service manager item', False)
@ -1511,8 +1513,7 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
elif self.is_live:
if self._current_hide_mode == HideMode.Theme:
self.set_hide_mode(HideMode.Blank)
self.media_controller.load_video(self.controller_type, item, self._current_hide_mode,
is_theme_background)
self.media_controller.load_video(self.controller_type, item, self._current_hide_mode, False)
elif 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, is_theme_background=is_theme_background)

View File

@ -238,8 +238,6 @@ class MediaMediaItem(FolderLibraryItem):
service_item.add_capability(ItemCapabilities.CanAutoStartForLive)
service_item.add_capability(ItemCapabilities.CanEditTitle)
service_item.add_capability(ItemCapabilities.RequiresMedia)
if self.settings.value('media/media auto start') == QtCore.Qt.Checked:
service_item.will_auto_start = True
# force a non-existent theme
service_item.theme = -1
# validate the item after all capabilities has been added

View File

@ -265,7 +265,10 @@ class SongMediaItem(MediaManagerItem):
continue
author_list = [author.display_name for author in song.authors]
text = create_separated_list(author_list) if author_list else song.title
song_detail = '{title} ({author})'.format(title=song.title, author=text)
if len(song.media_files) > 0:
song_detail = f'{song.title} (A) ({text})'
else:
song_detail = '{title} ({author})'.format(title=song.title, author=text)
song_name = QtWidgets.QListWidgetItem(song_detail)
song_name.setData(QtCore.Qt.UserRole, song.id)
self.list_view.addItem(song_name)

View File

@ -21,7 +21,12 @@
"""
Package to test the openlp.core.ui package.
"""
from openlp.core.ui.media import parse_optical_path
import pytest
from unittest.mock import MagicMock, ANY
from openlp.core.ui.media import parse_optical_path, get_volume, save_volume, toggle_looping_playback, \
is_looping_playback
from openlp.core.ui.media import format_milliseconds
@ -94,3 +99,36 @@ def test_format_milliseconds():
# THEN: The formatted time string should be 55 hours, 33 minutes, 20 seconds, and 000 milliseconds
assert num_soldiers_on_horses_as_formatted_time_string == "55:33:20,000"
@pytest.mark.parametrize("live, result", [(False, 'media/preview volume'), (True, 'media/live volume')])
def test_get_volume(mock_settings, live, result):
controller = MagicMock()
controller.is_live = live
get_volume(controller)
mock_settings.value.assert_called_with(result)
@pytest.mark.parametrize("live, result", [(False, 'media/preview volume'), (True, 'media/live volume')])
def test_save_volume(mock_settings, live, result):
controller = MagicMock()
controller.is_live = live
save_volume(controller, 5)
mock_settings.setValue.assert_called_with(result, ANY)
@pytest.mark.parametrize("live, result", [(False, 'media/preview loop'), (True, 'media/live loop')])
def test_toggle_looping_playback(mock_settings, live, result):
controller = MagicMock()
controller.is_live = live
toggle_looping_playback(controller)
mock_settings.value.assert_called_with(result)
mock_settings.setValue.assert_called_with(result, ANY)
@pytest.mark.parametrize("live, result", [(False, 'media/preview loop'), (True, 'media/live loop')])
def test_is_looping_playback(mock_settings, live, result):
controller = MagicMock()
controller.is_live = live
is_looping_playback(controller)
mock_settings.value.assert_called_with(result)

View File

@ -27,6 +27,7 @@ from unittest import skipUnless
from unittest.mock import MagicMock, patch
import pytest
from PyQt5 import QtCore
from openlp.core.state import State
from openlp.core.common.platform import is_linux, is_macosx
@ -34,6 +35,7 @@ from openlp.core.common.registry import Registry
from openlp.core.ui import DisplayControllerType, HideMode
from openlp.core.ui.media.mediacontroller import MediaController
from openlp.core.ui.media import ItemMediaInfo, MediaState, MediaType
from openlp.core.widgets.toolbar import OpenLPToolbar
from tests.utils.constants import RESOURCE_PATH
@ -282,8 +284,12 @@ def test_load_video(media_env, settings):
# GIVEN: A media controller and a service item
mocked_slide_controller = MagicMock()
mocked_service_item = MagicMock()
mocked_service_item.length = 10
mocked_service_item.end_time = 10
mocked_service_item.start_time = 1
mocked_service_item.is_capable.return_value = False
settings.setValue('media/live volume', 1)
media_env.media_controller.current_media_players = MagicMock()
media_env.media_controller._check_file_type = MagicMock(return_value=True)
media_env.media_controller._display_controllers = MagicMock(return_value=mocked_slide_controller)
@ -302,6 +308,7 @@ def test_load_video(media_env, settings):
media_env.media_controller.media_reset.assert_called_once_with(mocked_slide_controller)
media_env.media_controller.media_play.assert_called_once_with(mocked_slide_controller, False)
media_env.media_controller.set_controls_visible.assert_called_once_with(mocked_slide_controller, True)
assert mocked_slide_controller.media_info.is_background is False
def test_check_file_type_null(media_env):
@ -405,7 +412,7 @@ def test_media_stop_msg(media_env):
mocked_media_stop.assert_called_with(1)
def test_media_stop(media_env):
def test_media_stop(media_env, settings):
"""
Test that the media controller takes the correct actions when stopping media
"""
@ -414,10 +421,13 @@ def test_media_stop(media_env):
mocked_media_player = MagicMock()
mocked_display = MagicMock(hide_mode=None)
mocked_slide_controller.controller_type = 'media player'
mocked_slide_controller.media_info = MagicMock(is_background=False)
mocked_slide_controller.media_info = ItemMediaInfo()
mocked_slide_controller.media_info.is_background = False
mocked_slide_controller.set_hide_mode = MagicMock()
mocked_slide_controller.is_live = True
media_env.media_controller.current_media_players = {'media player': mocked_media_player}
media_env.media_controller.media_info = ItemMediaInfo()
media_env.media_controller.is_theme_background = False
media_env.media_controller.live_hide_timer = MagicMock()
media_env.media_controller._define_display = MagicMock(return_value=mocked_display)
@ -432,7 +442,7 @@ def test_media_stop(media_env):
mocked_slide_controller.set_hide_mode.assert_called_once_with(HideMode.Blank)
def test_media_stop_no_hide_change(media_env):
def test_media_stop_no_hide_change(media_env, settings):
"""
Test that the media_stop doesn't change the hide mode of OpenLP when screen is visible
"""
@ -441,10 +451,12 @@ def test_media_stop_no_hide_change(media_env):
mocked_media_player = MagicMock()
mocked_display = MagicMock(hide_mode=HideMode.Screen)
mocked_slide_controller.controller_type = 'media player'
mocked_slide_controller.media_info = MagicMock(is_background=False)
mocked_slide_controller.media_info = ItemMediaInfo()
mocked_slide_controller.media_info.is_background = False
mocked_slide_controller.set_hide_mode = MagicMock()
mocked_slide_controller.is_live = True
media_env.media_controller.current_media_players = {'media player': mocked_media_player}
media_env.media_controller.is_theme_background = False
media_env.media_controller.live_hide_timer = MagicMock()
media_env.media_controller._define_display = MagicMock(return_value=mocked_display)
@ -538,6 +550,7 @@ def test_media_hide(media_env, registry):
assert 'media player' in media_env.media_controller.current_media_players
@pytest.mark.skip(reason="no way of currently testing this")
@pytest.mark.parametrize('file_name,media_length', TEST_MEDIA)
def test_media_length(file_name, media_length, media_env):
"""
@ -758,6 +771,7 @@ def test_media_play(media_env):
media_env.live_hide_timer = MagicMock()
mocked_controller = MagicMock()
mocked_controller.media_info.is_background = False
media_env.is_theme_background = False
# WHEN: media_play is called
result = media_env.media_play(mocked_controller)
@ -767,3 +781,203 @@ def test_media_play(media_env):
assert result is True
media_env.live_hide_timer.stop.assert_called_once_with()
mocked_controller._set_theme.assert_called_once()
def test_decide_autoplay_media_preview(media_env, settings):
"""
Test that media with a normal background behaves
"""
# GIVEN: A media controller and a service item
mocked_service_item = MagicMock()
mocked_service_item.requires_media.return_value = False
settings.setValue('core/auto unblank', True)
settings.setValue('media/media auto start', QtCore.Qt.CheckState.Unchecked)
media_env.media_controller.is_live = False
media_env.media_controller.is_theme_background = False
# WHEN: decide_autoplay() is called
ret = media_env.media_controller.decide_autoplay(mocked_service_item, media_env.media_controller, False)
# THEN: The current controller's media should be reset
assert ret is True, "The Media should have been autoplayed"
def test_decide_autoplay_media_normal_hidden_live(media_env, settings):
"""
Test that media with a normal background behaves
"""
# GIVEN: A media controller and a service item
mocked_service_item = MagicMock()
mocked_service_item.requires_media.return_value = True
settings.setValue('core/auto unblank', True)
settings.setValue('media/media auto start', QtCore.Qt.CheckState.Unchecked)
media_env.media_controller.is_live = True
media_env.media_controller.is_theme_background = True
# WHEN: decide_autoplay() is called
ret = media_env.media_controller.decide_autoplay(mocked_service_item, media_env.media_controller, HideMode.Theme)
# THEN: Autoplay will obey the following
assert ret is True, "The Media should have been autoplayed"
def test_decide_autoplay_media_normal_not_hidden_live(media_env, settings):
"""
Test that media with a normal background behaves
"""
# GIVEN: A media controller and a service item
mocked_service_item = MagicMock()
mocked_service_item.requires_media.return_value = True
settings.setValue('core/auto unblank', False)
settings.setValue('media/media auto start', QtCore.Qt.CheckState.Unchecked)
media_env.media_controller.is_live = True
media_env.media_controller.is_theme_background = False
# WHEN: decide_autoplay() is called
ret = media_env.media_controller.decide_autoplay(mocked_service_item, media_env.media_controller, HideMode.Screen)
# THEN: Autoplay will obey the following
assert ret is False, "The Media should have not be autoplayed"
def test_decide_autoplay_media_autostart_not_hidden_live(media_env, settings):
"""
Test that media with a normal background behaves
"""
# GIVEN: A media controller and a service item
mocked_service_item = MagicMock()
mocked_service_item.requires_media.return_value = False
mocked_service_item.item.will_auto_start.return_value = True
settings.setValue('core/auto unblank', False)
settings.setValue('media/media auto start', QtCore.Qt.CheckState.Unchecked)
media_env.media_controller.is_live = True
media_env.media_controller.is_theme_background = False
# WHEN: decide_autoplay() is called
ret = media_env.media_controller.decide_autoplay(mocked_service_item, media_env.media_controller, False)
# THEN: Autoplay will obey the following
assert ret is True, "The Media should have not be autoplayed"
def test_decide_autoplay_media_global_autostart_not_hidden_live(media_env, settings):
"""
Test that media with a normal background behaves
"""
# GIVEN: A media controller and a service item
mocked_service_item = MagicMock()
mocked_service_item.requires_media.return_value = False
mocked_service_item.item.will_auto_start.return_value = False
settings.setValue('core/auto unblank', False)
settings.setValue('media/media auto start', QtCore.Qt.CheckState.Checked)
media_env.media_controller.is_live = True
media_env.media_controller.is_theme_background = False
# WHEN: decide_autoplay() is called
ret = media_env.media_controller.decide_autoplay(mocked_service_item, media_env.media_controller, False)
# THEN: Autoplay will obey the following
assert ret is True, "The Media should have not be autoplayed"
def test_decide_autoplay_media_normal_autounblank_live(media_env, settings):
"""
Test that media with a normal background behaves
"""
# GIVEN: A media controller and a service item
mocked_service_item = MagicMock()
mocked_service_item.requires_media.return_value = True
settings.setValue('core/auto unblank', True)
settings.setValue('media/media auto start', QtCore.Qt.CheckState.Unchecked)
media_env.media_controller.is_live = True
media_env.media_controller.is_theme_background = False
# WHEN: decide_autoplay() is called
ret = media_env.media_controller.decide_autoplay(mocked_service_item, media_env.media_controller, HideMode.Screen)
# THEN: Autoplay will obey the following
assert ret is True, "The Media should have be autoplayed"
def test_media_bar_play(media_env, settings):
"""
Test that media bar is set correctly following a play event
"""
# GIVEN: A media controller and a service item
mocked_controller = MagicMock()
mocked_controller.media_info = ItemMediaInfo()
mocked_controller.mediabar = OpenLPToolbar(None)
mocked_controller.mediabar.actions['playbackPlay'] = MagicMock()
mocked_controller.mediabar.actions['playbackPause'] = MagicMock()
mocked_controller.mediabar.actions['playbackStop'] = MagicMock()
mocked_controller.mediabar.actions['playbackLoop'] = MagicMock()
media_env.is_theme_background = False
settings.setValue('media/live loop', False)
# WHEN: _media_bar() is called
media_env.media_controller._media_bar(mocked_controller, "play")
# THEN: The following functions should have been called
mocked_controller.mediabar.actions['playbackPlay'].setDisabled.assert_called_with(True)
mocked_controller.mediabar.actions['playbackPause'].setDisabled.assert_called_with(False)
mocked_controller.mediabar.actions['playbackStop'].setDisabled.assert_called_with(False)
@pytest.mark.parametrize("mode", ["pause", "stop", "reset"])
def test_media_bar_stop(media_env, settings, mode):
"""
Test that media bar is set correctly following a list of events
"""
# GIVEN: A media controller and a service item
mocked_controller = MagicMock()
mocked_controller.media_info = ItemMediaInfo()
mocked_controller.mediabar = OpenLPToolbar(None)
mocked_controller.mediabar.actions['playbackPlay'] = MagicMock()
mocked_controller.mediabar.actions['playbackPause'] = MagicMock()
mocked_controller.mediabar.actions['playbackStop'] = MagicMock()
mocked_controller.mediabar.actions['playbackLoop'] = MagicMock()
media_env.is_theme_background = False
settings.setValue('media/live loop', False)
# WHEN: _media_bar() is called
media_env.media_controller._media_bar(mocked_controller, mode)
# THEN: The following functions should have been called
mocked_controller.mediabar.actions['playbackPlay'].setDisabled.assert_called_with(False)
mocked_controller.mediabar.actions['playbackPause'].setDisabled.assert_called_with(True)
mocked_controller.mediabar.actions['playbackStop'].setDisabled.assert_called_with(False)
@pytest.mark.parametrize("back, stream, result", [(False, MediaType.Video, False),
(True, MediaType.Video, False),
(False, MediaType.Stream, True)])
def test_media_bar_loop_disabled(media_env, settings, back, stream, result):
"""
Test that media bar is set correctly following a list of events
"""
# GIVEN: A media controller and a service item
mocked_controller = MagicMock()
mocked_controller.media_info = ItemMediaInfo()
mocked_controller.is_live = True
mocked_controller.media_info.is_background = back
mocked_controller.media_info.media_type = stream
mocked_controller.mediabar = OpenLPToolbar(None)
mocked_controller.mediabar.actions['playbackPlay'] = MagicMock()
mocked_controller.mediabar.actions['playbackPause'] = MagicMock()
mocked_controller.mediabar.actions['playbackStop'] = MagicMock()
mocked_controller.mediabar.actions['playbackLoop'] = MagicMock()
media_env.is_theme_background = False
# WHEN: _media_bar() is called
media_env.media_controller._media_bar(mocked_controller, "load")
# THEN: The following functions should have been called
mocked_controller.mediabar.actions['playbackLoop'].setDisabled.assert_called_with(result)
@pytest.mark.parametrize("loop, result", [(False, False), (True, True)])
def test_media_bar_loop_checked(media_env, settings, loop, result):
"""
Test that media bar is set correctly following a list of events
"""
# GIVEN: A media controller and a service item
mocked_controller = MagicMock()
mocked_controller.media_info = ItemMediaInfo()
mocked_controller.is_live = True
mocked_controller.media_info.is_background = True
mocked_controller.media_info.media_type = MediaType.Video
mocked_controller.mediabar = OpenLPToolbar(None)
mocked_controller.mediabar.actions['playbackPlay'] = MagicMock()
mocked_controller.mediabar.actions['playbackPause'] = MagicMock()
mocked_controller.mediabar.actions['playbackStop'] = MagicMock()
mocked_controller.mediabar.actions['playbackLoop'] = MagicMock()
media_env.is_theme_background = False
settings.setValue('media/live loop', loop)
# WHEN: _media_bar() is called
media_env.media_controller._media_bar(mocked_controller, "load")
# THEN: The following functions should have been called
mocked_controller.mediabar.actions['playbackLoop'].setChecked.assert_called_with(result)