aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/gsr-info.c
diff options
context:
space:
mode:
authorTrung Lê <8@tle.id.au>2026-02-21 23:40:05 +1100
committerTrung Lê <8@tle.id.au>2026-02-21 23:47:49 +1100
commit3eed1b64af72dcb4bcb8ae28d45856767d4acff1 (patch)
tree0aeef768169d69c4ed24c3cc03398c63aef3ff59 /src/gsr-info.c
Initial commit
Diffstat (limited to 'src/gsr-info.c')
-rw-r--r--src/gsr-info.c393
1 files changed, 393 insertions, 0 deletions
diff --git a/src/gsr-info.c b/src/gsr-info.c
new file mode 100644
index 0000000..04da95c
--- /dev/null
+++ b/src/gsr-info.c
@@ -0,0 +1,393 @@
+#include "gsr-info.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/wait.h>
+
+/* ── Helpers ─────────────────────────────────────────────────────── */
+
+static char *
+read_command_output(const char *cmd, int *exit_code)
+{
+ FILE *f = popen(cmd, "r");
+ if (!f) {
+ if (exit_code) *exit_code = -1;
+ return NULL;
+ }
+
+ size_t capacity = 8192;
+ size_t len = 0;
+ char *buf = g_malloc(capacity);
+
+ for (;;) {
+ size_t n = fread(buf + len, 1, capacity - len - 1, f);
+ if (n == 0) break;
+ len += n;
+ if (len + 1 >= capacity) {
+ capacity *= 2;
+ buf = g_realloc(buf, capacity);
+ }
+ }
+ buf[len] = '\0';
+
+ int status = pclose(f);
+ if (exit_code) {
+ if (WIFEXITED(status))
+ *exit_code = WEXITSTATUS(status);
+ else
+ *exit_code = -1;
+ }
+ return buf;
+}
+
+typedef void (*LineCallback)(const char *line, size_t len, void *user_data);
+
+static void
+for_each_line(const char *text, LineCallback cb, void *user_data)
+{
+ const char *p = text;
+ while (*p) {
+ const char *nl = strchr(p, '\n');
+ size_t len = nl ? (size_t)(nl - p) : strlen(p);
+ if (len > 0)
+ cb(p, len, user_data);
+ if (!nl) break;
+ p = nl + 1;
+ }
+}
+
+static bool
+str_eq(const char *s, size_t len, const char *literal)
+{
+ size_t llen = strlen(literal);
+ return len == llen && memcmp(s, literal, llen) == 0;
+}
+
+static bool
+str_starts_with(const char *s, size_t len, const char *prefix)
+{
+ size_t plen = strlen(prefix);
+ return len >= plen && memcmp(s, prefix, plen) == 0;
+}
+
+/* ── Section parsing ─────────────────────────────────────────────── */
+
+typedef enum {
+ SECTION_UNKNOWN,
+ SECTION_SYSTEM_INFO,
+ SECTION_GPU_INFO,
+ SECTION_VIDEO_CODECS,
+ SECTION_CAPTURE_OPTIONS,
+} InfoSection;
+
+typedef struct {
+ GsrInfo *info;
+ InfoSection section;
+ /* temporary growable monitor array */
+ GsrMonitor *monitors;
+ int n_monitors;
+ int monitors_capacity;
+} ParseState;
+
+static void
+parse_system_info(ParseState *st, const char *line, size_t len)
+{
+ const char *sep = memchr(line, '|', len);
+ if (!sep) return;
+
+ size_t klen = (size_t)(sep - line);
+ const char *val = sep + 1;
+ size_t vlen = len - klen - 1;
+
+ if (str_eq(line, klen, "display_server")) {
+ if (str_eq(val, vlen, "x11"))
+ st->info->system_info.display_server = GSR_DISPLAY_SERVER_X11;
+ else if (str_eq(val, vlen, "wayland"))
+ st->info->system_info.display_server = GSR_DISPLAY_SERVER_WAYLAND;
+ } else if (str_eq(line, klen, "is_steam_deck")) {
+ st->info->system_info.is_steam_deck = str_eq(val, vlen, "yes");
+ } else if (str_eq(line, klen, "supports_app_audio")) {
+ st->info->system_info.supports_app_audio = str_eq(val, vlen, "yes");
+ }
+}
+
+static void
+parse_gpu_info(ParseState *st, const char *line, size_t len)
+{
+ const char *sep = memchr(line, '|', len);
+ if (!sep) return;
+
+ size_t klen = (size_t)(sep - line);
+ const char *val = sep + 1;
+ size_t vlen = len - klen - 1;
+
+ if (str_eq(line, klen, "vendor")) {
+ if (str_eq(val, vlen, "amd"))
+ st->info->gpu_info.vendor = GSR_GPU_VENDOR_AMD;
+ else if (str_eq(val, vlen, "intel"))
+ st->info->gpu_info.vendor = GSR_GPU_VENDOR_INTEL;
+ else if (str_eq(val, vlen, "nvidia"))
+ st->info->gpu_info.vendor = GSR_GPU_VENDOR_NVIDIA;
+ else if (str_eq(val, vlen, "broadcom"))
+ st->info->gpu_info.vendor = GSR_GPU_VENDOR_BROADCOM;
+ }
+}
+
+static void
+parse_video_codecs(ParseState *st, const char *line, size_t len)
+{
+ GsrSupportedVideoCodecs *vc = &st->info->supported_video_codecs;
+
+ if (str_eq(line, len, "h264")) vc->h264 = true;
+ else if (str_eq(line, len, "h264_software")) vc->h264_software = true;
+ else if (str_eq(line, len, "hevc")) vc->hevc = true;
+ else if (str_eq(line, len, "hevc_hdr")) vc->hevc_hdr = true;
+ else if (str_eq(line, len, "hevc_10bit")) vc->hevc_10bit = true;
+ else if (str_eq(line, len, "av1")) vc->av1 = true;
+ else if (str_eq(line, len, "av1_hdr")) vc->av1_hdr = true;
+ else if (str_eq(line, len, "av1_10bit")) vc->av1_10bit = true;
+ else if (str_eq(line, len, "vp8")) vc->vp8 = true;
+ else if (str_eq(line, len, "vp9")) vc->vp9 = true;
+}
+
+static void
+parse_capture_options(ParseState *st, const char *line, size_t len)
+{
+ if (str_eq(line, len, "window")) {
+ st->info->supported_capture_options.window = true;
+ } else if (str_eq(line, len, "focused")) {
+ st->info->supported_capture_options.focused = true;
+ } else if (str_eq(line, len, "portal")) {
+ st->info->supported_capture_options.portal = true;
+ } else if (str_eq(line, len, "region") || (len > 0 && line[0] == '/')) {
+ /* skip "region" and DRM card paths */
+ } else {
+ /* monitor entry: name|WxH */
+ if (st->n_monitors >= st->monitors_capacity) {
+ st->monitors_capacity = st->monitors_capacity ? st->monitors_capacity * 2 : 8;
+ st->monitors = g_realloc_n(st->monitors, (size_t)st->monitors_capacity,
+ sizeof(GsrMonitor));
+ }
+ GsrMonitor *m = &st->monitors[st->n_monitors];
+ memset(m, 0, sizeof(*m));
+
+ const char *sep = memchr(line, '|', len);
+ if (sep) {
+ size_t name_len = (size_t)(sep - line);
+ m->name = g_strndup(line, name_len);
+ sscanf(sep + 1, "%dx%d", &m->width, &m->height);
+ } else {
+ m->name = g_strndup(line, len);
+ }
+ st->n_monitors++;
+ }
+}
+
+static void
+info_line_cb(const char *line, size_t len, void *user_data)
+{
+ ParseState *st = user_data;
+
+ if (str_starts_with(line, len, "section=")) {
+ const char *name = line + 8;
+ size_t nlen = len - 8;
+ if (str_eq(name, nlen, "system_info"))
+ st->section = SECTION_SYSTEM_INFO;
+ else if (str_eq(name, nlen, "gpu_info"))
+ st->section = SECTION_GPU_INFO;
+ else if (str_eq(name, nlen, "video_codecs"))
+ st->section = SECTION_VIDEO_CODECS;
+ else if (str_eq(name, nlen, "capture_options"))
+ st->section = SECTION_CAPTURE_OPTIONS;
+ else
+ st->section = SECTION_UNKNOWN;
+ return;
+ }
+
+ switch (st->section) {
+ case SECTION_SYSTEM_INFO: parse_system_info(st, line, len); break;
+ case SECTION_GPU_INFO: parse_gpu_info(st, line, len); break;
+ case SECTION_VIDEO_CODECS: parse_video_codecs(st, line, len); break;
+ case SECTION_CAPTURE_OPTIONS: parse_capture_options(st, line, len); break;
+ case SECTION_UNKNOWN: break;
+ }
+}
+
+/* ── Public API ──────────────────────────────────────────────────── */
+
+GsrInfoExitStatus
+gsr_info_load(GsrInfo *info)
+{
+ memset(info, 0, sizeof(*info));
+
+ int exit_code = -1;
+ char *output = read_command_output("gpu-screen-recorder --info", &exit_code);
+ if (!output) {
+ fprintf(stderr, "error: 'gpu-screen-recorder --info' failed to run\n");
+ return GSR_INFO_EXIT_FAILED_TO_RUN;
+ }
+
+ ParseState st = {
+ .info = info,
+ .section = SECTION_UNKNOWN,
+ };
+ for_each_line(output, info_line_cb, &st);
+ g_free(output);
+
+ /* Transfer monitors */
+ info->supported_capture_options.monitors = st.monitors;
+ info->supported_capture_options.n_monitors = st.n_monitors;
+
+ switch (exit_code) {
+ case 0: return GSR_INFO_EXIT_OK;
+ case 22: return GSR_INFO_EXIT_OPENGL_FAILED;
+ case 23: return GSR_INFO_EXIT_NO_DRM_CARD;
+ default: return GSR_INFO_EXIT_FAILED_TO_RUN;
+ }
+}
+
+void
+gsr_info_clear(GsrInfo *info)
+{
+ for (int i = 0; i < info->supported_capture_options.n_monitors; i++)
+ g_free(info->supported_capture_options.monitors[i].name);
+ g_free(info->supported_capture_options.monitors);
+ memset(info, 0, sizeof(*info));
+}
+
+bool
+gsr_info_is_codec_supported(const GsrInfo *info, const char *codec_id)
+{
+ if (strcmp(codec_id, "auto") == 0) return true;
+
+ const GsrSupportedVideoCodecs *vc = &info->supported_video_codecs;
+ if (strcmp(codec_id, "h264") == 0) return vc->h264;
+ if (strcmp(codec_id, "h264_software") == 0) return vc->h264_software;
+ if (strcmp(codec_id, "hevc") == 0) return vc->hevc;
+ if (strcmp(codec_id, "hevc_hdr") == 0) return vc->hevc_hdr;
+ if (strcmp(codec_id, "hevc_10bit") == 0) return vc->hevc_10bit;
+ if (strcmp(codec_id, "av1") == 0) return vc->av1;
+ if (strcmp(codec_id, "av1_hdr") == 0) return vc->av1_hdr;
+ if (strcmp(codec_id, "av1_10bit") == 0) return vc->av1_10bit;
+ if (strcmp(codec_id, "vp8") == 0) return vc->vp8;
+ if (strcmp(codec_id, "vp9") == 0) return vc->vp9;
+ return false;
+}
+
+bool
+gsr_info_is_capture_option_enabled(const GsrInfo *info, const char *option_id)
+{
+ if (info->system_info.display_server == GSR_DISPLAY_SERVER_WAYLAND) {
+ if (strcmp(option_id, "window") == 0 || strcmp(option_id, "focused") == 0)
+ return false;
+ }
+ if (strcmp(option_id, "portal") == 0)
+ return info->supported_capture_options.portal;
+ return true;
+}
+
+const char *
+gsr_info_get_first_usable_hw_video_codec(const GsrInfo *info)
+{
+ const GsrSupportedVideoCodecs *vc = &info->supported_video_codecs;
+ if (vc->h264) return "h264";
+ if (vc->hevc) return "hevc";
+ if (vc->av1) return "av1";
+ if (vc->vp8) return "vp8";
+ if (vc->vp9) return "vp9";
+ return NULL;
+}
+
+/* ── Audio device queries ────────────────────────────────────────── */
+
+GsrAudioDevice *
+gsr_audio_devices_get(int *n_devices)
+{
+ *n_devices = 0;
+
+ int exit_code = -1;
+ char *output = read_command_output("gpu-screen-recorder --list-audio-devices", &exit_code);
+ if (!output) return NULL;
+
+ /* Count lines */
+ int capacity = 16;
+ GsrAudioDevice *devs = g_new0(GsrAudioDevice, capacity);
+ int count = 0;
+
+ const char *p = output;
+ while (*p) {
+ const char *nl = strchr(p, '\n');
+ size_t len = nl ? (size_t)(nl - p) : strlen(p);
+ if (len > 0) {
+ const char *sep = memchr(p, '|', len);
+ if (sep) {
+ if (count >= capacity) {
+ capacity *= 2;
+ devs = g_realloc_n(devs, (size_t)capacity, sizeof(GsrAudioDevice));
+ }
+ devs[count].name = g_strndup(p, (size_t)(sep - p));
+ devs[count].description = g_strndup(sep + 1, len - (size_t)(sep - p) - 1);
+ count++;
+ }
+ }
+ if (!nl) break;
+ p = nl + 1;
+ }
+
+ g_free(output);
+ *n_devices = count;
+ return devs;
+}
+
+void
+gsr_audio_devices_free(GsrAudioDevice *devices, int n_devices)
+{
+ for (int i = 0; i < n_devices; i++) {
+ g_free(devices[i].name);
+ g_free(devices[i].description);
+ }
+ g_free(devices);
+}
+
+char **
+gsr_application_audio_get(int *n_apps)
+{
+ *n_apps = 0;
+
+ int exit_code = -1;
+ char *output = read_command_output("gpu-screen-recorder --list-application-audio", &exit_code);
+ if (!output) return NULL;
+
+ int capacity = 16;
+ char **apps = g_new0(char *, capacity + 1);
+ int count = 0;
+
+ const char *p = output;
+ while (*p) {
+ const char *nl = strchr(p, '\n');
+ size_t len = nl ? (size_t)(nl - p) : strlen(p);
+ if (len > 0) {
+ if (count >= capacity) {
+ capacity *= 2;
+ apps = g_realloc_n(apps, (size_t)(capacity + 1), sizeof(char *));
+ }
+ apps[count] = g_strndup(p, len);
+ count++;
+ }
+ if (!nl) break;
+ p = nl + 1;
+ }
+ apps[count] = NULL;
+
+ g_free(output);
+ *n_apps = count;
+ return apps;
+}
+
+void
+gsr_application_audio_free(char **apps, int n_apps)
+{
+ for (int i = 0; i < n_apps; i++)
+ g_free(apps[i]);
+ g_free(apps);
+}