GameSentenceMiner 2.15.3__py3-none-any.whl → 2.15.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
GameSentenceMiner/obs.py CHANGED
@@ -5,6 +5,7 @@ import subprocess
5
5
  import threading
6
6
  import time
7
7
  import logging
8
+ import contextlib
8
9
 
9
10
  import psutil
10
11
 
@@ -16,7 +17,7 @@ from GameSentenceMiner.util.gsm_utils import sanitize_filename, make_unique_file
16
17
  import tkinter as tk
17
18
  from tkinter import messagebox
18
19
 
19
- client: obs.ReqClient = None
20
+ connection_pool: 'OBSConnectionPool' = None
20
21
  event_client: obs.EventClient = None
21
22
  obs_process_pid = None
22
23
  OBS_PID_FILE = os.path.join(configuration.get_app_directory(), 'obs-studio', 'obs_pid.txt')
@@ -24,8 +25,83 @@ obs_connection_manager = None
24
25
  logging.getLogger("obsws_python").setLevel(logging.CRITICAL)
25
26
  connecting = False
26
27
 
28
+ class OBSConnectionPool:
29
+ """Manages a pool of thread-safe connections to the OBS WebSocket."""
30
+ def __init__(self, size=3, **kwargs):
31
+ self.size = size
32
+ self.connection_kwargs = kwargs
33
+ self._clients = [None] * self.size
34
+ self._locks = [threading.Lock() for _ in range(self.size)]
35
+ self._next_idx = 0
36
+ self._idx_lock = threading.Lock()
37
+ logger.info(f"Initialized OBSConnectionPool with size {self.size}")
38
+
39
+ def connect_all(self):
40
+ """Initializes all client objects in the pool."""
41
+ for i in range(self.size):
42
+ try:
43
+ self._clients[i] = obs.ReqClient(**self.connection_kwargs)
44
+ except Exception as e:
45
+ logger.error(f"Failed to create client {i} in pool: {e}")
46
+ return True
47
+
48
+ def disconnect_all(self):
49
+ """Disconnects all clients in the pool."""
50
+ for client in self._clients:
51
+ if client:
52
+ try:
53
+ client.disconnect()
54
+ except Exception:
55
+ pass
56
+ self._clients = [None] * self.size
57
+ logger.info("Disconnected all clients in OBSConnectionPool.")
58
+
59
+ def _check_and_reconnect(self, index):
60
+ """Checks a specific client and reconnects if necessary."""
61
+ client = self._clients[index]
62
+ if not client:
63
+ self._clients[index] = obs.ReqClient(**self.connection_kwargs)
64
+ logger.info(f"Re-initialized client {index} in pool.")
65
+ return
66
+ try:
67
+ client.get_version()
68
+ except Exception:
69
+ logger.info(f"Reconnecting client {index} in pool.")
70
+ try:
71
+ client.disconnect()
72
+ except Exception:
73
+ pass
74
+ self._clients[index] = obs.ReqClient(**self.connection_kwargs)
75
+
76
+ @contextlib.contextmanager
77
+ def get_client(self):
78
+ """A context manager to safely get a client from the pool."""
79
+ with self._idx_lock:
80
+ idx = self._next_idx
81
+ self._next_idx = (self._next_idx + 1) % self.size
82
+
83
+ lock = self._locks[idx]
84
+ lock.acquire()
85
+ try:
86
+ self._check_and_reconnect(idx)
87
+ yield self._clients[idx]
88
+ finally:
89
+ lock.release()
90
+
91
+ def get_healthcheck_client(self):
92
+ """Returns a dedicated client for health checks, separate from the main pool."""
93
+ if not hasattr(self, '_healthcheck_client') or self._healthcheck_client is None:
94
+ try:
95
+ self._healthcheck_client = obs.ReqClient(**self.connection_kwargs)
96
+ logger.info("Initialized dedicated healthcheck client.")
97
+ except Exception as e:
98
+ logger.error(f"Failed to create healthcheck client: {e}")
99
+ self._healthcheck_client = None
100
+ return self._healthcheck_client
101
+
102
+
27
103
  class OBSConnectionManager(threading.Thread):
28
- def __init__(self, check_output=True):
104
+ def __init__(self, check_output=False):
29
105
  super().__init__()
30
106
  self.daemon = True
31
107
  self.running = True
