Skip to content

Commit

Permalink
Initial implementation of resolution changer.
Browse files Browse the repository at this point in the history
  • Loading branch information
foxy82 committed May 5, 2023
1 parent 9586885 commit 3ea7908
Show file tree
Hide file tree
Showing 7 changed files with 344 additions and 1 deletion.
13 changes: 13 additions & 0 deletions Pipfile
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"
98 changes: 98 additions & 0 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 80 additions & 1 deletion README.md
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`

![](docs/static/playnite_setup.png)

### 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.
1 change: 1 addition & 0 deletions build.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pipenv.exe run pyinstaller --onefile resolution_change.py
Binary file added dist/resolution_change.exe
Binary file not shown.
Binary file added docs/static/playnite_setup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
152 changes: 152 additions & 0 deletions resolution_change.py
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.")

0 comments on commit 3ea7908

Please sign in to comment.