import os from osc import VRChatOSCReceiver, VRChatOSC, EyeId from config import EyeTrackConfig from camera_widget import CameraWidget from settings_widget import SettingsWidget import queue import threading import PySimpleGUI as sg import sys from urllib.request import urlopen from bs4 import BeautifulSoup import webbrowser if sys.platform.startswith("win"): from win10toast_click import ToastNotifier # Random environment variable to speed up webcam opening on the MSMF backend. # https://github.com/opencv/opencv/issues/17687 os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0" WINDOW_NAME = "EyeTrackApp" RIGHT_EYE_NAME = "-RIGHTEYEWIDGET-" LEFT_EYE_NAME = "-LEFTEYEWIDGET-" SETTINGS_NAME = "-SETTINGSWIDGET-" LEFT_EYE_RADIO_NAME = "-LEFTEYERADIO-" RIGHT_EYE_RADIO_NAME = "-RIGHTEYERADIO-" BOTH_EYE_RADIO_NAME = "-BOTHEYERADIO-" SETTINGS_RADIO_NAME = '-SETTINGSRADIO-' page_url = 'https://github.com/RedHawk989/EyeTrackVR/releases/latest' def open_url(): try: webbrowser.open_new(page_url) print('Opening URL...') except: print('Failed to open URL. Unsupported variable type.') def main(): # Get Configuration config: EyeTrackConfig = EyeTrackConfig.load() config.save() cancellation_event = threading.Event() # Check to see if we can connect to our video source first. If not, bring up camera finding # dialog. appversion = "0.1.7.2" url = "https://raw.githubusercontent.com/RedHawk989/EyeTrackVR-Installer/master/Version-Data/Version_Num.txt" html = urlopen(url).read() soup = BeautifulSoup(html, features="html.parser") for script in soup(["script", "style"]): script.extract() text = soup.get_text() # break into lines and remove leading and trailing space on each lines = (line.strip() for line in text.splitlines()) # break multi-headlines into a line each chunks = (phrase.strip() for line in lines for phrase in line.split(" ")) # drop blank lines latestversion = '\n'.join(chunk for chunk in chunks if chunk) if appversion == latestversion: # If what we scraped and hardcoded versions are same, assume we are up to date. print(f"[INFO] App is up to date! {latestversion}") else: print(f"[INFO] You have app version {appversion} installed. Please update to {latestversion} for the newest fixes.") if sys.platform.startswith("win"): toaster = ToastNotifier() toaster.show_toast( #show windows toast "EyeTrackVR has an update.", "Click to go to the latest version.", icon_path= "Images/logo.ico", duration=5, threaded=True, callback_on_click=open_url ) # Check to see if we have an ROI. If not, bring up ROI finder GUI. # Spawn worker threads osc_queue: queue.Queue[tuple[bool, int, int]] = queue.Queue() osc = VRChatOSC(cancellation_event, osc_queue, config) osc_thread = threading.Thread(target=osc.run) # start worker threads osc_thread.start() eyes = [ CameraWidget(EyeId.RIGHT, config, osc_queue), CameraWidget(EyeId.LEFT, config, osc_queue), ] settings = [ SettingsWidget(EyeId.SETTINGS, config, osc_queue), ] layout = [ [ sg.Radio( "Right Eye", "EYESELECTRADIO", background_color='#292929', default=(config.eye_display_id == EyeId.RIGHT), key=RIGHT_EYE_RADIO_NAME, ), sg.Radio( "Left Eye", "EYESELECTRADIO", background_color='#292929', default=(config.eye_display_id == EyeId.LEFT), key=LEFT_EYE_RADIO_NAME, ), sg.Radio( "Both Eyes", "EYESELECTRADIO", background_color='#292929', default=(config.eye_display_id == EyeId.BOTH), key=BOTH_EYE_RADIO_NAME, ), sg.Radio( "Settings", "EYESELECTRADIO", background_color='#292929', default=(config.eye_display_id == EyeId.SETTINGS), key=SETTINGS_RADIO_NAME, ), ], [ sg.Column( eyes[1].widget_layout, vertical_alignment="top", key=LEFT_EYE_NAME, visible=(config.eye_display_id in [EyeId.LEFT, EyeId.BOTH]), background_color='#424042', ), sg.Column( eyes[0].widget_layout, vertical_alignment="top", key=RIGHT_EYE_NAME, visible=(config.eye_display_id in [EyeId.RIGHT, EyeId.BOTH]), background_color='#424042', ), sg.Column( settings[0].widget_layout, vertical_alignment="top", key=SETTINGS_NAME, visible=(config.eye_display_id in [EyeId.SETTINGS]), background_color='#424042', ), ], ] if config.eye_display_id in [EyeId.LEFT, EyeId.BOTH]: eyes[1].start() if config.eye_display_id in [EyeId.RIGHT, EyeId.BOTH]: eyes[0].start() if config.eye_display_id in [EyeId.SETTINGS, EyeId.BOTH]: settings[0].start() # the eye's needs to be running before it is passed to the OSC osc_receiver = VRChatOSCReceiver(cancellation_event, config, eyes) osc_receiver_thread = threading.Thread(target=osc_receiver.run) osc_receiver_thread.start() # Create the window window = sg.Window(f"EyeTrackVR {appversion}" , layout, icon='Images/logo.ico', background_color='#292929') # GUI Render loop while True: # First off, check for any events from the GUI event, values = window.read(timeout=1) # If we're in either mode and someone hits q, quit immediately if event == "Exit" or event == sg.WIN_CLOSED: for eye in eyes: eye.stop() cancellation_event.set() # shut down worker threads osc_thread.join() # TODO: find a way to have this function run on join maybe?? # threading.Event() wont work because pythonosc spawns its own thread. # only way i can see to get around this is an ugly while loop that only checks if a threading event is triggered # and then call the pythonosc shutdown function osc_receiver.shutdown() osc_receiver_thread.join() print("Exiting EyeTrackApp") return if values[RIGHT_EYE_RADIO_NAME] and config.eye_display_id != EyeId.RIGHT: eyes[0].start() eyes[1].stop() settings[0].stop() window[RIGHT_EYE_NAME].update(visible=True) window[LEFT_EYE_NAME].update(visible=False) window[SETTINGS_NAME].update(visible=False) config.eye_display_id = EyeId.RIGHT config.settings.tracker_single_eye = 2 config.save() elif values[LEFT_EYE_RADIO_NAME] and config.eye_display_id != EyeId.LEFT: settings[0].stop() eyes[0].stop() eyes[1].start() window[RIGHT_EYE_NAME].update(visible=False) window[LEFT_EYE_NAME].update(visible=True) window[SETTINGS_NAME].update(visible=False) config.eye_display_id = EyeId.LEFT config.settings.tracker_single_eye = 1 config.save() elif values[BOTH_EYE_RADIO_NAME] and config.eye_display_id != EyeId.BOTH: settings[0].stop() eyes[0].stop() eyes[1].start() eyes[0].start() window[LEFT_EYE_NAME].update(visible=True) window[RIGHT_EYE_NAME].update(visible=True) window[SETTINGS_NAME].update(visible=False) config.eye_display_id = EyeId.BOTH config.settings.tracker_single_eye = 0 config.save() elif values[SETTINGS_RADIO_NAME] and config.eye_display_id != EyeId.SETTINGS: eyes[0].stop() eyes[1].stop() settings[0].start() window[RIGHT_EYE_NAME].update(visible=False) window[LEFT_EYE_NAME].update(visible=False) window[SETTINGS_NAME].update(visible=True) config.eye_display_id = EyeId.SETTINGS config.save() # Otherwise, render all of our cameras for eye in eyes: if eye.started(): eye.render(window, event, values) settings[0].render(window, event, values) if __name__ == "__main__": main()