Skip to content

Commit

Permalink
Spliced code into modules, added displayswitch clone option, big cleanup
Browse files Browse the repository at this point in the history
* removed unused function

* fixed desktop video box not loading, changed parsing to monitors list instead of cfg

* cleanup

* created separated modules for audio, monitor, shortcut, mode. reverted to displayswitch with clone option on unsuported hardware

* moved window detection to window_monitor

* removed unused variable in setup.py

* readme and screenshot
  • Loading branch information
Odizinne authored Aug 1, 2024
1 parent 60047e7 commit 0bfa871
Show file tree
Hide file tree
Showing 12 changed files with 349 additions and 322 deletions.
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,14 @@ Place the directory wherever you like (`%localappdata%\Programs` is a good one)

## Usage


If you have a two monitors setup (Main monitor + TV), i do recommend you uncheck `Do not use displayswitch`.
displayswitch.exe will automatically disable your primary monitor and enable your external monitor.

If you have more than two monitors, you'll need to check it, and select your preferred monitor in Steam Big Picture settings.

Specify your audio outputs.
You can use a short name. BigPictureTV will try to find the correct audio output from keywords. Less is more.

For monitor switching, BigPictureTV relies on Windows built in `displayswitch.exe`.
Documentation for this is pretty weak, from my personal testing when using external screen with multiple monitors displayswitch seems to select the highest resolution monitor. For most case it will work just fine but not always.

If it does not work for you, you should check `Clone screen instead of switching`.

**You're all set!** You can now close the settings window.

## Build
Expand Down
Binary file modified assets/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
157 changes: 31 additions & 126 deletions src/BigPictureTV.py
Original file line number Diff line number Diff line change
@@ -1,112 +1,19 @@
import sys
import os
import json
import subprocess
import time
import re
import winshell
import pygetwindow as gw
from enum import Enum
import darkdetect
from PyQt6.QtWidgets import QApplication, QSystemTrayIcon, QMenu, QMainWindow
from PyQt6.QtGui import QIcon, QAction
from PyQt6.QtCore import QTimer, QSharedMemory
from design import Ui_MainWindow
from steam_language_reader import get_big_picture_window_title
from monitor_manager import enable_clone_mode, enable_external_mode, enable_internal_mode
from audio_manager import switch_audio, is_audio_device_cmdlets_installed
from mode_manager import Mode, read_current_mode, write_current_mode
from shortcut_manager import check_startup_shortcut, handle_startup_checkbox_state_changed
from window_monitor import is_bigpicture_running

SETTINGS_FILE = os.path.join(os.environ["APPDATA"], "BigPictureTV", "settings.json")
ICONS_FOLDER = "icons" if getattr(sys, "frozen", False) else os.path.join(os.path.dirname(__file__), "icons")
MULTIMONITORTOOL_PATH = "dependencies/multimonitortool-x64/MultiMonitorTool.exe"


class Mode(Enum):
DESKTOP = 1
GAMEMODE = 2


def get_mode_file_path():
app_data_folder = os.path.join(os.environ["APPDATA"], "BigPictureTV")
if not os.path.exists(app_data_folder):
os.makedirs(app_data_folder)
return os.path.join(app_data_folder, "current_mode.txt")


def read_current_mode():
return Mode.GAMEMODE if os.path.exists(get_mode_file_path()) else Mode.DESKTOP


def get_audio_devices():
cmd = "powershell Get-AudioDevice -list"
output = subprocess.check_output(cmd, shell=True, text=True)
devices = re.findall(r"Index\s+:\s+(\d+)\s+.*?Name\s+:\s+(.*?)\s+ID\s+:\s+{(.*?)}", output, re.DOTALL)
return devices


def set_audio_device(device_name, devices):
device_words = device_name.lower().split()
for index, name, _ in devices:
if all(word in name.lower() for word in device_words):
cmd = f"powershell set-audiodevice -index {index}"
result = subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return result.returncode == 0
return False


def switch_audio(audio_output):
devices = get_audio_devices()
success = set_audio_device(audio_output, devices)
retries = 0
while not success and retries < 10:
print("Failed to switch audio, retrying...")
time.sleep(1)
success = set_audio_device(audio_output, devices)
retries += 1
if not success:
print("Failed to switch audio after 10 attempts.")


def is_bigpicture_running():
big_picture_title = get_big_picture_window_title().lower()
big_picture_words = big_picture_title.split()
current_window_titles = [title.lower() for title in gw.getAllTitles()]

for window_title in current_window_titles:
if all(word in window_title for word in big_picture_words):
return True

return False


def write_current_mode(current_mode):
file_path = get_mode_file_path()
if current_mode == Mode.GAMEMODE:
open(file_path, "w").close()
elif current_mode == Mode.DESKTOP and os.path.exists(file_path):
os.remove(file_path)


def manage_startup_shortcut(state):
target_path = os.path.join(os.getcwd(), "BigPictureTV.exe")
startup_folder = winshell.startup()
shortcut_path = os.path.join(startup_folder, "BigPictureTV.lnk")
if state:
winshell.CreateShortcut(
Path=shortcut_path,
Target=target_path,
Icon=(target_path, 0),
Description="Launch BigPictureTV",
StartIn=os.path.dirname(target_path),
)
elif os.path.exists(shortcut_path):
os.remove(shortcut_path)


def check_startup_shortcut():
return os.path.exists(os.path.join(winshell.startup(), "BigPictureTV.lnk"))


def handle_startup_checkbox_state_changed(state):
manage_startup_shortcut(state)


