← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~trb143/openlp/cherrypy into lp:openlp

 

Tim Bentley has proposed merging lp:~trb143/openlp/cherrypy into lp:openlp.

Requested reviews:
  Andreas Preikschat (googol)
Related bugs:
  Bug #826724 in OpenLP: "Make the Web Remote password protected"
  https://bugs.launchpad.net/openlp/+bug/826724
  Bug #826731 in OpenLP: "Add optional SSL to web remote"
  https://bugs.launchpad.net/openlp/+bug/826731

For more details, see:
https://code.launchpad.net/~trb143/openlp/cherrypy/+merge/167121

Add a /live option to the remote interface to all images of the live to be displayed.

This is a no secure option like stage.

Add comments to settings 

Minor cleanups
-- 
https://code.launchpad.net/~trb143/openlp/cherrypy/+merge/167121
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/ui/mainwindow.py'
--- openlp/core/ui/mainwindow.py	2013-04-20 20:34:46 +0000
+++ openlp/core/ui/mainwindow.py	2013-06-03 18:51:25 +0000
@@ -779,8 +779,8 @@
         """
         We need to make sure, that the SlidePreview's size is correct.
         """
-        self.preview_controller.previewSizeChanged()
-        self.live_controller.previewSizeChanged()
+        self.preview_controller.preview_size_changed()
+        self.live_controller.preview_size_changed()
 
     def on_settings_shortcuts_item_clicked(self):
         """
@@ -989,8 +989,8 @@
         self.application.set_busy_cursor()
         self.image_manager.update_display()
         self.renderer.update_display()
-        self.preview_controller.screenSizeChanged()
-        self.live_controller.screenSizeChanged()
+        self.preview_controller.screen_size_changed()
+        self.live_controller.screen_size_changed()
         self.setFocus()
         self.activateWindow()
         self.application.set_normal_cursor()

=== modified file 'openlp/core/ui/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py	2013-04-23 21:46:07 +0000
+++ openlp/core/ui/slidecontroller.py	2013-06-03 18:51:25 +0000
@@ -89,7 +89,7 @@
         Set up the Slide Controller.
         """
         DisplayController.__init__(self, parent, is_live)
-        Registry().register_function(u'bootstrap_post_set_up', self.screenSizeChanged)
+        Registry().register_function(u'bootstrap_post_set_up', self.screen_size_changed)
         self.screens = ScreenList()
         try:
             self.ratio = float(self.screens.current[u'size'].width()) / float(self.screens.current[u'size'].height())
@@ -121,6 +121,8 @@
         self.update_slide_limits()
         self.panel = QtGui.QWidget(parent.controlSplitter)
         self.slideList = {}
+        self.slide_count = 0
+        self.slide_image = None
         # Layout for holding panel
         self.panel_layout = QtGui.QVBoxLayout(self.panel)
         self.panel_layout.setSpacing(0)
@@ -321,18 +323,18 @@
         self.slide_layout.insertWidget(0, self.preview_display)
         self.preview_display.hide()
         # Actual preview screen
-        self.slidePreview = QtGui.QLabel(self)
-        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
-        sizePolicy.setHorizontalStretch(0)
-        sizePolicy.setVerticalStretch(0)
-        sizePolicy.setHeightForWidth(self.slidePreview.sizePolicy().hasHeightForWidth())
-        self.slidePreview.setSizePolicy(sizePolicy)
-        self.slidePreview.setFrameShape(QtGui.QFrame.Box)
-        self.slidePreview.setFrameShadow(QtGui.QFrame.Plain)
-        self.slidePreview.setLineWidth(1)
-        self.slidePreview.setScaledContents(True)
-        self.slidePreview.setObjectName(u'slidePreview')
-        self.slide_layout.insertWidget(0, self.slidePreview)
+        self.slide_preview = QtGui.QLabel(self)
+        size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
+        size_policy.setHorizontalStretch(0)
+        size_policy.setVerticalStretch(0)
+        size_policy.setHeightForWidth(self.slide_preview.sizePolicy().hasHeightForWidth())
+        self.slide_preview.setSizePolicy(size_policy)
+        self.slide_preview.setFrameShape(QtGui.QFrame.Box)
+        self.slide_preview.setFrameShadow(QtGui.QFrame.Plain)
+        self.slide_preview.setLineWidth(1)
+        self.slide_preview.setScaledContents(True)
+        self.slide_preview.setObjectName(u'slide_preview')
+        self.slide_layout.insertWidget(0, self.slide_preview)
         self.grid.addLayout(self.slide_layout, 0, 0, 1, 1)
         if self.is_live:
             self.current_shortcut = u''
