replace client-triggered state refresh with server-triggered
authorAndrew Lorimer <andrew@lorimer.id.au>
Sat, 8 May 2021 03:51:35 +0000 (13:51 +1000)
committerAndrew Lorimer <andrew@lorimer.id.au>
Sat, 8 May 2021 03:51:35 +0000 (13:51 +1000)
ppt_control/ppt_control.py
ppt_control/ppt_control_obs.py
ppt_control/static/ppt-control.js
index 6b0f8181ef68edd6bb72188e894b2026440cf5ac..f942b328f2c2a637ef94015b184a54fc1eaa9941 100755 (executable)
@@ -115,11 +115,7 @@ def state_event():
     return json.dumps({"type": "state", **STATE})\r
 \r
 \r
     return json.dumps({"type": "state", **STATE})\r
 \r
 \r
-def users_event():\r
-    return json.dumps({"type": "users", "count": len(USERS)})\r
-\r
-\r
-async def notify_state():\r
+def notify_state():\r
     global STATE\r
     if current_slideshow and STATE["connected"] == 1:\r
         try:\r
     global STATE\r
     if current_slideshow and STATE["connected"] == 1:\r
         try:\r
@@ -134,85 +130,86 @@ async def notify_state():
         STATE = copy(STATE_DEFAULT)\r
     if USERS:  # asyncio.wait doesn't accept an empty list\r
         message = state_event()\r
         STATE = copy(STATE_DEFAULT)\r
     if USERS:  # asyncio.wait doesn't accept an empty list\r
         message = state_event()\r
-        await asyncio.wait([user.send(message) for user in USERS])\r
-\r
-\r
-async def notify_users():\r
-    if USERS:  # asyncio.wait doesn't accept an empty list\r
-        message = users_event()\r
-        await asyncio.wait([user.send(message) for user in USERS])\r
-\r
-\r
-async def register(websocket):\r
-    USERS.add(websocket)\r
-    await notify_users()\r
+        loop.call_soon_threadsafe(ws_queue.put_nowait, state_event())\r
 \r
 \r
 \r
 \r
-async def unregister(websocket):\r
-    USERS.remove(websocket)\r
-    await notify_users()\r
 \r
 \r
+async def ws_handler(websocket, path):\r
+    logger.debug("Handling WebSocket connection")\r
+    recv_task = asyncio.ensure_future(ws_receive(websocket, path))\r
+    send_task = asyncio.ensure_future(ws_send(websocket, path))\r
+    done, pending = await asyncio.wait(\r
+        [recv_task, send_task],\r
+        return_when=asyncio.FIRST_COMPLETED,\r
+    )\r
+    for task in pending:\r
+        task.cancel()\r
 \r
 \r
-async def ws_handle(websocket, path):\r
+async def ws_receive(websocket, path):\r
     logger.debug("Received websocket request")\r
     logger.debug("Received websocket request")\r
-    global current_slideshow\r
-    # register(websocket) sends user_event() to websocket\r
-    await register(websocket)\r
+    USERS.add(websocket)\r
     try:\r
     try:\r
-        await websocket.send(state_event())\r
+        # Send initial state to clients on load\r
+        notify_state()\r
         async for message in websocket:\r
         async for message in websocket:\r
+            logger.debug("Received websocket message: " + str(message))\r
             data = json.loads(message)\r
             if data["action"] == "prev":\r
                 if current_slideshow:\r
                     current_slideshow.prev()\r
             data = json.loads(message)\r
             if data["action"] == "prev":\r
                 if current_slideshow:\r
                     current_slideshow.prev()\r
-                await notify_state()\r
+                notify_state()\r
             elif data["action"] == "next":\r
                 if current_slideshow:\r
                     current_slideshow.next()\r
             elif data["action"] == "next":\r
                 if current_slideshow:\r
                     current_slideshow.next()\r
-                await notify_state()\r
+                notify_state()\r
             elif data["action"] == "first":\r
                 if current_slideshow:\r
                     current_slideshow.first()\r
             elif data["action"] == "first":\r
                 if current_slideshow:\r
                     current_slideshow.first()\r
-                await notify_state()\r
+                notify_state()\r
             elif data["action"] == "last":\r
                 if current_slideshow:\r
                     current_slideshow.last()\r
             elif data["action"] == "last":\r
                 if current_slideshow:\r
                     current_slideshow.last()\r
-                await notify_state()\r
+                notify_state()\r
             elif data["action"] == "black":\r
                 if current_slideshow:\r
                     if current_slideshow.visible() == 3:\r
                         current_slideshow.normal()\r
                     else:\r
                         current_slideshow.black()\r
             elif data["action"] == "black":\r
                 if current_slideshow:\r
                     if current_slideshow.visible() == 3:\r
                         current_slideshow.normal()\r
                     else:\r
                         current_slideshow.black()\r
-                await notify_state()\r
+                notify_state()\r
             elif data["action"] == "white":\r
                 if current_slideshow:\r
                     if current_slideshow.visible() == 4:\r
                         current_slideshow.normal()\r
                     else:\r
                         current_slideshow.white()\r
             elif data["action"] == "white":\r
                 if current_slideshow:\r
                     if current_slideshow.visible() == 4:\r
                         current_slideshow.normal()\r
                     else:\r
                         current_slideshow.white()\r