@@ -38,8 +114,11 @@ class OBSConnectionManager(threading.Thread):
38
114
  while self.running:
39
115
  time.sleep(self.check_connection_interval)
40
116
  try:
41
- if not connecting:
117
+ client = connection_pool.get_healthcheck_client() if connection_pool else None
118
+ if client and not connecting:
42
119
  client.get_version()
120
+ else:
121
+ raise ConnectionError("Healthcheck client not healthy or not initialized")
43
122
  except Exception as e:
44
123
  logger.info(f"OBS WebSocket not connected. Attempting to reconnect... {e}")
45
124
  gsm_status.obs_connected = False
@@ -128,12 +207,12 @@ def start_obs():
128
207
  return None
129
208
 
130
209
  async def wait_for_obs_connected():
131
- global client
132
- if not client:
210
+ if not connection_pool:
133
211
  return False
134
212
  for _ in range(10):
135
213
  try:
136
- response = client.get_version()
214
+ with connection_pool.get_client() as client:
215
+ response = client.get_version()
137
216
  if response:
138
217
  return True
139
218
  except Exception as e:
@@ -182,20 +261,26 @@ def get_obs_websocket_config_values():
182
261
  full_config.save()
183
262
  reload_config()
184
263
 
185
- async def connect_to_obs(retry=5, check_output=True):
186
- global client, obs_connection_manager, event_client, connecting
264
+ async def connect_to_obs(retry=5, connections=2, check_output=False):
265
+ global connection_pool, obs_connection_manager, event_client, connecting
187
266
  if is_windows():
188
267
  get_obs_websocket_config_values()
189
268
 
190
269
  while True:
191
270
  connecting = True
192
271
  try:
