<!DOCTYPE html>\r
<html>\r
- <head>\r
-<style>\r
-img {\r
- #width: 100%;\r
- border: 3px solid #000;\r
-}\r
-h1 {\r
- font-size: 20px;\r
- font-weight: 300;\r
- margin-bottom: 0;\r
-}\r
-#slide_label {\r
- float: right;\r
- font-size: 16px;\r
-}\r
-::-webkit-scrollbar { \r
- display: none; \r
-}\r
-body {\r
- background: #3a393a;\r
- color: #efefef;\r
- font-family: sans-serif;\r
- #max-width: 500px;\r
-}\r
-</style>\r
- <title>WebSocket demo</title>\r
+ <head>\r
+ <link href="style.css" rel="stylesheet" type="text/css" media="all" />\r
+ <script src="settings.js"></script>\r
+ <title>ppt-control</title>\r
</head>\r
- <body>\r
- <div>\r
- <h1>Current slide</h1>\r
- <img id="current_img" src="/current.jpg" />\r
+ <body onload="initSettings();">\r
+ <div id="img_container">\r
+ <div id="current_div">\r
+ <h1>Current slide</h1>\r
+ <img id="current_img" src="/black.jpg" />\r
+ </div>\r
+ <div id="next_div">\r
+ <h1>Next slide</h1>\r
+ <img id="next_img" src="/black.jpg" />\r
+ </div>\r
</div>\r
- <div>\r
- <h1>Next slide</h1>\r
- <img id="next_img" src="/next.jpg" />\r
+\r
+ <div id="controls_container">\r
+ <p>\r
+ <button id="prev">Prev</button>\r
+ <button id="next">Next</button>\r
+ <button id="first">First</button>\r
+ <button id="last">Last</button>\r
+ <button id="black">Black</button>\r
+ <button id="white">White</button>\r
+ <span id="count"><span id="slide_label">Current: </span><input type="text" id="current"></input>/<span id="total">?</span></span>\r
+ </p>\r
+\r
+ <input type="checkbox" checked="true" id="show_current">Show current slide</input>\r
+ <input type="checkbox" checked="true" id="show_next">Show next slide</input>\r
+\r
+ <p class="users">Not connected</p>\r
</div>\r
- <p>\r
- <button id="prev">Prev</button>\r
- <button id="next">Next</button>\r
- <button id="first">First</button>\r
- <button id="last">Last</button>\r
- <span id="slide_label">Current: <span id="slide"></span>\r
- </p>\r
- <div class="state">\r
- <span class="users">?</span>\r
- </div>\r
- \r
- <script>\r
- function imageRefresh(id) {\r
- img = document.getElementById(id);\r
- var d = new Date;\r
- var http = img.src;\r
- if (http.indexOf("?t=") != -1) { http = http.split("?t=")[0]; } \r
- img.src = http + '?t=' + d.getTime();\r
- }\r
- var prev = document.querySelector('#prev'),\r
- next = document.querySelector('#next'),\r
- first = document.querySelector('#first'),\r
- last = document.querySelector('#last'),\r
- slide = document.querySelector('#slide'),\r
- users = document.querySelector('.users'),\r
- websocket = new WebSocket("ws://" + window.location.host + ":5678/");\r
- prev.onclick = function (event) {\r
- websocket.send(JSON.stringify({action: 'prev'}));\r
- }\r
- next.onclick = function (event) {\r
- websocket.send(JSON.stringify({action: 'next'}));\r
- }\r
- first.onclick = function (event) {\r
- websocket.send(JSON.stringify({action: 'first'}));\r
- }\r
- last.onclick = function (event) {\r
- websocket.send(JSON.stringify({action: 'last'}));\r
- }\r
- websocket.onmessage = function (event) {\r
- data = JSON.parse(event.data);\r
- switch (data.type) {\r
- case 'state':\r
- slide.textContent = data.value;\r
- imageRefresh("current_img");\r
- imageRefresh("next_img");\r
- break;\r
- case 'users':\r
- users.textContent = (\r
- data.count.toString() + " client" +\r
- (data.count == 1 ? "" : "s"));\r
- break;\r
- default:\r
- console.error(\r
- "unsupported event", data);\r
- }\r
- };\r
- </script>\r
+\r
+ <script src="ppt-control.js"></script>\r
+\r
</body>\r
-</html>
\ No newline at end of file
+</html>\r
+import sys\r
+sys.coinit_flags= 0\r
import win32com.client\r
import pywintypes\r
import os\r
import asyncio\r
import websockets\r
import logging, json\r
+import urllib\r
+import posixpath\r
+import time\r
+import pythoncom\r
\r
logging.basicConfig()\r
\r
-powerpoint = None\r
-cache = r'''C:\Windows\Temp'''\r
+global current_slideshow\r
+current_slideshow = None\r
+CACHEDIR = r'''C:\Windows\Temp\ppt-cache'''\r
+CACHE_TIMEOUT = 2*60*60\r
\r
-class Handler(server.CGIHTTPRequestHandler):\r
+class Handler(server.SimpleHTTPRequestHandler):\r
def __init__(self, *args, **kwargs):\r
super().__init__(*args, directory=os.path.dirname(os.path.realpath(__file__)))\r
+ \r
+ def translate_path(self, path):\r
+ """Translate a /-separated PATH to the local filename syntax.\r
+\r
+ Components that mean special things to the local file system\r
+ (e.g. drive or directory names) are ignored. (XXX They should\r
+ probably be diagnosed.)\r
+\r
+ """\r
+ # abandon query parameters\r
+ path = path.split('?',1)[0]\r
+ path = path.split('#',1)[0]\r
+ # Don't forget explicit trailing slash when normalizing. Issue17324\r
+ trailing_slash = path.rstrip().endswith('/')\r
+ try:\r
+ path = urllib.parse.unquote(path, errors='surrogatepass')\r
+ except UnicodeDecodeError:\r
+ path = urllib.parse.unquote(path)\r
+ path = posixpath.normpath(path)\r
+ words = path.split('/')\r
+ words = list(filter(None, words))\r
+ if len(words) > 0 and words[0] == "cache":\r
+ if current_slideshow:\r
+ path = CACHEDIR + "\\" + current_slideshow.name()\r
+ words.pop(0)\r
+ else:\r
+ path = self.directory\r
+ for word in words:\r
+ if os.path.dirname(word) or word in (os.curdir, os.pardir):\r
+ # Ignore components that are not a simple file/directory name\r
+ continue\r
+ path = os.path.join(path, word)\r
+ if trailing_slash:\r
+ path += '/'\r
+ print(path)\r
+ return path\r
+\r
\r
def run_http():\r
http_server = server.HTTPServer(("", 80), Handler)\r
http_server.serve_forever()\r
\r
-async def first(websocket, path):\r
- slideshow_view_first()\r
- await websocket.send(True)\r
-\r
-STATE = {"value": "?", "visible": "1"}\r
-\r
+STATE = {"connected": 0, "current": 0, "total": 0, "visible": 0}\r
USERS = set()\r
\r
\r
\r
\r
async def notify_state():\r
- STATE["value"] = str(current_slide()) + "/" + str(total_slides())\r
+ global current_slideshow\r
+ if current_slideshow:\r
+ STATE["current"] = current_slideshow.current_slide()\r
+ STATE["total"] = current_slideshow.total_slides()\r
+ STATE["visible"] = current_slideshow.visible()\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 ws_handle(websocket, path):\r
+ global current_slideshow\r
# register(websocket) sends user_event() to websocket\r
await register(websocket)\r
try:\r
async for message in websocket:\r
data = json.loads(message)\r
if data["action"] == "prev":\r
- slideshow_view_previous()\r
+ if current_slideshow:\r
+ current_slideshow.prev()\r
await notify_state()\r
elif data["action"] == "next":\r
- slideshow_view_next()\r
+ if current_slideshow:\r
+ current_slideshow.next()\r
await notify_state()\r
elif data["action"] == "first":\r
- slideshow_view_first()\r
+ if current_slideshow:\r
+ current_slideshow.first()\r
await notify_state()\r
elif data["action"] == "last":\r
- slideshow_view_last()\r
+ if current_slideshow:\r
+ current_slideshow.last()\r
+ await 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
+ await 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
+ await notify_state()\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
+ if current_slideshow:\r
+ current_slideshow.export_current_next()\r
+ current_slideshow.refresh()\r
await notify_state()\r
else:\r
logging.error("unsupported event: {}", data)\r
await unregister(websocket)\r
\r
def run_ws():\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)\r
+ print("Initialised websocket server")\r
asyncio.get_event_loop().run_until_complete(start_server)\r
+ print("Running websocket server until complete") \r
asyncio.get_event_loop().run_forever()\r
\r
def start_server():\r
- STATE["value"] = current_slide()\r
+ #STATE["current"] = current_slide()\r
http_daemon = threading.Thread(name="http_daemon", target=run_http)\r
http_daemon.setDaemon(True)\r
http_daemon.start()\r
+ print("Started HTTP server")\r
\r
- run_ws()\r
- #ws_daemon = threading.Thread(name="ws_daemon", target=run_ws)\r
- #ws_daemon.setDaemon(True)\r
- #ws_daemon.start()\r
+ #run_ws()\r
+ \r
+ ws_daemon = threading.Thread(name="ws_daemon", target=run_ws)\r
+ ws_daemon.setDaemon(True)\r
+ ws_daemon.start()\r
+ print("Started websocket server")\r
\r
#try:\r
# ws_daemon.start()\r
# cleanup_stop_thread()\r
# sys.exit()\r
\r
-\r
-def get_slideshow_view():\r
- global powerpoint\r
-\r
- if powerpoint is None:\r
- powerpoint = win32com.client.Dispatch('Powerpoint.Application')\r
-\r
- if powerpoint is None:\r
- return\r
-\r
- ssw = powerpoint.SlideShowWindows\r
- if ssw.Count == 0:\r
- return\r
-\r
- # https://docs.microsoft.com/en-us/office/vba/api/powerpoint.slideshowwindow.view\r
- ssv = ssw[0].View\r
-\r
- return ssv\r
-\r
-def get_activepresentation():\r
- global powerpoint\r
-\r
- if powerpoint is None:\r
- powerpoint = win32com.client.Dispatch('Powerpoint.Application')\r
-\r
- if powerpoint is None:\r
- return\r
-\r
- activepres = powerpoint.ActivePresentation\r
- return activepres\r
-\r
-def total_slides():\r
- ssp = get_activepresentation()\r
- if ssp:\r
- return len(ssp.Slides)\r
-\r
-def current_slide():\r
- ssv = get_slideshow_view()\r
- if ssv:\r
- return ssv.CurrentShowPosition\r
-\r
-def export(slide):\r
- global cache\r
- ssp = get_activepresentation()\r
- if ssp:\r
- for (slide, name) in [(slide, "current"), (slide+1, "next")]:\r
- if slide < len(ssp.Slides):\r
- ssp.Slides(slide).Export(os.path.dirname(os.path.realpath(__file__)) + r'''\\''' + name + r'''0.jpg''', "JPG")\r
+class Slideshow:\r
+ def __init__(self, instance):\r
+ self.instance = instance\r
+ if self.instance is None:\r
+ raise ValueError("PPT instance cannot be None")\r
+\r
+ if self.instance.SlideShowWindows.Count == 0:\r
+ raise ValueError("PPT instance has no slideshow windows")\r
+ self.view = self.instance.SlideShowWindows[0].View\r
+\r
+ if self.instance.ActivePresentation is None:\r
+ raise ValueError("PPT instance has no active presentation")\r
+ self.presentation = self.instance.ActivePresentation\r
+ \r
+ self.export_all()\r
+\r
+ def refresh(self):\r
+ if self.instance is None:\r
+ raise ValueError("PPT instance cannot be None")\r
+\r
+ #if self.instance.SlideShowWindows.Count == 0:\r
+ # raise ValueError("PPT instance has no slideshow windows")\r
+ self.view = self.instance.SlideShowWindows[0].View\r
+\r
+ if self.instance.ActivePresentation is None:\r
+ raise ValueError("PPT instance has no active presentation")\r
+\r
+ def total_slides(self):\r
+ return len(self.presentation.Slides)\r
+\r
+ def current_slide(self):\r
+ return self.view.CurrentShowPosition\r
+\r
+ def visible(self):\r
+ return self.view.State\r
+\r
+ def prev(self):\r
+ self.refresh()\r
+ self.view.Previous()\r
+\r
+ def next(self):\r
+ self.refresh()\r
+ self.view.Next()\r
+ self.export_current_next()\r
+\r
+ def first(self):\r
+ self.refresh()\r
+ self.view.First()\r
+ self.export_current_next()\r
+ \r
+ def last(self):\r
+ self.refresh()\r
+ self.view.Last()\r
+ self.export_current_next()\r
+\r
+ def goto(self, slide):\r
+ self.refresh()\r
+ if slide <= self.total_slides():\r
+ self.view.GotoSlide(slide)\r
+ else:\r
+ self.last()\r
+ self.next()\r
+ self.export_current_next()\r
+\r
+ def black(self):\r
+ self.refresh()\r
+ self.view.State = 3\r
+\r
+ def white(self):\r
+ self.refresh()\r
+ self.view.State = 4\r
+\r
+ def normal(self):\r
+ self.refresh()\r
+ self.view.State = 1\r
+\r
+ def name(self):\r
+ return self.presentation.Name\r
+\r
+ def export_current_next(self):\r
+ self.export(self.current_slide())\r
+ self.export(self.current_slide() + 1)\r
+\r
+ def export(self, slide):\r
+ destination = CACHEDIR + "\\" + self.name() + "\\" + str(slide) + ".jpg"\r
+ os.makedirs(os.path.dirname(destination), exist_ok=True)\r
+ if not os.path.exists(destination) or time.time() - os.path.getmtime(destination) > CACHE_TIMEOUT:\r
+ if slide <= self.total_slides():\r
attempts = 0\r
while attempts < 3:\r
try:\r
- os.replace(os.path.dirname(os.path.realpath(__file__)) + r'''\\''' + name + r'''0.jpg''', os.path.dirname(os.path.realpath(__file__)) + r'''\\''' + name + '''.jpg''')\r
+ self.presentation.Slides(slide).Export(destination, "JPG")\r
+ time.sleep(0.5)\r
+ break\r
except:\r
pass\r
attempts += 1\r
+ elif slide == self.total_slides() + 1:\r
+ shutil.copyfileobj(open(os.path.dirname(os.path.realpath(__file__)) + r'''\black.jpg''', 'rb'), open(destination, 'wb'))\r
else:\r
- shutil.copyfileobj(open(os.path.dirname(os.path.realpath(__file__)) + r'''\blank.jpg''', 'rb'), open(os.path.dirname(os.path.realpath(__file__)) + r'''\\''' + name + r'''next.jpg''', 'wb'))\r
-\r
-def slideshow_view_first():\r
- ssv = get_slideshow_view()\r
- if ssv:\r
- ssv.First()\r
- export(ssv.CurrentShowPosition)\r
-\r
-def slideshow_view_previous():\r
- ssv = get_slideshow_view()\r
- if ssv:\r
- ssv.Previous()\r
- export(ssv.CurrentShowPosition)\r
-\r
-def slideshow_view_next():\r
- ssv = get_slideshow_view()\r
- if ssv:\r
- ssv.Next()\r
- export(ssv.CurrentShowPosition)\r
- \r
-\r
-def slideshow_view_last():\r
- ssv = get_slideshow_view()\r
- if ssv:\r
- ssv.Last()\r
- export(ssv.CurrentShowPosition)\r
-\r
-def slideshow_view_black():\r
- ssv = get_slideshow_view()\r
- if ssv:\r
- ssv.State = 3\r
-\r
-def slideshow_view_white():\r
- ssv = get_slideshow_view()\r
- if ssv:\r
- ssv.State = 4\r
-\r
-def slideshow_view_normal():\r
- ssv = get_slideshow_view()\r
- if ssv:\r
- ssv.State = 1\r
+ pass\r
+\r
+ def export_all(self):\r
+ for i in range(1, self.total_slides()):\r
+ self.export(i)\r
+\r
+def get_ppt_instance():\r
+ instance = win32com.client.Dispatch('Powerpoint.Application')\r
+ if instance is None or instance.SlideShowWindows.Count == 0:\r
+ return None\r
+ return instance\r
+\r
+def get_current_slideshow():\r
+ print(str(current_slideshow))\r
+ return current_slideshow\r
+\r
\r
if __name__ == "__main__":\r
+\r
start_server()\r
+ \r
+ while True:\r
+ # Check if PowerPoint is running\r
+ instance = get_ppt_instance()\r
+ try:\r
+ current_slideshow = Slideshow(instance)\r
+ STATE["connected"] = 1\r
+ STATE["current"] = current_slideshow.current_slide()\r
+ STATE["total"] = current_slideshow.total_slides()\r
+ print("Connected to PowerPoint instance " + str(get_current_slideshow()))\r
+ break\r
+ except ValueError as e:\r
+ current_slideshow = None\r
+ pass\r
+ time.sleep(1)\r
--- /dev/null
+function imageRefresh(id) {
+ img = document.getElementById(id);
+ var d = new Date;
+ var http = img.src;
+ if (http.indexOf("?t=") != -1) { http = http.split("?t=")[0]; }
+ img.src = http + '?t=' + d.getTime();
+}
+
+function startWebsocket() {
+ ws = new WebSocket("ws://" + window.location.host + ":5678/");
+ ws.onclose = function(){
+ //websocket = null;
+ setTimeout(function(){startWebsocket()}, 10000);
+ }
+ return ws;
+}
+
+var websocket = startWebsocket();
+
+//if (window.obssstudio) {
+//}
+
+var prev = document.querySelector('#prev'),
+ next = document.querySelector('#next'),
+ first = document.querySelector('#first'),
+ last = document.querySelector('#last'),
+ black = document.querySelector('#black'),
+ white = document.querySelector('#white'),
+ slide_label = document.querySelector('#slide_label'),
+ current = document.querySelector('#current'),
+ total = document.querySelector('#total'),
+ users = document.querySelector('.users'),
+ prev_img = document.querySelector('#prev_img'),
+ next_img = document.querySelector('#next_img'),
+ current_div = document.querySelector('#current_div'),
+ next_div = document.querySelector('#next_div'),
+ show_current = document.querySelector('#show_current'),
+ show_next = document.querySelector('#show_next');
+
+prev.onclick = function (event) {
+ websocket.send(JSON.stringify({action: 'prev'}));
+}
+
+next.onclick = function (event) {
+ websocket.send(JSON.stringify({action: 'next'}));
+}
+
+first.onclick = function (event) {
+ websocket.send(JSON.stringify({action: 'first'}));
+}
+
+last.onclick = function (event) {
+ websocket.send(JSON.stringify({action: 'last'}));
+}
+
+black.onclick = function (event) {
+ websocket.send(JSON.stringify({action: 'black'}));
+}
+
+white.onclick = function (event) {
+ websocket.send(JSON.stringify({action: 'white'}));
+}
+
+current.onblur = function (event) {
+ websocket.send(JSON.stringify({action: 'goto', value: current.value}));
+}
+
+current.addEventListener('keyup',function(e){
+ if (e.which == 13) this.blur();
+});
+
+function sync_current() {
+ console.log("State of current checkbox changed");
+ if (show_current.checked) {
+ current_div.style.display = "block";
+ slide_label.style.display = "none";
+ next_div.style.width = "25%";
+ } else {
+ current_div.style.display = "none";
+ slide_label.style.display = "block";
+ next_div.style.width = "95%";
+ }
+ saveSettings();
+}
+show_current.onclick = sync_current;
+
+function sync_next() {
+ console.log("State of next checkbox changed");
+ if (show_next.checked) {
+ next_div.style.display = "block";
+ current_div.style.width = "70%";
+ } else {
+ next_div.style.display = "none";
+ current_div.style.width = "95%";
+ }
+ saveSettings();
+}
+show_next.onclick = sync_next;
+
+websocket.onmessage = function (event) {
+ data = JSON.parse(event.data);
+ switch (data.type) {
+ case 'state':
+ 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();
+ break;
+ }
+ 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";
+ } else {
+ //next_img.src = "/cache/" + (data.current + 1) + ".jpg?t=" + d.getTime();
+ next_img.src = "/cache/" + (data.current + 1) + ".jpg";
+ }
+
+ if (document.activeElement != current) {
+ current.value = data.current;
+ }
+ total.textContent = data.total;
+ break;
+ case 'users':
+ users.textContent = (
+ data.count.toString() + " client" +
+ (data.count == 1 ? "" : "s"));
+ break;
+ default:
+ console.error(
+ "unsupported event", data);
+ }
+};
+
+var interval = setInterval(refresh, 5000);
+
+function refresh() {
+ websocket.send(JSON.stringify({action: 'refresh'}));
+}