-                await notify_state()\r
+                notify_state()\r
             elif data["action"] == "goto":\r
                 if current_slideshow:\r
                     current_slideshow.goto(int(data["value"]))\r
             elif data["action"] == "goto":\r
                 if current_slideshow:\r
                     current_slideshow.goto(int(data["value"]))\r
-                await notify_state()\r
-            elif data["action"] == "refresh":\r
-                await notify_state()\r
-                if current_slideshow:\r
-                    current_slideshow.export_current_next()\r
-                    current_slideshow.refresh()\r
+                notify_state()\r
             else:\r
             else:\r
-                logger.error("unsupported event: {}", data)\r
+                logger.error("Received unnsupported event: {}", data)\r
+            message = ""\r
     finally:\r
     finally:\r
-        await unregister(websocket)\r
+        USERS.remove(websocket)\r
+\r
+async def ws_send(websocket, path):\r
+    while True:\r
+        message = await ws_queue.get()\r
+        await asyncio.wait([user.send(message) for user in USERS])\r
+\r
 \r
 def run_ws():\r
     # https://stackoverflow.com/questions/21141217/how-to-launch-win32-applications-in-separate-threads-in-python/22619084#22619084\r
     # https://www.reddit.com/r/learnpython/comments/mwt4qi/pywintypescom_error_2147417842_the_application/\r
     pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED)\r
     asyncio.set_event_loop(asyncio.new_event_loop())\r
 \r
 def run_ws():\r
     # https://stackoverflow.com/questions/21141217/how-to-launch-win32-applications-in-separate-threads-in-python/22619084#22619084\r
     # https://www.reddit.com/r/learnpython/comments/mwt4qi/pywintypescom_error_2147417842_the_application/\r
     pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED)\r
     asyncio.set_event_loop(asyncio.new_event_loop())\r
-    #start_server = websockets.serve(ws_handle, "0.0.0.0", 5678, ping_interval=None)\r
-    start_server = websockets.serve(ws_handle, "0.0.0.0", 5678)\r
+    global ws_queue\r
+    ws_queue = asyncio.Queue()\r
+    global loop\r
+    loop = asyncio.get_event_loop()\r
+    start_server = websockets.serve(ws_handler, "0.0.0.0", 5678, ping_interval=None)\r
     asyncio.get_event_loop().run_until_complete(start_server)\r
     asyncio.get_event_loop().run_forever()\r
 \r
     asyncio.get_event_loop().run_until_complete(start_server)\r
     asyncio.get_event_loop().run_forever()\r
 \r
@@ -258,7 +255,6 @@ class Slideshow:
         connect_ppt()\r
 \r
     def refresh(self):\r
         connect_ppt()\r
 \r
     def refresh(self):\r
-        logger.debug("Refreshing")\r
         try:\r
             if self.instance is None:\r
                 raise ValueError("PPT instance cannot be None")\r
         try:\r
             if self.instance is None:\r
                 raise ValueError("PPT instance cannot be None")\r
@@ -388,7 +384,10 @@ class Slideshow:
                         pass\r
                     attempts += 1\r
             elif slide == self.total_slides() + 1:\r
                         pass\r
                     attempts += 1\r
             elif slide == self.total_slides() + 1:\r
-                shutil.copyfileobj(open(os.path.dirname(os.path.realpath(__file__)), r'''\static\black.jpg''', 'rb'), open(destination, 'wb'))\r
+                try:\r
+                    shutil.copyfileobj(open(os.path.dirname(os.path.realpath(__file__)), r'''\static\black.jpg''', 'rb'), open(destination, 'wb'))\r
+                except Exception as e:\r
+                    logger.warning("Failed to copy black slide: " + str(e))\r
             else:\r
                 pass\r
 \r
             else:\r
                 pass\r
 \r
@@ -407,10 +406,10 @@ def get_current_slideshow():
 \r
 def refresh_interval():\r
     while getattr(refresh_daemon, "do_run", True):\r
 \r
 def refresh_interval():\r
     while getattr(refresh_daemon, "do_run", True):\r
-        logger.debug("Triggering server-side refresh")\r
         current_slideshow.refresh()\r
         current_slideshow.refresh()\r
+        notify_state()\r
         refresh_status()\r
         refresh_status()\r
-        time.sleep(1)\r
+        time.sleep(0.5)\r
 \r
 def refresh_status():\r
     if status_label is not None:\r
 \r
 def refresh_status():\r
     if status_label is not None:\r
@@ -564,10 +563,10 @@ def start_interface():
     console_handler.setLevel(log_level)\r
     logger.addHandler(console_handler)\r
 \r
     console_handler.setLevel(log_level)\r
     logger.addHandler(console_handler)\r
 \r