@@ -517,10 +519,9 @@
                     self.service_manager.next_item()
             self.keypress_loop = False
 
-    def screenSizeChanged(self):
+    def screen_size_changed(self):
         """
-        Settings dialog has changed the screen size of adjust output and
-        screen previews.
+        Settings dialog has changed the screen size of adjust output and screen previews.
         """
         # rebuild display as screen size changed
         if self.display:
@@ -536,14 +537,14 @@
         except ZeroDivisionError:
             self.ratio = 1
         self.media_controller.setup_display(self.display, False)
-        self.previewSizeChanged()
+        self.preview_size_changed()
         self.preview_display.setup()
         service_item = ServiceItem()
         self.preview_display.web_view.setHtml(build_html(service_item, self.preview_display.screen, None, self.is_live,
             plugins=self.plugin_manager.plugins))
         self.media_controller.setup_display(self.preview_display, True)
         if self.service_item:
-            self.refreshServiceItem()
+            self.refresh_service_item()
 
     def __addActionsToWidget(self, widget):
         """
@@ -554,7 +555,7 @@
             self.previousService, self.nextService,
             self.escapeItem])
 
-    def previewSizeChanged(self):
+    def preview_size_changed(self):
         """
         Takes care of the SlidePreview's size. Is called when one of the the
         splitters is moved or when the screen size is changed. Note, that this
@@ -563,14 +564,14 @@
         if self.ratio < float(self.preview_frame.width()) / float(self.preview_frame.height()):
             # We have to take the height as limit.
             max_height = self.preview_frame.height() - self.grid.margin() * 2
-            self.slidePreview.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height))
+            self.slide_preview.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height))
             self.preview_display.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height))
             self.preview_display.screen = {
                 u'size': self.preview_display.geometry()}
         else:
             # We have to take the width as limit.
             max_width = self.preview_frame.width() - self.grid.margin() * 2
-            self.slidePreview.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio))
+            self.slide_preview.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio))
             self.preview_display.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio))
             self.preview_display.screen = {
                 u'size': self.preview_display.geometry()}
@@ -624,17 +625,16 @@
         """
         self.slide_limits = Settings().value(self.main_window.advanced_settings_section + u'/slide limits')
 
-    def enableToolBar(self, item):
+    def enable_tool_bar(self, item):
         """
-        Allows the toolbars to be reconfigured based on Controller Type
-        and ServiceItem Type
+        Allows the toolbars to be reconfigured based on Controller Type and ServiceItem Type
         """
         if self.is_live:
-            self.enableLiveToolBar(item)
+            self.enable_live_tool_bar(item)
         else:
-            self.enablePreviewToolBar(item)
+            self.enable_preview_tool_bar(item)
 
-    def enableLiveToolBar(self, item):
+    def enable_live_tool_bar(self, item):
         """
         Allows the live toolbar to be customised
         """
@@ -663,7 +663,7 @@
         # See bug #791050
         self.toolbar.show()
 
-    def enablePreviewToolBar(self, item):
+    def enable_preview_tool_bar(self, item):
         """
         Allows the Preview toolbar to be customised
         """
@@ -682,15 +682,15 @@
         # See bug #791050
         self.toolbar.show()
 
-    def refreshServiceItem(self):
+    def refresh_service_item(self):
         """
         Method to update the service item if the screen has changed
         """
-        log.debug(u'refreshServiceItem live = %s' % self.is_live)
+        log.debug(u'refresh_service_item live = %s' % self.is_live)
         if self.service_item.is_text() or self.service_item.is_image():
             item = self.service_item
             item.render()
-            self._processItem(item, self.selected_row)
+            self._process_item(item, self.selected_row)
 
     def add_service_item(self, item):
         """
@@ -703,14 +703,14 @@
         if self.song_edit:
             slideno = self.selected_row
         self.song_edit = False
-        self._processItem(item, slideno)
+        self._process_item(item, slideno)
 
     def replaceServiceManagerItem(self, item):
         """
         Replacement item following a remote edit
         """
         if item == self.service_item:
