-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial implementation of resolution changer.
- Loading branch information
Showing
7 changed files
with
344 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
[[source]] | ||
name = "pypi" | ||
url = "https://pypi.org/simple" | ||
verify_ssl = true | ||
|
||
[dev-packages] | ||
|
||
[packages] | ||
pywin32 = "*" | ||
pyinstaller = "*" | ||
|
||
[requires] | ||
python_version = "3.11" |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,80 @@ | ||
# sunshine_utils | ||
# Sunshine Utils | ||
|
||
Sunshine Utils offers some useful utils for using with Sunshine / Moonlight / Playnite game streaming. | ||
|
||
1. `resolution_change` - a Windows utility to change the screen resolution before a gaming session and reset it after | ||
|
||
## resolution_change | ||
|
||
### Basic setup | ||
|
||
1. Copy `resolution_change.exe` to a folder on your computer - I use the sunshine folder | ||
2. In Sunshine Add a "Command Perparations" | ||
1. In the "Do" box set the resolution you want e.g. `C:\Program Files\Sunshine\resolution_change.exe -p 1080p` | ||
2. In the "Undo" box to reset the resolution: `C:\Program Files\Sunshine\resolution_change.exe -r` | ||
3. If using this with Playnite use the following in the "Command" for Playnite `"C:\<PLAYNITE_FOLDER>\Playnite.FullscreenApp.exe" --hidesplashscreen` | ||
|
||
 | ||