-    logging.getLogger("asyncio").setLevel(logging.ERROR)\r
-    logging.getLogger("asyncio.coroutines").setLevel(logging.ERROR)\r
-    logging.getLogger("websockets.server").setLevel(logging.ERROR)\r
-    logging.getLogger("websockets.protocol").setLevel(logging.ERROR)\r
+    #logging.getLogger("asyncio").setLevel(logging.ERROR)\r
+    #logging.getLogger("asyncio.coroutines").setLevel(logging.ERROR)\r
+    #logging.getLogger("websockets.server").setLevel(logging.ERROR)\r
+    #logging.getLogger("websockets.protocol").setLevel(logging.ERROR)\r
 \r
 \r
     logger.debug("Finished setting up config and logging")\r
 \r
 \r
     logger.debug("Finished setting up config and logging")\r
index e36957eeaecf3e99a79ada9e5ba8b3314a80f7d3..aa2e44b7bb72e42a8f7634127404b0dae7102d0f 100755 (executable)
@@ -44,8 +44,9 @@ async def communicate():
         while True:\r
             if cmd:\r
                 try:\r
         while True:\r
             if cmd:\r
                 try:\r
-                    await websocket.send('{"action": "%s"}' % cmd)\r
+                    cmd_temp = cmd\r
                     cmd = ""\r
                     cmd = ""\r
+                    await websocket.send('{"action": "%s"}' % cmd_temp)\r
                 except websockets.ConnectionClosed as exc:\r
                     attempts += 1\r
                     if attempts == 4:\r
                 except websockets.ConnectionClosed as exc:\r
                     attempts += 1\r
                     if attempts == 4:\r
index 9bba1714f2b97a088cc6f4c39d6209cacb41bb17..2265ca3f25aa30e8f4e45b3bcae1715107ce44c4 100644 (file)
@@ -172,8 +172,13 @@ function disconnect() {
     users.textContent = "Connection to PowerPoint failed";
 }
 
     users.textContent = "Connection to PowerPoint failed";
 }
 
+function update_current() {
+}
+
+function update_next() {
+}
+
 websocket.onmessage = function (event) {
 websocket.onmessage = function (event) {
-       console.log("Received data");
     data = JSON.parse(event.data);
     switch (data.type) {
         case 'state':
     data = JSON.parse(event.data);
     switch (data.type) {
         case 'state':
@@ -183,40 +188,37 @@ websocket.onmessage = function (event) {
                break;
             }
             var d = new Date;
                break;
             }
             var d = new Date;
-            switch (data.visible) {
-                case 3:
-                    current_img.src = "/black.jpg";
-                    break;
-                case 4:
-                    current_img.src = "/white.jpg";
-                    break;
-                default:
-                    //current_img.src = "/cache/" + data.current + ".jpg?t=" + d.getTime();
-                    current_img.src = "/cache/" + data.current + ".jpg";
-                    break;
+            if (show_current.checked) {
+              switch (data.visible) {
+                  case 3:
+                      current_img.src = "/black.jpg";
+                      break;
+                  case 4:
+                      current_img.src = "/white.jpg";
+                      break;
+                  default:
+                      current_img.src = "/cache/" + data.current + ".jpg?t=" + d.getTime();
+                      //current_img.src = "/cache/" + data.current + ".jpg";
+                      break;
+              }
             }
             if (data.current == data.total + 1) { 
             }
             if (data.current == data.total + 1) { 
-                //next_img.src = "/cache/" + (data.total + 1) + ".jpg?t=" + d.getTime();
-                next_img.src = "/cache/" + (data.total + 1) + ".jpg";
+                next_img.src = "/cache/" + (data.total + 1) + ".jpg?t=" + d.getTime();
+                //next_img.src = "/cache/" + (data.total + 1) + ".jpg";
             } else {
             } else {
-                //next_img.src = "/cache/" + (data.current + 1) + ".jpg?t=" + d.getTime();
-                next_img.src = "/cache/" + (data.current + 1) + ".jpg";
+                next_img.src = "/cache/" + (data.current + 1) + ".jpg?t=" + d.getTime();
+                //next_img.src = "/cache/" + (data.current + 1) + ".jpg";
             }
 
             }
 
-                       if (document.activeElement != current) {
+            if (document.activeElement != current) {
                current.value = data.current;
             }
             total.textContent = data.total;
             document.title = data.name;
             break;
                current.value = data.current;
             }
             total.textContent = data.total;
             document.title = data.name;
             break;
-        case 'users':
-            users.textContent = (
-                data.count.toString() + " client" +
-                (data.count == 1 ? "" : "s"));
-            break;
         default:
             console.error(
         default:
             console.error(
-                "unsupported event", data);
+                "Unsupported event", data);
     }
        if (preloaded == false && ! isNaN(total.textContent)) {
                image = document.getElementById("preload_img");
     }
        if (preloaded == false && ! isNaN(total.textContent)) {
                image = document.getElementById("preload_img");
@@ -230,11 +232,3 @@ websocket.onmessage = function (event) {
        }
 
 };
        }
 
 };
-
-var interval = setInterval(refresh, 1000);
-
-function refresh() {
-       console.log("Refreshing")
-    websocket.send(JSON.stringify({action: 'refresh'}));
-}
-