-            self._processItem(item, self.preview_list_widget.currentRow())
+            self._process_item(item, self.preview_list_widget.currentRow())
 
     def addServiceManagerItem(self, item, slideno):
         """
@@ -729,7 +729,7 @@
             self.__checkUpdateSelectedSlide(slidenum)
             self.slideSelected()
         else:
-            self._processItem(item, slidenum)
+            self._process_item(item, slidenum)
             if self.is_live and item.auto_play_slides_loop and item.timed_slide_interval > 0:
                 self.play_slides_loop.setChecked(item.auto_play_slides_loop)
                 self.delay_spin_box.setValue(int(item.timed_slide_interval))
@@ -739,7 +739,7 @@
                 self.delay_spin_box.setValue(int(item.timed_slide_interval))
                 self.onPlaySlidesOnce()
 
-    def _processItem(self, service_item, slideno):
+    def _process_item(self, service_item, slideno):
         """
         Loads a ServiceItem into the system from ServiceManager
         Display the slide number passed
@@ -827,10 +827,9 @@
         self.preview_list_widget.setVerticalHeaderLabels(text)
         if self.service_item.is_text():
             self.preview_list_widget.resizeRowsToContents()
-        self.preview_list_widget.setColumnWidth(0,
-            self.preview_list_widget.viewport().size().width())
+        self.preview_list_widget.setColumnWidth(0, self.preview_list_widget.viewport().size().width())
         self.__updatePreviewSelection(slideno)
-        self.enableToolBar(service_item)
+        self.enable_tool_bar(service_item)
         # Pass to display for viewing.
         # Postpone image build, we need to do this later to avoid the theme
         # flashing on the screen
@@ -1050,27 +1049,28 @@
 
     def updatePreview(self):
         """
-        This updates the preview frame, for example after changing a slide or
-        using *Blank to Theme*.
+        This updates the preview frame, for example after changing a slide or using *Blank to Theme*.
         """
         log.debug(u'updatePreview %s ' % self.screens.current[u'primary'])
         if not self.screens.current[u'primary'] and self.service_item and \
                 self.service_item.is_capable(ItemCapabilities.ProvidesOwnDisplay):
-            # Grab now, but try again in a couple of seconds if slide change
-            # is slow
-            QtCore.QTimer.singleShot(0.5, self.grabMainDisplay)
-            QtCore.QTimer.singleShot(2.5, self.grabMainDisplay)
+            # Grab now, but try again in a couple of seconds if slide change is slow
+            QtCore.QTimer.singleShot(0.5, self.grab_maindisplay)
+            QtCore.QTimer.singleShot(2.5, self.grab_maindisplay)
         else:
-            self.slidePreview.setPixmap(self.display.preview())
+            self.slide_image = self.display.preview()
+            self.slide_preview.setPixmap(self.slide_image)
+        self.slide_count += 1
 
-    def grabMainDisplay(self):
+    def grab_maindisplay(self):
         """
         Creates an image of the current screen and updates the preview frame.
         """
-        winid = QtGui.QApplication.desktop().winId()
+        win_id = QtGui.QApplication.desktop().winId()
         rect = self.screens.current[u'size']
-        winimg = QtGui.QPixmap.grabWindow(winid, rect.x(), rect.y(), rect.width(), rect.height())
-        self.slidePreview.setPixmap(winimg)
+        win_image = QtGui.QPixmap.grabWindow(win_id, rect.x(), rect.y(), rect.width(), rect.height())
+        self.slide_preview.setPixmap(win_image)
+        self.slide_image = win_image
 
     def on_slide_selected_next_action(self, checked):
         """
@@ -1276,7 +1276,7 @@
         self.media_controller.video(self.controller_type, item, self.hide_mode())
         if not self.is_live:
             self.preview_display.show()
-            self.slidePreview.hide()
+            self.slide_preview.hide()
 
     def onMediaClose(self):
         """
@@ -1285,7 +1285,7 @@
         log.debug(u'SlideController onMediaClose')
         self.media_controller.media_reset(self)
         self.preview_display.hide()