class BigPictureTV(QMainWindow):
Expand All @@ -122,13 +29,10 @@ def __init__(self):
self.settings = {}
self.first_run = False
self.paused = False
self.use_displayswitch = False
self.timer = QTimer()
self.gamemode_screen = "/external"
self.desktop_screen = "/internal"
self.load_settings()
self.initialize_ui()
self.is_audio_device_cmdlets_installed()
self.get_audio_capabilities()
self.current_mode = read_current_mode()
self.switch_mode(self.current_mode or Mode.DESKTOP)
self.tray_icon = self.create_tray_icon()
Expand All @@ -144,8 +48,8 @@ def initialize_ui(self):
self.ui.gamemodeEntry.textChanged.connect(self.save_settings)
self.ui.desktopEntry.textChanged.connect(self.save_settings)
self.ui.checkRateSpinBox.valueChanged.connect(self.save_settings)
self.ui.disable_displayswitch_box.stateChanged.connect(self.save_settings)
self.ui.startupCheckBox.setChecked(check_startup_shortcut())
self.ui.clone_checkbox.stateChanged.connect(self.save_settings)

self.apply_settings()

Expand Down Expand Up @@ -186,16 +90,16 @@ def apply_settings(self):
self.ui.desktopEntry.setText(self.settings.get("DESKTOP_AUDIO", ""))
self.ui.disableAudioCheckbox.setChecked(self.settings.get("DisableAudioSwitch", False))
self.ui.checkRateSpinBox.setValue(self.settings.get("CheckRate", 1000))
self.ui.disable_displayswitch_box.setChecked(self.settings.get("DisableDisplayswitch", False))
self.toggle_audio_settings(not self.ui.disableAudioCheckbox.isChecked())
self.ui.clone_checkbox.setChecked(self.settings.get("use_clone", False))

def save_settings(self):
self.settings = {
"GAMEMODE_AUDIO": self.ui.gamemodeEntry.text(),
"DESKTOP_AUDIO": self.ui.desktopEntry.text(),
"DisableAudioSwitch": self.ui.disableAudioCheckbox.isChecked(),
"CheckRate": self.ui.checkRateSpinBox.value(),
"DisableDisplayswitch": self.ui.disable_displayswitch_box.isChecked(),
"use_clone": self.ui.clone_checkbox.isChecked(),
}
os.makedirs(os.path.dirname(SETTINGS_FILE), exist_ok=True)
with open(SETTINGS_FILE, "w") as f:
Expand All @@ -204,34 +108,35 @@ def save_settings(self):
def switch_mode(self, mode):
if mode == self.current_mode:
return

self.current_mode = mode
self.switch_screen(self.gamemode_screen if mode == Mode.GAMEMODE else self.desktop_screen)
gamemode_audio = self.settings.get("GAMEMODE_AUDIO")
desktop_audio = self.settings.get("DESKTOP_AUDIO")

self.switch_screen("gamemode" if mode == Mode.GAMEMODE else "desktop")

if not self.ui.disableAudioCheckbox.isChecked():
switch_audio(
self.settings.get("GAMEMODE_AUDIO") if mode == Mode.GAMEMODE else self.settings.get("DESKTOP_AUDIO")
)
switch_audio(gamemode_audio if mode == Mode.GAMEMODE else desktop_audio)
write_current_mode(mode)

if self.tray_icon:
self.update_tray_icon_menu()
self.update_tray_icon()

def switch_screen(self, mode):
if not self.ui.disable_displayswitch_box.isChecked():
if mode == self.gamemode_screen:
mode = "/external"
else:
mode = "/internal"
subprocess.run(["displayswitch.exe", mode], check=True)

def is_audio_device_cmdlets_installed(self):
cmd = 'powershell "Get-Module -ListAvailable -Name AudioDeviceCmdlets"'
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if "AudioDeviceCmdlets" in result.stdout:
return True
self.ui.disableAudioCheckbox.setChecked(True)
self.ui.disableAudioCheckbox.setEnabled(False)
self.toggle_audio_settings(False)
return False
def switch_screen(self, screen):
if screen == "gamemode" and self.ui.clone_checkbox.isChecked():
enable_clone_mode()
elif screen == "gamemode" and not self.ui.clone_checkbox.isChecked():
enable_external_mode()
elif screen == "desktop":
enable_internal_mode()

def get_audio_capabilities(self):
if is_audio_device_cmdlets_installed() is False:
self.ui.disableAudioCheckbox.setChecked(True)
self.ui.disableAudioCheckbox.setEnabled(False)
self.toggle_audio_settings(False)
return

def update_mode(self):
if is_bigpicture_running() and self.current_mode != Mode.GAMEMODE:
Expand Down
42 changes: 42 additions & 0 deletions src/audio_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import subprocess
import re
import time


def get_audio_devices():
cmd = "powershell Get-AudioDevice -list"
output = subprocess.check_output(cmd, shell=True, text=True)
devices = re.findall(r"Index\s+:\s+(\d+)\s+.*?Name\s+:\s+(.*?)\s+ID\s+:\s+{(.*?)}", output, re.DOTALL)
return devices


def set_audio_device(device_name, devices):
device_words = device_name.lower().split()
for index, name, _ in devices:
if all(word in name.lower() for word in device_words):
cmd = f"powershell set-audiodevice -index {index}"
result = subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return result.returncode == 0
return False


def switch_audio(audio_output):
devices = get_audio_devices()
success = set_audio_device(audio_output, devices)
retries = 0
while not success and retries < 10:
print("Failed to switch audio, retrying...")
time.sleep(1)
success = set_audio_device(audio_output, devices)
retries += 1
if not success:
print("Failed to switch audio after 10 attempts.")


def is_audio_device_cmdlets_installed():
cmd = 'powershell "Get-Module -ListAvailable -Name AudioDeviceCmdlets"'
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if "AudioDeviceCmdlets" in result.stdout:
return True
else:
return False
Loading

0 comments on commit 0bfa871

Please sign in to comment.