Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/screenshots/per-game-quality-custom.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/per-game-quality-default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 9 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,15 @@
"details": "About this game",
"notOwned": "Not owned",
"playOn": "Play on {{store}}",
"storeOption": "{{store}}, {{ownership}}"
"storeOption": "{{store}}, {{ownership}}",
"streamProfile": {
"title": "Stream quality for this game",
"description": "Automatically use this profile whenever you launch this game.",
"custom": "Custom",
"global": "Global default",
"useCustom": "Use custom quality",
"useCustomHint": "Keep the global stream settings for every other game."
}
},
"streamLoading": {
"labels": {
Expand Down
17 changes: 16 additions & 1 deletion opennow-stable/src/main/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
normalizeRecordingBitrateMbps,
normalizeRecordingFps,
normalizeRecordingResolution,
normalizeGameStreamProfiles,
} from "@shared/gfn";
import type { StatsOverlayPosition } from "@shared/gfn";

Expand Down Expand Up @@ -303,6 +304,12 @@ export class SettingsManager {
migrated = true;
}

const gameStreamProfiles = normalizeGameStreamProfiles(settings.gameStreamProfiles);
if (JSON.stringify(settings.gameStreamProfiles) !== JSON.stringify(gameStreamProfiles)) {
settings.gameStreamProfiles = gameStreamProfiles;
migrated = true;
}

if (typeof settings.steamControllerCompatibilityMode !== "boolean") {
settings.steamControllerCompatibilityMode = false;
migrated = true;
Expand Down Expand Up @@ -385,7 +392,15 @@ export class SettingsManager {
* Get all current settings
*/
getAll(): Settings {
return { ...this.settings };
return {
...this.settings,
gameStreamProfiles: Object.fromEntries(
Object.entries(this.settings.gameStreamProfiles).map(([gameId, profile]) => [
gameId,
{ ...profile },
]),
),
};
}

/**
Expand Down
40 changes: 31 additions & 9 deletions opennow-stable/src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
createDefaultSettings,
createPlatformShortcutDefaults,
resolveEntitledStreamProfile,
resolveGameStreamProfile,
resolveRuntimePlatform,
SAFE_FALLBACK_STREAM_PROFILE,
} from "@shared/gfn";
Expand Down Expand Up @@ -688,11 +689,15 @@ export function App(): JSX.Element {
},
});

const buildCurrentStreamSettings = useCallback((subscriptionOverride?: SubscriptionInfo | null): StreamSettings => {
const buildCurrentStreamSettings = useCallback((
subscriptionOverride?: SubscriptionInfo | null,
gameId?: string | null,
): StreamSettings => {
const currentSubscription = subscriptionOverride === undefined ? subscriptionInfo : subscriptionOverride;
const requestedProfile = resolveGameStreamProfile(settings, gameId);
const entitledProfile = resolveEntitledStreamProfile(currentSubscription?.entitledResolutions ?? [], {
resolution: settings.resolution,
fps: settings.fps,
resolution: requestedProfile.resolution,
fps: requestedProfile.fps,
});
const streamProfile = entitledProfile ?? SAFE_FALLBACK_STREAM_PROFILE;
const codecProfile = resolveStreamProfileCodec(
Expand All @@ -704,7 +709,7 @@ export function App(): JSX.Element {
return {
resolution: streamProfile.resolution,
fps: streamProfile.fps,
maxBitrateMbps: settings.maxBitrateMbps,
maxBitrateMbps: requestedProfile.maxBitrateMbps,
codec: codecProfile.codec,
colorQuality: codecProfile.colorQuality,
keyboardLayout: settings.keyboardLayout,
Expand All @@ -730,6 +735,7 @@ export function App(): JSX.Element {
settings.enableCloudGsync,
settings.enableL4S,
settings.fps,
settings.gameStreamProfiles,
settings.gameLanguage,
settings.keyboardLayout,
settings.launchInConsoleMode,
Expand Down Expand Up @@ -1020,8 +1026,11 @@ export function App(): JSX.Element {
toggleRecording: "",
}), [shortcuts]);

const buildSignalingConnectRequest = useCallback((activeSession: SessionInfo): SignalingConnectRequest => {
const streamSettings = buildCurrentStreamSettings();
const buildSignalingConnectRequest = useCallback((
activeSession: SessionInfo,
gameId?: string | null,
): SignalingConnectRequest => {
const streamSettings = buildCurrentStreamSettings(undefined, gameId ?? streamingGameRef.current?.id);
return {
sessionId: activeSession.sessionId,
signalingServer: activeSession.signalingServer,
Expand Down Expand Up @@ -1616,14 +1625,15 @@ export function App(): JSX.Element {

const matchedContext = findGameContextForSession(existingSession);
if (matchedContext) {
streamingGameRef.current = matchedContext.game;
setStreamingGame(matchedContext.game);
setStreamingStore(matchedContext.variant?.store ?? null);
} else {
setStreamingStore(null);
}

const launchSubscription = await resolveSubscriptionInfoForLaunch();
const streamSettings = buildCurrentStreamSettings(launchSubscription);
const streamSettings = buildCurrentStreamSettings(launchSubscription, matchedContext?.game.id);
const claimed = await window.openNow.claimSession({
token,
streamingBaseUrl: effectiveStreamingBaseUrl,
Expand Down Expand Up @@ -1743,8 +1753,15 @@ export function App(): JSX.Element {
throw new Error("The running session is missing a server address, so resume was not possible.");
}

const matchedContext = findGameContextForSession(candidate);
if (matchedContext) {
streamingGameRef.current = matchedContext.game;
}
const recoverySubscription = await resolveSubscriptionInfoForLaunch();
const recoveryStreamSettings = buildCurrentStreamSettings(recoverySubscription);
const recoveryStreamSettings = buildCurrentStreamSettings(
recoverySubscription,
matchedContext?.game.id ?? streamingGameRef.current?.id,
);
const claimed = await window.openNow.claimSession({
token,
streamingBaseUrl: effectiveStreamingBaseUrl,
Expand All @@ -1762,7 +1779,6 @@ export function App(): JSX.Element {
return false;
}

const matchedContext = findGameContextForSession(candidate);
if (matchedContext) {
setStreamingGame(matchedContext.game);
setStreamingStore(matchedContext.variant?.store ?? null);
Expand Down Expand Up @@ -3014,6 +3030,12 @@ export function App(): JSX.Element {
onSelectVariant={(variantId) => {
if (detailsGame) handleSelectGameVariant(detailsGame.id, variantId);
}}
settings={settings}
entitledResolutions={subscriptionInfo?.entitledResolutions ?? []}
onGameStreamProfilesPreview={(profiles) => previewSetting("gameStreamProfiles", profiles)}
onGameStreamProfilesChange={(profiles) => {
void updateSetting("gameStreamProfiles", profiles);
}}
onPlay={(game, variantId) => {
handleCloseDetails();
void handleInitiatePlay(game, variantId);
Expand Down
19 changes: 18 additions & 1 deletion opennow-stable/src/renderer/src/components/GameDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import {
useState,
type JSX,
} from "react";
import type { GameInfo } from "@shared/gfn";
import type { EntitledResolution, GameInfo, GameStreamProfiles, Settings } from "@shared/gfn";
import { getControllerHeroBackgroundCandidates, getPlayerSummary } from "../lib/controllerCatalogUi";
import { getActiveStoreOption, getStoreOptions } from "../lib/gameCardStores";
import { useTranslation } from "../i18n";
import { getStoreDisplayName, getStoreIconComponent } from "./GameCard";
import { ModalSurface } from "./ui/ModalSurface";
import { GameStreamProfileSection } from "./GameStreamProfileSection";

export interface GameDetailModalProps {
open: boolean;
Expand All @@ -21,6 +22,10 @@ export interface GameDetailModalProps {
onExitComplete?: () => void;
onPlay: (game: GameInfo, variantId?: string) => void;
onSelectVariant: (variantId: string) => void;
settings: Settings;
entitledResolutions: EntitledResolution[];
onGameStreamProfilesPreview: (profiles: GameStreamProfiles) => void;
onGameStreamProfilesChange: (profiles: GameStreamProfiles) => void;
}

function GameDetailHero({ game }: { game: GameInfo }): JSX.Element {
Expand Down Expand Up @@ -102,6 +107,10 @@ export function GameDetailModal({
onExitComplete,
onPlay,
onSelectVariant,
settings,
entitledResolutions,
onGameStreamProfilesPreview,
onGameStreamProfilesChange,
}: GameDetailModalProps): JSX.Element {
const { t } = useTranslation();
const generatedId = useId();
Expand Down Expand Up @@ -235,6 +244,14 @@ export function GameDetailModal({
</div>
</section>
)}

<GameStreamProfileSection
game={game}
settings={settings}
entitledResolutions={entitledResolutions}
onPreview={onGameStreamProfilesPreview}
onChange={onGameStreamProfilesChange}
/>
</div>

<footer className="game-detail-actions">
Expand Down
Loading
Loading