-        self.slidePreview.show()
+        self.slide_preview.show()
 
     def _resetBlank(self):
         """

=== added file 'openlp/plugins/remotes/html/live.css'
--- openlp/plugins/remotes/html/live.css	1970-01-01 00:00:00 +0000
+++ openlp/plugins/remotes/html/live.css	2013-06-03 18:51:25 +0000
@@ -0,0 +1,39 @@
+/******************************************************************************
+* OpenLP - Open Source Lyrics Projection                                      *
+* --------------------------------------------------------------------------- *
+* Copyright (c) 2008-2013 Raoul Snyman                                        *
+* Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan      *
+* Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      *
+* Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   *
+* Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          *
+* Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,             *
+* Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock,              *
+* Frode Woldsund, Martin Zibricky                                             *
+* --------------------------------------------------------------------------- *
+* 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; version 2 of the License.                              *
+*                                                                             *
+* 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, write to the Free Software Foundation, Inc., 59  *
+* Temple Place, Suite 330, Boston, MA 02111-1307 USA                          *
+******************************************************************************/
+body {
+  background-color: black;
+  font-family: sans-serif;
+  overflow: hidden;
+}
+
+.size {
+    position: absolute;
+    top: 0px;
+    vertical-align: middle;
+    height: 100%;
+    background-size: cover;
+    background-repeat: no-repeat;
+}
\ No newline at end of file

=== added file 'openlp/plugins/remotes/html/live.html'
--- openlp/plugins/remotes/html/live.html	1970-01-01 00:00:00 +0000
+++ openlp/plugins/remotes/html/live.html	2013-06-03 18:51:25 +0000
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html>
+<!--
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2013 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          #
+# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,             #
+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock,              #
+# Frode Woldsund, Martin Zibricky, Patrick Zimmermann                         #
+# --------------------------------------------------------------------------- #
+# 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; version 2 of the License.                              #
+#                                                                             #
+# 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, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+-->
+<head>
+  <meta charset="utf-8" />
+  <title>${live_title}</title>
+  <link rel="stylesheet" href="/files/live.css" />
+  <link rel="shortcut icon" type="image/x-icon" href="/files/images/favicon.ico">
+  <script type="text/javascript" src="/files/jquery.js"></script>
+  <script type="text/javascript" src="/files/live.js"></script>
+</head>
+<body>
+<img id="image" class="size"/>
+</body>
+</html>
\ No newline at end of file

=== added file 'openlp/plugins/remotes/html/live.js'
--- openlp/plugins/remotes/html/live.js	1970-01-01 00:00:00 +0000
+++ openlp/plugins/remotes/html/live.js	2013-06-03 18:51:25 +0000
@@ -0,0 +1,52 @@
+/******************************************************************************
+ * OpenLP - Open Source Lyrics Projection                                      *
+ * --------------------------------------------------------------------------- *
+ * Copyright (c) 2008-2013 Raoul Snyman                                        *
+ * Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan      *
+ * Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub,      *
+ * Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer.   *
+ * Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru,          *
+ * Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,             *
+ * Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock,              *
+ * Frode Woldsund, Martin Zibricky                                             *
+ * --------------------------------------------------------------------------- *
+ * 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; version 2 of the License.                              *
+ *                                                                             *
+ * 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, write to the Free Software Foundation, Inc., 59  *
+ * Temple Place, Suite 330, Boston, MA 02111-1307 USA                          *
+ ******************************************************************************/
+window.OpenLP = {
+  loadSlide: function (event) {
+    $.getJSON(
+      "/live/image",
+      function (data, status) {
+        var img = document.getElementById('image');
+        img.src = data.results.slide_image;
+        img.style.display = 'block';
+      }
+    );
+  },
+  pollServer: function () {
+    $.getJSON(
+      "/live/poll",
+      function (data, status) {
+        if (OpenLP.slideCount != data.results.slide_count) {
+          OpenLP.slideCount = data.results.slide_count;
+          OpenLP.loadSlide();
+        }
+      }
+    );
+  }
+}
+$.ajaxSetup({ cache: false });
+setInterval("OpenLP.pollServer();", 500);
+OpenLP.pollServer();
+

=== modified file 'openlp/plugins/remotes/lib/httpserver.py'
--- openlp/plugins/remotes/lib/httpserver.py	2013-04-23 20:31:19 +0000
+++ openlp/plugins/remotes/lib/httpserver.py	2013-06-03 18:51:25 +0000
@@ -124,7 +124,7 @@
 from mako.template import Template
 from PyQt4 import QtCore
 
-from openlp.core.lib import Registry, Settings, PluginStatus, StringContent
+from openlp.core.lib import Registry, Settings, PluginStatus, StringContent, image_to_byte
 from openlp.core.utils import AppLocation, translate
 
 from cherrypy._cpcompat import sha, ntob
@@ -136,6 +136,7 @@
     """
     Create an encrypted password for the given password.
     """
+    log.debug("make_sha_hash")
     return sha(ntob(password)).hexdigest()
 
 
@@ -143,6 +144,7 @@
     """
     Fetch the password for a provided user.
     """
+    log.debug("Fetch Password")
     if username != Settings().value(u'remotes/user id'):
         return None
     return make_sha_hash(Settings().value(u'remotes/password'))
@@ -175,9 +177,11 @@
         self.root = self.Public()
         self.root.files = self.Files()
         self.root.stage = self.Stage()
+        self.root.live = self.Live()
         self.root.router = self.router
         self.root.files.router = self.router
         self.root.stage.router = self.router
+        self.root.live.router = self.router
         cherrypy.tree.mount(self.root, '/', config=self.define_config())
         # Turn off the flood of access messages cause by poll
         cherrypy.log.access_log.propagate = False
@@ -213,6 +217,9 @@
                                      u'tools.basic_auth.on': False},
                          u'/stage': {u'tools.staticdir.on': True,
                                      u'tools.staticdir.dir': self.router.html_dir,
+                                     u'tools.basic_auth.on': False},
+                         u'/live': {u'tools.staticdir.on': True,
+                                     u'tools.staticdir.dir': self.router.html_dir,
                                      u'tools.basic_auth.on': False}}
         return directory_config
 
@@ -239,7 +246,16 @@
 
     class Stage(object):
         """