|
||
### Advanced Customization | ||
|
||
``` | ||
usage: resolution_change.py [-h] [-l] [--width WIDTH] [--height HEIGHT] | ||
[--refreshrate REFRESHRATE] [-d DISPLAY] | ||
[-p PRESET] [-r] [--wait WAIT] [--debug] | ||
Changes monitor resolution | ||
options: | ||
-h, --help show this help message and exit | ||
-l, --list-displays List current displays and exit | ||
--width WIDTH Resolution Width | ||
--height HEIGHT Resolution Height | ||
--refreshrate REFRESHRATE | ||
The refresh rate in hertz | ||
-d DISPLAY, --display DISPLAY | ||
Which display to use e.g. \\.\DISPLAY1 (defaults to current primary) | ||
-p PRESET, --preset PRESET | ||
A preset to use can be one of: 4k, 2k, 1080p, 720p | ||
-r, --reset Reset the resolution that a previous run has changed | ||
--wait WAIT Time in seconds to wait after changing resolution before exiting | ||
--debug Enable debug mode so the program won't exit after running | ||
``` | ||
|
||
You can list out displays on your machine using `-l` or `--list-displays` this will show the current enabled displays | ||
|
||
By default, the primary monitor's resolution will be changed. You can target resolution changes to a particular display using `-d` or `--display` e.g. `resolution_change.exe --width 1920 --height 1080 -d "\\.\DISPLAY2"` | ||
|
||
Not all of these have been tested please raise an issue if you find any. | ||
|
||
|
||
## Development | ||
|
||
This is a Windows based utilities so developement needs to be done on Windows. | ||
|
||
### Developer Dependencies | ||
1. Install [Python 3.11](https://www.python.org/) for Windows, ensuring that you select "Add Python to PATH" during installation. | ||
2. Install [pipenv](https://pypi.org/project/pipenv/) via `pip install pipenv`. | ||
3. Install [git](https://git-scm.com/download/win) for windows | ||
|
||
### Developer Setup | ||
1. Open a command prompt | ||
2. Clone this repo | ||
3. In the repo directory run `python.exe -m venv venv` | ||
4. To active the venv run `.\venv\Scripts\activate.bat` | ||
5. Run `pipenv install` to install the dependencies | ||
6. To run locally `python resolution_change.py -p 1080p` | ||
7. When finished to deactive the virtual env `.\venv\Scripts\deactivate.bat` | ||
|
||
### To Build the exe | ||
1. Activate the venv as above (and ensure you have installed the dependenices) | ||
2. Run `build.bat` | ||
3. You will find the exe in the dist folder | ||
|
||
# Contributions | ||
|
||
A big thank you goes out to [cgarst](https://github.com/cgarst). | ||
He wrote [gamestream_launchpad](https://github.com/cgarst/gamestream_launchpad) a lot | ||
of the code in this repo uses his methods for changing the resolution. Also thanks to | ||
[ventorvar](https://github.com/ventorvar) whose pull request on gamestream_launchpad helped | ||
with multi monitor support. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pipenv.exe run pyinstaller --onefile resolution_change.py |
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import win32api | ||
import win32.lib.win32con as win32con | ||
import pywintypes | ||
from time import sleep | ||
|
||
import argparse | ||
|
||
PRESETS = { | ||
'4k': (3840, 2160), | ||
'2k': (2560, 1440), | ||
'1080p': (1920, 1080), | ||
'720p': (1280, 720), | ||
} | ||
|
||
|
||
class ResolutionChanger: | ||
|
||
def __init__(self): | ||
self.displays = {} | ||
self._update_display_devices() | ||
|
||
def _update_display_devices(self): | ||
self.displays = {} | ||
i = 0 | ||
while True: | ||
try: | ||
d = win32api.EnumDisplayDevices(None, i) | ||
enabled = d.StateFlags & win32con.DISPLAY_DEVICE_ATTACHED_TO_DESKTOP == win32con.DISPLAY_DEVICE_ATTACHED_TO_DESKTOP | ||
if enabled: | ||
self.displays[d.DeviceName] = { | ||
'device': d, | ||
'settings': win32api.EnumDisplaySettings(d.DeviceName, win32con.ENUM_CURRENT_SETTINGS) | ||
} | ||
i += 1 | ||
except pywintypes.error as e: | ||
break | ||
|
||
def _get_primary_display_name(self): | ||
for (display_name, details_map) in self.displays.items(): | ||
device = details_map['device'] | ||
primary = device.StateFlags & win32con.DISPLAY_DEVICE_PRIMARY_DEVICE == win32con.DISPLAY_DEVICE_PRIMARY_DEVICE | ||
if primary: | ||
return display_name | ||
return None | ||
|
||
def print_display_details(self): | ||
for (display_name, details_map) in self.displays.items(): | ||
device = details_map['device'] | ||
settings = details_map['settings'] | ||
enabled = device.StateFlags & win32con.DISPLAY_DEVICE_ATTACHED_TO_DESKTOP == win32con.DISPLAY_DEVICE_ATTACHED_TO_DESKTOP | ||
primary = device.StateFlags & win32con.DISPLAY_DEVICE_PRIMARY_DEVICE == win32con.DISPLAY_DEVICE_PRIMARY_DEVICE | ||
width = "NOT SET" | ||
height = "NOT SET" | ||
if settings: | ||
width = settings.PelsWidth | ||
height = settings.PelsHeight | ||
print(f"Display name: {display_name} - Enabled: {enabled}, Primary: {primary} - {width}x{height}") | ||
|
||
def change_display(self, target_display, new_width, new_height, new_refresh_rate=None, set_primary=False): | ||
if target_display == "primary": | ||
target_display = self._get_primary_display_name() | ||
|
||
pos = None | ||
if pos is None: | ||
pos = ( | ||
self.displays[target_display]['settings'].Position_x, | ||
self.displays[target_display]['settings'].Position_y) | ||
|
||
if new_refresh_rate is None: | ||
print("Switching resolution to {0}x{1}".format(new_width, new_height)) | ||
else: | ||
print("Switching resolution to {0}x{1} at {2}Hz".format(new_width, new_height, new_refresh_rate)) | ||
|
||
devmode = win32api.EnumDisplaySettings(target_display, win32con.ENUM_CURRENT_SETTINGS) | ||
devmode.PelsWidth = int(new_width) | ||
devmode.PelsHeight = int(new_height) | ||
devmode.Position_x = int(pos[0]) | ||
devmode.Position_y = int(pos[1]) | ||
devmode.Fields = win32con.DM_PELSWIDTH | win32con.DM_PELSHEIGHT | win32con.DM_POSITION | ||
|
||
if new_refresh_rate is not None: | ||
devmode.DisplayFrequency = new_refresh_rate | ||
devmode.Fields |= win32con.DM_DISPLAYFREQUENCY | ||
|
||
win32api.ChangeDisplaySettingsEx( | ||
target_display, devmode, win32con.CDS_SET_PRIMARY if set_primary else 0) | ||
pass | ||
|
||
def reset_resolutions(self): | ||
print("Resetting resolution") | ||
win32api.ChangeDisplaySettings(None, 0) | ||
print("Reset to:") | ||
self._update_display_devices() | ||
self.print_display_details() | ||
|
||
|
||
if __name__ == "__main__": | ||
parser = argparse.ArgumentParser(description="Changes monitor resolution") | ||
|
||
group = parser.add_mutually_exclusive_group(required=True) | ||
|
||
group.add_argument('-l', '--list-displays', default=False, action="store_true", | ||
help='List current displays and exit') | ||
|
||
group.add_argument('--width', type=int, help='Resolution Width', required=False) | ||
parser.add_argument('--height', type=int, help='Resolution Height', required=False) | ||
|
||
parser.add_argument('--refreshrate', type=int, required=False, | ||
help=f'The refresh rate in hertz') | ||
|
||
parser.add_argument('-d', '--display', default='primary', type=str, required=False, | ||
help=r'Which display to use e.g. \\.\DISPLAY1 (defaults to current primary)') | ||
|
||
group.add_argument('-p', '--preset', type=str, required=False, | ||
help=f'A preset to use can be one of: {PRESETS.keys()}') | ||
|
||
group.add_argument('-r', '--reset', default=False, action="store_true", | ||
help='Reset the resolution that a previous run has changed') | ||
|
||
parser.add_argument('--wait', type=int, default=5, | ||
help="Time in seconds to wait after changing resolution before exiting") | ||
|
||
parser.add_argument('--debug', default=False, action="store_true", | ||
help="Enable debug mode so the program won't exit after running") | ||
|
||
args = parser.parse_args() | ||
resolution_changer = ResolutionChanger() | ||
|
||
if args.list_displays: | ||
resolution_changer.print_display_details() | ||
|
||
elif args.width and not args.height: | ||
print("If using width you must also specfiy a height") | ||
parser.print_help() | ||
exit(-1) | ||
else: | ||
if args.width: | ||
resolution_changer.change_display(args.display, args.width, args.height, args.refreshrate) | ||
elif args.preset: | ||
(width, height) = PRESETS.get(args.preset) | ||
resolution_changer.change_display(args.display, width, height, args.refreshrate) | ||
elif args.reset: | ||
resolution_changer.reset_resolutions() | ||
else: | ||
print("Unexpected options") | ||
parser.print_help() | ||
|
||
sleep(args.wait) | ||
|
||
if args.debug: | ||
# Leave window open for debugging | ||
input("Paused for debug review. Press Enter key to close.") |