ppt_control / ppt_control_obs.pyon commit replace client-triggered state refresh with server-triggered (d75ca91)
   1# -*- coding: utf-8 -*-
   2
   3import obspython as obs
   4import asyncio
   5import websockets
   6import threading
   7from time import sleep
   8
   9PORT_DEFAULT = 5678
  10HOSTNAME_DEFAULT = "localhost"
  11
  12hotkey_id_first = None
  13hotkey_id_prev = None
  14hotkey_id_next = None
  15hotkey_id_last = None
  16hotkey_id_black = None
  17hotkey_id_white = None
  18
  19HOTKEY_NAME_FIRST = 'powerpoint_slides.first'
  20HOTKEY_NAME_PREV = 'powerpoint_slides.previous'
  21HOTKEY_NAME_NEXT = 'powerpoint_slides.next'
  22HOTKEY_NAME_LAST = 'powerpoint_slides.last'
  23HOTKEY_NAME_BLACK = 'powerpoint_slides.black'
  24HOTKEY_NAME_WHITE = 'powerpoint_slides.white'
  25
  26HOTKEY_DESC_FIRST = 'First PowerPoint slide'
  27HOTKEY_DESC_PREV = 'Previous PowerPoint slide'
  28HOTKEY_DESC_NEXT = 'Next PowerPoint slide'
  29HOTKEY_DESC_LAST = 'Last PowerPoint slide'
  30HOTKEY_DESC_BLACK = 'Black PowerPoint slide'
  31HOTKEY_DESC_WHITE = 'White PowerPoint slide'
  32
  33global cmd
  34global attempts
  35cmd = ""
  36hostname = HOSTNAME_DEFAULT
  37port = PORT_DEFAULT
  38attempts = 0
  39
  40async def communicate():
  41    async with websockets.connect("ws://%s:%s" % (hostname, port), ping_interval=None) as websocket:
  42        global cmd
  43        global attempts 
  44        while True:
  45            if cmd:
  46                try:
  47                    cmd_temp = cmd
  48                    cmd = ""
  49                    await websocket.send('{"action": "%s"}' % cmd_temp)
  50                except websockets.ConnectionClosed as exc:
  51                    attempts += 1
  52                    if attempts == 4:
  53                        print("Failed to send command after {} attempts - aborting connection".format(attempts))
  54                        attempts = 0
  55                        cmd = ""
  56                        raise websockets.exceptions.ConnectionClosedError(1006, "Sending command failed after {} attempts".format(attempts))
  57            await asyncio.sleep(0.05 + 0.5*attempts**2)
  58
  59def run_ws():
  60    while True:
  61        try:
  62            asyncio.set_event_loop(asyncio.new_event_loop())
  63            asyncio.get_event_loop().run_until_complete(communicate())
  64        except (OSError, websockets.exceptions.ConnectionClosedError):
  65            # No server available - just keep trying
  66            pass
  67        except Exception as e:
  68            print("Failed to connect to websocket: {} - {}".format(type(e), e))
  69        finally:
  70            sleep(1)
  71
  72#------------------------------------------------------------
  73# global functions for script plugins
  74
  75def script_load(settings):
  76    global hotkey_id_first
  77    global hotkey_id_prev
  78    global hotkey_id_next
  79    global hotkey_id_last
  80    global hotkey_id_black
  81    global hotkey_id_white
  82
  83    hotkey_id_first = register_and_load_hotkey(settings, HOTKEY_NAME_FIRST, HOTKEY_DESC_FIRST, first_slide)
  84    hotkey_id_prev = register_and_load_hotkey(settings, HOTKEY_NAME_PREV, HOTKEY_DESC_PREV, prev_slide)
  85    hotkey_id_next = register_and_load_hotkey(settings, HOTKEY_NAME_NEXT, HOTKEY_DESC_NEXT, next_slide)
  86    hotkey_id_last = register_and_load_hotkey(settings, HOTKEY_NAME_LAST, HOTKEY_DESC_LAST, last_slide)
  87    hotkey_id_black = register_and_load_hotkey(settings, HOTKEY_NAME_BLACK, HOTKEY_DESC_BLACK, black)
  88    hotkey_id_white = register_and_load_hotkey(settings, HOTKEY_NAME_WHITE, HOTKEY_DESC_WHITE, white)
  89
  90    ws_daemon = threading.Thread(name="ws_daemon", target=run_ws)
  91    ws_daemon.setDaemon(True)
  92    ws_daemon.start()
  93    print("Started websocket client")
  94
  95def script_unload():
  96    obs.obs_hotkey_unregister(first_slide)
  97    obs.obs_hotkey_unregister(prev_slide)
  98    obs.obs_hotkey_unregister(next_slide)
  99    obs.obs_hotkey_unregister(last_slide)
 100    obs.obs_hotkey_unregister(black)
 101    obs.obs_hotkey_unregister(white)
 102
 103def script_save(settings):
 104    save_hotkey(settings, HOTKEY_NAME_FIRST, hotkey_id_first)
 105    save_hotkey(settings, HOTKEY_NAME_PREV, hotkey_id_prev)
 106    save_hotkey(settings, HOTKEY_NAME_NEXT, hotkey_id_next)
 107    save_hotkey(settings, HOTKEY_NAME_LAST, hotkey_id_last)
 108    save_hotkey(settings, HOTKEY_NAME_BLACK, hotkey_id_black)
 109    save_hotkey(settings, HOTKEY_NAME_WHITE, hotkey_id_white)
 110
 111def script_description():
 112    return """ppt-control client
 113
 114    Provides hotkeys for controlling PowerPoint slides using websockets.
 115    Go to OBS settings -> Hotkeys to change hotkeys (none set by default)."""
 116
 117def script_defaults(settings):
 118    obs.obs_data_set_default_string(settings, 'hostname', HOSTNAME_DEFAULT)
 119    obs.obs_data_set_default_int(settings, 'port', PORT_DEFAULT)
 120
 121def script_properties():
 122    props = obs.obs_properties_create()
 123
 124    obs.obs_properties_add_text(props, "hostname", "Hostname: ", obs.OBS_TEXT_DEFAULT)
 125    obs.obs_properties_add_int(props, "port", "Port: ", 0, 9999, 1)
 126    return props
 127
 128def script_update(settings):
 129    global port
 130    port = obs.obs_data_get_int(settings, "port")
 131    hostname = obs.obs_data_get_string(settings, "hostname")
 132
 133def register_and_load_hotkey(settings, name, description, callback):
 134    hotkey_id = obs.obs_hotkey_register_frontend(name, description, callback)
 135    hotkey_save_array = obs.obs_data_get_array(settings, name)
 136    obs.obs_hotkey_load(hotkey_id, hotkey_save_array)
 137    obs.obs_data_array_release(hotkey_save_array)
 138
 139    return hotkey_id
 140
 141def save_hotkey(settings, name, hotkey_id):
 142    hotkey_save_array = obs.obs_hotkey_save(hotkey_id)
 143    obs.obs_data_set_array(settings, name, hotkey_save_array)
 144    obs.obs_data_array_release(hotkey_save_array)
 145
 146#-------------------------------------
 147
 148def first_slide(pressed):
 149    if pressed:
 150        global cmd
 151        cmd = "first"
 152
 153def prev_slide(pressed):
 154    if pressed:
 155        global cmd
 156        cmd = "prev"
 157
 158def next_slide(pressed):
 159    if pressed:
 160        global cmd
 161        cmd = "next"
 162
 163def last_slide(pressed):
 164    if pressed:
 165        global cmd
 166        cmd = "last"
 167
 168def black(pressed):
 169    if pressed:
 170        global cmd
 171        cmd = "black"
 172
 173def white(pressed):
 174    if pressed:
 175        global cmd
 176        cmd = "white"