obs_ppt_server.pyon commit fix threading bugs, add further HTML controls (384420f)
   1import sys
   2sys.coinit_flags= 0
   3import win32com.client
   4import pywintypes
   5import os
   6import shutil
   7import http_server_39 as server
   8#import http.server as server
   9import socketserver
  10import threading
  11import asyncio
  12import websockets
  13import logging, json
  14import urllib
  15import posixpath
  16import time
  17import pythoncom
  18
  19logging.basicConfig()
  20
  21global current_slideshow
  22current_slideshow = None
  23CACHEDIR = r'''C:\Windows\Temp\ppt-cache'''
  24CACHE_TIMEOUT = 2*60*60
  25
  26class Handler(server.SimpleHTTPRequestHandler):
  27    def __init__(self, *args, **kwargs):
  28        super().__init__(*args, directory=os.path.dirname(os.path.realpath(__file__)))
  29        
  30    def translate_path(self, path):
  31        """Translate a /-separated PATH to the local filename syntax.
  32
  33        Components that mean special things to the local file system
  34        (e.g. drive or directory names) are ignored.  (XXX They should
  35        probably be diagnosed.)
  36
  37        """
  38        # abandon query parameters
  39        path = path.split('?',1)[0]
  40        path = path.split('#',1)[0]
  41        # Don't forget explicit trailing slash when normalizing. Issue17324
  42        trailing_slash = path.rstrip().endswith('/')
  43        try:
  44            path = urllib.parse.unquote(path, errors='surrogatepass')
  45        except UnicodeDecodeError:
  46            path = urllib.parse.unquote(path)
  47        path = posixpath.normpath(path)
  48        words = path.split('/')
  49        words = list(filter(None, words))
  50        if len(words) > 0 and words[0] == "cache":
  51            if current_slideshow:
  52                path = CACHEDIR + "\\" + current_slideshow.name()
  53            words.pop(0)
  54        else:
  55            path = self.directory
  56        for word in words:
  57            if os.path.dirname(word) or word in (os.curdir, os.pardir):
  58                # Ignore components that are not a simple file/directory name
  59                continue
  60            path = os.path.join(path, word)
  61        if trailing_slash:
  62            path += '/'
  63        print(path)
  64        return path
  65
  66
  67def run_http():
  68    http_server = server.HTTPServer(("", 80), Handler)
  69    http_server.serve_forever()
  70
  71STATE = {"connected": 0, "current": 0, "total": 0, "visible": 0}
  72USERS = set()
  73
  74
  75def state_event():
  76    return json.dumps({"type": "state", **STATE})
  77
  78
  79def users_event():
  80    return json.dumps({"type": "users", "count": len(USERS)})
  81
  82
  83async def notify_state():
  84    global current_slideshow
  85    if current_slideshow:
  86        STATE["current"] = current_slideshow.current_slide()
  87        STATE["total"] = current_slideshow.total_slides()
  88        STATE["visible"] = current_slideshow.visible()
  89    if USERS:  # asyncio.wait doesn't accept an empty list
  90        message = state_event()
  91        await asyncio.wait([user.send(message) for user in USERS])
  92
  93
  94async def notify_users():
  95    if USERS:  # asyncio.wait doesn't accept an empty list
  96        message = users_event()
  97        await asyncio.wait([user.send(message) for user in USERS])
  98
  99
 100async def register(websocket):
 101    USERS.add(websocket)
 102    await notify_users()
 103
 104
 105async def unregister(websocket):
 106    USERS.remove(websocket)
 107    await notify_users()
 108
 109
 110async def ws_handle(websocket, path):
 111    global current_slideshow
 112    # register(websocket) sends user_event() to websocket
 113    await register(websocket)
 114    try:
 115        await websocket.send(state_event())
 116        async for message in websocket:
 117            data = json.loads(message)
 118            if data["action"] == "prev":
 119                if current_slideshow:
 120                    current_slideshow.prev()
 121                await notify_state()
 122            elif data["action"] == "next":
 123                if current_slideshow:
 124                    current_slideshow.next()
 125                await notify_state()
 126            elif data["action"] == "first":
 127                if current_slideshow:
 128                    current_slideshow.first()
 129                await notify_state()
 130            elif data["action"] == "last":
 131                if current_slideshow:
 132                    current_slideshow.last()
 133                await notify_state()
 134            elif data["action"] == "black":
 135                if current_slideshow:
 136                    if current_slideshow.visible() == 3:
 137                        current_slideshow.normal()
 138                    else:
 139                        current_slideshow.black()
 140                await notify_state()
 141            elif data["action"] == "white":
 142                if current_slideshow:
 143                    if current_slideshow.visible() == 4:
 144                        current_slideshow.normal()
 145                    else:
 146                        current_slideshow.white()
 147                await notify_state()
 148            elif data["action"] == "goto":
 149                if current_slideshow:
 150                    current_slideshow.goto(int(data["value"]))
 151                await notify_state()
 152            elif data["action"] == "refresh":
 153                if current_slideshow:
 154                    current_slideshow.export_current_next()
 155                    current_slideshow.refresh()
 156                await notify_state()
 157            else:
 158                logging.error("unsupported event: {}", data)
 159    finally:
 160        await unregister(websocket)
 161
 162def run_ws():
 163    pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED)
 164    asyncio.set_event_loop(asyncio.new_event_loop())
 165    start_server = websockets.serve(ws_handle, "0.0.0.0", 5678)
 166    print("Initialised websocket server")
 167    asyncio.get_event_loop().run_until_complete(start_server)
 168    print("Running websocket server until complete") 
 169    asyncio.get_event_loop().run_forever()
 170
 171def start_server():
 172    #STATE["current"] = current_slide()
 173    http_daemon = threading.Thread(name="http_daemon", target=run_http)
 174    http_daemon.setDaemon(True)
 175    http_daemon.start()
 176    print("Started HTTP server")
 177
 178    #run_ws()
 179    
 180    ws_daemon = threading.Thread(name="ws_daemon", target=run_ws)
 181    ws_daemon.setDaemon(True)
 182    ws_daemon.start()
 183    print("Started websocket server")
 184
 185    #try:
 186    #    ws_daemon.start()
 187    #    http_daemon.start()
 188    #except (KeyboardInterrupt, SystemExit):
 189    #    cleanup_stop_thread()
 190    #    sys.exit()
 191
 192class Slideshow:
 193    def __init__(self, instance):
 194        self.instance = instance
 195        if self.instance is None:
 196            raise ValueError("PPT instance cannot be None")
 197
 198        if self.instance.SlideShowWindows.Count == 0:
 199            raise ValueError("PPT instance has no slideshow windows")
 200        self.view = self.instance.SlideShowWindows[0].View
 201
 202        if self.instance.ActivePresentation is None:
 203            raise ValueError("PPT instance has no  active presentation")
 204        self.presentation = self.instance.ActivePresentation
 205        
 206        self.export_all()
 207
 208    def refresh(self):
 209        if self.instance is None:
 210            raise ValueError("PPT instance cannot be None")
 211
 212        #if self.instance.SlideShowWindows.Count == 0:
 213        #    raise ValueError("PPT instance has no slideshow windows")
 214        self.view = self.instance.SlideShowWindows[0].View
 215
 216        if self.instance.ActivePresentation is None:
 217            raise ValueError("PPT instance has no  active presentation")
 218
 219    def total_slides(self):
 220        return len(self.presentation.Slides)
 221
 222    def current_slide(self):
 223        return self.view.CurrentShowPosition
 224
 225    def visible(self):
 226        return self.view.State
 227
 228    def prev(self):
 229        self.refresh()
 230        self.view.Previous()
 231
 232    def next(self):
 233        self.refresh()
 234        self.view.Next()
 235        self.export_current_next()
 236
 237    def first(self):
 238        self.refresh()
 239        self.view.First()
 240        self.export_current_next()
 241                
 242    def last(self):
 243        self.refresh()
 244        self.view.Last()
 245        self.export_current_next()
 246
 247    def goto(self, slide):
 248        self.refresh()
 249        if slide <= self.total_slides():
 250            self.view.GotoSlide(slide)
 251        else:
 252            self.last()
 253            self.next()
 254        self.export_current_next()
 255
 256    def black(self):
 257        self.refresh()
 258        self.view.State = 3
 259
 260    def white(self):
 261        self.refresh()
 262        self.view.State = 4
 263
 264    def normal(self):
 265        self.refresh()
 266        self.view.State = 1
 267
 268    def name(self):
 269        return self.presentation.Name
 270
 271    def export_current_next(self):
 272        self.export(self.current_slide())
 273        self.export(self.current_slide() + 1)
 274
 275    def export(self, slide):
 276        destination = CACHEDIR + "\\" + self.name() + "\\" + str(slide) + ".jpg"
 277        os.makedirs(os.path.dirname(destination), exist_ok=True)
 278        if not os.path.exists(destination) or time.time() - os.path.getmtime(destination) > CACHE_TIMEOUT:
 279            if slide <= self.total_slides():
 280                attempts = 0
 281                while attempts < 3:
 282                    try:
 283                        self.presentation.Slides(slide).Export(destination, "JPG")
 284                        time.sleep(0.5)
 285                        break
 286                    except:
 287                        pass
 288                    attempts += 1
 289            elif slide == self.total_slides() + 1:
 290                shutil.copyfileobj(open(os.path.dirname(os.path.realpath(__file__)) + r'''\black.jpg''', 'rb'), open(destination, 'wb'))
 291            else:
 292                pass
 293
 294    def export_all(self):
 295        for i in range(1, self.total_slides()):
 296            self.export(i)
 297
 298def get_ppt_instance():
 299    instance = win32com.client.Dispatch('Powerpoint.Application')
 300    if instance is None or instance.SlideShowWindows.Count == 0:
 301        return None
 302    return instance
 303
 304def get_current_slideshow():
 305    print(str(current_slideshow))
 306    return current_slideshow
 307
 308
 309if __name__ == "__main__":
 310
 311    start_server()
 312    
 313    while True:
 314        # Check if PowerPoint is running
 315        instance = get_ppt_instance()
 316        try:
 317            current_slideshow = Slideshow(instance)
 318            STATE["connected"] = 1
 319            STATE["current"] = current_slideshow.current_slide()
 320            STATE["total"] = current_slideshow.total_slides()
 321            print("Connected to PowerPoint instance " + str(get_current_slideshow()))
 322            break
 323        except ValueError as e:
 324            current_slideshow = None
 325            pass
 326        time.sleep(1)