This commit is contained in:
2026-06-04 04:18:07 +01:00
parent 33fee44e63
commit 63024408e8

View File

@ -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
try:
for _ in range(self.additionalPictures):
time.sleep(1.1)
# 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