-        Stageview is read only so security is not relevant and would reduce it's usability
+        Stage view is read only so security is not relevant and would reduce it's usability
+        """
+        @cherrypy.expose
+        def default(self, *args, **kwargs):
+            url = urlparse.urlparse(cherrypy.url())
+            return self.router.process_http_request(url.path, *args)
+
+    class Live(object):
+        """
+        Live view is read only so security is not relevant and would reduce it's usability
         """
         @cherrypy.expose
         def default(self, *args, **kwargs):
@@ -265,9 +281,12 @@
         self.routes = [
             (u'^/$', self.serve_file),
             (u'^/(stage)$', self.serve_file),
+            (u'^/(live)$', self.serve_file),
             (r'^/files/(.*)$', self.serve_file),
             (r'^/api/poll$', self.poll),
             (r'^/stage/poll$', self.poll),
+            (r'^/live/poll$', self.live_poll),
+            (r'^/live/image$', self.live_image),
             (r'^/api/controller/(live|preview)/(.*)$', self.controller),
             (r'^/stage/controller/(live|preview)/(.*)$', self.controller),
             (r'^/api/service/(.*)$', self.service),
@@ -305,6 +324,7 @@
         if response:
             return response
         else:
+            log.debug('Path not found %s', url_path)
             return self._http_not_found()
 
     def _get_service_items(self):
@@ -334,6 +354,7 @@
         self.template_vars = {
             'app_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Remote'),
             'stage_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Stage View'),
+            'live_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Live View'),
             'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
             'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
             'alerts': translate('RemotePlugin.Mobile', 'Alerts'),
@@ -359,18 +380,19 @@
 
     def serve_file(self, filename=None):
         """
-        Send a file to the socket. For now, just a subset of file types
-        and must be top level inside the html folder.
+        Send a file to the socket. For now, just a subset of file types and must be top level inside the html folder.
         If subfolders requested return 404, easier for security for the present.
 
-        Ultimately for i18n, this could first look for xx/file.html before
-        falling back to file.html... where xx is the language, e.g. 'en'
+        Ultimately for i18n, this could first look for xx/file.html before falling back to file.html.
+        where xx is the language, e.g. 'en'
         """
         log.debug(u'serve file request %s' % filename)
         if not filename:
             filename = u'index.html'
         elif filename == u'stage':
             filename = u'stage.html'
+        elif filename == u'live':
+            filename = u'live.html'
         path = os.path.normpath(os.path.join(self.html_dir, filename))
         if not path.startswith(self.html_dir):
             return self._http_not_found()
@@ -425,6 +447,26 @@
         cherrypy.response.headers['Content-Type'] = u'application/json'
         return json.dumps({u'results': result})
 
+    def live_poll(self):
+        """
+        Poll OpenLP to determine the current slide count.
+        """
+        result = {
+            u'slide_count': self.live_controller.slide_count
+        }
+        cherrypy.response.headers['Content-Type'] = u'application/json'
+        return json.dumps({u'results': result})
+
+    def live_image(self):
+        """
+        Return the latest display image as a byte stream.
+        """
+        result = {
+            u'slide_image': u'data:image/png;base64,' + str(image_to_byte(self.live_controller.slide_image))
+        }
+        cherrypy.response.headers['Content-Type'] = u'application/json'
+        return json.dumps({u'results': result})
+
     def display(self, action):
         """
         Hide or show the display screen.

