Skip to content
Open
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
30 changes: 28 additions & 2 deletions src/provider/geforce_now/session_client.zig
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub const Client = struct {
last_response_retryable: ?bool = null,
stream_width: u16 = fallback_stream_width,
stream_height: u16 = fallback_stream_height,
requested_frames_per_second: u16 = preferred_frames_per_second,
stream_frames_per_second: u16 = preferred_frames_per_second,

pub fn create(
Expand All @@ -48,6 +49,9 @@ pub const Client = struct {
.client_id = undefined,
};
uuid.generate(&client.client_id);
const configured_fps = c.go_handheld_ui_frames_per_second(client.ui);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
if (configured_fps > 0) client.requested_frames_per_second = @intCast(configured_fps);
client.stream_frames_per_second = client.requested_frames_per_second;
return client;
}

Expand Down Expand Up @@ -348,7 +352,7 @@ pub const Client = struct {
) !void {
self.stream_width = fallback_stream_width;
self.stream_height = fallback_stream_height;
self.stream_frames_per_second = preferred_frames_per_second;
self.stream_frames_per_second = self.requested_frames_per_second;
const summary = self.fetchSubscriptionStatus(provider_url, bearer) catch |err| {
if (err == error.Cancelled) return err;
std.debug.print("GeForce NOW membership check unavailable: {s}\n", .{@errorName(err)});
Expand All @@ -369,7 +373,7 @@ pub const Client = struct {
if (summary.bestForDisplayWithin(
display_width,
display_height,
preferred_frames_per_second,
self.requested_frames_per_second,
maximum_stream_width,
maximum_stream_height,
)) |selected| {
Expand Down Expand Up @@ -730,3 +734,25 @@ test "membership and region discovery requests preserve cancellation" {
try std.testing.expectEqual(@as(usize, 1), http.requests);
try std.testing.expectEqual(@as(usize, 1), http.polls);
}

test "stream mode selection keeps the configured frame rate" {
const http = @import("gfn_http_fake");
http.reset();
http.cancel_on_poll = std.math.maxInt(usize);
var auth = http.authClient(auth_client.Client);
var client = Client{
.allocator = std.testing.allocator,
.auth = &auth,
.ui = @ptrCast(auth.ui),
.client_id = [_]u8{0} ** 37,
.requested_frames_per_second = 60,
};
http.replyJson(
\\{"features":{"resolutions":[
\\ {"widthInPixels":1024,"heightInPixels":768,"framesPerSecond":30,"isEntitled":true},
\\ {"widthInPixels":1024,"heightInPixels":768,"framesPerSecond":60,"isEntitled":true}
\\]}}
);
try client.selectStreamMode("https://example.invalid/", auth.bearer().?, 640, 480);
try std.testing.expectEqual(@as(u16, 60), client.stream_frames_per_second);
}
16 changes: 13 additions & 3 deletions src/provider/geforce_now/webrtc_session.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ const c = @cImport({
const event_capacity = 64;
const maximum_tracks = 8;
const maximum_channels = 8;
const maximum_bitrate_kbps = 6000;
const minimum_bitrate_kbps = 4000;
const maximum_bitrate_kbps = 20000;

const EarlyIceCandidates = struct {
messages: std.BoundedArray(signaling_protocol.DecodedMessage, 32) = .{},
Expand Down Expand Up @@ -352,10 +353,19 @@ pub const Session = struct {
pub fn requestBitrate(self: *Session) void {
if (self.bitrate_requested or self.video_track < 0 or
c.go_video_pipeline_has_media(self.video) == 0) return;
if (c.rtcRequestBitrate(self.video_track, maximum_bitrate_kbps * 1000) >= 0)
const ceiling_bps = @as(u64, self.bitrateCeilingKbps()) * 1000;
if (c.rtcRequestBitrate(self.video_track, @intCast(ceiling_bps)) >= 0)
self.bitrate_requested = true;
}

fn bitrateCeilingKbps(self: *const Session) u32 {
return std.math.clamp(
c.go_handheld_ui_max_bitrate_kbps(self.ui),
minimum_bitrate_kbps,
maximum_bitrate_kbps,
);
}

pub fn destroy(self: *Session) void {
self.shutting_down.store(true, .release);
if (self.peer >= 0) {
Expand Down Expand Up @@ -432,7 +442,7 @@ pub const Session = struct {
self.width,
self.height,
self.frames_per_second,
maximum_bitrate_kbps,
self.bitrateCeilingKbps(),
);
defer self.allocator.free(nvst);
try self.signaling.?.sendAnswer(narrowed_sdp, nvst);
Expand Down
2 changes: 2 additions & 0 deletions src/ui/handheld_ui.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ int go_handheld_ui_pick_title(GoHandheldUi* ui, const GoCatalogTitle* titles, in
int go_handheld_ui_cancelled(const GoHandheldUi* ui);
unsigned int go_handheld_ui_stream_width(const GoHandheldUi* ui);
unsigned int go_handheld_ui_stream_height(const GoHandheldUi* ui);
unsigned int go_handheld_ui_frames_per_second(const GoHandheldUi* ui);
unsigned int go_handheld_ui_max_bitrate_kbps(const GoHandheldUi* ui);
void go_handheld_ui_draw_stream_controls(GoHandheldUi* ui, int mouse_mode, float pointer_x,
float pointer_y, int source_width, int source_height,
int show_hint);
Expand Down
8 changes: 8 additions & 0 deletions src/ui/handheld_ui.zig
Original file line number Diff line number Diff line change
Expand Up @@ -747,3 +747,11 @@ pub export fn go_handheld_ui_stream_width(ui: ?*const Ui) c_uint {
pub export fn go_handheld_ui_stream_height(ui: ?*const Ui) c_uint {
return (ui orelse return 480).stream_height;
}

pub export fn go_handheld_ui_frames_per_second(ui: ?*const Ui) c_uint {
return (ui orelse return 30).settings.frames_per_second;
}

pub export fn go_handheld_ui_max_bitrate_kbps(ui: ?*const Ui) c_uint {
return (ui orelse return 6000).settings.max_bitrate_kbps;
}
35 changes: 35 additions & 0 deletions src/ui/persistent_settings.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ const std = @import("std");

pub const max_games = 1024;
pub const product_id_capacity = 64;
pub const frames_per_second_choices = [_]u16{ 30, 60 };
pub const bitrate_choices_kbps = [_]u32{ 4000, 6000, 8000, 12000, 16000, 20000 };

pub const FaceButtonMode = enum {
system,
Expand All @@ -18,6 +20,8 @@ pub const Store = struct {
path_length: usize = 0,
face_buttons: FaceButtonMode = .system,
artwork_enabled: bool = true,
frames_per_second: u16 = 30,
max_bitrate_kbps: u32 = 6000,
games: [max_games]GameSettings = [_]GameSettings{.{}} ** max_games,
game_count: usize = 0,

Expand Down Expand Up @@ -70,6 +74,8 @@ pub const Store = struct {
try writer.writeAll("version\t1\n");
try writer.print("face_buttons\t{s}\n", .{@tagName(self.face_buttons)});
try writer.print("artwork\t{d}\n", .{@intFromBool(self.artwork_enabled)});
try writer.print("frames_per_second\t{d}\n", .{self.frames_per_second});
try writer.print("max_bitrate_kbps\t{d}\n", .{self.max_bitrate_kbps});
for (self.games[0..self.game_count]) |*entry| {
try writer.print("game\t{s}\t{d}\n", .{
productId(entry),
Expand Down Expand Up @@ -118,6 +124,16 @@ pub const Store = struct {
} else if (std.mem.eql(u8, kind, "artwork")) {
const value = fields.next() orelse continue;
self.artwork_enabled = std.mem.eql(u8, value, "1");
} else if (std.mem.eql(u8, kind, "frames_per_second")) {
const value = fields.next() orelse continue;
const parsed = std.fmt.parseUnsigned(u16, value, 10) catch continue;
if (isChoice(u16, &frames_per_second_choices, parsed))
self.frames_per_second = parsed;
} else if (std.mem.eql(u8, kind, "max_bitrate_kbps")) {
const value = fields.next() orelse continue;
const parsed = std.fmt.parseUnsigned(u32, value, 10) catch continue;
if (isChoice(u32, &bitrate_choices_kbps, parsed))
self.max_bitrate_kbps = parsed;
} else if (std.mem.eql(u8, kind, "game")) {
const id = fields.next() orelse continue;
const favorite = fields.next() orelse continue;
Expand All @@ -141,6 +157,10 @@ pub fn validProductId(value: []const u8) bool {
return true;
}

fn isChoice(comptime T: type, choices: []const T, value: T) bool {
return std.mem.indexOfScalar(T, choices, value) != null;
}

test "settings round trip through the file format" {
var store = Store{};
store.face_buttons = .swapped;
Expand Down Expand Up @@ -169,3 +189,18 @@ test "settings reject unknown versions and unsafe ids" {
try std.testing.expectError(error.UnsupportedSettings, store.parse("artwork\t0\nversion\t1\n"));
try std.testing.expect(store.game("bad\tid") == null);
}

test "settings keep stream quality within the supported choices" {
var store = Store{};
try store.parse("version\t1\nframes_per_second\t60\nmax_bitrate_kbps\t12000\n");
try std.testing.expectEqual(@as(u16, 60), store.frames_per_second);
try std.testing.expectEqual(@as(u32, 12000), store.max_bitrate_kbps);

var invalid = Store{};
try invalid.parse("version\t1\nframes_per_second\t1\nmax_bitrate_kbps\t0\n");
try std.testing.expectEqual(@as(u16, 30), invalid.frames_per_second);
try std.testing.expectEqual(@as(u32, 6000), invalid.max_bitrate_kbps);

try invalid.parse("version\t1\nmax_bitrate_kbps\t4294967295\n");
try std.testing.expectEqual(@as(u32, 6000), invalid.max_bitrate_kbps);
}
38 changes: 31 additions & 7 deletions src/ui/settings_view.zig
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,21 @@ const StopRequested = ?*const fn (?*anyopaque) callconv(.c) c_int;
const Row = enum {
face_buttons,
artwork,
framerate,
bitrate,
service,
sign_out,
};

const bitrate_choices = settings.bitrate_choices_kbps;

fn nextBitrate(current: u32) u32 {
for (bitrate_choices, 0..) |value, index| {
if (value == current) return bitrate_choices[(index + 1) % bitrate_choices.len];
}
return bitrate_choices[0];
}

const Action = enum {
none,
switch_provider,
Expand Down Expand Up @@ -86,7 +97,7 @@ pub fn run(
repeat.begin(.down, c.SDL_GetTicks());
},
c.SDL_CONTROLLER_BUTTON_DPAD_LEFT, c.SDL_CONTROLLER_BUTTON_DPAD_RIGHT => {
if (selected == .face_buttons or selected == .artwork)
if (selected != .service and selected != .sign_out)
_ = activate(selected, controller, store);
},
else => {},
Expand Down Expand Up @@ -130,6 +141,8 @@ fn activate(
);
},
.artwork => store.artwork_enabled = !store.artwork_enabled,
.framerate => store.frames_per_second = if (store.frames_per_second >= 60) 30 else 60,
.bitrate => store.max_bitrate_kbps = nextBitrate(store.max_bitrate_kbps),
.service => return .switch_provider,
.sign_out => return .sign_out,
}
Expand Down Expand Up @@ -165,15 +178,19 @@ fn previousRow(row: Row) Row {
return switch (row) {
.face_buttons => .sign_out,
.artwork => .face_buttons,
.service => .artwork,
.framerate => .artwork,
.bitrate => .framerate,
.service => .bitrate,
.sign_out => .service,
};
}

fn nextRow(row: Row) Row {
return switch (row) {
.face_buttons => .artwork,
.artwork => .service,
.artwork => .framerate,
.framerate => .bitrate,
.bitrate => .service,
.service => .sign_out,
.sign_out => .face_buttons,
};
Expand Down Expand Up @@ -204,13 +221,20 @@ fn draw(
_ = c.SDL_RenderFillRect(renderer, &footer);
font.text(renderer, 18, 14, 4, "SETTINGS", style.bright());

var fps_buffer: [16]u8 = undefined;
const fps_text = std.fmt.bufPrintZ(&fps_buffer, "{d} FPS", .{store.frames_per_second}) catch "30 FPS";
var bitrate_buffer: [16]u8 = undefined;
const bitrate_text = std.fmt.bufPrintZ(&bitrate_buffer, "{d} MBPS", .{store.max_bitrate_kbps / 1000}) catch "6 MBPS";

drawRow(renderer, 82, "FACE BUTTONS", if (store.face_buttons == .system) "SYSTEM" else "SWAPPED", selected == .face_buttons);
drawRow(renderer, 130, "GAME ARTWORK", if (store.artwork_enabled) "ON" else "OFF", selected == .artwork);
drawRow(renderer, 178, "STREAMING SERVICE", if (provider == .xbox) "XBOX" else "GEFORCE NOW", selected == .service);
drawRow(renderer, 226, "ACCOUNT", "SIGN OUT", selected == .sign_out);
drawRow(renderer, 128, "GAME ARTWORK", if (store.artwork_enabled) "ON" else "OFF", selected == .artwork);
drawRow(renderer, 174, "TARGET FRAMERATE", fps_text, selected == .framerate);
drawRow(renderer, 220, "MAX BITRATE", bitrate_text, selected == .bitrate);
drawRow(renderer, 266, "STREAMING SERVICE", if (provider == .xbox) "XBOX" else "GEFORCE NOW", selected == .service);
drawRow(renderer, 312, "ACCOUNT", "SIGN OUT", selected == .sign_out);

drawMappingExplanation(renderer, store.face_buttons);
font.text(renderer, 18, 340, 2, "USE SWAPPED ONLY IF BUTTONS ARE REVERSED", style.muted());
font.text(renderer, 18, 366, 2, "FRAMERATE AND BITRATE APPLY ON NEXT STREAM", style.muted());
const prompts = [_]controls.Prompt{
controls.Prompt.one(controls.face(store.face_buttons, .a), "CHANGE"),
controls.Prompt.one(.dpad, "MOVE"),
Expand Down