From 63024408e8ebd8cb96cc101f97f18eee637318b3 Mon Sep 17 00:00:00 2001 From: Brosef Date: Thu, 4 Jun 2026 04:18:07 +0100 Subject: [PATCH] Fixed #2 --- camera.py | 88 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 13 deletions(-) diff --git a/camera.py b/camera.py index e517af5..677a707 100644 --- a/camera.py +++ b/camera.py @@ -32,6 +32,8 @@ class NewPictureHandler(watchdog.events.FileSystemEventHandler): def __init__(self, callback: callable): self._lastPicture = 0 self._callback = callback + self.lastClosedType = None + self.lastClosedTime = 0 super().__init__() def on_created(self, event: watchdog.events.FileSystemEvent) -> None: @@ -48,6 +50,20 @@ class NewPictureHandler(watchdog.events.FileSystemEventHandler): Thread(target=self._callback, args=(event.src_path, timeGap)).start() self._lastPicture = time.perf_counter() + def on_closed(self, event: watchdog.events.FileSystemEvent) -> None: + exExt = '.'.join(event.src_path.split('.')[:-1]) + + if exExt.endswith('_Environment'): + self.lastClosedType = 'ENVIRONMENT' + elif exExt.endswith('_Player'): + self.lastClosedType = 'PLAYER' + elif exExt.endswith('_UI'): + self.lastClosedType = 'UI' + else: + self.lastClosedType = 'COMBINED' + + self.lastClosedTime = time.perf_counter() + def createNewPictureObserver(callback: callable) -> Observer: """ Creates an Observer that watches the VRChat pictures folder and calls callback @@ -77,9 +93,10 @@ def createNewPictureObserver(callback: callable) -> Observer: return None observer = Observer() - observer.schedule(NewPictureHandler(callback), path, recursive=True) + handler = NewPictureHandler(callback) + observer.schedule(handler, path, recursive=True) observer.start() - return observer + return handler, observer @@ -92,9 +109,9 @@ class Camera: onCameraEnabled: callable=None): self.oscClient = oscClient self.additionalPictures = 0 # How many additional pictures to take - self.addPicsMinGap = 0 # How long to wait between multi-shot batches + self.lastPicture = 0 self._lastMode = 0 - self._newPictureObserver = createNewPictureObserver(self._newPicture) + self._newPicHandler, self._newPicObserver = createNewPictureObserver(self._newPicture) self._multiPictureOngoing = False self._onEnabledCallback = onCameraEnabled self._masks = {'UI': False} @@ -112,11 +129,48 @@ class Camera: Safely closes the picture watcher, and the connection to VRChat. """ - if self._newPictureObserver is not None: - self._newPictureObserver.stop() - self._newPictureObserver.join() + if self._newPicObserver is not None: + self._newPicObserver.stop() + self._newPicObserver.join() + + def _waitForCameraReady(self, timeout: float=7.5): + """ + Waits until the camera is considered to be "ready" again. + This is done by checking self._newPicHandler's last closed flags. + This function will also raise a TimeoutError if it has taken more than + `timeout` seconds to get the expected close event. + """ + + # If the camera is not in multilayer mode, use COMBINED as the last type. + if self._lastMode != 4: + lastType = 'COMBINED' + # Otherwise, check for the UI flag. + elif self._masks['UI']: + lastType = 'UI' + # If none of the above are true, the only other outcome is PLAYER. See #2 for more details. + else: + lastType = 'PLAYER' + + # Start a timer for the timeout. + st = time.perf_counter() + + # Spin until we're looking at relevant close events with a 0.05s margin for error. + while self._newPicHandler.lastClosedTime < self.lastPicture - 0.05 and\ + time.perf_counter() - st < timeout: + time.sleep(0.01) + + # Spin until the desired picture has been reached. + while self._newPicHandler.lastClosedType != lastType and\ + time.perf_counter() - st < timeout: + time.sleep(0.01) + + # If the timeout was exceeded, raise TimeoutError. + if time.perf_counter() - st > timeout: + raise TimeoutError() def _newPicture(self, _, timeGap: float): + self.lastPicture = time.perf_counter() + if self._multiPictureOngoing: return @@ -124,17 +178,25 @@ class Camera: if self._lastMode == 5: return - if timeGap < self.addPicsMinGap: - return - if self.additionalPictures == 0: return self._multiPictureOngoing = True - for _ in range(self.additionalPictures): - time.sleep(1.1) - self.oscClient.send_message('/usercamera/Capture', True) + try: + for _ in range(self.additionalPictures): + # Wait until the camera is ready + self._waitForCameraReady() + time.sleep(0.75) + # Take a new picture + self.oscClient.send_message('/usercamera/Capture', True) + # Reset the lastPicture time, the set that happens at the start of this + # function is not fast enough and causes race conditions. + self.lastPicture = time.perf_counter() + # Wait for the camera to be ready one last time to prevent a cycle + self._waitForCameraReady() + except TimeoutError: + print('WARNING: Timeout occured during multishot. Bailing out.') time.sleep(0.5) self._multiPictureOngoing = False