=== modified file 'openlp/plugins/remotes/lib/remotetab.py'
--- openlp/plugins/remotes/lib/remotetab.py	2013-03-29 20:58:06 +0000
+++ openlp/plugins/remotes/lib/remotetab.py	2013-06-03 18:51:25 +0000
@@ -86,6 +86,12 @@
         self.stage_url.setObjectName(u'stage_url')
         self.stage_url.setOpenExternalLinks(True)
         self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
+        self.live_url_label = QtGui.QLabel(self.http_settings_group_box)
+        self.live_url_label.setObjectName(u'live_url_label')
+        self.live_url = QtGui.QLabel(self.http_settings_group_box)
+        self.live_url.setObjectName(u'live_url')
+        self.live_url.setOpenExternalLinks(True)
+        self.http_setting_layout.addRow(self.live_url_label, self.live_url)
         self.left_layout.addWidget(self.http_settings_group_box)
         self.https_settings_group_box = QtGui.QGroupBox(self.left_column)
         self.https_settings_group_box.setCheckable(True)
@@ -116,6 +122,12 @@
         self.stage_https_url.setObjectName(u'stage_https_url')
         self.stage_https_url.setOpenExternalLinks(True)
         self.https_settings_layout.addRow(self.stage_https_url_label, self.stage_https_url)
+        self.live_https_url_label = QtGui.QLabel(self.https_settings_group_box)
+        self.live_https_url_label.setObjectName(u'live_url_label')
+        self.live_https_url = QtGui.QLabel(self.https_settings_group_box)
+        self.live_https_url.setObjectName(u'live_https_url')
+        self.live_https_url.setOpenExternalLinks(True)
+        self.https_settings_layout.addRow(self.live_https_url_label, self.live_https_url)
         self.left_layout.addWidget(self.https_settings_group_box)
         self.user_login_group_box = QtGui.QGroupBox(self.left_column)
         self.user_login_group_box.setCheckable(True)
@@ -163,6 +175,7 @@
         self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:'))
         self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:'))
         self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
+        self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:'))
         self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
         self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App'))
         self.qr_description_label.setText(translate('RemotePlugin.RemoteTab',
@@ -176,6 +189,7 @@
         self.https_port_label.setText(self.port_label.text())
         self.remote_https_url_label.setText(self.remote_url_label.text())
         self.stage_https_url_label.setText(self.stage_url_label.text())
+        self.live_https_url_label.setText(self.live_url_label.text())
         self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication'))
         self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:'))
         self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:'))
@@ -203,10 +217,14 @@
         https_url = u'https://%s:%s/' % (ip_address, self.https_port_spin_box.value())
         self.remote_url.setText(u'<a href="%s">%s</a>' % (http_url, http_url))
         self.remote_https_url.setText(u'<a href="%s">%s</a>' % (https_url, https_url))
-        http_url += u'stage'
-        https_url += u'stage'
-        self.stage_url.setText(u'<a href="%s">%s</a>' % (http_url, http_url))
-        self.stage_https_url.setText(u'<a href="%s">%s</a>' % (https_url, https_url))
+        http_url_temp = http_url + u'stage'
+        https_url_temp = https_url + u'stage'
+        self.stage_url.setText(u'<a href="%s">%s</a>' % (http_url_temp, http_url_temp))
+        self.stage_https_url.setText(u'<a href="%s">%s</a>' % (https_url_temp, https_url_temp))
+        http_url_temp = http_url + u'live'
+        https_url_temp = https_url + u'live'
+        self.live_url.setText(u'<a href="%s">%s</a>' % (http_url_temp, http_url_temp))
+        self.live_https_url.setText(u'<a href="%s">%s</a>' % (https_url_temp, https_url_temp))
 
     def load(self):
         """


Follow ups