From: Andrew Lorimer Date: Tue, 27 Apr 2021 09:02:25 +0000 (+1000) Subject: fix threading bugs, add further HTML controls X-Git-Tag: v0.0.1~5 X-Git-Url: https://git.lorimer.id.au/ppt-control.git/diff_plain/384420f45de9bd39a4c0f1e2a89d876c52efe1c9 fix threading bugs, add further HTML controls --- diff --git a/black.jpg b/black.jpg new file mode 100755 index 0000000..7b05935 Binary files /dev/null and b/black.jpg differ diff --git a/blank.jpg b/blank.jpg deleted file mode 100755 index 7b05935..0000000 Binary files a/blank.jpg and /dev/null differ diff --git a/index.html b/index.html index d9522b9..a1a4c3f 100755 --- a/index.html +++ b/index.html @@ -1,97 +1,40 @@ - - - WebSocket demo + + + + ppt-control - -
-

Current slide

- + +
+
+

Current slide

+ +
+
+

Next slide

+ +
-
-

Next slide

- + +
+

+ + + + + + + Current: /? +

+ + Show current slide + Show next slide + +

Not connected

-

- - - - - Current: -

-
- ? -
- - + + + - \ No newline at end of file + diff --git a/obs_ppt_server.py b/obs_ppt_server.py index 221179e..4172d94 100755 --- a/obs_ppt_server.py +++ b/obs_ppt_server.py @@ -1,3 +1,5 @@ +import sys +sys.coinit_flags= 0 import win32com.client import pywintypes import os @@ -9,26 +11,64 @@ import threading import asyncio import websockets import logging, json +import urllib +import posixpath +import time +import pythoncom logging.basicConfig() -powerpoint = None -cache = r'''C:\Windows\Temp''' +global current_slideshow +current_slideshow = None +CACHEDIR = r'''C:\Windows\Temp\ppt-cache''' +CACHE_TIMEOUT = 2*60*60 -class Handler(server.CGIHTTPRequestHandler): +class Handler(server.SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): super().__init__(*args, directory=os.path.dirname(os.path.realpath(__file__))) + + def translate_path(self, path): + """Translate a /-separated PATH to the local filename syntax. + + Components that mean special things to the local file system + (e.g. drive or directory names) are ignored. (XXX They should + probably be diagnosed.) + + """ + # abandon query parameters + path = path.split('?',1)[0] + path = path.split('#',1)[0] + # Don't forget explicit trailing slash when normalizing. Issue17324 + trailing_slash = path.rstrip().endswith('/') + try: + path = urllib.parse.unquote(path, errors='surrogatepass') + except UnicodeDecodeError: + path = urllib.parse.unquote(path) + path = posixpath.normpath(path) + words = path.split('/') + words = list(filter(None, words)) + if len(words) > 0 and words[0] == "cache": + if current_slideshow: + path = CACHEDIR + "\\" + current_slideshow.name() + words.pop(0) + else: + path = self.directory + for word in words: + if os.path.dirname(word) or word in (os.curdir, os.pardir): + # Ignore components that are not a simple file/directory name + continue + path = os.path.join(path, word) + if trailing_slash: + path += '/' + print(path) + return path + def run_http(): http_server = server.HTTPServer(("", 80), Handler) http_server.serve_forever() -async def first(websocket, path): - slideshow_view_first() - await websocket.send(True) - -STATE = {"value": "?", "visible": "1"} - +STATE = {"connected": 0, "current": 0, "total": 0, "visible": 0} USERS = set() @@ -41,7 +81,11 @@ def users_event(): async def notify_state(): - STATE["value"] = str(current_slide()) + "/" + str(total_slides()) + global current_slideshow + if current_slideshow: + STATE["current"] = current_slideshow.current_slide() + STATE["total"] = current_slideshow.total_slides() + STATE["visible"] = current_slideshow.visible() if USERS: # asyncio.wait doesn't accept an empty list message = state_event() await asyncio.wait([user.send(message) for user in USERS]) @@ -64,6 +108,7 @@ async def unregister(websocket): async def ws_handle(websocket, path): + global current_slideshow # register(websocket) sends user_event() to websocket await register(websocket) try: @@ -71,16 +116,43 @@ async def ws_handle(websocket, path): async for message in websocket: data = json.loads(message) if data["action"] == "prev": - slideshow_view_previous() + if current_slideshow: + current_slideshow.prev() await notify_state() elif data["action"] == "next": - slideshow_view_next() + if current_slideshow: + current_slideshow.next() await notify_state() elif data["action"] == "first": - slideshow_view_first() + if current_slideshow: + current_slideshow.first() await notify_state() elif data["action"] == "last": - slideshow_view_last() + if current_slideshow: + current_slideshow.last() + await notify_state() + elif data["action"] == "black": + if current_slideshow: + if current_slideshow.visible() == 3: + current_slideshow.normal() + else: + current_slideshow.black() + await notify_state() + elif data["action"] == "white": + if current_slideshow: + if current_slideshow.visible() == 4: + current_slideshow.normal() + else: + current_slideshow.white() + await notify_state() + elif data["action"] == "goto": + if current_slideshow: + current_slideshow.goto(int(data["value"])) + await notify_state() + elif data["action"] == "refresh": + if current_slideshow: + current_slideshow.export_current_next() + current_slideshow.refresh() await notify_state() else: logging.error("unsupported event: {}", data) @@ -88,20 +160,27 @@ async def ws_handle(websocket, path): await unregister(websocket) def run_ws(): + pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED) + asyncio.set_event_loop(asyncio.new_event_loop()) start_server = websockets.serve(ws_handle, "0.0.0.0", 5678) + print("Initialised websocket server") asyncio.get_event_loop().run_until_complete(start_server) + print("Running websocket server until complete") asyncio.get_event_loop().run_forever() def start_server(): - STATE["value"] = current_slide() + #STATE["current"] = current_slide() http_daemon = threading.Thread(name="http_daemon", target=run_http) http_daemon.setDaemon(True) http_daemon.start() + print("Started HTTP server") - run_ws() - #ws_daemon = threading.Thread(name="ws_daemon", target=run_ws) - #ws_daemon.setDaemon(True) - #ws_daemon.start() + #run_ws() + + ws_daemon = threading.Thread(name="ws_daemon", target=run_ws) + ws_daemon.setDaemon(True) + ws_daemon.start() + print("Started websocket server") #try: # ws_daemon.start() @@ -110,103 +189,138 @@ def start_server(): # cleanup_stop_thread() # sys.exit() - -def get_slideshow_view(): - global powerpoint - - if powerpoint is None: - powerpoint = win32com.client.Dispatch('Powerpoint.Application') - - if powerpoint is None: - return - - ssw = powerpoint.SlideShowWindows - if ssw.Count == 0: - return - - # https://docs.microsoft.com/en-us/office/vba/api/powerpoint.slideshowwindow.view - ssv = ssw[0].View - - return ssv - -def get_activepresentation(): - global powerpoint - - if powerpoint is None: - powerpoint = win32com.client.Dispatch('Powerpoint.Application') - - if powerpoint is None: - return - - activepres = powerpoint.ActivePresentation - return activepres - -def total_slides(): - ssp = get_activepresentation() - if ssp: - return len(ssp.Slides) - -def current_slide(): - ssv = get_slideshow_view() - if ssv: - return ssv.CurrentShowPosition - -def export(slide): - global cache - ssp = get_activepresentation() - if ssp: - for (slide, name) in [(slide, "current"), (slide+1, "next")]: - if slide < len(ssp.Slides): - ssp.Slides(slide).Export(os.path.dirname(os.path.realpath(__file__)) + r'''\\''' + name + r'''0.jpg''', "JPG") +class Slideshow: + def __init__(self, instance): + self.instance = instance + if self.instance is None: + raise ValueError("PPT instance cannot be None") + + if self.instance.SlideShowWindows.Count == 0: + raise ValueError("PPT instance has no slideshow windows") + self.view = self.instance.SlideShowWindows[0].View + + if self.instance.ActivePresentation is None: + raise ValueError("PPT instance has no active presentation") + self.presentation = self.instance.ActivePresentation + + self.export_all() + + def refresh(self): + if self.instance is None: + raise ValueError("PPT instance cannot be None") + + #if self.instance.SlideShowWindows.Count == 0: + # raise ValueError("PPT instance has no slideshow windows") + self.view = self.instance.SlideShowWindows[0].View + + if self.instance.ActivePresentation is None: + raise ValueError("PPT instance has no active presentation") + + def total_slides(self): + return len(self.presentation.Slides) + + def current_slide(self): + return self.view.CurrentShowPosition + + def visible(self): + return self.view.State + + def prev(self): + self.refresh() + self.view.Previous() + + def next(self): + self.refresh() + self.view.Next() + self.export_current_next() + + def first(self): + self.refresh() + self.view.First() + self.export_current_next() + + def last(self): + self.refresh() + self.view.Last() + self.export_current_next() + + def goto(self, slide): + self.refresh() + if slide <= self.total_slides(): + self.view.GotoSlide(slide) + else: + self.last() + self.next() + self.export_current_next() + + def black(self): + self.refresh() + self.view.State = 3 + + def white(self): + self.refresh() + self.view.State = 4 + + def normal(self): + self.refresh() + self.view.State = 1 + + def name(self): + return self.presentation.Name + + def export_current_next(self): + self.export(self.current_slide()) + self.export(self.current_slide() + 1) + + def export(self, slide): + destination = CACHEDIR + "\\" + self.name() + "\\" + str(slide) + ".jpg" + os.makedirs(os.path.dirname(destination), exist_ok=True) + if not os.path.exists(destination) or time.time() - os.path.getmtime(destination) > CACHE_TIMEOUT: + if slide <= self.total_slides(): attempts = 0 while attempts < 3: try: - os.replace(os.path.dirname(os.path.realpath(__file__)) + r'''\\''' + name + r'''0.jpg''', os.path.dirname(os.path.realpath(__file__)) + r'''\\''' + name + '''.jpg''') + self.presentation.Slides(slide).Export(destination, "JPG") + time.sleep(0.5) + break except: pass attempts += 1 + elif slide == self.total_slides() + 1: + shutil.copyfileobj(open(os.path.dirname(os.path.realpath(__file__)) + r'''\black.jpg''', 'rb'), open(destination, 'wb')) else: - 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')) - -def slideshow_view_first(): - ssv = get_slideshow_view() - if ssv: - ssv.First() - export(ssv.CurrentShowPosition) - -def slideshow_view_previous(): - ssv = get_slideshow_view() - if ssv: - ssv.Previous() - export(ssv.CurrentShowPosition) - -def slideshow_view_next(): - ssv = get_slideshow_view() - if ssv: - ssv.Next() - export(ssv.CurrentShowPosition) - - -def slideshow_view_last(): - ssv = get_slideshow_view() - if ssv: - ssv.Last() - export(ssv.CurrentShowPosition) - -def slideshow_view_black(): - ssv = get_slideshow_view() - if ssv: - ssv.State = 3 - -def slideshow_view_white(): - ssv = get_slideshow_view() - if ssv: - ssv.State = 4 - -def slideshow_view_normal(): - ssv = get_slideshow_view() - if ssv: - ssv.State = 1 + pass + + def export_all(self): + for i in range(1, self.total_slides()): + self.export(i) + +def get_ppt_instance(): + instance = win32com.client.Dispatch('Powerpoint.Application') + if instance is None or instance.SlideShowWindows.Count == 0: + return None + return instance + +def get_current_slideshow(): + print(str(current_slideshow)) + return current_slideshow + if __name__ == "__main__": + start_server() + + while True: + # Check if PowerPoint is running + instance = get_ppt_instance() + try: + current_slideshow = Slideshow(instance) + STATE["connected"] = 1 + STATE["current"] = current_slideshow.current_slide() + STATE["total"] = current_slideshow.total_slides() + print("Connected to PowerPoint instance " + str(get_current_slideshow())) + break + except ValueError as e: + current_slideshow = None + pass + time.sleep(1) diff --git a/ppt-control.js b/ppt-control.js new file mode 100644 index 0000000..7496f1b --- /dev/null +++ b/ppt-control.js @@ -0,0 +1,144 @@ +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'})); +} diff --git a/settings.js b/settings.js new file mode 100644 index 0000000..2807801 --- /dev/null +++ b/settings.js @@ -0,0 +1,48 @@ +const COOKIENAME = "settings"; +const COOKIEEXP = 365; + +function setCookie(cname, cvalue, exdays) { + var d = new Date(); + d.setTime(d.getTime() + (exdays*24*60*60*1000)); + var expires = "expires="+ d.toUTCString(); + document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; +} + +function getCookie(cname) { + var name = cname + "="; + var decodedCookie = decodeURIComponent(document.cookie); + var ca = decodedCookie.split(';'); + for(var i = 0; i