Stop videos playing if no VLC.

#633
This commit is contained in:
Tim Bentley 2020-10-04 19:20:10 +00:00 committed by Tomas Groth
parent 13f47d2a92
commit 867779da43
12 changed files with 251 additions and 40 deletions

View File

@ -34,6 +34,7 @@ class Poller(RegistryProperties):
def raw_poll(self):
return {
'counter': self.live_controller.slide_count if self.live_controller.slide_count else 0,
'service': self.service_manager.service_id,
'slide': self.live_controller.selected_row or 0,
'item': self.live_controller.service_item.unique_identifier if self.live_controller.service_item else '',

View File

@ -33,6 +33,7 @@ log = logging.getLogger(__name__)
@service_views.route('/items')
def service_items():
log.debug('service/v2/items')
live_controller = Registry().get('live_controller')
service_items = []
if live_controller.service_item:
@ -61,6 +62,7 @@ def service_items():
@service_views.route('/show/<item_id>', methods=['POST'])
@login_required
def service_set(item_id=None):
log.debug('service/v2/show')
data = request.json
item_id = item_id or data.get('id') or data.get('uid')
if not item_id:
@ -81,6 +83,7 @@ def service_set(item_id=None):
@service_views.route('/progress', methods=['POST'])
@login_required
def service_direction():
log.debug('service/v2/progress')
ALLOWED_ACTIONS = ['next', 'previous']
data = request.json
if not data:
@ -97,5 +100,6 @@ def service_direction():
@service_views.route('/new', methods=['GET'])
@login_required
def new_service():
log.debug('service/v2/new')
getattr(Registry().get('service_manager'), 'servicemanager_new_file').emit()
return '', 204

View File

@ -31,7 +31,6 @@ import os
import sys
import time
from datetime import datetime
from logging.handlers import RotatingFileHandler
from pathlib import Path
from shutil import copytree
from traceback import format_exception
@ -307,7 +306,7 @@ def set_up_logging(log_path):
"""
create_paths(log_path, do_not_log=True)
file_path = log_path / 'openlp.log'
logfile = RotatingFileHandler(str(file_path), 'w', encoding='UTF-8', backupCount=2)
logfile = logging.FileHandler(file_path, 'w', encoding='UTF-8')
logfile.setFormatter(logging.Formatter('%(asctime)s %(threadName)s %(name)-55s %(levelname)-8s %(message)s'))
log.addHandler(logfile)
if log.isEnabledFor(logging.DEBUG):

View File

@ -29,7 +29,7 @@ from openlp.core.common.registry import Registry
DO_NOT_TRACE_EVENTS = ['timerEvent', 'paintEvent', 'drag_enter_event', 'drop_event', 'on_controller_size_changed',
'preview_size_changed', 'resizeEvent', 'eventFilter']
'preview_size_changed', 'resizeEvent', 'eventFilter', 'tick']
class LogMixin(object):

View File

@ -1001,8 +1001,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
self.application.set_normal_cursor()
# if a warning has been shown within the last 5 seconds, skip showing again to avoid spamming user,
# also do not show if the settings window is visible
if not self.settings_form.isVisible() and \
not self.screen_change_timestamp or (datetime.now() - self.screen_change_timestamp).seconds > 5:
if not self.settings_form.isVisible() and not self.screen_change_timestamp or \
self.screen_change_timestamp and (datetime.now() - self.screen_change_timestamp).seconds > 5:
QtWidgets.QMessageBox.warning(self, translate('OpenLP.MainWindow', 'Screen setup has changed'),
translate('OpenLP.MainWindow',
'The screen setup has changed. '

View File

@ -56,6 +56,11 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
"""
The implementation of the Media Controller which manages how media is played.
"""
def __init__(self, parent=None):
"""
"""
super(MediaController, self).__init__(parent)
self.log_info('MediaController Initialising')
def setup(self):
self.vlc_player = None
@ -66,8 +71,8 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
self.preview_timer = QtCore.QTimer()
self.preview_timer.setInterval(TICK_TIME)
# Signals
self.live_timer.timeout.connect(self.media_state_live)
self.preview_timer.timeout.connect(self.media_state_preview)
self.live_timer.timeout.connect(self._media_state_live)
self.preview_timer.timeout.connect(self._media_state_preview)
Registry().register_function('playbackPlay', self.media_play_msg)
Registry().register_function('playbackPause', self.media_pause_msg)
Registry().register_function('playbackStop', self.media_stop_msg)
@ -135,7 +140,7 @@ 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):
"""
Decides which controller to use.
@ -145,35 +150,35 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
return self.live_controller
return self.preview_controller
def media_state_live(self):
def _media_state_live(self):
"""
Check if there is a running Live media Player and do updating stuff (e.g. update the UI)
"""
display = self._define_display(self.display_controllers(DisplayControllerType.Live))
display = self._define_display(self._display_controllers(DisplayControllerType.Live))
if DisplayControllerType.Live in self.current_media_players:
self.current_media_players[DisplayControllerType.Live].resize(self.live_controller)
self.current_media_players[DisplayControllerType.Live].update_ui(self.live_controller, display)
self.tick(self.display_controllers(DisplayControllerType.Live))
self.tick(self._display_controllers(DisplayControllerType.Live))
if self.current_media_players[DisplayControllerType.Live].get_live_state() is not MediaState.Playing:
self.live_timer.stop()
else:
self.live_timer.stop()
self.media_stop(self.display_controllers(DisplayControllerType.Live))
self.media_stop(self._display_controllers(DisplayControllerType.Live))
def media_state_preview(self):
def _media_state_preview(self):
"""
Check if there is a running Preview media Player and do updating stuff (e.g. update the UI)
"""
display = self._define_display(self.display_controllers(DisplayControllerType.Preview))
display = self._define_display(self._display_controllers(DisplayControllerType.Preview))
if DisplayControllerType.Preview in self.current_media_players:
self.current_media_players[DisplayControllerType.Preview].resize(self.live_controller)
self.current_media_players[DisplayControllerType.Preview].update_ui(self.preview_controller, display)
self.tick(self.display_controllers(DisplayControllerType.Preview))
self.tick(self._display_controllers(DisplayControllerType.Preview))
if self.current_media_players[DisplayControllerType.Preview].get_preview_state() is not MediaState.Playing:
self.preview_timer.stop()
else:
self.preview_timer.stop()
self.media_stop(self.display_controllers(DisplayControllerType.Preview))
self.media_stop(self._display_controllers(DisplayControllerType.Preview))
def setup_display(self, controller, preview):
"""
@ -218,7 +223,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
:param hidden: The player which is doing the playing
"""
is_valid = True
controller = self.display_controllers(source)
controller = self._display_controllers(source)
# stop running videos
self.media_reset(controller)
controller.media_info = ItemMediaInfo()
@ -246,27 +251,27 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
if controller.is_live:
# if this is an optical device use special handling
if service_item.is_capable(ItemCapabilities.IsOptical):
log.debug('video is optical and live')
self.log_debug('video is optical and live')
path = service_item.get_frame_path()
(name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(path)
is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display,
controller)
elif service_item.is_capable(ItemCapabilities.CanStream):
log.debug('video is stream and live')
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:
log.debug('video is not optical or stream, but live')
self.log_debug('video is not optical or stream, but live')
controller.media_info.length = service_item.media_length
is_valid = self._check_file_type(controller, display)
controller.media_info.start_time = service_item.start_time
controller.media_info.end_time = service_item.end_time
elif controller.preview_display:
if service_item.is_capable(ItemCapabilities.IsOptical):
log.debug('video is optical and preview')
self.log_debug('video is optical and preview')
path = service_item.get_frame_path()
(name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(path)
is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display,
@ -278,7 +283,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
controller.media_info.file_info = (mrl, options)
is_valid = self._check_file_type(controller, display)
else:
log.debug('video is not optical or stream, but preview')
self.log_debug('video is not optical or stream, but preview')
controller.media_info.length = service_item.media_length
is_valid = self._check_file_type(controller, display)
if not is_valid:
@ -286,7 +291,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
critical_error_message_box(translate('MediaPlugin.MediaItem', 'Unsupported File'),
translate('MediaPlugin.MediaItem', 'Unsupported File'))
return False
log.debug('video media type: {tpe} '.format(tpe=str(controller.media_info.media_type)))
self.log_debug('video media type: {tpe} '.format(tpe=str(controller.media_info.media_type)))
autoplay = False
if service_item.requires_media():
autoplay = True
@ -305,7 +310,8 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
translate('MediaPlugin.MediaItem', 'Unsupported File'))
return False
self.set_controls_visible(controller, True)
log.debug('use {nm} controller'.format(nm=self.current_media_players[controller.controller_type].display_name))
self.log_debug('use {nm} controller'.
format(nm=self.current_media_players[controller.controller_type].display_name))
return True
@staticmethod
@ -417,7 +423,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
"""
Responds to the request to play a loaded video from the web.
"""
return self.media_play(Registry().get('live_controller'), False)
return self.media_play(self.live_controller)
def media_play(self, controller, first_time=True):
"""
@ -426,6 +432,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
:param controller: The controller to be played
:param first_time:
"""
self.log_debug(f"media_play with {first_time}")
controller.seek_slider.blockSignals(True)
controller.volume_slider.blockSignals(True)
display = self._define_display(controller)
@ -457,6 +464,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
display.hide_display(HideMode.Screen)
controller._set_theme(controller.service_item)
display.load_verses([{"verse": "v1", "text": "", "footer": " "}])
controller.output_has_changed()
return True
def tick(self, controller):
@ -501,7 +509,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
"""
Responds to the request to pause a loaded video from the web.
"""
return self.media_pause(Registry().get('live_controller'))
return self.media_pause(self.live_controller)
def media_pause(self, controller):
"""
@ -515,6 +523,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
controller.mediabar.actions['playbackStop'].setDisabled(False)
controller.mediabar.actions['playbackPause'].setVisible(False)
controller.media_info.is_playing = False
controller.output_has_changed()
return True
return False
@ -548,7 +557,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
"""
Responds to the request to stop a loaded video from the web.
"""
return self.media_stop(Registry().get('live_controller'))
return self.media_stop(self.live_controller)
def media_stop(self, controller):
"""
@ -572,6 +581,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
controller.media_timer = 0
display = self._define_display(controller)
display.show_display()
controller.output_has_changed()
return True
return False
@ -592,7 +602,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
:param controller: The Controller to use
:param volume: The volume to be set
"""
log.debug('media_volume {vol}'.format(vol=volume))
self.log_debug(f'media_volume {volume}')
if controller.is_live:
self.settings.setValue('media/live volume', volume)
else:
@ -692,8 +702,8 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties):
"""
self.live_timer.stop()
self.preview_timer.stop()
self.media_reset(self.display_controllers(DisplayControllerType.Live))
self.media_reset(self.display_controllers(DisplayControllerType.Preview))
self.media_reset(self._display_controllers(DisplayControllerType.Live))
self.media_reset(self._display_controllers(DisplayControllerType.Preview))
@staticmethod
def _define_display(controller):

View File

@ -41,6 +41,7 @@ def media_play():
if live.service_item.name != 'media':
abort(400)
status = live.mediacontroller_live_play.emit()
log.debug(f'media_play return {status}')
if status:
return '', 202
abort(400)
@ -54,6 +55,7 @@ def media_pause():
if live.service_item.name != 'media':
abort(400)
status = live.mediacontroller_live_pause.emit()
log.debug(f'media_pause return {status}')
if status:
return '', 202
abort(400)
@ -67,6 +69,7 @@ def media_stop():
if live.service_item.name != 'media':
abort(400)
status = live.mediacontroller_live_stop.emit()
log.debug(f'media_stop return {status}')
if status:
return '', 202
abort(400)

View File

@ -171,7 +171,7 @@ class VlcPlayer(MediaPlayer):
if not controller.vlc_instance:
return False
vlc = get_vlc()
log.debug('load vid in Vlc Controller')
log.debug('load video in VLC Controller')
path = None
if file and not controller.media_info.media_type == MediaType.Stream:
path = os.path.normcase(file)

View File

@ -129,9 +129,6 @@ class SettingsForm(QtWidgets.QDialog, Ui_SettingsDialog, RegistryProperties):
tab_widget = self.stacked_layout.widget(tab_index)
if tab_widget.tab_title == plugin_name:
tab_widget.save()
# if the image background has been changed we need to regenerate the image cache
if 'images_config_updated' in self.processes or 'config_screen_changed' in self.processes:
self.register_post_process('images_regenerate')
# Now lets process all the post save handlers
while self.processes:
Registry().execute(self.processes.pop(0))

View File

@ -204,6 +204,7 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
self.update_slide_limits()
self.panel = QtWidgets.QWidget(self.main_window.control_splitter)
self.slide_list = {}
self.slide_count = 0
self.ignore_toolbar_resize_events = False
# Layout for holding panel
self.panel_layout = QtWidgets.QVBoxLayout(self.panel)
@ -1242,6 +1243,13 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
self.preview_display.set_single_image('#000', image_path)
else:
self.preview_display.go_to_slide(self.selected_row)
self.output_has_changed()
def output_has_changed(self):
"""
The output has changed so we need to poke to POLL Websocket.
"""
self.slide_count += 1
def display_maindisplay(self):
"""
@ -1462,11 +1470,15 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties):
def on_clear(self):
"""
Clear the preview bar.
Clear the preview bar and other bits.
"""
self.preview_widget.clear_list()
self.toolbar.set_widget_visible('editSong', False)
self.toolbar.set_widget_visible('clear', False)
self.service_item = None
if self.is_live:
self.mediabar.setVisible(False)
else:
self.toolbar.set_widget_visible('editSong', False)
self.toolbar.set_widget_visible('clear', False)
def on_preview_add_to_service(self):
"""

View File

@ -219,7 +219,7 @@ def test_on_media_play(media_env):
media_env.media_controller.on_media_play()
# The mocked live controller should be called
media_env.media_controller.media_play.assert_called_once_with(mocked_live_controller, False)
media_env.media_controller.media_play.assert_called_once_with(mocked_live_controller)
def test_on_media_pause(media_env):
@ -265,7 +265,7 @@ def test_display_controllers_live(media_env):
Registry().register('preview_controller', mocked_preview_controller)
# WHEN: display_controllers() is called with DisplayControllerType.Live
controller = media_env.media_controller.display_controllers(DisplayControllerType.Live)
controller = media_env.media_controller._display_controllers(DisplayControllerType.Live)
# THEN: the controller should be the live controller
assert controller is mocked_live_controller
@ -282,7 +282,7 @@ def test_display_controllers_preview(media_env):
Registry().register('preview_controller', mocked_preview_controller)
# WHEN: display_controllers() is called with DisplayControllerType.Preview
controller = media_env.media_controller.display_controllers(DisplayControllerType.Preview)
controller = media_env.media_controller._display_controllers(DisplayControllerType.Preview)
# THEN: the controller should be the live controller
assert controller is mocked_preview_controller

View File

@ -0,0 +1,185 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2020 OpenLP Developers #
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
Package to test the openlp.core.ui.media.remote package.
"""
import pytest
from unittest.mock import MagicMock
from openlp.core.api import app as flask_app
from openlp.core.common.registry import Registry
from openlp.core.ui.media.remote import media_play, media_pause, media_stop, register_views
def test_media_play_valid(settings):
# GIVEN: A loaded service with media set up
settings.setValue('api/authentication enabled', False)
service_item = MagicMock()
service_item.name = 'media'
live_controller = MagicMock()
live_controller.service_item = service_item
Registry().register('live_controller', live_controller)
# WHEN: Play is pressed
ret = media_play()
# THEN: We should have a successful outcome
assert live_controller.mediacontroller_live_play.emit.call_count == 1, 'Should have be called once'
assert ret[1] == 202, 'Should be a valid call'
def test_media_play_not_media(settings):
# GIVEN: A loaded service with songs not media
settings.setValue('api/authentication enabled', False)
service_item = MagicMock()
service_item.name = 'songs'
live_controller = MagicMock()
live_controller.service_item = service_item
Registry().register('live_controller', live_controller)
# WHEN: Play is pressed
with pytest.raises(Exception) as e:
_ = media_play()
# THEN: We should have a rejected outcome
assert e.value.code == 400, 'Should be an invalid call'
assert live_controller.mediacontroller_live_play.emit.call_count == 0, 'Should have be not have been called'
def test_media_play_call_fail(settings):
# GIVEN: A loaded service with no media_controller
settings.setValue('api/authentication enabled', False)
service_item = MagicMock()
service_item.name = 'media'
live_controller = MagicMock()
live_controller.service_item = service_item
live_controller.mediacontroller_live_play.emit.return_value = False
Registry().register('live_controller', live_controller)
# WHEN: Play is pressed
with pytest.raises(Exception) as e:
_ = media_play()
# THEN: We should have a rejected outcome
assert e.value.code == 400, 'Should be an invalid call'
assert live_controller.mediacontroller_live_play.emit.call_count == 1, 'Should have be called once'
def test_media_pause_valid(settings):
# GIVEN: A loaded service with media set up
settings.setValue('api/authentication enabled', False)
service_item = MagicMock()
service_item.name = 'media'
live_controller = MagicMock()
live_controller.service_item = service_item
Registry().register('live_controller', live_controller)
# WHEN: Pause is pressed
ret = media_pause()
# THEN: We should have a successful outcome
assert live_controller.mediacontroller_live_pause.emit.call_count == 1, 'Should have be called once'
assert ret[1] == 202, 'Should be a valid call'
def test_media_pause_not_media(settings):
# GIVEN: A loaded service with songs not media
settings.setValue('api/authentication enabled', False)
service_item = MagicMock()
service_item.name = 'songs'
live_controller = MagicMock()
live_controller.service_item = service_item
Registry().register('live_controller', live_controller)
# WHEN: Pause is pressed
with pytest.raises(Exception) as e:
_ = media_pause()
# THEN: We should have a rejected outcome
assert e.value.code == 400, 'Should be an invalid call'
assert live_controller.mediacontroller_live_pause.emit.call_count == 0, 'Should have be not have been called'
def test_media_pause_call_fail(settings):
# GIVEN: A loaded service with no media_controller
settings.setValue('api/authentication enabled', False)
service_item = MagicMock()
service_item.name = 'media'
live_controller = MagicMock()
live_controller.service_item = service_item
live_controller.mediacontroller_live_pause.emit.return_value = False
Registry().register('live_controller', live_controller)
# WHEN: Pause is pressed
with pytest.raises(Exception) as e:
_ = media_pause()
# THEN: We should have a rejected outcome
assert e.value.code == 400, 'Should be an invalid call'
assert live_controller.mediacontroller_live_pause.emit.call_count == 1, 'Should have be called once'
def test_media_stop_valid(settings):
# GIVEN: A loaded service with media set up
settings.setValue('api/authentication enabled', False)
service_item = MagicMock()
service_item.name = 'media'
live_controller = MagicMock()
live_controller.service_item = service_item
Registry().register('live_controller', live_controller)
# WHEN: Stop is pressed
ret = media_stop()
# THEN: We should have a successful outcome
assert live_controller.mediacontroller_live_stop.emit.call_count == 1, 'Should have be called once'
assert ret[1] == 202, 'Should be a valid call'
def test_media_stop_not_media(settings):
# GIVEN: A loaded service with songs not media
settings.setValue('api/authentication enabled', False)
service_item = MagicMock()
service_item.name = 'songs'
live_controller = MagicMock()
live_controller.service_item = service_item
Registry().register('live_controller', live_controller)
# WHEN: Stop is pressed
with pytest.raises(Exception) as e:
_ = media_stop()
# THEN: We should have a rejected outcome
assert e.value.code == 400, 'Should be an invalid call'
assert live_controller.mediacontroller_live_stop.emit.call_count == 0, 'Should have be not have been called'
def test_media_stop_call_fail(settings):
# GIVEN: A loaded service with no media_controller
settings.setValue('api/authentication enabled', False)
service_item = MagicMock()
service_item.name = 'media'
live_controller = MagicMock()
live_controller.service_item = service_item
live_controller.mediacontroller_live_stop.emit.return_value = False
Registry().register('live_controller', live_controller)
# WHEN: Stop is pressed
with pytest.raises(Exception) as e:
_ = media_stop()
# THEN: We should have a rejected outcome
assert e.value.code == 400, 'Should be an invalid call'
assert live_controller.mediacontroller_live_stop.emit.call_count == 1, 'Should have be called once'
def test_register():
# GIVEN: A blank setup
# WHEN: I register the views for media
register_views()
# THEN: The following elements should have been defined.
assert len(flask_app.blueprints['v2-media-controller'].deferred_functions) == 3, \
'we should have 3 functions defined'
assert 'v2-media-controller.media_play' in flask_app.view_functions, 'we should the play defined'
assert 'v2-media-controller.media_pause' in flask_app.view_functions, 'we should the pause defined'
assert 'v2-media-controller.media_stop' in flask_app.view_functions, 'we should the stop defined'