@@ -51,8 +51,6 @@ local osx_mouse_location = nil
5151local use_auto_follow_mouse = true
5252local use_follow_outside_bounds = false
5353local is_following_mouse = false
54- local force_16_9 = true
55- local lock_in = false
5654local follow_speed = 0.1
5755local follow_border = 0
5856local follow_safezone_sensitivity = 10
@@ -89,7 +87,11 @@ local m1, m2 = version:match("(%d+%.%d+)%.(%d+)")
8987local major = tonumber (m1 ) or 0
9088local minor = tonumber (m2 ) or 0
9189
92- local __ar16_9__ = 16 / 9
90+ local keep_shape = false
91+ local auto_start = false
92+ local auto_start_running = false
93+ local aspect_ratio_w = 1
94+ local aspect_ratio_h = 1
9395
9496-- Define the mouse cursor functions for each platform
9597if ffi .os == " Windows" then
@@ -300,6 +302,33 @@ function clamp(min, max, value)
300302 return math.max (min , math.min (max , value ))
301303end
302304
305+ ---
306+ -- Function to calculate GCD (Greatest Common Divisor)
307+ --- @param a number
308+ --- @param b number
309+ function gcd (a , b )
310+ while b ~= 0 do
311+ local temp = b
312+ b = a % b
313+ a = temp
314+ end
315+ return a
316+ end
317+
318+ ---
319+ -- Function to convert resolution to aspect ratio
320+ --- @param width number the width of the resolution
321+ --- @param height number the height of the resolution
322+ --- @return number aspect_width , number aspect_height the simplified aspect ratio as two numbers
323+ function resolution_to_aspect_ratio (width , height )
324+ -- Calculate GCD of width and height
325+ local divisor = gcd (width , height )
326+ -- Simplify width and height using GCD
327+ local aspect_width = width / divisor
328+ local aspect_height = height / divisor
329+ return aspect_width , aspect_height
330+ end
331+
303332---
304333-- Get the size and position of the monitor so that we know the top-left mouse point
305334--- @param source any The OBS source
@@ -462,6 +491,7 @@ function release_sceneitem()
462491 sceneitem_crop_orig = nil
463492 end
464493
494+ toggle_sceneitem_change_listener (false )
465495 obs .obs_sceneitem_release (sceneitem )
466496 sceneitem = nil
467497 end
@@ -545,16 +575,19 @@ function refresh_sceneitem(find_newest)
545575 -- We start at the current scene and use a BFS to look into any nested scenes
546576 local current = obs .obs_scene_from_source (scene_source )
547577 sceneitem = find_scene_item_by_name (current )
578+ toggle_sceneitem_change_listener (true )
548579
549580 obs .obs_source_release (scene_source )
550581 end
551582
552583 if not sceneitem then
553584 log (" WARNING: Source not part of the current scene hierarchy.\n " ..
554585 " Try selecting a different zoom source or switching scenes." )
586+ toggle_sceneitem_change_listener (false )
555587 obs .obs_sceneitem_release (sceneitem )
556588 obs .obs_source_release (source )
557589
590+
558591 sceneitem = nil
559592 source = nil
560593 return
@@ -788,8 +821,12 @@ function get_target_position(zoom)
788821 -- Remember that because we are using a crop/pad filter making the size smaller (dividing by zoom) means that we see less of the image
789822 -- in the same amount of space making it look bigger (aka zoomed in)
790823 local new_size = {
791- -- if aspect ratio should be fixed to 16:9, compute width from height instead of getting directly from display size
792- width = (force_16_9 and (zoom .source_size .height * __ar16_9__ ) or zoom .source_size .width ) / zoom .zoom_to ,
824+ width = (
825+ -- if should keep shape, use aspect ratio to get new width
826+ keep_shape and (zoom .source_size .height * (aspect_ratio_w / aspect_ratio_h ))
827+ -- else use source resolution
828+ or zoom .source_size .width
829+ ) / zoom .zoom_to ,
793830 height = zoom .source_size .height / zoom .zoom_to
794831 }
795832
@@ -831,26 +868,11 @@ function on_toggle_follow(pressed)
831868end
832869
833870function on_toggle_zoom (pressed , force_value )
834- if force_value or pressed then
871+ if pressed or force_value then
835872 -- Check if we are in a safe state to zoom
836- if force_value or zoom_state == ZoomState .ZoomedIn or zoom_state == ZoomState .None then
837- local should_zoom
838- if force_value == nil then
839- should_zoom = zoom_state == ZoomState .ZoomedIn or zoom_state == ZoomState .None
840- else
841- should_zoom = force_value
842- end
843-
844- if should_zoom then
845- log (" Zooming in" )
846- -- To zoom in, we get a new target based on where the mouse was when zoom was clicked
847- zoom_state = ZoomState .ZoomingIn
848- zoom_info .zoom_to = zoom_value
849- zoom_time = 0
850- locked_center = nil
851- locked_last_pos = nil
852- zoom_target = get_target_position (zoom_info )
853- else
873+ if zoom_state == ZoomState .ZoomedIn or zoom_state == ZoomState .None or force_value then
874+ local should_zoom = (force_value ~= nil ) and force_value or (zoom_state ~= ZoomState .ZoomedIn )
875+ if not should_zoom then
854876 log (" Zooming out" )
855877 -- To zoom out, we set the target back to whatever it was originally
856878 zoom_state = ZoomState .ZoomingOut
@@ -862,6 +884,18 @@ function on_toggle_zoom(pressed, force_value)
862884 is_following_mouse = false
863885 log (" Tracking mouse is off (due to zoom out)" )
864886 end
887+ else
888+ log (" Zooming in" )
889+ -- To zoom in, we get a new target based on where the mouse was when zoom was clicked
890+ if keep_shape then
891+ update_aspect_ratio ()
892+ end
893+ zoom_state = ZoomState .ZoomingIn
894+ zoom_info .zoom_to = zoom_value
895+ zoom_time = 0
896+ locked_center = nil
897+ locked_last_pos = nil
898+ zoom_target = get_target_position (zoom_info )
865899 end
866900
867901 -- Since we are zooming we need to start the timer for the animation and tracking
@@ -1081,6 +1115,53 @@ function on_transition_start(t)
10811115 -- We need to remove the crop from the sceneitem as the transition starts to avoid
10821116 -- a delay with the rendering where you see the old crop and jump to the new one
10831117 release_sceneitem ()
1118+
1119+ ---
1120+ -- Ensure to restart filters on scene change back
1121+ ---
1122+ if source_name ~= " obs-zoom-to-mouse-none" and auto_start and not auto_start_running then
1123+ log (" Auto starting" )
1124+ auto_start_running = true
1125+ local timer_interval = math.floor (obs .obs_get_frame_interval_ns () / 100000 )
1126+ obs .timer_add (wait_for_auto_start , timer_interval )
1127+ end
1128+ end
1129+
1130+ function update_aspect_ratio ()
1131+ if keep_shape == nil or not keep_shape or sceneitem == nil then
1132+ return
1133+ end
1134+
1135+ sceneitem_info_current = obs .obs_transform_info ()
1136+ obs .obs_sceneitem_get_info (sceneitem , sceneitem_info_current )
1137+ aspect_ratio_w , aspect_ratio_h = resolution_to_aspect_ratio (
1138+ sceneitem_info_current .bounds .x , sceneitem_info_current .bounds .y
1139+ )
1140+ end
1141+
1142+ function on_transform_update ()
1143+ update_aspect_ratio ()
1144+ if keep_shape then
1145+ if zoom_state == ZoomState .ZoomedIn then
1146+ -- Perform zoom again
1147+ zoom_state = ZoomState .ZoomingIn
1148+ zoom_info .zoom_to = zoom_value
1149+ zoom_time = 0
1150+ zoom_target = get_target_position (zoom_info )
1151+ end
1152+ end
1153+ end
1154+
1155+ function toggle_sceneitem_change_listener (value )
1156+ local scene = obs .obs_sceneitem_get_scene (sceneitem )
1157+ local scene_source = obs .obs_scene_get_source (scene )
1158+ local handler = obs .obs_source_get_signal_handler (scene_source )
1159+ if value then
1160+ update_aspect_ratio ()
1161+ obs .signal_handler_connect (handler , " item_transform" , on_transform_update )
1162+ else
1163+ obs .signal_handler_disconnect (handler , on_transform_update )
1164+ end
10841165end
10851166
10861167function on_frontend_event (event )
@@ -1142,15 +1223,14 @@ function on_settings_modified(props, prop, settings)
11421223 local sources_list = obs .obs_properties_get (props , " source" )
11431224 populate_zoom_sources (sources_list )
11441225 return true
1226+ elseif name == " keep_shape" then
1227+ on_transform_update ()
11451228 elseif name == " debug_logs" then
11461229 if obs .obs_data_get_bool (settings , " debug_logs" ) then
11471230 log_current_settings ()
11481231 end
11491232 end
11501233
1151- if lock_in ~= nil then
1152- on_toggle_zoom (true , lock_in )
1153- end
11541234 return false
11551235end
11561236
@@ -1160,6 +1240,8 @@ function log_current_settings()
11601240 local settings = {
11611241 zoom_value = zoom_value ,
11621242 zoom_speed = zoom_speed ,
1243+ auto_start = auto_start ,
1244+ keep_shape = keep_shape ,
11631245 use_auto_follow_mouse = use_auto_follow_mouse ,
11641246 use_follow_outside_bounds = use_follow_outside_bounds ,
11651247 follow_speed = follow_speed ,
@@ -1179,8 +1261,6 @@ function log_current_settings()
11791261 socket_port = socket_port ,
11801262 socket_poll = socket_poll ,
11811263 debug_logs = debug_logs ,
1182- force_16_9 = force_16_9 ,
1183- lock_in = lock_in ,
11841264 version = VERSION
11851265 }
11861266
@@ -1199,8 +1279,9 @@ function on_print_help()
11991279 " Zoom Source: The display capture in the current scene to use for zooming\n " ..
12001280 " Zoom Factor: How much to zoom in by\n " ..
12011281 " Zoom Speed: The speed of the zoom in/out animation\n " ..
1282+ " Zoomed at OBS startup: Start OBS with source zoomed\n " ..
1283+ " Dynamic Aspect Ratio: Adjusst zoom aspect ratio to canvas source size\n " ..
12021284 " Auto follow mouse: True to track the cursor while you are zoomed in\n " ..
1203- " Force 16:9: True to get zoomed window as 16:9 (fixes problems with wide resolutions)\n " ..
12041285 " Follow outside bounds: True to track the cursor even when it is outside the bounds of the source\n " ..
12051286 " Follow Speed: The speed at which the zoomed area will follow the mouse when tracking\n " ..
12061287 " Follow Border: The %distance from the edge of the source that will re-enable mouse tracking\n " ..
@@ -1256,10 +1337,15 @@ function script_properties()
12561337 -- Add the rest of the settings UI
12571338 local zoom = obs .obs_properties_add_float (props , " zoom_value" , " Zoom Factor" , 1 , 5 , 0.5 )
12581339 local zoom_speed = obs .obs_properties_add_float_slider (props , " zoom_speed" , " Zoom Speed" , 0.01 , 1 , 0.01 )
1259- local lock_in = obs .obs_properties_add_bool (props , " lock_in" , " Lock-In " )
1260- obs .obs_property_set_long_description (lock_in ,
1261- " When enabled, auto zoom feature cannot be disabled manually, and auto zoom restarts with OBS too" )
1262- local force_16_9 = obs .obs_properties_add_bool (props , " force_16_9" , " Force 16:9 aspect ratio " )
1340+
1341+ local auto_start = obs .obs_properties_add_bool (props , " auto_start" , " Zoomed at OBS startup " )
1342+ obs .obs_property_set_long_description (auto_start ,
1343+ " When enabled, auto zoom is activated on OBS start up as soon as possible" )
1344+
1345+ local keep_shape = obs .obs_properties_add_bool (props , " keep_shape" , " Dynamic Aspect Ratio " )
1346+ obs .obs_property_set_long_description (keep_shape ,
1347+ " When enabled, zoom will follow he aspect ratio of source in canvas" )
1348+
12631349 local follow = obs .obs_properties_add_bool (props , " follow" , " Auto follow mouse " )
12641350 obs .obs_property_set_long_description (follow ,
12651351 " When enabled mouse traking will auto-start when zoomed in without waiting for tracking toggle hotkey" )
@@ -1375,7 +1461,6 @@ function script_load(settings)
13751461 -- Load any other settings
13761462 zoom_value = obs .obs_data_get_double (settings , " zoom_value" )
13771463 zoom_speed = obs .obs_data_get_double (settings , " zoom_speed" )
1378- use_auto_follow_mouse = obs .obs_data_get_bool (settings , " follow" )
13791464 use_follow_outside_bounds = obs .obs_data_get_bool (settings , " follow_outside_bounds" )
13801465 follow_speed = obs .obs_data_get_double (settings , " follow_speed" )
13811466 follow_border = obs .obs_data_get_int (settings , " follow_border" )
@@ -1395,8 +1480,8 @@ function script_load(settings)
13951480 socket_port = obs .obs_data_get_int (settings , " socket_port" )
13961481 socket_poll = obs .obs_data_get_int (settings , " socket_poll" )
13971482 debug_logs = obs .obs_data_get_bool (settings , " debug_logs" )
1398- lock_in = obs .obs_data_get_bool (settings , " lock_in " )
1399- force_16_9 = obs .obs_data_get_bool (settings , " force_16_9 " )
1483+ auto_start = obs .obs_data_get_bool (settings , " auto_start " )
1484+ keep_shape = obs .obs_data_get_bool (settings , " keep_shape " )
14001485
14011486 obs .obs_frontend_add_event_callback (on_frontend_event )
14021487
@@ -1424,6 +1509,13 @@ function script_load(settings)
14241509 source_name = " "
14251510 use_socket = false
14261511 is_script_loaded = true
1512+
1513+ if source_name ~= " obs-zoom-to-mouse-none" and auto_start and not auto_start_running then
1514+ log (" Auto starting" )
1515+ auto_start_running = true
1516+ local timer_interval = math.floor (obs .obs_get_frame_interval_ns () / 100000 )
1517+ obs .timer_add (wait_for_auto_start , timer_interval )
1518+ end
14271519end
14281520
14291521function script_unload ()
@@ -1455,10 +1547,6 @@ function script_unload()
14551547 if socket_server ~= nil then
14561548 stop_server ()
14571549 end
1458-
1459- if lock_in then
1460- on_toggle_zoom (true , false )
1461- end
14621550end
14631551
14641552function script_defaults (settings )
@@ -1485,8 +1573,8 @@ function script_defaults(settings)
14851573 obs .obs_data_set_default_int (settings , " socket_port" , 12345 )
14861574 obs .obs_data_set_default_int (settings , " socket_poll" , 10 )
14871575 obs .obs_data_set_default_bool (settings , " debug_logs" , false )
1488- obs .obs_data_set_default_bool (settings , " force_16_9 " , true )
1489- obs .obs_data_set_default_bool (settings , " lock_in " , false )
1576+ obs .obs_data_set_default_bool (settings , " auto_start " , false )
1577+ obs .obs_data_set_default_bool (settings , " keep_shape " , false )
14901578end
14911579
14921580function script_save (settings )
@@ -1543,8 +1631,8 @@ function script_update(settings)
15431631 socket_port = obs .obs_data_get_int (settings , " socket_port" )
15441632 socket_poll = obs .obs_data_get_int (settings , " socket_poll" )
15451633 debug_logs = obs .obs_data_get_bool (settings , " debug_logs" )
1546- force_16_9 = obs .obs_data_get_bool (settings , " force_16_9 " )
1547- lock_in = obs .obs_data_get_bool (settings , " lock_in " )
1634+ auto_start = obs .obs_data_get_bool (settings , " auto_start " )
1635+ keep_shape = obs .obs_data_get_bool (settings , " keep_shape " )
15481636
15491637 -- Only do the expensive refresh if the user selected a new source
15501638 if source_name ~= old_source_name and is_obs_loaded then
@@ -1578,20 +1666,30 @@ function script_update(settings)
15781666 start_server ()
15791667 end
15801668
1581- if lock_in ~= nil and source == nil then
1669+ if source_name ~= " obs-zoom-to-mouse-none" and auto_start and not auto_start_running then
1670+ log (" Auto starting" )
1671+ auto_start_running = true
15821672 local timer_interval = math.floor (obs .obs_get_frame_interval_ns () / 100000 )
15831673 obs .timer_add (wait_for_auto_start , timer_interval )
1584- elseif lock_in ~= nil and source ~= nil then
1585- on_toggle_zoom (true , lock_in )
15861674 end
15871675end
15881676
15891677function wait_for_auto_start ()
1590- local found_source = obs .obs_get_source_by_name (source_name )
1591- if found_source ~= nil then
1592- source = found_source
1593- on_toggle_zoom (true , lock_in )
1678+ if source_name == " obs-zoom-to-mouse-none" or not auto_start then
15941679 obs .remove_current_callback ()
1680+ auto_start_running = false
1681+ log (" Auto start cancelled" )
1682+ else
1683+ auto_start_running = true
1684+ local found_source = obs .obs_get_source_by_name (source_name )
1685+ if found_source ~= nil then
1686+ -- zoom_state = ZoomState.ZoomingIn
1687+ source = found_source
1688+ on_toggle_zoom (true , true )
1689+ obs .remove_current_callback ()
1690+ auto_start_running = false
1691+ log (" Auto start done" )
1692+ end
15951693 end
15961694end
15971695
0 commit comments