193
- client = obs.ReqClient(
194
- host=get_config().obs.host,
195
- port=get_config().obs.port,
196
- password=get_config().obs.password,
197
- timeout=1,
198
- )
272
+ pool_kwargs = {
273
+ 'host': get_config().obs.host,
274
+ 'port': get_config().obs.port,
275
+ 'password': get_config().obs.password,
276
+ 'timeout': 3,
277
+ }
278
+ connection_pool = OBSConnectionPool(size=connections, **pool_kwargs)
279
+ connection_pool.connect_all()
280
+
281
+ with connection_pool.get_client() as client:
282
+ client.get_version() # Test one connection to confirm it works
283
+
199
284
  event_client = obs.EventClient(
200
285
  host=get_config().obs.host,
201
286
  port=get_config().obs.port,
@@ -213,7 +298,7 @@ async def connect_to_obs(retry=5, check_output=True):
213
298
  if retry <= 0:
214
299
  gsm_status.obs_connected = False
215
300
  logger.error(f"Failed to connect to OBS WebSocket: {e}")
216
- client = None
301
+ connection_pool = None
217
302
  event_client = None
218
303
  connecting = False
219
304
  break
@@ -221,70 +306,49 @@ async def connect_to_obs(retry=5, check_output=True):
221
306
  retry -= 1
222
307
  connecting = False
223
308
 
224
- def connect_to_obs_sync(retry=2, check_output=True):
225
- global client, obs_connection_manager, event_client
226
- if is_windows():
227
- get_obs_websocket_config_values()
228
-
229
- while True:
230
- try:
231
- client = obs.ReqClient(
232
- host=get_config().obs.host,
233
- port=get_config().obs.port,
234
- password=get_config().obs.password,
235
- timeout=1,
236
- )
237
- event_client = obs.EventClient(
238
- host=get_config().obs.host,
239
- port=get_config().obs.port,
240
- password=get_config().obs.password,
241
- timeout=1,
242
- )
243
- if not obs_connection_manager:
244
- obs_connection_manager = OBSConnectionManager(check_output=check_output)
245
- obs_connection_manager.start()
246
- update_current_game()
247
- logger.info("Connected to OBS WebSocket.")
248
- break # Exit the loop once connected
249
- except Exception as e:
250
- if retry <= 0:
251
- gsm_status.obs_connected = False
252
- logger.error(f"Failed to connect to OBS WebSocket: {e}")
253
- client = None
254
- event_client = None
255
- break
256
- time.sleep(1)
257
- retry -= 1
309
+ def connect_to_obs_sync(retry=2, connections=2, check_output=False):
310
+ asyncio.run(connect_to_obs(retry=retry, connections=connections, check_output=check_output))
258
311
 
259
312
 
260
313
  def disconnect_from_obs():
261
- global client
262
- if client:
263
- client.disconnect()
264
- client = None
314
+ global connection_pool
315
+ if connection_pool:
316
+ connection_pool.disconnect_all()
317
+ connection_pool = None
265
318
  logger.info("Disconnected from OBS WebSocket.")
266
319
 
267
- def do_obs_call(request, *args, from_dict=None, retry=3):
268
- connect_to_obs()
269
- if not client:
320
+ def do_obs_call(method_name: str, from_dict=None, retry=3, **kwargs):
321
+ if not connection_pool:
322
+ connect_to_obs_sync(retry=1)
323
+ if not connection_pool:
270
324
  return None
325
+
326
+ last_exception = None
271
327
  for _ in range(retry + 1):
272
328
  try:
273
- response = request(*args)
274
- if response and response.ok:
275
- return from_dict(response.datain) if from_dict else response.datain
329
+ with connection_pool.get_client() as client:
330
+ method_to_call = getattr(client, method_name)
331
+ response = method_to_call(**kwargs)
332
+ if response and response.ok:
333
+ return from_dict(response.datain) if from_dict else response.datain
276
334
  time.sleep(0.3)
335
+ except AttributeError:
336
+ logger.error(f"OBS client has no method '{method_name}'")
337
+ return None
277
338
  except Exception as e:
278
- logger.error(f"Error calling OBS: {e}")
339
+ last_exception = e
340
+ logger.error(f"Error calling OBS ('{method_name}'): {e}")
279
341
  if "socket is already closed" in str(e) or "object has no attribute" in str(e):
280
342
  time.sleep(0.3)
281
343
  else:
282
344
  return None
345
+ logger.error(f"OBS call '{method_name}' failed after retries. Last error: {last_exception}")
283
346
  return None
284
347
 
285
348
  def toggle_replay_buffer():
286
349
  try:
287
- response = client.toggle_replay_buffer()
350
+ with connection_pool.get_client() as client:
351
+ response = client.toggle_replay_buffer()
288
352
  if response:
289
353
  logger.info("Replay buffer Toggled.")
290
354
  except Exception as e:
@@ -292,7 +356,8 @@ def toggle_replay_buffer():
292
356
 
293
357
  def start_replay_buffer():
294
358
  try:
295
- response = client.start_replay_buffer()
359
+ with connection_pool.get_client() as client:
360
+ response = client.start_replay_buffer()
296
361
  if response and response.ok:
297
362
  logger.info("Replay buffer started.")
298
363
  except Exception as e:
@@ -300,31 +365,34 @@ def start_replay_buffer():
300
365
 
301
366
  def get_replay_buffer_status():
302
367
  try:
303
- return client.get_replay_buffer_status().output_active
368
+ with connection_pool.get_client() as client:
369
+ return client.get_replay_buffer_status().output_active
304
370
  except Exception as e:
305
371
  logger.debug(f"Error getting replay buffer status: {e}")
306
372
  return None
307
373
 
308
374
  def stop_replay_buffer():
309
375
  try:
310
- response = client.stop_replay_buffer()
376
+ with connection_pool.get_client() as client:
377
+ response = client.stop_replay_buffer()
311
378
  if response and response.ok:
312
379
  logger.info("Replay buffer stopped.")
313
380
  except Exception as e:
314
381
  logger.warning(f"Error stopping replay buffer: {e}")
315
382
 
316
383
  def save_replay_buffer():
317
- status = get_replay_buffer_status()
318
- if status:
319
- response = client.save_replay_buffer()
384
+ try:
385
+ with connection_pool.get_client() as client:
386
+ response = client.save_replay_buffer()
320
387
  if response and response.ok:
321
388
  logger.info("Replay buffer saved. If your log stops here, make sure your obs output path matches \"Path To Watch\" in GSM settings.")
322
- else:
323
- raise Exception("Replay Buffer is not active, could not save Replay Buffer!")
389
+ except Exception as e:
390
+ raise Exception(f"Error saving replay buffer: {e}")
324
391
 
325
392
  def get_current_scene():
326
393
  try:
327
- response = client.get_current_program_scene()
394
+ with connection_pool.get_client() as client:
395
+ response = client.get_current_program_scene()
328
396
  return response.scene_name if response else ''
329
397
  except Exception as e:
330
398
  logger.debug(f"Couldn't get scene: {e}")
@@ -332,7 +400,8 @@ def get_current_scene():
332
400
 
333
401
  def get_source_from_scene(scene_name):
334
402
  try:
335
- response = client.get_scene_item_list(name=scene_name)
403
+ with connection_pool.get_client() as client:
404
+ response = client.get_scene_item_list(name=scene_name)
336
405
  return response.scene_items[0] if response and response.scene_items else ''
337
406
  except Exception as e:
338
407
  logger.error(f"Error getting source from scene: {e}")
@@ -346,7 +415,8 @@ def get_active_source():
346
415
 
347
416
  def get_record_directory():
348
417
  try:
349
- response = client.get_record_directory()
418
+ with connection_pool.get_client() as client:
419
+ response = client.get_record_directory()
350
420
  return response.record_directory if response else ''
351
421
  except Exception as e:
352
422
  logger.error(f"Error getting recording folder: {e}")
@@ -354,17 +424,17 @@ def get_record_directory():
354
424
 
355
425
  def get_obs_scenes():
356
426
  try:
357
- response = client.get_scene_list()
427
+ with connection_pool.get_client() as client:
428
+ response = client.get_scene_list()
358
429
  return response.scenes if response else None
359
430
  except Exception as e:
360
431
  logger.error(f"Error getting scenes: {e}")
361
432
  return None
362
433
 
363
434
  async def register_scene_change_callback(callback):
364
- global client
365
435
  if await wait_for_obs_connected():
366
- if not client:
367
- logger.error("OBS client is not connected.")
436
+ if not connection_pool:
437
+ logger.error("OBS connection pool is not connected.")
368
438
  return
369
439
 
370
440
  def on_current_program_scene_changed(data):
@@ -391,7 +461,8 @@ def get_screenshot(compression=-1):
391
461
  return None
392
462
  start = time.time()
393
463
  logger.debug(f"Current source name: {current_source_name}")
394
- client.save_source_screenshot(name=current_source_name, img_format='png', width=None, height=None, file_path=screenshot, quality=compression)
464
+ with connection_pool.get_client() as client:
465
+ client.save_source_screenshot(name=current_source_name, img_format='png', width=None, height=None, file_path=screenshot, quality=compression)
395
466
  logger.debug(f"Screenshot took {time.time() - start:.3f} seconds to save")
396
467
  return screenshot
397
468
  except Exception as e:
@@ -410,11 +481,10 @@ def get_screenshot_base64(compression=75, width=None, height=None):
410
481
  if not current_source_name:
411
482
  logger.error("No active source found in the current scene.")
412
483
  return None
413
- # version = client.send("GetVersion", raw=True)
414
- # pprint(version)
415
- # responseraw = client.send("GetSourceScreenshot", {"sourceName": current_source_name, "imageFormat": "png", "imageWidth": width, "imageHeight": height, "compressionQuality": compression}, raw=True)
416
- response = client.get_source_screenshot(name=current_source_name, img_format='png', quality=compression, width=width, height=height)
417
- # print(responseraw)
484
+
485
+ with connection_pool.get_client() as client:
486
+ response = client.get_source_screenshot(name=current_source_name, img_format='png', quality=compression, width=width, height=height)
487
+
418
488
  if response and response.image_data:
419
489
  return response.image_data.split(',', 1)[-1] # Remove data:image/png;base64, prefix if present
420
490
  else:
@@ -435,7 +505,8 @@ def get_screenshot_PIL(source_name=None, compression=75, img_format='png', width
435
505
  logger.error("No active source found in the current scene.")
436
506
  return None
437
507
  while True:
438
- response = client.get_source_screenshot(name=source_name, img_format=img_format, quality=compression, width=width, height=height)
508
+ with connection_pool.get_client() as client:
509
+ response = client.get_source_screenshot(name=source_name, img_format=img_format, quality=compression, width=width, height=height)
439
510
  try:
440
511
  response.image_data = response.image_data.split(',', 1)[-1] # Remove data:image/png;base64, prefix if present
441
512
  except AttributeError:
@@ -448,8 +519,6 @@ def get_screenshot_PIL(source_name=None, compression=75, img_format='png', width
448
519
  image_data = response.image_data.split(',', 1)[-1] # Remove data:image/png;base64, prefix if present
449
520
  image_data = base64.b64decode(image_data)
450
521
  img = Image.open(io.BytesIO(image_data)).convert("RGBA")
451
- # if width and height:
452
- # img = img.resize((width, height), Image.Resampling.LANCZOS)
453
522
  return img
454
523
  return None
455
524
 
@@ -472,103 +541,84 @@ def get_current_game(sanitize=False, update=True):
472
541
  def set_fit_to_screen_for_scene_items(scene_name: str):
473
542
  """
474
543
  Sets all sources in a given scene to "Fit to Screen" (like Ctrl+F in OBS).
475
-
476
- This function fetches the canvas dimensions, then iterates through all scene
477
- items in the specified scene and applies a transform that scales them to
478
- fit within the canvas while maintaining aspect ratio and centering them.
479
-
480
- Args:
481
- scene_name: The name of the scene to modify.
482
544
  """
545
+ if not scene_name:
546
+ return
547
+
483
548
  try:
484
- # 1. Get the canvas (base) resolution from OBS video settings
485
- video_settings = client.get_video_settings()
486
- if not hasattr(video_settings, 'base_width') or not hasattr(video_settings, 'base_height'):
487
- logger.debug("Video settings do not have base_width or base_height attributes, probably weird websocket error issue? Idk what causes it..")
488
- return
489
- canvas_width = video_settings.base_width
490
- canvas_height = video_settings.base_height
549
+ with connection_pool.get_client() as client:
550
+ # 1. Get the canvas (base) resolution from OBS video settings
551
+ video_settings = client.get_video_settings()
552
+ if not hasattr(video_settings, 'base_width') or not hasattr(video_settings, 'base_height'):
553
+ logger.debug("Video settings do not have base_width or base_height attributes, probably weird websocket error issue? Idk what causes it..")
554
+ return
555
+ canvas_width = video_settings.base_width
556
+ canvas_height = video_settings.base_height
491
557
 
492
- # 2. Get the list of items in the specified scene
493
- scene_items_response = client.get_scene_item_list(scene_name)
494
- items = scene_items_response.scene_items if scene_items_response.scene_items else []
558
+ # 2. Get the list of items in the specified scene
559
+ scene_items_response = client.get_scene_item_list(scene_name)
560
+ items = scene_items_response.scene_items if scene_items_response.scene_items else []
495
561
 
496
- if not items:
497
- logger.warning(f"No items found in scene '{scene_name}'.")
498
- return
562
+ if not items:
563
+ logger.warning(f"No items found in scene '{scene_name}'.")
564
+ return
499
565
 
500
- # 3. Loop through each item and apply the "Fit to Screen" transform
501
- for item in items:
502
- item_id = item['sceneItemId']
503
- source_name = item['sourceName']
504
-
505
- scene_item_transform = item.get('sceneItemTransform', {})
506
-
507
- source_width = scene_item_transform.get('sourceWidth', None)
508
- source_height = scene_item_transform.get('sourceHeight', None)
509
-
510
- aspect_ratio_different = False
511
- already_cropped = any([
512
- scene_item_transform.get('cropLeft', 0) != 0,
513
- scene_item_transform.get('cropRight', 0) != 0,
514
- scene_item_transform.get('cropTop', 0) != 0,
515
- scene_item_transform.get('cropBottom', 0) != 0,
516
- ])
517
-
518
- if source_width and source_height and not already_cropped:
519
- source_aspect_ratio = source_width / source_height
520
- canvas_aspect_ratio = canvas_width / canvas_height
521
- # Check if aspect ratio is different and if it's a standard aspect ratio
522
- aspect_ratio_different = abs(source_aspect_ratio - canvas_aspect_ratio) > 0.01
523
-
524
- # List of standard aspect ratios
525
- standard_ratios = [
526
- 4 / 3,
527
- 16 / 9,
528
- 16 / 10,
529
- 21 / 9,
530
- 32 / 9,
531
- 5 / 4,
532
- 3 / 2,
533
- ]
534
-
535
- def is_standard_ratio(ratio):
536
- return any(abs(ratio - std) < 0.02 for std in standard_ratios)
537
-
538
- # Only crop if both source and canvas are standard aspect ratios
539
- if aspect_ratio_different:
540
- if not (is_standard_ratio(source_aspect_ratio) and is_standard_ratio(canvas_aspect_ratio)):
541
- aspect_ratio_different = False
542
-
543
- # This transform object is the equivalent of "Fit to Screen"
544
- fit_to_screen_transform = {
545
- 'boundsType': 'OBS_BOUNDS_SCALE_INNER',
546
- 'alignment': 5, # 5 = Center alignment (horizontal and vertical)
547
- 'boundsWidth': canvas_width,
548
- 'boundsHeight': canvas_height,
549
- 'positionX': 0,
550
- 'positionY': 0,
551
- }
552
-
553
- if not already_cropped:
554
- fit_to_screen_transform.update({
555
- 'cropLeft': 0 if not aspect_ratio_different or canvas_width > source_width else (source_width - canvas_width) // 2,
556
- 'cropRight': 0 if not aspect_ratio_different or canvas_width > source_width else (source_width - canvas_width) // 2,
557
- 'cropTop': 0 if not aspect_ratio_different or canvas_height > source_height else (source_height - canvas_height) // 2,
558
- 'cropBottom': 0 if not aspect_ratio_different or canvas_height > source_height else (source_height - canvas_height) // 2,
559
- })
566
+ # 3. Loop through each item and apply the "Fit to Screen" transform
567
+ for item in items:
568
+ item_id = item['sceneItemId']
569
+ source_name = item['sourceName']
570
+
571
+ scene_item_transform = item.get('sceneItemTransform', {})
572
+
573
+ source_width = scene_item_transform.get('sourceWidth', None)
574
+ source_height = scene_item_transform.get('sourceHeight', None)
575
+
576
+ aspect_ratio_different = False
577
+ already_cropped = any([
578
+ scene_item_transform.get('cropLeft', 0) != 0,
579
+ scene_item_transform.get('cropRight', 0) != 0,
580
+ scene_item_transform.get('cropTop', 0) != 0,
581
+ scene_item_transform.get('cropBottom', 0) != 0,
582
+ ])
583
+
584
+ if source_width and source_height and not already_cropped:
585
+ source_aspect_ratio = source_width / source_height
586
+ canvas_aspect_ratio = canvas_width / canvas_height
587
+ aspect_ratio_different = abs(source_aspect_ratio - canvas_aspect_ratio) > 0.01
588
+
589
+ standard_ratios = [4 / 3, 16 / 9, 16 / 10, 21 / 9, 32 / 9, 5 / 4, 3 / 2]
590
+
591
+ def is_standard_ratio(ratio):
592
+ return any(abs(ratio - std) < 0.02 for std in standard_ratios)
593
+
594
+ if aspect_ratio_different:
595
+ if not (is_standard_ratio(source_aspect_ratio) and is_standard_ratio(canvas_aspect_ratio)):
596
+ aspect_ratio_different = False
597
+
598
+ fit_to_screen_transform = {
599
+ 'boundsType': 'OBS_BOUNDS_SCALE_INNER', 'alignment': 5,
600
+ 'boundsWidth': canvas_width, 'boundsHeight': canvas_height,
601
+ 'positionX': 0, 'positionY': 0,
602
+ }
603
+
604
+ if not already_cropped:
605
+ fit_to_screen_transform.update({
606
+ 'cropLeft': 0 if not aspect_ratio_different or canvas_width > source_width else (source_width - canvas_width) // 2,
607
+ 'cropRight': 0 if not aspect_ratio_different or canvas_width > source_width else (source_width - canvas_width) // 2,
608
+ 'cropTop': 0 if not aspect_ratio_different or canvas_height > source_height else (source_height - canvas_height) // 2,
609
+ 'cropBottom': 0 if not aspect_ratio_different or canvas_height > source_height else (source_height - canvas_height) // 2,
610
+ })
560
611
 
561
- try:
562
- client.set_scene_item_transform(
563
- scene_name=scene_name,
564
- item_id=item_id,
565
- transform=fit_to_screen_transform
566
- )
567
- except obs.error.OBSSDKError as e:
568
- logger.error(f"Failed to set transform for source '{source_name}': {e}")
612
+ try:
613
+ client.set_scene_item_transform(
614
+ scene_name=scene_name,
615
+ item_id=item_id,
616
+ transform=fit_to_screen_transform
617
+ )
618
+ except obs.error.OBSSDKError as e:
619
+ logger.error(f"Failed to set transform for source '{source_name}': {e}")
569
620
 
570
621
  except obs.error.OBSSDKError as e:
571
- # This will catch errors like "scene not found"
572
622
  logger.error(f"An OBS error occurred: {e}")
573
623
  except Exception as e:
574
624
  logger.error(f"An unexpected error occurred: {e}")
@@ -576,12 +626,14 @@ def set_fit_to_screen_for_scene_items(scene_name: str):
576
626
 
577
627
  def main():
578
628
  start_obs()
579
- connect_to_obs()
629
+ # connect_to_obs() is async, main is not. Use the sync version.
630
+ connect_to_obs_sync()
580
631
  # Test each method
581
632
  print("Testing `get_obs_path`:", get_obs_path())
582
633
  print("Testing `is_process_running` with PID 1:", is_process_running(1))
583
634
  print("Testing `check_obs_folder_is_correct`:")
584
- check_obs_folder_is_correct()
635
+ # This is async, need to run it in an event loop if testing from main
636
+ asyncio.run(check_obs_folder_is_correct())
585
637
  print("Testing `get_obs_websocket_config_values`:")
586
638
  try:
587
639
  get_obs_websocket_config_values()
@@ -595,10 +647,13 @@ def main():
595
647
  print("Testing `stop_replay_buffer`:")
596
648
  stop_replay_buffer()
597
649
  print("Testing `save_replay_buffer`:")
598
- save_replay_buffer()
650
+ try:
651
+ save_replay_buffer()
652
+ except Exception as e:
653
+ print(f"Could not save replay buffer: {e}")
599
654
  current_scene = get_current_scene()
600
655
  print("Testing `get_current_scene`:", current_scene)
601
- print("Testing `get_source_from_scene` with dummy scene:", get_source_from_scene(current_scene))
656
+ print("Testing `get_source_from_scene` with current scene:", get_source_from_scene(current_scene))
602
657
  print("Testing `get_record_directory`:", get_record_directory())
603
658
  print("Testing `get_obs_scenes`:", get_obs_scenes())
604
659
  print("Testing `get_screenshot`:", get_screenshot())
@@ -612,51 +667,4 @@ def main():
612
667
  if __name__ == '__main__':
613
668
  logging.basicConfig(level=logging.INFO)
614
669
  connect_to_obs_sync()
615
- set_fit_to_screen_for_scene_items(get_current_scene())
616
- # main()
617
- # connect_to_obs_sync()
618
- # img = get_screenshot_PIL(source_name="Display Capture 2", compression=75, img_format='png', width=1280, height=720)
619
- # img.show()
620
- # while True:
621
- # print(get_active_source())
622
- # time.sleep(3)
623
- # i = 100
624
- # for i in range(1, 100):
625
- # print(f"Getting screenshot {i}")
626
- # start = time.time()
627
- # # get_screenshot(compression=95)
628
- # # get_screenshot_base64(compression=95, width=1280, height=720)
629
-
630
- # img = get_screenshot_PIL(compression=i, img_format='jpg', width=1280, height=720)
631
- # end = time.time()
632
- # print(f"Time taken to get screenshot with compression {i}: {end - start} seconds")
633
-
634
- # for i in range(1, 100):
635
- # print(f"Getting screenshot {i}")
636
- # start = time.time()
637
- # # get_screenshot(compression=95)
638
- # # get_screenshot_base64(compression=95, width=1280, height=720)
639
-
640
- # img = get_screenshot_PIL(compression=i, img_format='jpg', width=2560, height=1440)
641
- # end = time.time()
642
- # print(f"Time taken to get screenshot full sized jpg with compression {i}: {end - start} seconds")
643
-
644
- # png_img = get_screenshot_PIL(compression=75, img_format='png', width=1280, height=720)
645
-
646
- # jpg_img = get_screenshot_PIL(compression=100, img_format='jpg', width=2560, height=1440)
647
-
648
- # png_img.show()
649
- # jpg_img.show()
650
-
651
- # start = time.time()
652
- # with mss() as sct:
653
- # monitor = sct.monitors[1]
654
- # sct_img = sct.grab(monitor)
655
- # img = Image.frombytes('RGB', sct_img.size, sct_img.bgra, 'raw', 'BGRX')
656
- # img.show()
657
- # end = time.time()
658
- # print(f"Time taken to get screenshot with mss: {end - start} seconds")
659
-
660
-
661
- # print(get_screenshot_base64(compression=75, width=1280, height=720))
662
-
670
+ set_fit_to_screen_for_scene_items(get_current_scene())